diff --git a/.agents/skills/impeccable/SKILL.md b/.agents/skills/impeccable/SKILL.md index 8af38df3d..f107ec6ca 100644 --- a/.agents/skills/impeccable/SKILL.md +++ b/.agents/skills/impeccable/SKILL.md @@ -7,85 +7,84 @@ Designs and iterates production-grade frontend interfaces. Real working code, co ## Setup -Before any design work or file edits: +You MUST do these steps before proceeding: -1. Load context (PRODUCT.md / DESIGN.md) via the loader script. -2. Identify the register and load the matching register reference (brand.md or product.md). -3. **If the user invoked a sub-command (e.g. `craft`, `shape`, `audit`), load its reference file too.** This is non-negotiable: `craft` without `craft.md` loaded means you'll skip the shape-and-confirm step the user expects. +1. Run `node .agents/skills/impeccable/scripts/context.mjs` once per session. If you've already seen its output in this conversation, do not re-run it. The script either prints the project's PRODUCT.md (and DESIGN.md when present) as a markdown block, or tells you it's missing. Follow whatever it prints. **If it reports `NO_PRODUCT_MD`, stop and follow `reference/init.md` before doing anything else.** If the output ends with an `UPDATE_AVAILABLE` directive, follow it (ask the user once about updating, then continue). It never blocks the current task. +2. If the user invoked a sub-command (`craft`, `shape`, `audit`, `polish`, ...), you MUST read `reference/.md` next. Non-optional. The reference defines the command's flow; without it you will skip steps the user expects. +3. Familiarize yourself with any existing design system, conventions, and components in the code. Read at least one project file (CSS / tokens / theme / a representative component or page). **Required even when you've loaded a sub-command reference in step 2.** Don't reinvent the wheel; use what's there when it works, branch out when the UX wins. +4. Read the matching register reference. **This is non-optional; skipping it produces generic output.** If the project is marketing, a landing page, a campaign, long-form content, or a portfolio (design IS the product), read `reference/brand.md`. If it is app UI, admin, a dashboard, or a tool (design SERVES the product), read `reference/product.md`. Pick by first match: (1) task cue ("landing page" vs "dashboard"); (2) surface in focus (the page, file, or route being worked on); (3) `register` field in PRODUCT.md. +5. **If the project is brand-new (no existing CSS tokens / theme / committed brand colors found in step 3)**, run `node .agents/skills/impeccable/scripts/palette.mjs` to receive a brand seed color and composition guidance. This is the anchor for your primary brand color. Compose the rest of the palette (bg, surface, ink, accent, muted) around it per the script's instructions. Use OKLCH throughout. **Skip this step only if step 3 found committed brand colors in existing tokens; in that case identity-preservation wins.** -Skipping these produces generic output that ignores the project. +## Design guidance -### 1. Context gathering +Produce ready-to-ship, production-grade code, not prototypes or starting points. Take no shortcuts unless the user asks for them (when in doubt, ask). Don't stop until arriving at a complete implementation (beautiful, responsive, fast, precise, bug-free, on brand). You take attention to detail seriously: every page, section or component crafted is battle tested using the tools available to you (browser screenshotting, computer use, etc). GPT is capable of extraordinary work. Don't hold back. -Two files, case-insensitive. The loader looks at the project root by default and falls back to `.agents/context/` and `docs/` if the root is clean. Override with `IMPECCABLE_CONTEXT_DIR=path/to/dir` (absolute or relative to cwd). +### General rules -- **PRODUCT.md**: required. Users, brand, tone, anti-references, strategic principles. -- **DESIGN.md**: optional, strongly recommended. Colors, typography, elevation, components. +#### Color -Load both in one call: +- **Verify contrast.** Body text must hit ≥4.5:1 against its background; large text (≥18px or bold ≥14px) needs ≥3:1. Placeholder text needs the same 4.5:1, not the muted-gray default. The most common failure: muted gray body text on a tinted near-white. If the contrast is even close, bump the body color toward the ink end of the ramp; light gray "for elegance" is the single biggest reason AI designs feel hard to read. +- Gray text on a colored background looks washed out. Use a darker shade of the background's own hue, or a transparency of the text color. -```bash -node .agents/skills/impeccable/scripts/load-context.mjs -``` +#### Typography -Consume the full JSON output. Never pipe through `head`, `tail`, `grep`, or `jq`. The output's `contextDir` field tells you where the files were resolved from. +- Cap body line length at 65–75ch. +- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales. +- Cap font-family count at 3 (display + body + optional mono). More than 3 reads as indecision, not richness. One well-tuned family with weight contrast usually beats three competing typefaces. +- Don't pair fonts that are similar but not identical (two geometric sans-serifs, two humanist sans-serifs). Pair on a contrast axis (serif + sans, geometric + humanist) or use one family in multiple weights. +- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. +- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. +- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". +- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. -If the output is already in this session's conversation history, don't re-run. Exceptions requiring a fresh load: you just ran `$impeccable teach` or `$impeccable document` (they rewrite the files), or the user manually edited one. +Two hard typographic ceilings you currently miss: +- Hero clamp() max ≤ 6rem. 8–11rem (128–176px) reads as comically loud, not bold. +- Display letter-spacing ≥ -0.04em. Your default of -0.05 to -0.085em on display H1s makes the letters touch and reads as cramped. -0.02 to -0.03em is plenty for tight grotesque display; -0.04em is the floor. -`$impeccable live` already warms context via `live.mjs`. If you've run `live.mjs`, don't also run `load-context.mjs` this session. +#### Layout -If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `$impeccable teach`, then resume the user's original task with the fresh context. If the original task was `$impeccable craft`, resume into `$impeccable shape` before any implementation work. +- Vary spacing for rhythm. +- Cards are the lazy answer. Use them only when they're truly the best affordance. Nested cards are always wrong. +- Flexbox for 1D, Grid for 2D. Don't default to Grid when `flex-wrap` would be simpler. +- For responsive grids without breakpoints: `repeat(auto-fit, minmax(280px, 1fr))`. +- Build a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip). Never arbitrary values like 999 or 9999. -If DESIGN.md is missing: nudge once per session (*"Run `$impeccable document` for more on-brand output"*), then proceed. +#### Motion +- Motion should be intentional, and not be an afterthought. consider it as part of the build. +- Don't animate CSS layout properties unless truly needed. +- Ease out with exponential curves (ease-out-quart / quint / expo). No bounce, no elastic. +- Use libraries for more advanced motion needs (e.g. motion, gsap, anime.js, lenis etc) +- Reduced motion is not optional. Every animation needs a `@media (prefers-reduced-motion: reduce)` alternative: typically a crossfade or instant transition. +- Staggering the items within one list is legitimate. The tell is the uniform reflex (one identical entrance applied to every section), not motion itself; each reveal should fit what it reveals. Suppressing the reflex is never a reason to ship a page with no motion at all. +- Reveal animations must enhance an already-visible default. Don't gate content visibility on a class-triggered transition; transitions pause on hidden tabs and headless renderers, so the reveal never fires and the section ships blank. +- Premium motion materials are not just transform/opacity. Blur, backdrop-filter, clip-path, mask, and shadow/glow are part of the palette when they materially improve the effect and stay smooth. -### 2. Register +#### Interaction -Every design task is **brand** (marketing, landing, campaign, long-form content, portfolio: design IS the product) or **product** (app UI, admin, dashboard, tool: design SERVES the product). +- Dropdowns rendered with `position: absolute` inside an `overflow: hidden` or `overflow: auto` container will be clipped. Use the native `` / popover API, `position: fixed`, or a portal to escape the stacking context. -Identify before designing. Priority: (1) cue in the task itself ("landing page" vs "dashboard"); (2) the surface in focus (the page, file, or route being worked on); (3) `register` field in PRODUCT.md. First match wins. +### Copy -If PRODUCT.md lacks the `register` field (legacy), infer it once from its "Users" and "Product Purpose" sections, then cache the inferred value for the session. Suggest the user run `$impeccable teach` to add the field explicitly. +- Every word earns its place. No restated headings, no intros that repeat the title. +- **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`. +- **No aphoristic-cadence body copy as a default voice.** Don't fall into the rhythm of "serious statement, then punchy short negation" as the page's recurring voice. If three or more section copy blocks on the page land on a short rebuttal-shaped sentence, rewrite. Specific, not aphoristic. +- **No marketing buzzwords.** The streamline / empower / supercharge / leverage / unleash / transform / seamless / world-class / enterprise-grade / next-generation / cutting-edge / game-changer / mission-critical family of phrases. Pick a specific noun and a verb that describes what the product literally does. +- Button labels: verb + object. "Save changes" beats "OK"; "Delete project" beats "Yes". The label should say what will happen. +- Link text needs standalone meaning. "View pricing plans" beats "Click here"; screen readers announce links out of context. -Load the matching reference: [reference/brand.md](reference/brand.md) or [reference/product.md](reference/product.md). The shared design laws below apply to both. +### New projects only (when no prior work exists) -## Shared design laws +#### Color & Theme -Apply to every design, both registers. Match implementation complexity to the aesthetic vision: maximalism needs elaborate code, minimalism needs precision. Interpret creatively. Vary across projects; never converge on the same choices. GPT is capable of extraordinary work. Don't hold back. - -### Color - -- Use OKLCH. Reduce chroma as lightness approaches 0 or 100; high chroma at extremes looks garish. -- Never use `#000` or `#fff`. Tint every neutral toward the brand hue (chroma 0.005–0.01 is enough). +- Use OKLCH. +- **The cream / sand / beige body bg is the saturated AI default of 2026.** The whole warm-neutral band (OKLCH L 0.84-0.97, C < 0.06, hue 40-100) reads as cream/sand/paper/parchment regardless of what you call it. Token names like `--paper`, `--cream`, `--sand`, `--bone`, `--flour`, `--linen`, `--parchment`, `--wheat`, `--biscuit`, `--ivory` are tells in themselves. If the brief is "warm, traditional, family-coastal-Italian" or "magazine-warm" or "editorial-restraint", DO NOT translate that into a near-white warm-tinted bg; that's the AI move. Pick: (a) a saturated brand color as the body (terracotta, oxblood, deep ochre, near-black), (b) a true off-white at chroma 0 (or chroma toward the brand's own hue, not toward warmth-by-default), or (c) a darker mid-tone tinted neutral that's clearly the brand's own. "Warmth" in the brand is carried by accent + typography + imagery, not by body bg. +- Tinted neutrals: add 0.005–0.015 chroma toward the brand's hue. Don't default-tint toward warm or cool "because the brand feels that way"; that's the cross-project monoculture move. +- When picking a theme: Dark vs. light is never a default. Not dark "because tools look cool dark." Not light "to be safe.".Before choosing, write one sentence of physical scene: who uses this, where, under what ambient light, in what mood. If the sentence doesn't force the answer, it's not concrete enough. Add detail until it does. - Pick a **color strategy** before picking colors. Four steps on the commitment axis: - **Restrained**: tinted neutrals + one accent ≤10%. Product default; brand minimalism. - **Committed**: one saturated color carries 30–60% of the surface. Brand default for identity-driven pages. - **Full palette**: 3–4 named roles, each used deliberately. Brand campaigns; product data viz. - **Drenched**: the surface IS the color. Brand heroes, campaign pages. -- The "one accent ≤10%" rule is Restrained only. Committed / Full palette / Drenched exceed it on purpose. Don't collapse every design to Restrained by reflex. - -### Theme - -Dark vs. light is never a default. Not dark "because tools look cool dark." Not light "to be safe." - -Before choosing, write one sentence of physical scene: who uses this, where, under what ambient light, in what mood. If the sentence doesn't force the answer, it's not concrete enough. Add detail until it does. - -"Observability dashboard" does not force an answer. "SRE glancing at incident severity on a 27-inch monitor at 2am in a dim room" does. Run the sentence, not the category. - -### Typography - -- Cap body line length at 65–75ch. -- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales. - -### Layout - -- Vary spacing for rhythm. Same padding everywhere is monotony. -- Cards are the lazy answer. Use them only when they're truly the best affordance. Nested cards are always wrong. -- Don't wrap everything in a container. Most things don't need one. - -### Motion - -- Don't animate CSS layout properties. -- Ease out with exponential curves (ease-out-quart / quint / expo). No bounce, no elastic. ### Absolute bans @@ -96,12 +95,17 @@ Match-and-refuse. If you're about to write any of these, rewrite the element wit - **Glassmorphism as default.** Blurs and glass cards used decoratively. Rare and purposeful, or nothing. - **The hero-metric template.** Big number, small label, supporting stats, gradient accent. SaaS cliché. - **Identical card grids.** Same-sized cards with icon + heading + text, repeated endlessly. -- **Modal as first thought.** Modals are usually laziness. Exhaust inline / progressive alternatives first. +- **Tiny uppercase tracked eyebrow above every section.** The 2023-era kicker (small all-caps text with wide tracking, "ABOUT" "PROCESS" "PRICING" above each heading) is now the saturated AI scaffold; it appears on 55-95% of generations regardless of brief, which is the definition of a tell. One named kicker as a deliberate brand system is voice; an eyebrow on every section is AI grammar. Choose a different cadence. +- **Numbered section markers as default scaffolding (01 / 02 / 03).** Putting `01 · About / 02 · Process / 03 · Pricing` above every section is the eyebrow trope one tier deeper: reach for it because "landing pages do this" and you're scaffolding by reflex. Numbers earn their place when the section actually IS a sequence (a real 3-step process, an ordered flow, a typed timeline) and the order carries information the reader needs. One deliberate numbered sequence on one page is voice; numbered eyebrows on every section across the site is AI grammar. +- **Text that overflows its container.** Long heading words plus large clamp scales plus narrow grids cause headline overflow on tablet/mobile. Test the heading copy at every breakpoint; if it overflows, reduce the clamp max or rewrite the copy. The viewport is part of the design. -### Copy +**Codex-specific defects** (your most-frequent giveaways; refuse-and-rewrite): -- Every word earns its place. No restated headings, no intros that repeat the title. -- **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`. +- **`border: 1px solid X` + `box-shadow: 0 Npx Mpx ...` with M ≥ 16px** on the same element. The "ghost-card" pattern: 1px border plus soft wide drop shadow on buttons and cards. Don't pair them. Pick one (a single solid border at the brand color, OR a defined shadow at no more than 8px blur), never both as decoration. +- **`border-radius: 32px+` on cards / sections / inputs.** You over-round. Cards top out at 12–16px; full-pill is fine for tags/buttons. Picking 24/28/32/40px on a card is the codex tell; no brand wants "insanely rounded". +- **Hand-drawn / sketchy SVG illustrations.** Class names like `loose-sketch`, `*-sketch`, `doodle`, `wavy`; `feTurbulence` / `feDisplacementMap` "paper grain" filters; 5-to-30 path crude scenes meant to depict a tangible subject (an otter, a table-and-fork, an album cover). All of these read as amateurish, not whimsical. If you can't render the scene with real assets, ship no illustration. Don't attempt sketchy SVG as a fallback. +- **`repeating-linear-gradient(...)` stripe backgrounds.** Diagonal stripes in `body:before` or section backgrounds are pure codex decoration. Don't. +- **"X theater" / "actually X" / "not just X, it's Y" copy.** "Productivity theater", "engagement theater", "growth theater": instant AI slop. Choose a specific noun, not a meta-criticism phrase. ### The AI slop test @@ -109,7 +113,7 @@ If someone could look at this interface and say "AI made that" without doubt, it **Category-reflex check.** Run at two altitudes; the second one catches what the first one misses. -- **First-order:** if someone could guess the theme + palette from the category alone ("observability → dark blue", "healthcare → white + teal", "finance → navy + gold", "crypto → neon on black"), it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain. +- **First-order:** if someone could guess the theme + palette from the category alone, it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain. - **Second-order:** if someone could guess the aesthetic family from category-plus-anti-references ("AI workflow tool that's not SaaS-cream → editorial-typographic", "fintech that's not navy-and-gold → terminal-native dark mode"), it's the trap one tier deeper. The first reflex was avoided; the second wasn't. Rework until both answers are not obvious. The brand register's [reflex-reject aesthetic lanes](reference/brand.md) list catches the currently-saturated families. ## Commands @@ -118,7 +122,7 @@ If someone could look at this interface and say "AI made that" without doubt, it |---|---|---|---| | `craft [feature]` | Build | Shape, then build a feature end-to-end | [reference/craft.md](reference/craft.md) | | `shape [feature]` | Build | Plan UX/UI before writing code | [reference/shape.md](reference/shape.md) | -| `teach` | Build | Set up PRODUCT.md and DESIGN.md context | [reference/teach.md](reference/teach.md) | +| `init` | Build | Set up project context: PRODUCT.md, DESIGN.md, live config, next steps | [reference/init.md](reference/init.md) | | `document` | Build | Generate DESIGN.md from existing project code | [reference/document.md](reference/document.md) | | `extract [target]` | Build | Pull reusable tokens and components into design system | [reference/extract.md](reference/extract.md) | | `critique [target]` | Evaluate | UX design review with heuristic scoring | [reference/critique.md](reference/critique.md) | @@ -146,11 +150,14 @@ Plus two management commands: `pin ` and `unpin `, detailed be 1. **No argument**: render the table above as the user-facing command menu, grouped by category. Ask what they'd like to do. 2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. -3. **First word doesn't match**: general design invocation. Apply the setup steps, shared design laws, and the loaded register reference, using the full argument as context. +3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. +4. **No clear command match**: general design invocation. Apply the setup steps, the General rules, and the loaded register reference, using the full argument as context. Setup (context gathering, register) is already loaded by then; sub-commands don't re-invoke `$impeccable`. -If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `teach` as a blocker, finish teach, refresh context, then resume the original command and target. +If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `init` as a blocker, finish init, refresh context, then resume the original command and target. + +`teach` is a deprecated alias for `init`: if the user types it, load [reference/init.md](reference/init.md) and proceed as if they ran `init`. ## Pin / Unpin diff --git a/.agents/skills/impeccable/reference/adapt.md b/.agents/skills/impeccable/reference/adapt.md index 21bb0255a..9909edf13 100644 --- a/.agents/skills/impeccable/reference/adapt.md +++ b/.agents/skills/impeccable/reference/adapt.md @@ -188,3 +188,124 @@ Test thoroughly across contexts: - **Slow connections**: Test on throttled network When the adaptation feels native to each context, hand off to `$impeccable polish` for the final pass. + +--- + +## Reference Material + +The sections below were previously `responsive-design.md` and live inline now so the adapt flow has its deep responsive reference in one place. + +### Responsive Design + +#### Mobile-First: Write It Right + +Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first. + +#### Breakpoints: Content-Driven + +Don't chase device sizes; let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints. + +#### Detect Input Method, Not Just Screen Size + +**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard. Use pointer and hover queries: + +```css +/* Fine pointer (mouse, trackpad) */ +@media (pointer: fine) { + .button { padding: 8px 16px; } +} + +/* Coarse pointer (touch, stylus) */ +@media (pointer: coarse) { + .button { padding: 12px 20px; } /* Larger touch target */ +} + +/* Device supports hover */ +@media (hover: hover) { + .card:hover { transform: translateY(-2px); } +} + +/* Device doesn't support hover (touch) */ +@media (hover: none) { + .card { /* No hover state - use active instead */ } +} +``` + +**Critical**: Don't rely on hover for functionality. Touch users can't hover. + +#### Safe Areas: Handle the Notch + +Modern phones have notches, rounded corners, and home indicators. Use `env()`: + +```css +body { + padding-top: env(safe-area-inset-top); + padding-bottom: env(safe-area-inset-bottom); + padding-left: env(safe-area-inset-left); + padding-right: env(safe-area-inset-right); +} + +/* With fallback */ +.footer { + padding-bottom: max(1rem, env(safe-area-inset-bottom)); +} +``` + +**Enable viewport-fit** in your meta tag: +```html + +``` + +#### Responsive Images: Get It Right + +##### srcset with Width Descriptors + +```html +Hero image +``` + +**How it works**: +- `srcset` lists available images with their actual widths (`w` descriptors) +- `sizes` tells the browser how wide the image will display +- Browser picks the best file based on viewport width AND device pixel ratio + +##### Picture Element for Art Direction + +When you need different crops/compositions (not just resolutions): + +```html + + + + ... + +``` + +#### Layout Adaptation Patterns + +**Navigation**: Three stages: hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `
/` for content that can collapse on mobile. + +#### Testing: Don't Trust DevTools Alone + +DevTools device emulation is useful for layout but misses: + +- Actual touch interactions +- Real CPU/memory constraints +- Network latency patterns +- Font rendering differences +- Browser chrome/keyboard appearances + +**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators. + +--- + +**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful. diff --git a/.agents/skills/impeccable/reference/animate.md b/.agents/skills/impeccable/reference/animate.md index 48b5e268d..729271ad6 100644 --- a/.agents/skills/impeccable/reference/animate.md +++ b/.agents/skills/impeccable/reference/animate.md @@ -6,7 +6,7 @@ Add motion that conveys state, gives feedback, and clarifies hierarchy. Cut moti ## Register -Brand: orchestrated page-load sequences, staggered reveals, scroll-driven animation. Motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions. +Brand: motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions. The saturated AI default is fade-and-rise reveals on every scrolled section; that's a tell, not a choreography. Reserve scroll-triggered motion for moments that earn it. Product: 150–250 ms on most transitions. Motion conveys state: feedback, reveal, loading, transitions between views. No page-load choreography; users are in a task and won't wait for it. @@ -49,10 +49,11 @@ Create a purposeful animation plan: Add motion systematically across these categories: ### Entrance Animations -- **Page load choreography**: Stagger element reveals (100-150ms delays), fade + slide combinations - **Hero section**: Dramatic entrance for primary content (scale, parallax, or creative effects) -- **Content reveals**: Scroll-triggered animations using intersection observer - **Modal/drawer entry**: Smooth slide + fade, backdrop fade, focus management +- **List rhythm**: Sibling stagger is legitimate for cards-in-a-grid or list-items-appearing. Whole-section fade-on-scroll is not a list and is not legitimate. Cap total stagger time: 10 items at 50ms each = 500ms total. For more items, reduce per-item delay or cap the staggered count. + + Use CSS custom properties for clean stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"`, `style="--i: 1"`, etc. on each item. ### Micro-interactions - **Button feedback**: @@ -97,11 +98,14 @@ Use appropriate techniques for each animation: ### Timing & Easing -**Durations by purpose:** -- **100-150ms**: Instant feedback (button press, toggle) -- **200-300ms**: State changes (hover, menu open) -- **300-500ms**: Layout changes (accordion, modal) -- **500-800ms**: Entrance animations (page load) +**Duration: the 100/300/500 rule.** Timing matters more than easing for "feels right": + +| Duration | Use Case | Examples | +|----------|----------|----------| +| **100–150ms** | Instant feedback | Button press, toggle, color change | +| **200–300ms** | State changes | Menu open, tooltip, hover state | +| **300–500ms** | Layout changes | Accordion, modal, drawer | +| **500–800ms** | Entrance animations | Page load, hero reveal | **Easing curves (use these, not CSS defaults):** ```css @@ -134,13 +138,35 @@ Use appropriate techniques for each animation: - GSAP for complex sequences ``` +### Motion Materials + +Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties. Match material to effect: + +- **Transform / opacity**: movement, press feedback, simple reveals, list choreography +- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances +- **Clip-path / masks**: wipes, reveals, editorial cropping, product-like transitions +- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state +- **Grid-template-rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly + +The hard rule isn't "transform and opacity only." It's: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify smoothness in-browser on target viewports. + ### Performance -- **Motion materials**: Use transform/opacity for reliable movement, but use blur, filters, masks, shadows, and color shifts when they materially improve the effect - **Layout safety**: Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins) -- **will-change**: Add sparingly for known expensive animations +- **will-change**: Add sparingly for known expensive animations only (e.g. on `:hover` or an `.animating` class), never preemptively across the whole page +- **Scroll triggers**: Use Intersection Observer instead of scroll event listeners; unobserve after the animation fires once - **Bound expensive effects**: Keep blur/filter/shadow areas small or isolated, use `contain` where appropriate - **Monitor FPS**: Ensure 60fps on target devices +### Perceived Performance + +Nobody cares how fast your site *is*, only how fast it feels. The 80ms threshold: anything under ~80ms feels instant because our brains buffer sensory input for that long to synchronize perception. Target this for micro-interactions. + +- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening. +- **Early completion**: Show content progressively, don't wait for everything (progressive images, streaming HTML, skeleton fade-ins). +- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Use for low-stakes actions (likes, follows). Avoid for payments or destructive operations. +- **Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances. +- **Caution**: Too-fast responses can decrease perceived value for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening. + ### Accessibility ```css @media (prefers-reduced-motion: reduce) { diff --git a/.agents/skills/impeccable/reference/bolder.md b/.agents/skills/impeccable/reference/bolder.md index 25e94d597..bd96bd9a8 100644 --- a/.agents/skills/impeccable/reference/bolder.md +++ b/.agents/skills/impeccable/reference/bolder.md @@ -50,7 +50,7 @@ Create a strategy to increase impact while maintaining coherence: Systematically increase impact across these dimensions: ### Typography Amplification -- **Replace generic fonts**: Swap system fonts for distinctive choices (see the parent skill's typography guidelines and [typography.md](typography.md) for inspiration) +- **Replace generic fonts**: Swap system fonts for distinctive choices (see the parent skill's typography guidelines and the [Reference Material section of typeset.md](typeset.md#reference-material) for inspiration) - **Extreme scale**: Create dramatic size jumps (3x-5x differences, not 1.5x) - **Weight contrast**: Pair 900 weights with 200 weights, not 600 with 400 - **Unexpected choices**: Variable fonts, display fonts for headlines, condensed/extended widths, monospace as intentional accent (not as lazy "dev tool" default) @@ -78,10 +78,10 @@ Systematically increase impact across these dimensions: - **Custom elements**: Illustrative elements, custom icons, decorative details that reinforce brand ### Motion & Animation -- **Entrance choreography**: Staggered, dramatic page load animations with 50-100ms delays -- **Scroll effects**: Parallax, reveal animations, scroll-triggered sequences -- **Micro-interactions**: Satisfying hover effects, click feedback, state changes -- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic, which cheapen the effect) +- **Hero moment**: One signature entrance, once. Not on every visit and not on every section. +- **Micro-interactions**: Satisfying hover effects, click feedback, state changes. +- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic, which cheapen the effect). +- **Bolder ≠ scroll-fade-rise on every section.** That's the saturated AI default, the opposite of bold. ### Composition Boldness - **Hero moments**: Create clear focal points with dramatic treatment diff --git a/.agents/skills/impeccable/reference/brand.md b/.agents/skills/impeccable/reference/brand.md index 3d83a1cdc..194514e95 100644 --- a/.agents/skills/impeccable/reference/brand.md +++ b/.agents/skills/impeccable/reference/brand.md @@ -43,17 +43,10 @@ The reflex-reject lists apply to **new design choices**. When the existing brand ### Pairing and voice -Distinctive + refined is the goal. The specific shape depends on the brand: - -- **Editorial / long-form / luxury**: display serif + sans body (a magazine shape). -- **Tech / dev tools / fintech**: one committed sans, usually; custom-tight tracking, strong weight contrast inside a single family. -- **Consumer / food / travel**: warmer pairings, often a humanist sans plus a script or display serif. -- **Creative studios / agencies**: rule-breaking welcome. Mono-only, or display-only, or custom-drawn type as voice. +Distinctive + refined is the goal. The specific shape depends on the brand, not on the brand's category. A category ("restaurant", "dev tool", "magazine", "fintech") is not a recipe; treating it as one is the first-order reflex SKILL.md warns against. Two families minimum is the rule *only* when the voice needs it. A single well-chosen family with committed weight/size contrast is stronger than a timid display+body pair. -Vary across projects. If the last brief was a serif-display landing page, this one isn't. - ### Scale Modular scale, fluid `clamp()` for headings, ≥1.25 ratio between steps. Flat scales (1.1× apart) read as uncommitted. @@ -74,8 +67,7 @@ Brand surfaces have permission for Committed, Full palette, and Drenched strateg - Asymmetric compositions are one option. Break the grid intentionally for emphasis. - Fluid spacing with `clamp()` that breathes on larger viewports. Vary for rhythm: generous separations, tight groupings. -- Alternative: a strict, visible grid as the voice (brutalist / Swiss / tech-spec aesthetics). Either asymmetric or rigorously-gridded can be "designed"; the failure mode is splitting the difference into a generic centered stack. -- Don't default to centering everything. Left-aligned with asymmetric layouts feels more designed; a strict grid reads as confident structure. A centered-stack hero with icon-title-subtitle cards reads as template. +- For image-led briefs (hotels, restaurants, magazines, photography), full-bleed hero imagery with overlaid menu and centered headline is a canonical move; let the photograph be the design. - When cards ARE the right affordance, use `grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` for breakpoint-free responsiveness. ## Imagery @@ -93,8 +85,7 @@ Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landin ## Motion -- One well-orchestrated page-load with staggered reveals beats scattered micro-interactions, when the brand invites it. Tech-minimal brands often skip entrance motion entirely; the restraint is the voice. -- For collapsing/expanding sections, transition `grid-template-rows` rather than `height`. +- One well-orchestrated page-load beats scattered micro-interactions, when the brand invites it. Some brands skip entrance motion entirely; the restraint is the voice. ## Brand bans (on top of the shared absolute bans) @@ -111,8 +102,7 @@ Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landin Brand can afford things product can't. Take them. -- Ambitious first-load motion. Reveals, scroll-triggered transitions, typographic choreography. +- Ambitious first-load motion. Reveals and typographic choreography that earn their place; not fade-on-scroll for every section. - Single-purpose viewports. One dominant idea per fold, long scroll, deliberate pacing. -- Typographic risk. Enormous display type, unexpected italic cuts, mixed cases, hand-drawn headlines, a single oversize word as a hero. - Unexpected color strategies. Palette IS voice; a calm brand and a restless brand should not share palette mechanics. - Art direction per section. Different sections can have different visual worlds if the narrative demands it. Consistency of voice beats consistency of treatment. diff --git a/.agents/skills/impeccable/reference/clarify.md b/.agents/skills/impeccable/reference/clarify.md index 07b9d8d26..123f294d1 100644 --- a/.agents/skills/impeccable/reference/clarify.md +++ b/.agents/skills/impeccable/reference/clarify.md @@ -172,3 +172,117 @@ Test that copy improvements work: - **Tone**: Is it appropriate for the situation? When the copy reads cleanly, hand off to `$impeccable polish` for the final pass. + +--- + +## Reference Material + +The sections below were previously `ux-writing.md` and live inline now so the clarify flow has its deep UX-writing reference in one place. + +### UX Writing + +#### The Button Label Problem + +**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns: + +| Bad | Good | Why | +|-----|------|-----| +| OK | Save changes | Says what will happen | +| Submit | Create account | Outcome-focused | +| Yes | Delete message | Confirms the action | +| Cancel | Keep editing | Clarifies what "cancel" means | +| Click here | Download PDF | Describes the destination | + +**For destructive actions**, name the destruction: +- "Delete" not "Remove" (delete is permanent, remove implies recoverable) +- "Delete 5 items" not "Delete selected" (show the count) + +#### Error Messages: The Formula + +Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input". + +##### Error Message Templates + +| Situation | Template | +|-----------|----------| +| **Format error** | "[Field] needs to be [format]. Example: [example]" | +| **Missing required** | "Please enter [what's missing]" | +| **Permission denied** | "You don't have access to [thing]. [What to do instead]" | +| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." | +| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" | + +##### Don't Blame the User + +Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date". + +#### Empty States Are Opportunities + +Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items". + +#### Voice vs Tone + +**Voice** is your brand's personality, consistent everywhere. +**Tone** adapts to the moment. + +| Moment | Tone Shift | +|--------|------------| +| Success | Celebratory, brief: "Done! Your changes are live." | +| Error | Empathetic, helpful: "That didn't work. Here's what to try..." | +| Loading | Reassuring: "Saving your work..." | +| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | + +**Never use humor for errors.** Users are already frustrated. Be helpful, not cute. + +#### Writing for Accessibility + +**Link text** must have standalone meaning: "View pricing plans" not "Click here". **Alt text** describes information, not the image: "Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context. + +#### Writing for Translation + +##### Plan for Expansion + +German text is ~30% longer than English. Allocate space: + +| Language | Expansion | +|----------|-----------| +| German | +30% | +| French | +20% | +| Finnish | +30-40% | +| Chinese | -30% (fewer chars, but same width) | + +##### Translation-Friendly Patterns + +Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear. + +#### Consistency: The Terminology Problem + +Pick one term and stick with it: + +| Inconsistent | Consistent | +|--------------|------------| +| Delete / Remove / Trash | Delete | +| Settings / Preferences / Options | Settings | +| Sign in / Log in / Enter | Sign in | +| Create / Add / New | Create | + +Build a terminology glossary and enforce it. Variety creates confusion. + +#### Avoid Redundant Copy + +If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well. + +#### Loading States + +Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress. + +#### Confirmation Dialogs: Use Sparingly + +Most confirmation dialogs are design failures; consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No"). + +#### Form Instructions + +Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking. + +--- + +**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors. diff --git a/.agents/skills/impeccable/reference/codex.md b/.agents/skills/impeccable/reference/codex.md index 0b35f3e34..f174e1f3c 100644 --- a/.agents/skills/impeccable/reference/codex.md +++ b/.agents/skills/impeccable/reference/codex.md @@ -23,8 +23,8 @@ Before generating anything, run a brief direction conversation grounded in the s Ask **2-3 targeted questions** about visual lane, color strategy, atmosphere, and named anchor references. Don't enumerate generic menus; tie each question to the shape brief's answers. Example shape-grounded questions: -- "Brief says 'editorial restraint, Klim-adjacent.' Are we closer to a quiet specimen page or a magazine-spread feel with hero imagery?" -- "Palette strategy from shape was 'Committed.' Want it warm-grounded (deep oxblood + cream) or cool-grounded (slate + paper white)?" +- "Brief says 'specimen-page restraint.' Are we closer to a quiet typographic page or a wider editorial spread with hero imagery?" +- "Palette strategy from shape was 'Committed.' Which one color carries the surface (a brand-driven pick rather than a default warm-or-cool framing)? (And no, the answer isn't a cream/sand body bg; that's the saturated AI default.)" **STOP and wait for answers.** These pin the palette before any pixel gets generated. Do not proceed to Step B until the user has responded. diff --git a/.agents/skills/impeccable/reference/cognitive-load.md b/.agents/skills/impeccable/reference/cognitive-load.md deleted file mode 100644 index 48f8ad58b..000000000 --- a/.agents/skills/impeccable/reference/cognitive-load.md +++ /dev/null @@ -1,106 +0,0 @@ -# Cognitive Load Assessment - -Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload. - ---- - -## Three Types of Cognitive Load - -### Intrinsic Load: The Task Itself -Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it. - -**Manage it by**: -- Breaking complex tasks into discrete steps -- Providing scaffolding (templates, defaults, examples) -- Progressive disclosure: show what's needed now, hide the rest -- Grouping related decisions together - -### Extraneous Load: Bad Design -Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste. - -**Common sources**: -- Confusing navigation that requires mental mapping -- Unclear labels that force users to guess meaning -- Visual clutter competing for attention -- Inconsistent patterns that prevent learning -- Unnecessary steps between user intent and result - -### Germane Load: Learning Effort -Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery. - -**Support it by**: -- Progressive disclosure that reveals complexity gradually -- Consistent patterns that reward learning -- Feedback that confirms correct understanding -- Onboarding that teaches through action, not walls of text - ---- - -## Cognitive Load Checklist - -Evaluate the interface against these 8 items: - -- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements? -- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)? -- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)? -- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen? -- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next? -- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)? -- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one? -- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it? - -**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed). - ---- - -## The Working Memory Rule - -**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001). - -At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider: -- **≤4 items**: Within working memory limits, manageable -- **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure -- **8+ items**: Overloaded; users will skip, misclick, or abandon - -**Practical applications**: -- Navigation menus: ≤5 top-level items (group the rest under clear categories) -- Form sections: ≤4 fields visible per group before a visual break -- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu -- Dashboard widgets: ≤4 key metrics visible without scrolling -- Pricing tiers: ≤3 options (more causes analysis paralysis) - ---- - -## Common Cognitive Load Violations - -### 1. The Wall of Options -**Problem**: Presenting 10+ choices at once with no hierarchy. -**Fix**: Group into categories, highlight recommended, use progressive disclosure. - -### 2. The Memory Bridge -**Problem**: User must remember info from step 1 to complete step 3. -**Fix**: Keep relevant context visible, or repeat it where it's needed. - -### 3. The Hidden Navigation -**Problem**: User must build a mental map of where things are. -**Fix**: Always show current location (breadcrumbs, active states, progress indicators). - -### 4. The Jargon Barrier -**Problem**: Technical or domain language forces translation effort. -**Fix**: Use plain language. If domain terms are unavoidable, define them inline. - -### 5. The Visual Noise Floor -**Problem**: Every element has the same visual weight; nothing stands out. -**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted. - -### 6. The Inconsistent Pattern -**Problem**: Similar actions work differently in different places. -**Fix**: Standardize interaction patterns. Same type of action = same type of UI. - -### 7. The Multi-Task Demand -**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating). -**Fix**: Sequence the steps. Let the user do one thing at a time. - -### 8. The Context Switch -**Problem**: User must jump between screens/tabs/modals to gather info for a single decision. -**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth. diff --git a/.agents/skills/impeccable/reference/color-and-contrast.md b/.agents/skills/impeccable/reference/color-and-contrast.md deleted file mode 100644 index 110c2ee47..000000000 --- a/.agents/skills/impeccable/reference/color-and-contrast.md +++ /dev/null @@ -1,105 +0,0 @@ -# Color & Contrast - -## Color Spaces: Use OKLCH - -**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal, unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark. - -The OKLCH function takes three components: `oklch(lightness chroma hue)` where lightness is 0-100%, chroma is roughly 0-0.4, and hue is 0-360. To build a primary color and its lighter / darker variants, hold the chroma+hue roughly constant and vary the lightness, but **reduce chroma as you approach white or black**, because high chroma at extreme lightness looks garish. - -The hue you pick is a brand decision and should not come from a default. Do not reach for blue (hue 250) or warm orange (hue 60) by reflex; those are the dominant AI-design defaults, not the right answer for any specific brand. - -## Building Functional Palettes - -### Tinted Neutrals - -**Pure gray is dead.** A neutral with zero chroma feels lifeless next to a colored brand. Add a tiny chroma value (0.005-0.015) to all your neutrals, hued toward whatever your brand color is. The chroma is small enough not to read as "tinted" consciously, but it creates subconscious cohesion between brand color and UI surfaces. - -The hue you tint toward should come from THIS project's brand, not from a "warm = friendly, cool = tech" formula. If your brand color is teal, your neutrals lean toward teal. If your brand color is amber, they lean toward amber. The point is cohesion with the SPECIFIC brand, not a stock palette. - -**Avoid** the trap of always tinting toward warm orange or always tinting toward cool blue. Those are the two laziest defaults and they create their own monoculture across projects. - -### Palette Structure - -A complete system needs: - -| Role | Purpose | Example | -|------|---------|---------| -| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades | -| **Neutral** | Text, backgrounds, borders | 9-11 shade scale | -| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each | -| **Surface** | Cards, modals, overlays | 2-3 elevation levels | - -**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise. - -### The 60-30-10 Rule (Applied Correctly) - -This rule is about **visual weight**, not pixel count: - -- **60%**: Neutral backgrounds, white space, base surfaces -- **30%**: Secondary colors: text, borders, inactive states -- **10%**: Accent: CTAs, highlights, focus states - -The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power. - -## Contrast & Accessibility - -### WCAG Requirements - -| Content Type | AA Minimum | AAA Target | -|--------------|------------|------------| -| Body text | 4.5:1 | 7:1 | -| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | -| UI components, icons | 3:1 | 4.5:1 | -| Non-essential decorations | None | None | - -**The gotcha**: Placeholder text still needs 4.5:1. That light gray placeholder you see everywhere? Usually fails WCAG. - -### Dangerous Color Combinations - -These commonly fail contrast or cause readability issues: - -- Light gray text on white (the #1 accessibility fail) -- **Gray text on any colored background**: gray looks washed out and dead on color. Use a darker shade of the background color, or transparency -- Red text on green background (or vice versa): 8% of men can't distinguish these -- Blue text on red background (vibrates visually) -- Yellow text on white (almost always fails) -- Thin light text on images (unpredictable contrast) - -### Never Use Pure Gray or Pure Black - -Pure gray (`oklch(50% 0 0)`) and pure black (`#000`) don't exist in nature; real shadows and surfaces always have a color cast. Even a chroma of 0.005-0.01 is enough to feel natural without being obviously tinted. (See tinted neutrals example above.) - -### Testing - -Don't trust your eyes. Use tools: - -- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) -- Browser DevTools → Rendering → Emulate vision deficiencies -- [Polypane](https://polypane.app/) for real-time testing - -## Theming: Light & Dark Mode - -### Dark Mode Is Not Inverted Light Mode - -You can't just swap colors. Dark mode requires different design decisions: - -| Light Mode | Dark Mode | -|------------|-----------| -| Shadows for depth | Lighter surfaces for depth (no shadows) | -| Dark text on light | Light text on dark (reduce font weight) | -| Vibrant accents | Desaturate accents slightly | -| White backgrounds | Never pure black; use dark gray (oklch 12-18%) | - -In dark mode, depth comes from surface lightness, not shadow. Build a 3-step surface scale where higher elevations are lighter (e.g. 15% / 20% / 25% lightness). Use the SAME hue and chroma as your brand color (whatever it is for THIS project; do not reach for blue) and only vary the lightness. Reduce body text weight slightly (e.g. 350 instead of 400) because light text on dark reads as heavier than dark text on light. - -### Token Hierarchy - -Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer; primitives stay the same. - -## Alpha Is A Design Smell - -Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed. - ---- - -**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Using pure black (#000) for large areas. Skipping color blindness testing (8% of men affected). diff --git a/.agents/skills/impeccable/reference/colorize.md b/.agents/skills/impeccable/reference/colorize.md index a6ddfb292..08a3862d4 100644 --- a/.agents/skills/impeccable/reference/colorize.md +++ b/.agents/skills/impeccable/reference/colorize.md @@ -68,10 +68,10 @@ Add color systematically across these dimensions: - **Hover states**: Introduce color on interaction ### Background & Surfaces -- **Tinted backgrounds**: Replace pure gray (`#f5f5f5`) with warm neutrals (`oklch(97% 0.01 60)`) or cool tints (`oklch(97% 0.01 250)`) +- **Tinted backgrounds**: If you replace pure gray, tint toward the brand hue, not toward a generic-warm-or-cool pair. The default-warm-tint (`oklch(97% 0.01 60)` and its neighbors) is now the AI cream/sand giveaway. Be specific to the brand or stay neutral. - **Colored sections**: Use subtle background colors to separate areas - **Gradient backgrounds**: Add depth with subtle, intentional gradients (not generic purple-blue) -- **Cards & surfaces**: Tint cards or surfaces slightly for warmth +- **Cards & surfaces**: Tint cards or surfaces toward the brand, not "for warmth" by reflex **Use OKLCH for color**: It's perceptually uniform, meaning equal steps in lightness *look* equal. Great for generating harmonious scales. @@ -124,8 +124,6 @@ Ensure color addition improves rather than overwhelms: - Use every color in the rainbow (choose 2-4 colors beyond neutrals) - Apply color randomly without semantic meaning - Put gray text on colored backgrounds. It looks washed out; use a darker shade of the background color or transparency instead -- Use pure gray for neutrals. Add subtle color tint (warm or cool) for depth -- Use pure black (`#000`) or pure white (`#fff`) for large areas - Violate WCAG contrast requirements - Use color as the only indicator (accessibility issue) - Make everything colorful (defeats the purpose) @@ -152,3 +150,108 @@ When invoked from live mode, each variant MUST declare a `color-amount` param so ``` Layer 1-2 variant-specific params on top: palette selection (`steps` with named options), temperature warmth, or tint vs. true color. See `reference/live.md` for the full params contract. + +--- + +## Reference Material + +The sections below were previously `color-and-contrast.md` and live inline now so the colorize flow has its deep color reference in one place. + +### Color & Contrast + +#### Color Spaces: Use OKLCH + +**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal, unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark. + +The OKLCH function takes three components: `oklch(lightness chroma hue)` where lightness is 0-100%, chroma is roughly 0-0.4, and hue is 0-360. To build a primary color and its lighter / darker variants, hold the chroma+hue roughly constant and vary the lightness, but **reduce chroma as you approach white or black**, because high chroma at extreme lightness looks garish. + +The hue you pick is a brand decision and should not come from a default. Do not reach for blue (hue 250) or warm orange (hue 60) by reflex; those are the dominant AI-design defaults, not the right answer for any specific brand. + +#### Building Functional Palettes + +##### Tinted Neutrals + +**Pure gray is dead.** A neutral with zero chroma feels lifeless next to a colored brand. Add a tiny chroma value (0.005-0.015) to all your neutrals, hued toward whatever your brand color is. The chroma is small enough not to read as "tinted" consciously, but it creates subconscious cohesion between brand color and UI surfaces. + +The hue you tint toward should come from THIS project's brand, not from a "warm = friendly, cool = tech" formula. If your brand color is teal, your neutrals lean toward teal. If your brand color is amber, they lean toward amber. The point is cohesion with the SPECIFIC brand, not a stock palette. + +**Avoid** the trap of always tinting toward warm orange or always tinting toward cool blue. Those are the two laziest defaults and they create their own monoculture across projects. + +##### Palette Structure + +A complete system needs: + +| Role | Purpose | Example | +|------|---------|---------| +| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades | +| **Neutral** | Text, backgrounds, borders | 9-11 shade scale | +| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each | +| **Surface** | Cards, modals, overlays | 2-3 elevation levels | + +**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise. + +##### The 60-30-10 Rule (Applied Correctly) + +This rule is about **visual weight**, not pixel count: + +- **60%**: Neutral backgrounds, white space, base surfaces +- **30%**: Secondary colors: text, borders, inactive states +- **10%**: Accent: CTAs, highlights, focus states + +The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power. + +#### Contrast & Accessibility + +##### WCAG Requirements + +| Content Type | AA Minimum | AAA Target | +|--------------|------------|------------| +| Body text | 4.5:1 | 7:1 | +| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | +| UI components, icons | 3:1 | 4.5:1 | +| Non-essential decorations | None | None | + +##### Dangerous Color Combinations + +These commonly fail contrast or cause readability issues: + +- Light gray text on white (the #1 accessibility fail) +- Red text on green background (or vice versa): 8% of men can't distinguish these +- Blue text on red background (vibrates visually) +- Yellow text on white (almost always fails) +- Thin light text on images (unpredictable contrast) + +##### Testing + +Don't trust your eyes. Use tools: + +- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) +- Browser DevTools → Rendering → Emulate vision deficiencies +- [Polypane](https://polypane.app/) for real-time testing + +#### Theming: Light & Dark Mode + +##### Dark Mode Is Not Inverted Light Mode + +You can't just swap colors. Dark mode requires different design decisions: + +| Light Mode | Dark Mode | +|------------|-----------| +| Shadows for depth | Lighter surfaces for depth (no shadows) | +| Dark text on light | Light text on dark (reduce font weight) | +| Vibrant accents | Desaturate accents slightly | +| White backgrounds | Either pure black or a deep surface that fits the brand (a brand-tinted near-black at oklch 12-18% works too) | + +In dark mode, depth comes from surface lightness, not shadow. Build a 3-step surface scale where higher elevations are lighter (e.g. 15% / 20% / 25% lightness). Use the SAME hue and chroma as your brand color (whatever it is for THIS project; do not reach for blue) and only vary the lightness. Reduce body text weight slightly (e.g. 350 instead of 400) because light text on dark reads as heavier than dark text on light. + +##### Token Hierarchy + +Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer; primitives stay the same. + +#### Alpha Is A Design Smell + +Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed. + +--- + +**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Skipping color blindness testing (8% of men affected). diff --git a/.agents/skills/impeccable/reference/craft.md b/.agents/skills/impeccable/reference/craft.md index 831b08ab9..4adbc2b1f 100644 --- a/.agents/skills/impeccable/reference/craft.md +++ b/.agents/skills/impeccable/reference/craft.md @@ -56,15 +56,15 @@ If the harness has native image generation (Codex), a compact shape's "confirm o Based on the design brief's "Recommended References" section, consult the relevant impeccable reference files. At minimum, always consult: -- [spatial-design.md](spatial-design.md) for layout and spacing -- [typography.md](typography.md) for type hierarchy +- [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 [motion-design.md](motion-design.md) -- Color-heavy or themed? Consult [color-and-contrast.md](color-and-contrast.md) -- Responsive requirements? Consult [responsive-design.md](responsive-design.md) -- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.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) diff --git a/.agents/skills/impeccable/reference/critique.md b/.agents/skills/impeccable/reference/critique.md index 4e84fcb55..d039627f5 100644 --- a/.agents/skills/impeccable/reference/critique.md +++ b/.agents/skills/impeccable/reference/critique.md @@ -46,9 +46,9 @@ Read relevant source files and visually inspect the live page when browser autom Evaluate: - **AI slop**: Would someone believe "AI made this" immediately? Check all DON'T guidance from the parent Impeccable skill. - **Holistic design**: hierarchy, IA, emotional fit, discoverability, composition, typography, color, accessibility, states, copy, and edge cases. -- **Cognitive load**: consult [cognitive-load](cognitive-load.md); report checklist failures and decision points with >4 visible options. +- **Cognitive load**: consult the [Cognitive Load Assessment](#cognitive-load-assessment) section below; report checklist failures and decision points with >4 visible options. - **Emotional journey**: peak-end rule, emotional valleys, reassurance at high-stakes moments. -- **Nielsen heuristics**: consult [heuristics-scoring](heuristics-scoring.md); score all 10 heuristics 0-4. +- **Nielsen heuristics**: consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below; score all 10 heuristics 0-4. Return: AI slop verdict, heuristic scores, cognitive load, emotional journey, 2-3 strengths, 3-5 priority issues, persona red flags, minor observations, and provocative questions. @@ -94,7 +94,7 @@ Codex final-answer note: `$impeccable critique` produces a report artifact, so t Structure your feedback as a design director would: #### Design Health Score -> *Consult [heuristics-scoring](heuristics-scoring.md)* +> *Consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below.* Present the Nielsen's 10 heuristics scores as a table: @@ -133,16 +133,16 @@ Highlight 2-3 things done well. Be specific about why they work. #### Priority Issues The 3-5 most impactful design problems, ordered by importance. -For each issue, tag with **P0-P3 severity** (consult [heuristics-scoring](heuristics-scoring.md) for severity definitions): +For each issue, tag with **P0-P3 severity** (see [Issue Severity below](#issue-severity-p0p3) for definitions): - **[P?] What**: Name the problem clearly - **Why it matters**: How this hurts users or undermines goals - **Fix**: What to do about it (be concrete) - **Suggested command**: Which command could address this (from: $impeccable adapt, $impeccable animate, $impeccable audit, $impeccable bolder, $impeccable clarify, $impeccable colorize, $impeccable critique, $impeccable delight, $impeccable distill, $impeccable document, $impeccable harden, $impeccable layout, $impeccable onboard, $impeccable optimize, $impeccable overdrive, $impeccable polish, $impeccable quieter, $impeccable shape, $impeccable typeset) #### Persona Red Flags -> *Consult [personas](personas.md)* +> *Consult the [Personas reference](#persona-based-design-testing) below.* -Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `AGENTS.md` contains a `## Design Context` section from `impeccable teach`, also generate 1-2 project-specific personas from the audience/brand info. +Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `AGENTS.md` contains a `## Design Context` section from `impeccable init`, also generate 1-2 project-specific personas from the audience/brand info. For each selected persona, walk through the primary user action and list specific red flags found: @@ -257,3 +257,534 @@ After presenting the summary, tell the user: > You can ask me to run these one at a time, all at once, or in any order you prefer. > > Re-run `$impeccable critique` after fixes to see your score improve. + +--- + +## Reference Material + +The sections below were previously separate reference files (`cognitive-load.md`, `heuristics-scoring.md`, `personas.md`). They live inline now so the critique flow has all its deep context in one place. + +### Cognitive Load Assessment + +Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload. + +--- + +#### Three Types of Cognitive Load + +##### Intrinsic Load: The Task Itself +Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it. + +**Manage it by**: +- Breaking complex tasks into discrete steps +- Providing scaffolding (templates, defaults, examples) +- Progressive disclosure: show what's needed now, hide the rest +- Grouping related decisions together + +##### Extraneous Load: Bad Design +Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste. + +**Common sources**: +- Confusing navigation that requires mental mapping +- Unclear labels that force users to guess meaning +- Visual clutter competing for attention +- Inconsistent patterns that prevent learning +- Unnecessary steps between user intent and result + +##### Germane Load: Learning Effort +Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery. + +**Support it by**: +- Progressive disclosure that reveals complexity gradually +- Consistent patterns that reward learning +- Feedback that confirms correct understanding +- Onboarding that teaches through action, not walls of text + +--- + +#### Cognitive Load Checklist + +Evaluate the interface against these 8 items: + +- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements? +- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)? +- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)? +- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen? +- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next? +- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)? +- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one? +- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it? + +**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed). + +--- + +#### The Working Memory Rule + +**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001). + +At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider: +- **≤4 items**: Within working memory limits, manageable +- **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure +- **8+ items**: Overloaded; users will skip, misclick, or abandon + +**Practical applications**: +- Navigation menus: ≤5 top-level items (group the rest under clear categories) +- Form sections: ≤4 fields visible per group before a visual break +- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu +- Dashboard widgets: ≤4 key metrics visible without scrolling +- Pricing tiers: ≤3 options (more causes analysis paralysis) + +--- + +#### Common Cognitive Load Violations + +##### 1. The Wall of Options +**Problem**: Presenting 10+ choices at once with no hierarchy. +**Fix**: Group into categories, highlight recommended, use progressive disclosure. + +##### 2. The Memory Bridge +**Problem**: User must remember info from step 1 to complete step 3. +**Fix**: Keep relevant context visible, or repeat it where it's needed. + +##### 3. The Hidden Navigation +**Problem**: User must build a mental map of where things are. +**Fix**: Always show current location (breadcrumbs, active states, progress indicators). + +##### 4. The Jargon Barrier +**Problem**: Technical or domain language forces translation effort. +**Fix**: Use plain language. If domain terms are unavoidable, define them inline. + +##### 5. The Visual Noise Floor +**Problem**: Every element has the same visual weight; nothing stands out. +**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted. + +##### 6. The Inconsistent Pattern +**Problem**: Similar actions work differently in different places. +**Fix**: Standardize interaction patterns. Same type of action = same type of UI. + +##### 7. The Multi-Task Demand +**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating). +**Fix**: Sequence the steps. Let the user do one thing at a time. + +##### 8. The Context Switch +**Problem**: User must jump between screens/tabs/modals to gather info for a single decision. +**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth. + +--- + +### Heuristics Scoring Guide + +Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough." + +#### Nielsen's 10 Heuristics + +##### 1. Visibility of System Status + +Keep users informed about what's happening through timely, appropriate feedback. + +**Check for**: +- Loading indicators during async operations +- Confirmation of user actions (save, submit, delete) +- Progress indicators for multi-step processes +- Current location in navigation (breadcrumbs, active states) +- Form validation feedback (inline, not just on submit) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | No feedback; user is guessing what happened | +| 1 | Rare feedback; most actions produce no visible response | +| 2 | Partial; some states communicated, major gaps remain | +| 3 | Good; most operations give clear feedback, minor gaps | +| 4 | Excellent; every action confirms, progress is always visible | + +##### 2. Match Between System and Real World + +Speak the user's language. Follow real-world conventions. Information appears in natural, logical order. + +**Check for**: +- Familiar terminology (no unexplained jargon) +- Logical information order matching user expectations +- Recognizable icons and metaphors +- Domain-appropriate language for the target audience +- Natural reading flow (left-to-right, top-to-bottom priority) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Pure tech jargon, alien to users | +| 1 | Mostly confusing; requires domain expertise to navigate | +| 2 | Mixed; some plain language, some jargon leaks through | +| 3 | Mostly natural; occasional term needs context | +| 4 | Speaks the user's language fluently throughout | + +##### 3. User Control and Freedom + +Users need a clear "emergency exit" from unwanted states without extended dialogue. + +**Check for**: +- Undo/redo functionality +- Cancel buttons on forms and modals +- Clear navigation back to safety (home, previous) +- Easy way to clear filters, search, selections +- Escape from long or multi-step processes + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Users get trapped; no way out without refreshing | +| 1 | Difficult exits; must find obscure paths to escape | +| 2 | Some exits; main flows have escape, edge cases don't | +| 3 | Good control; users can exit and undo most actions | +| 4 | Full control; undo, cancel, back, and escape everywhere | + +##### 4. Consistency and Standards + +Users shouldn't wonder whether different words, situations, or actions mean the same thing. + +**Check for**: +- Consistent terminology throughout the interface +- Same actions produce same results everywhere +- Platform conventions followed (standard UI patterns) +- Visual consistency (colors, typography, spacing, components) +- Consistent interaction patterns (same gesture = same behavior) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Inconsistent everywhere; feels like different products stitched together | +| 1 | Many inconsistencies; similar things look/behave differently | +| 2 | Partially consistent; main flows match, details diverge | +| 3 | Mostly consistent; occasional deviation, nothing confusing | +| 4 | Fully consistent; cohesive system, predictable behavior | + +##### 5. Error Prevention + +Better than good error messages is a design that prevents problems in the first place. + +**Check for**: +- Confirmation before destructive actions (delete, overwrite) +- Constraints preventing invalid input (date pickers, dropdowns) +- Smart defaults that reduce errors +- Clear labels that prevent misunderstanding +- Autosave and draft recovery + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Errors easy to make; no guardrails anywhere | +| 1 | Few safeguards; some inputs validated, most aren't | +| 2 | Partial prevention; common errors caught, edge cases slip | +| 3 | Good prevention; most error paths blocked proactively | +| 4 | Excellent; errors nearly impossible through smart constraints | + +##### 6. Recognition Rather Than Recall + +Minimize memory load. Make objects, actions, and options visible or easily retrievable. + +**Check for**: +- Visible options (not buried in hidden menus) +- Contextual help when needed (tooltips, inline hints) +- Recent items and history +- Autocomplete and suggestions +- Labels on icons (not icon-only navigation) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Heavy memorization; users must remember paths and commands | +| 1 | Mostly recall; many hidden features, few visible cues | +| 2 | Some aids; main actions visible, secondary features hidden | +| 3 | Good recognition; most things discoverable, few memory demands | +| 4 | Everything discoverable; users never need to memorize | + +##### 7. Flexibility and Efficiency of Use + +Accelerators, invisible to novices, speed up expert interaction. + +**Check for**: +- Keyboard shortcuts for common actions +- Customizable interface elements +- Recent items and favorites +- Bulk/batch actions +- Power user features that don't complicate the basics + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | One rigid path; no shortcuts or alternatives | +| 1 | Limited flexibility; few alternatives to the main path | +| 2 | Some shortcuts; basic keyboard support, limited bulk actions | +| 3 | Good accelerators; keyboard nav, some customization | +| 4 | Highly flexible; multiple paths, power features, customizable | + +##### 8. Aesthetic and Minimalist Design + +Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose. + +**Check for**: +- Only necessary information visible at each step +- Clear visual hierarchy directing attention +- Purposeful use of color and emphasis +- No decorative clutter competing for attention +- Focused, uncluttered layouts + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Overwhelming; everything competes for attention equally | +| 1 | Cluttered; too much noise, hard to find what matters | +| 2 | Some clutter; main content clear, periphery noisy | +| 3 | Mostly clean; focused design, minor visual noise | +| 4 | Perfectly minimal; every element earns its pixel | + +##### 9. Help Users Recognize, Diagnose, and Recover from Errors + +Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution. + +**Check for**: +- Plain language error messages (no error codes for users) +- Specific problem identification ("Email is missing @" not "Invalid input") +- Actionable recovery suggestions +- Errors displayed near the source of the problem +- Non-blocking error handling (don't wipe the form) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Cryptic errors; codes, jargon, or no message at all | +| 1 | Vague errors; "Something went wrong" with no guidance | +| 2 | Clear but unhelpful; names the problem but not the fix | +| 3 | Clear with suggestions; identifies problem and offers next steps | +| 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work | + +##### 10. Help and Documentation + +Even if the system is usable without docs, help should be easy to find, task-focused, and concise. + +**Check for**: +- Searchable help or documentation +- Contextual help (tooltips, inline hints, guided tours) +- Task-focused organization (not feature-organized) +- Concise, scannable content +- Easy access without leaving current context + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | No help available anywhere | +| 1 | Help exists but hard to find or irrelevant | +| 2 | Basic help; FAQ or docs exist, not contextual | +| 3 | Good documentation; searchable, mostly task-focused | +| 4 | Excellent contextual help; right info at the right moment | + +--- + +#### Score Summary + +**Total possible**: 40 points (10 heuristics × 4 max) + +| Score Range | Rating | What It Means | +|-------------|--------|---------------| +| 36–40 | Excellent | Minor polish only; ship it | +| 28–35 | Good | Address weak areas, solid foundation | +| 20–27 | Acceptable | Significant improvements needed before users are happy | +| 12–19 | Poor | Major UX overhaul required; core experience broken | +| 0–11 | Critical | Redesign needed; unusable in current state | + +--- + +#### Issue Severity (P0–P3) + +Tag each individual issue found during scoring with a priority level: + +| Priority | Name | Description | Action | +|----------|------|-------------|--------| +| **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper | +| **P1** | Major | Causes significant difficulty or confusion | Fix before release | +| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass | +| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits | + +**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1. + +--- + +### Persona-Based Design Testing + +Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss. + +**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns. + +--- + +#### 1. Impatient Power User: "Alex" + +**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave. + +**Behaviors**: +- Skips all onboarding and instructions +- Looks for keyboard shortcuts immediately +- Tries to bulk-select, batch-edit, and automate +- Gets frustrated by required steps that feel unnecessary +- Abandons if anything feels slow or patronizing + +**Test Questions**: +- Can Alex complete the core task in under 60 seconds? +- Are there keyboard shortcuts for common actions? +- Can onboarding be skipped entirely? +- Do modals have keyboard dismiss (Esc)? +- Is there a "power user" path (shortcuts, bulk actions)? + +**Red Flags** (report these specifically): +- Forced tutorials or unskippable onboarding +- No keyboard navigation for primary actions +- Slow animations that can't be skipped +- One-item-at-a-time workflows where batch would be natural +- Redundant confirmation steps for low-risk actions + +--- + +#### 2. Confused First-Timer: "Jordan" + +**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out. + +**Behaviors**: +- Reads all instructions carefully +- Hesitates before clicking anything unfamiliar +- Looks for help or support constantly +- Misunderstands jargon and abbreviations +- Takes the most literal interpretation of any label + +**Test Questions**: +- Is the first action obviously clear within 5 seconds? +- Are all icons labeled with text? +- Is there contextual help at decision points? +- Does terminology assume prior knowledge? +- Is there a clear "back" or "undo" at every step? + +**Red Flags** (report these specifically): +- Icon-only navigation with no labels +- Technical jargon without explanation +- No visible help option or guidance +- Ambiguous next steps after completing an action +- No confirmation that an action succeeded + +--- + +#### 3. Accessibility-Dependent User: "Sam" + +**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences. + +**Behaviors**: +- Tabs through the interface linearly +- Relies on ARIA labels and heading structure +- Cannot see hover states or visual-only indicators +- Needs adequate color contrast (4.5:1 minimum) +- May use browser zoom up to 200% + +**Test Questions**: +- Can the entire primary flow be completed keyboard-only? +- Are all interactive elements focusable with visible focus indicators? +- Do images have meaningful alt text? +- Is color contrast WCAG AA compliant (4.5:1 for text)? +- Does the screen reader announce state changes (loading, success, errors)? + +**Red Flags** (report these specifically): +- Click-only interactions with no keyboard alternative +- Missing or invisible focus indicators +- Meaning conveyed by color alone (red = error, green = success) +- Unlabeled form fields or buttons +- Time-limited actions without extension option +- Custom components that break screen reader flow + +--- + +#### 4. Deliberate Stress Tester: "Riley" + +**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience. + +**Behaviors**: +- Tests edge cases intentionally (empty states, long strings, special characters) +- Submits forms with unexpected data (emoji, RTL text, very long values) +- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs +- Looks for inconsistencies between what the UI promises and what actually happens +- Documents problems methodically + +**Test Questions**: +- What happens at the edges (0 items, 1000 items, very long text)? +- Do error states recover gracefully or leave the UI in a broken state? +- What happens on refresh mid-workflow? Is state preserved? +- Are there features that appear to work but produce broken results? +- How does the UI handle unexpected input (emoji, special chars, paste from Excel)? + +**Red Flags** (report these specifically): +- Features that appear to work but silently fail or produce wrong results +- Error handling that exposes technical details or leaves UI in a broken state +- Empty states that show nothing useful ("No results" with no guidance) +- Workflows that lose user data on refresh or navigation +- Inconsistent behavior between similar interactions in different parts of the UI + +--- + +#### 5. Distracted Mobile User: "Casey" + +**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection. + +**Behaviors**: +- Uses thumb only; prefers bottom-of-screen actions +- Gets interrupted mid-flow and returns later +- Switches between apps frequently +- Has limited attention span and low patience +- Types as little as possible, prefers taps and selections + +**Test Questions**: +- Are primary actions in the thumb zone (bottom half of screen)? +- Is state preserved if the user leaves and returns? +- Does it work on slow connections (3G)? +- Can forms use autocomplete and smart defaults? +- Are touch targets at least 44×44pt? + +**Red Flags** (report these specifically): +- Important actions positioned at the top of the screen (unreachable by thumb) +- No state persistence; progress lost on tab switch or interruption +- Large text inputs required where selection would work +- Heavy assets loading on every page (no lazy loading) +- Tiny tap targets or targets too close together + +--- + +#### Selecting Personas + +Choose personas based on the interface type: + +| Interface Type | Primary Personas | Why | +|---------------|-----------------|-----| +| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile | +| Dashboard / admin | Alex, Sam | Power users, accessibility | +| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity | +| Onboarding flow | Jordan, Casey | Confusion, interruption | +| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav | +| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile | + +--- + +#### Project-Specific Personas + +If `AGENTS.md` contains a `## Design Context` section (generated by `impeccable init`), derive 1–2 additional personas from the audience and brand information: + +1. Read the target audience description +2. Identify the primary user archetype not covered by the 5 predefined personas +3. Create a persona following this template: + +``` +##### [Role]: "[Name]" + +**Profile**: [2-3 key characteristics derived from Design Context] + +**Behaviors**: [3-4 specific behaviors based on the described audience] + +**Red Flags**: [3-4 things that would alienate this specific user type] +``` + +Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists. diff --git a/.agents/skills/impeccable/reference/document.md b/.agents/skills/impeccable/reference/document.md index ebafe4f41..7667d691d 100644 --- a/.agents/skills/impeccable/reference/document.md +++ b/.agents/skills/impeccable/reference/document.md @@ -45,7 +45,7 @@ Rules that matter: - **Token refs** use `{path.to.token}` (e.g. `{colors.primary}`, `{rounded.md}`). Components may reference primitives; primitives may not reference each other. - **Stitch validates colors as hex sRGB only** (`#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`); OKLCH/HSL/P3 trigger a linter warning, not a hard error. YAML accepts the string either way and our own parser is format-agnostic. Choose based on project posture: (a) if the project has an "OKLCH-only" doctrine or uses Display-P3 values that don't round-trip through sRGB, put OKLCH directly in the frontmatter and accept the Stitch linter warning; (b) if the project wants strict Stitch compliance or plans to use their Tailwind/DTCG export pipeline, put hex in the frontmatter and keep OKLCH in prose as the canonical reference. Never split the source of truth without explicit reason. - **Component sub-tokens** are limited to 8 props: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, `width`. Shadows, motion, focus rings, backdrop-filter: none of those fit. Carry them in the sidecar (Step 4b). -- **Scale keys are open-ended.** Use whatever names the project already uses (`warm-ash-cream`, `surface-container-low`). Don't rename to Material defaults. +- **Scale keys are open-ended.** Use whatever names the project already uses (`oxblood-deep`, `surface-container-low`). Don't rename to Material defaults. - **Variants are naming convention, not schema.** `button-primary` / `button-primary-hover` / `button-primary-active` as sibling keys. ## The markdown body: six sections (exact order) @@ -61,7 +61,7 @@ Optional evocative subtitles are allowed in the form `## 2. Colors: The [Name] P ## When to run -- The user just ran `$impeccable teach` and needs the visual side documented. +- The user just ran `$impeccable init` and needs the visual side documented. - The skill noticed no `DESIGN.md` exists and nudged the user to create one. - An existing `DESIGN.md` is stale (the design has drifted). - Before a large redesign, to capture the current state as a reference. @@ -71,7 +71,7 @@ If a `DESIGN.md` already exists, **do not silently overwrite it**. Show the user ## Two paths - **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 teach, nothing built yet). Interview for five high-level answers, write a minimal DESIGN.md marked ``. Re-run in scan mode once there's code. +- **Seed mode**: the project is pre-implementation (fresh init, nothing built yet). Interview for five high-level answers, 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` forces seed mode regardless of code presence. @@ -103,7 +103,7 @@ Build a structured draft from the discovered tokens. For each token class: From the auto-extracted tokens, draft the YAML frontmatter now (you'll write it at the top of DESIGN.md in Step 4). This is the machine-readable layer: what the live panel and Stitch's linter consume. -- **Colors**: one entry per extracted color. Key = descriptive slug (`warm-ash-cream`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. +- **Colors**: one entry per extracted color. Key = descriptive slug (`oxblood-deep`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. - **Typography**: one entry per role (`display`, `headline`, `title`, `body`, `label`). Typography is an object; include only the props that are real for the project (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`). - **Rounded / Spacing**: whatever scale steps the project actually uses, keyed by whatever scale name the project uses (`sm` / `md` / `lg`, or `surface-sm`, or numeric steps). - **Components**: one entry per variant (`button-primary`, `button-primary-hover`, `button-ghost`). Reference primitives via `{colors.X}`, `{rounded.Y}`. If a variant needs a property Stitch's 8-prop set doesn't cover (shadow, focus ring, backdrop-filter), carry the full snippet in the sidecar instead. @@ -253,7 +253,7 @@ Regenerate the sidecar whenever you regenerate root `DESIGN.md`. If the user onl "extensions": { "colorMeta": { "primary": { "role": "primary", "displayName": "Editorial Magenta", "canonical": "oklch(60% 0.25 350)", "tonalRamp": ["...", "...", "..."] }, - "warm-ash-cream": { "role": "neutral", "displayName": "Warm Ash Cream", "canonical": "oklch(96% 0.005 350)", "tonalRamp": ["...", "...", "..."] } + "cool-paper": { "role": "neutral", "displayName": "Cool Paper", "canonical": "oklch(96% 0.005 230)", "tonalRamp": ["...", "...", "..."] } }, "typographyMeta": { "display": { "displayName": "Display", "purpose": "Hero headlines only." } @@ -328,12 +328,13 @@ Pull directly from the DESIGN.md you just wrote: Do not reword. The panel shows these as secondary collapsible context; the same voice that's in the Markdown carries through. -### Step 5: Confirm, refine, and refresh session cache +### Step 5: Confirm and refine 1. Show the user the full DESIGN.md you wrote. Briefly highlight the non-obvious creative choices (descriptive color names, atmosphere language, named rules). 2. Mention that `.impeccable/design.json` was also written alongside; the live panel will now render this project's actual button/input/nav primitives instead of generic approximations. 3. Offer to refine any section: "Want me to revise a section, add component patterns I missed, or adjust the atmosphere language?" -4. **Refresh the session cache.** Run `node .agents/skills/impeccable/scripts/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. + +Your own write is the freshest source; subsequent commands in this session don't need a reload. ## Seed mode @@ -394,11 +395,12 @@ Per-section guidance in seed mode: 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. -### Step 4: Confirm and refresh session cache +### Step 4: Confirm 1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment). 2. Tell the user: "Re-run `$impeccable document` once you have some code. That pass will extract real tokens and generate the sidecar." -3. Run `node .agents/skills/impeccable/scripts/load-context.mjs` once so the seed lands in conversation for the rest of the session. + +Your own write is the freshest source; no reload needed. ## Style guidelines diff --git a/.agents/skills/impeccable/reference/heuristics-scoring.md b/.agents/skills/impeccable/reference/heuristics-scoring.md deleted file mode 100644 index edbe5028d..000000000 --- a/.agents/skills/impeccable/reference/heuristics-scoring.md +++ /dev/null @@ -1,234 +0,0 @@ -# Heuristics Scoring Guide - -Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough." - -## Nielsen's 10 Heuristics - -### 1. Visibility of System Status - -Keep users informed about what's happening through timely, appropriate feedback. - -**Check for**: -- Loading indicators during async operations -- Confirmation of user actions (save, submit, delete) -- Progress indicators for multi-step processes -- Current location in navigation (breadcrumbs, active states) -- Form validation feedback (inline, not just on submit) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | No feedback; user is guessing what happened | -| 1 | Rare feedback; most actions produce no visible response | -| 2 | Partial; some states communicated, major gaps remain | -| 3 | Good; most operations give clear feedback, minor gaps | -| 4 | Excellent; every action confirms, progress is always visible | - -### 2. Match Between System and Real World - -Speak the user's language. Follow real-world conventions. Information appears in natural, logical order. - -**Check for**: -- Familiar terminology (no unexplained jargon) -- Logical information order matching user expectations -- Recognizable icons and metaphors -- Domain-appropriate language for the target audience -- Natural reading flow (left-to-right, top-to-bottom priority) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Pure tech jargon, alien to users | -| 1 | Mostly confusing; requires domain expertise to navigate | -| 2 | Mixed; some plain language, some jargon leaks through | -| 3 | Mostly natural; occasional term needs context | -| 4 | Speaks the user's language fluently throughout | - -### 3. User Control and Freedom - -Users need a clear "emergency exit" from unwanted states without extended dialogue. - -**Check for**: -- Undo/redo functionality -- Cancel buttons on forms and modals -- Clear navigation back to safety (home, previous) -- Easy way to clear filters, search, selections -- Escape from long or multi-step processes - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Users get trapped; no way out without refreshing | -| 1 | Difficult exits; must find obscure paths to escape | -| 2 | Some exits; main flows have escape, edge cases don't | -| 3 | Good control; users can exit and undo most actions | -| 4 | Full control; undo, cancel, back, and escape everywhere | - -### 4. Consistency and Standards - -Users shouldn't wonder whether different words, situations, or actions mean the same thing. - -**Check for**: -- Consistent terminology throughout the interface -- Same actions produce same results everywhere -- Platform conventions followed (standard UI patterns) -- Visual consistency (colors, typography, spacing, components) -- Consistent interaction patterns (same gesture = same behavior) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Inconsistent everywhere; feels like different products stitched together | -| 1 | Many inconsistencies; similar things look/behave differently | -| 2 | Partially consistent; main flows match, details diverge | -| 3 | Mostly consistent; occasional deviation, nothing confusing | -| 4 | Fully consistent; cohesive system, predictable behavior | - -### 5. Error Prevention - -Better than good error messages is a design that prevents problems in the first place. - -**Check for**: -- Confirmation before destructive actions (delete, overwrite) -- Constraints preventing invalid input (date pickers, dropdowns) -- Smart defaults that reduce errors -- Clear labels that prevent misunderstanding -- Autosave and draft recovery - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Errors easy to make; no guardrails anywhere | -| 1 | Few safeguards; some inputs validated, most aren't | -| 2 | Partial prevention; common errors caught, edge cases slip | -| 3 | Good prevention; most error paths blocked proactively | -| 4 | Excellent; errors nearly impossible through smart constraints | - -### 6. Recognition Rather Than Recall - -Minimize memory load. Make objects, actions, and options visible or easily retrievable. - -**Check for**: -- Visible options (not buried in hidden menus) -- Contextual help when needed (tooltips, inline hints) -- Recent items and history -- Autocomplete and suggestions -- Labels on icons (not icon-only navigation) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Heavy memorization; users must remember paths and commands | -| 1 | Mostly recall; many hidden features, few visible cues | -| 2 | Some aids; main actions visible, secondary features hidden | -| 3 | Good recognition; most things discoverable, few memory demands | -| 4 | Everything discoverable; users never need to memorize | - -### 7. Flexibility and Efficiency of Use - -Accelerators, invisible to novices, speed up expert interaction. - -**Check for**: -- Keyboard shortcuts for common actions -- Customizable interface elements -- Recent items and favorites -- Bulk/batch actions -- Power user features that don't complicate the basics - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | One rigid path; no shortcuts or alternatives | -| 1 | Limited flexibility; few alternatives to the main path | -| 2 | Some shortcuts; basic keyboard support, limited bulk actions | -| 3 | Good accelerators; keyboard nav, some customization | -| 4 | Highly flexible; multiple paths, power features, customizable | - -### 8. Aesthetic and Minimalist Design - -Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose. - -**Check for**: -- Only necessary information visible at each step -- Clear visual hierarchy directing attention -- Purposeful use of color and emphasis -- No decorative clutter competing for attention -- Focused, uncluttered layouts - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Overwhelming; everything competes for attention equally | -| 1 | Cluttered; too much noise, hard to find what matters | -| 2 | Some clutter; main content clear, periphery noisy | -| 3 | Mostly clean; focused design, minor visual noise | -| 4 | Perfectly minimal; every element earns its pixel | - -### 9. Help Users Recognize, Diagnose, and Recover from Errors - -Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution. - -**Check for**: -- Plain language error messages (no error codes for users) -- Specific problem identification ("Email is missing @" not "Invalid input") -- Actionable recovery suggestions -- Errors displayed near the source of the problem -- Non-blocking error handling (don't wipe the form) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Cryptic errors; codes, jargon, or no message at all | -| 1 | Vague errors; "Something went wrong" with no guidance | -| 2 | Clear but unhelpful; names the problem but not the fix | -| 3 | Clear with suggestions; identifies problem and offers next steps | -| 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work | - -### 10. Help and Documentation - -Even if the system is usable without docs, help should be easy to find, task-focused, and concise. - -**Check for**: -- Searchable help or documentation -- Contextual help (tooltips, inline hints, guided tours) -- Task-focused organization (not feature-organized) -- Concise, scannable content -- Easy access without leaving current context - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | No help available anywhere | -| 1 | Help exists but hard to find or irrelevant | -| 2 | Basic help; FAQ or docs exist, not contextual | -| 3 | Good documentation; searchable, mostly task-focused | -| 4 | Excellent contextual help; right info at the right moment | - ---- - -## Score Summary - -**Total possible**: 40 points (10 heuristics × 4 max) - -| Score Range | Rating | What It Means | -|-------------|--------|---------------| -| 36–40 | Excellent | Minor polish only; ship it | -| 28–35 | Good | Address weak areas, solid foundation | -| 20–27 | Acceptable | Significant improvements needed before users are happy | -| 12–19 | Poor | Major UX overhaul required; core experience broken | -| 0–11 | Critical | Redesign needed; unusable in current state | - ---- - -## Issue Severity (P0–P3) - -Tag each individual issue found during scoring with a priority level: - -| Priority | Name | Description | Action | -|----------|------|-------------|--------| -| **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper | -| **P1** | Major | Causes significant difficulty or confusion | Fix before release | -| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass | -| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits | - -**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1. diff --git a/.agents/skills/impeccable/reference/teach.md b/.agents/skills/impeccable/reference/init.md similarity index 59% rename from .agents/skills/impeccable/reference/teach.md rename to .agents/skills/impeccable/reference/init.md index 6b7df209c..37c6460a6 100644 --- a/.agents/skills/impeccable/reference/teach.md +++ b/.agents/skills/impeccable/reference/init.md @@ -1,21 +1,16 @@ -# Teach Flow +# Init Flow -Gathers design context for a project and writes two complementary files at the project root: +The setup command for a project. One codebase crawl feeds everything it writes: - **PRODUCT.md** (strategic): root project file for register, target users, product purpose, brand personality, anti-references, strategic design principles. Answers "who/what/why". - **DESIGN.md** (visual): root project file for visual theme, color palette, typography, components, layout. Follows the [Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/). Answers "how it looks". +- **`.impeccable/live/config.json`** (live mode): pre-configured so `$impeccable live` boots straight into variant mode with no first-time detour. -Every other impeccable command reads these files before doing any work. +It closes by pointing the user at the best command to run next. Every other impeccable command reads PRODUCT.md and DESIGN.md before doing any work. ## Step 1: Load current state -Run the shared loader first so you know what already exists: - -```bash -node .agents/skills/impeccable/scripts/load-context.mjs -``` - -The output tells you whether PRODUCT.md and/or DESIGN.md already exist. If `migrated: true`, legacy `.impeccable.md` was auto-renamed to `PRODUCT.md`. Mention this once to the user. +Check what already exists. PRODUCT.md and DESIGN.md live at the project root, or under `.agents/context/` or `docs/` (case-insensitive). Read whichever are present with your native file tool. Also note whether `.impeccable/live/config.json` already exists (Step 6 leaves it untouched if so). Decision tree: - **Neither file exists (empty project or no context yet)**: do Steps 2-4 (write PRODUCT.md), then decide on DESIGN.md based on whether there's code to analyze. @@ -26,14 +21,14 @@ Decision tree: Never silently overwrite an existing file. Always confirm first. -If teach was invoked as a setup blocker by another command, such as `$impeccable craft landing page`, pause that command here. Complete teach, re-run the loader, then resume the original command with the freshly loaded context. For craft, resume into shape next; teach creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief. +If init was invoked as a setup blocker by another command, such as `$impeccable craft landing page`, pause that command here. Complete init, then resume the original command. Your own writes are the freshest source; no reload needed. For craft, resume into shape next; init creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief. ## Step 2: Explore the codebase -Before asking questions, thoroughly scan the project to discover what you can: +Before asking questions, thoroughly scan the project to discover what you can. This single crawl feeds PRODUCT.md, DESIGN.md, **and** the live-mode framework detection in Step 6, so be thorough once rather than re-scanning later: - **README and docs**: Project purpose, target audience, any stated goals -- **Package.json / config files**: Tech stack, dependencies, existing design libraries +- **Package.json / config files**: Tech stack, dependencies, existing design libraries, **and the framework** (Vite/SPA, Next.js, Nuxt, SvelteKit, Astro, multi-page static) plus the HTML entry the browser actually loads - **Existing components**: Current design patterns, spacing, typography in use - **Brand assets**: Logos, favicons, color values already defined - **Design tokens / CSS variables**: Existing color palettes, font stacks, spacing scales @@ -46,7 +41,7 @@ Also form a **register hypothesis** from what you find: Register is a hypothesis at this point, not a decision; Step 3 confirms it. -Note what you've learned and what remains unclear. This exploration feeds both PRODUCT.md and DESIGN.md. +Note what you've learned and what remains unclear. Also note any rough edges worth a follow-up command (thin hierarchy, flat or gray palette, missing error/empty states, dull copy); Step 7 turns these into concrete recommendations without re-analyzing. ## Step 3: Ask strategic questions (for PRODUCT.md) @@ -84,8 +79,7 @@ If the signal is genuinely split (e.g. a product with a big marketing landing), ### Brand & Personality - How would you describe the brand personality in 3 words? - Reference sites or apps that capture the right feel? What specifically about them? - - For brand, push for real-world references in the right lane (tech-minimal, editorial-magazine, consumer-warm, brutalist-grid, etc.), not generic "modern" adjectives. - - For product, push for category best-tool references (Linear, Figma, Notion, Raycast, Stripe). + - Push for specific named references with the *specific* thing about them that fits this brand, not generic "modern" adjectives or category-bucket lanes. - What should this explicitly NOT look like? Any anti-references? ### Accessibility & Inclusion @@ -141,16 +135,38 @@ If the user agrees, delegate to `$impeccable document` (it auto-detects scan vs If the user prefers to skip, mention they can run `$impeccable document` any time later. -## Step 6: Confirm and wrap up +## Step 6: Configure live mode (when code exists) -Summarize: +If the project has code with HTML entries and a dev server (the same "code exists" condition that puts `$impeccable document` in scan mode), pre-configure live mode now. You already identified the framework and the served HTML entry in Step 2, so this is nearly free, and it spares the user the first-time setup detour when they later run `$impeccable live`. + +**Skip this step for empty / pre-implementation projects** (nothing to inject into yet). Tell the user live mode will configure itself the first time they run it once there's code. + +**If `.impeccable/live/config.json` already exists, leave it untouched** and note that live mode is already configured. + +Otherwise: + +1. Write `.impeccable/live/config.json`. Choose `files` (the HTML entries the browser actually loads), `insertBefore`, and `commentSyntax` from the framework table in [live.md](live.md)'s **First-time setup** section, using the framework you found in Step 2. That table is canonical; do not restate it here. For multi-page static sites, prefer a glob (`["public/**/*.html"]`) over a literal list. +2. Run `node .agents/skills/impeccable/scripts/detect-csp.mjs`. If it reports a patchable shape (`append-arrays` / `append-string`), use the **consent prompt template** from live.md before editing any source file. On decline, skip the patch. For `middleware` / `meta-tag` shapes, surface the detected files and ask the user to add `http://localhost:8400` to `script-src` and `connect-src` manually. For `null`, there's nothing to do. +3. Set `cspChecked: true` in the config once CSP is handled (patched, declined, manual, or not needed). The schema and per-shape patch details live in live.md's First-time setup; follow it rather than duplicating. + +Writing the config file is harmless and needs no consent; only the CSP **source-file patch** requires a yes. + +## Step 7: Recommend starting points, then wrap up + +Summarize tersely: - Register captured (brand / product) -- What was written (PRODUCT.md, DESIGN.md, or both) +- What was written (PRODUCT.md, DESIGN.md, live config, or a subset) - The 3-5 strategic principles from PRODUCT.md that will guide future work -- If DESIGN.md is pending, remind the user how to generate it later +- If DESIGN.md or live config is pending, one line on how to set it up later -**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node .agents/skills/impeccable/scripts/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. +Then recommend the **best commands to run next**, drawn from what your Step 2 crawl already surfaced. Do not run a fresh analysis here; surface observations you already have. Tailor to register and to what you saw, offer the 2-4 most relevant (not a menu dump), and give the exact command to type. Group by intent: -If teach was invoked as a blocker by another impeccable command (e.g. the user ran `$impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context. +- **Build something new**: `$impeccable craft ` (shape, then build end-to-end) or `$impeccable shape ` (plan first). Lead with this for empty or early-stage projects. +- **Improve what's there**: name the specific surface. `$impeccable critique ` for a scored UX review; `$impeccable audit ` for a11y / perf / responsive checks; `$impeccable polish ` for a pre-ship pass. When the crawl flagged a specific weakness, point the matching command at it: thin hierarchy or spacing → `layout`, flat or gray palette → `colorize`, missing error / empty states → `harden` or `onboard`, dull or unclear copy → `clarify`. +- **Iterate visually**: `$impeccable live` (configured in Step 6) to pick elements in the browser and generate variants in place. + +The full command menu is one bare `$impeccable` away; keep this list short and pointed. + +If init was invoked as a blocker by another impeccable command (e.g. the user ran `$impeccable polish` with no PRODUCT.md), resume that original task now. Your own writes are the freshest source; no reload needed. Optionally STOP and use Codex's structured user-input/question tool when available; if unavailable, ask directly in chat to clarify what you cannot infer. Ask whether they'd like a brief summary of PRODUCT.md appended to AGENTS.md for easier agent reference. If yes, append a short **Design Context** pointer section there. diff --git a/.agents/skills/impeccable/reference/interaction-design.md b/.agents/skills/impeccable/reference/interaction-design.md index 15aed5b29..c42c9d810 100644 --- a/.agents/skills/impeccable/reference/interaction-design.md +++ b/.agents/skills/impeccable/reference/interaction-design.md @@ -150,12 +150,6 @@ For browsers without anchor positioning support, `position: fixed` with manual c Check viewport boundaries before rendering. If the dropdown would overflow the bottom edge, flip it above the trigger. If it would overflow the right edge, align it to the trigger's right side instead. -### Anti-Patterns - -- **`position: absolute` inside `overflow: hidden`** - The dropdown will be clipped. Use `position: fixed` or the top layer instead. -- **Arbitrary z-index values** like `z-index: 9999` - Use a semantic z-index scale: `dropdown (100) -> sticky (200) -> modal-backdrop (300) -> modal (400) -> toast (500) -> tooltip (600)`. -- **Rendering dropdown markup inline** without an escape hatch from the parent's stacking context. Either use `popover` (top layer), a portal, or `position: fixed`. - ## Destructive Actions: Undo > Confirm **Undo is better than confirmation dialogs.** Users click through confirmations mindlessly. Remove from UI immediately, show undo toast, actually delete after toast expires. Use confirmation only for truly irreversible actions (account deletion), high-cost actions, or batch operations. diff --git a/.agents/skills/impeccable/reference/layout.md b/.agents/skills/impeccable/reference/layout.md index cd2bcdda7..72e6fbea3 100644 --- a/.agents/skills/impeccable/reference/layout.md +++ b/.agents/skills/impeccable/reference/layout.md @@ -1,4 +1,4 @@ -Space is the most underused design tool. Find the layout's actual problem (monotone spacing, weak hierarchy, identical card grids, the centered-stack default) and fix the structure, not the surface. +Space is the most underused design tool. Find the layout's actual problem (monotone spacing, weak hierarchy, identical card grids) and fix the structure, not the surface. --- @@ -27,7 +27,6 @@ Analyze what's weak about the current spatial design: 3. **Grid & structure**: - Is there a clear underlying structure, or does the layout feel random? - Are identical card grids used everywhere? (Icon + heading + text, repeated endlessly) - - Is everything centered? (Left-aligned with asymmetric layouts feels more designed, but not a hard and fast rule) 4. **Rhythm & variety**: - Does the layout have visual rhythm? (Alternating tight/generous spacing) @@ -43,8 +42,6 @@ Analyze what's weak about the current spatial design: ## Plan Layout Improvements -Consult the [spatial design reference](spatial-design.md) for detailed guidance on grids, rhythm, and container queries. - Create a systematic plan: - **Spacing system**: Use a consistent scale (a framework's built-in scale like Tailwind's, rem-based tokens, or a custom system). The specific values matter less than consistency. @@ -57,6 +54,7 @@ Create a systematic plan: ### Establish a Spacing System - Use a consistent spacing scale (framework scales like Tailwind, rem-based tokens, or a custom scale all work). What matters is that values come from a defined set, not arbitrary numbers. +- Prefer a 4pt base scale (4, 8, 12, 16, 24, 32, 48, 64, 96px) over 8pt; 8pt is too coarse and you'll frequently need 12px between 8 and 16. - Name tokens semantically if using custom properties: `--space-xs` through `--space-xl`, not `--spacing-8` - Use `gap` for sibling spacing instead of margins; eliminates margin collapse hacks - Apply `clamp()` for fluid spacing that breathes on larger screens @@ -66,15 +64,22 @@ Create a systematic plan: - **Tight grouping** for related elements (8-12px between siblings) - **Generous separation** between distinct sections (48-96px) - **Varied spacing** within sections (not every row needs the same gap) -- **Asymmetric compositions**: break the predictable centered-content pattern when it makes sense +- **Asymmetric compositions**: a deliberate choice when the content invites it (not a default to chase). ### Choose the Right Layout Tool -- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. Flex is simpler and more appropriate for the majority of layout tasks. +- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. - **Use Grid for 2D layouts**: Page-level structure, dashboards, data-dense interfaces, anything where rows AND columns need coordinated control. -- **Don't default to Grid** when Flexbox with `flex-wrap` would be simpler and more flexible. -- Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. - Use named grid areas (`grid-template-areas`) for complex page layouts; redefine at breakpoints. +- Use **container queries** for components, viewport queries for page layouts. A card in a narrow sidebar can stay compact while the same card in a main content area expands automatically: + +```css +.card-container { container-type: inline-size; } +.card { display: grid; gap: var(--space-md); } +@container (min-width: 400px) { + .card { grid-template-columns: 120px 1fr; } +} +``` ### Break Card Grid Monotony @@ -85,18 +90,36 @@ Create a systematic plan: ### Strengthen Visual Hierarchy - Use the fewest dimensions needed for clear hierarchy. Space alone can be enough; generous whitespace around an element draws the eye. Some of the most polished designs achieve rhythm with just space and weight. Add color or size contrast only when simpler means aren't sufficient. +- The best hierarchy combines 2–3 dimensions at once. A heading that's larger, bolder, AND has more space above it reads as primary without trying: + +| Tool | Strong Hierarchy | Weak Hierarchy | +|------|------------------|----------------| +| **Size** | 3:1 ratio or more | <2:1 ratio | +| **Weight** | Bold vs Regular | Medium vs Regular | +| **Color** | High contrast | Similar tones | +| **Position** | Top/left (primary) | Bottom/right | +| **Space** | Surrounded by white space | Crowded | + - Be aware of reading flow: in LTR languages, the eye naturally scans top-left to bottom-right, but primary action placement depends on context (e.g., bottom-right in dialogs, top in navigation). - Create clear content groupings through proximity and separation. ### Manage Depth & Elevation -- Create a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip) - Build a consistent shadow scale (sm → md → lg → xl); shadows should be subtle - Use elevation to reinforce hierarchy, not as decoration ### Optical Adjustments - If an icon looks visually off-center despite being geometrically centered, nudge it. But only if you're confident it actually looks wrong. Don't adjust speculatively. +- Text at `margin-left: 0` looks slightly indented because of letterform whitespace; a negative margin (`-0.05em`) optically aligns it. Geometrically centered glyphs often look off-center (play icons need to shift right, arrows shift toward their direction). +- Touch targets must be 44×44px minimum even when the visual element is smaller. Expand the hit area with padding or a pseudo-element: + +```css +.icon-button { width: 24px; height: 24px; position: relative; } +.icon-button::before { + content: ''; position: absolute; inset: -10px; +} +``` **NEVER**: - Use arbitrary spacing values outside your scale @@ -104,10 +127,7 @@ Create a systematic plan: - Wrap everything in cards (not everything needs a container) - Nest cards inside cards (use spacing and dividers for hierarchy within) - Use identical card grids everywhere (icon + heading + text, repeated) -- Center everything (left-aligned with asymmetry feels more designed) - Default to the hero metric layout (big number, small label, stats, gradient) as a template. If showing real user data, a prominent metric can work, but it should display actual data, not decorative numbers. -- Default to CSS Grid when Flexbox would be simpler; use the simplest tool for the job -- Use arbitrary z-index values (999, 9999); build a semantic scale ## Verify Layout Improvements diff --git a/.agents/skills/impeccable/reference/live.md b/.agents/skills/impeccable/reference/live.md index 412a2e7e7..a7725d35b 100644 --- a/.agents/skills/impeccable/reference/live.md +++ b/.agents/skills/impeccable/reference/live.md @@ -4,23 +4,28 @@ Interactive live variant mode: select elements in the browser, pick a design act A running dev server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser. +Codex: run live helper commands, the app dev server, and any dependency-installing setup with `sandbox_permissions: "require_escalated"` from the start; live mode depends on localhost and package-manager network access that the sandbox blocks. + ## The contract (read once) Execute in order. No step skipped, no step reordered. 1. `live.mjs`: boot. -2. Navigate to the URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). If you can't infer it confidently, tell the user once to open their dev/preview URL. Never use `serverPort` as that URL; it's the helper, not the app. +2. Open the app URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). Never use `serverPort`; it's the helper, not the app. **Cursor:** `browser_navigate` to that URL before polling; do not skip. **Other harnesses:** use the available browser tool; if the URL is uncertain, ask the user once. 3. Poll loop with the default long timeout (600000 ms). After every event or `--reply`, run `live-poll.mjs` again immediately. Never pass a short `--timeout=`. + +The global bar **Impeccable mark** dims and shows a pulsing amber dot when no agent is long-polling `/poll`. Hover the mark for the hint; restart `live-poll.mjs` to reconnect. 4. On `generate`: read screenshot if present; load the action's reference; plan three distinct directions; write all variants in one edit; `--reply done`; poll again. -5. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts remain recoverable until you finish cleanup, run `live-complete.mjs --id EVENT_ID`, and only then poll again. -6. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart. -7. On `exit`: run the cleanup at the bottom. +5. On `steer`: read the message and `pageUrl`; do the work (page edits, navigation help, or a short reply in the `--reply` message); `--reply steer_done`; poll again. No pickup ack. The Steer bar unlocks when `steer_done` arrives over SSE. +6. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts remain recoverable until you finish cleanup, run `live-complete.mjs --id EVENT_ID`, and only then poll again. +7. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart. +8. On `exit`: run the cleanup at the bottom. Harness policy: - **Claude Code**: run the poll as a **background task** (no short timeout). The harness notifies you when it completes, so the main conversation stays free. Do not block the shell. -- **Cursor**: run the poll in the **foreground** (blocking shell; not a background terminal, not a subagent). Cursor background terminals and subagents do not reliably resume the chat with poll stdout. +- **Cursor**: run **one-shot** poll in a **background terminal** with notify on `"type":"(steer|generate|accept|discard|exit)"`. After each event the poll exits; handle it, `--reply`, then start `live-poll.mjs` again. Do **not** use `--stream` on Cursor: incremental stdout notify is slower in practice than exit-based notify (~5s vs sub-second in testing). - **Codex**: run the poll in the **foreground** (blocking shell; not a background task, not a subagent). Codex background exec sessions do not reliably surface poll stdout back into the conversation at the moment events arrive, so a "fire-and-forget" background poll will stall live mode. -- **Other harnesses**: foreground unless you know stdout reliably returns to this session. +- **Other harnesses**: one-shot foreground unless you know stdout reliably returns to this session when a shell exits. Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodies. Spend tokens on tools and edits; on failure, one or two short sentences. @@ -30,7 +35,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi node .agents/skills/impeccable/scripts/live.mjs ``` -Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation; **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `$impeccable document` for the matching DESIGN.md. +Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation; **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. `serverPort` and `serverToken` belong to the small **Impeccable live helper** HTTP server (serves `/live.js`, SSE, and `/poll`). That port is **not** your dev server and is usually not the URL you open to view the app. The browser page is whatever origin serves one of the `pageFiles` entries (Vite / Next / Bun / tunnel / LAN hostname). @@ -38,12 +43,15 @@ If output is `{ ok: false, error: "config_missing" | "config_invalid", path }`, ## Poll loop +**Default (portable, all harnesses):** + ``` LOOP: node .agents/skills/impeccable/scripts/live-poll.mjs # default long timeout; no --timeout= Read JSON; dispatch on "type" "generate" → Handle Generate; reply done; LOOP + "steer" → Handle Steer; reply steer_done; LOOP "accept" → Handle Accept; complete carbonize cleanup if required; LOOP "discard" → Handle Discard; LOOP "prefetch" → Handle Prefetch; LOOP @@ -51,6 +59,16 @@ LOOP: "exit" → break → Cleanup ``` +**Stream mode (experimental, not for Cursor):** + +``` +node .agents/skills/impeccable/scripts/live-poll.mjs --stream # stays running; one JSON line per event + Handle event; run --reply in a separate command + Repeat until "exit" line → Cleanup +``` + +Stream keeps one process alive and waits for `--reply` ack before polling again. Useful only when the harness reads incremental stdout reliably and quickly. **Cursor is not one of those:** background pattern notify on a long-running shell was ~5s to pick up events vs sub-second for one-shot exit notify. Default to one-shot everywhere unless you have measured otherwise. + ## Recovery commands The live helper persists an append-only journal under `.impeccable/live/sessions/`. Browser checkpoints are advisory but durable; the journal is canonical. This is local durable recovery state, not project source. @@ -71,9 +89,32 @@ Server restart rule: start `live-server.mjs` again, then poll. Startup requeues ## Handle `generate` -Event: `{id, action, freeformPrompt?, count, pageUrl, element, screenshotPath?, comments?, strokes?}`. +**Replace mode** (default): `{id, action, freeformPrompt?, count, pageUrl, element, screenshotPath?, comments?, strokes?}`. -Speed matters; the user is watching a spinner. Minimize tool calls by using the `wrap` helper and writing all variants in a single edit. +**Insert mode** (`event.mode === "insert"`): `{id, mode: "insert", count, pageUrl, insert: { position, anchor }, placeholder: { width, height }, freeformPrompt?, screenshotPath?, comments?, strokes?}`. No `action`. Requires a non-empty `freeformPrompt` **or** annotations. Screenshot is sent only when annotations exist (same rule as replace). Use `placeholder` dimensions as a soft size hint for net-new content. + +Speed matters; the user is watching a spinner. Minimize tool calls by using the wrap/insert helper and writing all variants in a single edit. + +### Insert mode branch + +When `event.mode === "insert"`: + +1. Read the screenshot if `event.screenshotPath` is present (annotations only). +2. Run the insert helper instead of wrap: + +```bash +node .agents/skills/impeccable/scripts/live-insert.mjs --id EVENT_ID --count EVENT_COUNT --position after \ + --element-id "ANCHOR_ID" --classes "class1,class2" --tag "section" --text "ANCHOR_TEXT" +``` + +- `--position` ← `event.insert.position` (`before` | `after`) +- Anchor flags ← `event.insert.anchor` (same mapping as wrap: id, classes, tag, text) + +The scaffold has **no** `data-impeccable-variant="original"`. Variants are net-new HTML+CSS inserted at `insertLine`. Load `brand.md` or `product.md` (freeform only, no action sub-command). Write all variants in one edit, then `--reply done`. + +On accept/discard, `live-accept.mjs` removes the wrapper block; the anchor element is untouched. + +### Replace mode (default) ### 1. Read the screenshot (if present) @@ -424,6 +465,28 @@ A background agent may be used for the rewrite, but the current thread is respon Event: `{id, _acceptResult, _completionAck}`. The poll script already restored the original, removed all variant markers, and acknowledged `discarded` durable completion. Nothing to do unless `_completionAck.ok !== true`; in that case run `live-complete.mjs --id EVENT_ID --discarded`, then poll again. +## Handle `steer` + +Event: `{id, message, pageUrl}`. The user typed or spoke into the global bar **Steer** control: page-level direction without picking an element or launching variant generation. + +The mic button uses the browser **Web Speech API** (MVP): click to start, speak, stop automatically when the utterance ends, then the transcript submits as a steer event. Click again while listening to cancel without submitting. + +This is lighter than `generate`: no screenshot, no element context, no variant cycling. Read `message` and inspect the live page or project files as needed, then either make edits or answer in prose. + +When finished: + +```bash +node .agents/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID steer_done ["Optional short note for a browser toast"] +``` + +On failure: + +```bash +node .agents/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID error "Short reason" +``` + +Then poll again immediately. Do not send a separate "picked up" reply. The Steer bar stays locked until `steer_done` or `error` arrives over SSE. + ## Handle `prefetch` Event: `{pageUrl}`. The browser fires this the first time the user selects an element on a given route, as a latency shortcut; it signals the user is likely about to Go on a page you haven't read yet. diff --git a/.agents/skills/impeccable/reference/motion-design.md b/.agents/skills/impeccable/reference/motion-design.md deleted file mode 100644 index 78b6fd260..000000000 --- a/.agents/skills/impeccable/reference/motion-design.md +++ /dev/null @@ -1,109 +0,0 @@ -# Motion Design - -## Duration: The 100/300/500 Rule - -Timing matters more than easing. These durations feel right for most UI: - -| Duration | Use Case | Examples | -|----------|----------|----------| -| **100-150ms** | Instant feedback | Button press, toggle, color change | -| **200-300ms** | State changes | Menu open, tooltip, hover states | -| **300-500ms** | Layout changes | Accordion, modal, drawer | -| **500-800ms** | Entrance animations | Page load, hero reveals | - -**Exit animations are faster than entrances.** Use ~75% of enter duration. - -## Easing: Pick the Right Curve - -**Don't use `ease`.** It's a compromise that's rarely optimal. Instead: - -| Curve | Use For | CSS | -|-------|---------|-----| -| **ease-out** | Elements entering | `cubic-bezier(0.16, 1, 0.3, 1)` | -| **ease-in** | Elements leaving | `cubic-bezier(0.7, 0, 0.84, 0)` | -| **ease-in-out** | State toggles (there → back) | `cubic-bezier(0.65, 0, 0.35, 1)` | - -**For micro-interactions, use exponential curves.** They feel natural because they mimic real physics (friction, deceleration): - -```css -/* Quart out - smooth, refined (recommended default) */ ---ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1); - -/* Quint out - slightly more dramatic */ ---ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1); - -/* Expo out - snappy, confident */ ---ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); -``` - -**Avoid bounce and elastic curves.** They were trendy in 2015 but now feel tacky and amateurish. Real objects don't bounce when they stop; they decelerate smoothly. Overshoot effects draw attention to the animation itself rather than the content. - -## Premium Motion Materials - -Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties: blur reveals, backdrop-filter panels, saturation or brightness shifts, shadow bloom, SVG filters, masks, clip paths, gradient-position movement, and variable font or shader-driven effects. - -Use the right material for the effect: - -- **Transform / opacity**: movement, press feedback, simple reveals, list choreography. -- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances, atmospheric transitions. -- **Clip path / masks**: wipes, reveals, editorial cropping, product-like transitions. -- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state. -- **Grid-template rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly. - -The hard rule is not "transform and opacity only." The hard rule is: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify in-browser that the result is smooth on the target viewports. If blur/filter makes the interaction feel significantly more premium and remains smooth, use it. - -## Staggered Animations - -Use CSS custom properties for cleaner stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"` on each item. **Cap total stagger time**: 10 items at 50ms = 500ms total. For many items, reduce per-item delay or cap staggered count. - -## Reduced Motion - -This is not optional. Vestibular disorders affect ~35% of adults over 40. - -```css -/* Define animations normally */ -.card { - animation: slide-up 500ms ease-out; -} - -/* Provide alternative for reduced motion */ -@media (prefers-reduced-motion: reduce) { - .card { - animation: fade-in 200ms ease-out; /* Crossfade instead of motion */ - } -} - -/* Or disable entirely */ -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - transition-duration: 0.01ms !important; - } -} -``` - -**What to preserve**: Functional animations like progress bars, loading spinners (slowed down), and focus indicators should still work, just without spatial movement. - -## Perceived Performance - -**Nobody cares how fast your site is, just how fast it feels.** Perception can be as effective as actual performance. - -**The 80ms threshold**: Our brains buffer sensory input for ~80ms to synchronize perception. Anything under 80ms feels instant and simultaneous. This is your target for micro-interactions. - -**Active vs passive time**: Passive waiting (staring at a spinner) feels longer than active engagement. Strategies to shift the balance: - -- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening. -- **Early completion**: Show content progressively, don't wait for everything. Video buffering, progressive images, streaming HTML. -- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Instagram likes work offline; the UI updates instantly, syncs later. Use for low-stakes actions; avoid for payments or destructive operations. - -**Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances, but ease-in toward a task's end compresses perceived time. - -**Caution**: Too-fast responses can decrease perceived value. Users may distrust instant results for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening. - -## Performance - -Don't use `will-change` preemptively, only when animation is imminent (`:hover`, `.animating`). For scroll-triggered animations, use Intersection Observer instead of scroll events; unobserve after animating once. Create motion tokens for consistency (durations, easings, common transitions). - ---- - -**Avoid**: Animating everything (animation fatigue is real). Using >500ms for UI feedback. Ignoring `prefers-reduced-motion`. Using animation to hide slow loading. diff --git a/.agents/skills/impeccable/reference/personas.md b/.agents/skills/impeccable/reference/personas.md deleted file mode 100644 index 43ae8eb2f..000000000 --- a/.agents/skills/impeccable/reference/personas.md +++ /dev/null @@ -1,179 +0,0 @@ -# Persona-Based Design Testing - -Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss. - -**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns. - ---- - -## 1. Impatient Power User: "Alex" - - -**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave. - -**Behaviors**: -- Skips all onboarding and instructions -- Looks for keyboard shortcuts immediately -- Tries to bulk-select, batch-edit, and automate -- Gets frustrated by required steps that feel unnecessary -- Abandons if anything feels slow or patronizing - -**Test Questions**: -- Can Alex complete the core task in under 60 seconds? -- Are there keyboard shortcuts for common actions? -- Can onboarding be skipped entirely? -- Do modals have keyboard dismiss (Esc)? -- Is there a "power user" path (shortcuts, bulk actions)? - -**Red Flags** (report these specifically): -- Forced tutorials or unskippable onboarding -- No keyboard navigation for primary actions -- Slow animations that can't be skipped -- One-item-at-a-time workflows where batch would be natural -- Redundant confirmation steps for low-risk actions - ---- - -## 2. Confused First-Timer: "Jordan" - -**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out. - -**Behaviors**: -- Reads all instructions carefully -- Hesitates before clicking anything unfamiliar -- Looks for help or support constantly -- Misunderstands jargon and abbreviations -- Takes the most literal interpretation of any label - -**Test Questions**: -- Is the first action obviously clear within 5 seconds? -- Are all icons labeled with text? -- Is there contextual help at decision points? -- Does terminology assume prior knowledge? -- Is there a clear "back" or "undo" at every step? - -**Red Flags** (report these specifically): -- Icon-only navigation with no labels -- Technical jargon without explanation -- No visible help option or guidance -- Ambiguous next steps after completing an action -- No confirmation that an action succeeded - ---- - -## 3. Accessibility-Dependent User: "Sam" - -**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences. - -**Behaviors**: -- Tabs through the interface linearly -- Relies on ARIA labels and heading structure -- Cannot see hover states or visual-only indicators -- Needs adequate color contrast (4.5:1 minimum) -- May use browser zoom up to 200% - -**Test Questions**: -- Can the entire primary flow be completed keyboard-only? -- Are all interactive elements focusable with visible focus indicators? -- Do images have meaningful alt text? -- Is color contrast WCAG AA compliant (4.5:1 for text)? -- Does the screen reader announce state changes (loading, success, errors)? - -**Red Flags** (report these specifically): -- Click-only interactions with no keyboard alternative -- Missing or invisible focus indicators -- Meaning conveyed by color alone (red = error, green = success) -- Unlabeled form fields or buttons -- Time-limited actions without extension option -- Custom components that break screen reader flow - ---- - -## 4. Deliberate Stress Tester: "Riley" - -**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience. - -**Behaviors**: -- Tests edge cases intentionally (empty states, long strings, special characters) -- Submits forms with unexpected data (emoji, RTL text, very long values) -- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs -- Looks for inconsistencies between what the UI promises and what actually happens -- Documents problems methodically - -**Test Questions**: -- What happens at the edges (0 items, 1000 items, very long text)? -- Do error states recover gracefully or leave the UI in a broken state? -- What happens on refresh mid-workflow? Is state preserved? -- Are there features that appear to work but produce broken results? -- How does the UI handle unexpected input (emoji, special chars, paste from Excel)? - -**Red Flags** (report these specifically): -- Features that appear to work but silently fail or produce wrong results -- Error handling that exposes technical details or leaves UI in a broken state -- Empty states that show nothing useful ("No results" with no guidance) -- Workflows that lose user data on refresh or navigation -- Inconsistent behavior between similar interactions in different parts of the UI - ---- - -## 5. Distracted Mobile User: "Casey" - -**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection. - -**Behaviors**: -- Uses thumb only; prefers bottom-of-screen actions -- Gets interrupted mid-flow and returns later -- Switches between apps frequently -- Has limited attention span and low patience -- Types as little as possible, prefers taps and selections - -**Test Questions**: -- Are primary actions in the thumb zone (bottom half of screen)? -- Is state preserved if the user leaves and returns? -- Does it work on slow connections (3G)? -- Can forms use autocomplete and smart defaults? -- Are touch targets at least 44×44pt? - -**Red Flags** (report these specifically): -- Important actions positioned at the top of the screen (unreachable by thumb) -- No state persistence; progress lost on tab switch or interruption -- Large text inputs required where selection would work -- Heavy assets loading on every page (no lazy loading) -- Tiny tap targets or targets too close together - ---- - -## Selecting Personas - -Choose personas based on the interface type: - -| Interface Type | Primary Personas | Why | -|---------------|-----------------|-----| -| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile | -| Dashboard / admin | Alex, Sam | Power users, accessibility | -| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity | -| Onboarding flow | Jordan, Casey | Confusion, interruption | -| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav | -| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile | - ---- - -## Project-Specific Personas - -If `AGENTS.md` contains a `## Design Context` section (generated by `impeccable teach`), derive 1–2 additional personas from the audience and brand information: - -1. Read the target audience description -2. Identify the primary user archetype not covered by the 5 predefined personas -3. Create a persona following this template: - -``` -### [Role]: "[Name]" - -**Profile**: [2-3 key characteristics derived from Design Context] - -**Behaviors**: [3-4 specific behaviors based on the described audience] - -**Red Flags**: [3-4 things that would alienate this specific user type] -``` - -Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists. diff --git a/.agents/skills/impeccable/reference/polish.md b/.agents/skills/impeccable/reference/polish.md index f67b5e379..8a0039319 100644 --- a/.agents/skills/impeccable/reference/polish.md +++ b/.agents/skills/impeccable/reference/polish.md @@ -91,7 +91,6 @@ Visual polish on a misshapen flow is wasted work. Match the *shape* of the exper - **Theme consistency**: Works in all theme variants - **Color meaning**: Same colors mean same things throughout - **Accessible focus**: Focus indicators visible with sufficient contrast -- **Tinted neutrals**: No pure gray or pure black; add subtle color tint (0.01 chroma) - **Gray on color**: Never put gray text on colored backgrounds; use a shade of that color or transparency ### Interaction States diff --git a/.agents/skills/impeccable/reference/product.md b/.agents/skills/impeccable/reference/product.md index 64b3b0169..2ca462350 100644 --- a/.agents/skills/impeccable/reference/product.md +++ b/.agents/skills/impeccable/reference/product.md @@ -10,7 +10,6 @@ Product UI's failure mode isn't flatness, it's strangeness without purpose: over ## Typography -- **System fonts are legitimate.** `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif` gives you native feel on every platform. Inter is the common cross-platform default for a reason. - **One family is often right.** Product UIs don't need display/body pairing. A well-tuned sans carries headings, buttons, labels, body, data. - **Fixed rem scale, not fluid.** Clamp-sized headings don't serve product UI. Users view at consistent DPI, and a fluid h1 that shrinks in a sidebar looks worse, not better. - **Tighter scale ratio.** 1.125–1.2 between steps is typical. More type elements here than on brand surfaces; exaggerated contrast creates noise. @@ -26,8 +25,6 @@ Product defaults to Restrained. A single surface can earn Committed (a dashboard ## Layout -- Predictable grids. Consistency IS an affordance; users navigate faster when the structure is expected. -- Familiar patterns are features. Standard navigation (top bar, side nav), breadcrumbs, tabs, and form layouts have established user expectations. Don't reinvent for flavor. - Responsive behavior is structural (collapse sidebar, responsive table, breakpoint-driven columns), not fluid typography. ## Components @@ -51,6 +48,7 @@ Every interactive component has: default, hover, focus, active, disabled, loadin - Display fonts in UI labels, buttons, data. - Reinventing standard affordances for flavor (custom scrollbars, weird form controls, non-standard modals). - Heavy color or full-saturation accents on inactive states. +- Modal as first thought. Modals are usually laziness. Exhaust inline / progressive alternatives first. ## Product permissions diff --git a/.agents/skills/impeccable/reference/responsive-design.md b/.agents/skills/impeccable/reference/responsive-design.md deleted file mode 100644 index f079e5e5d..000000000 --- a/.agents/skills/impeccable/reference/responsive-design.md +++ /dev/null @@ -1,114 +0,0 @@ -# Responsive Design - -## Mobile-First: Write It Right - -Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first. - -## Breakpoints: Content-Driven - -Don't chase device sizes; let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints. - -## Detect Input Method, Not Just Screen Size - -**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard. Use pointer and hover queries: - -```css -/* Fine pointer (mouse, trackpad) */ -@media (pointer: fine) { - .button { padding: 8px 16px; } -} - -/* Coarse pointer (touch, stylus) */ -@media (pointer: coarse) { - .button { padding: 12px 20px; } /* Larger touch target */ -} - -/* Device supports hover */ -@media (hover: hover) { - .card:hover { transform: translateY(-2px); } -} - -/* Device doesn't support hover (touch) */ -@media (hover: none) { - .card { /* No hover state - use active instead */ } -} -``` - -**Critical**: Don't rely on hover for functionality. Touch users can't hover. - -## Safe Areas: Handle the Notch - -Modern phones have notches, rounded corners, and home indicators. Use `env()`: - -```css -body { - padding-top: env(safe-area-inset-top); - padding-bottom: env(safe-area-inset-bottom); - padding-left: env(safe-area-inset-left); - padding-right: env(safe-area-inset-right); -} - -/* With fallback */ -.footer { - padding-bottom: max(1rem, env(safe-area-inset-bottom)); -} -``` - -**Enable viewport-fit** in your meta tag: -```html - -``` - -## Responsive Images: Get It Right - -### srcset with Width Descriptors - -```html -Hero image -``` - -**How it works**: -- `srcset` lists available images with their actual widths (`w` descriptors) -- `sizes` tells the browser how wide the image will display -- Browser picks the best file based on viewport width AND device pixel ratio - -### Picture Element for Art Direction - -When you need different crops/compositions (not just resolutions): - -```html - - - - ... - -``` - -## Layout Adaptation Patterns - -**Navigation**: Three stages: hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `
/` for content that can collapse on mobile. - -## Testing: Don't Trust DevTools Alone - -DevTools device emulation is useful for layout but misses: - -- Actual touch interactions -- Real CPU/memory constraints -- Network latency patterns -- Font rendering differences -- Browser chrome/keyboard appearances - -**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators. - ---- - -**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful. diff --git a/.agents/skills/impeccable/reference/shape.md b/.agents/skills/impeccable/reference/shape.md index 87beb61b2..6252c5166 100644 --- a/.agents/skills/impeccable/reference/shape.md +++ b/.agents/skills/impeccable/reference/shape.md @@ -151,7 +151,7 @@ How users interact with this feature. What happens on click, hover, scroll? What 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., spatial-design.md for complex layouts, motion-design.md for animated features, interaction-design.md for form-heavy features). +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. diff --git a/.agents/skills/impeccable/reference/spatial-design.md b/.agents/skills/impeccable/reference/spatial-design.md deleted file mode 100644 index 4d4b83afc..000000000 --- a/.agents/skills/impeccable/reference/spatial-design.md +++ /dev/null @@ -1,100 +0,0 @@ -# Spatial Design - -## Spacing Systems - -### Use 4pt Base, Not 8pt - -8pt systems are too coarse; you'll frequently need 12px (between 8 and 16). Use 4pt for granularity: 4, 8, 12, 16, 24, 32, 48, 64, 96px. - -### Name Tokens Semantically - -Name by relationship (`--space-sm`, `--space-lg`), not value (`--spacing-8`). Use `gap` instead of margins for sibling spacing; it eliminates margin collapse and cleanup hacks. - -## Grid Systems - -### The Self-Adjusting Grid - -Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. Columns are at least 280px, as many as fit per row, leftovers stretch. For complex layouts, use named grid areas (`grid-template-areas`) and redefine them at breakpoints. - -## Visual Hierarchy - -### The Squint Test - -Blur your eyes (or screenshot and blur). Can you still identify: -- The most important element? -- The second most important? -- Clear groupings? - -If everything looks the same weight blurred, you have a hierarchy problem. - -### Hierarchy Through Multiple Dimensions - -Don't rely on size alone. Combine: - -| Tool | Strong Hierarchy | Weak Hierarchy | -|------|------------------|----------------| -| **Size** | 3:1 ratio or more | <2:1 ratio | -| **Weight** | Bold vs Regular | Medium vs Regular | -| **Color** | High contrast | Similar tones | -| **Position** | Top/left (primary) | Bottom/right | -| **Space** | Surrounded by white space | Crowded | - -**The best hierarchy uses 2-3 dimensions at once**: A heading that's larger, bolder, AND has more space above it. - -### Cards Are Not Required - -Cards are overused. Spacing and alignment create visual grouping naturally. Use cards only when content is truly distinct and actionable, items need visual comparison in a grid, or content needs clear interaction boundaries. **Never nest cards inside cards.** Use spacing, typography, and subtle dividers for hierarchy within a card. - -## Container Queries - -Viewport queries are for page layouts. **Container queries are for components**: - -```css -.card-container { - container-type: inline-size; -} - -.card { - display: grid; - gap: var(--space-md); -} - -/* Card layout changes based on its container, not viewport */ -@container (min-width: 400px) { - .card { - grid-template-columns: 120px 1fr; - } -} -``` - -**Why this matters**: A card in a narrow sidebar stays compact, while the same card in a main content area expands automatically, without viewport hacks. - -## Optical Adjustments - -Text at `margin-left: 0` looks indented due to letterform whitespace; use negative margin (`-0.05em`) to optically align. Geometrically centered icons often look off-center; play icons need to shift right, arrows shift toward their direction. - -### Touch Targets vs Visual Size - -Buttons can look small but need large touch targets (44px minimum). Use padding or pseudo-elements: - -```css -.icon-button { - width: 24px; /* Visual size */ - height: 24px; - position: relative; -} - -.icon-button::before { - content: ''; - position: absolute; - inset: -10px; /* Expand tap target to 44px */ -} -``` - -## Depth & Elevation - -Create semantic z-index scales (dropdown → sticky → modal-backdrop → modal → toast → tooltip) instead of arbitrary numbers. For shadows, create a consistent elevation scale (sm → md → lg → xl). **Key insight**: Shadows should be subtle. If you can clearly see it, it's probably too strong. - ---- - -**Avoid**: Arbitrary spacing values outside your scale. Making all spacing equal (variety creates hierarchy). Creating hierarchy through size alone - combine size, weight, color, and space. diff --git a/.agents/skills/impeccable/reference/typeset.md b/.agents/skills/impeccable/reference/typeset.md index a44772eca..b537516c7 100644 --- a/.agents/skills/impeccable/reference/typeset.md +++ b/.agents/skills/impeccable/reference/typeset.md @@ -4,7 +4,7 @@ Typography carries most of the information on the page. Replace generic defaults ## Register -Brand: run the font selection procedure in [brand.md](brand.md). Pairing follows the brand's lane (display serif + sans body for editorial/luxury, one committed sans for tech, etc.). Fluid `clamp()` scale, ≥1.25 ratio between steps. +Brand: run the font selection procedure in [brand.md](brand.md). Fluid `clamp()` scale, ≥1.25 ratio between steps. Product: system fonts and familiar sans stacks are legitimate here. One well-tuned family typically carries the whole UI. Fixed `rem` scale, 1.125–1.2 ratio between more closely-spaced steps. @@ -43,7 +43,7 @@ Analyze what's weak or generic about the current type: ## Plan Typography Improvements -Consult the [typography reference](typography.md) for detailed guidance on scales, pairing, and loading strategies. +Consult the [Reference Material](#reference-material) section below for detailed guidance on scales, pairing, and loading strategies. Create a systematic plan: @@ -122,3 +122,158 @@ Each variant MUST declare a `scale` param controlling the hierarchy ratio. Expre Where the variant riffs on a specific pairing, expose the pairing choice as a `steps` param (e.g. "serif display + sans body" vs. "mono display + sans body" vs. "all-sans"). Each branch routes through `:scope[data-p-pairing="X"]` selectors in scoped CSS. See `reference/live.md` for the full params contract. + +--- + +## Reference Material + +The sections below were previously `typography.md` and live inline now so the typeset flow has its deep typography reference in one place. `bolder.md` also references this section. + +### Typography + +#### Classic Typography Principles + +##### Vertical Rhythm + +Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony; text and space share a mathematical foundation. + +##### Modular Scale & Hierarchy + +The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy. + +**Use fewer sizes with more contrast.** A 5-size system covers most needs: + +| Role | Typical Ratio | Use Case | +|------|---------------|----------| +| xs | 0.75rem | Captions, legal | +| sm | 0.875rem | Secondary UI, metadata | +| base | 1rem | Body text | +| lg | 1.25-1.5rem | Subheadings, lead text | +| xl+ | 2-4rem | Headlines, hero text | + +Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit. + +##### Readability & Measure + +Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length: narrow columns need tighter leading, wide columns need more. + +**Non-obvious**: Light text on dark backgrounds needs compensation on three axes, not just one. Bump line-height by 0.05–0.1, add a touch of letter-spacing (0.01–0.02em), and optionally step the body weight up one notch (regular → medium). The perceived weight drops across all three; fix all three. + +**Paragraph rhythm**: Pick either space between paragraphs OR first-line indentation. Never both. Digital usually wants space; editorial/long-form can justify indent-only. + +#### Font Selection & Pairing + +The tactical selection procedure and the reflex-reject list live in [reference/brand.md](brand.md) under **Font selection procedure** and **Reflex-reject list** (loaded for brand-register tasks). The rest of this section covers the adjacent knowledge: anti-reflex corrections, system font use, and pairing rules. + +##### Anti-reflexes worth defending against + +- A technical/utilitarian brief does NOT need a serif "for warmth." Most tech tools should look like tech tools. +- An editorial/premium brief does NOT need the same expressive serif everyone is using right now. Premium can be Swiss-modern, can be neo-grotesque, can be a literal monospace, can be a quiet humanist sans. +- A children's product does NOT need a rounded display font. Kids' books use real type. +- A "modern" brief does NOT need a geometric sans. The most modern thing you can do is not use the font everyone else is using. + +**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality. + +##### Pairing Principles + +**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif). + +When pairing, contrast on multiple axes: +- Serif + Sans (structure contrast) +- Geometric + Humanist (personality contrast) +- Condensed display + Wide body (proportion contrast) + +##### Web Font Loading + +The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix: + +```css +/* 1. Use font-display: swap for visibility */ +@font-face { + font-family: 'CustomFont'; + src: url('font.woff2') format('woff2'); + font-display: swap; +} + +/* 2. Match fallback metrics to minimize shift */ +@font-face { + font-family: 'CustomFont-Fallback'; + src: local('Arial'); + size-adjust: 105%; /* Scale to match x-height */ + ascent-override: 90%; /* Match ascender height */ + descent-override: 20%; /* Match descender depth */ + line-gap-override: 10%; /* Match line spacing */ +} + +body { + font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; +} +``` + +Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically. + +**`swap` vs `optional`**: `swap` shows fallback text immediately and FOUT-swaps when the web font arrives. `optional` uses the fallback if the web font misses a small load budget (~100ms) and avoids the shift entirely. Pick `optional` when zero layout shift matters more than seeing the branded font on slow networks. + +**Preload the critical weight only**: typically the regular-weight body font used above the fold. Preloading every weight costs more bandwidth than it saves. + +**Variable fonts for 3+ weights or styles**: a single variable font file is usually smaller than three static weight files, gives fractional weight control, and pairs well with `font-optical-sizing: auto`. For 1–2 weights, static is fine. + +#### Modern Web Typography + +##### Fluid Type + +Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate (higher vw = faster scaling). Add a rem offset so it doesn't collapse to 0 on small screens. + +**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes. + +**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI; fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it. + +**Bound your clamp()**: keep `max-size ≤ ~2.5 × min-size`. Wider ratios break the browser's zoom and reflow behaviour and make large viewports feel like the page is shouting. + +**Scale container width and font-size together** so effective character measure stays in the 45–75ch band at every viewport. A heading that widens faster than its container drifts out of the comfortable measure at the top end. + +##### OpenType Features + +Most developers don't know these exist. Use them for polish: + +```css +/* Proper fractions */ +.recipe-amount { font-variant-numeric: diagonal-fractions; } + +/* Small caps for abbreviations */ +abbr { font-variant-caps: all-small-caps; } + +/* Disable ligatures in code */ +code { font-variant-ligatures: none; } + +/* Enable kerning (usually on by default, but be explicit) */ +body { font-kerning: normal; } +``` + +Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/). + +##### Rendering polish + +```css +/* Variable fonts: pick the right optical-size master automatically */ +body { font-optical-sizing: auto; } +``` + +**ALL-CAPS tracking**: capitals sit too close at default spacing. Add 5–12% letter-spacing (`letter-spacing: 0.05em` to `0.12em`) to short all-caps labels, eyebrows, and small headings. Real small caps (via `font-variant-caps`) need the same treatment, slightly gentler. + +#### Typography System Architecture + +Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system. + +#### Accessibility Considerations + +Beyond contrast ratios (which are well-documented), consider: + +- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout. +- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text. +- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile. +- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets. + +--- + +**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text. diff --git a/.agents/skills/impeccable/reference/typography.md b/.agents/skills/impeccable/reference/typography.md deleted file mode 100644 index ed5ca272a..000000000 --- a/.agents/skills/impeccable/reference/typography.md +++ /dev/null @@ -1,159 +0,0 @@ -# Typography - -## Classic Typography Principles - -### Vertical Rhythm - -Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony; text and space share a mathematical foundation. - -### Modular Scale & Hierarchy - -The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy. - -**Use fewer sizes with more contrast.** A 5-size system covers most needs: - -| Role | Typical Ratio | Use Case | -|------|---------------|----------| -| xs | 0.75rem | Captions, legal | -| sm | 0.875rem | Secondary UI, metadata | -| base | 1rem | Body text | -| lg | 1.25-1.5rem | Subheadings, lead text | -| xl+ | 2-4rem | Headlines, hero text | - -Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit. - -### Readability & Measure - -Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length: narrow columns need tighter leading, wide columns need more. - -**Non-obvious**: Light text on dark backgrounds needs compensation on three axes, not just one. Bump line-height by 0.05–0.1, add a touch of letter-spacing (0.01–0.02em), and optionally step the body weight up one notch (regular → medium). The perceived weight drops across all three; fix all three. - -**Paragraph rhythm**: Pick either space between paragraphs OR first-line indentation. Never both. Digital usually wants space; editorial/long-form can justify indent-only. - -## Font Selection & Pairing - -The tactical selection procedure and the reflex-reject list live in [reference/brand.md](brand.md) under **Font selection procedure** and **Reflex-reject list** (loaded for brand-register tasks). The rest of this section covers the adjacent knowledge: anti-reflex corrections, system font use, and pairing rules. - -### Anti-reflexes worth defending against - -- A technical/utilitarian brief does NOT need a serif "for warmth." Most tech tools should look like tech tools. -- An editorial/premium brief does NOT need the same expressive serif everyone is using right now. Premium can be Swiss-modern, can be neo-grotesque, can be a literal monospace, can be a quiet humanist sans. -- A children's product does NOT need a rounded display font. Kids' books use real type. -- A "modern" brief does NOT need a geometric sans. The most modern thing you can do is not use the font everyone else is using. - -**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality. - -### Pairing Principles - -**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif). - -When pairing, contrast on multiple axes: -- Serif + Sans (structure contrast) -- Geometric + Humanist (personality contrast) -- Condensed display + Wide body (proportion contrast) - -**Never pair fonts that are similar but not identical** (e.g., two geometric sans-serifs). They create visual tension without clear hierarchy. - -### Web Font Loading - -The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix: - -```css -/* 1. Use font-display: swap for visibility */ -@font-face { - font-family: 'CustomFont'; - src: url('font.woff2') format('woff2'); - font-display: swap; -} - -/* 2. Match fallback metrics to minimize shift */ -@font-face { - font-family: 'CustomFont-Fallback'; - src: local('Arial'); - size-adjust: 105%; /* Scale to match x-height */ - ascent-override: 90%; /* Match ascender height */ - descent-override: 20%; /* Match descender depth */ - line-gap-override: 10%; /* Match line spacing */ -} - -body { - font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; -} -``` - -Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically. - -**`swap` vs `optional`**: `swap` shows fallback text immediately and FOUT-swaps when the web font arrives. `optional` uses the fallback if the web font misses a small load budget (~100ms) and avoids the shift entirely. Pick `optional` when zero layout shift matters more than seeing the branded font on slow networks. - -**Preload the critical weight only**: typically the regular-weight body font used above the fold. Preloading every weight costs more bandwidth than it saves. - -**Variable fonts for 3+ weights or styles**: a single variable font file is usually smaller than three static weight files, gives fractional weight control, and pairs well with `font-optical-sizing: auto`. For 1–2 weights, static is fine. - -## Modern Web Typography - -### Fluid Type - -Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate (higher vw = faster scaling). Add a rem offset so it doesn't collapse to 0 on small screens. - -**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes. - -**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI; fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it. - -**Bound your clamp()**: keep `max-size ≤ ~2.5 × min-size`. Wider ratios break the browser's zoom and reflow behaviour and make large viewports feel like the page is shouting. - -**Scale container width and font-size together** so effective character measure stays in the 45–75ch band at every viewport. A heading that widens faster than its container drifts out of the comfortable measure at the top end. - -### OpenType Features - -Most developers don't know these exist. Use them for polish: - -```css -/* Tabular numbers for data alignment */ -.data-table { font-variant-numeric: tabular-nums; } - -/* Proper fractions */ -.recipe-amount { font-variant-numeric: diagonal-fractions; } - -/* Small caps for abbreviations */ -abbr { font-variant-caps: all-small-caps; } - -/* Disable ligatures in code */ -code { font-variant-ligatures: none; } - -/* Enable kerning (usually on by default, but be explicit) */ -body { font-kerning: normal; } -``` - -Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/). - -### Rendering polish - -```css -/* Even out heading line lengths (browser picks better break points) */ -h1, h2, h3 { text-wrap: balance; } - -/* Reduce orphans and ragged endings in long prose */ -article p { text-wrap: pretty; } - -/* Variable fonts: pick the right optical-size master automatically */ -body { font-optical-sizing: auto; } -``` - -**ALL-CAPS tracking**: capitals sit too close at default spacing. Add 5–12% letter-spacing (`letter-spacing: 0.05em` to `0.12em`) to short all-caps labels, eyebrows, and small headings. Real small caps (via `font-variant-caps`) need the same treatment, slightly gentler. - -## Typography System Architecture - -Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system. - -## Accessibility Considerations - -Beyond contrast ratios (which are well-documented), consider: - -- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout. -- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text. -- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile. -- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets. - ---- - -**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text. diff --git a/.agents/skills/impeccable/reference/ux-writing.md b/.agents/skills/impeccable/reference/ux-writing.md deleted file mode 100644 index 417b5f56c..000000000 --- a/.agents/skills/impeccable/reference/ux-writing.md +++ /dev/null @@ -1,107 +0,0 @@ -# UX Writing - -## The Button Label Problem - -**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns: - -| Bad | Good | Why | -|-----|------|-----| -| OK | Save changes | Says what will happen | -| Submit | Create account | Outcome-focused | -| Yes | Delete message | Confirms the action | -| Cancel | Keep editing | Clarifies what "cancel" means | -| Click here | Download PDF | Describes the destination | - -**For destructive actions**, name the destruction: -- "Delete" not "Remove" (delete is permanent, remove implies recoverable) -- "Delete 5 items" not "Delete selected" (show the count) - -## Error Messages: The Formula - -Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input". - -### Error Message Templates - -| Situation | Template | -|-----------|----------| -| **Format error** | "[Field] needs to be [format]. Example: [example]" | -| **Missing required** | "Please enter [what's missing]" | -| **Permission denied** | "You don't have access to [thing]. [What to do instead]" | -| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." | -| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" | - -### Don't Blame the User - -Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date". - -## Empty States Are Opportunities - -Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items". - -## Voice vs Tone - -**Voice** is your brand's personality, consistent everywhere. -**Tone** adapts to the moment. - -| Moment | Tone Shift | -|--------|------------| -| Success | Celebratory, brief: "Done! Your changes are live." | -| Error | Empathetic, helpful: "That didn't work. Here's what to try..." | -| Loading | Reassuring: "Saving your work..." | -| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | - -**Never use humor for errors.** Users are already frustrated. Be helpful, not cute. - -## Writing for Accessibility - -**Link text** must have standalone meaning: "View pricing plans" not "Click here". **Alt text** describes information, not the image: "Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context. - -## Writing for Translation - -### Plan for Expansion - -German text is ~30% longer than English. Allocate space: - -| Language | Expansion | -|----------|-----------| -| German | +30% | -| French | +20% | -| Finnish | +30-40% | -| Chinese | -30% (fewer chars, but same width) | - -### Translation-Friendly Patterns - -Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear. - -## Consistency: The Terminology Problem - -Pick one term and stick with it: - -| Inconsistent | Consistent | -|--------------|------------| -| Delete / Remove / Trash | Delete | -| Settings / Preferences / Options | Settings | -| Sign in / Log in / Enter | Sign in | -| Create / Add / New | Create | - -Build a terminology glossary and enforce it. Variety creates confusion. - -## Avoid Redundant Copy - -If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well. - -## Loading States - -Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress. - -## Confirmation Dialogs: Use Sparingly - -Most confirmation dialogs are design failures; consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No"). - -## Form Instructions - -Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking. - ---- - -**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors. diff --git a/.agents/skills/impeccable/scripts/cleanup-deprecated.mjs b/.agents/skills/impeccable/scripts/cleanup-deprecated.mjs index 6aee4710a..bc6400e90 100644 --- a/.agents/skills/impeccable/scripts/cleanup-deprecated.mjs +++ b/.agents/skills/impeccable/scripts/cleanup-deprecated.mjs @@ -25,7 +25,7 @@ import { join, resolve } from 'node:path'; const DEPRECATED_NAMES = [ // v2.0 renames 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable teach + 'teach-impeccable', // folded into /impeccable init // v2.1 merges 'arrange', // renamed to layout 'normalize', // merged into polish diff --git a/.agents/skills/impeccable/scripts/command-metadata.json b/.agents/skills/impeccable/scripts/command-metadata.json index 1488bd5f1..83796d54b 100644 --- a/.agents/skills/impeccable/scripts/command-metadata.json +++ b/.agents/skills/impeccable/scripts/command-metadata.json @@ -3,8 +3,8 @@ "description": "Full confirmed-brief-then-build flow. Runs multi-round shape discovery first, resolves visual probe and north-star mock gates when available, then builds and visually iterates. Use when building a new feature end-to-end.", "argumentHint": "[feature description]" }, - "teach": { - "description": "Gathers design context for a project. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.", + "init": { + "description": "Sets up a project for impeccable. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles); offers DESIGN.md (visual: colors, typography, components) when code exists; pre-configures live mode; then recommends the best commands to run next. Every other command reads these files before doing work. Use once per project.", "argumentHint": "" }, "document": { diff --git a/.agents/skills/impeccable/scripts/context.mjs b/.agents/skills/impeccable/scripts/context.mjs new file mode 100644 index 000000000..52263c883 --- /dev/null +++ b/.agents/skills/impeccable/scripts/context.mjs @@ -0,0 +1,253 @@ +/** + * Context loader: prints PRODUCT.md (and DESIGN.md if present) as one + * markdown block on stdout, or exits with empty stdout when no PRODUCT.md + * is found anywhere. The skill keys off "empty stdout" to branch into the + * init flow. + * + * Path resolution (first match wins): + * 1. cwd, if PRODUCT.md or DESIGN.md is there + * 2. .agents/context/ then docs/ + * 3. $IMPECCABLE_CONTEXT_DIR (absolute or cwd-relative) — power-user + * escape hatch, only consulted when defaults are empty + * 4. cwd as a "nothing found" default + * + * `resolveContextDir()` and `loadContext()` are also exported for the + * server-side scripts (live.mjs, live-server.mjs) that need the structured + * shape rather than the markdown block. + */ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; +const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; +const FALLBACK_DIRS = ['.agents/context', 'docs']; + +// ─── Update check ────────────────────────────────────────────────────────── +// Piggyback a lightweight skill-version check on the once-per-session boot. +// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent +// can offer `npx impeccable skills update`. Everything here is best-effort and +// silent on failure: a network problem, sandbox, or missing cache must never +// block context output or print an error. + +const UPDATE_HOST = (process.env.IMPECCABLE_UPDATE_HOST || 'https://impeccable.style').replace(/\/$/, ''); +const UPDATE_CACHE_PATH = + process.env.IMPECCABLE_UPDATE_CACHE || path.join(os.homedir(), '.impeccable', 'update-check.json'); +const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // throttle the network poll to once a day +const RENOTIFY_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000; // don't re-surface the same version for a week +const FETCH_TIMEOUT_MS = 1200; + +export function resolveContextDir(cwd = process.cwd()) { + if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { + return cwd; + } + for (const rel of FALLBACK_DIRS) { + const candidate = path.resolve(cwd, rel); + if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { + return candidate; + } + } + const envDir = process.env.IMPECCABLE_CONTEXT_DIR; + if (envDir && envDir.trim()) { + const trimmed = envDir.trim(); + return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); + } + return cwd; +} + +export function loadContext(cwd = process.cwd()) { + const contextDir = resolveContextDir(cwd); + const productPath = firstExisting(contextDir, PRODUCT_NAMES); + const designPath = firstExisting(contextDir, DESIGN_NAMES); + const product = productPath ? safeRead(productPath) : null; + const design = designPath ? safeRead(designPath) : null; + return { + hasProduct: !!product, + product, + productPath: productPath ? path.relative(cwd, productPath) : null, + hasDesign: !!design, + design, + designPath: designPath ? path.relative(cwd, designPath) : null, + contextDir, + }; +} + +function firstExisting(dir, names) { + for (const name of names) { + const abs = path.join(dir, name); + if (fs.existsSync(abs)) return abs; + } + return null; +} + +function safeRead(p) { + try { + return fs.readFileSync(p, 'utf-8'); + } catch { + return null; + } +} + +/** + * Pull the register (`brand` or `product`) out of PRODUCT.md by looking + * for a `## Register` section and reading the first non-empty line that + * follows it. Returns null when the file is legacy / register-less. + */ +function extractRegister(product) { + if (!product) return null; + const lines = product.split('\n'); + for (let i = 0; i < lines.length; i++) { + if (/^##\s+Register\b/i.test(lines[i].trim())) { + for (let j = i + 1; j < lines.length; j++) { + const next = lines[j].trim(); + if (!next) continue; + const word = next.toLowerCase(); + if (word === 'brand' || word === 'product') return word; + return null; + } + } + } + return null; +} + +/** + * Read the installed skill's own version from the sibling SKILL.md frontmatter + * (this file lives at `/scripts/context.mjs`). Returns null when the + * frontmatter is missing or unreadable. + */ +function readLocalSkillVersion() { + try { + const here = path.dirname(fileURLToPath(import.meta.url)); + const skillMd = path.join(here, '..', 'SKILL.md'); + const content = fs.readFileSync(skillMd, 'utf-8'); + const match = content.match(/^version:\s*(.+)$/m); + return match ? match[1].trim().replace(/^["']|["']$/g, '') : null; + } catch { + return null; + } +} + +function readUpdateCache() { + try { + return JSON.parse(fs.readFileSync(UPDATE_CACHE_PATH, 'utf-8')); + } catch { + return {}; + } +} + +function writeUpdateCache(cache) { + try { + fs.mkdirSync(path.dirname(UPDATE_CACHE_PATH), { recursive: true }); + fs.writeFileSync(UPDATE_CACHE_PATH, JSON.stringify(cache)); + } catch { + // Best-effort: a read-only home dir just means we re-poll next session. + } +} + +/** Compare dotted numeric versions. Returns >0 when a is newer than b. */ +function compareSemver(a, b) { + const pa = String(a).split('.').map(n => parseInt(n, 10) || 0); + const pb = String(b).split('.').map(n => parseInt(n, 10) || 0); + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + const diff = (pa[i] || 0) - (pb[i] || 0); + if (diff !== 0) return diff; + } + return 0; +} + +async function fetchLatestSkillVersion() { + try { + const res = await fetch(`${UPDATE_HOST}/api/version`, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }); + if (!res.ok) return null; + const data = await res.json(); + return typeof data?.skills === 'string' ? data.skills : null; + } catch { + return null; // offline, sandboxed, timed out, or bad JSON: all non-fatal + } +} + +function buildUpdateDirective(localVersion, latestVersion) { + return ( + `UPDATE_AVAILABLE: A newer Impeccable skill is available ` + + `(installed v${localVersion}, latest v${latestVersion}). ` + + `Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` + + `Update now? It runs \`npx impeccable skills update\`." ` + + `If they agree, run \`npx impeccable skills update\` (the update applies to the next session, not this one). ` + + `Either way, continue the current task without waiting, and do not raise this again.` + ); +} + +/** + * Best-effort update directive for the boot output. Returns a string to append + * or null. Polls the version endpoint at most once per day (cached globally in + * the user's home dir) and re-surfaces a given version at most once per week so + * the agent never nags. Opt out entirely with IMPECCABLE_NO_UPDATE_CHECK=1. + */ +async function computeUpdateDirective(now = Date.now()) { + try { + if (process.env.IMPECCABLE_NO_UPDATE_CHECK) return null; + const localVersion = readLocalSkillVersion(); + if (!localVersion) return null; + + const cache = readUpdateCache(); + + // Poll the network only when the throttle window has elapsed. Stamp + // lastCheck even on failure so an offline machine doesn't poll every boot. + if (!cache.lastCheck || now - cache.lastCheck > CHECK_INTERVAL_MS) { + const latest = await fetchLatestSkillVersion(); + cache.lastCheck = now; + if (latest) cache.latestVersion = latest; + writeUpdateCache(cache); + } + + const latest = cache.latestVersion; + if (!latest || compareSemver(latest, localVersion) <= 0) return null; + + // Anti-nag: surface a given version at most once per RENOTIFY window. + if (cache.notifiedVersion === latest && cache.notifiedAt && now - cache.notifiedAt < RENOTIFY_INTERVAL_MS) { + return null; + } + cache.notifiedVersion = latest; + cache.notifiedAt = now; + writeUpdateCache(cache); + + return buildUpdateDirective(localVersion, latest); + } catch { + return null; + } +} + +async function cli() { + const ctx = loadContext(process.cwd()); + const updateDirective = await computeUpdateDirective(); + + if (!ctx.hasProduct) { + // Direct stdout message instead of relying on empty output as a signal + // — cheap models miss the empty case more often than the explicit one. + const parts = [ + 'NO_PRODUCT_MD: This project has no PRODUCT.md yet. ' + + 'Stop the current task, load reference/init.md, and follow its ' + + 'instructions to write PRODUCT.md before resuming.', + ]; + if (updateDirective) parts.push(updateDirective); + process.stdout.write(parts.join('\n\n---\n\n') + '\n'); + process.exit(0); + } + const parts = [`# PRODUCT.md\n\n${ctx.product.trim()}`]; + if (ctx.hasDesign) { + parts.push(`# DESIGN.md\n\n${ctx.design.trim()}`); + } + const register = extractRegister(ctx.product); + const next = register + ? `NEXT STEP: This project's register is \`${register}\`. You MUST now read \`reference/${register}.md\` before producing any design output.` + : `NEXT STEP: You MUST now read the matching register reference (\`reference/brand.md\` or \`reference/product.md\`) before producing any design output. Pick based on PRODUCT.md above.`; + parts.push(next); + if (updateDirective) parts.push(updateDirective); + process.stdout.write(parts.join('\n\n---\n\n') + '\n'); +} + +const _running = process.argv[1]; +if (_running?.endsWith('context.mjs') || _running?.endsWith('context.mjs/')) { + cli(); +} diff --git a/.agents/skills/impeccable/scripts/design-parser.mjs b/.agents/skills/impeccable/scripts/design-parser.mjs index b68756640..82a5cd7dc 100644 --- a/.agents/skills/impeccable/scripts/design-parser.mjs +++ b/.agents/skills/impeccable/scripts/design-parser.mjs @@ -63,7 +63,7 @@ function parseYamlSubset(yaml) { } const key = content.slice(0, colonIdx).trim(); - const rest = content.slice(colonIdx + 1).trim(); + const rest = stripInlineYamlComment(content.slice(colonIdx + 1).trim()); const parent = stack[stack.length - 1].obj; if (rest === '') { @@ -93,6 +93,21 @@ function findTopLevelColon(s) { return -1; } +function stripInlineYamlComment(s) { + let inQuote = null; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (inQuote) { + if (ch === inQuote && s[i - 1] !== '\\') inQuote = null; + } else if (ch === '"' || ch === "'") { + inQuote = ch; + } else if (ch === '#' && i > 0 && /\s/.test(s[i - 1])) { + return s.slice(0, i).trimEnd(); + } + } + return s; +} + function parseScalar(raw) { const s = raw.trim(); if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { diff --git a/.agents/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.agents/skills/impeccable/scripts/detector/browser/injected/index.mjs index c03a69caf..ceff16737 100644 --- a/.agents/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/.agents/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -9,8 +9,21 @@ if (IS_BROWSER) { const EXTENSION_MODE = (_myScript && _myScript.dataset.impeccableExtension === 'true') || document.documentElement.dataset.impeccableExtension === 'true'; - const BRAND_COLOR = 'oklch(55% 0.25 350)'; - const BRAND_COLOR_HOVER = 'oklch(45% 0.25 350)'; + // Kinpaku gold — pinned to the site's brand token (see + // site/styles/kinpaku-tokens.css --ks-kinpaku). Keep this in sync with + // the picker's C.brand in skill/scripts/live-browser.js and the kit's + // picker section in site/styles/kinpaku-kit.css. + // + // One color across both light and dark host pages. The outline is a + // 2px gesture pointing at an element + a labeled tag — it's a marker, + // not body text, so it doesn't need WCAG AA against the page. The + // label text inside the gold tag is dark (LABEL_INK) which has ~16:1 + // against the leaf gold, so reading the rule name is solid in both + // modes. Hover deepens the gold (preserves chroma — never drops it, + // dropping chroma washes the gold into a sand/olive tone). + const BRAND_COLOR = 'oklch(84% 0.19 80.46)'; + const BRAND_COLOR_HOVER = 'oklch(74% 0.18 80)'; + const LABEL_INK = 'oklch(4% 0.004 95)'; const LABEL_BG = BRAND_COLOR; const OUTLINE_COLOR = BRAND_COLOR; @@ -278,7 +291,7 @@ if (IS_BROWSER) { display: 'flex', alignItems: 'center', whiteSpace: 'nowrap', fontSize: '11px', fontWeight: '600', letterSpacing: '0.02em', - color: 'white', lineHeight: '14px', + color: LABEL_INK, lineHeight: '14px', background: LABEL_BG, fontFamily: 'system-ui, sans-serif', borderRadius: '4px 4px 0 0', @@ -398,7 +411,7 @@ if (IS_BROWSER) { banner.className = 'impeccable-overlay impeccable-banner'; Object.assign(banner.style, { position: 'fixed', top: '0', left: '0', right: '0', zIndex: '100000', - background: LABEL_BG, color: 'white', + background: LABEL_BG, color: LABEL_INK, fontFamily: 'system-ui, sans-serif', fontSize: '13px', display: 'flex', alignItems: 'center', pointerEvents: 'auto', height: '36px', overflow: 'hidden', maxWidth: '100vw', @@ -1223,12 +1236,12 @@ if (IS_BROWSER) { } console.group( `%c[impeccable] ${allFindings.length} anti-pattern${allFindings.length === 1 ? '' : 's'} found`, - 'color: oklch(60% 0.25 350); font-weight: bold' + 'color: oklch(84% 0.19 80.46); font-weight: bold' ); for (const { el, findings } of allFindings) { for (const f of findings) { console.log(`%c${f.type || f.id}%c ${f.detail || f.snippet}`, - 'color: oklch(55% 0.25 350); font-weight: bold', 'color: inherit', el); + 'color: oklch(84% 0.19 80.46); font-weight: bold', 'color: inherit', el); } } console.groupEnd(); @@ -1249,6 +1262,10 @@ if (IS_BROWSER) { const groupMap = new Map(); const _disabled = EXTENSION_MODE ? (window.__IMPECCABLE_CONFIG__?.disabledRules || []) : []; const _ruleOk = (id) => !_disabled.length || !_disabled.includes(id); + // Note: provider-gated rules (--gpt / --gemini) are NOT filtered here. In a + // real browser env (detector page, live overlay, extension) running every + // check is free, so we always surface them; the gating is purely a CLI + // output concern, applied in the Node engines' detect* return paths. for (const el of document.querySelectorAll('*')) { // Skip impeccable's own elements and any descendants (overlays, labels, banner, nav buttons) @@ -1270,11 +1287,23 @@ if (IS_BROWSER) { ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), - ...checkElementHeroEyebrowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementOversizedH1DOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementClippedOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementGptBorderShadowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementTextOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ].filter(f => _ruleOk(f.type)); addBrowserFindings(groupMap, el, findings); + + // Hero eyebrow: the offending element is the eyebrow above the heading, + // not the heading itself — highlight the previous sibling instead. + const eyebrowFindings = checkElementHeroEyebrowDOM(el) + .map(f => ({ type: f.id, detail: f.snippet })) + .filter(f => _ruleOk(f.type)); + if (eyebrowFindings.length > 0 && el.previousElementSibling) { + addBrowserFindings(groupMap, el.previousElementSibling, eyebrowFindings); + } } const pageLevelFindings = []; @@ -1306,6 +1335,14 @@ if (IS_BROWSER) { addBrowserFindings(groupMap, document.body, qualityFindings); } + const creamFindings = checkCreamPalette(document) + .map(f => ({ type: f.id, detail: f.snippet })) + .filter(f => _ruleOk(f.type)); + if (creamFindings.length > 0) { + pageLevelFindings.push(...creamFindings); + addBrowserFindings(groupMap, document.body, creamFindings); + } + // Regex-on-HTML checks (shared with Node) // Clone the document and strip impeccable-live overlay nodes before the // regex scan, so the inspector's own inline styles (transitions on top/ diff --git a/.agents/skills/impeccable/scripts/detector/cli/main.mjs b/.agents/skills/impeccable/scripts/detector/cli/main.mjs index 76da09f0c..94283e01b 100644 --- a/.agents/skills/impeccable/scripts/detector/cli/main.mjs +++ b/.agents/skills/impeccable/scripts/detector/cli/main.mjs @@ -41,7 +41,7 @@ function formatFindings(findings, jsonMode) { // Stdin handling // --------------------------------------------------------------------------- -async function handleStdin() { +async function handleStdin(options = {}) { const chunks = []; for await (const chunk of process.stdin) chunks.push(chunk); const input = Buffer.concat(chunks).toString('utf-8'); @@ -50,10 +50,10 @@ async function handleStdin() { const fp = parsed?.tool_input?.file_path; if (fp && fs.existsSync(fp)) { return HTML_EXTENSIONS.has(path.extname(fp).toLowerCase()) - ? detectHtml(fp) : detectText(fs.readFileSync(fp, 'utf-8'), fp); + ? detectHtml(fp, options) : detectText(fs.readFileSync(fp, 'utf-8'), fp, options); } } catch { /* not JSON */ } - return detectText(input, ''); + return detectText(input, '', options); } @@ -81,6 +81,8 @@ Scan files or URLs for UI anti-patterns and design quality issues. Options: --fast Regex-only mode (skip static HTML/CSS analysis, faster but misses linked stylesheets) --json Output results as JSON + --gpt Also report GPT-specific provider tells (off by default) + --gemini Also report Gemini-specific provider tells (off by default) --help Show this help message Detection modes: @@ -106,6 +108,10 @@ async function detectCli() { const jsonMode = args.includes('--json'); const helpMode = args.includes('--help'); const fastMode = args.includes('--fast'); + const providers = []; + if (args.includes('--gpt')) providers.push('gpt'); + if (args.includes('--gemini')) providers.push('gemini'); + const scanOptions = { providers }; const targets = args.filter(a => !a.startsWith('--')); if (helpMode) { printUsage(); process.exit(0); } @@ -113,7 +119,7 @@ async function detectCli() { let allFindings = []; if (!process.stdin.isTTY && targets.length === 0) { - allFindings = await handleStdin(); + allFindings = await handleStdin(scanOptions); } else { const paths = targets.length > 0 ? targets : [process.cwd()]; const urlTargetCount = paths.filter(target => /^https?:\/\//i.test(target)).length; @@ -124,8 +130,8 @@ async function detectCli() { if (/^https?:\/\//i.test(target)) { try { const scanner = browserDetector - ? (url) => browserDetector.detectUrl(url) - : (url) => detectUrl(url); + ? (url) => browserDetector.detectUrl(url, scanOptions) + : (url) => detectUrl(url, scanOptions); allFindings.push(...await scanner(target)); } catch (e) { process.stderr.write(`Error: ${e.message}\n`); } continue; @@ -192,9 +198,9 @@ async function detectCli() { const ext = path.extname(file).toLowerCase(); let fileFindings; if (!fastMode && HTML_EXTENSIONS.has(ext)) { - fileFindings = await detectHtml(file); + fileFindings = await detectHtml(file, scanOptions); } else { - fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file); + fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file, scanOptions); } // Annotate findings with import context const importers = importedByMap.get(file); @@ -209,9 +215,9 @@ async function detectCli() { } else if (stat.isFile()) { const ext = path.extname(resolved).toLowerCase(); if (!fastMode && HTML_EXTENSIONS.has(ext)) { - allFindings.push(...await detectHtml(resolved)); + allFindings.push(...await detectHtml(resolved, scanOptions)); } else { - allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved)); + allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved, scanOptions)); } } } diff --git a/.agents/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.agents/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 1afbe3f12..88439a5c3 100644 --- a/.agents/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.agents/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -38,7 +38,7 @@ const OVERUSED_FONTS = new Set([ // Older monoculture (still ubiquitous): 'inter', 'roboto', 'open sans', 'lato', 'montserrat', 'arial', 'helvetica', // Newer monoculture (the Anthropic-skill / Vercel / GitHub default wave): - 'fraunces', 'instrument sans', + 'fraunces', 'instrument sans', 'instrument serif', 'geist', 'geist sans', 'geist mono', 'mona sans', 'plus jakarta sans', 'space grotesk', 'recoleta', @@ -165,6 +165,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'AI color palette', }, + { + id: 'cream-palette', + category: 'slop', + name: 'Cream / beige palette', + description: + 'A warm cream or beige page background has become the default "tasteful" AI surface, reached for by reflex. Choose a background that comes from a deliberate palette, not the safe warm off-white.', + skillSection: 'Color & Contrast', + skillGuideline: 'cream and beige as the default surface', + }, { id: 'nested-cards', category: 'slop', @@ -183,15 +192,6 @@ const ANTIPATTERNS = [ skillSection: 'Layout & Space', skillGuideline: 'same spacing everywhere', }, - { - id: 'everything-centered', - category: 'slop', - name: 'Everything centered', - description: - 'Every text element is center-aligned. Left-aligned text with asymmetric layouts feels more designed. Center only hero sections and CTAs.', - skillSection: 'Layout & Space', - skillGuideline: 'Center everything', - }, { id: 'bounce-easing', category: 'slop', @@ -247,17 +247,72 @@ const ANTIPATTERNS = [ skillSection: 'Typography', skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding', }, + { + id: 'numbered-section-markers', + category: 'slop', + severity: 'advisory', + name: 'Numbered section markers (01 / 02 / 03)', + description: + 'Numbered display markers as section labels (01, 02, 03) are the AI editorial scaffold one tier deeper than tracked eyebrow chips. If you find yourself reaching for them, choose a different section cadence.', + skillSection: 'Layout & Space', + skillGuideline: 'numbered section markers', + }, + { + id: 'em-dash-overuse', + category: 'slop', + name: 'Em-dash overuse', + description: + 'More than two em-dashes (— or --) in body copy is an AI cadence tell. Use commas, colons, periods, or parentheses instead.', + skillSection: 'Copy', + skillGuideline: 'no em dashes', + }, + { + id: 'marketing-buzzword', + category: 'slop', + name: 'Marketing buzzword', + description: + 'Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does.', + skillSection: 'Copy', + skillGuideline: 'marketing buzzwords', + }, + { + id: 'aphoristic-cadence', + category: 'slop', + name: 'Aphoristic-cadence copy', + description: + 'Three or more sections landing on a short rebuttal sentence ("X. No Y." / "X. Just Y.") or a manufactured-contrast aphorism ("Not a feature. A platform.") reads as AI cadence, not voice. Once is fine; the pattern is the tell.', + skillSection: 'Copy', + skillGuideline: 'aphoristic cadence', + }, + { + id: 'oversized-h1', + category: 'slop', + name: 'Oversized hero headline', + description: + 'A full-sentence headline set at display size ends up dominating the viewport, leaving no room for anything else above the fold. A punchy one- or two-word headline at that size is fine — the problem is a long headline blown up too large. Set long headlines smaller, or tighten the copy.', + skillSection: 'Typography', + skillGuideline: 'long headline set at display size', + }, + { + id: 'extreme-negative-tracking', + category: 'slop', + name: 'Crushed letter spacing', + description: + 'Letter-spacing pulled tighter than the point where characters keep their own shapes costs legibility. Tighten display type optically, not destructively.', + skillSection: 'Typography', + skillGuideline: 'letter spacing crushed past legibility', + }, + { + id: 'broken-image', + category: 'quality', + name: 'Broken or placeholder image', + description: + ' tags with empty src, missing src, or placeholder values ship as broken-image boxes. Use real images, generated assets, or remove the tag.', + skillSection: 'Imagery', + skillGuideline: 'broken image references', + }, // ── Quality: general design and accessibility issues ── - { - id: 'pure-black-white', - category: 'quality', - name: 'Pure black background', - description: - 'Pure #000000 as a background color looks harsh and unnatural. Tint it slightly toward your brand hue (e.g., oklch(12% 0.01 250)) for a more refined feel.', - skillSection: 'Color & Contrast', - skillGuideline: 'pure black (#000)', - }, { id: 'gray-on-color', category: 'quality', @@ -297,7 +352,9 @@ const ANTIPATTERNS = [ category: 'quality', name: 'Cramped padding', description: - 'Text is too close to the edge of its container. Add at least 8px (ideally 12-16px) of padding inside bordered or colored containers.', + 'Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.', + skillSection: 'Layout & Space', + skillGuideline: 'inside bordered or colored containers', }, { id: 'body-text-viewport-edge', @@ -350,6 +407,70 @@ const ANTIPATTERNS = [ description: 'Letter spacing above 0.05em on body text disrupts natural character groupings and slows reading. Reserve wide tracking for short uppercase labels only.', }, + { + id: 'text-overflow', + category: 'quality', + name: 'Content overflowing its container', + description: + 'Content renders wider than its container, spilling out or forcing a horizontal scrollbar. Let text wrap, constrain widths, or give the region a deliberate scroll affordance.', + skillSection: 'Layout & Space', + skillGuideline: 'content wider than its container', + }, + { + id: 'clipped-overflow-container', + category: 'quality', + name: 'Positioned child clipped by overflow container', + description: + 'A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.', + skillSection: 'Layout & Space', + skillGuideline: 'overflow container clipping positioned children', + }, + + // ── Provider tells: opt-in via --gpt / --gemini (gated off by default) ── + { + id: 'gpt-thin-border-wide-shadow', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Hairline border with wide shadow', + description: + 'A hairline border paired with a wide, diffuse shadow is a recurring generated-UI signature. Commit to one — a defined edge or a soft elevation — rather than both at once.', + skillSection: 'Visual Details', + skillGuideline: 'hairline border plus wide diffuse shadow', + }, + { + id: 'repeating-stripes-gradient', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Repeating-gradient stripes', + description: + 'Repeating-gradient stripes used as surface decoration are a recurring generated-UI signature. Reach for a deliberate texture or leave the surface plain.', + skillSection: 'Visual Details', + skillGuideline: 'repeating-gradient decorative stripes', + }, + { + id: 'theater-slop-phrase', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Theater framing copy', + description: + 'Dismissing something as "theater" is a recurring generated-copy tic. Say plainly what the thing does or does not do.', + skillSection: 'Copy', + skillGuideline: 'theater framing copy', + }, + { + id: 'image-hover-transform', + category: 'slop', + severity: 'advisory', + gated: 'gemini', + name: 'Image hover transform', + description: + 'Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction.', + skillSection: 'Motion', + skillGuideline: 'image scale or rotate on hover', + }, ]; // --- cli/engine/shared/color.mjs --- @@ -527,11 +648,6 @@ function checkColors(opts) { } const findings = []; - // Pure black background (only solid or near-solid, not semi-transparent overlays) - if (bgColor && bgColor.a >= 0.9 && bgColor.r === 0 && bgColor.g === 0 && bgColor.b === 0) { - findings.push({ id: 'pure-black-white', snippet: '#000000 background' }); - } - if (hasDirectText && textColor && !isEmojiOnly) { // Run background-dependent checks against either a solid bg or, if the // ancestor is a gradient, against every gradient stop (use the worst case). @@ -587,9 +703,6 @@ function checkColors(opts) { // Tailwind class checks if (classList) { const classStr = typeof classList === 'string' ? classList : Array.from(classList).join(' '); - if (/\bbg-black\b(?!\/)/.test(classStr)) { - findings.push({ id: 'pure-black-white', snippet: 'bg-black' }); - } const grayMatch = classStr.match(/\btext-(?:gray|slate|zinc|neutral|stone)-\d+\b/); const colorBgMatch = classStr.match(/\bbg-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+\b/); @@ -905,12 +1018,6 @@ function checkHtmlPatterns(html) { // --- Color --- - // Pure black background - const pureBlackBgRe = /background(?:-color)?\s*:\s*(?:#000000|#000|rgb\(\s*0,\s*0,\s*0\s*\))\b/gi; - if (pureBlackBgRe.test(html)) { - findings.push({ id: 'pure-black-white', snippet: 'Pure #000 background' }); - } - // AI color palette: purple/violet const purpleHexRe = /#(?:7c3aed|8b5cf6|a855f7|9333ea|7e22ce|6d28d9|6366f1|764ba2|667eea)\b/gi; if (purpleHexRe.test(html)) { @@ -1027,6 +1134,39 @@ function checkHtmlPatterns(html) { } } + // --- Provider tells (gated): repeating-gradient stripes (GPT) --- + if (/repeating-(?:linear|radial|conic)-gradient\s*\(/i.test(html)) { + findings.push({ id: 'repeating-stripes-gradient', snippet: 'repeating-gradient decorative stripes' }); + } + + // --- Provider tells (gated): "X theater" framing copy (GPT) --- + // Lives here (regex-on-HTML) rather than in the text-content analyzers so it + // runs in the bundled browser path too, not just the CLI/static path. + { + const bodyText = html + .replace(/]*>[\s\S]*?<\/script>/gi, ' ') + .replace(/]*>[\s\S]*?<\/style>/gi, ' ') + .replace(/<[^>]+>/g, ' '); + const tm = /\b(\w+)\s+theater\b/i.exec(bodyText); + if (tm) findings.push({ id: 'theater-slop-phrase', snippet: `"${tm[0].trim()}"` }); + } + + // --- Provider tells (gated): image hover transform (Gemini) --- + // A CSS `img...:hover { transform: ... }` rule, or a Tailwind hover:scale / + // hover:rotate / hover:translate utility on an . Each distinct + // mechanism is its own finding. + const imgHoverCss = /\bimg\b[^,{}]*:hover\b[^{}]*\{[^}]*\btransform\s*:\s*(?:scale|rotate|translate|matrix|skew)/i; + if (imgHoverCss.test(html)) { + findings.push({ id: 'image-hover-transform', snippet: 'img:hover { transform } rule' }); + } + const imgTagRe = /]*\bclass\s*=\s*"([^"]*)"/gi; + let im; + while ((im = imgTagRe.exec(html)) !== null) { + if (/\bhover:(?:scale|rotate|translate|skew)-/.test(im[1])) { + findings.push({ id: 'image-hover-transform', snippet: 'Tailwind hover transform on ' }); + } + } + return findings; } @@ -1673,7 +1813,7 @@ function resolveLengthPx(value, fontSizePx) { // Both adapters resolve font-size, line-height and letter-spacing to pixels // before calling this so the pure function only deals with numbers. function checkQuality(opts) { - const { el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax = 80, viewportWidth = 0 } = opts; + const { el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax = 80, viewportWidth = 0, win = null } = opts; const findings = []; // Skip browser extension injected elements const elId = el.id || ''; @@ -1724,6 +1864,155 @@ function checkQuality(opts) { } } + // --- Flush against a visible boundary --- + // Fires when a container has a visible boundary (border, outline, OR a + // non-transparent background) AND near-zero padding on the bounded + // side(s) AND text-bearing children land flush against the boundary. + // + // Distinct from cramped-padding: that rule needs the element itself to + // have direct text (hasDirectText). This rule targets the OPPOSITE + // shape — a container with NO direct text, only children — which is + // exactly what cramped-padding misses (a section wrapping a label + + // list lands a free pass). + // + // The classic shape: agent writes `padding: 28px 0 0` shorthand on a + // section that also has a border, zeroing horizontal padding so the + // text-bearing children touch the side borders. Background and + // outline count too: a colored card with zero padding has the same + // visual failure mode. + { + const FLUSH_SKIP_TAGS = new Set(['HTML', 'BODY', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ARTICLE', 'ASIDE', 'BUTTON', 'A', 'LABEL', 'SUMMARY', 'CODE', 'PRE', 'INPUT', 'TEXTAREA', 'SELECT', 'FORM', 'FIGURE', 'TABLE', 'TBODY', 'THEAD', 'TR', 'TD', 'TH']); + const upperTag = tag ? tag.toUpperCase() : ''; + const elPosition = style.position || ''; + if ( + !FLUSH_SKIP_TAGS.has(upperTag) && + !hasDirectText && + !['fixed', 'absolute'].includes(elPosition) && + el.children && el.children.length > 0 + ) { + const isTransparent = (c) => + !c || c === 'transparent' || c === 'rgba(0, 0, 0, 0)' || + /^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*0(?:\.0+)?\s*\)$/.test(c); + + const borderW = { + top: parseFloat(style.borderTopWidth) || 0, + right: parseFloat(style.borderRightWidth) || 0, + bottom: parseFloat(style.borderBottomWidth) || 0, + left: parseFloat(style.borderLeftWidth) || 0, + }; + const borderVisible = { + top: borderW.top > 0 && !isTransparent(style.borderTopColor), + right: borderW.right > 0 && !isTransparent(style.borderRightColor), + bottom: borderW.bottom > 0 && !isTransparent(style.borderBottomColor), + left: borderW.left > 0 && !isTransparent(style.borderLeftColor), + }; + // Outline detection. jsdom decomposes `border` shorthand into + // border{Top,…}Width/Color but does NOT decompose `outline` — + // the longhands come back empty when the value was set via the + // shorthand. Fall back to parsing `style.outline` ourselves. + let outlineW = parseFloat(style.outlineWidth) || 0; + let outlineStyleVal = style.outlineStyle || ''; + let outlineColorVal = style.outlineColor || ''; + if (!outlineW && style.outline) { + const wMatch = style.outline.match(/(\d+(?:\.\d+)?)\s*px/); + if (wMatch) outlineW = parseFloat(wMatch[1]) || 0; + if (!outlineStyleVal) { + outlineStyleVal = /\b(solid|dashed|dotted|double|groove|ridge|inset|outset)\b/.test(style.outline) ? 'solid' : ''; + } + if (!outlineColorVal) { + const cMatch = style.outline.match(/(rgba?\([^)]+\)|#[0-9a-fA-F]{3,8}|[a-zA-Z]+)\s*$/); + if (cMatch) outlineColorVal = cMatch[1]; + } + } + const outlineVisible = outlineW > 0 && !isTransparent(outlineColorVal) && outlineStyleVal && outlineStyleVal !== 'none'; + const bgVisible = !isTransparent(style.backgroundColor); + + const anyVisible = borderVisible.top || borderVisible.right || borderVisible.bottom || borderVisible.left || outlineVisible || bgVisible; + if (anyVisible) { + // Resolve padding to px (jsdom returns raw "1.5rem" etc., not the + // computed px value; parseFloat would strip the unit and treat + // 1.5rem as 1.5px, false-flagging legitimate insets). + const pad = { + top: resolveLengthPx(style.paddingTop, fontSize) ?? 0, + right: resolveLengthPx(style.paddingRight, fontSize) ?? 0, + bottom: resolveLengthPx(style.paddingBottom, fontSize) ?? 0, + left: resolveLengthPx(style.paddingLeft, fontSize) ?? 0, + }; + const PAD_THRESHOLD = 2; + // Children-insulate-this-side: a side is insulated if ANY direct + // child has its own padding ≥ 4px on that side. Rationale: in + // typical flow, only the first/last (or leftmost/rightmost) + // children actually sit at the parent's edges. If even one of + // them has its own padding, the visual flush is broken on that + // side. Classic example: a column-flow card frame where the + // top child (header) has padding-top:12 and the bottom child + // (footer) has padding-bottom:8 — the parent's padding:0 doesn't + // matter; nothing is actually flush. The `any-child-insulates` + // heuristic accepts some false negatives (a card with one heavily + // padded middle child won't flag) for far fewer false positives. + const CHILD_INSULATE_THRESHOLD = 4; + const childrenInsulate = { top: false, right: false, bottom: false, left: false }; + for (const child of el.children) { + let childStyle = null; + if (win && typeof win.getComputedStyle === 'function') { + try { childStyle = win.getComputedStyle(child); } catch {} + } + if (!childStyle && typeof getComputedStyle === 'function') { + try { childStyle = getComputedStyle(child); } catch {} + } + if (!childStyle) continue; + const childPad = { + top: resolveLengthPx(childStyle.paddingTop, fontSize) ?? 0, + right: resolveLengthPx(childStyle.paddingRight, fontSize) ?? 0, + bottom: resolveLengthPx(childStyle.paddingBottom, fontSize) ?? 0, + left: resolveLengthPx(childStyle.paddingLeft, fontSize) ?? 0, + }; + for (const s of ['top', 'right', 'bottom', 'left']) { + if (childPad[s] >= CHILD_INSULATE_THRESHOLD) childrenInsulate[s] = true; + } + } + + const flushSides = []; + for (const side of ['top', 'right', 'bottom', 'left']) { + const sideBounded = borderVisible[side] || outlineVisible || bgVisible; + if (sideBounded && pad[side] <= PAD_THRESHOLD && !childrenInsulate[side]) { + flushSides.push(side); + } + } + + if (flushSides.length > 0) { + // Confirm at least one direct child has substantial text content + // (> 4 chars). Without this, the flush is harmless: e.g. an + // image-only card. + let hasTextChild = false; + for (const child of el.children) { + const childText = (child.textContent || '').trim(); + if (childText.length > 4) { hasTextChild = true; break; } + } + if (hasTextChild) { + const cls = (typeof el.className === 'string' && el.className.trim()) + ? el.className.trim().split(/\s+/)[0] + : ''; + const boundaryParts = []; + const borderSidesVisible = ['top', 'right', 'bottom', 'left'].filter(s => borderVisible[s]); + if (borderSidesVisible.length === 4) boundaryParts.push('border'); + else if (borderSidesVisible.length > 0) boundaryParts.push(`border-${borderSidesVisible.join('/')}`); + if (outlineVisible) boundaryParts.push('outline'); + if (bgVisible) boundaryParts.push('bg'); + const sidesLabel = flushSides.length === 4 ? 'all sides' : flushSides.join('/'); + const ident = cls + ? `<${tag.toLowerCase()}> "${cls}"` + : `<${tag.toLowerCase()}>`; + findings.push({ + id: 'cramped-padding', + snippet: `${ident}: children flush against ${boundaryParts.join('+')} on ${sidesLabel} (no inset)`, + }); + } + } + } + } + } + // --- Body text touching viewport edge --- (browser-only: needs rect) // Catches the failure mode where the agent ships body paragraphs // with NO container providing horizontal padding — text bleeds @@ -1804,6 +2093,20 @@ function checkQuality(opts) { } } + // --- Crushed letter spacing (mirror of wide-tracking) --- + // Tracking pulled tighter than ~-0.05em crushes characters into each other. + // Optical tightening that display type legitimately wants (around -0.02em) + // stays well above this floor. + if (hasDirectText && textLen > 20 && fontSize > 0) { + if (letterSpacingPx != null && letterSpacingPx < 0) { + const trackingEm = letterSpacingPx / fontSize; + if (trackingEm <= -0.05) { + const excerpt = (el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 40); + findings.push({ id: 'extreme-negative-tracking', snippet: `letter-spacing: ${trackingEm.toFixed(2)}em — "${excerpt}"` }); + } + } + } + return findings; } @@ -1820,7 +2123,7 @@ function checkElementQualityDOM(el) { const rect = el.getBoundingClientRect(); const lineMax = (typeof window !== 'undefined' && window.__IMPECCABLE_CONFIG__?.lineLengthMax) || 80; const viewportWidth = (typeof window !== 'undefined' ? window.innerWidth : 0) || 0; - return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax, viewportWidth }); + return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax, viewportWidth, win: typeof window !== 'undefined' ? window : null }); } // Pure page-level skipped-heading walk. Takes a Document so it works in both @@ -1862,7 +2165,7 @@ function checkElementQuality(el, style, tag, window) { const fontSize = resolveFontSizePx(el, window); const lineHeightPx = resolveLengthPx(style.lineHeight, fontSize); const letterSpacingPx = resolveLengthPx(style.letterSpacing, fontSize); - return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect: null }); + return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect: null, win: window }); } function checkElementBorders(tag, style, overrides, resolvedRadius) { @@ -2303,40 +2606,213 @@ function checkPageLayout(doc, win) { } } - // Everything centered - const textEls = doc.querySelectorAll('h1, h2, h3, h4, h5, h6, p, li, div, button'); - let centeredCount = 0; - let totalText = 0; - for (const el of textEls) { - const hasDirectText = [...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim().length >= 3); - if (!hasDirectText) continue; - totalText++; - - let cur = el; - let isCentered = false; - while (cur && cur.nodeType === 1) { - const rawStyle = cur.getAttribute?.('style') || ''; - const cls = cur.getAttribute?.('class') || ''; - if (/text-align\s*:\s*center/i.test(rawStyle) || /\btext-center\b/.test(cls)) { - isCentered = true; - break; - } - if (cur.tagName === 'BODY') break; - cur = cur.parentElement; - } - if (isCentered) centeredCount++; - } - - if (totalText >= 5 && centeredCount / totalText > 0.7) { - findings.push({ - id: 'everything-centered', - snippet: `${centeredCount}/${totalText} text elements centered (${Math.round(centeredCount / totalText * 100)}%)`, - }); - } - return findings; } +// ─── Cream / beige palette (the default "tasteful" AI surface) ──────────────── +// A warm, lightly-tinted off-white page background — light, with R≥G≥B and a +// small warm tint (not white, not a strong color). The current reflex surface. +function isCreamColor(rgb) { + if (!rgb) return false; + const { r, g, b } = rgb; + if (Math.min(r, g, b) < 209) return false; // must be light + if (!(r >= g && g >= b)) return false; // warm ordering + const warmth = r - b; + return warmth >= 6 && warmth <= 48; // tinted, not white, not strong +} + +// Tailwind background utilities that render as a warm off-white surface. The +// static engine doesn't fetch Tailwind's CSS, so a `bg-amber-50` on +// resolves to nothing in computed style — catch it from the class list +// instead. Candidate tokens map to their actual Tailwind hex and are still +// filtered through isCreamColor, so neutral grays (stone) and over-saturated +// shades drop out on their own. +const TAILWIND_BG_HEX = { + 'bg-amber-50': '#fffbeb', 'bg-amber-100': '#fef3c7', + 'bg-orange-50': '#fff7ed', 'bg-orange-100': '#ffedd5', + 'bg-yellow-50': '#fefce8', + 'bg-stone-50': '#fafaf9', 'bg-stone-100': '#f5f5f4', 'bg-stone-200': '#e7e5e4', +}; + +function creamFromClassList(cls) { + if (!cls) return null; + // Arbitrary value: bg-[#f5f0e6] / bg-[rgb(245_240_230)] (underscores = spaces). + const arb = cls.match(/\bbg-\[([^\]]+)\]/); + if (arb && isCreamColor(parseAnyColor(arb[1].replace(/_/g, ' ')))) return `bg-[${arb[1]}]`; + // Named warm-light utilities. + for (const [tok, hex] of Object.entries(TAILWIND_BG_HEX)) { + if (new RegExp(`(^|\\s)${tok}($|\\s)`).test(cls) && isCreamColor(parseAnyColor(hex))) return tok; + } + return null; +} + +function checkCreamPalette(doc, win) { + const findings = []; + const body = doc.body || (doc.querySelector ? doc.querySelector('body') : null); + if (!body) return findings; + const html = doc.documentElement; + const getCS = (el) => (win ? win.getComputedStyle(el) : getComputedStyle(el)); + + // 1. Computed background — covers inline / close.', 'Prefix every preview selector with the matching [data-impeccable-variant="N"] selector.', 'Keep selectors anchored to the generated variant wrapper; do not rely on component CSS scoping for preview rules.', ], forbidden: [ 'Do not use @scope for this styleMode.', + 'Do not wrap style content in a JSX/TSX template literal ({` ... `}); that syntax is for .tsx/.jsx only.', + 'Do not put { immediately after the style opening tag; Astro parses { as expression syntax.', ], }; } @@ -629,4 +632,15 @@ if (_running?.endsWith('live-wrap.mjs') || _running?.endsWith('live-wrap.mjs/')) } // Test exports (used by tests/live-wrap.test.mjs) -export { buildSearchQueries, findElement, findClosingLine, detectCommentSyntax }; +export { + buildSearchQueries, + findElement, + findClosingLine, + detectCommentSyntax, + findAllElements, + filterByText, + findFileWithQuery, + detectStyleMode, + buildCssAuthoring, + buildCssSelectorPrefixExamples, +}; diff --git a/.agents/skills/impeccable/scripts/live.mjs b/.agents/skills/impeccable/scripts/live.mjs index cafb0eca9..8acd300ed 100644 --- a/.agents/skills/impeccable/scripts/live.mjs +++ b/.agents/skills/impeccable/scripts/live.mjs @@ -21,7 +21,7 @@ import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { loadContext } from './load-context.mjs'; +import { loadContext } from './context.mjs'; import { resolveFiles } from './live-inject.mjs'; import { readLiveServerInfo } from './impeccable-paths.mjs'; @@ -80,7 +80,7 @@ The agent should then: process.exit(1); } - // 4. Load PRODUCT.md + DESIGN.md context (auto-migrates legacy .impeccable.md) + // 4. Load PRODUCT.md + DESIGN.md context. const ctx = loadContext(process.cwd()); // 5. Compute drift-heal: compare resolved inject targets against the @@ -102,7 +102,6 @@ The agent should then: hasDesign: ctx.hasDesign, design: ctx.design, designPath: ctx.designPath, - migrated: ctx.migrated, }, null, 2)); } diff --git a/.agents/skills/impeccable/scripts/load-context.mjs b/.agents/skills/impeccable/scripts/load-context.mjs deleted file mode 100644 index dc340bf16..000000000 --- a/.agents/skills/impeccable/scripts/load-context.mjs +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Shared context loader for every impeccable command that needs to know - * "who is this for" and "what does this look like". - * - * Input: project root (process.cwd()). - * - * Output (JSON to stdout): - * { - * hasProduct: boolean, // PRODUCT.md found (or auto-migrated) - * product: string | null, // PRODUCT.md contents - * productPath: string | null, // relative path - * hasDesign: boolean, // DESIGN.md found - * design: string | null, // DESIGN.md contents - * designPath: string | null, - * migrated: boolean, // true if we auto-renamed .impeccable.md -> PRODUCT.md - * contextDir: string, // absolute path of the directory the files were found in - * } - * - * Filename matching is case-insensitive for PRODUCT.md and DESIGN.md. The - * Google DESIGN.md convention is uppercase at repo root; Kiro-style and - * lowercase variants are also matched so users don't get punished for case. - * - * Lookup directory resolution (first match wins): - * 1. process.env.IMPECCABLE_CONTEXT_DIR (absolute or relative to cwd) - * 2. cwd, if PRODUCT.md / DESIGN.md / .impeccable.md is there (back-compat) - * 3. Auto-fallback subdirectories of cwd: .agents/context/, then docs/ - * 4. cwd as a default "no context found" location - * - * Legacy `.impeccable.md` -> PRODUCT.md migration only fires at cwd root; - * fallback directories are read-only as far as auto-rename is concerned. - */ - -import fs from 'node:fs'; -import path from 'node:path'; - -const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; -const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; -const LEGACY_NAMES = ['.impeccable.md']; -const FALLBACK_DIRS = ['.agents/context', 'docs']; - -/** - * Resolve the directory that holds PRODUCT.md / DESIGN.md for - * this project. Exported so other scripts (e.g. live-server.mjs) can read the - * design files from the same location the loader uses. - */ -export function resolveContextDir(cwd = process.cwd()) { - // 1. Explicit override - const envDir = process.env.IMPECCABLE_CONTEXT_DIR; - if (envDir && envDir.trim()) { - const trimmed = envDir.trim(); - return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); - } - - // 2. cwd wins if any canonical or legacy file is there. We check legacy too - // so the auto-migration path in loadContext stays predictable. - if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES, ...LEGACY_NAMES])) { - return cwd; - } - - // 3. Auto-fallback subdirs. Match if PRODUCT.md or DESIGN.md is present; - // legacy `.impeccable.md` does not pull the lookup into a fallback dir. - for (const rel of FALLBACK_DIRS) { - const candidate = path.resolve(cwd, rel); - if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { - return candidate; - } - } - - // 4. Nothing found — keep the historical "default to cwd" behaviour so the - // caller's `hasProduct === false` branch still fires the same way. - return cwd; -} - -export function loadContext(cwd = process.cwd()) { - let migrated = false; - const contextDir = resolveContextDir(cwd); - - // 1. Look for PRODUCT.md (case-insensitive) in the resolved dir - let productPath = firstExisting(contextDir, PRODUCT_NAMES); - - // 2. Legacy: if no PRODUCT.md but .impeccable.md exists at cwd root, rename - // it in place. We only migrate at the root — fallback dirs are read-only - // so we don't surprise users by mutating files under docs/ or .agents/. - if (!productPath && contextDir === cwd) { - const legacyPath = firstExisting(cwd, LEGACY_NAMES); - if (legacyPath) { - const newPath = path.join(cwd, 'PRODUCT.md'); - try { - fs.renameSync(legacyPath, newPath); - productPath = newPath; - migrated = true; - } catch { - // Rename failed (permissions, etc.) — fall back to reading legacy in place - productPath = legacyPath; - } - } - } - - // 3. DESIGN.md (case-insensitive) - const designPath = firstExisting(contextDir, DESIGN_NAMES); - - const product = productPath ? safeRead(productPath) : null; - const design = designPath ? safeRead(designPath) : null; - - return { - hasProduct: !!product, - product, - productPath: productPath ? path.relative(cwd, productPath) : null, - hasDesign: !!design, - design, - designPath: designPath ? path.relative(cwd, designPath) : null, - migrated, - contextDir, - }; -} - -function firstExisting(dir, names) { - for (const name of names) { - const abs = path.join(dir, name); - if (fs.existsSync(abs)) return abs; - } - return null; -} - -function safeRead(p) { - try { return fs.readFileSync(p, 'utf-8'); } catch { return null; } -} - -// --------------------------------------------------------------------------- -// CLI mode — print the context as JSON -// --------------------------------------------------------------------------- - -function cli() { - const result = loadContext(process.cwd()); - console.log(JSON.stringify(result, null, 2)); -} - -const _running = process.argv[1]; -if (_running?.endsWith('load-context.mjs') || _running?.endsWith('load-context.mjs/')) { - cli(); -} diff --git a/.agents/skills/impeccable/scripts/palette.mjs b/.agents/skills/impeccable/scripts/palette.mjs new file mode 100644 index 000000000..4c3f1751a --- /dev/null +++ b/.agents/skills/impeccable/scripts/palette.mjs @@ -0,0 +1,633 @@ +#!/usr/bin/env node +/** + * Brand-seed picker. Returns one OKLCH seed color + the mood it most + * naturally evokes, and teaches the model how to compose a full palette + * around it. + * + * The seed is the brand's anchor color. The 5-role palette (bg, surface, + * ink, accent, muted) is composed by the caller at runtime using their + * judgment + the brief (PRODUCT.md / DESIGN.md / user prompt), NOT picked + * from a frozen 4-color preset. + * + * Why: 4-color frozen palettes drift toward safe defaults (warm-cream bg, + * complementary accent on near-white) regardless of brief. A single seed + + * the model's own composition lets the same seed produce a dark-mode jazz + * club or a light-mode hospitality brand depending on what the brief calls + * for. Tested empirically against curated 4-color palettes; seed approach + * wins on mood-fit in 3 of 5 cases and ties on the rest. + * + * Usage: + * node scripts/palette.mjs # pick at random + * node scripts/palette.mjs --id seed-021 # pick a specific seed + * node scripts/palette.mjs --from # hash to a seed (deterministic) + * + * Env vars: + * IMPECCABLE_PALETTE_SEED — same as --from; useful for the eval harness + * to make runs reproducible. + */ + +import crypto from 'node:crypto'; + +// Seeds are inlined (129 entries, hand-curated via a tinder review of +// ~400 candidates from ColorHunt + synthesis + Radix/brand/Pantone anchors). +// Each carries a mood + strategy the judging model produced — surfaced as +// hints, not commands; the brief still drives composition. +const SEEDS = [ + { id: "seed-200", oklch: [0.360, 0.137, 0.0], + mood: "Aesop apothecary shelf — oxblood bottle glass against linen, considered and unhurried", + strategy: "Seed is a deep desaturated red-brown that reads as brand ink itself; I push primary darker toward bottle-glass oxblood, pair with a pure white surface so the red does the work, and use a clear pale-blush accent that can carry dark text in pills." }, + { id: "seed-000", oklch: [0.400, 0.130, 0.0], + mood: "oxblood leather banquette in a 1940s steakhouse — low lamplight on dark wood and burgundy", + strategy: "Near-black bg with the faintest red undertone lets the oxblood primary glow like lamplit leather; warm cream ink and a brass accent complete the chophouse register." }, + { id: "seed-002", oklch: [0.450, 0.150, 0.0], + mood: "darkroom red light — analog photography, blood-warm safelight glow on chemical trays", + strategy: "Near-black surface with a deep oxblood primary lets the seed function like a safelight in a darkroom — the bg disappears so the red becomes the only emotional signal." }, + { id: "seed-003", oklch: [0.500, 0.194, 0.0], + mood: "darkroom safelight — the deep oxblood glow of analog photography, chemical and contemplative", + strategy: "Anchored the seed as primary against pure near-black so the red reads like a single illuminated bulb in a developing room, with cool desaturated ink to evoke silver gelatin print tones." }, + { id: "seed-004", oklch: [0.546, 0.204, 3.4], + mood: "midnight boudoir — velvet rose under low lamplight, perfumed and intimate", + strategy: "Near-black surface lets the rose seed glow like silk in shadow; a warm champagne accent provides the candle-flame counterpoint without breaking the hush." }, + { id: "seed-005", oklch: [0.550, 0.180, 0.0], + mood: "smoldering vermillion at dusk — the last red ember in a blacksmith's forge, iron-rich and quietly violent", + strategy: "Near-black gallery surround lets the seed read as glowing forged metal; ink stays warm-off-white, accent shifts to a hotter ember orange so the primary feels like cooling steel against a fresh strike." }, + { id: "seed-201", oklch: [0.647, 0.262, 0.3], + mood: "Figma plugin marketplace red — confident product-brand crimson, the kind a modern dev tool uses for a 'live' indicator or a primary CTA on a pristine docs page", + strategy: "Pure white surface lets a high-chroma crimson primary do all the brand work, paired with a hue-shifted warm coral accent for hierarchy without competing saturation" }, + { id: "seed-006", oklch: [0.650, 0.160, 0.0], + mood: "1960s Italian cinema — Technicolor lipstick red against a darkened theater", + strategy: "Pure near-black surface lets a saturated cinematic red and its warm peach accent perform like film light projected in a dark room — the brand colors carry the drama, the bg disappears." }, + { id: "seed-008", oklch: [0.520, 0.200, 10.4], + mood: "Negroni hour at a Milanese bar — bittersweet crimson, vermouth and amaro under low tungsten", + strategy: "Seed is a saturated red-crimson with cinematic weight, so I sit it on near-black to let the primary glow like backlit liquor, with a warmer amber accent acting as the citrus twist against the bitter red." }, + { id: "seed-010", oklch: [0.563, 0.223, 11.0], + mood: "Negroni hour on a Milan rooftop — bittersweet crimson, aperitivo light, polished restraint", + strategy: "Seed is a vivid carmine-red with strong chroma, so the surface gets out of the way (pure white) and lets the primary do the aperitivo work, with a cooled garnet accent for tension." }, + { id: "seed-202", oklch: [0.643, 0.247, 7.0], + mood: "Glossier brand pink — modern beauty editorial, confident and current", + strategy: "Pure white bg lets a saturated rose-red primary do all the brand work, paired with a deeper crimson accent for hierarchy — the Stripe/Glossier move where the color carries the mood." }, + { id: "seed-013", oklch: [0.400, 0.130, 20.0], + mood: "Tuscan cellar at dusk — aged terracotta, oxidized iron, the deep red of decanted Sangiovese", + strategy: "Black surface lets the oxblood seed and copper accent glow like firelight on cellar stone; brand colors carry all the warmth while the room recedes." }, + { id: "seed-014", oklch: [0.450, 0.150, 20.0], + mood: "smoldering tannery — oxblood leather, cured under low workshop light", + strategy: "Anchor the deep oxblood seed as primary against a near-black architectural ground, then lift with a single warm ember accent so the leather reads burnished rather than bloody." }, + { id: "seed-016", oklch: [0.550, 0.180, 20.0], + mood: "Negroni hour on a Roman terrace — bitter campari red, vermouth, late golden light spilling on white linen", + strategy: "Pure white surface lets the campari-red primary do all the emotional work, paired with a deeper oxblood accent for bittersweet depth — Italian aperitivo restraint, not warmth-washed." }, + { id: "seed-205", oklch: [0.634, 0.254, 17.6], + mood: "Aesop apothecary bottle — considered red-coral on a clinical white surface, the kind of brand restraint where one saturated object does all the work", + strategy: "Default A pure white surface lets a single coral-red primary carry the entire brand voice; accent shifts to a deeper oxblood for hierarchy without competing chroma." }, + { id: "seed-011", oklch: [0.639, 0.207, 13.5], + mood: "Aperitivo hour in Milan — Campari glow on a white marble bar, crisp and effervescent", + strategy: "Pure white gallery backdrop lets the Campari-red primary ring like a single bitter note; ink is near-black with a whisper of warmth, accent shifts to a deeper oxblood for hierarchy without competing hues." }, + { id: "seed-015", oklch: [0.527, 0.202, 22.7], + mood: "Negroni hour on a Milanese terrace — bittersweet vermillion, aperitivo glassware catching low sun", + strategy: "Seed becomes a saturated aperitivo-red primary against pure white so the color carries the bittersweet warmth alone, paired with a deep oxblood accent for typographic gravitas." }, + { id: "seed-023", oklch: [0.427, 0.175, 29.2], + mood: "blacksmith's forge at dusk — iron heated to ember red, the deep glow of oxidized metal and quenching oil", + strategy: "Pure black bg lets the seed's ember-red glow radiate like hot iron in a dark forge; accent shifts to a copper-amber to suggest scaling metal and sparks, while ink stays near-white for tool-precise legibility." }, + { id: "seed-206", oklch: [0.614, 0.234, 28.2], + mood: "Aesop apothecary bottle — considered red-orange on lab-white, calm utility with a single confident pigment", + strategy: "Pure white surface lets a saturated vermilion primary do all the brand work, paired with a deep oxblood accent for hierarchy without introducing a second hue family" }, + { id: "seed-029", oklch: [0.665, 0.222, 25.7], + mood: "Negroni hour at a Milanese bar — bittersweet orange-red liqueur catching late afternoon light on polished marble", + strategy: "Pure white surface lets the seed's vermilion read like Campari in a glass; a deeper oxblood accent provides the bitter depth, with neutral graphite ink keeping the editorial restraint of Italian design." }, + { id: "seed-022", oklch: [0.418, 0.155, 27.2], + mood: "Pompeiian red fresco — oxidized cinnabar on a museum wall, archaeological gravity", + strategy: "Pure black gallery surface lets the seed's iron-oxide red read as a lit artifact; accent shifts to an aged terracotta amber, so primary and accent form a fired-clay duet against neutral void." }, + { id: "seed-024", oklch: [0.464, 0.169, 26.9], + mood: "Mid-century darkroom under the safelight — developer trays, oxblood leather, the quiet patience of a print emerging", + strategy: "Seed becomes a deep oxblood primary; surface stays pure black so the red glows like a safelight, with a warmer ember accent for hierarchy" }, + { id: "seed-026", oklch: [0.489, 0.190, 28.3], + mood: "smoldering ember in a blacksmith's forge — iron-hot rust, soot, and controlled fire", + strategy: "Near-black soot background lets the seed's red-orange glow like heated metal; ink is bone-white, accent is a cooler tempered-steel orange that creates internal heat gradient with the primary." }, + { id: "seed-027", oklch: [0.568, 0.208, 27.1], + mood: "Sicilian blood orange at golden hour — citrus rind, terracotta, sun on stucco", + strategy: "Seed reads as vivid blood-orange — picked pure white surface so the citrus-red primary and a deep oxblood accent do all the emotional work, like a Loro Piana editorial spread." }, + { id: "seed-028", oklch: [0.591, 0.172, 24.0], + mood: "Sienna-fired ceramic studio at dusk — terracotta cooling on a wheel, hands still dusted with slip", + strategy: "Pure black stage lets the fired-clay primary glow like a kiln ember, with a deeper oxblood accent providing tonal weight rather than hue contrast — a monochrome warm-axis play." }, + { id: "seed-033", oklch: [0.544, 0.169, 31.3], + mood: "1960s Italian terracotta workshop — fired clay, espresso, late-afternoon Mediterranean dust", + strategy: "Pure black ground lets the seed's burnt-sienna primary glow like a lit kiln, with a deeper oxblood accent for restrained warmth tension — the brand carries the heat, the surface stays out." }, + { id: "seed-207", oklch: [0.564, 0.231, 29.1], + mood: "Aesop apothecary bottle — considered red oxide, the calm authority of a well-made object on a white shelf", + strategy: "Seed becomes the singular brand voice against pure white, with a deeper oxblood accent for hierarchy — the surface disappears so the red does all the speaking." }, + { id: "seed-035", oklch: [0.663, 0.153, 32.1], + mood: "Aesop apothecary bottle — clay-fired warmth, considered retail", + strategy: "Pure white surface lets the terracotta primary do the brand work, paired with a deep umber ink and a cooler clay accent for editorial tension." }, + { id: "seed-037", oklch: [0.590, 0.188, 35.8], + mood: "Aesop apothecary bottle — considered terracotta, herbalist restraint, the warmth comes from the glass not the room", + strategy: "Seed becomes a muted terracotta primary against pure white so the brand's warmth carries entirely through the color itself; accent shifts to a deeper umber for quiet hierarchy." }, + { id: "seed-038", oklch: [0.652, 0.229, 34.8], + mood: "blown-glass furnace at dusk — molten orange iron pulled from the kiln, a craftsman's signature heat", + strategy: "Pure black stage so the seed reads as live ember; primary holds the seed's heat, accent shifts to a brass-amber a hue-step away for a 1.7+ contrast pairing without leaving the fire." }, + { id: "seed-039", oklch: [0.653, 0.185, 33.5], + mood: "Aesop apothecary bottle — considered terracotta, quiet retail craft", + strategy: "Seed becomes a grounded clay primary against pure white, paired with a deeper umber accent so the warmth lives entirely in the brand marks, not the surface." }, + { id: "seed-167", oklch: [0.495, 0.134, 36.0], + mood: "Aesop apothecary shelf — burnished terracotta on clinical white, considered craft pharmacy", + strategy: "Treat the seed as a brand-carrying burnt-sienna against a pure paper-white surface so the warmth lives entirely in the primary, with a deep umber accent pulled along the same warm axis for typographic gravity." }, + { id: "seed-147", oklch: [0.500, 0.151, 40.0], + mood: "Aesop apothecary shelf — considered terracotta, pharmacy restraint, the brand color does the work against clinical white", + strategy: "Anchor the seed's burnt-sienna primary against a pure white surface so the rust speaks alone, with a deep umber ink and a cooler clay accent to give the palette product-brand discipline rather than environmental warmth." }, + { id: "seed-040", oklch: [0.660, 0.201, 40.0], + mood: "Aesop apothecary bottle — amber glass on a clean dispensary shelf, considered and clinical-warm", + strategy: "Seed becomes a burnt-amber primary against pure white so the bottle-glass color does the emotional work; accent shifts to a deep olive-bronze for the apothecary-label pairing." }, + { id: "seed-041", oklch: [0.673, 0.217, 38.6], + mood: "Aesop apothecary shelf — considered orange glass, clinical retail restraint", + strategy: "Pure white surface lets the burnt-orange primary do all the brand work, with a deep ink-brown for editorial gravity and a muted clay accent that reads as a sibling, not a contrast." }, + { id: "seed-042", oklch: [0.688, 0.133, 35.8], + mood: "Aesop apothecary shelf — terracotta glass, considered retail", + strategy: "Seed becomes a warm clay primary against pure white so the bottle-on-marble retail feel comes from the brand color alone; a deeper umber accent gives the label-print contrast." }, + { id: "seed-043", oklch: [0.781, 0.119, 38.1], + mood: "Aesop apothecary catalogue — considered terracotta, dermatological restraint, the warm color doing all the work against clinical white", + strategy: "Pure white surface lets the seed's warm clay tone read as the entire brand voice, paired with a deeper umber accent for hierarchy without competing with the primary's warmth." }, + { id: "seed-168", oklch: [0.400, 0.103, 50.0], + mood: "Aesop apothecary bottle — amber glass on a clinical white shelf, considered and pharmaceutical", + strategy: "Pure white surface lets the deep amber primary act like tinted glass against a clean shelf; accent is a muted clay that complements without competing, keeping the brand quiet and product-led." }, + { id: "seed-044", oklch: [0.568, 0.149, 45.9], + mood: "1970s desert highway at golden hour — sun-faded terracotta, denim dust, the warmth of a Polaroid pulled from a glovebox", + strategy: "Seed becomes a burnt-sienna primary against pure white so the terracotta does all the emotional work; a deep indigo accent acts as the denim shadow opposing the sun, creating the era's signature warm/cool tension without tinting the page." }, + { id: "seed-045", oklch: [0.607, 0.163, 47.7], + mood: "Aesop apothecary shelf — considered amber glass, clinical restraint, craft pharmacy", + strategy: "Pure white bg lets the burnt-amber primary do the apothecary work alone, paired with a deeper umber accent and graphite ink for editorial calm." }, + { id: "seed-046", oklch: [0.653, 0.175, 45.0], + mood: "Aesop apothecary shelf — considered amber glass, quiet luxury, restrained craft", + strategy: "Pure black backdrop lets the warm amber primary glow like backlit apothecary glass, with a deeper rust accent providing tonal depth in the same hue family — monochromatic warm against neutral void." }, + { id: "seed-047", oklch: [0.695, 0.205, 43.2], + mood: "Aesop apothecary label — sun-warmed amber glass on a clinical countertop, restrained botanical pharmacy", + strategy: "Pure white surface lets the burnt-amber primary and a deeper sienna accent do all the brand work, like an apothecary bottle photographed under daylight." }, + { id: "seed-051", oklch: [0.704, 0.189, 49.0], + mood: "blacksmith's forge at dusk — glowing iron, hammered copper, ember light against cooling steel", + strategy: "Pure near-black surface lets the seed's molten orange burn like heated metal; accent shifts to a deeper amber-red to suggest the cooling end of the same iron, while ink stays a clean off-white so type reads like chalk on slate." }, + { id: "seed-171", oklch: [0.550, 0.124, 60.0], + mood: "Klim Type Foundry specimen page — considered ochre on paper, design-school-honest", + strategy: "Seed becomes a muted ochre primary on pure white; accent is a deep ink-navy pulled across the wheel for editorial contrast without warmth-pooling in the bg" }, + { id: "seed-148", oklch: [0.650, 0.146, 60.0], + mood: "Klim-style editorial gold — late-afternoon paper light on a serif specimen sheet, considered and dry", + strategy: "Hold the seed's amber as primary on a pure white page so the gold reads as ink rather than atmosphere, and pair with a deep aubergine accent for typographic contrast." }, + { id: "seed-052", oklch: [0.700, 0.130, 60.0], + mood: "late-afternoon terracotta studio — sun-warmed clay, hands-on craft, the hour before dusk", + strategy: "Seed is a saturated amber-ochre with strong environmental association (ceramics, adobe, sunlit plaster), so I lean into Exception (a) with a faintly warm bone surface that reads as lime-washed wall, then deepen the seed slightly for primary and pair it with a fired-clay rust accent for hand-thrown warmth." }, + { id: "seed-053", oklch: [0.773, 0.157, 56.6], + mood: "late-summer apricot orchard at golden hour — sun-warmed fruit, considered Californian craft", + strategy: "Seed is a juicy mid-warm orange at daylight luminance — leaning optimistic/editorial, so pure white surface lets the apricot primary glow without muddying it; a deep wine accent provides the bite." }, + { id: "seed-149", oklch: [0.600, 0.124, 70.0], + mood: "1970s desert highway — late-afternoon amber light on chrome and asphalt", + strategy: "Anchor the amber seed as primary against pure black so the warm hue reads as headlight glow against night; a cooler dusk-mauve accent provides the complementary tension of horizon vs. sun." }, + { id: "seed-054", oklch: [0.740, 0.162, 68.1], + mood: "late-afternoon honey on terracotta — Mediterranean stucco at golden hour, sun-baked amber", + strategy: "Seed is a saturated honey-amber at high lightness; pairing it with pure black lets the warmth read as luminous gold against gravity, like lamplight in a dark room." }, + { id: "seed-055", oklch: [0.774, 0.174, 65.1], + mood: "late-summer honey hour — amber light slanting through a west-facing window, optimistic and golden", + strategy: "Anchor a saturated honey-amber primary on pure white so the warmth radiates from the brand itself, then pair with a deep teak accent for grounded contrast rather than tinting the canvas." }, + { id: "seed-056", oklch: [0.691, 0.146, 74.6], + mood: "Klim-style modern publishing house — late-afternoon paper warmth, considered editorial gold", + strategy: "Pure white surface so the amber seed becomes the brand voice; ink stays near-black neutral and accent shifts to a deep ink-blue to give the gold something structural to lean on." }, + { id: "seed-150", oklch: [0.750, 0.148, 80.0], + mood: "Klim Type Foundry specimen page — late-summer editorial gold, considered and grown-up", + strategy: "Pure white surface lets a single restrained ochre primary do all the brand work, paired with a deep ink-blue accent for typographic contrast in the Klim/Commercial Type tradition." }, + { id: "seed-058", oklch: [0.764, 0.120, 77.1], + mood: "Klim Type Foundry specimen page — late-afternoon ochre, considered editorial typography", + strategy: "Pure white surface lets the ochre primary do the brand work, paired with a deep ink-blue accent for editorial contrast — the type-foundry move where one warm hue carries the whole feeling against neutral paper." }, + { id: "seed-059", oklch: [0.784, 0.144, 79.8], + mood: "late afternoon in a Tuscan limonaia — sun-cured amber on whitewashed plaster", + strategy: "Pure white surface lets the saffron-amber primary and a deep olive accent carry the Mediterranean warmth, with split-complementary tension between gold and a quiet evergreen." }, + { id: "seed-061", oklch: [0.817, 0.161, 75.1], + mood: "late-afternoon honey on Tuscan limestone — golden hour, slow and luminous", + strategy: "Pure white surface lets the amber primary glow like sunlight on a wall, paired with a deep terracotta accent for warm tonal contrast within the same hue family." }, + { id: "seed-063", oklch: [0.842, 0.165, 91.3], + mood: "late-afternoon Tuscan sun on limestone — golden hour, considered, optimistic", + strategy: "Pure white surface lets the amber-gold primary radiate as the mood-carrier, with a deep aubergine accent providing the long shadow that golden light needs to feel three-dimensional." }, + { id: "seed-174", oklch: [0.350, 0.075, 110.0], + mood: "olive grove at late afternoon — sun-cured leaves, dust, and quiet Mediterranean weight", + strategy: "Pure white surface lets a deep, sun-cured olive primary do the emotional work, with a burnt-terracotta accent providing the warm-earth counterpoint olive groves are known for." }, + { id: "seed-117", oklch: [0.650, 0.100, 110.0], + mood: "Klim-style editorial sage — late-summer foundry catalogue, considered olive-yellow on paper", + strategy: "Seed sits at olive-chartreuse; treating it as a quiet typographic primary on pure paper, with a deeper bronze-olive accent for hierarchy — the color does the work, the page disappears." }, + { id: "seed-118", oklch: [0.750, 0.090, 110.0], + mood: "Klim Type Foundry specimen page — late-summer olive light on a working specimen, the honesty of a type designer showing their work", + strategy: "Pure white bg lets a desaturated olive-yellow primary do the editorial work, with a deeper olive-bronze accent providing typographic emphasis the way a specimen uses one heavy weight against the body roman." }, + { id: "seed-065", oklch: [0.797, 0.166, 113.1], + mood: "late-summer olive grove at noon — sun-bleached leaves, dry stone, Mediterranean glare", + strategy: "Hold the seed as a luminous chartreuse-olive primary against pure white so the color reads as sunlit foliage, pairing it with a deep umber accent for the dry-stone contrast." }, + { id: "seed-176", oklch: [0.300, 0.071, 120.0], + mood: "moss-darkened apothecary jar — herbal, shadowed, mid-19th-century botanical study", + strategy: "Seed is a deep desaturated olive-green that reads as preserved botanical pigment; I anchor it on pure white so the dim moss-green primary feels like ink on a herbarium page, with a warm ochre accent supplying the aged-paper counterpoint." }, + { id: "seed-155", oklch: [0.550, 0.142, 130.0], + mood: "moss-bed forest floor at noon — chlorophyll, lichen, sunlit fern", + strategy: "Seed is a confident mid-olive green with strong chroma; mood is daylight botanical, so I let the brand greens do the work on a pure paper-white bg and pair with a warm umber accent for fern-against-bark contrast." }, + { id: "seed-119", oklch: [0.600, 0.154, 130.0], + mood: "moss garden at Saihō-ji — damp stone, filtered green light through old cedar", + strategy: "Pure near-black bg lets the seed's mossy green glow like wet lichen under low light; accent shifts to a pale ochre-gold like sun catching through canopy." }, + { id: "seed-179", oklch: [0.300, 0.096, 140.0], + mood: "moss on wet stone — forest floor at dusk, deep botanical hush", + strategy: "Kept the seed's deep moss green as primary against a near-black surface so the green reads as living shadow, with a pale lichen accent providing the single point of light." }, + { id: "seed-180", oklch: [0.350, 0.110, 140.0], + mood: "moss-darkened apothecary — herbal tinctures in amber glass, pressed botanicals, the deep green of a conservatory at dusk", + strategy: "Near-black bg with a whisper of green undertone lets the seed's deep moss read as luminous foliage; a warm parchment accent provides the apothecary-label counterpoint without breaking the herbal register." }, + { id: "seed-120", oklch: [0.650, 0.100, 140.0], + mood: "moss on weathered stone — quiet botanical garden conservatory at midday", + strategy: "Pure white bg lets the muted sage-green primary read as a considered botanical mark, with a deeper terracotta accent providing earthen counterpoint without breaking the gallery-like restraint." }, + { id: "seed-121", oklch: [0.750, 0.090, 140.0], + mood: "moss garden at Saihō-ji — diffuse green light filtered through wet stone and lichen", + strategy: "Pure near-black bg lets the muted sage-green primary glow like lichen under low light; a warm pale-bone accent acts as the single ray of sun cutting through canopy." }, + { id: "seed-182", oklch: [0.400, 0.106, 150.0], + mood: "moss garden at Saiho-ji — deep cultivated green under wet stone shadow, contemplative and damp", + strategy: "Near-black bg with the faintest cool-green undertone evokes shaded stone; primary holds the seed's moss tone while accent shifts to a lichen-yellow for organic counterpoint without breaking the hush." }, + { id: "seed-157", oklch: [0.550, 0.145, 150.0], + mood: "moss garden at Saiho-ji — damp stone, filtered green light through cedar canopy", + strategy: "Near-black bg with a faint green undertone evokes deep forest shadow; primary holds the seed's verdant register while accent shifts to a pale lichen-cream to mimic light catching moss." }, + { id: "seed-122", oklch: [0.600, 0.158, 150.0], + mood: "forest floor at first light — moss, lichen, and clean morning air", + strategy: "Seed reads as a living, daylight green; surface stays pure white so the green carries the freshness, with a cool teal accent pulling it toward dew rather than earth." }, + { id: "seed-195", oklch: [0.650, 0.150, 145.0], + mood: "Considered horticulture brand — botanical research lab, the green of a healthy stem photographed in clean daylight", + strategy: "Pure white surface lets the seed's vegetal green carry the entire brand voice, paired with a deep forest ink and a warm clay accent for editorial contrast." }, + { id: "seed-183", oklch: [0.350, 0.077, 160.0], + mood: "moss-stained apothecary — deep forest glass, herbal tinctures shelved in low candlelight", + strategy: "Anchored the seed as primary and built a near-black dark surface with whisper-tinted green to evoke aged apothecary glass, letting the green glow rather than shout." }, + { id: "seed-184", oklch: [0.400, 0.087, 160.0], + mood: "deep forest apothecary — moss, bottle glass, and herbal tincture under afternoon light", + strategy: "Seed becomes a botanical-bottle-green primary on pure white, paired with a warm clove-amber accent to evoke herbal pharmacy contrast without tinting the surface." }, + { id: "seed-158", oklch: [0.550, 0.119, 160.0], + mood: "moss on wet stone — forest floor after rain, mineral and quiet", + strategy: "Pure white surface lets the deep mossy green carry the entire mood; accent shifts to a damp slate-teal to sit beside primary like lichen on stone without competing." }, + { id: "seed-159", oklch: [0.600, 0.130, 160.0], + mood: "moss-covered forest apothecary — herbal tinctures in amber glass, eucalyptus shadow", + strategy: "Anchored the green seed in a near-black backdrop so it reads like botanical glassware lit from within, with a warm amber accent pulled across the wheel to evoke tincture bottles against dark wood." }, + { id: "seed-185", oklch: [0.450, 0.086, 170.0], + mood: "weathered copper patina on a Pacific Northwest greenhouse — oxidized teal, glass light, botanical hush", + strategy: "Seed sits as a deep oxidized-teal primary against pure white so the patina reads as pigment, not atmosphere; a rust-copper accent completes the verdigris/oxidation story across the warm-cool axis." }, + { id: "seed-124", oklch: [0.750, 0.080, 170.0], + mood: "sea-glass on a foggy Pacific shoreline — weathered, mineral, quietly oxidized", + strategy: "Seed is a soft desaturated teal-green; pairing it on pure white lets the mineral primary read as patinated copper-glass, with a deeper kelp-toned primary and a rusted coral accent to spark the muted teal against its complement." }, + { id: "seed-160", oklch: [0.550, 0.095, 180.0], + mood: "weathered copper patina on a museum bronze — oxidized teal, conservatorial quiet", + strategy: "Pure near-black gallery surround lets the patina-teal primary glow like a lit artifact, with a warm verdigris-adjacent accent providing the oxidation contrast against the cool seed." }, + { id: "seed-161", oklch: [0.720, 0.100, 188.0], + mood: "climate-tech dashboard — calm verdigris on plain paper, the quiet confidence of an instrument that just works", + strategy: "Seed teal carries the entire mood as a single considered brand color on pure white, with a desaturated copper accent providing warm signal against the cool primary without competing for attention." }, + { id: "seed-186", oklch: [0.450, 0.074, 200.0], + mood: "deep hydrothermal vent — mineral teal under pressure, the cold blue-green of oxidized copper in submerged light", + strategy: "Near-black surface lets the mineral teal glow as if lit from within; accent shifts toward verdigris-copper to suggest patina on submerged metal, while ink stays cool-neutral to keep the register austere rather than aquatic-cute." }, + { id: "seed-125", oklch: [0.650, 0.100, 200.0], + mood: "climate-tech dashboard — calm operational teal, the color of clean water data and atmospheric sensors", + strategy: "Pure white surface lets a single muted-teal primary do all the brand work, with a deeper marine accent providing hierarchy without competing chroma." }, + { id: "seed-126", oklch: [0.750, 0.080, 200.0], + mood: "climate-tech product brand — quiet competence, dashboards for hard infrastructure problems", + strategy: "Hold the seed's muted teal as primary, pair with a sharper cyan-leaning accent for interactive lift, and let a pure white surface do the disappearing act so the brand reads as a tool, not an atmosphere." }, + { id: "seed-162", oklch: [0.550, 0.091, 210.0], + mood: "weathered nautical instrument — patinated brass on oxidized steel, the cool blue-grey of a ship's chronometer at dawn", + strategy: "Pure white surface lets the muted teal-steel primary read as a precise instrument mark, with a warm brass accent providing the single point of patina against clinical white." }, + { id: "seed-163", oklch: [0.450, 0.086, 230.0], + mood: "deep harbor at dusk — weathered nautical instruments, brass dials on oxidized steel", + strategy: "Near-black background with subtle cool tint evokes the marine dusk; primary holds the seed's teal-blue while a warm brass accent creates the instrument-on-steel tension." }, + { id: "seed-164", oklch: [0.550, 0.105, 230.0], + mood: "deep harbor at dawn — cold steel water, fog-muted light, the quiet before the boats leave", + strategy: "Pure near-black bg lets the seed's cold marine blue read as a luminous beacon, while a pale frost-cyan accent evokes diffused dawn light cutting through fog." }, + { id: "seed-127", oklch: [0.650, 0.100, 230.0], + mood: "climate-tech dashboard — atmospheric sensor blue, calm operational clarity", + strategy: "Anchor the seed as a confident mid-blue primary on pure white so the brand color carries all the atmospheric feeling, with a deep navy accent for hierarchy and a soft slate muted for body text." }, + { id: "seed-128", oklch: [0.750, 0.080, 230.0], + mood: "climate-tech dashboard — calm atmospheric data, considered sky-blue", + strategy: "Pure white surface lets the muted sky-blue primary carry the meteorological calm, with a deep-navy accent providing readable weight against the soft primary." }, + { id: "seed-187", oklch: [0.350, 0.078, 240.0], + mood: "deep harbor at blue hour — wet stone, cold steel, the quiet before night fully lands", + strategy: "Near-black architectural bg with a hint of marine chroma lets the seed read as ambient atmosphere rather than UI chrome; a cooler steel accent sits opposite the warmer-shifted primary for navigational clarity." }, + { id: "seed-077", oklch: [0.578, 0.130, 241.7], + mood: "pre-dawn signal tower — cold blue solitude, instruments glowing against the dark", + strategy: "Pure near-black bg lets the seed's cold tower-light blue glow as the sole emotional source, with a frost-cyan accent acting as a secondary indicator light." }, + { id: "seed-188", oklch: [0.400, 0.110, 250.0], + mood: "Linear's considered indigo — the calm authority of a well-built developer tool, blueprint ink on a clean page", + strategy: "Held the seed as a deep indigo primary against pure white so the brand color carries all the gravity; accent shifts to a cooler, brighter cyan-blue to create a crisp hierarchy pair without warming the surface." }, + { id: "seed-165", oklch: [0.450, 0.123, 250.0], + mood: "blueprint room at dusk — drafting table, graphite, civic-engineering blue", + strategy: "Seed is a mid-deep architectural blue with real chroma and no environmental cue, so I stay out of the way with a pure white surface and let the primary do all the talking, pairing it with a burnt-ochre accent for drafting-pencil contrast." }, + { id: "seed-079", oklch: [0.478, 0.136, 251.8], + mood: "twilight cartography — the blue of deep dusk over open water, precise and navigational", + strategy: "Pure white surface lets the seed's oceanic blue act as a single navigational anchor, with a warm amber accent struck across it like a lighthouse beam at dusk." }, + { id: "seed-080", oklch: [0.541, 0.122, 248.2], + mood: "Linear-style considered tool blue — the calm, exact register of a modern engineering app where every pixel is intentional", + strategy: "Pure white surface lets the considered indigo-blue primary carry the entire brand; a deeper navy accent provides hierarchy without warmth, keeping the palette in a single cool family for that focused-software feel" }, + { id: "seed-166", oklch: [0.550, 0.149, 250.0], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and quietly intense", + strategy: "Near-black bg with the faintest cool tint reads like a darkened cockpit; the seed becomes a luminous instrument-blue primary, paired with a warm amber accent that mimics avionics readouts for unmistakable signal contrast." }, + { id: "seed-081", oklch: [0.650, 0.160, 250.0], + mood: "deep-sea research vessel at dawn — instrument glow against cold steel light", + strategy: "Pure near-white bg keeps the palette technical and instrument-like; the seed blue holds as primary while a desaturated steel-cyan accent reads like signal readouts on glass." }, + { id: "seed-082", oklch: [0.742, 0.140, 247.4], + mood: "high-altitude flight deck at dawn — cold cabin instruments glowing against a sky still holding night", + strategy: "Near-black cockpit ground with a faint blue cast lets the seed read as an illuminated instrument; primary holds the seed, accent shifts to cyan for signal/indicator contrast." }, + { id: "seed-210", oklch: [0.360, 0.140, 260.0], + mood: "Linear-style considered tool indigo — late-night focused work, the deep blue of a code editor at 2am where everything else falls away", + strategy: "Pure black bg lets the indigo primary carry all the cognitive-focus weight, with a slightly brighter periwinkle accent for interactive lift — the surface disappears so the tool feels weightless." }, + { id: "seed-189", oklch: [0.400, 0.130, 260.0], + mood: "pre-dawn observatory — cold instrument blue, star-chart precision", + strategy: "Seed becomes the primary on pure black so the deep instrument-blue glows like a calibration light, with a faint cyan accent reading as starlight against the void." }, + { id: "seed-211", oklch: [0.420, 0.161, 260.0], + mood: "Linear's considered indigo — the tool-for-thought blue of focused product work, calm authority without coldness", + strategy: "Hold the seed as a deep indigo primary against pure white, then pair with a slightly warmer, lighter periwinkle accent to create gentle hue separation without breaking the disciplined tool-brand register." }, + { id: "seed-129", oklch: [0.450, 0.150, 260.0], + mood: "pre-dawn observatory — deep cobalt sky just before astronomical twilight, instruments cool to the touch", + strategy: "Near-black surface lets the cobalt seed read as luminous starlight; a single warm amber accent acts as the calibration lamp against the cold blue field." }, + { id: "seed-084", oklch: [0.476, 0.207, 261.2], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and awake", + strategy: "Default B black bg lets the cobalt primary read as a luminous instrument signal, with a cyan accent striking the analogous 'cockpit display' relationship." }, + { id: "seed-085", oklch: [0.681, 0.132, 258.4], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky", + strategy: "Anchored the seed as a luminous primary against a near-black architectural ground, with a warm amber accent acting as the single instrument light cutting through cold blue." }, + { id: "seed-086", oklch: [0.767, 0.106, 255.9], + mood: "Scandinavian winter morning — quiet light through frost, pale sky over snow", + strategy: "Anchored a pure white editorial stage so the seed's cool sky-blue reads as crisp polar light, with a deeper navy primary providing the only saturated weight — like a single dark pine against snow." }, + { id: "seed-083", oklch: [0.340, 0.159, 262.4], + mood: "deep cobalt twilight — the moment after sunset when the sky goes electric blue and city windows start to glow", + strategy: "Pure black stage lets the cobalt seed act as a luminous neon-window glow, with a warm amber accent across the wheel for the lit-window contrast." }, + { id: "seed-212", oklch: [0.360, 0.219, 270.0], + mood: "Linear-grade tooling indigo — considered software for people who care about craft", + strategy: "Anchored the deep indigo seed as primary on a pure white surface so the brand color carries all the weight, with a slightly cooler violet-blue accent for hierarchy without competing chroma." }, + { id: "seed-130", oklch: [0.400, 0.150, 270.0], + mood: "Linear-grade indigo — considered productivity tool, ink on paper, no theatrics", + strategy: "Pure white surface lets a deep cool indigo carry all the brand weight, paired with a slightly warmer violet-blue accent for hierarchy without acid." }, + { id: "seed-213", oklch: [0.411, 0.241, 267.9], + mood: "Linear-style indigo — considered tool surface, the kind of blue-violet that sits behind a developer's keyboard at 11pm without shouting", + strategy: "Pure black canvas lets a saturated indigo primary do all the brand work, with a cooler cyan-violet accent providing UI signal without competing." }, + { id: "seed-131", oklch: [0.450, 0.180, 270.0], + mood: "monastic indigo dusk — vespers light through stained glass, contemplative and severe", + strategy: "Seed becomes a deep indigo primary against pure near-black so the violet reads as luminous stained-glass against architectural shadow, with a cooler iris accent for tonal lift." }, + { id: "seed-088", oklch: [0.476, 0.158, 268.5], + mood: "pre-dawn astronomer's notebook — deep indigo sky just before the stars fade, ink and graphite", + strategy: "Near-black bg with the faintest cool tint to evoke night sky without theatrics; primary holds the seed's indigo, accent shifts to a paler periwinkle for stellar contrast, keeping the palette monochromatic-cool and observational." }, + { id: "seed-196", oklch: [0.530, 0.130, 268.0], + mood: "Linear-style considered tool indigo — the deep-focus blue-violet of a thoughtfully built productivity surface, the color of a well-typeset keyboard shortcut", + strategy: "Pure white bg lets the indigo seed do all the brand work as primary, with a slightly darker, more saturated violet-shifted accent for hierarchy and interactive states — the surface disappears so the brand color reads as the entire identity." }, + { id: "seed-132", oklch: [0.700, 0.120, 270.0], + mood: "Linear-style considered tool indigo — the quiet violet of a focused product workspace, late-afternoon thinking", + strategy: "Pure white surface lets a muted indigo-violet primary and a slightly cooler accent do all the brand work, keeping the register calm and software-like rather than theatrical." }, + { id: "seed-090", oklch: [0.445, 0.206, 279.1], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, not a nightclub", + strategy: "Anchor the seed as a confident product primary on pure white, with a cooler indigo-shift accent that reads as a sibling tool color, so the brand violet does all the emotional work." }, + { id: "seed-133", oklch: [0.500, 0.160, 280.0], + mood: "Linear-adjacent indigo — considered productivity tool, the violet of a thinking workspace", + strategy: "Seed becomes a measured indigo primary on pure white; accent shifts to a cooler blue-violet to create hierarchy without nightclub saturation, letting the brand color do all the emotional work." }, + { id: "seed-094", oklch: [0.533, 0.125, 294.3], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, calm authority for a creative workspace", + strategy: "Pure white canvas lets the indigo-violet primary carry the entire brand voice; accent shifts hue slightly toward blue for a cool, tool-like duotone rather than warm decorative pairing." }, + { id: "seed-137", oklch: [0.700, 0.120, 290.0], + mood: "Linear-adjacent indigo — the considered tool, late-evening focus mode, software made for people who care about craft", + strategy: "Pure black surface lets a single restrained indigo-violet carry the brand, with a cooler periwinkle accent providing UI hierarchy without competing — Vercel/Linear dark-mode discipline." }, + { id: "seed-100", oklch: [0.450, 0.150, 330.0], + mood: "velvet boudoir at last call — bruised orchid and lipstick traces under low lamplight", + strategy: "Pure near-black surface lets a deep magenta-rose primary smolder while a warm peach accent acts like skin-lit lamplight — drama lives in the brand pair, not the room." }, + { id: "seed-103", oklch: [0.650, 0.160, 330.0], + mood: "1980s Memphis boudoir — powder-pink neon humming against lacquered black, lipstick and lacquer", + strategy: "Near-black gallery surface lets the magenta-pink seed read as lit neon; accent shifts to warm coral to create cinematic dichromatic tension without competing chroma." }, + { id: "seed-228", oklch: [0.360, 0.147, 340.0], + mood: "Figma-era creative tool plum — considered productivity software for designers, the inky violet of a serif wordmark on a marketing site", + strategy: "Held the seed as a deep plum primary against pure white so the brand color does the emotional work; paired with a muted rose accent for warmth without breaking the productivity-tool restraint." }, + { id: "seed-107", oklch: [0.500, 0.200, 340.0], + mood: "Figma plum — creative-tool confidence, considered magenta for a modern design product", + strategy: "Pure white surface lets a saturated magenta-plum primary carry all the brand voice, paired with a cooler violet-leaning accent for hierarchy without competing." }, + { id: "seed-198", oklch: [0.600, 0.210, 340.0], + mood: "Figma-era creative tool plum — confident, considered, made for makers", + strategy: "Anchor a saturated plum primary against pure white so the brand color does all the emotional work, with a deeper magenta-rose accent for hierarchy." }, + { id: "seed-112", oklch: [0.754, 0.193, 343.4], + mood: "Figma-era creative tool — confident pink primary doing the brand work on a clean canvas, the way Linear uses indigo or Stripe uses violet", + strategy: "Anchor the seed pink as a saturated brand primary on pure white so the color carries all the personality; pair with a cooler plum accent to give the pink something to push against without competing." }, + { id: "seed-229", oklch: [0.420, 0.163, 350.0], + mood: "considered fintech rose — the deep magenta of a modern product brand (think Stripe-adjacent, but rotated toward berry), confident and current", + strategy: "pure white surface lets a single deep berry-rose primary do all the brand work, paired with a cooler indigo accent for the contrast move you see in modern product marketing" }, + { id: "seed-113", oklch: [0.470, 0.173, 354.8], + mood: "1960s velvet rope nightclub — crushed magenta, low light, cigarette smoke catching a spotlight", + strategy: "Pure black stage so the seed's smoky magenta reads as a single hot spotlight, paired with a cooler violet accent for the second light cue." }, + { id: "seed-114", oklch: [0.570, 0.158, 353.3], + mood: "fin-de-siècle Parisian rose — velvet curtain, theatre program, lipstick blotted on linen", + strategy: "Drop bg to true black so the dusty-rose primary reads as stage-lit silk; accent shifts to a warmer coral-mauve at higher lightness to create gentle hue rotation without breaking the romance." }, + { id: "seed-199", oklch: [0.650, 0.180, 350.0], + mood: "modern fintech rose — the considered pink of a Series B brand mark, confident and current without nostalgia", + strategy: "Pure white surface lets a saturated rose primary do the brand work, paired with a deep plum accent for hierarchy — the Stripe move applied to a pink hue." }, + { id: "seed-115", oklch: [0.636, 0.218, 355.3], + mood: "backstage at a cabaret — velvet rope, lipstick mark on a champagne glass", + strategy: "Seed reads as a saturated stage-light magenta-red; I push it into pure black so the primary glows like a neon sign and the accent (a cold pearl-pink) acts as the spotlight rim — the room is dark, the color does the singing." }, + { id: "seed-230", oklch: [0.650, 0.249, 354.5], + mood: "Modern fintech rose — the considered pink of a contemporary payments brand: confident, alive, and clear-headed", + strategy: "Pure white bg lets a saturated rose-magenta primary carry all the brand energy, paired with a cooler indigo accent for trustworthy contrast — the Stripe move applied to a pink hue." }, + { id: "seed-231", oklch: [0.682, 0.241, 353.2], + mood: "Figma-era creative tool — a confident pink-magenta product brand, the kind a modern design platform uses to feel alive without shouting", + strategy: "Default A pure white bg lets the saturated pink-magenta primary do all the brand work, with a near-complementary cool teal accent for tool-like clarity and a neutral ink for editorial calm" }, + { id: "seed-116", oklch: [0.734, 0.183, 356.8], + mood: "modern beauty brand DTC — Glossier-adjacent pink, confident and current without being saccharine", + 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." }, +]; + +function parseArgs(argv) { + const args = { id: null, from: null }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === '--id' && argv[i + 1]) { args.id = argv[++i]; } + else if (a === '--from' && argv[i + 1]) { args.from = argv[++i]; } + } + return args; +} + +// Hash a key into a stable float in [0, 1) for deterministic weighted picks. +function hashUnit(key) { + const h = crypto.createHash('sha256').update(key).digest(); + return h.readUInt32BE(0) / 0x100000000; +} + +// The curated library is hue-skewed (more reds/oranges than teals/magentas) +// because that's where the source material + taste landed. Left uniform, a +// random pick would land on red ~1/3 of the time. Inverse-frequency weighting +// gives each seed a weight of 1/(count in its 30° hue bucket), so each hue +// ZONE is roughly equally likely to be chosen regardless of how many seeds it +// holds — fair rainbow exposure across runs without pruning the library. +function buildWeights(seeds) { + const bucketCount = {}; + const bucketOf = (s) => Math.floor(((s.oklch[2] % 360) + 360) % 360 / 30); + for (const s of seeds) { const b = bucketOf(s); bucketCount[b] = (bucketCount[b] || 0) + 1; } + const weights = seeds.map((s) => 1 / bucketCount[bucketOf(s)]); + const total = weights.reduce((a, b) => a + b, 0); + return { weights, total }; +} + +function weightedPick(seeds, unit) { + const { weights, total } = buildWeights(seeds); + let target = unit * total; + for (let i = 0; i < seeds.length; i++) { + target -= weights[i]; + if (target < 0) return seeds[i]; + } + return seeds[seeds.length - 1]; +} + +function pickSeed(seeds, { id, from }) { + if (id) { + const found = seeds.find(s => s.id === id); + if (!found) { console.error(`no seed with id "${id}"`); process.exit(2); } + return found; + } + const envFrom = process.env.IMPECCABLE_PALETTE_SEED; + const key = from || envFrom; + const unit = key ? hashUnit(key) : Math.random(); + return weightedPick(seeds, unit); +} + +function fmtOklch([L, C, H]) { + return `oklch(${L.toFixed(3)} ${C.toFixed(3)} ${H.toFixed(1)})`; +} + +function hueWord(H) { + if (H < 15 || H >= 345) return 'pure red'; + if (H < 35) return 'warm red / crimson'; + if (H < 55) return 'warm coral / burnt orange'; + if (H < 80) return 'orange / honey'; + if (H < 105) return 'warm amber / honey-gold'; + if (H < 135) return 'yellow-green / olive'; + if (H < 170) return 'green'; + if (H < 200) return 'teal'; + if (H < 230) return 'sky blue'; + if (H < 265) return 'cobalt / indigo'; + if (H < 295) return 'violet / purple'; + if (H < 330) return 'magenta / pink'; + return 'deep pink / rose'; +} + +// --------------------------------------------------------------- + +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}` : ''; + +// --------------------------------------------------------------- +// Fat tool-exit response — what the model sees on stdout. +// --------------------------------------------------------------- + +process.stdout.write(`BRAND SEED · ${seed.id} + +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. + +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. + +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é. + + 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 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. + + 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. + +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): + + - 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. + +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. + +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. +`); diff --git a/.agents/skills/impeccable/scripts/pin.mjs b/.agents/skills/impeccable/scripts/pin.mjs index ba02783fb..320d98ce1 100644 --- a/.agents/skills/impeccable/scripts/pin.mjs +++ b/.agents/skills/impeccable/scripts/pin.mjs @@ -27,7 +27,7 @@ const HARNESS_DIRS = [ // Valid sub-command names const VALID_COMMANDS = [ - 'craft', 'teach', 'extract', 'document', 'shape', + 'craft', 'init', 'extract', 'document', 'shape', 'critique', 'audit', 'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live', 'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive', diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index abe5fe171..fa47ae172 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -12,7 +12,7 @@ { "name": "impeccable", "description": "Design fluency for frontend development. 1 skill with 23 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.", - "version": "3.1.1", + "version": "3.5.0", "author": { "name": "Paul Bakaus", "email": "paul@paulbakaus.com" diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index b6982f043..20c00a1ee 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "impeccable", "description": "Design fluency for frontend development. 1 skill with 23 commands (/impeccable polish, /impeccable audit, /impeccable critique, etc.) and curated anti-pattern detection.", - "version": "3.1.1", + "version": "3.5.0", "author": { "name": "Paul Bakaus", "email": "paul@paulbakaus.com" diff --git a/.claude/skills/impeccable/SKILL.md b/.claude/skills/impeccable/SKILL.md index dcb6ef147..1edabd6d6 100644 --- a/.claude/skills/impeccable/SKILL.md +++ b/.claude/skills/impeccable/SKILL.md @@ -1,10 +1,10 @@ --- name: impeccable description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks. -version: 3.1.1 +version: 3.5.0 user-invocable: true -argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · teach|document|extract|live] [target]" -license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution. +argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]" +license: Apache 2.0 allowed-tools: - Bash(npx impeccable *) --- @@ -13,85 +13,80 @@ Designs and iterates production-grade frontend interfaces. Real working code, co ## Setup -Before any design work or file edits: +You MUST do these steps before proceeding: -1. Load context (PRODUCT.md / DESIGN.md) via the loader script. -2. Identify the register and load the matching register reference (brand.md or product.md). -3. **If the user invoked a sub-command (e.g. `craft`, `shape`, `audit`), load its reference file too.** This is non-negotiable: `craft` without `craft.md` loaded means you'll skip the shape-and-confirm step the user expects. +1. Run `node .claude/skills/impeccable/scripts/context.mjs` once per session. If you've already seen its output in this conversation, do not re-run it. The script either prints the project's PRODUCT.md (and DESIGN.md when present) as a markdown block, or tells you it's missing. Follow whatever it prints. **If it reports `NO_PRODUCT_MD`, stop and follow `reference/init.md` before doing anything else.** If the output ends with an `UPDATE_AVAILABLE` directive, follow it (ask the user once about updating, then continue). It never blocks the current task. +2. If the user invoked a sub-command (`craft`, `shape`, `audit`, `polish`, ...), you MUST read `reference/.md` next. Non-optional. The reference defines the command's flow; without it you will skip steps the user expects. +3. Familiarize yourself with any existing design system, conventions, and components in the code. Read at least one project file (CSS / tokens / theme / a representative component or page). **Required even when you've loaded a sub-command reference in step 2.** Don't reinvent the wheel; use what's there when it works, branch out when the UX wins. +4. Read the matching register reference. **This is non-optional; skipping it produces generic output.** If the project is marketing, a landing page, a campaign, long-form content, or a portfolio (design IS the product), read `reference/brand.md`. If it is app UI, admin, a dashboard, or a tool (design SERVES the product), read `reference/product.md`. Pick by first match: (1) task cue ("landing page" vs "dashboard"); (2) surface in focus (the page, file, or route being worked on); (3) `register` field in PRODUCT.md. +5. **If the project is brand-new (no existing CSS tokens / theme / committed brand colors found in step 3)**, run `node .claude/skills/impeccable/scripts/palette.mjs` to receive a brand seed color and composition guidance. This is the anchor for your primary brand color. Compose the rest of the palette (bg, surface, ink, accent, muted) around it per the script's instructions. Use OKLCH throughout. **Skip this step only if step 3 found committed brand colors in existing tokens; in that case identity-preservation wins.** -Skipping these produces generic output that ignores the project. +## Design guidance -### 1. Context gathering +Produce ready-to-ship, production-grade code, not prototypes or starting points. Take no shortcuts unless the user asks for them (when in doubt, ask). Don't stop until arriving at a complete implementation (beautiful, responsive, fast, precise, bug-free, on brand). You take attention to detail seriously: every page, section or component crafted is battle tested using the tools available to you (browser screenshotting, computer use, etc). Claude is capable of extraordinary work. Don't hold back. -Two files, case-insensitive. The loader looks at the project root by default and falls back to `.agents/context/` and `docs/` if the root is clean. Override with `IMPECCABLE_CONTEXT_DIR=path/to/dir` (absolute or relative to cwd). +### General rules -- **PRODUCT.md**: required. Users, brand, tone, anti-references, strategic principles. -- **DESIGN.md**: optional, strongly recommended. Colors, typography, elevation, components. +#### Color -Load both in one call: +- **Verify contrast.** Body text must hit ≥4.5:1 against its background; large text (≥18px or bold ≥14px) needs ≥3:1. Placeholder text needs the same 4.5:1, not the muted-gray default. The most common failure: muted gray body text on a tinted near-white. If the contrast is even close, bump the body color toward the ink end of the ramp; light gray "for elegance" is the single biggest reason AI designs feel hard to read. +- Gray text on a colored background looks washed out. Use a darker shade of the background's own hue, or a transparency of the text color. -```bash -node .claude/skills/impeccable/scripts/load-context.mjs -``` +#### Typography -Consume the full JSON output. Never pipe through `head`, `tail`, `grep`, or `jq`. The output's `contextDir` field tells you where the files were resolved from. +- Cap body line length at 65–75ch. +- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales. +- Cap font-family count at 3 (display + body + optional mono). More than 3 reads as indecision, not richness. One well-tuned family with weight contrast usually beats three competing typefaces. +- Don't pair fonts that are similar but not identical (two geometric sans-serifs, two humanist sans-serifs). Pair on a contrast axis (serif + sans, geometric + humanist) or use one family in multiple weights. +- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. +- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. +- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". +- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. -If the output is already in this session's conversation history, don't re-run. Exceptions requiring a fresh load: you just ran `/impeccable teach` or `/impeccable document` (they rewrite the files), or the user manually edited one. +#### Layout -`/impeccable live` already warms context via `live.mjs`. If you've run `live.mjs`, don't also run `load-context.mjs` this session. +- Vary spacing for rhythm. +- Cards are the lazy answer. Use them only when they're truly the best affordance. Nested cards are always wrong. +- Flexbox for 1D, Grid for 2D. Don't default to Grid when `flex-wrap` would be simpler. +- For responsive grids without breakpoints: `repeat(auto-fit, minmax(280px, 1fr))`. +- Build a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip). Never arbitrary values like 999 or 9999. -If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `/impeccable teach`, then resume the user's original task with the fresh context. If the original task was `/impeccable craft`, resume into `/impeccable shape` before any implementation work. +#### Motion +- Motion should be intentional, and not be an afterthought. consider it as part of the build. +- Don't animate CSS layout properties unless truly needed. +- Ease out with exponential curves (ease-out-quart / quint / expo). No bounce, no elastic. +- Use libraries for more advanced motion needs (e.g. motion, gsap, anime.js, lenis etc) +- Reduced motion is not optional. Every animation needs a `@media (prefers-reduced-motion: reduce)` alternative: typically a crossfade or instant transition. +- Staggering the items within one list is legitimate. The tell is the uniform reflex (one identical entrance applied to every section), not motion itself; each reveal should fit what it reveals. Suppressing the reflex is never a reason to ship a page with no motion at all. +- Reveal animations must enhance an already-visible default. Don't gate content visibility on a class-triggered transition; transitions pause on hidden tabs and headless renderers, so the reveal never fires and the section ships blank. +- Premium motion materials are not just transform/opacity. Blur, backdrop-filter, clip-path, mask, and shadow/glow are part of the palette when they materially improve the effect and stay smooth. -If DESIGN.md is missing: nudge once per session (*"Run `/impeccable document` for more on-brand output"*), then proceed. +#### Interaction -### 2. Register +- Dropdowns rendered with `position: absolute` inside an `overflow: hidden` or `overflow: auto` container will be clipped. Use the native `` / popover API, `position: fixed`, or a portal to escape the stacking context. -Every design task is **brand** (marketing, landing, campaign, long-form content, portfolio: design IS the product) or **product** (app UI, admin, dashboard, tool: design SERVES the product). +### Copy -Identify before designing. Priority: (1) cue in the task itself ("landing page" vs "dashboard"); (2) the surface in focus (the page, file, or route being worked on); (3) `register` field in PRODUCT.md. First match wins. +- Every word earns its place. No restated headings, no intros that repeat the title. +- **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`. +- **No aphoristic-cadence body copy as a default voice.** Don't fall into the rhythm of "serious statement, then punchy short negation" as the page's recurring voice. If three or more section copy blocks on the page land on a short rebuttal-shaped sentence, rewrite. Specific, not aphoristic. +- **No marketing buzzwords.** The streamline / empower / supercharge / leverage / unleash / transform / seamless / world-class / enterprise-grade / next-generation / cutting-edge / game-changer / mission-critical family of phrases. Pick a specific noun and a verb that describes what the product literally does. +- Button labels: verb + object. "Save changes" beats "OK"; "Delete project" beats "Yes". The label should say what will happen. +- Link text needs standalone meaning. "View pricing plans" beats "Click here"; screen readers announce links out of context. -If PRODUCT.md lacks the `register` field (legacy), infer it once from its "Users" and "Product Purpose" sections, then cache the inferred value for the session. Suggest the user run `/impeccable teach` to add the field explicitly. +### New projects only (when no prior work exists) -Load the matching reference: [reference/brand.md](reference/brand.md) or [reference/product.md](reference/product.md). The shared design laws below apply to both. +#### Color & Theme -## Shared design laws - -Apply to every design, both registers. Match implementation complexity to the aesthetic vision: maximalism needs elaborate code, minimalism needs precision. Interpret creatively. Vary across projects; never converge on the same choices. Claude is capable of extraordinary work. Don't hold back. - -### Color - -- Use OKLCH. Reduce chroma as lightness approaches 0 or 100; high chroma at extremes looks garish. -- Never use `#000` or `#fff`. Tint every neutral toward the brand hue (chroma 0.005–0.01 is enough). +- Use OKLCH. +- **The cream / sand / beige body bg is the saturated AI default of 2026.** The whole warm-neutral band (OKLCH L 0.84-0.97, C < 0.06, hue 40-100) reads as cream/sand/paper/parchment regardless of what you call it. Token names like `--paper`, `--cream`, `--sand`, `--bone`, `--flour`, `--linen`, `--parchment`, `--wheat`, `--biscuit`, `--ivory` are tells in themselves. If the brief is "warm, traditional, family-coastal-Italian" or "magazine-warm" or "editorial-restraint", DO NOT translate that into a near-white warm-tinted bg; that's the AI move. Pick: (a) a saturated brand color as the body (terracotta, oxblood, deep ochre, near-black), (b) a true off-white at chroma 0 (or chroma toward the brand's own hue, not toward warmth-by-default), or (c) a darker mid-tone tinted neutral that's clearly the brand's own. "Warmth" in the brand is carried by accent + typography + imagery, not by body bg. +- Tinted neutrals: add 0.005–0.015 chroma toward the brand's hue. Don't default-tint toward warm or cool "because the brand feels that way"; that's the cross-project monoculture move. +- When picking a theme: Dark vs. light is never a default. Not dark "because tools look cool dark." Not light "to be safe.".Before choosing, write one sentence of physical scene: who uses this, where, under what ambient light, in what mood. If the sentence doesn't force the answer, it's not concrete enough. Add detail until it does. - Pick a **color strategy** before picking colors. Four steps on the commitment axis: - **Restrained**: tinted neutrals + one accent ≤10%. Product default; brand minimalism. - **Committed**: one saturated color carries 30–60% of the surface. Brand default for identity-driven pages. - **Full palette**: 3–4 named roles, each used deliberately. Brand campaigns; product data viz. - **Drenched**: the surface IS the color. Brand heroes, campaign pages. -- The "one accent ≤10%" rule is Restrained only. Committed / Full palette / Drenched exceed it on purpose. Don't collapse every design to Restrained by reflex. - -### Theme - -Dark vs. light is never a default. Not dark "because tools look cool dark." Not light "to be safe." - -Before choosing, write one sentence of physical scene: who uses this, where, under what ambient light, in what mood. If the sentence doesn't force the answer, it's not concrete enough. Add detail until it does. - -"Observability dashboard" does not force an answer. "SRE glancing at incident severity on a 27-inch monitor at 2am in a dim room" does. Run the sentence, not the category. - -### Typography - -- Cap body line length at 65–75ch. -- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales. - -### Layout - -- Vary spacing for rhythm. Same padding everywhere is monotony. -- Cards are the lazy answer. Use them only when they're truly the best affordance. Nested cards are always wrong. -- Don't wrap everything in a container. Most things don't need one. - -### Motion - -- Don't animate CSS layout properties. -- Ease out with exponential curves (ease-out-quart / quint / expo). No bounce, no elastic. ### Absolute bans @@ -102,12 +97,9 @@ Match-and-refuse. If you're about to write any of these, rewrite the element wit - **Glassmorphism as default.** Blurs and glass cards used decoratively. Rare and purposeful, or nothing. - **The hero-metric template.** Big number, small label, supporting stats, gradient accent. SaaS cliché. - **Identical card grids.** Same-sized cards with icon + heading + text, repeated endlessly. -- **Modal as first thought.** Modals are usually laziness. Exhaust inline / progressive alternatives first. - -### Copy - -- Every word earns its place. No restated headings, no intros that repeat the title. -- **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`. +- **Tiny uppercase tracked eyebrow above every section.** The 2023-era kicker (small all-caps text with wide tracking, "ABOUT" "PROCESS" "PRICING" above each heading) is now the saturated AI scaffold; it appears on 55-95% of generations regardless of brief, which is the definition of a tell. One named kicker as a deliberate brand system is voice; an eyebrow on every section is AI grammar. Choose a different cadence. +- **Numbered section markers as default scaffolding (01 / 02 / 03).** Putting `01 · About / 02 · Process / 03 · Pricing` above every section is the eyebrow trope one tier deeper: reach for it because "landing pages do this" and you're scaffolding by reflex. Numbers earn their place when the section actually IS a sequence (a real 3-step process, an ordered flow, a typed timeline) and the order carries information the reader needs. One deliberate numbered sequence on one page is voice; numbered eyebrows on every section across the site is AI grammar. +- **Text that overflows its container.** Long heading words plus large clamp scales plus narrow grids cause headline overflow on tablet/mobile. Test the heading copy at every breakpoint; if it overflows, reduce the clamp max or rewrite the copy. The viewport is part of the design. ### The AI slop test @@ -115,7 +107,7 @@ If someone could look at this interface and say "AI made that" without doubt, it **Category-reflex check.** Run at two altitudes; the second one catches what the first one misses. -- **First-order:** if someone could guess the theme + palette from the category alone ("observability → dark blue", "healthcare → white + teal", "finance → navy + gold", "crypto → neon on black"), it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain. +- **First-order:** if someone could guess the theme + palette from the category alone, it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain. - **Second-order:** if someone could guess the aesthetic family from category-plus-anti-references ("AI workflow tool that's not SaaS-cream → editorial-typographic", "fintech that's not navy-and-gold → terminal-native dark mode"), it's the trap one tier deeper. The first reflex was avoided; the second wasn't. Rework until both answers are not obvious. The brand register's [reflex-reject aesthetic lanes](reference/brand.md) list catches the currently-saturated families. ## Commands @@ -124,7 +116,7 @@ If someone could look at this interface and say "AI made that" without doubt, it |---|---|---|---| | `craft [feature]` | Build | Shape, then build a feature end-to-end | [reference/craft.md](reference/craft.md) | | `shape [feature]` | Build | Plan UX/UI before writing code | [reference/shape.md](reference/shape.md) | -| `teach` | Build | Set up PRODUCT.md and DESIGN.md context | [reference/teach.md](reference/teach.md) | +| `init` | Build | Set up project context: PRODUCT.md, DESIGN.md, live config, next steps | [reference/init.md](reference/init.md) | | `document` | Build | Generate DESIGN.md from existing project code | [reference/document.md](reference/document.md) | | `extract [target]` | Build | Pull reusable tokens and components into design system | [reference/extract.md](reference/extract.md) | | `critique [target]` | Evaluate | UX design review with heuristic scoring | [reference/critique.md](reference/critique.md) | @@ -152,11 +144,14 @@ Plus two management commands: `pin ` and `unpin `, detailed be 1. **No argument**: render the table above as the user-facing command menu, grouped by category. Ask what they'd like to do. 2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. -3. **First word doesn't match**: general design invocation. Apply the setup steps, shared design laws, and the loaded register reference, using the full argument as context. +3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. +4. **No clear command match**: general design invocation. Apply the setup steps, the General rules, and the loaded register reference, using the full argument as context. Setup (context gathering, register) is already loaded by then; sub-commands don't re-invoke `/impeccable`. -If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `teach` as a blocker, finish teach, refresh context, then resume the original command and target. +If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `init` as a blocker, finish init, refresh context, then resume the original command and target. + +`teach` is a deprecated alias for `init`: if the user types it, load [reference/init.md](reference/init.md) and proceed as if they ran `init`. ## Pin / Unpin diff --git a/.claude/skills/impeccable/reference/adapt.md b/.claude/skills/impeccable/reference/adapt.md index 9d0673ea2..5af7606cb 100644 --- a/.claude/skills/impeccable/reference/adapt.md +++ b/.claude/skills/impeccable/reference/adapt.md @@ -188,3 +188,124 @@ Test thoroughly across contexts: - **Slow connections**: Test on throttled network When the adaptation feels native to each context, hand off to `/impeccable polish` for the final pass. + +--- + +## Reference Material + +The sections below were previously `responsive-design.md` and live inline now so the adapt flow has its deep responsive reference in one place. + +### Responsive Design + +#### Mobile-First: Write It Right + +Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first. + +#### Breakpoints: Content-Driven + +Don't chase device sizes; let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints. + +#### Detect Input Method, Not Just Screen Size + +**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard. Use pointer and hover queries: + +```css +/* Fine pointer (mouse, trackpad) */ +@media (pointer: fine) { + .button { padding: 8px 16px; } +} + +/* Coarse pointer (touch, stylus) */ +@media (pointer: coarse) { + .button { padding: 12px 20px; } /* Larger touch target */ +} + +/* Device supports hover */ +@media (hover: hover) { + .card:hover { transform: translateY(-2px); } +} + +/* Device doesn't support hover (touch) */ +@media (hover: none) { + .card { /* No hover state - use active instead */ } +} +``` + +**Critical**: Don't rely on hover for functionality. Touch users can't hover. + +#### Safe Areas: Handle the Notch + +Modern phones have notches, rounded corners, and home indicators. Use `env()`: + +```css +body { + padding-top: env(safe-area-inset-top); + padding-bottom: env(safe-area-inset-bottom); + padding-left: env(safe-area-inset-left); + padding-right: env(safe-area-inset-right); +} + +/* With fallback */ +.footer { + padding-bottom: max(1rem, env(safe-area-inset-bottom)); +} +``` + +**Enable viewport-fit** in your meta tag: +```html + +``` + +#### Responsive Images: Get It Right + +##### srcset with Width Descriptors + +```html +Hero image +``` + +**How it works**: +- `srcset` lists available images with their actual widths (`w` descriptors) +- `sizes` tells the browser how wide the image will display +- Browser picks the best file based on viewport width AND device pixel ratio + +##### Picture Element for Art Direction + +When you need different crops/compositions (not just resolutions): + +```html + + + + ... + +``` + +#### Layout Adaptation Patterns + +**Navigation**: Three stages: hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `
/` for content that can collapse on mobile. + +#### Testing: Don't Trust DevTools Alone + +DevTools device emulation is useful for layout but misses: + +- Actual touch interactions +- Real CPU/memory constraints +- Network latency patterns +- Font rendering differences +- Browser chrome/keyboard appearances + +**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators. + +--- + +**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful. diff --git a/.claude/skills/impeccable/reference/animate.md b/.claude/skills/impeccable/reference/animate.md index d45fcfa05..97ef59fd5 100644 --- a/.claude/skills/impeccable/reference/animate.md +++ b/.claude/skills/impeccable/reference/animate.md @@ -6,7 +6,7 @@ Add motion that conveys state, gives feedback, and clarifies hierarchy. Cut moti ## Register -Brand: orchestrated page-load sequences, staggered reveals, scroll-driven animation. Motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions. +Brand: motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions. The saturated AI default is fade-and-rise reveals on every scrolled section; that's a tell, not a choreography. Reserve scroll-triggered motion for moments that earn it. Product: 150–250 ms on most transitions. Motion conveys state: feedback, reveal, loading, transitions between views. No page-load choreography; users are in a task and won't wait for it. @@ -49,10 +49,11 @@ Create a purposeful animation plan: Add motion systematically across these categories: ### Entrance Animations -- **Page load choreography**: Stagger element reveals (100-150ms delays), fade + slide combinations - **Hero section**: Dramatic entrance for primary content (scale, parallax, or creative effects) -- **Content reveals**: Scroll-triggered animations using intersection observer - **Modal/drawer entry**: Smooth slide + fade, backdrop fade, focus management +- **List rhythm**: Sibling stagger is legitimate for cards-in-a-grid or list-items-appearing. Whole-section fade-on-scroll is not a list and is not legitimate. Cap total stagger time: 10 items at 50ms each = 500ms total. For more items, reduce per-item delay or cap the staggered count. + + Use CSS custom properties for clean stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"`, `style="--i: 1"`, etc. on each item. ### Micro-interactions - **Button feedback**: @@ -97,11 +98,14 @@ Use appropriate techniques for each animation: ### Timing & Easing -**Durations by purpose:** -- **100-150ms**: Instant feedback (button press, toggle) -- **200-300ms**: State changes (hover, menu open) -- **300-500ms**: Layout changes (accordion, modal) -- **500-800ms**: Entrance animations (page load) +**Duration: the 100/300/500 rule.** Timing matters more than easing for "feels right": + +| Duration | Use Case | Examples | +|----------|----------|----------| +| **100–150ms** | Instant feedback | Button press, toggle, color change | +| **200–300ms** | State changes | Menu open, tooltip, hover state | +| **300–500ms** | Layout changes | Accordion, modal, drawer | +| **500–800ms** | Entrance animations | Page load, hero reveal | **Easing curves (use these, not CSS defaults):** ```css @@ -134,13 +138,35 @@ Use appropriate techniques for each animation: - GSAP for complex sequences ``` +### Motion Materials + +Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties. Match material to effect: + +- **Transform / opacity**: movement, press feedback, simple reveals, list choreography +- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances +- **Clip-path / masks**: wipes, reveals, editorial cropping, product-like transitions +- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state +- **Grid-template-rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly + +The hard rule isn't "transform and opacity only." It's: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify smoothness in-browser on target viewports. + ### Performance -- **Motion materials**: Use transform/opacity for reliable movement, but use blur, filters, masks, shadows, and color shifts when they materially improve the effect - **Layout safety**: Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins) -- **will-change**: Add sparingly for known expensive animations +- **will-change**: Add sparingly for known expensive animations only (e.g. on `:hover` or an `.animating` class), never preemptively across the whole page +- **Scroll triggers**: Use Intersection Observer instead of scroll event listeners; unobserve after the animation fires once - **Bound expensive effects**: Keep blur/filter/shadow areas small or isolated, use `contain` where appropriate - **Monitor FPS**: Ensure 60fps on target devices +### Perceived Performance + +Nobody cares how fast your site *is*, only how fast it feels. The 80ms threshold: anything under ~80ms feels instant because our brains buffer sensory input for that long to synchronize perception. Target this for micro-interactions. + +- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening. +- **Early completion**: Show content progressively, don't wait for everything (progressive images, streaming HTML, skeleton fade-ins). +- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Use for low-stakes actions (likes, follows). Avoid for payments or destructive operations. +- **Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances. +- **Caution**: Too-fast responses can decrease perceived value for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening. + ### Accessibility ```css @media (prefers-reduced-motion: reduce) { diff --git a/.claude/skills/impeccable/reference/bolder.md b/.claude/skills/impeccable/reference/bolder.md index 0aa20cac4..f190a5f81 100644 --- a/.claude/skills/impeccable/reference/bolder.md +++ b/.claude/skills/impeccable/reference/bolder.md @@ -50,7 +50,7 @@ Create a strategy to increase impact while maintaining coherence: Systematically increase impact across these dimensions: ### Typography Amplification -- **Replace generic fonts**: Swap system fonts for distinctive choices (see the parent skill's typography guidelines and [typography.md](typography.md) for inspiration) +- **Replace generic fonts**: Swap system fonts for distinctive choices (see the parent skill's typography guidelines and the [Reference Material section of typeset.md](typeset.md#reference-material) for inspiration) - **Extreme scale**: Create dramatic size jumps (3x-5x differences, not 1.5x) - **Weight contrast**: Pair 900 weights with 200 weights, not 600 with 400 - **Unexpected choices**: Variable fonts, display fonts for headlines, condensed/extended widths, monospace as intentional accent (not as lazy "dev tool" default) @@ -78,10 +78,10 @@ Systematically increase impact across these dimensions: - **Custom elements**: Illustrative elements, custom icons, decorative details that reinforce brand ### Motion & Animation -- **Entrance choreography**: Staggered, dramatic page load animations with 50-100ms delays -- **Scroll effects**: Parallax, reveal animations, scroll-triggered sequences -- **Micro-interactions**: Satisfying hover effects, click feedback, state changes -- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic, which cheapen the effect) +- **Hero moment**: One signature entrance, once. Not on every visit and not on every section. +- **Micro-interactions**: Satisfying hover effects, click feedback, state changes. +- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic, which cheapen the effect). +- **Bolder ≠ scroll-fade-rise on every section.** That's the saturated AI default, the opposite of bold. ### Composition Boldness - **Hero moments**: Create clear focal points with dramatic treatment diff --git a/.claude/skills/impeccable/reference/brand.md b/.claude/skills/impeccable/reference/brand.md index 3d83a1cdc..194514e95 100644 --- a/.claude/skills/impeccable/reference/brand.md +++ b/.claude/skills/impeccable/reference/brand.md @@ -43,17 +43,10 @@ The reflex-reject lists apply to **new design choices**. When the existing brand ### Pairing and voice -Distinctive + refined is the goal. The specific shape depends on the brand: - -- **Editorial / long-form / luxury**: display serif + sans body (a magazine shape). -- **Tech / dev tools / fintech**: one committed sans, usually; custom-tight tracking, strong weight contrast inside a single family. -- **Consumer / food / travel**: warmer pairings, often a humanist sans plus a script or display serif. -- **Creative studios / agencies**: rule-breaking welcome. Mono-only, or display-only, or custom-drawn type as voice. +Distinctive + refined is the goal. The specific shape depends on the brand, not on the brand's category. A category ("restaurant", "dev tool", "magazine", "fintech") is not a recipe; treating it as one is the first-order reflex SKILL.md warns against. Two families minimum is the rule *only* when the voice needs it. A single well-chosen family with committed weight/size contrast is stronger than a timid display+body pair. -Vary across projects. If the last brief was a serif-display landing page, this one isn't. - ### Scale Modular scale, fluid `clamp()` for headings, ≥1.25 ratio between steps. Flat scales (1.1× apart) read as uncommitted. @@ -74,8 +67,7 @@ Brand surfaces have permission for Committed, Full palette, and Drenched strateg - Asymmetric compositions are one option. Break the grid intentionally for emphasis. - Fluid spacing with `clamp()` that breathes on larger viewports. Vary for rhythm: generous separations, tight groupings. -- Alternative: a strict, visible grid as the voice (brutalist / Swiss / tech-spec aesthetics). Either asymmetric or rigorously-gridded can be "designed"; the failure mode is splitting the difference into a generic centered stack. -- Don't default to centering everything. Left-aligned with asymmetric layouts feels more designed; a strict grid reads as confident structure. A centered-stack hero with icon-title-subtitle cards reads as template. +- For image-led briefs (hotels, restaurants, magazines, photography), full-bleed hero imagery with overlaid menu and centered headline is a canonical move; let the photograph be the design. - When cards ARE the right affordance, use `grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` for breakpoint-free responsiveness. ## Imagery @@ -93,8 +85,7 @@ Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landin ## Motion -- One well-orchestrated page-load with staggered reveals beats scattered micro-interactions, when the brand invites it. Tech-minimal brands often skip entrance motion entirely; the restraint is the voice. -- For collapsing/expanding sections, transition `grid-template-rows` rather than `height`. +- One well-orchestrated page-load beats scattered micro-interactions, when the brand invites it. Some brands skip entrance motion entirely; the restraint is the voice. ## Brand bans (on top of the shared absolute bans) @@ -111,8 +102,7 @@ Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landin Brand can afford things product can't. Take them. -- Ambitious first-load motion. Reveals, scroll-triggered transitions, typographic choreography. +- Ambitious first-load motion. Reveals and typographic choreography that earn their place; not fade-on-scroll for every section. - Single-purpose viewports. One dominant idea per fold, long scroll, deliberate pacing. -- Typographic risk. Enormous display type, unexpected italic cuts, mixed cases, hand-drawn headlines, a single oversize word as a hero. - Unexpected color strategies. Palette IS voice; a calm brand and a restless brand should not share palette mechanics. - Art direction per section. Different sections can have different visual worlds if the narrative demands it. Consistency of voice beats consistency of treatment. diff --git a/.claude/skills/impeccable/reference/clarify.md b/.claude/skills/impeccable/reference/clarify.md index 5f7d420f4..00ee978c3 100644 --- a/.claude/skills/impeccable/reference/clarify.md +++ b/.claude/skills/impeccable/reference/clarify.md @@ -172,3 +172,117 @@ Test that copy improvements work: - **Tone**: Is it appropriate for the situation? When the copy reads cleanly, hand off to `/impeccable polish` for the final pass. + +--- + +## Reference Material + +The sections below were previously `ux-writing.md` and live inline now so the clarify flow has its deep UX-writing reference in one place. + +### UX Writing + +#### The Button Label Problem + +**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns: + +| Bad | Good | Why | +|-----|------|-----| +| OK | Save changes | Says what will happen | +| Submit | Create account | Outcome-focused | +| Yes | Delete message | Confirms the action | +| Cancel | Keep editing | Clarifies what "cancel" means | +| Click here | Download PDF | Describes the destination | + +**For destructive actions**, name the destruction: +- "Delete" not "Remove" (delete is permanent, remove implies recoverable) +- "Delete 5 items" not "Delete selected" (show the count) + +#### Error Messages: The Formula + +Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input". + +##### Error Message Templates + +| Situation | Template | +|-----------|----------| +| **Format error** | "[Field] needs to be [format]. Example: [example]" | +| **Missing required** | "Please enter [what's missing]" | +| **Permission denied** | "You don't have access to [thing]. [What to do instead]" | +| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." | +| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" | + +##### Don't Blame the User + +Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date". + +#### Empty States Are Opportunities + +Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items". + +#### Voice vs Tone + +**Voice** is your brand's personality, consistent everywhere. +**Tone** adapts to the moment. + +| Moment | Tone Shift | +|--------|------------| +| Success | Celebratory, brief: "Done! Your changes are live." | +| Error | Empathetic, helpful: "That didn't work. Here's what to try..." | +| Loading | Reassuring: "Saving your work..." | +| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | + +**Never use humor for errors.** Users are already frustrated. Be helpful, not cute. + +#### Writing for Accessibility + +**Link text** must have standalone meaning: "View pricing plans" not "Click here". **Alt text** describes information, not the image: "Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context. + +#### Writing for Translation + +##### Plan for Expansion + +German text is ~30% longer than English. Allocate space: + +| Language | Expansion | +|----------|-----------| +| German | +30% | +| French | +20% | +| Finnish | +30-40% | +| Chinese | -30% (fewer chars, but same width) | + +##### Translation-Friendly Patterns + +Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear. + +#### Consistency: The Terminology Problem + +Pick one term and stick with it: + +| Inconsistent | Consistent | +|--------------|------------| +| Delete / Remove / Trash | Delete | +| Settings / Preferences / Options | Settings | +| Sign in / Log in / Enter | Sign in | +| Create / Add / New | Create | + +Build a terminology glossary and enforce it. Variety creates confusion. + +#### Avoid Redundant Copy + +If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well. + +#### Loading States + +Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress. + +#### Confirmation Dialogs: Use Sparingly + +Most confirmation dialogs are design failures; consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No"). + +#### Form Instructions + +Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking. + +--- + +**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors. diff --git a/.claude/skills/impeccable/reference/codex.md b/.claude/skills/impeccable/reference/codex.md index f40ec746f..d563af95b 100644 --- a/.claude/skills/impeccable/reference/codex.md +++ b/.claude/skills/impeccable/reference/codex.md @@ -23,8 +23,8 @@ Before generating anything, run a brief direction conversation grounded in the s Ask **2-3 targeted questions** about visual lane, color strategy, atmosphere, and named anchor references. Don't enumerate generic menus; tie each question to the shape brief's answers. Example shape-grounded questions: -- "Brief says 'editorial restraint, Klim-adjacent.' Are we closer to a quiet specimen page or a magazine-spread feel with hero imagery?" -- "Palette strategy from shape was 'Committed.' Want it warm-grounded (deep oxblood + cream) or cool-grounded (slate + paper white)?" +- "Brief says 'specimen-page restraint.' Are we closer to a quiet typographic page or a wider editorial spread with hero imagery?" +- "Palette strategy from shape was 'Committed.' Which one color carries the surface (a brand-driven pick rather than a default warm-or-cool framing)? (And no, the answer isn't a cream/sand body bg; that's the saturated AI default.)" **STOP and wait for answers.** These pin the palette before any pixel gets generated. Do not proceed to Step B until the user has responded. diff --git a/.claude/skills/impeccable/reference/cognitive-load.md b/.claude/skills/impeccable/reference/cognitive-load.md deleted file mode 100644 index 48f8ad58b..000000000 --- a/.claude/skills/impeccable/reference/cognitive-load.md +++ /dev/null @@ -1,106 +0,0 @@ -# Cognitive Load Assessment - -Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload. - ---- - -## Three Types of Cognitive Load - -### Intrinsic Load: The Task Itself -Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it. - -**Manage it by**: -- Breaking complex tasks into discrete steps -- Providing scaffolding (templates, defaults, examples) -- Progressive disclosure: show what's needed now, hide the rest -- Grouping related decisions together - -### Extraneous Load: Bad Design -Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste. - -**Common sources**: -- Confusing navigation that requires mental mapping -- Unclear labels that force users to guess meaning -- Visual clutter competing for attention -- Inconsistent patterns that prevent learning -- Unnecessary steps between user intent and result - -### Germane Load: Learning Effort -Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery. - -**Support it by**: -- Progressive disclosure that reveals complexity gradually -- Consistent patterns that reward learning -- Feedback that confirms correct understanding -- Onboarding that teaches through action, not walls of text - ---- - -## Cognitive Load Checklist - -Evaluate the interface against these 8 items: - -- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements? -- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)? -- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)? -- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen? -- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next? -- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)? -- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one? -- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it? - -**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed). - ---- - -## The Working Memory Rule - -**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001). - -At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider: -- **≤4 items**: Within working memory limits, manageable -- **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure -- **8+ items**: Overloaded; users will skip, misclick, or abandon - -**Practical applications**: -- Navigation menus: ≤5 top-level items (group the rest under clear categories) -- Form sections: ≤4 fields visible per group before a visual break -- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu -- Dashboard widgets: ≤4 key metrics visible without scrolling -- Pricing tiers: ≤3 options (more causes analysis paralysis) - ---- - -## Common Cognitive Load Violations - -### 1. The Wall of Options -**Problem**: Presenting 10+ choices at once with no hierarchy. -**Fix**: Group into categories, highlight recommended, use progressive disclosure. - -### 2. The Memory Bridge -**Problem**: User must remember info from step 1 to complete step 3. -**Fix**: Keep relevant context visible, or repeat it where it's needed. - -### 3. The Hidden Navigation -**Problem**: User must build a mental map of where things are. -**Fix**: Always show current location (breadcrumbs, active states, progress indicators). - -### 4. The Jargon Barrier -**Problem**: Technical or domain language forces translation effort. -**Fix**: Use plain language. If domain terms are unavoidable, define them inline. - -### 5. The Visual Noise Floor -**Problem**: Every element has the same visual weight; nothing stands out. -**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted. - -### 6. The Inconsistent Pattern -**Problem**: Similar actions work differently in different places. -**Fix**: Standardize interaction patterns. Same type of action = same type of UI. - -### 7. The Multi-Task Demand -**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating). -**Fix**: Sequence the steps. Let the user do one thing at a time. - -### 8. The Context Switch -**Problem**: User must jump between screens/tabs/modals to gather info for a single decision. -**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth. diff --git a/.claude/skills/impeccable/reference/color-and-contrast.md b/.claude/skills/impeccable/reference/color-and-contrast.md deleted file mode 100644 index 110c2ee47..000000000 --- a/.claude/skills/impeccable/reference/color-and-contrast.md +++ /dev/null @@ -1,105 +0,0 @@ -# Color & Contrast - -## Color Spaces: Use OKLCH - -**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal, unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark. - -The OKLCH function takes three components: `oklch(lightness chroma hue)` where lightness is 0-100%, chroma is roughly 0-0.4, and hue is 0-360. To build a primary color and its lighter / darker variants, hold the chroma+hue roughly constant and vary the lightness, but **reduce chroma as you approach white or black**, because high chroma at extreme lightness looks garish. - -The hue you pick is a brand decision and should not come from a default. Do not reach for blue (hue 250) or warm orange (hue 60) by reflex; those are the dominant AI-design defaults, not the right answer for any specific brand. - -## Building Functional Palettes - -### Tinted Neutrals - -**Pure gray is dead.** A neutral with zero chroma feels lifeless next to a colored brand. Add a tiny chroma value (0.005-0.015) to all your neutrals, hued toward whatever your brand color is. The chroma is small enough not to read as "tinted" consciously, but it creates subconscious cohesion between brand color and UI surfaces. - -The hue you tint toward should come from THIS project's brand, not from a "warm = friendly, cool = tech" formula. If your brand color is teal, your neutrals lean toward teal. If your brand color is amber, they lean toward amber. The point is cohesion with the SPECIFIC brand, not a stock palette. - -**Avoid** the trap of always tinting toward warm orange or always tinting toward cool blue. Those are the two laziest defaults and they create their own monoculture across projects. - -### Palette Structure - -A complete system needs: - -| Role | Purpose | Example | -|------|---------|---------| -| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades | -| **Neutral** | Text, backgrounds, borders | 9-11 shade scale | -| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each | -| **Surface** | Cards, modals, overlays | 2-3 elevation levels | - -**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise. - -### The 60-30-10 Rule (Applied Correctly) - -This rule is about **visual weight**, not pixel count: - -- **60%**: Neutral backgrounds, white space, base surfaces -- **30%**: Secondary colors: text, borders, inactive states -- **10%**: Accent: CTAs, highlights, focus states - -The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power. - -## Contrast & Accessibility - -### WCAG Requirements - -| Content Type | AA Minimum | AAA Target | -|--------------|------------|------------| -| Body text | 4.5:1 | 7:1 | -| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | -| UI components, icons | 3:1 | 4.5:1 | -| Non-essential decorations | None | None | - -**The gotcha**: Placeholder text still needs 4.5:1. That light gray placeholder you see everywhere? Usually fails WCAG. - -### Dangerous Color Combinations - -These commonly fail contrast or cause readability issues: - -- Light gray text on white (the #1 accessibility fail) -- **Gray text on any colored background**: gray looks washed out and dead on color. Use a darker shade of the background color, or transparency -- Red text on green background (or vice versa): 8% of men can't distinguish these -- Blue text on red background (vibrates visually) -- Yellow text on white (almost always fails) -- Thin light text on images (unpredictable contrast) - -### Never Use Pure Gray or Pure Black - -Pure gray (`oklch(50% 0 0)`) and pure black (`#000`) don't exist in nature; real shadows and surfaces always have a color cast. Even a chroma of 0.005-0.01 is enough to feel natural without being obviously tinted. (See tinted neutrals example above.) - -### Testing - -Don't trust your eyes. Use tools: - -- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) -- Browser DevTools → Rendering → Emulate vision deficiencies -- [Polypane](https://polypane.app/) for real-time testing - -## Theming: Light & Dark Mode - -### Dark Mode Is Not Inverted Light Mode - -You can't just swap colors. Dark mode requires different design decisions: - -| Light Mode | Dark Mode | -|------------|-----------| -| Shadows for depth | Lighter surfaces for depth (no shadows) | -| Dark text on light | Light text on dark (reduce font weight) | -| Vibrant accents | Desaturate accents slightly | -| White backgrounds | Never pure black; use dark gray (oklch 12-18%) | - -In dark mode, depth comes from surface lightness, not shadow. Build a 3-step surface scale where higher elevations are lighter (e.g. 15% / 20% / 25% lightness). Use the SAME hue and chroma as your brand color (whatever it is for THIS project; do not reach for blue) and only vary the lightness. Reduce body text weight slightly (e.g. 350 instead of 400) because light text on dark reads as heavier than dark text on light. - -### Token Hierarchy - -Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer; primitives stay the same. - -## Alpha Is A Design Smell - -Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed. - ---- - -**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Using pure black (#000) for large areas. Skipping color blindness testing (8% of men affected). diff --git a/.claude/skills/impeccable/reference/colorize.md b/.claude/skills/impeccable/reference/colorize.md index 46b852316..95b7365af 100644 --- a/.claude/skills/impeccable/reference/colorize.md +++ b/.claude/skills/impeccable/reference/colorize.md @@ -68,10 +68,10 @@ Add color systematically across these dimensions: - **Hover states**: Introduce color on interaction ### Background & Surfaces -- **Tinted backgrounds**: Replace pure gray (`#f5f5f5`) with warm neutrals (`oklch(97% 0.01 60)`) or cool tints (`oklch(97% 0.01 250)`) +- **Tinted backgrounds**: If you replace pure gray, tint toward the brand hue, not toward a generic-warm-or-cool pair. The default-warm-tint (`oklch(97% 0.01 60)` and its neighbors) is now the AI cream/sand giveaway. Be specific to the brand or stay neutral. - **Colored sections**: Use subtle background colors to separate areas - **Gradient backgrounds**: Add depth with subtle, intentional gradients (not generic purple-blue) -- **Cards & surfaces**: Tint cards or surfaces slightly for warmth +- **Cards & surfaces**: Tint cards or surfaces toward the brand, not "for warmth" by reflex **Use OKLCH for color**: It's perceptually uniform, meaning equal steps in lightness *look* equal. Great for generating harmonious scales. @@ -124,8 +124,6 @@ Ensure color addition improves rather than overwhelms: - Use every color in the rainbow (choose 2-4 colors beyond neutrals) - Apply color randomly without semantic meaning - Put gray text on colored backgrounds. It looks washed out; use a darker shade of the background color or transparency instead -- Use pure gray for neutrals. Add subtle color tint (warm or cool) for depth -- Use pure black (`#000`) or pure white (`#fff`) for large areas - Violate WCAG contrast requirements - Use color as the only indicator (accessibility issue) - Make everything colorful (defeats the purpose) @@ -152,3 +150,108 @@ When invoked from live mode, each variant MUST declare a `color-amount` param so ``` Layer 1-2 variant-specific params on top: palette selection (`steps` with named options), temperature warmth, or tint vs. true color. See `reference/live.md` for the full params contract. + +--- + +## Reference Material + +The sections below were previously `color-and-contrast.md` and live inline now so the colorize flow has its deep color reference in one place. + +### Color & Contrast + +#### Color Spaces: Use OKLCH + +**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal, unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark. + +The OKLCH function takes three components: `oklch(lightness chroma hue)` where lightness is 0-100%, chroma is roughly 0-0.4, and hue is 0-360. To build a primary color and its lighter / darker variants, hold the chroma+hue roughly constant and vary the lightness, but **reduce chroma as you approach white or black**, because high chroma at extreme lightness looks garish. + +The hue you pick is a brand decision and should not come from a default. Do not reach for blue (hue 250) or warm orange (hue 60) by reflex; those are the dominant AI-design defaults, not the right answer for any specific brand. + +#### Building Functional Palettes + +##### Tinted Neutrals + +**Pure gray is dead.** A neutral with zero chroma feels lifeless next to a colored brand. Add a tiny chroma value (0.005-0.015) to all your neutrals, hued toward whatever your brand color is. The chroma is small enough not to read as "tinted" consciously, but it creates subconscious cohesion between brand color and UI surfaces. + +The hue you tint toward should come from THIS project's brand, not from a "warm = friendly, cool = tech" formula. If your brand color is teal, your neutrals lean toward teal. If your brand color is amber, they lean toward amber. The point is cohesion with the SPECIFIC brand, not a stock palette. + +**Avoid** the trap of always tinting toward warm orange or always tinting toward cool blue. Those are the two laziest defaults and they create their own monoculture across projects. + +##### Palette Structure + +A complete system needs: + +| Role | Purpose | Example | +|------|---------|---------| +| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades | +| **Neutral** | Text, backgrounds, borders | 9-11 shade scale | +| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each | +| **Surface** | Cards, modals, overlays | 2-3 elevation levels | + +**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise. + +##### The 60-30-10 Rule (Applied Correctly) + +This rule is about **visual weight**, not pixel count: + +- **60%**: Neutral backgrounds, white space, base surfaces +- **30%**: Secondary colors: text, borders, inactive states +- **10%**: Accent: CTAs, highlights, focus states + +The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power. + +#### Contrast & Accessibility + +##### WCAG Requirements + +| Content Type | AA Minimum | AAA Target | +|--------------|------------|------------| +| Body text | 4.5:1 | 7:1 | +| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | +| UI components, icons | 3:1 | 4.5:1 | +| Non-essential decorations | None | None | + +##### Dangerous Color Combinations + +These commonly fail contrast or cause readability issues: + +- Light gray text on white (the #1 accessibility fail) +- Red text on green background (or vice versa): 8% of men can't distinguish these +- Blue text on red background (vibrates visually) +- Yellow text on white (almost always fails) +- Thin light text on images (unpredictable contrast) + +##### Testing + +Don't trust your eyes. Use tools: + +- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) +- Browser DevTools → Rendering → Emulate vision deficiencies +- [Polypane](https://polypane.app/) for real-time testing + +#### Theming: Light & Dark Mode + +##### Dark Mode Is Not Inverted Light Mode + +You can't just swap colors. Dark mode requires different design decisions: + +| Light Mode | Dark Mode | +|------------|-----------| +| Shadows for depth | Lighter surfaces for depth (no shadows) | +| Dark text on light | Light text on dark (reduce font weight) | +| Vibrant accents | Desaturate accents slightly | +| White backgrounds | Either pure black or a deep surface that fits the brand (a brand-tinted near-black at oklch 12-18% works too) | + +In dark mode, depth comes from surface lightness, not shadow. Build a 3-step surface scale where higher elevations are lighter (e.g. 15% / 20% / 25% lightness). Use the SAME hue and chroma as your brand color (whatever it is for THIS project; do not reach for blue) and only vary the lightness. Reduce body text weight slightly (e.g. 350 instead of 400) because light text on dark reads as heavier than dark text on light. + +##### Token Hierarchy + +Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer; primitives stay the same. + +#### Alpha Is A Design Smell + +Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed. + +--- + +**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Skipping color blindness testing (8% of men affected). diff --git a/.claude/skills/impeccable/reference/craft.md b/.claude/skills/impeccable/reference/craft.md index 06094b1dd..2c7bd99b1 100644 --- a/.claude/skills/impeccable/reference/craft.md +++ b/.claude/skills/impeccable/reference/craft.md @@ -56,15 +56,15 @@ If the harness has native image generation (Codex), a compact shape's "confirm o Based on the design brief's "Recommended References" section, consult the relevant impeccable reference files. At minimum, always consult: -- [spatial-design.md](spatial-design.md) for layout and spacing -- [typography.md](typography.md) for type hierarchy +- [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 [motion-design.md](motion-design.md) -- Color-heavy or themed? Consult [color-and-contrast.md](color-and-contrast.md) -- Responsive requirements? Consult [responsive-design.md](responsive-design.md) -- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.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) diff --git a/.claude/skills/impeccable/reference/critique.md b/.claude/skills/impeccable/reference/critique.md index 4a5d6ab83..019122d27 100644 --- a/.claude/skills/impeccable/reference/critique.md +++ b/.claude/skills/impeccable/reference/critique.md @@ -38,9 +38,9 @@ Read relevant source files and visually inspect the live page when browser autom Evaluate: - **AI slop**: Would someone believe "AI made this" immediately? Check all DON'T guidance from the parent Impeccable skill. - **Holistic design**: hierarchy, IA, emotional fit, discoverability, composition, typography, color, accessibility, states, copy, and edge cases. -- **Cognitive load**: consult [cognitive-load](cognitive-load.md); report checklist failures and decision points with >4 visible options. +- **Cognitive load**: consult the [Cognitive Load Assessment](#cognitive-load-assessment) section below; report checklist failures and decision points with >4 visible options. - **Emotional journey**: peak-end rule, emotional valleys, reassurance at high-stakes moments. -- **Nielsen heuristics**: consult [heuristics-scoring](heuristics-scoring.md); score all 10 heuristics 0-4. +- **Nielsen heuristics**: consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below; score all 10 heuristics 0-4. Return: AI slop verdict, heuristic scores, cognitive load, emotional journey, 2-3 strengths, 3-5 priority issues, persona red flags, minor observations, and provocative questions. @@ -80,7 +80,7 @@ The chat response is the primary user-facing deliverable. Present the full struc Structure your feedback as a design director would: #### Design Health Score -> *Consult [heuristics-scoring](heuristics-scoring.md)* +> *Consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below.* Present the Nielsen's 10 heuristics scores as a table: @@ -119,16 +119,16 @@ Highlight 2-3 things done well. Be specific about why they work. #### Priority Issues The 3-5 most impactful design problems, ordered by importance. -For each issue, tag with **P0-P3 severity** (consult [heuristics-scoring](heuristics-scoring.md) for severity definitions): +For each issue, tag with **P0-P3 severity** (see [Issue Severity below](#issue-severity-p0p3) for definitions): - **[P?] What**: Name the problem clearly - **Why it matters**: How this hurts users or undermines goals - **Fix**: What to do about it (be concrete) - **Suggested command**: Which command could address this (from: /impeccable adapt, /impeccable animate, /impeccable audit, /impeccable bolder, /impeccable clarify, /impeccable colorize, /impeccable critique, /impeccable delight, /impeccable distill, /impeccable document, /impeccable harden, /impeccable layout, /impeccable onboard, /impeccable optimize, /impeccable overdrive, /impeccable polish, /impeccable quieter, /impeccable shape, /impeccable typeset) #### Persona Red Flags -> *Consult [personas](personas.md)* +> *Consult the [Personas reference](#persona-based-design-testing) below.* -Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `CLAUDE.md` contains a `## Design Context` section from `impeccable teach`, also generate 1-2 project-specific personas from the audience/brand info. +Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `CLAUDE.md` contains a `## Design Context` section from `impeccable init`, also generate 1-2 project-specific personas from the audience/brand info. For each selected persona, walk through the primary user action and list specific red flags found: @@ -234,3 +234,534 @@ After presenting the summary, tell the user: > You can ask me to run these one at a time, all at once, or in any order you prefer. > > Re-run `/impeccable critique` after fixes to see your score improve. + +--- + +## Reference Material + +The sections below were previously separate reference files (`cognitive-load.md`, `heuristics-scoring.md`, `personas.md`). They live inline now so the critique flow has all its deep context in one place. + +### Cognitive Load Assessment + +Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload. + +--- + +#### Three Types of Cognitive Load + +##### Intrinsic Load: The Task Itself +Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it. + +**Manage it by**: +- Breaking complex tasks into discrete steps +- Providing scaffolding (templates, defaults, examples) +- Progressive disclosure: show what's needed now, hide the rest +- Grouping related decisions together + +##### Extraneous Load: Bad Design +Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste. + +**Common sources**: +- Confusing navigation that requires mental mapping +- Unclear labels that force users to guess meaning +- Visual clutter competing for attention +- Inconsistent patterns that prevent learning +- Unnecessary steps between user intent and result + +##### Germane Load: Learning Effort +Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery. + +**Support it by**: +- Progressive disclosure that reveals complexity gradually +- Consistent patterns that reward learning +- Feedback that confirms correct understanding +- Onboarding that teaches through action, not walls of text + +--- + +#### Cognitive Load Checklist + +Evaluate the interface against these 8 items: + +- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements? +- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)? +- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)? +- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen? +- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next? +- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)? +- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one? +- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it? + +**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed). + +--- + +#### The Working Memory Rule + +**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001). + +At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider: +- **≤4 items**: Within working memory limits, manageable +- **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure +- **8+ items**: Overloaded; users will skip, misclick, or abandon + +**Practical applications**: +- Navigation menus: ≤5 top-level items (group the rest under clear categories) +- Form sections: ≤4 fields visible per group before a visual break +- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu +- Dashboard widgets: ≤4 key metrics visible without scrolling +- Pricing tiers: ≤3 options (more causes analysis paralysis) + +--- + +#### Common Cognitive Load Violations + +##### 1. The Wall of Options +**Problem**: Presenting 10+ choices at once with no hierarchy. +**Fix**: Group into categories, highlight recommended, use progressive disclosure. + +##### 2. The Memory Bridge +**Problem**: User must remember info from step 1 to complete step 3. +**Fix**: Keep relevant context visible, or repeat it where it's needed. + +##### 3. The Hidden Navigation +**Problem**: User must build a mental map of where things are. +**Fix**: Always show current location (breadcrumbs, active states, progress indicators). + +##### 4. The Jargon Barrier +**Problem**: Technical or domain language forces translation effort. +**Fix**: Use plain language. If domain terms are unavoidable, define them inline. + +##### 5. The Visual Noise Floor +**Problem**: Every element has the same visual weight; nothing stands out. +**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted. + +##### 6. The Inconsistent Pattern +**Problem**: Similar actions work differently in different places. +**Fix**: Standardize interaction patterns. Same type of action = same type of UI. + +##### 7. The Multi-Task Demand +**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating). +**Fix**: Sequence the steps. Let the user do one thing at a time. + +##### 8. The Context Switch +**Problem**: User must jump between screens/tabs/modals to gather info for a single decision. +**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth. + +--- + +### Heuristics Scoring Guide + +Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough." + +#### Nielsen's 10 Heuristics + +##### 1. Visibility of System Status + +Keep users informed about what's happening through timely, appropriate feedback. + +**Check for**: +- Loading indicators during async operations +- Confirmation of user actions (save, submit, delete) +- Progress indicators for multi-step processes +- Current location in navigation (breadcrumbs, active states) +- Form validation feedback (inline, not just on submit) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | No feedback; user is guessing what happened | +| 1 | Rare feedback; most actions produce no visible response | +| 2 | Partial; some states communicated, major gaps remain | +| 3 | Good; most operations give clear feedback, minor gaps | +| 4 | Excellent; every action confirms, progress is always visible | + +##### 2. Match Between System and Real World + +Speak the user's language. Follow real-world conventions. Information appears in natural, logical order. + +**Check for**: +- Familiar terminology (no unexplained jargon) +- Logical information order matching user expectations +- Recognizable icons and metaphors +- Domain-appropriate language for the target audience +- Natural reading flow (left-to-right, top-to-bottom priority) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Pure tech jargon, alien to users | +| 1 | Mostly confusing; requires domain expertise to navigate | +| 2 | Mixed; some plain language, some jargon leaks through | +| 3 | Mostly natural; occasional term needs context | +| 4 | Speaks the user's language fluently throughout | + +##### 3. User Control and Freedom + +Users need a clear "emergency exit" from unwanted states without extended dialogue. + +**Check for**: +- Undo/redo functionality +- Cancel buttons on forms and modals +- Clear navigation back to safety (home, previous) +- Easy way to clear filters, search, selections +- Escape from long or multi-step processes + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Users get trapped; no way out without refreshing | +| 1 | Difficult exits; must find obscure paths to escape | +| 2 | Some exits; main flows have escape, edge cases don't | +| 3 | Good control; users can exit and undo most actions | +| 4 | Full control; undo, cancel, back, and escape everywhere | + +##### 4. Consistency and Standards + +Users shouldn't wonder whether different words, situations, or actions mean the same thing. + +**Check for**: +- Consistent terminology throughout the interface +- Same actions produce same results everywhere +- Platform conventions followed (standard UI patterns) +- Visual consistency (colors, typography, spacing, components) +- Consistent interaction patterns (same gesture = same behavior) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Inconsistent everywhere; feels like different products stitched together | +| 1 | Many inconsistencies; similar things look/behave differently | +| 2 | Partially consistent; main flows match, details diverge | +| 3 | Mostly consistent; occasional deviation, nothing confusing | +| 4 | Fully consistent; cohesive system, predictable behavior | + +##### 5. Error Prevention + +Better than good error messages is a design that prevents problems in the first place. + +**Check for**: +- Confirmation before destructive actions (delete, overwrite) +- Constraints preventing invalid input (date pickers, dropdowns) +- Smart defaults that reduce errors +- Clear labels that prevent misunderstanding +- Autosave and draft recovery + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Errors easy to make; no guardrails anywhere | +| 1 | Few safeguards; some inputs validated, most aren't | +| 2 | Partial prevention; common errors caught, edge cases slip | +| 3 | Good prevention; most error paths blocked proactively | +| 4 | Excellent; errors nearly impossible through smart constraints | + +##### 6. Recognition Rather Than Recall + +Minimize memory load. Make objects, actions, and options visible or easily retrievable. + +**Check for**: +- Visible options (not buried in hidden menus) +- Contextual help when needed (tooltips, inline hints) +- Recent items and history +- Autocomplete and suggestions +- Labels on icons (not icon-only navigation) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Heavy memorization; users must remember paths and commands | +| 1 | Mostly recall; many hidden features, few visible cues | +| 2 | Some aids; main actions visible, secondary features hidden | +| 3 | Good recognition; most things discoverable, few memory demands | +| 4 | Everything discoverable; users never need to memorize | + +##### 7. Flexibility and Efficiency of Use + +Accelerators, invisible to novices, speed up expert interaction. + +**Check for**: +- Keyboard shortcuts for common actions +- Customizable interface elements +- Recent items and favorites +- Bulk/batch actions +- Power user features that don't complicate the basics + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | One rigid path; no shortcuts or alternatives | +| 1 | Limited flexibility; few alternatives to the main path | +| 2 | Some shortcuts; basic keyboard support, limited bulk actions | +| 3 | Good accelerators; keyboard nav, some customization | +| 4 | Highly flexible; multiple paths, power features, customizable | + +##### 8. Aesthetic and Minimalist Design + +Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose. + +**Check for**: +- Only necessary information visible at each step +- Clear visual hierarchy directing attention +- Purposeful use of color and emphasis +- No decorative clutter competing for attention +- Focused, uncluttered layouts + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Overwhelming; everything competes for attention equally | +| 1 | Cluttered; too much noise, hard to find what matters | +| 2 | Some clutter; main content clear, periphery noisy | +| 3 | Mostly clean; focused design, minor visual noise | +| 4 | Perfectly minimal; every element earns its pixel | + +##### 9. Help Users Recognize, Diagnose, and Recover from Errors + +Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution. + +**Check for**: +- Plain language error messages (no error codes for users) +- Specific problem identification ("Email is missing @" not "Invalid input") +- Actionable recovery suggestions +- Errors displayed near the source of the problem +- Non-blocking error handling (don't wipe the form) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Cryptic errors; codes, jargon, or no message at all | +| 1 | Vague errors; "Something went wrong" with no guidance | +| 2 | Clear but unhelpful; names the problem but not the fix | +| 3 | Clear with suggestions; identifies problem and offers next steps | +| 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work | + +##### 10. Help and Documentation + +Even if the system is usable without docs, help should be easy to find, task-focused, and concise. + +**Check for**: +- Searchable help or documentation +- Contextual help (tooltips, inline hints, guided tours) +- Task-focused organization (not feature-organized) +- Concise, scannable content +- Easy access without leaving current context + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | No help available anywhere | +| 1 | Help exists but hard to find or irrelevant | +| 2 | Basic help; FAQ or docs exist, not contextual | +| 3 | Good documentation; searchable, mostly task-focused | +| 4 | Excellent contextual help; right info at the right moment | + +--- + +#### Score Summary + +**Total possible**: 40 points (10 heuristics × 4 max) + +| Score Range | Rating | What It Means | +|-------------|--------|---------------| +| 36–40 | Excellent | Minor polish only; ship it | +| 28–35 | Good | Address weak areas, solid foundation | +| 20–27 | Acceptable | Significant improvements needed before users are happy | +| 12–19 | Poor | Major UX overhaul required; core experience broken | +| 0–11 | Critical | Redesign needed; unusable in current state | + +--- + +#### Issue Severity (P0–P3) + +Tag each individual issue found during scoring with a priority level: + +| Priority | Name | Description | Action | +|----------|------|-------------|--------| +| **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper | +| **P1** | Major | Causes significant difficulty or confusion | Fix before release | +| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass | +| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits | + +**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1. + +--- + +### Persona-Based Design Testing + +Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss. + +**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns. + +--- + +#### 1. Impatient Power User: "Alex" + +**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave. + +**Behaviors**: +- Skips all onboarding and instructions +- Looks for keyboard shortcuts immediately +- Tries to bulk-select, batch-edit, and automate +- Gets frustrated by required steps that feel unnecessary +- Abandons if anything feels slow or patronizing + +**Test Questions**: +- Can Alex complete the core task in under 60 seconds? +- Are there keyboard shortcuts for common actions? +- Can onboarding be skipped entirely? +- Do modals have keyboard dismiss (Esc)? +- Is there a "power user" path (shortcuts, bulk actions)? + +**Red Flags** (report these specifically): +- Forced tutorials or unskippable onboarding +- No keyboard navigation for primary actions +- Slow animations that can't be skipped +- One-item-at-a-time workflows where batch would be natural +- Redundant confirmation steps for low-risk actions + +--- + +#### 2. Confused First-Timer: "Jordan" + +**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out. + +**Behaviors**: +- Reads all instructions carefully +- Hesitates before clicking anything unfamiliar +- Looks for help or support constantly +- Misunderstands jargon and abbreviations +- Takes the most literal interpretation of any label + +**Test Questions**: +- Is the first action obviously clear within 5 seconds? +- Are all icons labeled with text? +- Is there contextual help at decision points? +- Does terminology assume prior knowledge? +- Is there a clear "back" or "undo" at every step? + +**Red Flags** (report these specifically): +- Icon-only navigation with no labels +- Technical jargon without explanation +- No visible help option or guidance +- Ambiguous next steps after completing an action +- No confirmation that an action succeeded + +--- + +#### 3. Accessibility-Dependent User: "Sam" + +**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences. + +**Behaviors**: +- Tabs through the interface linearly +- Relies on ARIA labels and heading structure +- Cannot see hover states or visual-only indicators +- Needs adequate color contrast (4.5:1 minimum) +- May use browser zoom up to 200% + +**Test Questions**: +- Can the entire primary flow be completed keyboard-only? +- Are all interactive elements focusable with visible focus indicators? +- Do images have meaningful alt text? +- Is color contrast WCAG AA compliant (4.5:1 for text)? +- Does the screen reader announce state changes (loading, success, errors)? + +**Red Flags** (report these specifically): +- Click-only interactions with no keyboard alternative +- Missing or invisible focus indicators +- Meaning conveyed by color alone (red = error, green = success) +- Unlabeled form fields or buttons +- Time-limited actions without extension option +- Custom components that break screen reader flow + +--- + +#### 4. Deliberate Stress Tester: "Riley" + +**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience. + +**Behaviors**: +- Tests edge cases intentionally (empty states, long strings, special characters) +- Submits forms with unexpected data (emoji, RTL text, very long values) +- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs +- Looks for inconsistencies between what the UI promises and what actually happens +- Documents problems methodically + +**Test Questions**: +- What happens at the edges (0 items, 1000 items, very long text)? +- Do error states recover gracefully or leave the UI in a broken state? +- What happens on refresh mid-workflow? Is state preserved? +- Are there features that appear to work but produce broken results? +- How does the UI handle unexpected input (emoji, special chars, paste from Excel)? + +**Red Flags** (report these specifically): +- Features that appear to work but silently fail or produce wrong results +- Error handling that exposes technical details or leaves UI in a broken state +- Empty states that show nothing useful ("No results" with no guidance) +- Workflows that lose user data on refresh or navigation +- Inconsistent behavior between similar interactions in different parts of the UI + +--- + +#### 5. Distracted Mobile User: "Casey" + +**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection. + +**Behaviors**: +- Uses thumb only; prefers bottom-of-screen actions +- Gets interrupted mid-flow and returns later +- Switches between apps frequently +- Has limited attention span and low patience +- Types as little as possible, prefers taps and selections + +**Test Questions**: +- Are primary actions in the thumb zone (bottom half of screen)? +- Is state preserved if the user leaves and returns? +- Does it work on slow connections (3G)? +- Can forms use autocomplete and smart defaults? +- Are touch targets at least 44×44pt? + +**Red Flags** (report these specifically): +- Important actions positioned at the top of the screen (unreachable by thumb) +- No state persistence; progress lost on tab switch or interruption +- Large text inputs required where selection would work +- Heavy assets loading on every page (no lazy loading) +- Tiny tap targets or targets too close together + +--- + +#### Selecting Personas + +Choose personas based on the interface type: + +| Interface Type | Primary Personas | Why | +|---------------|-----------------|-----| +| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile | +| Dashboard / admin | Alex, Sam | Power users, accessibility | +| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity | +| Onboarding flow | Jordan, Casey | Confusion, interruption | +| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav | +| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile | + +--- + +#### Project-Specific Personas + +If `CLAUDE.md` contains a `## Design Context` section (generated by `impeccable init`), derive 1–2 additional personas from the audience and brand information: + +1. Read the target audience description +2. Identify the primary user archetype not covered by the 5 predefined personas +3. Create a persona following this template: + +``` +##### [Role]: "[Name]" + +**Profile**: [2-3 key characteristics derived from Design Context] + +**Behaviors**: [3-4 specific behaviors based on the described audience] + +**Red Flags**: [3-4 things that would alienate this specific user type] +``` + +Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists. diff --git a/.claude/skills/impeccable/reference/document.md b/.claude/skills/impeccable/reference/document.md index a091ec98e..3f42f319c 100644 --- a/.claude/skills/impeccable/reference/document.md +++ b/.claude/skills/impeccable/reference/document.md @@ -45,7 +45,7 @@ Rules that matter: - **Token refs** use `{path.to.token}` (e.g. `{colors.primary}`, `{rounded.md}`). Components may reference primitives; primitives may not reference each other. - **Stitch validates colors as hex sRGB only** (`#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`); OKLCH/HSL/P3 trigger a linter warning, not a hard error. YAML accepts the string either way and our own parser is format-agnostic. Choose based on project posture: (a) if the project has an "OKLCH-only" doctrine or uses Display-P3 values that don't round-trip through sRGB, put OKLCH directly in the frontmatter and accept the Stitch linter warning; (b) if the project wants strict Stitch compliance or plans to use their Tailwind/DTCG export pipeline, put hex in the frontmatter and keep OKLCH in prose as the canonical reference. Never split the source of truth without explicit reason. - **Component sub-tokens** are limited to 8 props: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, `width`. Shadows, motion, focus rings, backdrop-filter: none of those fit. Carry them in the sidecar (Step 4b). -- **Scale keys are open-ended.** Use whatever names the project already uses (`warm-ash-cream`, `surface-container-low`). Don't rename to Material defaults. +- **Scale keys are open-ended.** Use whatever names the project already uses (`oxblood-deep`, `surface-container-low`). Don't rename to Material defaults. - **Variants are naming convention, not schema.** `button-primary` / `button-primary-hover` / `button-primary-active` as sibling keys. ## The markdown body: six sections (exact order) @@ -61,7 +61,7 @@ Optional evocative subtitles are allowed in the form `## 2. Colors: The [Name] P ## When to run -- The user just ran `/impeccable teach` and needs the visual side documented. +- The user just ran `/impeccable init` and needs the visual side documented. - The skill noticed no `DESIGN.md` exists and nudged the user to create one. - An existing `DESIGN.md` is stale (the design has drifted). - Before a large redesign, to capture the current state as a reference. @@ -71,7 +71,7 @@ If a `DESIGN.md` already exists, **do not silently overwrite it**. Show the user ## Two paths - **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 teach, nothing built yet). Interview for five high-level answers, write a minimal DESIGN.md marked ``. Re-run in scan mode once there's code. +- **Seed mode**: the project is pre-implementation (fresh init, nothing built yet). Interview for five high-level answers, 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` forces seed mode regardless of code presence. @@ -103,7 +103,7 @@ Build a structured draft from the discovered tokens. For each token class: From the auto-extracted tokens, draft the YAML frontmatter now (you'll write it at the top of DESIGN.md in Step 4). This is the machine-readable layer: what the live panel and Stitch's linter consume. -- **Colors**: one entry per extracted color. Key = descriptive slug (`warm-ash-cream`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. +- **Colors**: one entry per extracted color. Key = descriptive slug (`oxblood-deep`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. - **Typography**: one entry per role (`display`, `headline`, `title`, `body`, `label`). Typography is an object; include only the props that are real for the project (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`). - **Rounded / Spacing**: whatever scale steps the project actually uses, keyed by whatever scale name the project uses (`sm` / `md` / `lg`, or `surface-sm`, or numeric steps). - **Components**: one entry per variant (`button-primary`, `button-primary-hover`, `button-ghost`). Reference primitives via `{colors.X}`, `{rounded.Y}`. If a variant needs a property Stitch's 8-prop set doesn't cover (shadow, focus ring, backdrop-filter), carry the full snippet in the sidecar instead. @@ -253,7 +253,7 @@ Regenerate the sidecar whenever you regenerate root `DESIGN.md`. If the user onl "extensions": { "colorMeta": { "primary": { "role": "primary", "displayName": "Editorial Magenta", "canonical": "oklch(60% 0.25 350)", "tonalRamp": ["...", "...", "..."] }, - "warm-ash-cream": { "role": "neutral", "displayName": "Warm Ash Cream", "canonical": "oklch(96% 0.005 350)", "tonalRamp": ["...", "...", "..."] } + "cool-paper": { "role": "neutral", "displayName": "Cool Paper", "canonical": "oklch(96% 0.005 230)", "tonalRamp": ["...", "...", "..."] } }, "typographyMeta": { "display": { "displayName": "Display", "purpose": "Hero headlines only." } @@ -328,12 +328,13 @@ Pull directly from the DESIGN.md you just wrote: Do not reword. The panel shows these as secondary collapsible context; the same voice that's in the Markdown carries through. -### Step 5: Confirm, refine, and refresh session cache +### Step 5: Confirm and refine 1. Show the user the full DESIGN.md you wrote. Briefly highlight the non-obvious creative choices (descriptive color names, atmosphere language, named rules). 2. Mention that `.impeccable/design.json` was also written alongside; the live panel will now render this project's actual button/input/nav primitives instead of generic approximations. 3. Offer to refine any section: "Want me to revise a section, add component patterns I missed, or adjust the atmosphere language?" -4. **Refresh the session cache.** Run `node .claude/skills/impeccable/scripts/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. + +Your own write is the freshest source; subsequent commands in this session don't need a reload. ## Seed mode @@ -394,11 +395,12 @@ Per-section guidance in seed mode: 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. -### Step 4: Confirm and refresh session cache +### Step 4: Confirm 1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment). 2. Tell the user: "Re-run `/impeccable document` once you have some code. That pass will extract real tokens and generate the sidecar." -3. Run `node .claude/skills/impeccable/scripts/load-context.mjs` once so the seed lands in conversation for the rest of the session. + +Your own write is the freshest source; no reload needed. ## Style guidelines diff --git a/.claude/skills/impeccable/reference/heuristics-scoring.md b/.claude/skills/impeccable/reference/heuristics-scoring.md deleted file mode 100644 index edbe5028d..000000000 --- a/.claude/skills/impeccable/reference/heuristics-scoring.md +++ /dev/null @@ -1,234 +0,0 @@ -# Heuristics Scoring Guide - -Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough." - -## Nielsen's 10 Heuristics - -### 1. Visibility of System Status - -Keep users informed about what's happening through timely, appropriate feedback. - -**Check for**: -- Loading indicators during async operations -- Confirmation of user actions (save, submit, delete) -- Progress indicators for multi-step processes -- Current location in navigation (breadcrumbs, active states) -- Form validation feedback (inline, not just on submit) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | No feedback; user is guessing what happened | -| 1 | Rare feedback; most actions produce no visible response | -| 2 | Partial; some states communicated, major gaps remain | -| 3 | Good; most operations give clear feedback, minor gaps | -| 4 | Excellent; every action confirms, progress is always visible | - -### 2. Match Between System and Real World - -Speak the user's language. Follow real-world conventions. Information appears in natural, logical order. - -**Check for**: -- Familiar terminology (no unexplained jargon) -- Logical information order matching user expectations -- Recognizable icons and metaphors -- Domain-appropriate language for the target audience -- Natural reading flow (left-to-right, top-to-bottom priority) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Pure tech jargon, alien to users | -| 1 | Mostly confusing; requires domain expertise to navigate | -| 2 | Mixed; some plain language, some jargon leaks through | -| 3 | Mostly natural; occasional term needs context | -| 4 | Speaks the user's language fluently throughout | - -### 3. User Control and Freedom - -Users need a clear "emergency exit" from unwanted states without extended dialogue. - -**Check for**: -- Undo/redo functionality -- Cancel buttons on forms and modals -- Clear navigation back to safety (home, previous) -- Easy way to clear filters, search, selections -- Escape from long or multi-step processes - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Users get trapped; no way out without refreshing | -| 1 | Difficult exits; must find obscure paths to escape | -| 2 | Some exits; main flows have escape, edge cases don't | -| 3 | Good control; users can exit and undo most actions | -| 4 | Full control; undo, cancel, back, and escape everywhere | - -### 4. Consistency and Standards - -Users shouldn't wonder whether different words, situations, or actions mean the same thing. - -**Check for**: -- Consistent terminology throughout the interface -- Same actions produce same results everywhere -- Platform conventions followed (standard UI patterns) -- Visual consistency (colors, typography, spacing, components) -- Consistent interaction patterns (same gesture = same behavior) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Inconsistent everywhere; feels like different products stitched together | -| 1 | Many inconsistencies; similar things look/behave differently | -| 2 | Partially consistent; main flows match, details diverge | -| 3 | Mostly consistent; occasional deviation, nothing confusing | -| 4 | Fully consistent; cohesive system, predictable behavior | - -### 5. Error Prevention - -Better than good error messages is a design that prevents problems in the first place. - -**Check for**: -- Confirmation before destructive actions (delete, overwrite) -- Constraints preventing invalid input (date pickers, dropdowns) -- Smart defaults that reduce errors -- Clear labels that prevent misunderstanding -- Autosave and draft recovery - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Errors easy to make; no guardrails anywhere | -| 1 | Few safeguards; some inputs validated, most aren't | -| 2 | Partial prevention; common errors caught, edge cases slip | -| 3 | Good prevention; most error paths blocked proactively | -| 4 | Excellent; errors nearly impossible through smart constraints | - -### 6. Recognition Rather Than Recall - -Minimize memory load. Make objects, actions, and options visible or easily retrievable. - -**Check for**: -- Visible options (not buried in hidden menus) -- Contextual help when needed (tooltips, inline hints) -- Recent items and history -- Autocomplete and suggestions -- Labels on icons (not icon-only navigation) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Heavy memorization; users must remember paths and commands | -| 1 | Mostly recall; many hidden features, few visible cues | -| 2 | Some aids; main actions visible, secondary features hidden | -| 3 | Good recognition; most things discoverable, few memory demands | -| 4 | Everything discoverable; users never need to memorize | - -### 7. Flexibility and Efficiency of Use - -Accelerators, invisible to novices, speed up expert interaction. - -**Check for**: -- Keyboard shortcuts for common actions -- Customizable interface elements -- Recent items and favorites -- Bulk/batch actions -- Power user features that don't complicate the basics - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | One rigid path; no shortcuts or alternatives | -| 1 | Limited flexibility; few alternatives to the main path | -| 2 | Some shortcuts; basic keyboard support, limited bulk actions | -| 3 | Good accelerators; keyboard nav, some customization | -| 4 | Highly flexible; multiple paths, power features, customizable | - -### 8. Aesthetic and Minimalist Design - -Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose. - -**Check for**: -- Only necessary information visible at each step -- Clear visual hierarchy directing attention -- Purposeful use of color and emphasis -- No decorative clutter competing for attention -- Focused, uncluttered layouts - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Overwhelming; everything competes for attention equally | -| 1 | Cluttered; too much noise, hard to find what matters | -| 2 | Some clutter; main content clear, periphery noisy | -| 3 | Mostly clean; focused design, minor visual noise | -| 4 | Perfectly minimal; every element earns its pixel | - -### 9. Help Users Recognize, Diagnose, and Recover from Errors - -Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution. - -**Check for**: -- Plain language error messages (no error codes for users) -- Specific problem identification ("Email is missing @" not "Invalid input") -- Actionable recovery suggestions -- Errors displayed near the source of the problem -- Non-blocking error handling (don't wipe the form) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Cryptic errors; codes, jargon, or no message at all | -| 1 | Vague errors; "Something went wrong" with no guidance | -| 2 | Clear but unhelpful; names the problem but not the fix | -| 3 | Clear with suggestions; identifies problem and offers next steps | -| 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work | - -### 10. Help and Documentation - -Even if the system is usable without docs, help should be easy to find, task-focused, and concise. - -**Check for**: -- Searchable help or documentation -- Contextual help (tooltips, inline hints, guided tours) -- Task-focused organization (not feature-organized) -- Concise, scannable content -- Easy access without leaving current context - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | No help available anywhere | -| 1 | Help exists but hard to find or irrelevant | -| 2 | Basic help; FAQ or docs exist, not contextual | -| 3 | Good documentation; searchable, mostly task-focused | -| 4 | Excellent contextual help; right info at the right moment | - ---- - -## Score Summary - -**Total possible**: 40 points (10 heuristics × 4 max) - -| Score Range | Rating | What It Means | -|-------------|--------|---------------| -| 36–40 | Excellent | Minor polish only; ship it | -| 28–35 | Good | Address weak areas, solid foundation | -| 20–27 | Acceptable | Significant improvements needed before users are happy | -| 12–19 | Poor | Major UX overhaul required; core experience broken | -| 0–11 | Critical | Redesign needed; unusable in current state | - ---- - -## Issue Severity (P0–P3) - -Tag each individual issue found during scoring with a priority level: - -| Priority | Name | Description | Action | -|----------|------|-------------|--------| -| **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper | -| **P1** | Major | Causes significant difficulty or confusion | Fix before release | -| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass | -| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits | - -**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1. diff --git a/plugin/skills/impeccable/reference/teach.md b/.claude/skills/impeccable/reference/init.md similarity index 58% rename from plugin/skills/impeccable/reference/teach.md rename to .claude/skills/impeccable/reference/init.md index 34a189055..db6bacb22 100644 --- a/plugin/skills/impeccable/reference/teach.md +++ b/.claude/skills/impeccable/reference/init.md @@ -1,21 +1,16 @@ -# Teach Flow +# Init Flow -Gathers design context for a project and writes two complementary files at the project root: +The setup command for a project. One codebase crawl feeds everything it writes: - **PRODUCT.md** (strategic): root project file for register, target users, product purpose, brand personality, anti-references, strategic design principles. Answers "who/what/why". - **DESIGN.md** (visual): root project file for visual theme, color palette, typography, components, layout. Follows the [Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/). Answers "how it looks". +- **`.impeccable/live/config.json`** (live mode): pre-configured so `/impeccable live` boots straight into variant mode with no first-time detour. -Every other impeccable command reads these files before doing any work. +It closes by pointing the user at the best command to run next. Every other impeccable command reads PRODUCT.md and DESIGN.md before doing any work. ## Step 1: Load current state -Run the shared loader first so you know what already exists: - -```bash -node .claude/skills/impeccable/scripts/load-context.mjs -``` - -The output tells you whether PRODUCT.md and/or DESIGN.md already exist. If `migrated: true`, legacy `.impeccable.md` was auto-renamed to `PRODUCT.md`. Mention this once to the user. +Check what already exists. PRODUCT.md and DESIGN.md live at the project root, or under `.agents/context/` or `docs/` (case-insensitive). Read whichever are present with your native file tool. Also note whether `.impeccable/live/config.json` already exists (Step 6 leaves it untouched if so). Decision tree: - **Neither file exists (empty project or no context yet)**: do Steps 2-4 (write PRODUCT.md), then decide on DESIGN.md based on whether there's code to analyze. @@ -26,14 +21,14 @@ Decision tree: Never silently overwrite an existing file. Always confirm first. -If teach was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete teach, re-run the loader, then resume the original command with the freshly loaded context. For craft, resume into shape next; teach creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief. +If init was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete init, then resume the original command. Your own writes are the freshest source; no reload needed. For craft, resume into shape next; init creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief. ## Step 2: Explore the codebase -Before asking questions, thoroughly scan the project to discover what you can: +Before asking questions, thoroughly scan the project to discover what you can. This single crawl feeds PRODUCT.md, DESIGN.md, **and** the live-mode framework detection in Step 6, so be thorough once rather than re-scanning later: - **README and docs**: Project purpose, target audience, any stated goals -- **Package.json / config files**: Tech stack, dependencies, existing design libraries +- **Package.json / config files**: Tech stack, dependencies, existing design libraries, **and the framework** (Vite/SPA, Next.js, Nuxt, SvelteKit, Astro, multi-page static) plus the HTML entry the browser actually loads - **Existing components**: Current design patterns, spacing, typography in use - **Brand assets**: Logos, favicons, color values already defined - **Design tokens / CSS variables**: Existing color palettes, font stacks, spacing scales @@ -46,7 +41,7 @@ Also form a **register hypothesis** from what you find: Register is a hypothesis at this point, not a decision; Step 3 confirms it. -Note what you've learned and what remains unclear. This exploration feeds both PRODUCT.md and DESIGN.md. +Note what you've learned and what remains unclear. Also note any rough edges worth a follow-up command (thin hierarchy, flat or gray palette, missing error/empty states, dull copy); Step 7 turns these into concrete recommendations without re-analyzing. ## Step 3: Ask strategic questions (for PRODUCT.md) @@ -84,8 +79,7 @@ If the signal is genuinely split (e.g. a product with a big marketing landing), ### Brand & Personality - How would you describe the brand personality in 3 words? - Reference sites or apps that capture the right feel? What specifically about them? - - For brand, push for real-world references in the right lane (tech-minimal, editorial-magazine, consumer-warm, brutalist-grid, etc.), not generic "modern" adjectives. - - For product, push for category best-tool references (Linear, Figma, Notion, Raycast, Stripe). + - Push for specific named references with the *specific* thing about them that fits this brand, not generic "modern" adjectives or category-bucket lanes. - What should this explicitly NOT look like? Any anti-references? ### Accessibility & Inclusion @@ -141,16 +135,38 @@ If the user agrees, delegate to `/impeccable document` (it auto-detects scan vs If the user prefers to skip, mention they can run `/impeccable document` any time later. -## Step 6: Confirm and wrap up +## Step 6: Configure live mode (when code exists) -Summarize: +If the project has code with HTML entries and a dev server (the same "code exists" condition that puts `/impeccable document` in scan mode), pre-configure live mode now. You already identified the framework and the served HTML entry in Step 2, so this is nearly free, and it spares the user the first-time setup detour when they later run `/impeccable live`. + +**Skip this step for empty / pre-implementation projects** (nothing to inject into yet). Tell the user live mode will configure itself the first time they run it once there's code. + +**If `.impeccable/live/config.json` already exists, leave it untouched** and note that live mode is already configured. + +Otherwise: + +1. Write `.impeccable/live/config.json`. Choose `files` (the HTML entries the browser actually loads), `insertBefore`, and `commentSyntax` from the framework table in [live.md](live.md)'s **First-time setup** section, using the framework you found in Step 2. That table is canonical; do not restate it here. For multi-page static sites, prefer a glob (`["public/**/*.html"]`) over a literal list. +2. Run `node .claude/skills/impeccable/scripts/detect-csp.mjs`. If it reports a patchable shape (`append-arrays` / `append-string`), use the **consent prompt template** from live.md before editing any source file. On decline, skip the patch. For `middleware` / `meta-tag` shapes, surface the detected files and ask the user to add `http://localhost:8400` to `script-src` and `connect-src` manually. For `null`, there's nothing to do. +3. Set `cspChecked: true` in the config once CSP is handled (patched, declined, manual, or not needed). The schema and per-shape patch details live in live.md's First-time setup; follow it rather than duplicating. + +Writing the config file is harmless and needs no consent; only the CSP **source-file patch** requires a yes. + +## Step 7: Recommend starting points, then wrap up + +Summarize tersely: - Register captured (brand / product) -- What was written (PRODUCT.md, DESIGN.md, or both) +- What was written (PRODUCT.md, DESIGN.md, live config, or a subset) - The 3-5 strategic principles from PRODUCT.md that will guide future work -- If DESIGN.md is pending, remind the user how to generate it later +- If DESIGN.md or live config is pending, one line on how to set it up later -**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node .claude/skills/impeccable/scripts/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. +Then recommend the **best commands to run next**, drawn from what your Step 2 crawl already surfaced. Do not run a fresh analysis here; surface observations you already have. Tailor to register and to what you saw, offer the 2-4 most relevant (not a menu dump), and give the exact command to type. Group by intent: -If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context. +- **Build something new**: `/impeccable craft ` (shape, then build end-to-end) or `/impeccable shape ` (plan first). Lead with this for empty or early-stage projects. +- **Improve what's there**: name the specific surface. `/impeccable critique ` for a scored UX review; `/impeccable audit ` for a11y / perf / responsive checks; `/impeccable polish ` for a pre-ship pass. When the crawl flagged a specific weakness, point the matching command at it: thin hierarchy or spacing → `layout`, flat or gray palette → `colorize`, missing error / empty states → `harden` or `onboard`, dull or unclear copy → `clarify`. +- **Iterate visually**: `/impeccable live` (configured in Step 6) to pick elements in the browser and generate variants in place. + +The full command menu is one bare `/impeccable` away; keep this list short and pointed. + +If init was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now. Your own writes are the freshest source; no reload needed. Optionally STOP and call the AskUserQuestion tool to clarify. Ask whether they'd like a brief summary of PRODUCT.md appended to CLAUDE.md for easier agent reference. If yes, append a short **Design Context** pointer section there. diff --git a/.claude/skills/impeccable/reference/interaction-design.md b/.claude/skills/impeccable/reference/interaction-design.md index 15aed5b29..c42c9d810 100644 --- a/.claude/skills/impeccable/reference/interaction-design.md +++ b/.claude/skills/impeccable/reference/interaction-design.md @@ -150,12 +150,6 @@ For browsers without anchor positioning support, `position: fixed` with manual c Check viewport boundaries before rendering. If the dropdown would overflow the bottom edge, flip it above the trigger. If it would overflow the right edge, align it to the trigger's right side instead. -### Anti-Patterns - -- **`position: absolute` inside `overflow: hidden`** - The dropdown will be clipped. Use `position: fixed` or the top layer instead. -- **Arbitrary z-index values** like `z-index: 9999` - Use a semantic z-index scale: `dropdown (100) -> sticky (200) -> modal-backdrop (300) -> modal (400) -> toast (500) -> tooltip (600)`. -- **Rendering dropdown markup inline** without an escape hatch from the parent's stacking context. Either use `popover` (top layer), a portal, or `position: fixed`. - ## Destructive Actions: Undo > Confirm **Undo is better than confirmation dialogs.** Users click through confirmations mindlessly. Remove from UI immediately, show undo toast, actually delete after toast expires. Use confirmation only for truly irreversible actions (account deletion), high-cost actions, or batch operations. diff --git a/.claude/skills/impeccable/reference/layout.md b/.claude/skills/impeccable/reference/layout.md index cad4c62a1..8765978b0 100644 --- a/.claude/skills/impeccable/reference/layout.md +++ b/.claude/skills/impeccable/reference/layout.md @@ -1,4 +1,4 @@ -Space is the most underused design tool. Find the layout's actual problem (monotone spacing, weak hierarchy, identical card grids, the centered-stack default) and fix the structure, not the surface. +Space is the most underused design tool. Find the layout's actual problem (monotone spacing, weak hierarchy, identical card grids) and fix the structure, not the surface. --- @@ -27,7 +27,6 @@ Analyze what's weak about the current spatial design: 3. **Grid & structure**: - Is there a clear underlying structure, or does the layout feel random? - Are identical card grids used everywhere? (Icon + heading + text, repeated endlessly) - - Is everything centered? (Left-aligned with asymmetric layouts feels more designed, but not a hard and fast rule) 4. **Rhythm & variety**: - Does the layout have visual rhythm? (Alternating tight/generous spacing) @@ -43,8 +42,6 @@ Analyze what's weak about the current spatial design: ## Plan Layout Improvements -Consult the [spatial design reference](spatial-design.md) for detailed guidance on grids, rhythm, and container queries. - Create a systematic plan: - **Spacing system**: Use a consistent scale (a framework's built-in scale like Tailwind's, rem-based tokens, or a custom system). The specific values matter less than consistency. @@ -57,6 +54,7 @@ Create a systematic plan: ### Establish a Spacing System - Use a consistent spacing scale (framework scales like Tailwind, rem-based tokens, or a custom scale all work). What matters is that values come from a defined set, not arbitrary numbers. +- Prefer a 4pt base scale (4, 8, 12, 16, 24, 32, 48, 64, 96px) over 8pt; 8pt is too coarse and you'll frequently need 12px between 8 and 16. - Name tokens semantically if using custom properties: `--space-xs` through `--space-xl`, not `--spacing-8` - Use `gap` for sibling spacing instead of margins; eliminates margin collapse hacks - Apply `clamp()` for fluid spacing that breathes on larger screens @@ -66,15 +64,22 @@ Create a systematic plan: - **Tight grouping** for related elements (8-12px between siblings) - **Generous separation** between distinct sections (48-96px) - **Varied spacing** within sections (not every row needs the same gap) -- **Asymmetric compositions**: break the predictable centered-content pattern when it makes sense +- **Asymmetric compositions**: a deliberate choice when the content invites it (not a default to chase). ### Choose the Right Layout Tool -- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. Flex is simpler and more appropriate for the majority of layout tasks. +- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. - **Use Grid for 2D layouts**: Page-level structure, dashboards, data-dense interfaces, anything where rows AND columns need coordinated control. -- **Don't default to Grid** when Flexbox with `flex-wrap` would be simpler and more flexible. -- Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. - Use named grid areas (`grid-template-areas`) for complex page layouts; redefine at breakpoints. +- Use **container queries** for components, viewport queries for page layouts. A card in a narrow sidebar can stay compact while the same card in a main content area expands automatically: + +```css +.card-container { container-type: inline-size; } +.card { display: grid; gap: var(--space-md); } +@container (min-width: 400px) { + .card { grid-template-columns: 120px 1fr; } +} +``` ### Break Card Grid Monotony @@ -85,18 +90,36 @@ Create a systematic plan: ### Strengthen Visual Hierarchy - Use the fewest dimensions needed for clear hierarchy. Space alone can be enough; generous whitespace around an element draws the eye. Some of the most polished designs achieve rhythm with just space and weight. Add color or size contrast only when simpler means aren't sufficient. +- The best hierarchy combines 2–3 dimensions at once. A heading that's larger, bolder, AND has more space above it reads as primary without trying: + +| Tool | Strong Hierarchy | Weak Hierarchy | +|------|------------------|----------------| +| **Size** | 3:1 ratio or more | <2:1 ratio | +| **Weight** | Bold vs Regular | Medium vs Regular | +| **Color** | High contrast | Similar tones | +| **Position** | Top/left (primary) | Bottom/right | +| **Space** | Surrounded by white space | Crowded | + - Be aware of reading flow: in LTR languages, the eye naturally scans top-left to bottom-right, but primary action placement depends on context (e.g., bottom-right in dialogs, top in navigation). - Create clear content groupings through proximity and separation. ### Manage Depth & Elevation -- Create a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip) - Build a consistent shadow scale (sm → md → lg → xl); shadows should be subtle - Use elevation to reinforce hierarchy, not as decoration ### Optical Adjustments - If an icon looks visually off-center despite being geometrically centered, nudge it. But only if you're confident it actually looks wrong. Don't adjust speculatively. +- Text at `margin-left: 0` looks slightly indented because of letterform whitespace; a negative margin (`-0.05em`) optically aligns it. Geometrically centered glyphs often look off-center (play icons need to shift right, arrows shift toward their direction). +- Touch targets must be 44×44px minimum even when the visual element is smaller. Expand the hit area with padding or a pseudo-element: + +```css +.icon-button { width: 24px; height: 24px; position: relative; } +.icon-button::before { + content: ''; position: absolute; inset: -10px; +} +``` **NEVER**: - Use arbitrary spacing values outside your scale @@ -104,10 +127,7 @@ Create a systematic plan: - Wrap everything in cards (not everything needs a container) - Nest cards inside cards (use spacing and dividers for hierarchy within) - Use identical card grids everywhere (icon + heading + text, repeated) -- Center everything (left-aligned with asymmetry feels more designed) - Default to the hero metric layout (big number, small label, stats, gradient) as a template. If showing real user data, a prominent metric can work, but it should display actual data, not decorative numbers. -- Default to CSS Grid when Flexbox would be simpler; use the simplest tool for the job -- Use arbitrary z-index values (999, 9999); build a semantic scale ## Verify Layout Improvements diff --git a/.claude/skills/impeccable/reference/live.md b/.claude/skills/impeccable/reference/live.md index 112d6f6ec..397044b3e 100644 --- a/.claude/skills/impeccable/reference/live.md +++ b/.claude/skills/impeccable/reference/live.md @@ -9,18 +9,21 @@ A running dev server with hot module replacement (Vite, Next.js, Bun, etc.), OR Execute in order. No step skipped, no step reordered. 1. `live.mjs`: boot. -2. Navigate to the URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). If you can't infer it confidently, tell the user once to open their dev/preview URL. Never use `serverPort` as that URL; it's the helper, not the app. +2. Open the app URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). Never use `serverPort`; it's the helper, not the app. **Cursor:** `browser_navigate` to that URL before polling; do not skip. **Other harnesses:** use the available browser tool; if the URL is uncertain, ask the user once. 3. Poll loop with the default long timeout (600000 ms). After every event or `--reply`, run `live-poll.mjs` again immediately. Never pass a short `--timeout=`. + +The global bar **Impeccable mark** dims and shows a pulsing amber dot when no agent is long-polling `/poll`. Hover the mark for the hint; restart `live-poll.mjs` to reconnect. 4. On `generate`: read screenshot if present; load the action's reference; plan three distinct directions; write all variants in one edit; `--reply done`; poll again. -5. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts remain recoverable until you finish cleanup, run `live-complete.mjs --id EVENT_ID`, and only then poll again. -6. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart. -7. On `exit`: run the cleanup at the bottom. +5. On `steer`: read the message and `pageUrl`; do the work (page edits, navigation help, or a short reply in the `--reply` message); `--reply steer_done`; poll again. No pickup ack. The Steer bar unlocks when `steer_done` arrives over SSE. +6. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts remain recoverable until you finish cleanup, run `live-complete.mjs --id EVENT_ID`, and only then poll again. +7. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart. +8. On `exit`: run the cleanup at the bottom. Harness policy: - **Claude Code**: run the poll as a **background task** (no short timeout). The harness notifies you when it completes, so the main conversation stays free. Do not block the shell. -- **Cursor**: run the poll in the **foreground** (blocking shell; not a background terminal, not a subagent). Cursor background terminals and subagents do not reliably resume the chat with poll stdout. +- **Cursor**: run **one-shot** poll in a **background terminal** with notify on `"type":"(steer|generate|accept|discard|exit)"`. After each event the poll exits; handle it, `--reply`, then start `live-poll.mjs` again. Do **not** use `--stream` on Cursor: incremental stdout notify is slower in practice than exit-based notify (~5s vs sub-second in testing). - **Codex**: run the poll in the **foreground** (blocking shell; not a background task, not a subagent). Codex background exec sessions do not reliably surface poll stdout back into the conversation at the moment events arrive, so a "fire-and-forget" background poll will stall live mode. -- **Other harnesses**: foreground unless you know stdout reliably returns to this session. +- **Other harnesses**: one-shot foreground unless you know stdout reliably returns to this session when a shell exits. Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodies. Spend tokens on tools and edits; on failure, one or two short sentences. @@ -30,7 +33,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi node .claude/skills/impeccable/scripts/live.mjs ``` -Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation; **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md. +Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation; **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. `serverPort` and `serverToken` belong to the small **Impeccable live helper** HTTP server (serves `/live.js`, SSE, and `/poll`). That port is **not** your dev server and is usually not the URL you open to view the app. The browser page is whatever origin serves one of the `pageFiles` entries (Vite / Next / Bun / tunnel / LAN hostname). @@ -38,12 +41,15 @@ If output is `{ ok: false, error: "config_missing" | "config_invalid", path }`, ## Poll loop +**Default (portable, all harnesses):** + ``` LOOP: node .claude/skills/impeccable/scripts/live-poll.mjs # default long timeout; no --timeout= Read JSON; dispatch on "type" "generate" → Handle Generate; reply done; LOOP + "steer" → Handle Steer; reply steer_done; LOOP "accept" → Handle Accept; complete carbonize cleanup if required; LOOP "discard" → Handle Discard; LOOP "prefetch" → Handle Prefetch; LOOP @@ -51,6 +57,16 @@ LOOP: "exit" → break → Cleanup ``` +**Stream mode (experimental, not for Cursor):** + +``` +node .claude/skills/impeccable/scripts/live-poll.mjs --stream # stays running; one JSON line per event + Handle event; run --reply in a separate command + Repeat until "exit" line → Cleanup +``` + +Stream keeps one process alive and waits for `--reply` ack before polling again. Useful only when the harness reads incremental stdout reliably and quickly. **Cursor is not one of those:** background pattern notify on a long-running shell was ~5s to pick up events vs sub-second for one-shot exit notify. Default to one-shot everywhere unless you have measured otherwise. + ## Recovery commands The live helper persists an append-only journal under `.impeccable/live/sessions/`. Browser checkpoints are advisory but durable; the journal is canonical. This is local durable recovery state, not project source. @@ -71,9 +87,32 @@ Server restart rule: start `live-server.mjs` again, then poll. Startup requeues ## Handle `generate` -Event: `{id, action, freeformPrompt?, count, pageUrl, element, screenshotPath?, comments?, strokes?}`. +**Replace mode** (default): `{id, action, freeformPrompt?, count, pageUrl, element, screenshotPath?, comments?, strokes?}`. -Speed matters; the user is watching a spinner. Minimize tool calls by using the `wrap` helper and writing all variants in a single edit. +**Insert mode** (`event.mode === "insert"`): `{id, mode: "insert", count, pageUrl, insert: { position, anchor }, placeholder: { width, height }, freeformPrompt?, screenshotPath?, comments?, strokes?}`. No `action`. Requires a non-empty `freeformPrompt` **or** annotations. Screenshot is sent only when annotations exist (same rule as replace). Use `placeholder` dimensions as a soft size hint for net-new content. + +Speed matters; the user is watching a spinner. Minimize tool calls by using the wrap/insert helper and writing all variants in a single edit. + +### Insert mode branch + +When `event.mode === "insert"`: + +1. Read the screenshot if `event.screenshotPath` is present (annotations only). +2. Run the insert helper instead of wrap: + +```bash +node .claude/skills/impeccable/scripts/live-insert.mjs --id EVENT_ID --count EVENT_COUNT --position after \ + --element-id "ANCHOR_ID" --classes "class1,class2" --tag "section" --text "ANCHOR_TEXT" +``` + +- `--position` ← `event.insert.position` (`before` | `after`) +- Anchor flags ← `event.insert.anchor` (same mapping as wrap: id, classes, tag, text) + +The scaffold has **no** `data-impeccable-variant="original"`. Variants are net-new HTML+CSS inserted at `insertLine`. Load `brand.md` or `product.md` (freeform only, no action sub-command). Write all variants in one edit, then `--reply done`. + +On accept/discard, `live-accept.mjs` removes the wrapper block; the anchor element is untouched. + +### Replace mode (default) ### 1. Read the screenshot (if present) @@ -424,6 +463,28 @@ A background agent may be used for the rewrite, but the current thread is respon Event: `{id, _acceptResult, _completionAck}`. The poll script already restored the original, removed all variant markers, and acknowledged `discarded` durable completion. Nothing to do unless `_completionAck.ok !== true`; in that case run `live-complete.mjs --id EVENT_ID --discarded`, then poll again. +## Handle `steer` + +Event: `{id, message, pageUrl}`. The user typed or spoke into the global bar **Steer** control: page-level direction without picking an element or launching variant generation. + +The mic button uses the browser **Web Speech API** (MVP): click to start, speak, stop automatically when the utterance ends, then the transcript submits as a steer event. Click again while listening to cancel without submitting. + +This is lighter than `generate`: no screenshot, no element context, no variant cycling. Read `message` and inspect the live page or project files as needed, then either make edits or answer in prose. + +When finished: + +```bash +node .claude/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID steer_done ["Optional short note for a browser toast"] +``` + +On failure: + +```bash +node .claude/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID error "Short reason" +``` + +Then poll again immediately. Do not send a separate "picked up" reply. The Steer bar stays locked until `steer_done` or `error` arrives over SSE. + ## Handle `prefetch` Event: `{pageUrl}`. The browser fires this the first time the user selects an element on a given route, as a latency shortcut; it signals the user is likely about to Go on a page you haven't read yet. diff --git a/.claude/skills/impeccable/reference/motion-design.md b/.claude/skills/impeccable/reference/motion-design.md deleted file mode 100644 index 78b6fd260..000000000 --- a/.claude/skills/impeccable/reference/motion-design.md +++ /dev/null @@ -1,109 +0,0 @@ -# Motion Design - -## Duration: The 100/300/500 Rule - -Timing matters more than easing. These durations feel right for most UI: - -| Duration | Use Case | Examples | -|----------|----------|----------| -| **100-150ms** | Instant feedback | Button press, toggle, color change | -| **200-300ms** | State changes | Menu open, tooltip, hover states | -| **300-500ms** | Layout changes | Accordion, modal, drawer | -| **500-800ms** | Entrance animations | Page load, hero reveals | - -**Exit animations are faster than entrances.** Use ~75% of enter duration. - -## Easing: Pick the Right Curve - -**Don't use `ease`.** It's a compromise that's rarely optimal. Instead: - -| Curve | Use For | CSS | -|-------|---------|-----| -| **ease-out** | Elements entering | `cubic-bezier(0.16, 1, 0.3, 1)` | -| **ease-in** | Elements leaving | `cubic-bezier(0.7, 0, 0.84, 0)` | -| **ease-in-out** | State toggles (there → back) | `cubic-bezier(0.65, 0, 0.35, 1)` | - -**For micro-interactions, use exponential curves.** They feel natural because they mimic real physics (friction, deceleration): - -```css -/* Quart out - smooth, refined (recommended default) */ ---ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1); - -/* Quint out - slightly more dramatic */ ---ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1); - -/* Expo out - snappy, confident */ ---ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); -``` - -**Avoid bounce and elastic curves.** They were trendy in 2015 but now feel tacky and amateurish. Real objects don't bounce when they stop; they decelerate smoothly. Overshoot effects draw attention to the animation itself rather than the content. - -## Premium Motion Materials - -Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties: blur reveals, backdrop-filter panels, saturation or brightness shifts, shadow bloom, SVG filters, masks, clip paths, gradient-position movement, and variable font or shader-driven effects. - -Use the right material for the effect: - -- **Transform / opacity**: movement, press feedback, simple reveals, list choreography. -- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances, atmospheric transitions. -- **Clip path / masks**: wipes, reveals, editorial cropping, product-like transitions. -- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state. -- **Grid-template rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly. - -The hard rule is not "transform and opacity only." The hard rule is: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify in-browser that the result is smooth on the target viewports. If blur/filter makes the interaction feel significantly more premium and remains smooth, use it. - -## Staggered Animations - -Use CSS custom properties for cleaner stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"` on each item. **Cap total stagger time**: 10 items at 50ms = 500ms total. For many items, reduce per-item delay or cap staggered count. - -## Reduced Motion - -This is not optional. Vestibular disorders affect ~35% of adults over 40. - -```css -/* Define animations normally */ -.card { - animation: slide-up 500ms ease-out; -} - -/* Provide alternative for reduced motion */ -@media (prefers-reduced-motion: reduce) { - .card { - animation: fade-in 200ms ease-out; /* Crossfade instead of motion */ - } -} - -/* Or disable entirely */ -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - transition-duration: 0.01ms !important; - } -} -``` - -**What to preserve**: Functional animations like progress bars, loading spinners (slowed down), and focus indicators should still work, just without spatial movement. - -## Perceived Performance - -**Nobody cares how fast your site is, just how fast it feels.** Perception can be as effective as actual performance. - -**The 80ms threshold**: Our brains buffer sensory input for ~80ms to synchronize perception. Anything under 80ms feels instant and simultaneous. This is your target for micro-interactions. - -**Active vs passive time**: Passive waiting (staring at a spinner) feels longer than active engagement. Strategies to shift the balance: - -- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening. -- **Early completion**: Show content progressively, don't wait for everything. Video buffering, progressive images, streaming HTML. -- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Instagram likes work offline; the UI updates instantly, syncs later. Use for low-stakes actions; avoid for payments or destructive operations. - -**Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances, but ease-in toward a task's end compresses perceived time. - -**Caution**: Too-fast responses can decrease perceived value. Users may distrust instant results for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening. - -## Performance - -Don't use `will-change` preemptively, only when animation is imminent (`:hover`, `.animating`). For scroll-triggered animations, use Intersection Observer instead of scroll events; unobserve after animating once. Create motion tokens for consistency (durations, easings, common transitions). - ---- - -**Avoid**: Animating everything (animation fatigue is real). Using >500ms for UI feedback. Ignoring `prefers-reduced-motion`. Using animation to hide slow loading. diff --git a/.claude/skills/impeccable/reference/personas.md b/.claude/skills/impeccable/reference/personas.md deleted file mode 100644 index 026bbe7ab..000000000 --- a/.claude/skills/impeccable/reference/personas.md +++ /dev/null @@ -1,179 +0,0 @@ -# Persona-Based Design Testing - -Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss. - -**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns. - ---- - -## 1. Impatient Power User: "Alex" - - -**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave. - -**Behaviors**: -- Skips all onboarding and instructions -- Looks for keyboard shortcuts immediately -- Tries to bulk-select, batch-edit, and automate -- Gets frustrated by required steps that feel unnecessary -- Abandons if anything feels slow or patronizing - -**Test Questions**: -- Can Alex complete the core task in under 60 seconds? -- Are there keyboard shortcuts for common actions? -- Can onboarding be skipped entirely? -- Do modals have keyboard dismiss (Esc)? -- Is there a "power user" path (shortcuts, bulk actions)? - -**Red Flags** (report these specifically): -- Forced tutorials or unskippable onboarding -- No keyboard navigation for primary actions -- Slow animations that can't be skipped -- One-item-at-a-time workflows where batch would be natural -- Redundant confirmation steps for low-risk actions - ---- - -## 2. Confused First-Timer: "Jordan" - -**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out. - -**Behaviors**: -- Reads all instructions carefully -- Hesitates before clicking anything unfamiliar -- Looks for help or support constantly -- Misunderstands jargon and abbreviations -- Takes the most literal interpretation of any label - -**Test Questions**: -- Is the first action obviously clear within 5 seconds? -- Are all icons labeled with text? -- Is there contextual help at decision points? -- Does terminology assume prior knowledge? -- Is there a clear "back" or "undo" at every step? - -**Red Flags** (report these specifically): -- Icon-only navigation with no labels -- Technical jargon without explanation -- No visible help option or guidance -- Ambiguous next steps after completing an action -- No confirmation that an action succeeded - ---- - -## 3. Accessibility-Dependent User: "Sam" - -**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences. - -**Behaviors**: -- Tabs through the interface linearly -- Relies on ARIA labels and heading structure -- Cannot see hover states or visual-only indicators -- Needs adequate color contrast (4.5:1 minimum) -- May use browser zoom up to 200% - -**Test Questions**: -- Can the entire primary flow be completed keyboard-only? -- Are all interactive elements focusable with visible focus indicators? -- Do images have meaningful alt text? -- Is color contrast WCAG AA compliant (4.5:1 for text)? -- Does the screen reader announce state changes (loading, success, errors)? - -**Red Flags** (report these specifically): -- Click-only interactions with no keyboard alternative -- Missing or invisible focus indicators -- Meaning conveyed by color alone (red = error, green = success) -- Unlabeled form fields or buttons -- Time-limited actions without extension option -- Custom components that break screen reader flow - ---- - -## 4. Deliberate Stress Tester: "Riley" - -**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience. - -**Behaviors**: -- Tests edge cases intentionally (empty states, long strings, special characters) -- Submits forms with unexpected data (emoji, RTL text, very long values) -- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs -- Looks for inconsistencies between what the UI promises and what actually happens -- Documents problems methodically - -**Test Questions**: -- What happens at the edges (0 items, 1000 items, very long text)? -- Do error states recover gracefully or leave the UI in a broken state? -- What happens on refresh mid-workflow? Is state preserved? -- Are there features that appear to work but produce broken results? -- How does the UI handle unexpected input (emoji, special chars, paste from Excel)? - -**Red Flags** (report these specifically): -- Features that appear to work but silently fail or produce wrong results -- Error handling that exposes technical details or leaves UI in a broken state -- Empty states that show nothing useful ("No results" with no guidance) -- Workflows that lose user data on refresh or navigation -- Inconsistent behavior between similar interactions in different parts of the UI - ---- - -## 5. Distracted Mobile User: "Casey" - -**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection. - -**Behaviors**: -- Uses thumb only; prefers bottom-of-screen actions -- Gets interrupted mid-flow and returns later -- Switches between apps frequently -- Has limited attention span and low patience -- Types as little as possible, prefers taps and selections - -**Test Questions**: -- Are primary actions in the thumb zone (bottom half of screen)? -- Is state preserved if the user leaves and returns? -- Does it work on slow connections (3G)? -- Can forms use autocomplete and smart defaults? -- Are touch targets at least 44×44pt? - -**Red Flags** (report these specifically): -- Important actions positioned at the top of the screen (unreachable by thumb) -- No state persistence; progress lost on tab switch or interruption -- Large text inputs required where selection would work -- Heavy assets loading on every page (no lazy loading) -- Tiny tap targets or targets too close together - ---- - -## Selecting Personas - -Choose personas based on the interface type: - -| Interface Type | Primary Personas | Why | -|---------------|-----------------|-----| -| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile | -| Dashboard / admin | Alex, Sam | Power users, accessibility | -| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity | -| Onboarding flow | Jordan, Casey | Confusion, interruption | -| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav | -| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile | - ---- - -## Project-Specific Personas - -If `CLAUDE.md` contains a `## Design Context` section (generated by `impeccable teach`), derive 1–2 additional personas from the audience and brand information: - -1. Read the target audience description -2. Identify the primary user archetype not covered by the 5 predefined personas -3. Create a persona following this template: - -``` -### [Role]: "[Name]" - -**Profile**: [2-3 key characteristics derived from Design Context] - -**Behaviors**: [3-4 specific behaviors based on the described audience] - -**Red Flags**: [3-4 things that would alienate this specific user type] -``` - -Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists. diff --git a/.claude/skills/impeccable/reference/polish.md b/.claude/skills/impeccable/reference/polish.md index 0ba47d19d..22c18c157 100644 --- a/.claude/skills/impeccable/reference/polish.md +++ b/.claude/skills/impeccable/reference/polish.md @@ -91,7 +91,6 @@ Visual polish on a misshapen flow is wasted work. Match the *shape* of the exper - **Theme consistency**: Works in all theme variants - **Color meaning**: Same colors mean same things throughout - **Accessible focus**: Focus indicators visible with sufficient contrast -- **Tinted neutrals**: No pure gray or pure black; add subtle color tint (0.01 chroma) - **Gray on color**: Never put gray text on colored backgrounds; use a shade of that color or transparency ### Interaction States diff --git a/.claude/skills/impeccable/reference/product.md b/.claude/skills/impeccable/reference/product.md index 64b3b0169..2ca462350 100644 --- a/.claude/skills/impeccable/reference/product.md +++ b/.claude/skills/impeccable/reference/product.md @@ -10,7 +10,6 @@ Product UI's failure mode isn't flatness, it's strangeness without purpose: over ## Typography -- **System fonts are legitimate.** `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif` gives you native feel on every platform. Inter is the common cross-platform default for a reason. - **One family is often right.** Product UIs don't need display/body pairing. A well-tuned sans carries headings, buttons, labels, body, data. - **Fixed rem scale, not fluid.** Clamp-sized headings don't serve product UI. Users view at consistent DPI, and a fluid h1 that shrinks in a sidebar looks worse, not better. - **Tighter scale ratio.** 1.125–1.2 between steps is typical. More type elements here than on brand surfaces; exaggerated contrast creates noise. @@ -26,8 +25,6 @@ Product defaults to Restrained. A single surface can earn Committed (a dashboard ## Layout -- Predictable grids. Consistency IS an affordance; users navigate faster when the structure is expected. -- Familiar patterns are features. Standard navigation (top bar, side nav), breadcrumbs, tabs, and form layouts have established user expectations. Don't reinvent for flavor. - Responsive behavior is structural (collapse sidebar, responsive table, breakpoint-driven columns), not fluid typography. ## Components @@ -51,6 +48,7 @@ Every interactive component has: default, hover, focus, active, disabled, loadin - Display fonts in UI labels, buttons, data. - Reinventing standard affordances for flavor (custom scrollbars, weird form controls, non-standard modals). - Heavy color or full-saturation accents on inactive states. +- Modal as first thought. Modals are usually laziness. Exhaust inline / progressive alternatives first. ## Product permissions diff --git a/.claude/skills/impeccable/reference/responsive-design.md b/.claude/skills/impeccable/reference/responsive-design.md deleted file mode 100644 index f079e5e5d..000000000 --- a/.claude/skills/impeccable/reference/responsive-design.md +++ /dev/null @@ -1,114 +0,0 @@ -# Responsive Design - -## Mobile-First: Write It Right - -Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first. - -## Breakpoints: Content-Driven - -Don't chase device sizes; let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints. - -## Detect Input Method, Not Just Screen Size - -**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard. Use pointer and hover queries: - -```css -/* Fine pointer (mouse, trackpad) */ -@media (pointer: fine) { - .button { padding: 8px 16px; } -} - -/* Coarse pointer (touch, stylus) */ -@media (pointer: coarse) { - .button { padding: 12px 20px; } /* Larger touch target */ -} - -/* Device supports hover */ -@media (hover: hover) { - .card:hover { transform: translateY(-2px); } -} - -/* Device doesn't support hover (touch) */ -@media (hover: none) { - .card { /* No hover state - use active instead */ } -} -``` - -**Critical**: Don't rely on hover for functionality. Touch users can't hover. - -## Safe Areas: Handle the Notch - -Modern phones have notches, rounded corners, and home indicators. Use `env()`: - -```css -body { - padding-top: env(safe-area-inset-top); - padding-bottom: env(safe-area-inset-bottom); - padding-left: env(safe-area-inset-left); - padding-right: env(safe-area-inset-right); -} - -/* With fallback */ -.footer { - padding-bottom: max(1rem, env(safe-area-inset-bottom)); -} -``` - -**Enable viewport-fit** in your meta tag: -```html - -``` - -## Responsive Images: Get It Right - -### srcset with Width Descriptors - -```html -Hero image -``` - -**How it works**: -- `srcset` lists available images with their actual widths (`w` descriptors) -- `sizes` tells the browser how wide the image will display -- Browser picks the best file based on viewport width AND device pixel ratio - -### Picture Element for Art Direction - -When you need different crops/compositions (not just resolutions): - -```html - - - - ... - -``` - -## Layout Adaptation Patterns - -**Navigation**: Three stages: hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `
/` for content that can collapse on mobile. - -## Testing: Don't Trust DevTools Alone - -DevTools device emulation is useful for layout but misses: - -- Actual touch interactions -- Real CPU/memory constraints -- Network latency patterns -- Font rendering differences -- Browser chrome/keyboard appearances - -**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators. - ---- - -**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful. diff --git a/.claude/skills/impeccable/reference/shape.md b/.claude/skills/impeccable/reference/shape.md index 8178a5790..c76d25812 100644 --- a/.claude/skills/impeccable/reference/shape.md +++ b/.claude/skills/impeccable/reference/shape.md @@ -151,7 +151,7 @@ How users interact with this feature. What happens on click, hover, scroll? What 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., spatial-design.md for complex layouts, motion-design.md for animated features, interaction-design.md for form-heavy features). +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. diff --git a/.claude/skills/impeccable/reference/spatial-design.md b/.claude/skills/impeccable/reference/spatial-design.md deleted file mode 100644 index 4d4b83afc..000000000 --- a/.claude/skills/impeccable/reference/spatial-design.md +++ /dev/null @@ -1,100 +0,0 @@ -# Spatial Design - -## Spacing Systems - -### Use 4pt Base, Not 8pt - -8pt systems are too coarse; you'll frequently need 12px (between 8 and 16). Use 4pt for granularity: 4, 8, 12, 16, 24, 32, 48, 64, 96px. - -### Name Tokens Semantically - -Name by relationship (`--space-sm`, `--space-lg`), not value (`--spacing-8`). Use `gap` instead of margins for sibling spacing; it eliminates margin collapse and cleanup hacks. - -## Grid Systems - -### The Self-Adjusting Grid - -Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. Columns are at least 280px, as many as fit per row, leftovers stretch. For complex layouts, use named grid areas (`grid-template-areas`) and redefine them at breakpoints. - -## Visual Hierarchy - -### The Squint Test - -Blur your eyes (or screenshot and blur). Can you still identify: -- The most important element? -- The second most important? -- Clear groupings? - -If everything looks the same weight blurred, you have a hierarchy problem. - -### Hierarchy Through Multiple Dimensions - -Don't rely on size alone. Combine: - -| Tool | Strong Hierarchy | Weak Hierarchy | -|------|------------------|----------------| -| **Size** | 3:1 ratio or more | <2:1 ratio | -| **Weight** | Bold vs Regular | Medium vs Regular | -| **Color** | High contrast | Similar tones | -| **Position** | Top/left (primary) | Bottom/right | -| **Space** | Surrounded by white space | Crowded | - -**The best hierarchy uses 2-3 dimensions at once**: A heading that's larger, bolder, AND has more space above it. - -### Cards Are Not Required - -Cards are overused. Spacing and alignment create visual grouping naturally. Use cards only when content is truly distinct and actionable, items need visual comparison in a grid, or content needs clear interaction boundaries. **Never nest cards inside cards.** Use spacing, typography, and subtle dividers for hierarchy within a card. - -## Container Queries - -Viewport queries are for page layouts. **Container queries are for components**: - -```css -.card-container { - container-type: inline-size; -} - -.card { - display: grid; - gap: var(--space-md); -} - -/* Card layout changes based on its container, not viewport */ -@container (min-width: 400px) { - .card { - grid-template-columns: 120px 1fr; - } -} -``` - -**Why this matters**: A card in a narrow sidebar stays compact, while the same card in a main content area expands automatically, without viewport hacks. - -## Optical Adjustments - -Text at `margin-left: 0` looks indented due to letterform whitespace; use negative margin (`-0.05em`) to optically align. Geometrically centered icons often look off-center; play icons need to shift right, arrows shift toward their direction. - -### Touch Targets vs Visual Size - -Buttons can look small but need large touch targets (44px minimum). Use padding or pseudo-elements: - -```css -.icon-button { - width: 24px; /* Visual size */ - height: 24px; - position: relative; -} - -.icon-button::before { - content: ''; - position: absolute; - inset: -10px; /* Expand tap target to 44px */ -} -``` - -## Depth & Elevation - -Create semantic z-index scales (dropdown → sticky → modal-backdrop → modal → toast → tooltip) instead of arbitrary numbers. For shadows, create a consistent elevation scale (sm → md → lg → xl). **Key insight**: Shadows should be subtle. If you can clearly see it, it's probably too strong. - ---- - -**Avoid**: Arbitrary spacing values outside your scale. Making all spacing equal (variety creates hierarchy). Creating hierarchy through size alone - combine size, weight, color, and space. diff --git a/.claude/skills/impeccable/reference/typeset.md b/.claude/skills/impeccable/reference/typeset.md index dd3e67ea3..5f5e0c2c0 100644 --- a/.claude/skills/impeccable/reference/typeset.md +++ b/.claude/skills/impeccable/reference/typeset.md @@ -4,7 +4,7 @@ Typography carries most of the information on the page. Replace generic defaults ## Register -Brand: run the font selection procedure in [brand.md](brand.md). Pairing follows the brand's lane (display serif + sans body for editorial/luxury, one committed sans for tech, etc.). Fluid `clamp()` scale, ≥1.25 ratio between steps. +Brand: run the font selection procedure in [brand.md](brand.md). Fluid `clamp()` scale, ≥1.25 ratio between steps. Product: system fonts and familiar sans stacks are legitimate here. One well-tuned family typically carries the whole UI. Fixed `rem` scale, 1.125–1.2 ratio between more closely-spaced steps. @@ -43,7 +43,7 @@ Analyze what's weak or generic about the current type: ## Plan Typography Improvements -Consult the [typography reference](typography.md) for detailed guidance on scales, pairing, and loading strategies. +Consult the [Reference Material](#reference-material) section below for detailed guidance on scales, pairing, and loading strategies. Create a systematic plan: @@ -122,3 +122,158 @@ Each variant MUST declare a `scale` param controlling the hierarchy ratio. Expre Where the variant riffs on a specific pairing, expose the pairing choice as a `steps` param (e.g. "serif display + sans body" vs. "mono display + sans body" vs. "all-sans"). Each branch routes through `:scope[data-p-pairing="X"]` selectors in scoped CSS. See `reference/live.md` for the full params contract. + +--- + +## Reference Material + +The sections below were previously `typography.md` and live inline now so the typeset flow has its deep typography reference in one place. `bolder.md` also references this section. + +### Typography + +#### Classic Typography Principles + +##### Vertical Rhythm + +Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony; text and space share a mathematical foundation. + +##### Modular Scale & Hierarchy + +The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy. + +**Use fewer sizes with more contrast.** A 5-size system covers most needs: + +| Role | Typical Ratio | Use Case | +|------|---------------|----------| +| xs | 0.75rem | Captions, legal | +| sm | 0.875rem | Secondary UI, metadata | +| base | 1rem | Body text | +| lg | 1.25-1.5rem | Subheadings, lead text | +| xl+ | 2-4rem | Headlines, hero text | + +Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit. + +##### Readability & Measure + +Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length: narrow columns need tighter leading, wide columns need more. + +**Non-obvious**: Light text on dark backgrounds needs compensation on three axes, not just one. Bump line-height by 0.05–0.1, add a touch of letter-spacing (0.01–0.02em), and optionally step the body weight up one notch (regular → medium). The perceived weight drops across all three; fix all three. + +**Paragraph rhythm**: Pick either space between paragraphs OR first-line indentation. Never both. Digital usually wants space; editorial/long-form can justify indent-only. + +#### Font Selection & Pairing + +The tactical selection procedure and the reflex-reject list live in [reference/brand.md](brand.md) under **Font selection procedure** and **Reflex-reject list** (loaded for brand-register tasks). The rest of this section covers the adjacent knowledge: anti-reflex corrections, system font use, and pairing rules. + +##### Anti-reflexes worth defending against + +- A technical/utilitarian brief does NOT need a serif "for warmth." Most tech tools should look like tech tools. +- An editorial/premium brief does NOT need the same expressive serif everyone is using right now. Premium can be Swiss-modern, can be neo-grotesque, can be a literal monospace, can be a quiet humanist sans. +- A children's product does NOT need a rounded display font. Kids' books use real type. +- A "modern" brief does NOT need a geometric sans. The most modern thing you can do is not use the font everyone else is using. + +**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality. + +##### Pairing Principles + +**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif). + +When pairing, contrast on multiple axes: +- Serif + Sans (structure contrast) +- Geometric + Humanist (personality contrast) +- Condensed display + Wide body (proportion contrast) + +##### Web Font Loading + +The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix: + +```css +/* 1. Use font-display: swap for visibility */ +@font-face { + font-family: 'CustomFont'; + src: url('font.woff2') format('woff2'); + font-display: swap; +} + +/* 2. Match fallback metrics to minimize shift */ +@font-face { + font-family: 'CustomFont-Fallback'; + src: local('Arial'); + size-adjust: 105%; /* Scale to match x-height */ + ascent-override: 90%; /* Match ascender height */ + descent-override: 20%; /* Match descender depth */ + line-gap-override: 10%; /* Match line spacing */ +} + +body { + font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; +} +``` + +Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically. + +**`swap` vs `optional`**: `swap` shows fallback text immediately and FOUT-swaps when the web font arrives. `optional` uses the fallback if the web font misses a small load budget (~100ms) and avoids the shift entirely. Pick `optional` when zero layout shift matters more than seeing the branded font on slow networks. + +**Preload the critical weight only**: typically the regular-weight body font used above the fold. Preloading every weight costs more bandwidth than it saves. + +**Variable fonts for 3+ weights or styles**: a single variable font file is usually smaller than three static weight files, gives fractional weight control, and pairs well with `font-optical-sizing: auto`. For 1–2 weights, static is fine. + +#### Modern Web Typography + +##### Fluid Type + +Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate (higher vw = faster scaling). Add a rem offset so it doesn't collapse to 0 on small screens. + +**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes. + +**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI; fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it. + +**Bound your clamp()**: keep `max-size ≤ ~2.5 × min-size`. Wider ratios break the browser's zoom and reflow behaviour and make large viewports feel like the page is shouting. + +**Scale container width and font-size together** so effective character measure stays in the 45–75ch band at every viewport. A heading that widens faster than its container drifts out of the comfortable measure at the top end. + +##### OpenType Features + +Most developers don't know these exist. Use them for polish: + +```css +/* Proper fractions */ +.recipe-amount { font-variant-numeric: diagonal-fractions; } + +/* Small caps for abbreviations */ +abbr { font-variant-caps: all-small-caps; } + +/* Disable ligatures in code */ +code { font-variant-ligatures: none; } + +/* Enable kerning (usually on by default, but be explicit) */ +body { font-kerning: normal; } +``` + +Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/). + +##### Rendering polish + +```css +/* Variable fonts: pick the right optical-size master automatically */ +body { font-optical-sizing: auto; } +``` + +**ALL-CAPS tracking**: capitals sit too close at default spacing. Add 5–12% letter-spacing (`letter-spacing: 0.05em` to `0.12em`) to short all-caps labels, eyebrows, and small headings. Real small caps (via `font-variant-caps`) need the same treatment, slightly gentler. + +#### Typography System Architecture + +Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system. + +#### Accessibility Considerations + +Beyond contrast ratios (which are well-documented), consider: + +- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout. +- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text. +- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile. +- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets. + +--- + +**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text. diff --git a/.claude/skills/impeccable/reference/typography.md b/.claude/skills/impeccable/reference/typography.md deleted file mode 100644 index ed5ca272a..000000000 --- a/.claude/skills/impeccable/reference/typography.md +++ /dev/null @@ -1,159 +0,0 @@ -# Typography - -## Classic Typography Principles - -### Vertical Rhythm - -Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony; text and space share a mathematical foundation. - -### Modular Scale & Hierarchy - -The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy. - -**Use fewer sizes with more contrast.** A 5-size system covers most needs: - -| Role | Typical Ratio | Use Case | -|------|---------------|----------| -| xs | 0.75rem | Captions, legal | -| sm | 0.875rem | Secondary UI, metadata | -| base | 1rem | Body text | -| lg | 1.25-1.5rem | Subheadings, lead text | -| xl+ | 2-4rem | Headlines, hero text | - -Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit. - -### Readability & Measure - -Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length: narrow columns need tighter leading, wide columns need more. - -**Non-obvious**: Light text on dark backgrounds needs compensation on three axes, not just one. Bump line-height by 0.05–0.1, add a touch of letter-spacing (0.01–0.02em), and optionally step the body weight up one notch (regular → medium). The perceived weight drops across all three; fix all three. - -**Paragraph rhythm**: Pick either space between paragraphs OR first-line indentation. Never both. Digital usually wants space; editorial/long-form can justify indent-only. - -## Font Selection & Pairing - -The tactical selection procedure and the reflex-reject list live in [reference/brand.md](brand.md) under **Font selection procedure** and **Reflex-reject list** (loaded for brand-register tasks). The rest of this section covers the adjacent knowledge: anti-reflex corrections, system font use, and pairing rules. - -### Anti-reflexes worth defending against - -- A technical/utilitarian brief does NOT need a serif "for warmth." Most tech tools should look like tech tools. -- An editorial/premium brief does NOT need the same expressive serif everyone is using right now. Premium can be Swiss-modern, can be neo-grotesque, can be a literal monospace, can be a quiet humanist sans. -- A children's product does NOT need a rounded display font. Kids' books use real type. -- A "modern" brief does NOT need a geometric sans. The most modern thing you can do is not use the font everyone else is using. - -**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality. - -### Pairing Principles - -**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif). - -When pairing, contrast on multiple axes: -- Serif + Sans (structure contrast) -- Geometric + Humanist (personality contrast) -- Condensed display + Wide body (proportion contrast) - -**Never pair fonts that are similar but not identical** (e.g., two geometric sans-serifs). They create visual tension without clear hierarchy. - -### Web Font Loading - -The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix: - -```css -/* 1. Use font-display: swap for visibility */ -@font-face { - font-family: 'CustomFont'; - src: url('font.woff2') format('woff2'); - font-display: swap; -} - -/* 2. Match fallback metrics to minimize shift */ -@font-face { - font-family: 'CustomFont-Fallback'; - src: local('Arial'); - size-adjust: 105%; /* Scale to match x-height */ - ascent-override: 90%; /* Match ascender height */ - descent-override: 20%; /* Match descender depth */ - line-gap-override: 10%; /* Match line spacing */ -} - -body { - font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; -} -``` - -Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically. - -**`swap` vs `optional`**: `swap` shows fallback text immediately and FOUT-swaps when the web font arrives. `optional` uses the fallback if the web font misses a small load budget (~100ms) and avoids the shift entirely. Pick `optional` when zero layout shift matters more than seeing the branded font on slow networks. - -**Preload the critical weight only**: typically the regular-weight body font used above the fold. Preloading every weight costs more bandwidth than it saves. - -**Variable fonts for 3+ weights or styles**: a single variable font file is usually smaller than three static weight files, gives fractional weight control, and pairs well with `font-optical-sizing: auto`. For 1–2 weights, static is fine. - -## Modern Web Typography - -### Fluid Type - -Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate (higher vw = faster scaling). Add a rem offset so it doesn't collapse to 0 on small screens. - -**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes. - -**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI; fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it. - -**Bound your clamp()**: keep `max-size ≤ ~2.5 × min-size`. Wider ratios break the browser's zoom and reflow behaviour and make large viewports feel like the page is shouting. - -**Scale container width and font-size together** so effective character measure stays in the 45–75ch band at every viewport. A heading that widens faster than its container drifts out of the comfortable measure at the top end. - -### OpenType Features - -Most developers don't know these exist. Use them for polish: - -```css -/* Tabular numbers for data alignment */ -.data-table { font-variant-numeric: tabular-nums; } - -/* Proper fractions */ -.recipe-amount { font-variant-numeric: diagonal-fractions; } - -/* Small caps for abbreviations */ -abbr { font-variant-caps: all-small-caps; } - -/* Disable ligatures in code */ -code { font-variant-ligatures: none; } - -/* Enable kerning (usually on by default, but be explicit) */ -body { font-kerning: normal; } -``` - -Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/). - -### Rendering polish - -```css -/* Even out heading line lengths (browser picks better break points) */ -h1, h2, h3 { text-wrap: balance; } - -/* Reduce orphans and ragged endings in long prose */ -article p { text-wrap: pretty; } - -/* Variable fonts: pick the right optical-size master automatically */ -body { font-optical-sizing: auto; } -``` - -**ALL-CAPS tracking**: capitals sit too close at default spacing. Add 5–12% letter-spacing (`letter-spacing: 0.05em` to `0.12em`) to short all-caps labels, eyebrows, and small headings. Real small caps (via `font-variant-caps`) need the same treatment, slightly gentler. - -## Typography System Architecture - -Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system. - -## Accessibility Considerations - -Beyond contrast ratios (which are well-documented), consider: - -- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout. -- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text. -- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile. -- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets. - ---- - -**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text. diff --git a/.claude/skills/impeccable/reference/ux-writing.md b/.claude/skills/impeccable/reference/ux-writing.md deleted file mode 100644 index 417b5f56c..000000000 --- a/.claude/skills/impeccable/reference/ux-writing.md +++ /dev/null @@ -1,107 +0,0 @@ -# UX Writing - -## The Button Label Problem - -**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns: - -| Bad | Good | Why | -|-----|------|-----| -| OK | Save changes | Says what will happen | -| Submit | Create account | Outcome-focused | -| Yes | Delete message | Confirms the action | -| Cancel | Keep editing | Clarifies what "cancel" means | -| Click here | Download PDF | Describes the destination | - -**For destructive actions**, name the destruction: -- "Delete" not "Remove" (delete is permanent, remove implies recoverable) -- "Delete 5 items" not "Delete selected" (show the count) - -## Error Messages: The Formula - -Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input". - -### Error Message Templates - -| Situation | Template | -|-----------|----------| -| **Format error** | "[Field] needs to be [format]. Example: [example]" | -| **Missing required** | "Please enter [what's missing]" | -| **Permission denied** | "You don't have access to [thing]. [What to do instead]" | -| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." | -| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" | - -### Don't Blame the User - -Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date". - -## Empty States Are Opportunities - -Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items". - -## Voice vs Tone - -**Voice** is your brand's personality, consistent everywhere. -**Tone** adapts to the moment. - -| Moment | Tone Shift | -|--------|------------| -| Success | Celebratory, brief: "Done! Your changes are live." | -| Error | Empathetic, helpful: "That didn't work. Here's what to try..." | -| Loading | Reassuring: "Saving your work..." | -| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | - -**Never use humor for errors.** Users are already frustrated. Be helpful, not cute. - -## Writing for Accessibility - -**Link text** must have standalone meaning: "View pricing plans" not "Click here". **Alt text** describes information, not the image: "Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context. - -## Writing for Translation - -### Plan for Expansion - -German text is ~30% longer than English. Allocate space: - -| Language | Expansion | -|----------|-----------| -| German | +30% | -| French | +20% | -| Finnish | +30-40% | -| Chinese | -30% (fewer chars, but same width) | - -### Translation-Friendly Patterns - -Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear. - -## Consistency: The Terminology Problem - -Pick one term and stick with it: - -| Inconsistent | Consistent | -|--------------|------------| -| Delete / Remove / Trash | Delete | -| Settings / Preferences / Options | Settings | -| Sign in / Log in / Enter | Sign in | -| Create / Add / New | Create | - -Build a terminology glossary and enforce it. Variety creates confusion. - -## Avoid Redundant Copy - -If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well. - -## Loading States - -Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress. - -## Confirmation Dialogs: Use Sparingly - -Most confirmation dialogs are design failures; consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No"). - -## Form Instructions - -Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking. - ---- - -**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors. diff --git a/.claude/skills/impeccable/scripts/cleanup-deprecated.mjs b/.claude/skills/impeccable/scripts/cleanup-deprecated.mjs index 6aee4710a..bc6400e90 100644 --- a/.claude/skills/impeccable/scripts/cleanup-deprecated.mjs +++ b/.claude/skills/impeccable/scripts/cleanup-deprecated.mjs @@ -25,7 +25,7 @@ import { join, resolve } from 'node:path'; const DEPRECATED_NAMES = [ // v2.0 renames 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable teach + 'teach-impeccable', // folded into /impeccable init // v2.1 merges 'arrange', // renamed to layout 'normalize', // merged into polish diff --git a/.claude/skills/impeccable/scripts/command-metadata.json b/.claude/skills/impeccable/scripts/command-metadata.json index 1488bd5f1..83796d54b 100644 --- a/.claude/skills/impeccable/scripts/command-metadata.json +++ b/.claude/skills/impeccable/scripts/command-metadata.json @@ -3,8 +3,8 @@ "description": "Full confirmed-brief-then-build flow. Runs multi-round shape discovery first, resolves visual probe and north-star mock gates when available, then builds and visually iterates. Use when building a new feature end-to-end.", "argumentHint": "[feature description]" }, - "teach": { - "description": "Gathers design context for a project. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.", + "init": { + "description": "Sets up a project for impeccable. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles); offers DESIGN.md (visual: colors, typography, components) when code exists; pre-configures live mode; then recommends the best commands to run next. Every other command reads these files before doing work. Use once per project.", "argumentHint": "" }, "document": { diff --git a/.claude/skills/impeccable/scripts/context.mjs b/.claude/skills/impeccable/scripts/context.mjs new file mode 100644 index 000000000..52263c883 --- /dev/null +++ b/.claude/skills/impeccable/scripts/context.mjs @@ -0,0 +1,253 @@ +/** + * Context loader: prints PRODUCT.md (and DESIGN.md if present) as one + * markdown block on stdout, or exits with empty stdout when no PRODUCT.md + * is found anywhere. The skill keys off "empty stdout" to branch into the + * init flow. + * + * Path resolution (first match wins): + * 1. cwd, if PRODUCT.md or DESIGN.md is there + * 2. .agents/context/ then docs/ + * 3. $IMPECCABLE_CONTEXT_DIR (absolute or cwd-relative) — power-user + * escape hatch, only consulted when defaults are empty + * 4. cwd as a "nothing found" default + * + * `resolveContextDir()` and `loadContext()` are also exported for the + * server-side scripts (live.mjs, live-server.mjs) that need the structured + * shape rather than the markdown block. + */ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; +const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; +const FALLBACK_DIRS = ['.agents/context', 'docs']; + +// ─── Update check ────────────────────────────────────────────────────────── +// Piggyback a lightweight skill-version check on the once-per-session boot. +// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent +// can offer `npx impeccable skills update`. Everything here is best-effort and +// silent on failure: a network problem, sandbox, or missing cache must never +// block context output or print an error. + +const UPDATE_HOST = (process.env.IMPECCABLE_UPDATE_HOST || 'https://impeccable.style').replace(/\/$/, ''); +const UPDATE_CACHE_PATH = + process.env.IMPECCABLE_UPDATE_CACHE || path.join(os.homedir(), '.impeccable', 'update-check.json'); +const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // throttle the network poll to once a day +const RENOTIFY_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000; // don't re-surface the same version for a week +const FETCH_TIMEOUT_MS = 1200; + +export function resolveContextDir(cwd = process.cwd()) { + if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { + return cwd; + } + for (const rel of FALLBACK_DIRS) { + const candidate = path.resolve(cwd, rel); + if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { + return candidate; + } + } + const envDir = process.env.IMPECCABLE_CONTEXT_DIR; + if (envDir && envDir.trim()) { + const trimmed = envDir.trim(); + return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); + } + return cwd; +} + +export function loadContext(cwd = process.cwd()) { + const contextDir = resolveContextDir(cwd); + const productPath = firstExisting(contextDir, PRODUCT_NAMES); + const designPath = firstExisting(contextDir, DESIGN_NAMES); + const product = productPath ? safeRead(productPath) : null; + const design = designPath ? safeRead(designPath) : null; + return { + hasProduct: !!product, + product, + productPath: productPath ? path.relative(cwd, productPath) : null, + hasDesign: !!design, + design, + designPath: designPath ? path.relative(cwd, designPath) : null, + contextDir, + }; +} + +function firstExisting(dir, names) { + for (const name of names) { + const abs = path.join(dir, name); + if (fs.existsSync(abs)) return abs; + } + return null; +} + +function safeRead(p) { + try { + return fs.readFileSync(p, 'utf-8'); + } catch { + return null; + } +} + +/** + * Pull the register (`brand` or `product`) out of PRODUCT.md by looking + * for a `## Register` section and reading the first non-empty line that + * follows it. Returns null when the file is legacy / register-less. + */ +function extractRegister(product) { + if (!product) return null; + const lines = product.split('\n'); + for (let i = 0; i < lines.length; i++) { + if (/^##\s+Register\b/i.test(lines[i].trim())) { + for (let j = i + 1; j < lines.length; j++) { + const next = lines[j].trim(); + if (!next) continue; + const word = next.toLowerCase(); + if (word === 'brand' || word === 'product') return word; + return null; + } + } + } + return null; +} + +/** + * Read the installed skill's own version from the sibling SKILL.md frontmatter + * (this file lives at `/scripts/context.mjs`). Returns null when the + * frontmatter is missing or unreadable. + */ +function readLocalSkillVersion() { + try { + const here = path.dirname(fileURLToPath(import.meta.url)); + const skillMd = path.join(here, '..', 'SKILL.md'); + const content = fs.readFileSync(skillMd, 'utf-8'); + const match = content.match(/^version:\s*(.+)$/m); + return match ? match[1].trim().replace(/^["']|["']$/g, '') : null; + } catch { + return null; + } +} + +function readUpdateCache() { + try { + return JSON.parse(fs.readFileSync(UPDATE_CACHE_PATH, 'utf-8')); + } catch { + return {}; + } +} + +function writeUpdateCache(cache) { + try { + fs.mkdirSync(path.dirname(UPDATE_CACHE_PATH), { recursive: true }); + fs.writeFileSync(UPDATE_CACHE_PATH, JSON.stringify(cache)); + } catch { + // Best-effort: a read-only home dir just means we re-poll next session. + } +} + +/** Compare dotted numeric versions. Returns >0 when a is newer than b. */ +function compareSemver(a, b) { + const pa = String(a).split('.').map(n => parseInt(n, 10) || 0); + const pb = String(b).split('.').map(n => parseInt(n, 10) || 0); + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + const diff = (pa[i] || 0) - (pb[i] || 0); + if (diff !== 0) return diff; + } + return 0; +} + +async function fetchLatestSkillVersion() { + try { + const res = await fetch(`${UPDATE_HOST}/api/version`, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }); + if (!res.ok) return null; + const data = await res.json(); + return typeof data?.skills === 'string' ? data.skills : null; + } catch { + return null; // offline, sandboxed, timed out, or bad JSON: all non-fatal + } +} + +function buildUpdateDirective(localVersion, latestVersion) { + return ( + `UPDATE_AVAILABLE: A newer Impeccable skill is available ` + + `(installed v${localVersion}, latest v${latestVersion}). ` + + `Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` + + `Update now? It runs \`npx impeccable skills update\`." ` + + `If they agree, run \`npx impeccable skills update\` (the update applies to the next session, not this one). ` + + `Either way, continue the current task without waiting, and do not raise this again.` + ); +} + +/** + * Best-effort update directive for the boot output. Returns a string to append + * or null. Polls the version endpoint at most once per day (cached globally in + * the user's home dir) and re-surfaces a given version at most once per week so + * the agent never nags. Opt out entirely with IMPECCABLE_NO_UPDATE_CHECK=1. + */ +async function computeUpdateDirective(now = Date.now()) { + try { + if (process.env.IMPECCABLE_NO_UPDATE_CHECK) return null; + const localVersion = readLocalSkillVersion(); + if (!localVersion) return null; + + const cache = readUpdateCache(); + + // Poll the network only when the throttle window has elapsed. Stamp + // lastCheck even on failure so an offline machine doesn't poll every boot. + if (!cache.lastCheck || now - cache.lastCheck > CHECK_INTERVAL_MS) { + const latest = await fetchLatestSkillVersion(); + cache.lastCheck = now; + if (latest) cache.latestVersion = latest; + writeUpdateCache(cache); + } + + const latest = cache.latestVersion; + if (!latest || compareSemver(latest, localVersion) <= 0) return null; + + // Anti-nag: surface a given version at most once per RENOTIFY window. + if (cache.notifiedVersion === latest && cache.notifiedAt && now - cache.notifiedAt < RENOTIFY_INTERVAL_MS) { + return null; + } + cache.notifiedVersion = latest; + cache.notifiedAt = now; + writeUpdateCache(cache); + + return buildUpdateDirective(localVersion, latest); + } catch { + return null; + } +} + +async function cli() { + const ctx = loadContext(process.cwd()); + const updateDirective = await computeUpdateDirective(); + + if (!ctx.hasProduct) { + // Direct stdout message instead of relying on empty output as a signal + // — cheap models miss the empty case more often than the explicit one. + const parts = [ + 'NO_PRODUCT_MD: This project has no PRODUCT.md yet. ' + + 'Stop the current task, load reference/init.md, and follow its ' + + 'instructions to write PRODUCT.md before resuming.', + ]; + if (updateDirective) parts.push(updateDirective); + process.stdout.write(parts.join('\n\n---\n\n') + '\n'); + process.exit(0); + } + const parts = [`# PRODUCT.md\n\n${ctx.product.trim()}`]; + if (ctx.hasDesign) { + parts.push(`# DESIGN.md\n\n${ctx.design.trim()}`); + } + const register = extractRegister(ctx.product); + const next = register + ? `NEXT STEP: This project's register is \`${register}\`. You MUST now read \`reference/${register}.md\` before producing any design output.` + : `NEXT STEP: You MUST now read the matching register reference (\`reference/brand.md\` or \`reference/product.md\`) before producing any design output. Pick based on PRODUCT.md above.`; + parts.push(next); + if (updateDirective) parts.push(updateDirective); + process.stdout.write(parts.join('\n\n---\n\n') + '\n'); +} + +const _running = process.argv[1]; +if (_running?.endsWith('context.mjs') || _running?.endsWith('context.mjs/')) { + cli(); +} diff --git a/.claude/skills/impeccable/scripts/design-parser.mjs b/.claude/skills/impeccable/scripts/design-parser.mjs index b68756640..82a5cd7dc 100644 --- a/.claude/skills/impeccable/scripts/design-parser.mjs +++ b/.claude/skills/impeccable/scripts/design-parser.mjs @@ -63,7 +63,7 @@ function parseYamlSubset(yaml) { } const key = content.slice(0, colonIdx).trim(); - const rest = content.slice(colonIdx + 1).trim(); + const rest = stripInlineYamlComment(content.slice(colonIdx + 1).trim()); const parent = stack[stack.length - 1].obj; if (rest === '') { @@ -93,6 +93,21 @@ function findTopLevelColon(s) { return -1; } +function stripInlineYamlComment(s) { + let inQuote = null; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (inQuote) { + if (ch === inQuote && s[i - 1] !== '\\') inQuote = null; + } else if (ch === '"' || ch === "'") { + inQuote = ch; + } else if (ch === '#' && i > 0 && /\s/.test(s[i - 1])) { + return s.slice(0, i).trimEnd(); + } + } + return s; +} + function parseScalar(raw) { const s = raw.trim(); if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { diff --git a/.claude/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.claude/skills/impeccable/scripts/detector/browser/injected/index.mjs index c03a69caf..ceff16737 100644 --- a/.claude/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/.claude/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -9,8 +9,21 @@ if (IS_BROWSER) { const EXTENSION_MODE = (_myScript && _myScript.dataset.impeccableExtension === 'true') || document.documentElement.dataset.impeccableExtension === 'true'; - const BRAND_COLOR = 'oklch(55% 0.25 350)'; - const BRAND_COLOR_HOVER = 'oklch(45% 0.25 350)'; + // Kinpaku gold — pinned to the site's brand token (see + // site/styles/kinpaku-tokens.css --ks-kinpaku). Keep this in sync with + // the picker's C.brand in skill/scripts/live-browser.js and the kit's + // picker section in site/styles/kinpaku-kit.css. + // + // One color across both light and dark host pages. The outline is a + // 2px gesture pointing at an element + a labeled tag — it's a marker, + // not body text, so it doesn't need WCAG AA against the page. The + // label text inside the gold tag is dark (LABEL_INK) which has ~16:1 + // against the leaf gold, so reading the rule name is solid in both + // modes. Hover deepens the gold (preserves chroma — never drops it, + // dropping chroma washes the gold into a sand/olive tone). + const BRAND_COLOR = 'oklch(84% 0.19 80.46)'; + const BRAND_COLOR_HOVER = 'oklch(74% 0.18 80)'; + const LABEL_INK = 'oklch(4% 0.004 95)'; const LABEL_BG = BRAND_COLOR; const OUTLINE_COLOR = BRAND_COLOR; @@ -278,7 +291,7 @@ if (IS_BROWSER) { display: 'flex', alignItems: 'center', whiteSpace: 'nowrap', fontSize: '11px', fontWeight: '600', letterSpacing: '0.02em', - color: 'white', lineHeight: '14px', + color: LABEL_INK, lineHeight: '14px', background: LABEL_BG, fontFamily: 'system-ui, sans-serif', borderRadius: '4px 4px 0 0', @@ -398,7 +411,7 @@ if (IS_BROWSER) { banner.className = 'impeccable-overlay impeccable-banner'; Object.assign(banner.style, { position: 'fixed', top: '0', left: '0', right: '0', zIndex: '100000', - background: LABEL_BG, color: 'white', + background: LABEL_BG, color: LABEL_INK, fontFamily: 'system-ui, sans-serif', fontSize: '13px', display: 'flex', alignItems: 'center', pointerEvents: 'auto', height: '36px', overflow: 'hidden', maxWidth: '100vw', @@ -1223,12 +1236,12 @@ if (IS_BROWSER) { } console.group( `%c[impeccable] ${allFindings.length} anti-pattern${allFindings.length === 1 ? '' : 's'} found`, - 'color: oklch(60% 0.25 350); font-weight: bold' + 'color: oklch(84% 0.19 80.46); font-weight: bold' ); for (const { el, findings } of allFindings) { for (const f of findings) { console.log(`%c${f.type || f.id}%c ${f.detail || f.snippet}`, - 'color: oklch(55% 0.25 350); font-weight: bold', 'color: inherit', el); + 'color: oklch(84% 0.19 80.46); font-weight: bold', 'color: inherit', el); } } console.groupEnd(); @@ -1249,6 +1262,10 @@ if (IS_BROWSER) { const groupMap = new Map(); const _disabled = EXTENSION_MODE ? (window.__IMPECCABLE_CONFIG__?.disabledRules || []) : []; const _ruleOk = (id) => !_disabled.length || !_disabled.includes(id); + // Note: provider-gated rules (--gpt / --gemini) are NOT filtered here. In a + // real browser env (detector page, live overlay, extension) running every + // check is free, so we always surface them; the gating is purely a CLI + // output concern, applied in the Node engines' detect* return paths. for (const el of document.querySelectorAll('*')) { // Skip impeccable's own elements and any descendants (overlays, labels, banner, nav buttons) @@ -1270,11 +1287,23 @@ if (IS_BROWSER) { ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), - ...checkElementHeroEyebrowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementOversizedH1DOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementClippedOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementGptBorderShadowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementTextOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ].filter(f => _ruleOk(f.type)); addBrowserFindings(groupMap, el, findings); + + // Hero eyebrow: the offending element is the eyebrow above the heading, + // not the heading itself — highlight the previous sibling instead. + const eyebrowFindings = checkElementHeroEyebrowDOM(el) + .map(f => ({ type: f.id, detail: f.snippet })) + .filter(f => _ruleOk(f.type)); + if (eyebrowFindings.length > 0 && el.previousElementSibling) { + addBrowserFindings(groupMap, el.previousElementSibling, eyebrowFindings); + } } const pageLevelFindings = []; @@ -1306,6 +1335,14 @@ if (IS_BROWSER) { addBrowserFindings(groupMap, document.body, qualityFindings); } + const creamFindings = checkCreamPalette(document) + .map(f => ({ type: f.id, detail: f.snippet })) + .filter(f => _ruleOk(f.type)); + if (creamFindings.length > 0) { + pageLevelFindings.push(...creamFindings); + addBrowserFindings(groupMap, document.body, creamFindings); + } + // Regex-on-HTML checks (shared with Node) // Clone the document and strip impeccable-live overlay nodes before the // regex scan, so the inspector's own inline styles (transitions on top/ diff --git a/.claude/skills/impeccable/scripts/detector/cli/main.mjs b/.claude/skills/impeccable/scripts/detector/cli/main.mjs index 76da09f0c..94283e01b 100644 --- a/.claude/skills/impeccable/scripts/detector/cli/main.mjs +++ b/.claude/skills/impeccable/scripts/detector/cli/main.mjs @@ -41,7 +41,7 @@ function formatFindings(findings, jsonMode) { // Stdin handling // --------------------------------------------------------------------------- -async function handleStdin() { +async function handleStdin(options = {}) { const chunks = []; for await (const chunk of process.stdin) chunks.push(chunk); const input = Buffer.concat(chunks).toString('utf-8'); @@ -50,10 +50,10 @@ async function handleStdin() { const fp = parsed?.tool_input?.file_path; if (fp && fs.existsSync(fp)) { return HTML_EXTENSIONS.has(path.extname(fp).toLowerCase()) - ? detectHtml(fp) : detectText(fs.readFileSync(fp, 'utf-8'), fp); + ? detectHtml(fp, options) : detectText(fs.readFileSync(fp, 'utf-8'), fp, options); } } catch { /* not JSON */ } - return detectText(input, ''); + return detectText(input, '', options); } @@ -81,6 +81,8 @@ Scan files or URLs for UI anti-patterns and design quality issues. Options: --fast Regex-only mode (skip static HTML/CSS analysis, faster but misses linked stylesheets) --json Output results as JSON + --gpt Also report GPT-specific provider tells (off by default) + --gemini Also report Gemini-specific provider tells (off by default) --help Show this help message Detection modes: @@ -106,6 +108,10 @@ async function detectCli() { const jsonMode = args.includes('--json'); const helpMode = args.includes('--help'); const fastMode = args.includes('--fast'); + const providers = []; + if (args.includes('--gpt')) providers.push('gpt'); + if (args.includes('--gemini')) providers.push('gemini'); + const scanOptions = { providers }; const targets = args.filter(a => !a.startsWith('--')); if (helpMode) { printUsage(); process.exit(0); } @@ -113,7 +119,7 @@ async function detectCli() { let allFindings = []; if (!process.stdin.isTTY && targets.length === 0) { - allFindings = await handleStdin(); + allFindings = await handleStdin(scanOptions); } else { const paths = targets.length > 0 ? targets : [process.cwd()]; const urlTargetCount = paths.filter(target => /^https?:\/\//i.test(target)).length; @@ -124,8 +130,8 @@ async function detectCli() { if (/^https?:\/\//i.test(target)) { try { const scanner = browserDetector - ? (url) => browserDetector.detectUrl(url) - : (url) => detectUrl(url); + ? (url) => browserDetector.detectUrl(url, scanOptions) + : (url) => detectUrl(url, scanOptions); allFindings.push(...await scanner(target)); } catch (e) { process.stderr.write(`Error: ${e.message}\n`); } continue; @@ -192,9 +198,9 @@ async function detectCli() { const ext = path.extname(file).toLowerCase(); let fileFindings; if (!fastMode && HTML_EXTENSIONS.has(ext)) { - fileFindings = await detectHtml(file); + fileFindings = await detectHtml(file, scanOptions); } else { - fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file); + fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file, scanOptions); } // Annotate findings with import context const importers = importedByMap.get(file); @@ -209,9 +215,9 @@ async function detectCli() { } else if (stat.isFile()) { const ext = path.extname(resolved).toLowerCase(); if (!fastMode && HTML_EXTENSIONS.has(ext)) { - allFindings.push(...await detectHtml(resolved)); + allFindings.push(...await detectHtml(resolved, scanOptions)); } else { - allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved)); + allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved, scanOptions)); } } } diff --git a/.claude/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.claude/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 1afbe3f12..88439a5c3 100644 --- a/.claude/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.claude/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -38,7 +38,7 @@ const OVERUSED_FONTS = new Set([ // Older monoculture (still ubiquitous): 'inter', 'roboto', 'open sans', 'lato', 'montserrat', 'arial', 'helvetica', // Newer monoculture (the Anthropic-skill / Vercel / GitHub default wave): - 'fraunces', 'instrument sans', + 'fraunces', 'instrument sans', 'instrument serif', 'geist', 'geist sans', 'geist mono', 'mona sans', 'plus jakarta sans', 'space grotesk', 'recoleta', @@ -165,6 +165,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'AI color palette', }, + { + id: 'cream-palette', + category: 'slop', + name: 'Cream / beige palette', + description: + 'A warm cream or beige page background has become the default "tasteful" AI surface, reached for by reflex. Choose a background that comes from a deliberate palette, not the safe warm off-white.', + skillSection: 'Color & Contrast', + skillGuideline: 'cream and beige as the default surface', + }, { id: 'nested-cards', category: 'slop', @@ -183,15 +192,6 @@ const ANTIPATTERNS = [ skillSection: 'Layout & Space', skillGuideline: 'same spacing everywhere', }, - { - id: 'everything-centered', - category: 'slop', - name: 'Everything centered', - description: - 'Every text element is center-aligned. Left-aligned text with asymmetric layouts feels more designed. Center only hero sections and CTAs.', - skillSection: 'Layout & Space', - skillGuideline: 'Center everything', - }, { id: 'bounce-easing', category: 'slop', @@ -247,17 +247,72 @@ const ANTIPATTERNS = [ skillSection: 'Typography', skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding', }, + { + id: 'numbered-section-markers', + category: 'slop', + severity: 'advisory', + name: 'Numbered section markers (01 / 02 / 03)', + description: + 'Numbered display markers as section labels (01, 02, 03) are the AI editorial scaffold one tier deeper than tracked eyebrow chips. If you find yourself reaching for them, choose a different section cadence.', + skillSection: 'Layout & Space', + skillGuideline: 'numbered section markers', + }, + { + id: 'em-dash-overuse', + category: 'slop', + name: 'Em-dash overuse', + description: + 'More than two em-dashes (— or --) in body copy is an AI cadence tell. Use commas, colons, periods, or parentheses instead.', + skillSection: 'Copy', + skillGuideline: 'no em dashes', + }, + { + id: 'marketing-buzzword', + category: 'slop', + name: 'Marketing buzzword', + description: + 'Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does.', + skillSection: 'Copy', + skillGuideline: 'marketing buzzwords', + }, + { + id: 'aphoristic-cadence', + category: 'slop', + name: 'Aphoristic-cadence copy', + description: + 'Three or more sections landing on a short rebuttal sentence ("X. No Y." / "X. Just Y.") or a manufactured-contrast aphorism ("Not a feature. A platform.") reads as AI cadence, not voice. Once is fine; the pattern is the tell.', + skillSection: 'Copy', + skillGuideline: 'aphoristic cadence', + }, + { + id: 'oversized-h1', + category: 'slop', + name: 'Oversized hero headline', + description: + 'A full-sentence headline set at display size ends up dominating the viewport, leaving no room for anything else above the fold. A punchy one- or two-word headline at that size is fine — the problem is a long headline blown up too large. Set long headlines smaller, or tighten the copy.', + skillSection: 'Typography', + skillGuideline: 'long headline set at display size', + }, + { + id: 'extreme-negative-tracking', + category: 'slop', + name: 'Crushed letter spacing', + description: + 'Letter-spacing pulled tighter than the point where characters keep their own shapes costs legibility. Tighten display type optically, not destructively.', + skillSection: 'Typography', + skillGuideline: 'letter spacing crushed past legibility', + }, + { + id: 'broken-image', + category: 'quality', + name: 'Broken or placeholder image', + description: + ' tags with empty src, missing src, or placeholder values ship as broken-image boxes. Use real images, generated assets, or remove the tag.', + skillSection: 'Imagery', + skillGuideline: 'broken image references', + }, // ── Quality: general design and accessibility issues ── - { - id: 'pure-black-white', - category: 'quality', - name: 'Pure black background', - description: - 'Pure #000000 as a background color looks harsh and unnatural. Tint it slightly toward your brand hue (e.g., oklch(12% 0.01 250)) for a more refined feel.', - skillSection: 'Color & Contrast', - skillGuideline: 'pure black (#000)', - }, { id: 'gray-on-color', category: 'quality', @@ -297,7 +352,9 @@ const ANTIPATTERNS = [ category: 'quality', name: 'Cramped padding', description: - 'Text is too close to the edge of its container. Add at least 8px (ideally 12-16px) of padding inside bordered or colored containers.', + 'Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.', + skillSection: 'Layout & Space', + skillGuideline: 'inside bordered or colored containers', }, { id: 'body-text-viewport-edge', @@ -350,6 +407,70 @@ const ANTIPATTERNS = [ description: 'Letter spacing above 0.05em on body text disrupts natural character groupings and slows reading. Reserve wide tracking for short uppercase labels only.', }, + { + id: 'text-overflow', + category: 'quality', + name: 'Content overflowing its container', + description: + 'Content renders wider than its container, spilling out or forcing a horizontal scrollbar. Let text wrap, constrain widths, or give the region a deliberate scroll affordance.', + skillSection: 'Layout & Space', + skillGuideline: 'content wider than its container', + }, + { + id: 'clipped-overflow-container', + category: 'quality', + name: 'Positioned child clipped by overflow container', + description: + 'A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.', + skillSection: 'Layout & Space', + skillGuideline: 'overflow container clipping positioned children', + }, + + // ── Provider tells: opt-in via --gpt / --gemini (gated off by default) ── + { + id: 'gpt-thin-border-wide-shadow', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Hairline border with wide shadow', + description: + 'A hairline border paired with a wide, diffuse shadow is a recurring generated-UI signature. Commit to one — a defined edge or a soft elevation — rather than both at once.', + skillSection: 'Visual Details', + skillGuideline: 'hairline border plus wide diffuse shadow', + }, + { + id: 'repeating-stripes-gradient', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Repeating-gradient stripes', + description: + 'Repeating-gradient stripes used as surface decoration are a recurring generated-UI signature. Reach for a deliberate texture or leave the surface plain.', + skillSection: 'Visual Details', + skillGuideline: 'repeating-gradient decorative stripes', + }, + { + id: 'theater-slop-phrase', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Theater framing copy', + description: + 'Dismissing something as "theater" is a recurring generated-copy tic. Say plainly what the thing does or does not do.', + skillSection: 'Copy', + skillGuideline: 'theater framing copy', + }, + { + id: 'image-hover-transform', + category: 'slop', + severity: 'advisory', + gated: 'gemini', + name: 'Image hover transform', + description: + 'Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction.', + skillSection: 'Motion', + skillGuideline: 'image scale or rotate on hover', + }, ]; // --- cli/engine/shared/color.mjs --- @@ -527,11 +648,6 @@ function checkColors(opts) { } const findings = []; - // Pure black background (only solid or near-solid, not semi-transparent overlays) - if (bgColor && bgColor.a >= 0.9 && bgColor.r === 0 && bgColor.g === 0 && bgColor.b === 0) { - findings.push({ id: 'pure-black-white', snippet: '#000000 background' }); - } - if (hasDirectText && textColor && !isEmojiOnly) { // Run background-dependent checks against either a solid bg or, if the // ancestor is a gradient, against every gradient stop (use the worst case). @@ -587,9 +703,6 @@ function checkColors(opts) { // Tailwind class checks if (classList) { const classStr = typeof classList === 'string' ? classList : Array.from(classList).join(' '); - if (/\bbg-black\b(?!\/)/.test(classStr)) { - findings.push({ id: 'pure-black-white', snippet: 'bg-black' }); - } const grayMatch = classStr.match(/\btext-(?:gray|slate|zinc|neutral|stone)-\d+\b/); const colorBgMatch = classStr.match(/\bbg-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+\b/); @@ -905,12 +1018,6 @@ function checkHtmlPatterns(html) { // --- Color --- - // Pure black background - const pureBlackBgRe = /background(?:-color)?\s*:\s*(?:#000000|#000|rgb\(\s*0,\s*0,\s*0\s*\))\b/gi; - if (pureBlackBgRe.test(html)) { - findings.push({ id: 'pure-black-white', snippet: 'Pure #000 background' }); - } - // AI color palette: purple/violet const purpleHexRe = /#(?:7c3aed|8b5cf6|a855f7|9333ea|7e22ce|6d28d9|6366f1|764ba2|667eea)\b/gi; if (purpleHexRe.test(html)) { @@ -1027,6 +1134,39 @@ function checkHtmlPatterns(html) { } } + // --- Provider tells (gated): repeating-gradient stripes (GPT) --- + if (/repeating-(?:linear|radial|conic)-gradient\s*\(/i.test(html)) { + findings.push({ id: 'repeating-stripes-gradient', snippet: 'repeating-gradient decorative stripes' }); + } + + // --- Provider tells (gated): "X theater" framing copy (GPT) --- + // Lives here (regex-on-HTML) rather than in the text-content analyzers so it + // runs in the bundled browser path too, not just the CLI/static path. + { + const bodyText = html + .replace(/]*>[\s\S]*?<\/script>/gi, ' ') + .replace(/]*>[\s\S]*?<\/style>/gi, ' ') + .replace(/<[^>]+>/g, ' '); + const tm = /\b(\w+)\s+theater\b/i.exec(bodyText); + if (tm) findings.push({ id: 'theater-slop-phrase', snippet: `"${tm[0].trim()}"` }); + } + + // --- Provider tells (gated): image hover transform (Gemini) --- + // A CSS `img...:hover { transform: ... }` rule, or a Tailwind hover:scale / + // hover:rotate / hover:translate utility on an . Each distinct + // mechanism is its own finding. + const imgHoverCss = /\bimg\b[^,{}]*:hover\b[^{}]*\{[^}]*\btransform\s*:\s*(?:scale|rotate|translate|matrix|skew)/i; + if (imgHoverCss.test(html)) { + findings.push({ id: 'image-hover-transform', snippet: 'img:hover { transform } rule' }); + } + const imgTagRe = /]*\bclass\s*=\s*"([^"]*)"/gi; + let im; + while ((im = imgTagRe.exec(html)) !== null) { + if (/\bhover:(?:scale|rotate|translate|skew)-/.test(im[1])) { + findings.push({ id: 'image-hover-transform', snippet: 'Tailwind hover transform on ' }); + } + } + return findings; } @@ -1673,7 +1813,7 @@ function resolveLengthPx(value, fontSizePx) { // Both adapters resolve font-size, line-height and letter-spacing to pixels // before calling this so the pure function only deals with numbers. function checkQuality(opts) { - const { el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax = 80, viewportWidth = 0 } = opts; + const { el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax = 80, viewportWidth = 0, win = null } = opts; const findings = []; // Skip browser extension injected elements const elId = el.id || ''; @@ -1724,6 +1864,155 @@ function checkQuality(opts) { } } + // --- Flush against a visible boundary --- + // Fires when a container has a visible boundary (border, outline, OR a + // non-transparent background) AND near-zero padding on the bounded + // side(s) AND text-bearing children land flush against the boundary. + // + // Distinct from cramped-padding: that rule needs the element itself to + // have direct text (hasDirectText). This rule targets the OPPOSITE + // shape — a container with NO direct text, only children — which is + // exactly what cramped-padding misses (a section wrapping a label + + // list lands a free pass). + // + // The classic shape: agent writes `padding: 28px 0 0` shorthand on a + // section that also has a border, zeroing horizontal padding so the + // text-bearing children touch the side borders. Background and + // outline count too: a colored card with zero padding has the same + // visual failure mode. + { + const FLUSH_SKIP_TAGS = new Set(['HTML', 'BODY', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ARTICLE', 'ASIDE', 'BUTTON', 'A', 'LABEL', 'SUMMARY', 'CODE', 'PRE', 'INPUT', 'TEXTAREA', 'SELECT', 'FORM', 'FIGURE', 'TABLE', 'TBODY', 'THEAD', 'TR', 'TD', 'TH']); + const upperTag = tag ? tag.toUpperCase() : ''; + const elPosition = style.position || ''; + if ( + !FLUSH_SKIP_TAGS.has(upperTag) && + !hasDirectText && + !['fixed', 'absolute'].includes(elPosition) && + el.children && el.children.length > 0 + ) { + const isTransparent = (c) => + !c || c === 'transparent' || c === 'rgba(0, 0, 0, 0)' || + /^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*0(?:\.0+)?\s*\)$/.test(c); + + const borderW = { + top: parseFloat(style.borderTopWidth) || 0, + right: parseFloat(style.borderRightWidth) || 0, + bottom: parseFloat(style.borderBottomWidth) || 0, + left: parseFloat(style.borderLeftWidth) || 0, + }; + const borderVisible = { + top: borderW.top > 0 && !isTransparent(style.borderTopColor), + right: borderW.right > 0 && !isTransparent(style.borderRightColor), + bottom: borderW.bottom > 0 && !isTransparent(style.borderBottomColor), + left: borderW.left > 0 && !isTransparent(style.borderLeftColor), + }; + // Outline detection. jsdom decomposes `border` shorthand into + // border{Top,…}Width/Color but does NOT decompose `outline` — + // the longhands come back empty when the value was set via the + // shorthand. Fall back to parsing `style.outline` ourselves. + let outlineW = parseFloat(style.outlineWidth) || 0; + let outlineStyleVal = style.outlineStyle || ''; + let outlineColorVal = style.outlineColor || ''; + if (!outlineW && style.outline) { + const wMatch = style.outline.match(/(\d+(?:\.\d+)?)\s*px/); + if (wMatch) outlineW = parseFloat(wMatch[1]) || 0; + if (!outlineStyleVal) { + outlineStyleVal = /\b(solid|dashed|dotted|double|groove|ridge|inset|outset)\b/.test(style.outline) ? 'solid' : ''; + } + if (!outlineColorVal) { + const cMatch = style.outline.match(/(rgba?\([^)]+\)|#[0-9a-fA-F]{3,8}|[a-zA-Z]+)\s*$/); + if (cMatch) outlineColorVal = cMatch[1]; + } + } + const outlineVisible = outlineW > 0 && !isTransparent(outlineColorVal) && outlineStyleVal && outlineStyleVal !== 'none'; + const bgVisible = !isTransparent(style.backgroundColor); + + const anyVisible = borderVisible.top || borderVisible.right || borderVisible.bottom || borderVisible.left || outlineVisible || bgVisible; + if (anyVisible) { + // Resolve padding to px (jsdom returns raw "1.5rem" etc., not the + // computed px value; parseFloat would strip the unit and treat + // 1.5rem as 1.5px, false-flagging legitimate insets). + const pad = { + top: resolveLengthPx(style.paddingTop, fontSize) ?? 0, + right: resolveLengthPx(style.paddingRight, fontSize) ?? 0, + bottom: resolveLengthPx(style.paddingBottom, fontSize) ?? 0, + left: resolveLengthPx(style.paddingLeft, fontSize) ?? 0, + }; + const PAD_THRESHOLD = 2; + // Children-insulate-this-side: a side is insulated if ANY direct + // child has its own padding ≥ 4px on that side. Rationale: in + // typical flow, only the first/last (or leftmost/rightmost) + // children actually sit at the parent's edges. If even one of + // them has its own padding, the visual flush is broken on that + // side. Classic example: a column-flow card frame where the + // top child (header) has padding-top:12 and the bottom child + // (footer) has padding-bottom:8 — the parent's padding:0 doesn't + // matter; nothing is actually flush. The `any-child-insulates` + // heuristic accepts some false negatives (a card with one heavily + // padded middle child won't flag) for far fewer false positives. + const CHILD_INSULATE_THRESHOLD = 4; + const childrenInsulate = { top: false, right: false, bottom: false, left: false }; + for (const child of el.children) { + let childStyle = null; + if (win && typeof win.getComputedStyle === 'function') { + try { childStyle = win.getComputedStyle(child); } catch {} + } + if (!childStyle && typeof getComputedStyle === 'function') { + try { childStyle = getComputedStyle(child); } catch {} + } + if (!childStyle) continue; + const childPad = { + top: resolveLengthPx(childStyle.paddingTop, fontSize) ?? 0, + right: resolveLengthPx(childStyle.paddingRight, fontSize) ?? 0, + bottom: resolveLengthPx(childStyle.paddingBottom, fontSize) ?? 0, + left: resolveLengthPx(childStyle.paddingLeft, fontSize) ?? 0, + }; + for (const s of ['top', 'right', 'bottom', 'left']) { + if (childPad[s] >= CHILD_INSULATE_THRESHOLD) childrenInsulate[s] = true; + } + } + + const flushSides = []; + for (const side of ['top', 'right', 'bottom', 'left']) { + const sideBounded = borderVisible[side] || outlineVisible || bgVisible; + if (sideBounded && pad[side] <= PAD_THRESHOLD && !childrenInsulate[side]) { + flushSides.push(side); + } + } + + if (flushSides.length > 0) { + // Confirm at least one direct child has substantial text content + // (> 4 chars). Without this, the flush is harmless: e.g. an + // image-only card. + let hasTextChild = false; + for (const child of el.children) { + const childText = (child.textContent || '').trim(); + if (childText.length > 4) { hasTextChild = true; break; } + } + if (hasTextChild) { + const cls = (typeof el.className === 'string' && el.className.trim()) + ? el.className.trim().split(/\s+/)[0] + : ''; + const boundaryParts = []; + const borderSidesVisible = ['top', 'right', 'bottom', 'left'].filter(s => borderVisible[s]); + if (borderSidesVisible.length === 4) boundaryParts.push('border'); + else if (borderSidesVisible.length > 0) boundaryParts.push(`border-${borderSidesVisible.join('/')}`); + if (outlineVisible) boundaryParts.push('outline'); + if (bgVisible) boundaryParts.push('bg'); + const sidesLabel = flushSides.length === 4 ? 'all sides' : flushSides.join('/'); + const ident = cls + ? `<${tag.toLowerCase()}> "${cls}"` + : `<${tag.toLowerCase()}>`; + findings.push({ + id: 'cramped-padding', + snippet: `${ident}: children flush against ${boundaryParts.join('+')} on ${sidesLabel} (no inset)`, + }); + } + } + } + } + } + // --- Body text touching viewport edge --- (browser-only: needs rect) // Catches the failure mode where the agent ships body paragraphs // with NO container providing horizontal padding — text bleeds @@ -1804,6 +2093,20 @@ function checkQuality(opts) { } } + // --- Crushed letter spacing (mirror of wide-tracking) --- + // Tracking pulled tighter than ~-0.05em crushes characters into each other. + // Optical tightening that display type legitimately wants (around -0.02em) + // stays well above this floor. + if (hasDirectText && textLen > 20 && fontSize > 0) { + if (letterSpacingPx != null && letterSpacingPx < 0) { + const trackingEm = letterSpacingPx / fontSize; + if (trackingEm <= -0.05) { + const excerpt = (el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 40); + findings.push({ id: 'extreme-negative-tracking', snippet: `letter-spacing: ${trackingEm.toFixed(2)}em — "${excerpt}"` }); + } + } + } + return findings; } @@ -1820,7 +2123,7 @@ function checkElementQualityDOM(el) { const rect = el.getBoundingClientRect(); const lineMax = (typeof window !== 'undefined' && window.__IMPECCABLE_CONFIG__?.lineLengthMax) || 80; const viewportWidth = (typeof window !== 'undefined' ? window.innerWidth : 0) || 0; - return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax, viewportWidth }); + return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax, viewportWidth, win: typeof window !== 'undefined' ? window : null }); } // Pure page-level skipped-heading walk. Takes a Document so it works in both @@ -1862,7 +2165,7 @@ function checkElementQuality(el, style, tag, window) { const fontSize = resolveFontSizePx(el, window); const lineHeightPx = resolveLengthPx(style.lineHeight, fontSize); const letterSpacingPx = resolveLengthPx(style.letterSpacing, fontSize); - return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect: null }); + return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect: null, win: window }); } function checkElementBorders(tag, style, overrides, resolvedRadius) { @@ -2303,40 +2606,213 @@ function checkPageLayout(doc, win) { } } - // Everything centered - const textEls = doc.querySelectorAll('h1, h2, h3, h4, h5, h6, p, li, div, button'); - let centeredCount = 0; - let totalText = 0; - for (const el of textEls) { - const hasDirectText = [...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim().length >= 3); - if (!hasDirectText) continue; - totalText++; - - let cur = el; - let isCentered = false; - while (cur && cur.nodeType === 1) { - const rawStyle = cur.getAttribute?.('style') || ''; - const cls = cur.getAttribute?.('class') || ''; - if (/text-align\s*:\s*center/i.test(rawStyle) || /\btext-center\b/.test(cls)) { - isCentered = true; - break; - } - if (cur.tagName === 'BODY') break; - cur = cur.parentElement; - } - if (isCentered) centeredCount++; - } - - if (totalText >= 5 && centeredCount / totalText > 0.7) { - findings.push({ - id: 'everything-centered', - snippet: `${centeredCount}/${totalText} text elements centered (${Math.round(centeredCount / totalText * 100)}%)`, - }); - } - return findings; } +// ─── Cream / beige palette (the default "tasteful" AI surface) ──────────────── +// A warm, lightly-tinted off-white page background — light, with R≥G≥B and a +// small warm tint (not white, not a strong color). The current reflex surface. +function isCreamColor(rgb) { + if (!rgb) return false; + const { r, g, b } = rgb; + if (Math.min(r, g, b) < 209) return false; // must be light + if (!(r >= g && g >= b)) return false; // warm ordering + const warmth = r - b; + return warmth >= 6 && warmth <= 48; // tinted, not white, not strong +} + +// Tailwind background utilities that render as a warm off-white surface. The +// static engine doesn't fetch Tailwind's CSS, so a `bg-amber-50` on +// resolves to nothing in computed style — catch it from the class list +// instead. Candidate tokens map to their actual Tailwind hex and are still +// filtered through isCreamColor, so neutral grays (stone) and over-saturated +// shades drop out on their own. +const TAILWIND_BG_HEX = { + 'bg-amber-50': '#fffbeb', 'bg-amber-100': '#fef3c7', + 'bg-orange-50': '#fff7ed', 'bg-orange-100': '#ffedd5', + 'bg-yellow-50': '#fefce8', + 'bg-stone-50': '#fafaf9', 'bg-stone-100': '#f5f5f4', 'bg-stone-200': '#e7e5e4', +}; + +function creamFromClassList(cls) { + if (!cls) return null; + // Arbitrary value: bg-[#f5f0e6] / bg-[rgb(245_240_230)] (underscores = spaces). + const arb = cls.match(/\bbg-\[([^\]]+)\]/); + if (arb && isCreamColor(parseAnyColor(arb[1].replace(/_/g, ' ')))) return `bg-[${arb[1]}]`; + // Named warm-light utilities. + for (const [tok, hex] of Object.entries(TAILWIND_BG_HEX)) { + if (new RegExp(`(^|\\s)${tok}($|\\s)`).test(cls) && isCreamColor(parseAnyColor(hex))) return tok; + } + return null; +} + +function checkCreamPalette(doc, win) { + const findings = []; + const body = doc.body || (doc.querySelector ? doc.querySelector('body') : null); + if (!body) return findings; + const html = doc.documentElement; + const getCS = (el) => (win ? win.getComputedStyle(el) : getComputedStyle(el)); + + // 1. Computed background — covers inline / close.', 'Prefix every preview selector with the matching [data-impeccable-variant="N"] selector.', 'Keep selectors anchored to the generated variant wrapper; do not rely on component CSS scoping for preview rules.', ], forbidden: [ 'Do not use @scope for this styleMode.', + 'Do not wrap style content in a JSX/TSX template literal ({` ... `}); that syntax is for .tsx/.jsx only.', + 'Do not put { immediately after the style opening tag; Astro parses { as expression syntax.', ], }; } @@ -629,4 +632,15 @@ if (_running?.endsWith('live-wrap.mjs') || _running?.endsWith('live-wrap.mjs/')) } // Test exports (used by tests/live-wrap.test.mjs) -export { buildSearchQueries, findElement, findClosingLine, detectCommentSyntax }; +export { + buildSearchQueries, + findElement, + findClosingLine, + detectCommentSyntax, + findAllElements, + filterByText, + findFileWithQuery, + detectStyleMode, + buildCssAuthoring, + buildCssSelectorPrefixExamples, +}; diff --git a/.claude/skills/impeccable/scripts/live.mjs b/.claude/skills/impeccable/scripts/live.mjs index cafb0eca9..8acd300ed 100644 --- a/.claude/skills/impeccable/scripts/live.mjs +++ b/.claude/skills/impeccable/scripts/live.mjs @@ -21,7 +21,7 @@ import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { loadContext } from './load-context.mjs'; +import { loadContext } from './context.mjs'; import { resolveFiles } from './live-inject.mjs'; import { readLiveServerInfo } from './impeccable-paths.mjs'; @@ -80,7 +80,7 @@ The agent should then: process.exit(1); } - // 4. Load PRODUCT.md + DESIGN.md context (auto-migrates legacy .impeccable.md) + // 4. Load PRODUCT.md + DESIGN.md context. const ctx = loadContext(process.cwd()); // 5. Compute drift-heal: compare resolved inject targets against the @@ -102,7 +102,6 @@ The agent should then: hasDesign: ctx.hasDesign, design: ctx.design, designPath: ctx.designPath, - migrated: ctx.migrated, }, null, 2)); } diff --git a/.claude/skills/impeccable/scripts/load-context.mjs b/.claude/skills/impeccable/scripts/load-context.mjs deleted file mode 100644 index dc340bf16..000000000 --- a/.claude/skills/impeccable/scripts/load-context.mjs +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Shared context loader for every impeccable command that needs to know - * "who is this for" and "what does this look like". - * - * Input: project root (process.cwd()). - * - * Output (JSON to stdout): - * { - * hasProduct: boolean, // PRODUCT.md found (or auto-migrated) - * product: string | null, // PRODUCT.md contents - * productPath: string | null, // relative path - * hasDesign: boolean, // DESIGN.md found - * design: string | null, // DESIGN.md contents - * designPath: string | null, - * migrated: boolean, // true if we auto-renamed .impeccable.md -> PRODUCT.md - * contextDir: string, // absolute path of the directory the files were found in - * } - * - * Filename matching is case-insensitive for PRODUCT.md and DESIGN.md. The - * Google DESIGN.md convention is uppercase at repo root; Kiro-style and - * lowercase variants are also matched so users don't get punished for case. - * - * Lookup directory resolution (first match wins): - * 1. process.env.IMPECCABLE_CONTEXT_DIR (absolute or relative to cwd) - * 2. cwd, if PRODUCT.md / DESIGN.md / .impeccable.md is there (back-compat) - * 3. Auto-fallback subdirectories of cwd: .agents/context/, then docs/ - * 4. cwd as a default "no context found" location - * - * Legacy `.impeccable.md` -> PRODUCT.md migration only fires at cwd root; - * fallback directories are read-only as far as auto-rename is concerned. - */ - -import fs from 'node:fs'; -import path from 'node:path'; - -const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; -const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; -const LEGACY_NAMES = ['.impeccable.md']; -const FALLBACK_DIRS = ['.agents/context', 'docs']; - -/** - * Resolve the directory that holds PRODUCT.md / DESIGN.md for - * this project. Exported so other scripts (e.g. live-server.mjs) can read the - * design files from the same location the loader uses. - */ -export function resolveContextDir(cwd = process.cwd()) { - // 1. Explicit override - const envDir = process.env.IMPECCABLE_CONTEXT_DIR; - if (envDir && envDir.trim()) { - const trimmed = envDir.trim(); - return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); - } - - // 2. cwd wins if any canonical or legacy file is there. We check legacy too - // so the auto-migration path in loadContext stays predictable. - if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES, ...LEGACY_NAMES])) { - return cwd; - } - - // 3. Auto-fallback subdirs. Match if PRODUCT.md or DESIGN.md is present; - // legacy `.impeccable.md` does not pull the lookup into a fallback dir. - for (const rel of FALLBACK_DIRS) { - const candidate = path.resolve(cwd, rel); - if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { - return candidate; - } - } - - // 4. Nothing found — keep the historical "default to cwd" behaviour so the - // caller's `hasProduct === false` branch still fires the same way. - return cwd; -} - -export function loadContext(cwd = process.cwd()) { - let migrated = false; - const contextDir = resolveContextDir(cwd); - - // 1. Look for PRODUCT.md (case-insensitive) in the resolved dir - let productPath = firstExisting(contextDir, PRODUCT_NAMES); - - // 2. Legacy: if no PRODUCT.md but .impeccable.md exists at cwd root, rename - // it in place. We only migrate at the root — fallback dirs are read-only - // so we don't surprise users by mutating files under docs/ or .agents/. - if (!productPath && contextDir === cwd) { - const legacyPath = firstExisting(cwd, LEGACY_NAMES); - if (legacyPath) { - const newPath = path.join(cwd, 'PRODUCT.md'); - try { - fs.renameSync(legacyPath, newPath); - productPath = newPath; - migrated = true; - } catch { - // Rename failed (permissions, etc.) — fall back to reading legacy in place - productPath = legacyPath; - } - } - } - - // 3. DESIGN.md (case-insensitive) - const designPath = firstExisting(contextDir, DESIGN_NAMES); - - const product = productPath ? safeRead(productPath) : null; - const design = designPath ? safeRead(designPath) : null; - - return { - hasProduct: !!product, - product, - productPath: productPath ? path.relative(cwd, productPath) : null, - hasDesign: !!design, - design, - designPath: designPath ? path.relative(cwd, designPath) : null, - migrated, - contextDir, - }; -} - -function firstExisting(dir, names) { - for (const name of names) { - const abs = path.join(dir, name); - if (fs.existsSync(abs)) return abs; - } - return null; -} - -function safeRead(p) { - try { return fs.readFileSync(p, 'utf-8'); } catch { return null; } -} - -// --------------------------------------------------------------------------- -// CLI mode — print the context as JSON -// --------------------------------------------------------------------------- - -function cli() { - const result = loadContext(process.cwd()); - console.log(JSON.stringify(result, null, 2)); -} - -const _running = process.argv[1]; -if (_running?.endsWith('load-context.mjs') || _running?.endsWith('load-context.mjs/')) { - cli(); -} diff --git a/.claude/skills/impeccable/scripts/palette.mjs b/.claude/skills/impeccable/scripts/palette.mjs new file mode 100644 index 000000000..4c3f1751a --- /dev/null +++ b/.claude/skills/impeccable/scripts/palette.mjs @@ -0,0 +1,633 @@ +#!/usr/bin/env node +/** + * Brand-seed picker. Returns one OKLCH seed color + the mood it most + * naturally evokes, and teaches the model how to compose a full palette + * around it. + * + * The seed is the brand's anchor color. The 5-role palette (bg, surface, + * ink, accent, muted) is composed by the caller at runtime using their + * judgment + the brief (PRODUCT.md / DESIGN.md / user prompt), NOT picked + * from a frozen 4-color preset. + * + * Why: 4-color frozen palettes drift toward safe defaults (warm-cream bg, + * complementary accent on near-white) regardless of brief. A single seed + + * the model's own composition lets the same seed produce a dark-mode jazz + * club or a light-mode hospitality brand depending on what the brief calls + * for. Tested empirically against curated 4-color palettes; seed approach + * wins on mood-fit in 3 of 5 cases and ties on the rest. + * + * Usage: + * node scripts/palette.mjs # pick at random + * node scripts/palette.mjs --id seed-021 # pick a specific seed + * node scripts/palette.mjs --from # hash to a seed (deterministic) + * + * Env vars: + * IMPECCABLE_PALETTE_SEED — same as --from; useful for the eval harness + * to make runs reproducible. + */ + +import crypto from 'node:crypto'; + +// Seeds are inlined (129 entries, hand-curated via a tinder review of +// ~400 candidates from ColorHunt + synthesis + Radix/brand/Pantone anchors). +// Each carries a mood + strategy the judging model produced — surfaced as +// hints, not commands; the brief still drives composition. +const SEEDS = [ + { id: "seed-200", oklch: [0.360, 0.137, 0.0], + mood: "Aesop apothecary shelf — oxblood bottle glass against linen, considered and unhurried", + strategy: "Seed is a deep desaturated red-brown that reads as brand ink itself; I push primary darker toward bottle-glass oxblood, pair with a pure white surface so the red does the work, and use a clear pale-blush accent that can carry dark text in pills." }, + { id: "seed-000", oklch: [0.400, 0.130, 0.0], + mood: "oxblood leather banquette in a 1940s steakhouse — low lamplight on dark wood and burgundy", + strategy: "Near-black bg with the faintest red undertone lets the oxblood primary glow like lamplit leather; warm cream ink and a brass accent complete the chophouse register." }, + { id: "seed-002", oklch: [0.450, 0.150, 0.0], + mood: "darkroom red light — analog photography, blood-warm safelight glow on chemical trays", + strategy: "Near-black surface with a deep oxblood primary lets the seed function like a safelight in a darkroom — the bg disappears so the red becomes the only emotional signal." }, + { id: "seed-003", oklch: [0.500, 0.194, 0.0], + mood: "darkroom safelight — the deep oxblood glow of analog photography, chemical and contemplative", + strategy: "Anchored the seed as primary against pure near-black so the red reads like a single illuminated bulb in a developing room, with cool desaturated ink to evoke silver gelatin print tones." }, + { id: "seed-004", oklch: [0.546, 0.204, 3.4], + mood: "midnight boudoir — velvet rose under low lamplight, perfumed and intimate", + strategy: "Near-black surface lets the rose seed glow like silk in shadow; a warm champagne accent provides the candle-flame counterpoint without breaking the hush." }, + { id: "seed-005", oklch: [0.550, 0.180, 0.0], + mood: "smoldering vermillion at dusk — the last red ember in a blacksmith's forge, iron-rich and quietly violent", + strategy: "Near-black gallery surround lets the seed read as glowing forged metal; ink stays warm-off-white, accent shifts to a hotter ember orange so the primary feels like cooling steel against a fresh strike." }, + { id: "seed-201", oklch: [0.647, 0.262, 0.3], + mood: "Figma plugin marketplace red — confident product-brand crimson, the kind a modern dev tool uses for a 'live' indicator or a primary CTA on a pristine docs page", + strategy: "Pure white surface lets a high-chroma crimson primary do all the brand work, paired with a hue-shifted warm coral accent for hierarchy without competing saturation" }, + { id: "seed-006", oklch: [0.650, 0.160, 0.0], + mood: "1960s Italian cinema — Technicolor lipstick red against a darkened theater", + strategy: "Pure near-black surface lets a saturated cinematic red and its warm peach accent perform like film light projected in a dark room — the brand colors carry the drama, the bg disappears." }, + { id: "seed-008", oklch: [0.520, 0.200, 10.4], + mood: "Negroni hour at a Milanese bar — bittersweet crimson, vermouth and amaro under low tungsten", + strategy: "Seed is a saturated red-crimson with cinematic weight, so I sit it on near-black to let the primary glow like backlit liquor, with a warmer amber accent acting as the citrus twist against the bitter red." }, + { id: "seed-010", oklch: [0.563, 0.223, 11.0], + mood: "Negroni hour on a Milan rooftop — bittersweet crimson, aperitivo light, polished restraint", + strategy: "Seed is a vivid carmine-red with strong chroma, so the surface gets out of the way (pure white) and lets the primary do the aperitivo work, with a cooled garnet accent for tension." }, + { id: "seed-202", oklch: [0.643, 0.247, 7.0], + mood: "Glossier brand pink — modern beauty editorial, confident and current", + strategy: "Pure white bg lets a saturated rose-red primary do all the brand work, paired with a deeper crimson accent for hierarchy — the Stripe/Glossier move where the color carries the mood." }, + { id: "seed-013", oklch: [0.400, 0.130, 20.0], + mood: "Tuscan cellar at dusk — aged terracotta, oxidized iron, the deep red of decanted Sangiovese", + strategy: "Black surface lets the oxblood seed and copper accent glow like firelight on cellar stone; brand colors carry all the warmth while the room recedes." }, + { id: "seed-014", oklch: [0.450, 0.150, 20.0], + mood: "smoldering tannery — oxblood leather, cured under low workshop light", + strategy: "Anchor the deep oxblood seed as primary against a near-black architectural ground, then lift with a single warm ember accent so the leather reads burnished rather than bloody." }, + { id: "seed-016", oklch: [0.550, 0.180, 20.0], + mood: "Negroni hour on a Roman terrace — bitter campari red, vermouth, late golden light spilling on white linen", + strategy: "Pure white surface lets the campari-red primary do all the emotional work, paired with a deeper oxblood accent for bittersweet depth — Italian aperitivo restraint, not warmth-washed." }, + { id: "seed-205", oklch: [0.634, 0.254, 17.6], + mood: "Aesop apothecary bottle — considered red-coral on a clinical white surface, the kind of brand restraint where one saturated object does all the work", + strategy: "Default A pure white surface lets a single coral-red primary carry the entire brand voice; accent shifts to a deeper oxblood for hierarchy without competing chroma." }, + { id: "seed-011", oklch: [0.639, 0.207, 13.5], + mood: "Aperitivo hour in Milan — Campari glow on a white marble bar, crisp and effervescent", + strategy: "Pure white gallery backdrop lets the Campari-red primary ring like a single bitter note; ink is near-black with a whisper of warmth, accent shifts to a deeper oxblood for hierarchy without competing hues." }, + { id: "seed-015", oklch: [0.527, 0.202, 22.7], + mood: "Negroni hour on a Milanese terrace — bittersweet vermillion, aperitivo glassware catching low sun", + strategy: "Seed becomes a saturated aperitivo-red primary against pure white so the color carries the bittersweet warmth alone, paired with a deep oxblood accent for typographic gravitas." }, + { id: "seed-023", oklch: [0.427, 0.175, 29.2], + mood: "blacksmith's forge at dusk — iron heated to ember red, the deep glow of oxidized metal and quenching oil", + strategy: "Pure black bg lets the seed's ember-red glow radiate like hot iron in a dark forge; accent shifts to a copper-amber to suggest scaling metal and sparks, while ink stays near-white for tool-precise legibility." }, + { id: "seed-206", oklch: [0.614, 0.234, 28.2], + mood: "Aesop apothecary bottle — considered red-orange on lab-white, calm utility with a single confident pigment", + strategy: "Pure white surface lets a saturated vermilion primary do all the brand work, paired with a deep oxblood accent for hierarchy without introducing a second hue family" }, + { id: "seed-029", oklch: [0.665, 0.222, 25.7], + mood: "Negroni hour at a Milanese bar — bittersweet orange-red liqueur catching late afternoon light on polished marble", + strategy: "Pure white surface lets the seed's vermilion read like Campari in a glass; a deeper oxblood accent provides the bitter depth, with neutral graphite ink keeping the editorial restraint of Italian design." }, + { id: "seed-022", oklch: [0.418, 0.155, 27.2], + mood: "Pompeiian red fresco — oxidized cinnabar on a museum wall, archaeological gravity", + strategy: "Pure black gallery surface lets the seed's iron-oxide red read as a lit artifact; accent shifts to an aged terracotta amber, so primary and accent form a fired-clay duet against neutral void." }, + { id: "seed-024", oklch: [0.464, 0.169, 26.9], + mood: "Mid-century darkroom under the safelight — developer trays, oxblood leather, the quiet patience of a print emerging", + strategy: "Seed becomes a deep oxblood primary; surface stays pure black so the red glows like a safelight, with a warmer ember accent for hierarchy" }, + { id: "seed-026", oklch: [0.489, 0.190, 28.3], + mood: "smoldering ember in a blacksmith's forge — iron-hot rust, soot, and controlled fire", + strategy: "Near-black soot background lets the seed's red-orange glow like heated metal; ink is bone-white, accent is a cooler tempered-steel orange that creates internal heat gradient with the primary." }, + { id: "seed-027", oklch: [0.568, 0.208, 27.1], + mood: "Sicilian blood orange at golden hour — citrus rind, terracotta, sun on stucco", + strategy: "Seed reads as vivid blood-orange — picked pure white surface so the citrus-red primary and a deep oxblood accent do all the emotional work, like a Loro Piana editorial spread." }, + { id: "seed-028", oklch: [0.591, 0.172, 24.0], + mood: "Sienna-fired ceramic studio at dusk — terracotta cooling on a wheel, hands still dusted with slip", + strategy: "Pure black stage lets the fired-clay primary glow like a kiln ember, with a deeper oxblood accent providing tonal weight rather than hue contrast — a monochrome warm-axis play." }, + { id: "seed-033", oklch: [0.544, 0.169, 31.3], + mood: "1960s Italian terracotta workshop — fired clay, espresso, late-afternoon Mediterranean dust", + strategy: "Pure black ground lets the seed's burnt-sienna primary glow like a lit kiln, with a deeper oxblood accent for restrained warmth tension — the brand carries the heat, the surface stays out." }, + { id: "seed-207", oklch: [0.564, 0.231, 29.1], + mood: "Aesop apothecary bottle — considered red oxide, the calm authority of a well-made object on a white shelf", + strategy: "Seed becomes the singular brand voice against pure white, with a deeper oxblood accent for hierarchy — the surface disappears so the red does all the speaking." }, + { id: "seed-035", oklch: [0.663, 0.153, 32.1], + mood: "Aesop apothecary bottle — clay-fired warmth, considered retail", + strategy: "Pure white surface lets the terracotta primary do the brand work, paired with a deep umber ink and a cooler clay accent for editorial tension." }, + { id: "seed-037", oklch: [0.590, 0.188, 35.8], + mood: "Aesop apothecary bottle — considered terracotta, herbalist restraint, the warmth comes from the glass not the room", + strategy: "Seed becomes a muted terracotta primary against pure white so the brand's warmth carries entirely through the color itself; accent shifts to a deeper umber for quiet hierarchy." }, + { id: "seed-038", oklch: [0.652, 0.229, 34.8], + mood: "blown-glass furnace at dusk — molten orange iron pulled from the kiln, a craftsman's signature heat", + strategy: "Pure black stage so the seed reads as live ember; primary holds the seed's heat, accent shifts to a brass-amber a hue-step away for a 1.7+ contrast pairing without leaving the fire." }, + { id: "seed-039", oklch: [0.653, 0.185, 33.5], + mood: "Aesop apothecary bottle — considered terracotta, quiet retail craft", + strategy: "Seed becomes a grounded clay primary against pure white, paired with a deeper umber accent so the warmth lives entirely in the brand marks, not the surface." }, + { id: "seed-167", oklch: [0.495, 0.134, 36.0], + mood: "Aesop apothecary shelf — burnished terracotta on clinical white, considered craft pharmacy", + strategy: "Treat the seed as a brand-carrying burnt-sienna against a pure paper-white surface so the warmth lives entirely in the primary, with a deep umber accent pulled along the same warm axis for typographic gravity." }, + { id: "seed-147", oklch: [0.500, 0.151, 40.0], + mood: "Aesop apothecary shelf — considered terracotta, pharmacy restraint, the brand color does the work against clinical white", + strategy: "Anchor the seed's burnt-sienna primary against a pure white surface so the rust speaks alone, with a deep umber ink and a cooler clay accent to give the palette product-brand discipline rather than environmental warmth." }, + { id: "seed-040", oklch: [0.660, 0.201, 40.0], + mood: "Aesop apothecary bottle — amber glass on a clean dispensary shelf, considered and clinical-warm", + strategy: "Seed becomes a burnt-amber primary against pure white so the bottle-glass color does the emotional work; accent shifts to a deep olive-bronze for the apothecary-label pairing." }, + { id: "seed-041", oklch: [0.673, 0.217, 38.6], + mood: "Aesop apothecary shelf — considered orange glass, clinical retail restraint", + strategy: "Pure white surface lets the burnt-orange primary do all the brand work, with a deep ink-brown for editorial gravity and a muted clay accent that reads as a sibling, not a contrast." }, + { id: "seed-042", oklch: [0.688, 0.133, 35.8], + mood: "Aesop apothecary shelf — terracotta glass, considered retail", + strategy: "Seed becomes a warm clay primary against pure white so the bottle-on-marble retail feel comes from the brand color alone; a deeper umber accent gives the label-print contrast." }, + { id: "seed-043", oklch: [0.781, 0.119, 38.1], + mood: "Aesop apothecary catalogue — considered terracotta, dermatological restraint, the warm color doing all the work against clinical white", + strategy: "Pure white surface lets the seed's warm clay tone read as the entire brand voice, paired with a deeper umber accent for hierarchy without competing with the primary's warmth." }, + { id: "seed-168", oklch: [0.400, 0.103, 50.0], + mood: "Aesop apothecary bottle — amber glass on a clinical white shelf, considered and pharmaceutical", + strategy: "Pure white surface lets the deep amber primary act like tinted glass against a clean shelf; accent is a muted clay that complements without competing, keeping the brand quiet and product-led." }, + { id: "seed-044", oklch: [0.568, 0.149, 45.9], + mood: "1970s desert highway at golden hour — sun-faded terracotta, denim dust, the warmth of a Polaroid pulled from a glovebox", + strategy: "Seed becomes a burnt-sienna primary against pure white so the terracotta does all the emotional work; a deep indigo accent acts as the denim shadow opposing the sun, creating the era's signature warm/cool tension without tinting the page." }, + { id: "seed-045", oklch: [0.607, 0.163, 47.7], + mood: "Aesop apothecary shelf — considered amber glass, clinical restraint, craft pharmacy", + strategy: "Pure white bg lets the burnt-amber primary do the apothecary work alone, paired with a deeper umber accent and graphite ink for editorial calm." }, + { id: "seed-046", oklch: [0.653, 0.175, 45.0], + mood: "Aesop apothecary shelf — considered amber glass, quiet luxury, restrained craft", + strategy: "Pure black backdrop lets the warm amber primary glow like backlit apothecary glass, with a deeper rust accent providing tonal depth in the same hue family — monochromatic warm against neutral void." }, + { id: "seed-047", oklch: [0.695, 0.205, 43.2], + mood: "Aesop apothecary label — sun-warmed amber glass on a clinical countertop, restrained botanical pharmacy", + strategy: "Pure white surface lets the burnt-amber primary and a deeper sienna accent do all the brand work, like an apothecary bottle photographed under daylight." }, + { id: "seed-051", oklch: [0.704, 0.189, 49.0], + mood: "blacksmith's forge at dusk — glowing iron, hammered copper, ember light against cooling steel", + strategy: "Pure near-black surface lets the seed's molten orange burn like heated metal; accent shifts to a deeper amber-red to suggest the cooling end of the same iron, while ink stays a clean off-white so type reads like chalk on slate." }, + { id: "seed-171", oklch: [0.550, 0.124, 60.0], + mood: "Klim Type Foundry specimen page — considered ochre on paper, design-school-honest", + strategy: "Seed becomes a muted ochre primary on pure white; accent is a deep ink-navy pulled across the wheel for editorial contrast without warmth-pooling in the bg" }, + { id: "seed-148", oklch: [0.650, 0.146, 60.0], + mood: "Klim-style editorial gold — late-afternoon paper light on a serif specimen sheet, considered and dry", + strategy: "Hold the seed's amber as primary on a pure white page so the gold reads as ink rather than atmosphere, and pair with a deep aubergine accent for typographic contrast." }, + { id: "seed-052", oklch: [0.700, 0.130, 60.0], + mood: "late-afternoon terracotta studio — sun-warmed clay, hands-on craft, the hour before dusk", + strategy: "Seed is a saturated amber-ochre with strong environmental association (ceramics, adobe, sunlit plaster), so I lean into Exception (a) with a faintly warm bone surface that reads as lime-washed wall, then deepen the seed slightly for primary and pair it with a fired-clay rust accent for hand-thrown warmth." }, + { id: "seed-053", oklch: [0.773, 0.157, 56.6], + mood: "late-summer apricot orchard at golden hour — sun-warmed fruit, considered Californian craft", + strategy: "Seed is a juicy mid-warm orange at daylight luminance — leaning optimistic/editorial, so pure white surface lets the apricot primary glow without muddying it; a deep wine accent provides the bite." }, + { id: "seed-149", oklch: [0.600, 0.124, 70.0], + mood: "1970s desert highway — late-afternoon amber light on chrome and asphalt", + strategy: "Anchor the amber seed as primary against pure black so the warm hue reads as headlight glow against night; a cooler dusk-mauve accent provides the complementary tension of horizon vs. sun." }, + { id: "seed-054", oklch: [0.740, 0.162, 68.1], + mood: "late-afternoon honey on terracotta — Mediterranean stucco at golden hour, sun-baked amber", + strategy: "Seed is a saturated honey-amber at high lightness; pairing it with pure black lets the warmth read as luminous gold against gravity, like lamplight in a dark room." }, + { id: "seed-055", oklch: [0.774, 0.174, 65.1], + mood: "late-summer honey hour — amber light slanting through a west-facing window, optimistic and golden", + strategy: "Anchor a saturated honey-amber primary on pure white so the warmth radiates from the brand itself, then pair with a deep teak accent for grounded contrast rather than tinting the canvas." }, + { id: "seed-056", oklch: [0.691, 0.146, 74.6], + mood: "Klim-style modern publishing house — late-afternoon paper warmth, considered editorial gold", + strategy: "Pure white surface so the amber seed becomes the brand voice; ink stays near-black neutral and accent shifts to a deep ink-blue to give the gold something structural to lean on." }, + { id: "seed-150", oklch: [0.750, 0.148, 80.0], + mood: "Klim Type Foundry specimen page — late-summer editorial gold, considered and grown-up", + strategy: "Pure white surface lets a single restrained ochre primary do all the brand work, paired with a deep ink-blue accent for typographic contrast in the Klim/Commercial Type tradition." }, + { id: "seed-058", oklch: [0.764, 0.120, 77.1], + mood: "Klim Type Foundry specimen page — late-afternoon ochre, considered editorial typography", + strategy: "Pure white surface lets the ochre primary do the brand work, paired with a deep ink-blue accent for editorial contrast — the type-foundry move where one warm hue carries the whole feeling against neutral paper." }, + { id: "seed-059", oklch: [0.784, 0.144, 79.8], + mood: "late afternoon in a Tuscan limonaia — sun-cured amber on whitewashed plaster", + strategy: "Pure white surface lets the saffron-amber primary and a deep olive accent carry the Mediterranean warmth, with split-complementary tension between gold and a quiet evergreen." }, + { id: "seed-061", oklch: [0.817, 0.161, 75.1], + mood: "late-afternoon honey on Tuscan limestone — golden hour, slow and luminous", + strategy: "Pure white surface lets the amber primary glow like sunlight on a wall, paired with a deep terracotta accent for warm tonal contrast within the same hue family." }, + { id: "seed-063", oklch: [0.842, 0.165, 91.3], + mood: "late-afternoon Tuscan sun on limestone — golden hour, considered, optimistic", + strategy: "Pure white surface lets the amber-gold primary radiate as the mood-carrier, with a deep aubergine accent providing the long shadow that golden light needs to feel three-dimensional." }, + { id: "seed-174", oklch: [0.350, 0.075, 110.0], + mood: "olive grove at late afternoon — sun-cured leaves, dust, and quiet Mediterranean weight", + strategy: "Pure white surface lets a deep, sun-cured olive primary do the emotional work, with a burnt-terracotta accent providing the warm-earth counterpoint olive groves are known for." }, + { id: "seed-117", oklch: [0.650, 0.100, 110.0], + mood: "Klim-style editorial sage — late-summer foundry catalogue, considered olive-yellow on paper", + strategy: "Seed sits at olive-chartreuse; treating it as a quiet typographic primary on pure paper, with a deeper bronze-olive accent for hierarchy — the color does the work, the page disappears." }, + { id: "seed-118", oklch: [0.750, 0.090, 110.0], + mood: "Klim Type Foundry specimen page — late-summer olive light on a working specimen, the honesty of a type designer showing their work", + strategy: "Pure white bg lets a desaturated olive-yellow primary do the editorial work, with a deeper olive-bronze accent providing typographic emphasis the way a specimen uses one heavy weight against the body roman." }, + { id: "seed-065", oklch: [0.797, 0.166, 113.1], + mood: "late-summer olive grove at noon — sun-bleached leaves, dry stone, Mediterranean glare", + strategy: "Hold the seed as a luminous chartreuse-olive primary against pure white so the color reads as sunlit foliage, pairing it with a deep umber accent for the dry-stone contrast." }, + { id: "seed-176", oklch: [0.300, 0.071, 120.0], + mood: "moss-darkened apothecary jar — herbal, shadowed, mid-19th-century botanical study", + strategy: "Seed is a deep desaturated olive-green that reads as preserved botanical pigment; I anchor it on pure white so the dim moss-green primary feels like ink on a herbarium page, with a warm ochre accent supplying the aged-paper counterpoint." }, + { id: "seed-155", oklch: [0.550, 0.142, 130.0], + mood: "moss-bed forest floor at noon — chlorophyll, lichen, sunlit fern", + strategy: "Seed is a confident mid-olive green with strong chroma; mood is daylight botanical, so I let the brand greens do the work on a pure paper-white bg and pair with a warm umber accent for fern-against-bark contrast." }, + { id: "seed-119", oklch: [0.600, 0.154, 130.0], + mood: "moss garden at Saihō-ji — damp stone, filtered green light through old cedar", + strategy: "Pure near-black bg lets the seed's mossy green glow like wet lichen under low light; accent shifts to a pale ochre-gold like sun catching through canopy." }, + { id: "seed-179", oklch: [0.300, 0.096, 140.0], + mood: "moss on wet stone — forest floor at dusk, deep botanical hush", + strategy: "Kept the seed's deep moss green as primary against a near-black surface so the green reads as living shadow, with a pale lichen accent providing the single point of light." }, + { id: "seed-180", oklch: [0.350, 0.110, 140.0], + mood: "moss-darkened apothecary — herbal tinctures in amber glass, pressed botanicals, the deep green of a conservatory at dusk", + strategy: "Near-black bg with a whisper of green undertone lets the seed's deep moss read as luminous foliage; a warm parchment accent provides the apothecary-label counterpoint without breaking the herbal register." }, + { id: "seed-120", oklch: [0.650, 0.100, 140.0], + mood: "moss on weathered stone — quiet botanical garden conservatory at midday", + strategy: "Pure white bg lets the muted sage-green primary read as a considered botanical mark, with a deeper terracotta accent providing earthen counterpoint without breaking the gallery-like restraint." }, + { id: "seed-121", oklch: [0.750, 0.090, 140.0], + mood: "moss garden at Saihō-ji — diffuse green light filtered through wet stone and lichen", + strategy: "Pure near-black bg lets the muted sage-green primary glow like lichen under low light; a warm pale-bone accent acts as the single ray of sun cutting through canopy." }, + { id: "seed-182", oklch: [0.400, 0.106, 150.0], + mood: "moss garden at Saiho-ji — deep cultivated green under wet stone shadow, contemplative and damp", + strategy: "Near-black bg with the faintest cool-green undertone evokes shaded stone; primary holds the seed's moss tone while accent shifts to a lichen-yellow for organic counterpoint without breaking the hush." }, + { id: "seed-157", oklch: [0.550, 0.145, 150.0], + mood: "moss garden at Saiho-ji — damp stone, filtered green light through cedar canopy", + strategy: "Near-black bg with a faint green undertone evokes deep forest shadow; primary holds the seed's verdant register while accent shifts to a pale lichen-cream to mimic light catching moss." }, + { id: "seed-122", oklch: [0.600, 0.158, 150.0], + mood: "forest floor at first light — moss, lichen, and clean morning air", + strategy: "Seed reads as a living, daylight green; surface stays pure white so the green carries the freshness, with a cool teal accent pulling it toward dew rather than earth." }, + { id: "seed-195", oklch: [0.650, 0.150, 145.0], + mood: "Considered horticulture brand — botanical research lab, the green of a healthy stem photographed in clean daylight", + strategy: "Pure white surface lets the seed's vegetal green carry the entire brand voice, paired with a deep forest ink and a warm clay accent for editorial contrast." }, + { id: "seed-183", oklch: [0.350, 0.077, 160.0], + mood: "moss-stained apothecary — deep forest glass, herbal tinctures shelved in low candlelight", + strategy: "Anchored the seed as primary and built a near-black dark surface with whisper-tinted green to evoke aged apothecary glass, letting the green glow rather than shout." }, + { id: "seed-184", oklch: [0.400, 0.087, 160.0], + mood: "deep forest apothecary — moss, bottle glass, and herbal tincture under afternoon light", + strategy: "Seed becomes a botanical-bottle-green primary on pure white, paired with a warm clove-amber accent to evoke herbal pharmacy contrast without tinting the surface." }, + { id: "seed-158", oklch: [0.550, 0.119, 160.0], + mood: "moss on wet stone — forest floor after rain, mineral and quiet", + strategy: "Pure white surface lets the deep mossy green carry the entire mood; accent shifts to a damp slate-teal to sit beside primary like lichen on stone without competing." }, + { id: "seed-159", oklch: [0.600, 0.130, 160.0], + mood: "moss-covered forest apothecary — herbal tinctures in amber glass, eucalyptus shadow", + strategy: "Anchored the green seed in a near-black backdrop so it reads like botanical glassware lit from within, with a warm amber accent pulled across the wheel to evoke tincture bottles against dark wood." }, + { id: "seed-185", oklch: [0.450, 0.086, 170.0], + mood: "weathered copper patina on a Pacific Northwest greenhouse — oxidized teal, glass light, botanical hush", + strategy: "Seed sits as a deep oxidized-teal primary against pure white so the patina reads as pigment, not atmosphere; a rust-copper accent completes the verdigris/oxidation story across the warm-cool axis." }, + { id: "seed-124", oklch: [0.750, 0.080, 170.0], + mood: "sea-glass on a foggy Pacific shoreline — weathered, mineral, quietly oxidized", + strategy: "Seed is a soft desaturated teal-green; pairing it on pure white lets the mineral primary read as patinated copper-glass, with a deeper kelp-toned primary and a rusted coral accent to spark the muted teal against its complement." }, + { id: "seed-160", oklch: [0.550, 0.095, 180.0], + mood: "weathered copper patina on a museum bronze — oxidized teal, conservatorial quiet", + strategy: "Pure near-black gallery surround lets the patina-teal primary glow like a lit artifact, with a warm verdigris-adjacent accent providing the oxidation contrast against the cool seed." }, + { id: "seed-161", oklch: [0.720, 0.100, 188.0], + mood: "climate-tech dashboard — calm verdigris on plain paper, the quiet confidence of an instrument that just works", + strategy: "Seed teal carries the entire mood as a single considered brand color on pure white, with a desaturated copper accent providing warm signal against the cool primary without competing for attention." }, + { id: "seed-186", oklch: [0.450, 0.074, 200.0], + mood: "deep hydrothermal vent — mineral teal under pressure, the cold blue-green of oxidized copper in submerged light", + strategy: "Near-black surface lets the mineral teal glow as if lit from within; accent shifts toward verdigris-copper to suggest patina on submerged metal, while ink stays cool-neutral to keep the register austere rather than aquatic-cute." }, + { id: "seed-125", oklch: [0.650, 0.100, 200.0], + mood: "climate-tech dashboard — calm operational teal, the color of clean water data and atmospheric sensors", + strategy: "Pure white surface lets a single muted-teal primary do all the brand work, with a deeper marine accent providing hierarchy without competing chroma." }, + { id: "seed-126", oklch: [0.750, 0.080, 200.0], + mood: "climate-tech product brand — quiet competence, dashboards for hard infrastructure problems", + strategy: "Hold the seed's muted teal as primary, pair with a sharper cyan-leaning accent for interactive lift, and let a pure white surface do the disappearing act so the brand reads as a tool, not an atmosphere." }, + { id: "seed-162", oklch: [0.550, 0.091, 210.0], + mood: "weathered nautical instrument — patinated brass on oxidized steel, the cool blue-grey of a ship's chronometer at dawn", + strategy: "Pure white surface lets the muted teal-steel primary read as a precise instrument mark, with a warm brass accent providing the single point of patina against clinical white." }, + { id: "seed-163", oklch: [0.450, 0.086, 230.0], + mood: "deep harbor at dusk — weathered nautical instruments, brass dials on oxidized steel", + strategy: "Near-black background with subtle cool tint evokes the marine dusk; primary holds the seed's teal-blue while a warm brass accent creates the instrument-on-steel tension." }, + { id: "seed-164", oklch: [0.550, 0.105, 230.0], + mood: "deep harbor at dawn — cold steel water, fog-muted light, the quiet before the boats leave", + strategy: "Pure near-black bg lets the seed's cold marine blue read as a luminous beacon, while a pale frost-cyan accent evokes diffused dawn light cutting through fog." }, + { id: "seed-127", oklch: [0.650, 0.100, 230.0], + mood: "climate-tech dashboard — atmospheric sensor blue, calm operational clarity", + strategy: "Anchor the seed as a confident mid-blue primary on pure white so the brand color carries all the atmospheric feeling, with a deep navy accent for hierarchy and a soft slate muted for body text." }, + { id: "seed-128", oklch: [0.750, 0.080, 230.0], + mood: "climate-tech dashboard — calm atmospheric data, considered sky-blue", + strategy: "Pure white surface lets the muted sky-blue primary carry the meteorological calm, with a deep-navy accent providing readable weight against the soft primary." }, + { id: "seed-187", oklch: [0.350, 0.078, 240.0], + mood: "deep harbor at blue hour — wet stone, cold steel, the quiet before night fully lands", + strategy: "Near-black architectural bg with a hint of marine chroma lets the seed read as ambient atmosphere rather than UI chrome; a cooler steel accent sits opposite the warmer-shifted primary for navigational clarity." }, + { id: "seed-077", oklch: [0.578, 0.130, 241.7], + mood: "pre-dawn signal tower — cold blue solitude, instruments glowing against the dark", + strategy: "Pure near-black bg lets the seed's cold tower-light blue glow as the sole emotional source, with a frost-cyan accent acting as a secondary indicator light." }, + { id: "seed-188", oklch: [0.400, 0.110, 250.0], + mood: "Linear's considered indigo — the calm authority of a well-built developer tool, blueprint ink on a clean page", + strategy: "Held the seed as a deep indigo primary against pure white so the brand color carries all the gravity; accent shifts to a cooler, brighter cyan-blue to create a crisp hierarchy pair without warming the surface." }, + { id: "seed-165", oklch: [0.450, 0.123, 250.0], + mood: "blueprint room at dusk — drafting table, graphite, civic-engineering blue", + strategy: "Seed is a mid-deep architectural blue with real chroma and no environmental cue, so I stay out of the way with a pure white surface and let the primary do all the talking, pairing it with a burnt-ochre accent for drafting-pencil contrast." }, + { id: "seed-079", oklch: [0.478, 0.136, 251.8], + mood: "twilight cartography — the blue of deep dusk over open water, precise and navigational", + strategy: "Pure white surface lets the seed's oceanic blue act as a single navigational anchor, with a warm amber accent struck across it like a lighthouse beam at dusk." }, + { id: "seed-080", oklch: [0.541, 0.122, 248.2], + mood: "Linear-style considered tool blue — the calm, exact register of a modern engineering app where every pixel is intentional", + strategy: "Pure white surface lets the considered indigo-blue primary carry the entire brand; a deeper navy accent provides hierarchy without warmth, keeping the palette in a single cool family for that focused-software feel" }, + { id: "seed-166", oklch: [0.550, 0.149, 250.0], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and quietly intense", + strategy: "Near-black bg with the faintest cool tint reads like a darkened cockpit; the seed becomes a luminous instrument-blue primary, paired with a warm amber accent that mimics avionics readouts for unmistakable signal contrast." }, + { id: "seed-081", oklch: [0.650, 0.160, 250.0], + mood: "deep-sea research vessel at dawn — instrument glow against cold steel light", + strategy: "Pure near-white bg keeps the palette technical and instrument-like; the seed blue holds as primary while a desaturated steel-cyan accent reads like signal readouts on glass." }, + { id: "seed-082", oklch: [0.742, 0.140, 247.4], + mood: "high-altitude flight deck at dawn — cold cabin instruments glowing against a sky still holding night", + strategy: "Near-black cockpit ground with a faint blue cast lets the seed read as an illuminated instrument; primary holds the seed, accent shifts to cyan for signal/indicator contrast." }, + { id: "seed-210", oklch: [0.360, 0.140, 260.0], + mood: "Linear-style considered tool indigo — late-night focused work, the deep blue of a code editor at 2am where everything else falls away", + strategy: "Pure black bg lets the indigo primary carry all the cognitive-focus weight, with a slightly brighter periwinkle accent for interactive lift — the surface disappears so the tool feels weightless." }, + { id: "seed-189", oklch: [0.400, 0.130, 260.0], + mood: "pre-dawn observatory — cold instrument blue, star-chart precision", + strategy: "Seed becomes the primary on pure black so the deep instrument-blue glows like a calibration light, with a faint cyan accent reading as starlight against the void." }, + { id: "seed-211", oklch: [0.420, 0.161, 260.0], + mood: "Linear's considered indigo — the tool-for-thought blue of focused product work, calm authority without coldness", + strategy: "Hold the seed as a deep indigo primary against pure white, then pair with a slightly warmer, lighter periwinkle accent to create gentle hue separation without breaking the disciplined tool-brand register." }, + { id: "seed-129", oklch: [0.450, 0.150, 260.0], + mood: "pre-dawn observatory — deep cobalt sky just before astronomical twilight, instruments cool to the touch", + strategy: "Near-black surface lets the cobalt seed read as luminous starlight; a single warm amber accent acts as the calibration lamp against the cold blue field." }, + { id: "seed-084", oklch: [0.476, 0.207, 261.2], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and awake", + strategy: "Default B black bg lets the cobalt primary read as a luminous instrument signal, with a cyan accent striking the analogous 'cockpit display' relationship." }, + { id: "seed-085", oklch: [0.681, 0.132, 258.4], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky", + strategy: "Anchored the seed as a luminous primary against a near-black architectural ground, with a warm amber accent acting as the single instrument light cutting through cold blue." }, + { id: "seed-086", oklch: [0.767, 0.106, 255.9], + mood: "Scandinavian winter morning — quiet light through frost, pale sky over snow", + strategy: "Anchored a pure white editorial stage so the seed's cool sky-blue reads as crisp polar light, with a deeper navy primary providing the only saturated weight — like a single dark pine against snow." }, + { id: "seed-083", oklch: [0.340, 0.159, 262.4], + mood: "deep cobalt twilight — the moment after sunset when the sky goes electric blue and city windows start to glow", + strategy: "Pure black stage lets the cobalt seed act as a luminous neon-window glow, with a warm amber accent across the wheel for the lit-window contrast." }, + { id: "seed-212", oklch: [0.360, 0.219, 270.0], + mood: "Linear-grade tooling indigo — considered software for people who care about craft", + strategy: "Anchored the deep indigo seed as primary on a pure white surface so the brand color carries all the weight, with a slightly cooler violet-blue accent for hierarchy without competing chroma." }, + { id: "seed-130", oklch: [0.400, 0.150, 270.0], + mood: "Linear-grade indigo — considered productivity tool, ink on paper, no theatrics", + strategy: "Pure white surface lets a deep cool indigo carry all the brand weight, paired with a slightly warmer violet-blue accent for hierarchy without acid." }, + { id: "seed-213", oklch: [0.411, 0.241, 267.9], + mood: "Linear-style indigo — considered tool surface, the kind of blue-violet that sits behind a developer's keyboard at 11pm without shouting", + strategy: "Pure black canvas lets a saturated indigo primary do all the brand work, with a cooler cyan-violet accent providing UI signal without competing." }, + { id: "seed-131", oklch: [0.450, 0.180, 270.0], + mood: "monastic indigo dusk — vespers light through stained glass, contemplative and severe", + strategy: "Seed becomes a deep indigo primary against pure near-black so the violet reads as luminous stained-glass against architectural shadow, with a cooler iris accent for tonal lift." }, + { id: "seed-088", oklch: [0.476, 0.158, 268.5], + mood: "pre-dawn astronomer's notebook — deep indigo sky just before the stars fade, ink and graphite", + strategy: "Near-black bg with the faintest cool tint to evoke night sky without theatrics; primary holds the seed's indigo, accent shifts to a paler periwinkle for stellar contrast, keeping the palette monochromatic-cool and observational." }, + { id: "seed-196", oklch: [0.530, 0.130, 268.0], + mood: "Linear-style considered tool indigo — the deep-focus blue-violet of a thoughtfully built productivity surface, the color of a well-typeset keyboard shortcut", + strategy: "Pure white bg lets the indigo seed do all the brand work as primary, with a slightly darker, more saturated violet-shifted accent for hierarchy and interactive states — the surface disappears so the brand color reads as the entire identity." }, + { id: "seed-132", oklch: [0.700, 0.120, 270.0], + mood: "Linear-style considered tool indigo — the quiet violet of a focused product workspace, late-afternoon thinking", + strategy: "Pure white surface lets a muted indigo-violet primary and a slightly cooler accent do all the brand work, keeping the register calm and software-like rather than theatrical." }, + { id: "seed-090", oklch: [0.445, 0.206, 279.1], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, not a nightclub", + strategy: "Anchor the seed as a confident product primary on pure white, with a cooler indigo-shift accent that reads as a sibling tool color, so the brand violet does all the emotional work." }, + { id: "seed-133", oklch: [0.500, 0.160, 280.0], + mood: "Linear-adjacent indigo — considered productivity tool, the violet of a thinking workspace", + strategy: "Seed becomes a measured indigo primary on pure white; accent shifts to a cooler blue-violet to create hierarchy without nightclub saturation, letting the brand color do all the emotional work." }, + { id: "seed-094", oklch: [0.533, 0.125, 294.3], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, calm authority for a creative workspace", + strategy: "Pure white canvas lets the indigo-violet primary carry the entire brand voice; accent shifts hue slightly toward blue for a cool, tool-like duotone rather than warm decorative pairing." }, + { id: "seed-137", oklch: [0.700, 0.120, 290.0], + mood: "Linear-adjacent indigo — the considered tool, late-evening focus mode, software made for people who care about craft", + strategy: "Pure black surface lets a single restrained indigo-violet carry the brand, with a cooler periwinkle accent providing UI hierarchy without competing — Vercel/Linear dark-mode discipline." }, + { id: "seed-100", oklch: [0.450, 0.150, 330.0], + mood: "velvet boudoir at last call — bruised orchid and lipstick traces under low lamplight", + strategy: "Pure near-black surface lets a deep magenta-rose primary smolder while a warm peach accent acts like skin-lit lamplight — drama lives in the brand pair, not the room." }, + { id: "seed-103", oklch: [0.650, 0.160, 330.0], + mood: "1980s Memphis boudoir — powder-pink neon humming against lacquered black, lipstick and lacquer", + strategy: "Near-black gallery surface lets the magenta-pink seed read as lit neon; accent shifts to warm coral to create cinematic dichromatic tension without competing chroma." }, + { id: "seed-228", oklch: [0.360, 0.147, 340.0], + mood: "Figma-era creative tool plum — considered productivity software for designers, the inky violet of a serif wordmark on a marketing site", + strategy: "Held the seed as a deep plum primary against pure white so the brand color does the emotional work; paired with a muted rose accent for warmth without breaking the productivity-tool restraint." }, + { id: "seed-107", oklch: [0.500, 0.200, 340.0], + mood: "Figma plum — creative-tool confidence, considered magenta for a modern design product", + strategy: "Pure white surface lets a saturated magenta-plum primary carry all the brand voice, paired with a cooler violet-leaning accent for hierarchy without competing." }, + { id: "seed-198", oklch: [0.600, 0.210, 340.0], + mood: "Figma-era creative tool plum — confident, considered, made for makers", + strategy: "Anchor a saturated plum primary against pure white so the brand color does all the emotional work, with a deeper magenta-rose accent for hierarchy." }, + { id: "seed-112", oklch: [0.754, 0.193, 343.4], + mood: "Figma-era creative tool — confident pink primary doing the brand work on a clean canvas, the way Linear uses indigo or Stripe uses violet", + strategy: "Anchor the seed pink as a saturated brand primary on pure white so the color carries all the personality; pair with a cooler plum accent to give the pink something to push against without competing." }, + { id: "seed-229", oklch: [0.420, 0.163, 350.0], + mood: "considered fintech rose — the deep magenta of a modern product brand (think Stripe-adjacent, but rotated toward berry), confident and current", + strategy: "pure white surface lets a single deep berry-rose primary do all the brand work, paired with a cooler indigo accent for the contrast move you see in modern product marketing" }, + { id: "seed-113", oklch: [0.470, 0.173, 354.8], + mood: "1960s velvet rope nightclub — crushed magenta, low light, cigarette smoke catching a spotlight", + strategy: "Pure black stage so the seed's smoky magenta reads as a single hot spotlight, paired with a cooler violet accent for the second light cue." }, + { id: "seed-114", oklch: [0.570, 0.158, 353.3], + mood: "fin-de-siècle Parisian rose — velvet curtain, theatre program, lipstick blotted on linen", + strategy: "Drop bg to true black so the dusty-rose primary reads as stage-lit silk; accent shifts to a warmer coral-mauve at higher lightness to create gentle hue rotation without breaking the romance." }, + { id: "seed-199", oklch: [0.650, 0.180, 350.0], + mood: "modern fintech rose — the considered pink of a Series B brand mark, confident and current without nostalgia", + strategy: "Pure white surface lets a saturated rose primary do the brand work, paired with a deep plum accent for hierarchy — the Stripe move applied to a pink hue." }, + { id: "seed-115", oklch: [0.636, 0.218, 355.3], + mood: "backstage at a cabaret — velvet rope, lipstick mark on a champagne glass", + strategy: "Seed reads as a saturated stage-light magenta-red; I push it into pure black so the primary glows like a neon sign and the accent (a cold pearl-pink) acts as the spotlight rim — the room is dark, the color does the singing." }, + { id: "seed-230", oklch: [0.650, 0.249, 354.5], + mood: "Modern fintech rose — the considered pink of a contemporary payments brand: confident, alive, and clear-headed", + strategy: "Pure white bg lets a saturated rose-magenta primary carry all the brand energy, paired with a cooler indigo accent for trustworthy contrast — the Stripe move applied to a pink hue." }, + { id: "seed-231", oklch: [0.682, 0.241, 353.2], + mood: "Figma-era creative tool — a confident pink-magenta product brand, the kind a modern design platform uses to feel alive without shouting", + strategy: "Default A pure white bg lets the saturated pink-magenta primary do all the brand work, with a near-complementary cool teal accent for tool-like clarity and a neutral ink for editorial calm" }, + { id: "seed-116", oklch: [0.734, 0.183, 356.8], + mood: "modern beauty brand DTC — Glossier-adjacent pink, confident and current without being saccharine", + 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." }, +]; + +function parseArgs(argv) { + const args = { id: null, from: null }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === '--id' && argv[i + 1]) { args.id = argv[++i]; } + else if (a === '--from' && argv[i + 1]) { args.from = argv[++i]; } + } + return args; +} + +// Hash a key into a stable float in [0, 1) for deterministic weighted picks. +function hashUnit(key) { + const h = crypto.createHash('sha256').update(key).digest(); + return h.readUInt32BE(0) / 0x100000000; +} + +// The curated library is hue-skewed (more reds/oranges than teals/magentas) +// because that's where the source material + taste landed. Left uniform, a +// random pick would land on red ~1/3 of the time. Inverse-frequency weighting +// gives each seed a weight of 1/(count in its 30° hue bucket), so each hue +// ZONE is roughly equally likely to be chosen regardless of how many seeds it +// holds — fair rainbow exposure across runs without pruning the library. +function buildWeights(seeds) { + const bucketCount = {}; + const bucketOf = (s) => Math.floor(((s.oklch[2] % 360) + 360) % 360 / 30); + for (const s of seeds) { const b = bucketOf(s); bucketCount[b] = (bucketCount[b] || 0) + 1; } + const weights = seeds.map((s) => 1 / bucketCount[bucketOf(s)]); + const total = weights.reduce((a, b) => a + b, 0); + return { weights, total }; +} + +function weightedPick(seeds, unit) { + const { weights, total } = buildWeights(seeds); + let target = unit * total; + for (let i = 0; i < seeds.length; i++) { + target -= weights[i]; + if (target < 0) return seeds[i]; + } + return seeds[seeds.length - 1]; +} + +function pickSeed(seeds, { id, from }) { + if (id) { + const found = seeds.find(s => s.id === id); + if (!found) { console.error(`no seed with id "${id}"`); process.exit(2); } + return found; + } + const envFrom = process.env.IMPECCABLE_PALETTE_SEED; + const key = from || envFrom; + const unit = key ? hashUnit(key) : Math.random(); + return weightedPick(seeds, unit); +} + +function fmtOklch([L, C, H]) { + return `oklch(${L.toFixed(3)} ${C.toFixed(3)} ${H.toFixed(1)})`; +} + +function hueWord(H) { + if (H < 15 || H >= 345) return 'pure red'; + if (H < 35) return 'warm red / crimson'; + if (H < 55) return 'warm coral / burnt orange'; + if (H < 80) return 'orange / honey'; + if (H < 105) return 'warm amber / honey-gold'; + if (H < 135) return 'yellow-green / olive'; + if (H < 170) return 'green'; + if (H < 200) return 'teal'; + if (H < 230) return 'sky blue'; + if (H < 265) return 'cobalt / indigo'; + if (H < 295) return 'violet / purple'; + if (H < 330) return 'magenta / pink'; + return 'deep pink / rose'; +} + +// --------------------------------------------------------------- + +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}` : ''; + +// --------------------------------------------------------------- +// Fat tool-exit response — what the model sees on stdout. +// --------------------------------------------------------------- + +process.stdout.write(`BRAND SEED · ${seed.id} + +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. + +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. + +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é. + + 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 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. + + 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. + +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): + + - 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. + +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. + +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. +`); diff --git a/.claude/skills/impeccable/scripts/pin.mjs b/.claude/skills/impeccable/scripts/pin.mjs index ba02783fb..320d98ce1 100644 --- a/.claude/skills/impeccable/scripts/pin.mjs +++ b/.claude/skills/impeccable/scripts/pin.mjs @@ -27,7 +27,7 @@ const HARNESS_DIRS = [ // Valid sub-command names const VALID_COMMANDS = [ - 'craft', 'teach', 'extract', 'document', 'shape', + 'craft', 'init', 'extract', 'document', 'shape', 'critique', 'audit', 'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live', 'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive', diff --git a/.cursor/skills/impeccable/SKILL.md b/.cursor/skills/impeccable/SKILL.md index 609d47155..b4aaa698e 100644 --- a/.cursor/skills/impeccable/SKILL.md +++ b/.cursor/skills/impeccable/SKILL.md @@ -1,93 +1,88 @@ --- name: impeccable description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks. -version: 3.1.1 -license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution. +version: 3.5.0 +license: Apache 2.0 --- Designs and iterates production-grade frontend interfaces. Real working code, committed design choices, exceptional craft. ## Setup -Before any design work or file edits: +You MUST do these steps before proceeding: -1. Load context (PRODUCT.md / DESIGN.md) via the loader script. -2. Identify the register and load the matching register reference (brand.md or product.md). -3. **If the user invoked a sub-command (e.g. `craft`, `shape`, `audit`), load its reference file too.** This is non-negotiable: `craft` without `craft.md` loaded means you'll skip the shape-and-confirm step the user expects. +1. Run `node .cursor/skills/impeccable/scripts/context.mjs` once per session. If you've already seen its output in this conversation, do not re-run it. The script either prints the project's PRODUCT.md (and DESIGN.md when present) as a markdown block, or tells you it's missing. Follow whatever it prints. **If it reports `NO_PRODUCT_MD`, stop and follow `reference/init.md` before doing anything else.** If the output ends with an `UPDATE_AVAILABLE` directive, follow it (ask the user once about updating, then continue). It never blocks the current task. +2. If the user invoked a sub-command (`craft`, `shape`, `audit`, `polish`, ...), you MUST read `reference/.md` next. Non-optional. The reference defines the command's flow; without it you will skip steps the user expects. +3. Familiarize yourself with any existing design system, conventions, and components in the code. Read at least one project file (CSS / tokens / theme / a representative component or page). **Required even when you've loaded a sub-command reference in step 2.** Don't reinvent the wheel; use what's there when it works, branch out when the UX wins. +4. Read the matching register reference. **This is non-optional; skipping it produces generic output.** If the project is marketing, a landing page, a campaign, long-form content, or a portfolio (design IS the product), read `reference/brand.md`. If it is app UI, admin, a dashboard, or a tool (design SERVES the product), read `reference/product.md`. Pick by first match: (1) task cue ("landing page" vs "dashboard"); (2) surface in focus (the page, file, or route being worked on); (3) `register` field in PRODUCT.md. +5. **If the project is brand-new (no existing CSS tokens / theme / committed brand colors found in step 3)**, run `node .cursor/skills/impeccable/scripts/palette.mjs` to receive a brand seed color and composition guidance. This is the anchor for your primary brand color. Compose the rest of the palette (bg, surface, ink, accent, muted) around it per the script's instructions. Use OKLCH throughout. **Skip this step only if step 3 found committed brand colors in existing tokens; in that case identity-preservation wins.** -Skipping these produces generic output that ignores the project. +## Design guidance -### 1. Context gathering +Produce ready-to-ship, production-grade code, not prototypes or starting points. Take no shortcuts unless the user asks for them (when in doubt, ask). Don't stop until arriving at a complete implementation (beautiful, responsive, fast, precise, bug-free, on brand). You take attention to detail seriously: every page, section or component crafted is battle tested using the tools available to you (browser screenshotting, computer use, etc). the model is capable of extraordinary work. Don't hold back. -Two files, case-insensitive. The loader looks at the project root by default and falls back to `.agents/context/` and `docs/` if the root is clean. Override with `IMPECCABLE_CONTEXT_DIR=path/to/dir` (absolute or relative to cwd). +### General rules -- **PRODUCT.md**: required. Users, brand, tone, anti-references, strategic principles. -- **DESIGN.md**: optional, strongly recommended. Colors, typography, elevation, components. +#### Color -Load both in one call: +- **Verify contrast.** Body text must hit ≥4.5:1 against its background; large text (≥18px or bold ≥14px) needs ≥3:1. Placeholder text needs the same 4.5:1, not the muted-gray default. The most common failure: muted gray body text on a tinted near-white. If the contrast is even close, bump the body color toward the ink end of the ramp; light gray "for elegance" is the single biggest reason AI designs feel hard to read. +- Gray text on a colored background looks washed out. Use a darker shade of the background's own hue, or a transparency of the text color. -```bash -node .cursor/skills/impeccable/scripts/load-context.mjs -``` +#### Typography -Consume the full JSON output. Never pipe through `head`, `tail`, `grep`, or `jq`. The output's `contextDir` field tells you where the files were resolved from. +- Cap body line length at 65–75ch. +- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales. +- Cap font-family count at 3 (display + body + optional mono). More than 3 reads as indecision, not richness. One well-tuned family with weight contrast usually beats three competing typefaces. +- Don't pair fonts that are similar but not identical (two geometric sans-serifs, two humanist sans-serifs). Pair on a contrast axis (serif + sans, geometric + humanist) or use one family in multiple weights. +- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. +- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. +- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". +- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. -If the output is already in this session's conversation history, don't re-run. Exceptions requiring a fresh load: you just ran `/impeccable teach` or `/impeccable document` (they rewrite the files), or the user manually edited one. +#### Layout -`/impeccable live` already warms context via `live.mjs`. If you've run `live.mjs`, don't also run `load-context.mjs` this session. +- Vary spacing for rhythm. +- Cards are the lazy answer. Use them only when they're truly the best affordance. Nested cards are always wrong. +- Flexbox for 1D, Grid for 2D. Don't default to Grid when `flex-wrap` would be simpler. +- For responsive grids without breakpoints: `repeat(auto-fit, minmax(280px, 1fr))`. +- Build a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip). Never arbitrary values like 999 or 9999. -If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `/impeccable teach`, then resume the user's original task with the fresh context. If the original task was `/impeccable craft`, resume into `/impeccable shape` before any implementation work. +#### Motion +- Motion should be intentional, and not be an afterthought. consider it as part of the build. +- Don't animate CSS layout properties unless truly needed. +- Ease out with exponential curves (ease-out-quart / quint / expo). No bounce, no elastic. +- Use libraries for more advanced motion needs (e.g. motion, gsap, anime.js, lenis etc) +- Reduced motion is not optional. Every animation needs a `@media (prefers-reduced-motion: reduce)` alternative: typically a crossfade or instant transition. +- Staggering the items within one list is legitimate. The tell is the uniform reflex (one identical entrance applied to every section), not motion itself; each reveal should fit what it reveals. Suppressing the reflex is never a reason to ship a page with no motion at all. +- Reveal animations must enhance an already-visible default. Don't gate content visibility on a class-triggered transition; transitions pause on hidden tabs and headless renderers, so the reveal never fires and the section ships blank. +- Premium motion materials are not just transform/opacity. Blur, backdrop-filter, clip-path, mask, and shadow/glow are part of the palette when they materially improve the effect and stay smooth. -If DESIGN.md is missing: nudge once per session (*"Run `/impeccable document` for more on-brand output"*), then proceed. +#### Interaction -### 2. Register +- Dropdowns rendered with `position: absolute` inside an `overflow: hidden` or `overflow: auto` container will be clipped. Use the native `` / popover API, `position: fixed`, or a portal to escape the stacking context. -Every design task is **brand** (marketing, landing, campaign, long-form content, portfolio: design IS the product) or **product** (app UI, admin, dashboard, tool: design SERVES the product). +### Copy -Identify before designing. Priority: (1) cue in the task itself ("landing page" vs "dashboard"); (2) the surface in focus (the page, file, or route being worked on); (3) `register` field in PRODUCT.md. First match wins. +- Every word earns its place. No restated headings, no intros that repeat the title. +- **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`. +- **No aphoristic-cadence body copy as a default voice.** Don't fall into the rhythm of "serious statement, then punchy short negation" as the page's recurring voice. If three or more section copy blocks on the page land on a short rebuttal-shaped sentence, rewrite. Specific, not aphoristic. +- **No marketing buzzwords.** The streamline / empower / supercharge / leverage / unleash / transform / seamless / world-class / enterprise-grade / next-generation / cutting-edge / game-changer / mission-critical family of phrases. Pick a specific noun and a verb that describes what the product literally does. +- Button labels: verb + object. "Save changes" beats "OK"; "Delete project" beats "Yes". The label should say what will happen. +- Link text needs standalone meaning. "View pricing plans" beats "Click here"; screen readers announce links out of context. -If PRODUCT.md lacks the `register` field (legacy), infer it once from its "Users" and "Product Purpose" sections, then cache the inferred value for the session. Suggest the user run `/impeccable teach` to add the field explicitly. +### New projects only (when no prior work exists) -Load the matching reference: [reference/brand.md](reference/brand.md) or [reference/product.md](reference/product.md). The shared design laws below apply to both. +#### Color & Theme -## Shared design laws - -Apply to every design, both registers. Match implementation complexity to the aesthetic vision: maximalism needs elaborate code, minimalism needs precision. Interpret creatively. Vary across projects; never converge on the same choices. the model is capable of extraordinary work. Don't hold back. - -### Color - -- Use OKLCH. Reduce chroma as lightness approaches 0 or 100; high chroma at extremes looks garish. -- Never use `#000` or `#fff`. Tint every neutral toward the brand hue (chroma 0.005–0.01 is enough). +- Use OKLCH. +- **The cream / sand / beige body bg is the saturated AI default of 2026.** The whole warm-neutral band (OKLCH L 0.84-0.97, C < 0.06, hue 40-100) reads as cream/sand/paper/parchment regardless of what you call it. Token names like `--paper`, `--cream`, `--sand`, `--bone`, `--flour`, `--linen`, `--parchment`, `--wheat`, `--biscuit`, `--ivory` are tells in themselves. If the brief is "warm, traditional, family-coastal-Italian" or "magazine-warm" or "editorial-restraint", DO NOT translate that into a near-white warm-tinted bg; that's the AI move. Pick: (a) a saturated brand color as the body (terracotta, oxblood, deep ochre, near-black), (b) a true off-white at chroma 0 (or chroma toward the brand's own hue, not toward warmth-by-default), or (c) a darker mid-tone tinted neutral that's clearly the brand's own. "Warmth" in the brand is carried by accent + typography + imagery, not by body bg. +- Tinted neutrals: add 0.005–0.015 chroma toward the brand's hue. Don't default-tint toward warm or cool "because the brand feels that way"; that's the cross-project monoculture move. +- When picking a theme: Dark vs. light is never a default. Not dark "because tools look cool dark." Not light "to be safe.".Before choosing, write one sentence of physical scene: who uses this, where, under what ambient light, in what mood. If the sentence doesn't force the answer, it's not concrete enough. Add detail until it does. - Pick a **color strategy** before picking colors. Four steps on the commitment axis: - **Restrained**: tinted neutrals + one accent ≤10%. Product default; brand minimalism. - **Committed**: one saturated color carries 30–60% of the surface. Brand default for identity-driven pages. - **Full palette**: 3–4 named roles, each used deliberately. Brand campaigns; product data viz. - **Drenched**: the surface IS the color. Brand heroes, campaign pages. -- The "one accent ≤10%" rule is Restrained only. Committed / Full palette / Drenched exceed it on purpose. Don't collapse every design to Restrained by reflex. - -### Theme - -Dark vs. light is never a default. Not dark "because tools look cool dark." Not light "to be safe." - -Before choosing, write one sentence of physical scene: who uses this, where, under what ambient light, in what mood. If the sentence doesn't force the answer, it's not concrete enough. Add detail until it does. - -"Observability dashboard" does not force an answer. "SRE glancing at incident severity on a 27-inch monitor at 2am in a dim room" does. Run the sentence, not the category. - -### Typography - -- Cap body line length at 65–75ch. -- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales. - -### Layout - -- Vary spacing for rhythm. Same padding everywhere is monotony. -- Cards are the lazy answer. Use them only when they're truly the best affordance. Nested cards are always wrong. -- Don't wrap everything in a container. Most things don't need one. - -### Motion - -- Don't animate CSS layout properties. -- Ease out with exponential curves (ease-out-quart / quint / expo). No bounce, no elastic. ### Absolute bans @@ -98,12 +93,9 @@ Match-and-refuse. If you're about to write any of these, rewrite the element wit - **Glassmorphism as default.** Blurs and glass cards used decoratively. Rare and purposeful, or nothing. - **The hero-metric template.** Big number, small label, supporting stats, gradient accent. SaaS cliché. - **Identical card grids.** Same-sized cards with icon + heading + text, repeated endlessly. -- **Modal as first thought.** Modals are usually laziness. Exhaust inline / progressive alternatives first. - -### Copy - -- Every word earns its place. No restated headings, no intros that repeat the title. -- **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`. +- **Tiny uppercase tracked eyebrow above every section.** The 2023-era kicker (small all-caps text with wide tracking, "ABOUT" "PROCESS" "PRICING" above each heading) is now the saturated AI scaffold; it appears on 55-95% of generations regardless of brief, which is the definition of a tell. One named kicker as a deliberate brand system is voice; an eyebrow on every section is AI grammar. Choose a different cadence. +- **Numbered section markers as default scaffolding (01 / 02 / 03).** Putting `01 · About / 02 · Process / 03 · Pricing` above every section is the eyebrow trope one tier deeper: reach for it because "landing pages do this" and you're scaffolding by reflex. Numbers earn their place when the section actually IS a sequence (a real 3-step process, an ordered flow, a typed timeline) and the order carries information the reader needs. One deliberate numbered sequence on one page is voice; numbered eyebrows on every section across the site is AI grammar. +- **Text that overflows its container.** Long heading words plus large clamp scales plus narrow grids cause headline overflow on tablet/mobile. Test the heading copy at every breakpoint; if it overflows, reduce the clamp max or rewrite the copy. The viewport is part of the design. ### The AI slop test @@ -111,7 +103,7 @@ If someone could look at this interface and say "AI made that" without doubt, it **Category-reflex check.** Run at two altitudes; the second one catches what the first one misses. -- **First-order:** if someone could guess the theme + palette from the category alone ("observability → dark blue", "healthcare → white + teal", "finance → navy + gold", "crypto → neon on black"), it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain. +- **First-order:** if someone could guess the theme + palette from the category alone, it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain. - **Second-order:** if someone could guess the aesthetic family from category-plus-anti-references ("AI workflow tool that's not SaaS-cream → editorial-typographic", "fintech that's not navy-and-gold → terminal-native dark mode"), it's the trap one tier deeper. The first reflex was avoided; the second wasn't. Rework until both answers are not obvious. The brand register's [reflex-reject aesthetic lanes](reference/brand.md) list catches the currently-saturated families. ## Commands @@ -120,7 +112,7 @@ If someone could look at this interface and say "AI made that" without doubt, it |---|---|---|---| | `craft [feature]` | Build | Shape, then build a feature end-to-end | [reference/craft.md](reference/craft.md) | | `shape [feature]` | Build | Plan UX/UI before writing code | [reference/shape.md](reference/shape.md) | -| `teach` | Build | Set up PRODUCT.md and DESIGN.md context | [reference/teach.md](reference/teach.md) | +| `init` | Build | Set up project context: PRODUCT.md, DESIGN.md, live config, next steps | [reference/init.md](reference/init.md) | | `document` | Build | Generate DESIGN.md from existing project code | [reference/document.md](reference/document.md) | | `extract [target]` | Build | Pull reusable tokens and components into design system | [reference/extract.md](reference/extract.md) | | `critique [target]` | Evaluate | UX design review with heuristic scoring | [reference/critique.md](reference/critique.md) | @@ -148,11 +140,14 @@ Plus two management commands: `pin ` and `unpin `, detailed be 1. **No argument**: render the table above as the user-facing command menu, grouped by category. Ask what they'd like to do. 2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. -3. **First word doesn't match**: general design invocation. Apply the setup steps, shared design laws, and the loaded register reference, using the full argument as context. +3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. +4. **No clear command match**: general design invocation. Apply the setup steps, the General rules, and the loaded register reference, using the full argument as context. Setup (context gathering, register) is already loaded by then; sub-commands don't re-invoke `/impeccable`. -If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `teach` as a blocker, finish teach, refresh context, then resume the original command and target. +If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `init` as a blocker, finish init, refresh context, then resume the original command and target. + +`teach` is a deprecated alias for `init`: if the user types it, load [reference/init.md](reference/init.md) and proceed as if they ran `init`. ## Pin / Unpin diff --git a/.cursor/skills/impeccable/reference/adapt.md b/.cursor/skills/impeccable/reference/adapt.md index 9d0673ea2..5af7606cb 100644 --- a/.cursor/skills/impeccable/reference/adapt.md +++ b/.cursor/skills/impeccable/reference/adapt.md @@ -188,3 +188,124 @@ Test thoroughly across contexts: - **Slow connections**: Test on throttled network When the adaptation feels native to each context, hand off to `/impeccable polish` for the final pass. + +--- + +## Reference Material + +The sections below were previously `responsive-design.md` and live inline now so the adapt flow has its deep responsive reference in one place. + +### Responsive Design + +#### Mobile-First: Write It Right + +Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first. + +#### Breakpoints: Content-Driven + +Don't chase device sizes; let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints. + +#### Detect Input Method, Not Just Screen Size + +**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard. Use pointer and hover queries: + +```css +/* Fine pointer (mouse, trackpad) */ +@media (pointer: fine) { + .button { padding: 8px 16px; } +} + +/* Coarse pointer (touch, stylus) */ +@media (pointer: coarse) { + .button { padding: 12px 20px; } /* Larger touch target */ +} + +/* Device supports hover */ +@media (hover: hover) { + .card:hover { transform: translateY(-2px); } +} + +/* Device doesn't support hover (touch) */ +@media (hover: none) { + .card { /* No hover state - use active instead */ } +} +``` + +**Critical**: Don't rely on hover for functionality. Touch users can't hover. + +#### Safe Areas: Handle the Notch + +Modern phones have notches, rounded corners, and home indicators. Use `env()`: + +```css +body { + padding-top: env(safe-area-inset-top); + padding-bottom: env(safe-area-inset-bottom); + padding-left: env(safe-area-inset-left); + padding-right: env(safe-area-inset-right); +} + +/* With fallback */ +.footer { + padding-bottom: max(1rem, env(safe-area-inset-bottom)); +} +``` + +**Enable viewport-fit** in your meta tag: +```html + +``` + +#### Responsive Images: Get It Right + +##### srcset with Width Descriptors + +```html +Hero image +``` + +**How it works**: +- `srcset` lists available images with their actual widths (`w` descriptors) +- `sizes` tells the browser how wide the image will display +- Browser picks the best file based on viewport width AND device pixel ratio + +##### Picture Element for Art Direction + +When you need different crops/compositions (not just resolutions): + +```html + + + + ... + +``` + +#### Layout Adaptation Patterns + +**Navigation**: Three stages: hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `
/` for content that can collapse on mobile. + +#### Testing: Don't Trust DevTools Alone + +DevTools device emulation is useful for layout but misses: + +- Actual touch interactions +- Real CPU/memory constraints +- Network latency patterns +- Font rendering differences +- Browser chrome/keyboard appearances + +**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators. + +--- + +**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful. diff --git a/.cursor/skills/impeccable/reference/animate.md b/.cursor/skills/impeccable/reference/animate.md index 20aeea659..15d9f9080 100644 --- a/.cursor/skills/impeccable/reference/animate.md +++ b/.cursor/skills/impeccable/reference/animate.md @@ -6,7 +6,7 @@ Add motion that conveys state, gives feedback, and clarifies hierarchy. Cut moti ## Register -Brand: orchestrated page-load sequences, staggered reveals, scroll-driven animation. Motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions. +Brand: motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions. The saturated AI default is fade-and-rise reveals on every scrolled section; that's a tell, not a choreography. Reserve scroll-triggered motion for moments that earn it. Product: 150–250 ms on most transitions. Motion conveys state: feedback, reveal, loading, transitions between views. No page-load choreography; users are in a task and won't wait for it. @@ -49,10 +49,11 @@ Create a purposeful animation plan: Add motion systematically across these categories: ### Entrance Animations -- **Page load choreography**: Stagger element reveals (100-150ms delays), fade + slide combinations - **Hero section**: Dramatic entrance for primary content (scale, parallax, or creative effects) -- **Content reveals**: Scroll-triggered animations using intersection observer - **Modal/drawer entry**: Smooth slide + fade, backdrop fade, focus management +- **List rhythm**: Sibling stagger is legitimate for cards-in-a-grid or list-items-appearing. Whole-section fade-on-scroll is not a list and is not legitimate. Cap total stagger time: 10 items at 50ms each = 500ms total. For more items, reduce per-item delay or cap the staggered count. + + Use CSS custom properties for clean stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"`, `style="--i: 1"`, etc. on each item. ### Micro-interactions - **Button feedback**: @@ -97,11 +98,14 @@ Use appropriate techniques for each animation: ### Timing & Easing -**Durations by purpose:** -- **100-150ms**: Instant feedback (button press, toggle) -- **200-300ms**: State changes (hover, menu open) -- **300-500ms**: Layout changes (accordion, modal) -- **500-800ms**: Entrance animations (page load) +**Duration: the 100/300/500 rule.** Timing matters more than easing for "feels right": + +| Duration | Use Case | Examples | +|----------|----------|----------| +| **100–150ms** | Instant feedback | Button press, toggle, color change | +| **200–300ms** | State changes | Menu open, tooltip, hover state | +| **300–500ms** | Layout changes | Accordion, modal, drawer | +| **500–800ms** | Entrance animations | Page load, hero reveal | **Easing curves (use these, not CSS defaults):** ```css @@ -134,13 +138,35 @@ Use appropriate techniques for each animation: - GSAP for complex sequences ``` +### Motion Materials + +Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties. Match material to effect: + +- **Transform / opacity**: movement, press feedback, simple reveals, list choreography +- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances +- **Clip-path / masks**: wipes, reveals, editorial cropping, product-like transitions +- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state +- **Grid-template-rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly + +The hard rule isn't "transform and opacity only." It's: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify smoothness in-browser on target viewports. + ### Performance -- **Motion materials**: Use transform/opacity for reliable movement, but use blur, filters, masks, shadows, and color shifts when they materially improve the effect - **Layout safety**: Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins) -- **will-change**: Add sparingly for known expensive animations +- **will-change**: Add sparingly for known expensive animations only (e.g. on `:hover` or an `.animating` class), never preemptively across the whole page +- **Scroll triggers**: Use Intersection Observer instead of scroll event listeners; unobserve after the animation fires once - **Bound expensive effects**: Keep blur/filter/shadow areas small or isolated, use `contain` where appropriate - **Monitor FPS**: Ensure 60fps on target devices +### Perceived Performance + +Nobody cares how fast your site *is*, only how fast it feels. The 80ms threshold: anything under ~80ms feels instant because our brains buffer sensory input for that long to synchronize perception. Target this for micro-interactions. + +- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening. +- **Early completion**: Show content progressively, don't wait for everything (progressive images, streaming HTML, skeleton fade-ins). +- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Use for low-stakes actions (likes, follows). Avoid for payments or destructive operations. +- **Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances. +- **Caution**: Too-fast responses can decrease perceived value for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening. + ### Accessibility ```css @media (prefers-reduced-motion: reduce) { diff --git a/.cursor/skills/impeccable/reference/bolder.md b/.cursor/skills/impeccable/reference/bolder.md index ff0ef2f14..30d4e852c 100644 --- a/.cursor/skills/impeccable/reference/bolder.md +++ b/.cursor/skills/impeccable/reference/bolder.md @@ -50,7 +50,7 @@ Create a strategy to increase impact while maintaining coherence: Systematically increase impact across these dimensions: ### Typography Amplification -- **Replace generic fonts**: Swap system fonts for distinctive choices (see the parent skill's typography guidelines and [typography.md](typography.md) for inspiration) +- **Replace generic fonts**: Swap system fonts for distinctive choices (see the parent skill's typography guidelines and the [Reference Material section of typeset.md](typeset.md#reference-material) for inspiration) - **Extreme scale**: Create dramatic size jumps (3x-5x differences, not 1.5x) - **Weight contrast**: Pair 900 weights with 200 weights, not 600 with 400 - **Unexpected choices**: Variable fonts, display fonts for headlines, condensed/extended widths, monospace as intentional accent (not as lazy "dev tool" default) @@ -78,10 +78,10 @@ Systematically increase impact across these dimensions: - **Custom elements**: Illustrative elements, custom icons, decorative details that reinforce brand ### Motion & Animation -- **Entrance choreography**: Staggered, dramatic page load animations with 50-100ms delays -- **Scroll effects**: Parallax, reveal animations, scroll-triggered sequences -- **Micro-interactions**: Satisfying hover effects, click feedback, state changes -- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic, which cheapen the effect) +- **Hero moment**: One signature entrance, once. Not on every visit and not on every section. +- **Micro-interactions**: Satisfying hover effects, click feedback, state changes. +- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic, which cheapen the effect). +- **Bolder ≠ scroll-fade-rise on every section.** That's the saturated AI default, the opposite of bold. ### Composition Boldness - **Hero moments**: Create clear focal points with dramatic treatment diff --git a/.cursor/skills/impeccable/reference/brand.md b/.cursor/skills/impeccable/reference/brand.md index 3d83a1cdc..194514e95 100644 --- a/.cursor/skills/impeccable/reference/brand.md +++ b/.cursor/skills/impeccable/reference/brand.md @@ -43,17 +43,10 @@ The reflex-reject lists apply to **new design choices**. When the existing brand ### Pairing and voice -Distinctive + refined is the goal. The specific shape depends on the brand: - -- **Editorial / long-form / luxury**: display serif + sans body (a magazine shape). -- **Tech / dev tools / fintech**: one committed sans, usually; custom-tight tracking, strong weight contrast inside a single family. -- **Consumer / food / travel**: warmer pairings, often a humanist sans plus a script or display serif. -- **Creative studios / agencies**: rule-breaking welcome. Mono-only, or display-only, or custom-drawn type as voice. +Distinctive + refined is the goal. The specific shape depends on the brand, not on the brand's category. A category ("restaurant", "dev tool", "magazine", "fintech") is not a recipe; treating it as one is the first-order reflex SKILL.md warns against. Two families minimum is the rule *only* when the voice needs it. A single well-chosen family with committed weight/size contrast is stronger than a timid display+body pair. -Vary across projects. If the last brief was a serif-display landing page, this one isn't. - ### Scale Modular scale, fluid `clamp()` for headings, ≥1.25 ratio between steps. Flat scales (1.1× apart) read as uncommitted. @@ -74,8 +67,7 @@ Brand surfaces have permission for Committed, Full palette, and Drenched strateg - Asymmetric compositions are one option. Break the grid intentionally for emphasis. - Fluid spacing with `clamp()` that breathes on larger viewports. Vary for rhythm: generous separations, tight groupings. -- Alternative: a strict, visible grid as the voice (brutalist / Swiss / tech-spec aesthetics). Either asymmetric or rigorously-gridded can be "designed"; the failure mode is splitting the difference into a generic centered stack. -- Don't default to centering everything. Left-aligned with asymmetric layouts feels more designed; a strict grid reads as confident structure. A centered-stack hero with icon-title-subtitle cards reads as template. +- For image-led briefs (hotels, restaurants, magazines, photography), full-bleed hero imagery with overlaid menu and centered headline is a canonical move; let the photograph be the design. - When cards ARE the right affordance, use `grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` for breakpoint-free responsiveness. ## Imagery @@ -93,8 +85,7 @@ Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landin ## Motion -- One well-orchestrated page-load with staggered reveals beats scattered micro-interactions, when the brand invites it. Tech-minimal brands often skip entrance motion entirely; the restraint is the voice. -- For collapsing/expanding sections, transition `grid-template-rows` rather than `height`. +- One well-orchestrated page-load beats scattered micro-interactions, when the brand invites it. Some brands skip entrance motion entirely; the restraint is the voice. ## Brand bans (on top of the shared absolute bans) @@ -111,8 +102,7 @@ Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landin Brand can afford things product can't. Take them. -- Ambitious first-load motion. Reveals, scroll-triggered transitions, typographic choreography. +- Ambitious first-load motion. Reveals and typographic choreography that earn their place; not fade-on-scroll for every section. - Single-purpose viewports. One dominant idea per fold, long scroll, deliberate pacing. -- Typographic risk. Enormous display type, unexpected italic cuts, mixed cases, hand-drawn headlines, a single oversize word as a hero. - Unexpected color strategies. Palette IS voice; a calm brand and a restless brand should not share palette mechanics. - Art direction per section. Different sections can have different visual worlds if the narrative demands it. Consistency of voice beats consistency of treatment. diff --git a/.cursor/skills/impeccable/reference/clarify.md b/.cursor/skills/impeccable/reference/clarify.md index 5f7d420f4..00ee978c3 100644 --- a/.cursor/skills/impeccable/reference/clarify.md +++ b/.cursor/skills/impeccable/reference/clarify.md @@ -172,3 +172,117 @@ Test that copy improvements work: - **Tone**: Is it appropriate for the situation? When the copy reads cleanly, hand off to `/impeccable polish` for the final pass. + +--- + +## Reference Material + +The sections below were previously `ux-writing.md` and live inline now so the clarify flow has its deep UX-writing reference in one place. + +### UX Writing + +#### The Button Label Problem + +**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns: + +| Bad | Good | Why | +|-----|------|-----| +| OK | Save changes | Says what will happen | +| Submit | Create account | Outcome-focused | +| Yes | Delete message | Confirms the action | +| Cancel | Keep editing | Clarifies what "cancel" means | +| Click here | Download PDF | Describes the destination | + +**For destructive actions**, name the destruction: +- "Delete" not "Remove" (delete is permanent, remove implies recoverable) +- "Delete 5 items" not "Delete selected" (show the count) + +#### Error Messages: The Formula + +Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input". + +##### Error Message Templates + +| Situation | Template | +|-----------|----------| +| **Format error** | "[Field] needs to be [format]. Example: [example]" | +| **Missing required** | "Please enter [what's missing]" | +| **Permission denied** | "You don't have access to [thing]. [What to do instead]" | +| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." | +| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" | + +##### Don't Blame the User + +Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date". + +#### Empty States Are Opportunities + +Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items". + +#### Voice vs Tone + +**Voice** is your brand's personality, consistent everywhere. +**Tone** adapts to the moment. + +| Moment | Tone Shift | +|--------|------------| +| Success | Celebratory, brief: "Done! Your changes are live." | +| Error | Empathetic, helpful: "That didn't work. Here's what to try..." | +| Loading | Reassuring: "Saving your work..." | +| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | + +**Never use humor for errors.** Users are already frustrated. Be helpful, not cute. + +#### Writing for Accessibility + +**Link text** must have standalone meaning: "View pricing plans" not "Click here". **Alt text** describes information, not the image: "Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context. + +#### Writing for Translation + +##### Plan for Expansion + +German text is ~30% longer than English. Allocate space: + +| Language | Expansion | +|----------|-----------| +| German | +30% | +| French | +20% | +| Finnish | +30-40% | +| Chinese | -30% (fewer chars, but same width) | + +##### Translation-Friendly Patterns + +Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear. + +#### Consistency: The Terminology Problem + +Pick one term and stick with it: + +| Inconsistent | Consistent | +|--------------|------------| +| Delete / Remove / Trash | Delete | +| Settings / Preferences / Options | Settings | +| Sign in / Log in / Enter | Sign in | +| Create / Add / New | Create | + +Build a terminology glossary and enforce it. Variety creates confusion. + +#### Avoid Redundant Copy + +If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well. + +#### Loading States + +Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress. + +#### Confirmation Dialogs: Use Sparingly + +Most confirmation dialogs are design failures; consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No"). + +#### Form Instructions + +Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking. + +--- + +**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors. diff --git a/.cursor/skills/impeccable/reference/codex.md b/.cursor/skills/impeccable/reference/codex.md index f40ec746f..d563af95b 100644 --- a/.cursor/skills/impeccable/reference/codex.md +++ b/.cursor/skills/impeccable/reference/codex.md @@ -23,8 +23,8 @@ Before generating anything, run a brief direction conversation grounded in the s Ask **2-3 targeted questions** about visual lane, color strategy, atmosphere, and named anchor references. Don't enumerate generic menus; tie each question to the shape brief's answers. Example shape-grounded questions: -- "Brief says 'editorial restraint, Klim-adjacent.' Are we closer to a quiet specimen page or a magazine-spread feel with hero imagery?" -- "Palette strategy from shape was 'Committed.' Want it warm-grounded (deep oxblood + cream) or cool-grounded (slate + paper white)?" +- "Brief says 'specimen-page restraint.' Are we closer to a quiet typographic page or a wider editorial spread with hero imagery?" +- "Palette strategy from shape was 'Committed.' Which one color carries the surface (a brand-driven pick rather than a default warm-or-cool framing)? (And no, the answer isn't a cream/sand body bg; that's the saturated AI default.)" **STOP and wait for answers.** These pin the palette before any pixel gets generated. Do not proceed to Step B until the user has responded. diff --git a/.cursor/skills/impeccable/reference/cognitive-load.md b/.cursor/skills/impeccable/reference/cognitive-load.md deleted file mode 100644 index 48f8ad58b..000000000 --- a/.cursor/skills/impeccable/reference/cognitive-load.md +++ /dev/null @@ -1,106 +0,0 @@ -# Cognitive Load Assessment - -Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload. - ---- - -## Three Types of Cognitive Load - -### Intrinsic Load: The Task Itself -Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it. - -**Manage it by**: -- Breaking complex tasks into discrete steps -- Providing scaffolding (templates, defaults, examples) -- Progressive disclosure: show what's needed now, hide the rest -- Grouping related decisions together - -### Extraneous Load: Bad Design -Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste. - -**Common sources**: -- Confusing navigation that requires mental mapping -- Unclear labels that force users to guess meaning -- Visual clutter competing for attention -- Inconsistent patterns that prevent learning -- Unnecessary steps between user intent and result - -### Germane Load: Learning Effort -Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery. - -**Support it by**: -- Progressive disclosure that reveals complexity gradually -- Consistent patterns that reward learning -- Feedback that confirms correct understanding -- Onboarding that teaches through action, not walls of text - ---- - -## Cognitive Load Checklist - -Evaluate the interface against these 8 items: - -- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements? -- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)? -- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)? -- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen? -- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next? -- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)? -- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one? -- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it? - -**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed). - ---- - -## The Working Memory Rule - -**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001). - -At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider: -- **≤4 items**: Within working memory limits, manageable -- **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure -- **8+ items**: Overloaded; users will skip, misclick, or abandon - -**Practical applications**: -- Navigation menus: ≤5 top-level items (group the rest under clear categories) -- Form sections: ≤4 fields visible per group before a visual break -- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu -- Dashboard widgets: ≤4 key metrics visible without scrolling -- Pricing tiers: ≤3 options (more causes analysis paralysis) - ---- - -## Common Cognitive Load Violations - -### 1. The Wall of Options -**Problem**: Presenting 10+ choices at once with no hierarchy. -**Fix**: Group into categories, highlight recommended, use progressive disclosure. - -### 2. The Memory Bridge -**Problem**: User must remember info from step 1 to complete step 3. -**Fix**: Keep relevant context visible, or repeat it where it's needed. - -### 3. The Hidden Navigation -**Problem**: User must build a mental map of where things are. -**Fix**: Always show current location (breadcrumbs, active states, progress indicators). - -### 4. The Jargon Barrier -**Problem**: Technical or domain language forces translation effort. -**Fix**: Use plain language. If domain terms are unavoidable, define them inline. - -### 5. The Visual Noise Floor -**Problem**: Every element has the same visual weight; nothing stands out. -**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted. - -### 6. The Inconsistent Pattern -**Problem**: Similar actions work differently in different places. -**Fix**: Standardize interaction patterns. Same type of action = same type of UI. - -### 7. The Multi-Task Demand -**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating). -**Fix**: Sequence the steps. Let the user do one thing at a time. - -### 8. The Context Switch -**Problem**: User must jump between screens/tabs/modals to gather info for a single decision. -**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth. diff --git a/.cursor/skills/impeccable/reference/color-and-contrast.md b/.cursor/skills/impeccable/reference/color-and-contrast.md deleted file mode 100644 index 110c2ee47..000000000 --- a/.cursor/skills/impeccable/reference/color-and-contrast.md +++ /dev/null @@ -1,105 +0,0 @@ -# Color & Contrast - -## Color Spaces: Use OKLCH - -**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal, unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark. - -The OKLCH function takes three components: `oklch(lightness chroma hue)` where lightness is 0-100%, chroma is roughly 0-0.4, and hue is 0-360. To build a primary color and its lighter / darker variants, hold the chroma+hue roughly constant and vary the lightness, but **reduce chroma as you approach white or black**, because high chroma at extreme lightness looks garish. - -The hue you pick is a brand decision and should not come from a default. Do not reach for blue (hue 250) or warm orange (hue 60) by reflex; those are the dominant AI-design defaults, not the right answer for any specific brand. - -## Building Functional Palettes - -### Tinted Neutrals - -**Pure gray is dead.** A neutral with zero chroma feels lifeless next to a colored brand. Add a tiny chroma value (0.005-0.015) to all your neutrals, hued toward whatever your brand color is. The chroma is small enough not to read as "tinted" consciously, but it creates subconscious cohesion between brand color and UI surfaces. - -The hue you tint toward should come from THIS project's brand, not from a "warm = friendly, cool = tech" formula. If your brand color is teal, your neutrals lean toward teal. If your brand color is amber, they lean toward amber. The point is cohesion with the SPECIFIC brand, not a stock palette. - -**Avoid** the trap of always tinting toward warm orange or always tinting toward cool blue. Those are the two laziest defaults and they create their own monoculture across projects. - -### Palette Structure - -A complete system needs: - -| Role | Purpose | Example | -|------|---------|---------| -| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades | -| **Neutral** | Text, backgrounds, borders | 9-11 shade scale | -| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each | -| **Surface** | Cards, modals, overlays | 2-3 elevation levels | - -**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise. - -### The 60-30-10 Rule (Applied Correctly) - -This rule is about **visual weight**, not pixel count: - -- **60%**: Neutral backgrounds, white space, base surfaces -- **30%**: Secondary colors: text, borders, inactive states -- **10%**: Accent: CTAs, highlights, focus states - -The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power. - -## Contrast & Accessibility - -### WCAG Requirements - -| Content Type | AA Minimum | AAA Target | -|--------------|------------|------------| -| Body text | 4.5:1 | 7:1 | -| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | -| UI components, icons | 3:1 | 4.5:1 | -| Non-essential decorations | None | None | - -**The gotcha**: Placeholder text still needs 4.5:1. That light gray placeholder you see everywhere? Usually fails WCAG. - -### Dangerous Color Combinations - -These commonly fail contrast or cause readability issues: - -- Light gray text on white (the #1 accessibility fail) -- **Gray text on any colored background**: gray looks washed out and dead on color. Use a darker shade of the background color, or transparency -- Red text on green background (or vice versa): 8% of men can't distinguish these -- Blue text on red background (vibrates visually) -- Yellow text on white (almost always fails) -- Thin light text on images (unpredictable contrast) - -### Never Use Pure Gray or Pure Black - -Pure gray (`oklch(50% 0 0)`) and pure black (`#000`) don't exist in nature; real shadows and surfaces always have a color cast. Even a chroma of 0.005-0.01 is enough to feel natural without being obviously tinted. (See tinted neutrals example above.) - -### Testing - -Don't trust your eyes. Use tools: - -- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) -- Browser DevTools → Rendering → Emulate vision deficiencies -- [Polypane](https://polypane.app/) for real-time testing - -## Theming: Light & Dark Mode - -### Dark Mode Is Not Inverted Light Mode - -You can't just swap colors. Dark mode requires different design decisions: - -| Light Mode | Dark Mode | -|------------|-----------| -| Shadows for depth | Lighter surfaces for depth (no shadows) | -| Dark text on light | Light text on dark (reduce font weight) | -| Vibrant accents | Desaturate accents slightly | -| White backgrounds | Never pure black; use dark gray (oklch 12-18%) | - -In dark mode, depth comes from surface lightness, not shadow. Build a 3-step surface scale where higher elevations are lighter (e.g. 15% / 20% / 25% lightness). Use the SAME hue and chroma as your brand color (whatever it is for THIS project; do not reach for blue) and only vary the lightness. Reduce body text weight slightly (e.g. 350 instead of 400) because light text on dark reads as heavier than dark text on light. - -### Token Hierarchy - -Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer; primitives stay the same. - -## Alpha Is A Design Smell - -Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed. - ---- - -**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Using pure black (#000) for large areas. Skipping color blindness testing (8% of men affected). diff --git a/.cursor/skills/impeccable/reference/colorize.md b/.cursor/skills/impeccable/reference/colorize.md index 9e96eef59..2457f608c 100644 --- a/.cursor/skills/impeccable/reference/colorize.md +++ b/.cursor/skills/impeccable/reference/colorize.md @@ -68,10 +68,10 @@ Add color systematically across these dimensions: - **Hover states**: Introduce color on interaction ### Background & Surfaces -- **Tinted backgrounds**: Replace pure gray (`#f5f5f5`) with warm neutrals (`oklch(97% 0.01 60)`) or cool tints (`oklch(97% 0.01 250)`) +- **Tinted backgrounds**: If you replace pure gray, tint toward the brand hue, not toward a generic-warm-or-cool pair. The default-warm-tint (`oklch(97% 0.01 60)` and its neighbors) is now the AI cream/sand giveaway. Be specific to the brand or stay neutral. - **Colored sections**: Use subtle background colors to separate areas - **Gradient backgrounds**: Add depth with subtle, intentional gradients (not generic purple-blue) -- **Cards & surfaces**: Tint cards or surfaces slightly for warmth +- **Cards & surfaces**: Tint cards or surfaces toward the brand, not "for warmth" by reflex **Use OKLCH for color**: It's perceptually uniform, meaning equal steps in lightness *look* equal. Great for generating harmonious scales. @@ -124,8 +124,6 @@ Ensure color addition improves rather than overwhelms: - Use every color in the rainbow (choose 2-4 colors beyond neutrals) - Apply color randomly without semantic meaning - Put gray text on colored backgrounds. It looks washed out; use a darker shade of the background color or transparency instead -- Use pure gray for neutrals. Add subtle color tint (warm or cool) for depth -- Use pure black (`#000`) or pure white (`#fff`) for large areas - Violate WCAG contrast requirements - Use color as the only indicator (accessibility issue) - Make everything colorful (defeats the purpose) @@ -152,3 +150,108 @@ When invoked from live mode, each variant MUST declare a `color-amount` param so ``` Layer 1-2 variant-specific params on top: palette selection (`steps` with named options), temperature warmth, or tint vs. true color. See `reference/live.md` for the full params contract. + +--- + +## Reference Material + +The sections below were previously `color-and-contrast.md` and live inline now so the colorize flow has its deep color reference in one place. + +### Color & Contrast + +#### Color Spaces: Use OKLCH + +**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal, unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark. + +The OKLCH function takes three components: `oklch(lightness chroma hue)` where lightness is 0-100%, chroma is roughly 0-0.4, and hue is 0-360. To build a primary color and its lighter / darker variants, hold the chroma+hue roughly constant and vary the lightness, but **reduce chroma as you approach white or black**, because high chroma at extreme lightness looks garish. + +The hue you pick is a brand decision and should not come from a default. Do not reach for blue (hue 250) or warm orange (hue 60) by reflex; those are the dominant AI-design defaults, not the right answer for any specific brand. + +#### Building Functional Palettes + +##### Tinted Neutrals + +**Pure gray is dead.** A neutral with zero chroma feels lifeless next to a colored brand. Add a tiny chroma value (0.005-0.015) to all your neutrals, hued toward whatever your brand color is. The chroma is small enough not to read as "tinted" consciously, but it creates subconscious cohesion between brand color and UI surfaces. + +The hue you tint toward should come from THIS project's brand, not from a "warm = friendly, cool = tech" formula. If your brand color is teal, your neutrals lean toward teal. If your brand color is amber, they lean toward amber. The point is cohesion with the SPECIFIC brand, not a stock palette. + +**Avoid** the trap of always tinting toward warm orange or always tinting toward cool blue. Those are the two laziest defaults and they create their own monoculture across projects. + +##### Palette Structure + +A complete system needs: + +| Role | Purpose | Example | +|------|---------|---------| +| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades | +| **Neutral** | Text, backgrounds, borders | 9-11 shade scale | +| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each | +| **Surface** | Cards, modals, overlays | 2-3 elevation levels | + +**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise. + +##### The 60-30-10 Rule (Applied Correctly) + +This rule is about **visual weight**, not pixel count: + +- **60%**: Neutral backgrounds, white space, base surfaces +- **30%**: Secondary colors: text, borders, inactive states +- **10%**: Accent: CTAs, highlights, focus states + +The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power. + +#### Contrast & Accessibility + +##### WCAG Requirements + +| Content Type | AA Minimum | AAA Target | +|--------------|------------|------------| +| Body text | 4.5:1 | 7:1 | +| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | +| UI components, icons | 3:1 | 4.5:1 | +| Non-essential decorations | None | None | + +##### Dangerous Color Combinations + +These commonly fail contrast or cause readability issues: + +- Light gray text on white (the #1 accessibility fail) +- Red text on green background (or vice versa): 8% of men can't distinguish these +- Blue text on red background (vibrates visually) +- Yellow text on white (almost always fails) +- Thin light text on images (unpredictable contrast) + +##### Testing + +Don't trust your eyes. Use tools: + +- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) +- Browser DevTools → Rendering → Emulate vision deficiencies +- [Polypane](https://polypane.app/) for real-time testing + +#### Theming: Light & Dark Mode + +##### Dark Mode Is Not Inverted Light Mode + +You can't just swap colors. Dark mode requires different design decisions: + +| Light Mode | Dark Mode | +|------------|-----------| +| Shadows for depth | Lighter surfaces for depth (no shadows) | +| Dark text on light | Light text on dark (reduce font weight) | +| Vibrant accents | Desaturate accents slightly | +| White backgrounds | Either pure black or a deep surface that fits the brand (a brand-tinted near-black at oklch 12-18% works too) | + +In dark mode, depth comes from surface lightness, not shadow. Build a 3-step surface scale where higher elevations are lighter (e.g. 15% / 20% / 25% lightness). Use the SAME hue and chroma as your brand color (whatever it is for THIS project; do not reach for blue) and only vary the lightness. Reduce body text weight slightly (e.g. 350 instead of 400) because light text on dark reads as heavier than dark text on light. + +##### Token Hierarchy + +Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer; primitives stay the same. + +#### Alpha Is A Design Smell + +Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed. + +--- + +**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Skipping color blindness testing (8% of men affected). diff --git a/.cursor/skills/impeccable/reference/craft.md b/.cursor/skills/impeccable/reference/craft.md index 06094b1dd..2c7bd99b1 100644 --- a/.cursor/skills/impeccable/reference/craft.md +++ b/.cursor/skills/impeccable/reference/craft.md @@ -56,15 +56,15 @@ If the harness has native image generation (Codex), a compact shape's "confirm o Based on the design brief's "Recommended References" section, consult the relevant impeccable reference files. At minimum, always consult: -- [spatial-design.md](spatial-design.md) for layout and spacing -- [typography.md](typography.md) for type hierarchy +- [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 [motion-design.md](motion-design.md) -- Color-heavy or themed? Consult [color-and-contrast.md](color-and-contrast.md) -- Responsive requirements? Consult [responsive-design.md](responsive-design.md) -- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.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) diff --git a/.cursor/skills/impeccable/reference/critique.md b/.cursor/skills/impeccable/reference/critique.md index 9d3d9d3cf..a117ea0a2 100644 --- a/.cursor/skills/impeccable/reference/critique.md +++ b/.cursor/skills/impeccable/reference/critique.md @@ -38,9 +38,9 @@ Read relevant source files and visually inspect the live page when browser autom Evaluate: - **AI slop**: Would someone believe "AI made this" immediately? Check all DON'T guidance from the parent Impeccable skill. - **Holistic design**: hierarchy, IA, emotional fit, discoverability, composition, typography, color, accessibility, states, copy, and edge cases. -- **Cognitive load**: consult [cognitive-load](cognitive-load.md); report checklist failures and decision points with >4 visible options. +- **Cognitive load**: consult the [Cognitive Load Assessment](#cognitive-load-assessment) section below; report checklist failures and decision points with >4 visible options. - **Emotional journey**: peak-end rule, emotional valleys, reassurance at high-stakes moments. -- **Nielsen heuristics**: consult [heuristics-scoring](heuristics-scoring.md); score all 10 heuristics 0-4. +- **Nielsen heuristics**: consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below; score all 10 heuristics 0-4. Return: AI slop verdict, heuristic scores, cognitive load, emotional journey, 2-3 strengths, 3-5 priority issues, persona red flags, minor observations, and provocative questions. @@ -80,7 +80,7 @@ The chat response is the primary user-facing deliverable. Present the full struc Structure your feedback as a design director would: #### Design Health Score -> *Consult [heuristics-scoring](heuristics-scoring.md)* +> *Consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below.* Present the Nielsen's 10 heuristics scores as a table: @@ -119,16 +119,16 @@ Highlight 2-3 things done well. Be specific about why they work. #### Priority Issues The 3-5 most impactful design problems, ordered by importance. -For each issue, tag with **P0-P3 severity** (consult [heuristics-scoring](heuristics-scoring.md) for severity definitions): +For each issue, tag with **P0-P3 severity** (see [Issue Severity below](#issue-severity-p0p3) for definitions): - **[P?] What**: Name the problem clearly - **Why it matters**: How this hurts users or undermines goals - **Fix**: What to do about it (be concrete) - **Suggested command**: Which command could address this (from: /impeccable adapt, /impeccable animate, /impeccable audit, /impeccable bolder, /impeccable clarify, /impeccable colorize, /impeccable critique, /impeccable delight, /impeccable distill, /impeccable document, /impeccable harden, /impeccable layout, /impeccable onboard, /impeccable optimize, /impeccable overdrive, /impeccable polish, /impeccable quieter, /impeccable shape, /impeccable typeset) #### Persona Red Flags -> *Consult [personas](personas.md)* +> *Consult the [Personas reference](#persona-based-design-testing) below.* -Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `.cursorrules` contains a `## Design Context` section from `impeccable teach`, also generate 1-2 project-specific personas from the audience/brand info. +Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `.cursorrules` contains a `## Design Context` section from `impeccable init`, also generate 1-2 project-specific personas from the audience/brand info. For each selected persona, walk through the primary user action and list specific red flags found: @@ -234,3 +234,534 @@ After presenting the summary, tell the user: > You can ask me to run these one at a time, all at once, or in any order you prefer. > > Re-run `/impeccable critique` after fixes to see your score improve. + +--- + +## Reference Material + +The sections below were previously separate reference files (`cognitive-load.md`, `heuristics-scoring.md`, `personas.md`). They live inline now so the critique flow has all its deep context in one place. + +### Cognitive Load Assessment + +Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload. + +--- + +#### Three Types of Cognitive Load + +##### Intrinsic Load: The Task Itself +Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it. + +**Manage it by**: +- Breaking complex tasks into discrete steps +- Providing scaffolding (templates, defaults, examples) +- Progressive disclosure: show what's needed now, hide the rest +- Grouping related decisions together + +##### Extraneous Load: Bad Design +Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste. + +**Common sources**: +- Confusing navigation that requires mental mapping +- Unclear labels that force users to guess meaning +- Visual clutter competing for attention +- Inconsistent patterns that prevent learning +- Unnecessary steps between user intent and result + +##### Germane Load: Learning Effort +Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery. + +**Support it by**: +- Progressive disclosure that reveals complexity gradually +- Consistent patterns that reward learning +- Feedback that confirms correct understanding +- Onboarding that teaches through action, not walls of text + +--- + +#### Cognitive Load Checklist + +Evaluate the interface against these 8 items: + +- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements? +- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)? +- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)? +- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen? +- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next? +- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)? +- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one? +- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it? + +**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed). + +--- + +#### The Working Memory Rule + +**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001). + +At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider: +- **≤4 items**: Within working memory limits, manageable +- **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure +- **8+ items**: Overloaded; users will skip, misclick, or abandon + +**Practical applications**: +- Navigation menus: ≤5 top-level items (group the rest under clear categories) +- Form sections: ≤4 fields visible per group before a visual break +- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu +- Dashboard widgets: ≤4 key metrics visible without scrolling +- Pricing tiers: ≤3 options (more causes analysis paralysis) + +--- + +#### Common Cognitive Load Violations + +##### 1. The Wall of Options +**Problem**: Presenting 10+ choices at once with no hierarchy. +**Fix**: Group into categories, highlight recommended, use progressive disclosure. + +##### 2. The Memory Bridge +**Problem**: User must remember info from step 1 to complete step 3. +**Fix**: Keep relevant context visible, or repeat it where it's needed. + +##### 3. The Hidden Navigation +**Problem**: User must build a mental map of where things are. +**Fix**: Always show current location (breadcrumbs, active states, progress indicators). + +##### 4. The Jargon Barrier +**Problem**: Technical or domain language forces translation effort. +**Fix**: Use plain language. If domain terms are unavoidable, define them inline. + +##### 5. The Visual Noise Floor +**Problem**: Every element has the same visual weight; nothing stands out. +**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted. + +##### 6. The Inconsistent Pattern +**Problem**: Similar actions work differently in different places. +**Fix**: Standardize interaction patterns. Same type of action = same type of UI. + +##### 7. The Multi-Task Demand +**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating). +**Fix**: Sequence the steps. Let the user do one thing at a time. + +##### 8. The Context Switch +**Problem**: User must jump between screens/tabs/modals to gather info for a single decision. +**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth. + +--- + +### Heuristics Scoring Guide + +Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough." + +#### Nielsen's 10 Heuristics + +##### 1. Visibility of System Status + +Keep users informed about what's happening through timely, appropriate feedback. + +**Check for**: +- Loading indicators during async operations +- Confirmation of user actions (save, submit, delete) +- Progress indicators for multi-step processes +- Current location in navigation (breadcrumbs, active states) +- Form validation feedback (inline, not just on submit) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | No feedback; user is guessing what happened | +| 1 | Rare feedback; most actions produce no visible response | +| 2 | Partial; some states communicated, major gaps remain | +| 3 | Good; most operations give clear feedback, minor gaps | +| 4 | Excellent; every action confirms, progress is always visible | + +##### 2. Match Between System and Real World + +Speak the user's language. Follow real-world conventions. Information appears in natural, logical order. + +**Check for**: +- Familiar terminology (no unexplained jargon) +- Logical information order matching user expectations +- Recognizable icons and metaphors +- Domain-appropriate language for the target audience +- Natural reading flow (left-to-right, top-to-bottom priority) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Pure tech jargon, alien to users | +| 1 | Mostly confusing; requires domain expertise to navigate | +| 2 | Mixed; some plain language, some jargon leaks through | +| 3 | Mostly natural; occasional term needs context | +| 4 | Speaks the user's language fluently throughout | + +##### 3. User Control and Freedom + +Users need a clear "emergency exit" from unwanted states without extended dialogue. + +**Check for**: +- Undo/redo functionality +- Cancel buttons on forms and modals +- Clear navigation back to safety (home, previous) +- Easy way to clear filters, search, selections +- Escape from long or multi-step processes + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Users get trapped; no way out without refreshing | +| 1 | Difficult exits; must find obscure paths to escape | +| 2 | Some exits; main flows have escape, edge cases don't | +| 3 | Good control; users can exit and undo most actions | +| 4 | Full control; undo, cancel, back, and escape everywhere | + +##### 4. Consistency and Standards + +Users shouldn't wonder whether different words, situations, or actions mean the same thing. + +**Check for**: +- Consistent terminology throughout the interface +- Same actions produce same results everywhere +- Platform conventions followed (standard UI patterns) +- Visual consistency (colors, typography, spacing, components) +- Consistent interaction patterns (same gesture = same behavior) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Inconsistent everywhere; feels like different products stitched together | +| 1 | Many inconsistencies; similar things look/behave differently | +| 2 | Partially consistent; main flows match, details diverge | +| 3 | Mostly consistent; occasional deviation, nothing confusing | +| 4 | Fully consistent; cohesive system, predictable behavior | + +##### 5. Error Prevention + +Better than good error messages is a design that prevents problems in the first place. + +**Check for**: +- Confirmation before destructive actions (delete, overwrite) +- Constraints preventing invalid input (date pickers, dropdowns) +- Smart defaults that reduce errors +- Clear labels that prevent misunderstanding +- Autosave and draft recovery + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Errors easy to make; no guardrails anywhere | +| 1 | Few safeguards; some inputs validated, most aren't | +| 2 | Partial prevention; common errors caught, edge cases slip | +| 3 | Good prevention; most error paths blocked proactively | +| 4 | Excellent; errors nearly impossible through smart constraints | + +##### 6. Recognition Rather Than Recall + +Minimize memory load. Make objects, actions, and options visible or easily retrievable. + +**Check for**: +- Visible options (not buried in hidden menus) +- Contextual help when needed (tooltips, inline hints) +- Recent items and history +- Autocomplete and suggestions +- Labels on icons (not icon-only navigation) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Heavy memorization; users must remember paths and commands | +| 1 | Mostly recall; many hidden features, few visible cues | +| 2 | Some aids; main actions visible, secondary features hidden | +| 3 | Good recognition; most things discoverable, few memory demands | +| 4 | Everything discoverable; users never need to memorize | + +##### 7. Flexibility and Efficiency of Use + +Accelerators, invisible to novices, speed up expert interaction. + +**Check for**: +- Keyboard shortcuts for common actions +- Customizable interface elements +- Recent items and favorites +- Bulk/batch actions +- Power user features that don't complicate the basics + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | One rigid path; no shortcuts or alternatives | +| 1 | Limited flexibility; few alternatives to the main path | +| 2 | Some shortcuts; basic keyboard support, limited bulk actions | +| 3 | Good accelerators; keyboard nav, some customization | +| 4 | Highly flexible; multiple paths, power features, customizable | + +##### 8. Aesthetic and Minimalist Design + +Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose. + +**Check for**: +- Only necessary information visible at each step +- Clear visual hierarchy directing attention +- Purposeful use of color and emphasis +- No decorative clutter competing for attention +- Focused, uncluttered layouts + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Overwhelming; everything competes for attention equally | +| 1 | Cluttered; too much noise, hard to find what matters | +| 2 | Some clutter; main content clear, periphery noisy | +| 3 | Mostly clean; focused design, minor visual noise | +| 4 | Perfectly minimal; every element earns its pixel | + +##### 9. Help Users Recognize, Diagnose, and Recover from Errors + +Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution. + +**Check for**: +- Plain language error messages (no error codes for users) +- Specific problem identification ("Email is missing @" not "Invalid input") +- Actionable recovery suggestions +- Errors displayed near the source of the problem +- Non-blocking error handling (don't wipe the form) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Cryptic errors; codes, jargon, or no message at all | +| 1 | Vague errors; "Something went wrong" with no guidance | +| 2 | Clear but unhelpful; names the problem but not the fix | +| 3 | Clear with suggestions; identifies problem and offers next steps | +| 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work | + +##### 10. Help and Documentation + +Even if the system is usable without docs, help should be easy to find, task-focused, and concise. + +**Check for**: +- Searchable help or documentation +- Contextual help (tooltips, inline hints, guided tours) +- Task-focused organization (not feature-organized) +- Concise, scannable content +- Easy access without leaving current context + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | No help available anywhere | +| 1 | Help exists but hard to find or irrelevant | +| 2 | Basic help; FAQ or docs exist, not contextual | +| 3 | Good documentation; searchable, mostly task-focused | +| 4 | Excellent contextual help; right info at the right moment | + +--- + +#### Score Summary + +**Total possible**: 40 points (10 heuristics × 4 max) + +| Score Range | Rating | What It Means | +|-------------|--------|---------------| +| 36–40 | Excellent | Minor polish only; ship it | +| 28–35 | Good | Address weak areas, solid foundation | +| 20–27 | Acceptable | Significant improvements needed before users are happy | +| 12–19 | Poor | Major UX overhaul required; core experience broken | +| 0–11 | Critical | Redesign needed; unusable in current state | + +--- + +#### Issue Severity (P0–P3) + +Tag each individual issue found during scoring with a priority level: + +| Priority | Name | Description | Action | +|----------|------|-------------|--------| +| **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper | +| **P1** | Major | Causes significant difficulty or confusion | Fix before release | +| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass | +| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits | + +**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1. + +--- + +### Persona-Based Design Testing + +Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss. + +**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns. + +--- + +#### 1. Impatient Power User: "Alex" + +**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave. + +**Behaviors**: +- Skips all onboarding and instructions +- Looks for keyboard shortcuts immediately +- Tries to bulk-select, batch-edit, and automate +- Gets frustrated by required steps that feel unnecessary +- Abandons if anything feels slow or patronizing + +**Test Questions**: +- Can Alex complete the core task in under 60 seconds? +- Are there keyboard shortcuts for common actions? +- Can onboarding be skipped entirely? +- Do modals have keyboard dismiss (Esc)? +- Is there a "power user" path (shortcuts, bulk actions)? + +**Red Flags** (report these specifically): +- Forced tutorials or unskippable onboarding +- No keyboard navigation for primary actions +- Slow animations that can't be skipped +- One-item-at-a-time workflows where batch would be natural +- Redundant confirmation steps for low-risk actions + +--- + +#### 2. Confused First-Timer: "Jordan" + +**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out. + +**Behaviors**: +- Reads all instructions carefully +- Hesitates before clicking anything unfamiliar +- Looks for help or support constantly +- Misunderstands jargon and abbreviations +- Takes the most literal interpretation of any label + +**Test Questions**: +- Is the first action obviously clear within 5 seconds? +- Are all icons labeled with text? +- Is there contextual help at decision points? +- Does terminology assume prior knowledge? +- Is there a clear "back" or "undo" at every step? + +**Red Flags** (report these specifically): +- Icon-only navigation with no labels +- Technical jargon without explanation +- No visible help option or guidance +- Ambiguous next steps after completing an action +- No confirmation that an action succeeded + +--- + +#### 3. Accessibility-Dependent User: "Sam" + +**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences. + +**Behaviors**: +- Tabs through the interface linearly +- Relies on ARIA labels and heading structure +- Cannot see hover states or visual-only indicators +- Needs adequate color contrast (4.5:1 minimum) +- May use browser zoom up to 200% + +**Test Questions**: +- Can the entire primary flow be completed keyboard-only? +- Are all interactive elements focusable with visible focus indicators? +- Do images have meaningful alt text? +- Is color contrast WCAG AA compliant (4.5:1 for text)? +- Does the screen reader announce state changes (loading, success, errors)? + +**Red Flags** (report these specifically): +- Click-only interactions with no keyboard alternative +- Missing or invisible focus indicators +- Meaning conveyed by color alone (red = error, green = success) +- Unlabeled form fields or buttons +- Time-limited actions without extension option +- Custom components that break screen reader flow + +--- + +#### 4. Deliberate Stress Tester: "Riley" + +**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience. + +**Behaviors**: +- Tests edge cases intentionally (empty states, long strings, special characters) +- Submits forms with unexpected data (emoji, RTL text, very long values) +- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs +- Looks for inconsistencies between what the UI promises and what actually happens +- Documents problems methodically + +**Test Questions**: +- What happens at the edges (0 items, 1000 items, very long text)? +- Do error states recover gracefully or leave the UI in a broken state? +- What happens on refresh mid-workflow? Is state preserved? +- Are there features that appear to work but produce broken results? +- How does the UI handle unexpected input (emoji, special chars, paste from Excel)? + +**Red Flags** (report these specifically): +- Features that appear to work but silently fail or produce wrong results +- Error handling that exposes technical details or leaves UI in a broken state +- Empty states that show nothing useful ("No results" with no guidance) +- Workflows that lose user data on refresh or navigation +- Inconsistent behavior between similar interactions in different parts of the UI + +--- + +#### 5. Distracted Mobile User: "Casey" + +**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection. + +**Behaviors**: +- Uses thumb only; prefers bottom-of-screen actions +- Gets interrupted mid-flow and returns later +- Switches between apps frequently +- Has limited attention span and low patience +- Types as little as possible, prefers taps and selections + +**Test Questions**: +- Are primary actions in the thumb zone (bottom half of screen)? +- Is state preserved if the user leaves and returns? +- Does it work on slow connections (3G)? +- Can forms use autocomplete and smart defaults? +- Are touch targets at least 44×44pt? + +**Red Flags** (report these specifically): +- Important actions positioned at the top of the screen (unreachable by thumb) +- No state persistence; progress lost on tab switch or interruption +- Large text inputs required where selection would work +- Heavy assets loading on every page (no lazy loading) +- Tiny tap targets or targets too close together + +--- + +#### Selecting Personas + +Choose personas based on the interface type: + +| Interface Type | Primary Personas | Why | +|---------------|-----------------|-----| +| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile | +| Dashboard / admin | Alex, Sam | Power users, accessibility | +| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity | +| Onboarding flow | Jordan, Casey | Confusion, interruption | +| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav | +| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile | + +--- + +#### Project-Specific Personas + +If `.cursorrules` contains a `## Design Context` section (generated by `impeccable init`), derive 1–2 additional personas from the audience and brand information: + +1. Read the target audience description +2. Identify the primary user archetype not covered by the 5 predefined personas +3. Create a persona following this template: + +``` +##### [Role]: "[Name]" + +**Profile**: [2-3 key characteristics derived from Design Context] + +**Behaviors**: [3-4 specific behaviors based on the described audience] + +**Red Flags**: [3-4 things that would alienate this specific user type] +``` + +Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists. diff --git a/.cursor/skills/impeccable/reference/document.md b/.cursor/skills/impeccable/reference/document.md index 254e0183c..60075688a 100644 --- a/.cursor/skills/impeccable/reference/document.md +++ b/.cursor/skills/impeccable/reference/document.md @@ -45,7 +45,7 @@ Rules that matter: - **Token refs** use `{path.to.token}` (e.g. `{colors.primary}`, `{rounded.md}`). Components may reference primitives; primitives may not reference each other. - **Stitch validates colors as hex sRGB only** (`#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`); OKLCH/HSL/P3 trigger a linter warning, not a hard error. YAML accepts the string either way and our own parser is format-agnostic. Choose based on project posture: (a) if the project has an "OKLCH-only" doctrine or uses Display-P3 values that don't round-trip through sRGB, put OKLCH directly in the frontmatter and accept the Stitch linter warning; (b) if the project wants strict Stitch compliance or plans to use their Tailwind/DTCG export pipeline, put hex in the frontmatter and keep OKLCH in prose as the canonical reference. Never split the source of truth without explicit reason. - **Component sub-tokens** are limited to 8 props: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, `width`. Shadows, motion, focus rings, backdrop-filter: none of those fit. Carry them in the sidecar (Step 4b). -- **Scale keys are open-ended.** Use whatever names the project already uses (`warm-ash-cream`, `surface-container-low`). Don't rename to Material defaults. +- **Scale keys are open-ended.** Use whatever names the project already uses (`oxblood-deep`, `surface-container-low`). Don't rename to Material defaults. - **Variants are naming convention, not schema.** `button-primary` / `button-primary-hover` / `button-primary-active` as sibling keys. ## The markdown body: six sections (exact order) @@ -61,7 +61,7 @@ Optional evocative subtitles are allowed in the form `## 2. Colors: The [Name] P ## When to run -- The user just ran `/impeccable teach` and needs the visual side documented. +- The user just ran `/impeccable init` and needs the visual side documented. - The skill noticed no `DESIGN.md` exists and nudged the user to create one. - An existing `DESIGN.md` is stale (the design has drifted). - Before a large redesign, to capture the current state as a reference. @@ -71,7 +71,7 @@ If a `DESIGN.md` already exists, **do not silently overwrite it**. Show the user ## Two paths - **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 teach, nothing built yet). Interview for five high-level answers, write a minimal DESIGN.md marked ``. Re-run in scan mode once there's code. +- **Seed mode**: the project is pre-implementation (fresh init, nothing built yet). Interview for five high-level answers, 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` forces seed mode regardless of code presence. @@ -103,7 +103,7 @@ Build a structured draft from the discovered tokens. For each token class: From the auto-extracted tokens, draft the YAML frontmatter now (you'll write it at the top of DESIGN.md in Step 4). This is the machine-readable layer: what the live panel and Stitch's linter consume. -- **Colors**: one entry per extracted color. Key = descriptive slug (`warm-ash-cream`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. +- **Colors**: one entry per extracted color. Key = descriptive slug (`oxblood-deep`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. - **Typography**: one entry per role (`display`, `headline`, `title`, `body`, `label`). Typography is an object; include only the props that are real for the project (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`). - **Rounded / Spacing**: whatever scale steps the project actually uses, keyed by whatever scale name the project uses (`sm` / `md` / `lg`, or `surface-sm`, or numeric steps). - **Components**: one entry per variant (`button-primary`, `button-primary-hover`, `button-ghost`). Reference primitives via `{colors.X}`, `{rounded.Y}`. If a variant needs a property Stitch's 8-prop set doesn't cover (shadow, focus ring, backdrop-filter), carry the full snippet in the sidecar instead. @@ -253,7 +253,7 @@ Regenerate the sidecar whenever you regenerate root `DESIGN.md`. If the user onl "extensions": { "colorMeta": { "primary": { "role": "primary", "displayName": "Editorial Magenta", "canonical": "oklch(60% 0.25 350)", "tonalRamp": ["...", "...", "..."] }, - "warm-ash-cream": { "role": "neutral", "displayName": "Warm Ash Cream", "canonical": "oklch(96% 0.005 350)", "tonalRamp": ["...", "...", "..."] } + "cool-paper": { "role": "neutral", "displayName": "Cool Paper", "canonical": "oklch(96% 0.005 230)", "tonalRamp": ["...", "...", "..."] } }, "typographyMeta": { "display": { "displayName": "Display", "purpose": "Hero headlines only." } @@ -328,12 +328,13 @@ Pull directly from the DESIGN.md you just wrote: Do not reword. The panel shows these as secondary collapsible context; the same voice that's in the Markdown carries through. -### Step 5: Confirm, refine, and refresh session cache +### Step 5: Confirm and refine 1. Show the user the full DESIGN.md you wrote. Briefly highlight the non-obvious creative choices (descriptive color names, atmosphere language, named rules). 2. Mention that `.impeccable/design.json` was also written alongside; the live panel will now render this project's actual button/input/nav primitives instead of generic approximations. 3. Offer to refine any section: "Want me to revise a section, add component patterns I missed, or adjust the atmosphere language?" -4. **Refresh the session cache.** Run `node .cursor/skills/impeccable/scripts/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. + +Your own write is the freshest source; subsequent commands in this session don't need a reload. ## Seed mode @@ -394,11 +395,12 @@ Per-section guidance in seed mode: 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. -### Step 4: Confirm and refresh session cache +### Step 4: Confirm 1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment). 2. Tell the user: "Re-run `/impeccable document` once you have some code. That pass will extract real tokens and generate the sidecar." -3. Run `node .cursor/skills/impeccable/scripts/load-context.mjs` once so the seed lands in conversation for the rest of the session. + +Your own write is the freshest source; no reload needed. ## Style guidelines diff --git a/.cursor/skills/impeccable/reference/heuristics-scoring.md b/.cursor/skills/impeccable/reference/heuristics-scoring.md deleted file mode 100644 index edbe5028d..000000000 --- a/.cursor/skills/impeccable/reference/heuristics-scoring.md +++ /dev/null @@ -1,234 +0,0 @@ -# Heuristics Scoring Guide - -Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough." - -## Nielsen's 10 Heuristics - -### 1. Visibility of System Status - -Keep users informed about what's happening through timely, appropriate feedback. - -**Check for**: -- Loading indicators during async operations -- Confirmation of user actions (save, submit, delete) -- Progress indicators for multi-step processes -- Current location in navigation (breadcrumbs, active states) -- Form validation feedback (inline, not just on submit) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | No feedback; user is guessing what happened | -| 1 | Rare feedback; most actions produce no visible response | -| 2 | Partial; some states communicated, major gaps remain | -| 3 | Good; most operations give clear feedback, minor gaps | -| 4 | Excellent; every action confirms, progress is always visible | - -### 2. Match Between System and Real World - -Speak the user's language. Follow real-world conventions. Information appears in natural, logical order. - -**Check for**: -- Familiar terminology (no unexplained jargon) -- Logical information order matching user expectations -- Recognizable icons and metaphors -- Domain-appropriate language for the target audience -- Natural reading flow (left-to-right, top-to-bottom priority) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Pure tech jargon, alien to users | -| 1 | Mostly confusing; requires domain expertise to navigate | -| 2 | Mixed; some plain language, some jargon leaks through | -| 3 | Mostly natural; occasional term needs context | -| 4 | Speaks the user's language fluently throughout | - -### 3. User Control and Freedom - -Users need a clear "emergency exit" from unwanted states without extended dialogue. - -**Check for**: -- Undo/redo functionality -- Cancel buttons on forms and modals -- Clear navigation back to safety (home, previous) -- Easy way to clear filters, search, selections -- Escape from long or multi-step processes - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Users get trapped; no way out without refreshing | -| 1 | Difficult exits; must find obscure paths to escape | -| 2 | Some exits; main flows have escape, edge cases don't | -| 3 | Good control; users can exit and undo most actions | -| 4 | Full control; undo, cancel, back, and escape everywhere | - -### 4. Consistency and Standards - -Users shouldn't wonder whether different words, situations, or actions mean the same thing. - -**Check for**: -- Consistent terminology throughout the interface -- Same actions produce same results everywhere -- Platform conventions followed (standard UI patterns) -- Visual consistency (colors, typography, spacing, components) -- Consistent interaction patterns (same gesture = same behavior) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Inconsistent everywhere; feels like different products stitched together | -| 1 | Many inconsistencies; similar things look/behave differently | -| 2 | Partially consistent; main flows match, details diverge | -| 3 | Mostly consistent; occasional deviation, nothing confusing | -| 4 | Fully consistent; cohesive system, predictable behavior | - -### 5. Error Prevention - -Better than good error messages is a design that prevents problems in the first place. - -**Check for**: -- Confirmation before destructive actions (delete, overwrite) -- Constraints preventing invalid input (date pickers, dropdowns) -- Smart defaults that reduce errors -- Clear labels that prevent misunderstanding -- Autosave and draft recovery - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Errors easy to make; no guardrails anywhere | -| 1 | Few safeguards; some inputs validated, most aren't | -| 2 | Partial prevention; common errors caught, edge cases slip | -| 3 | Good prevention; most error paths blocked proactively | -| 4 | Excellent; errors nearly impossible through smart constraints | - -### 6. Recognition Rather Than Recall - -Minimize memory load. Make objects, actions, and options visible or easily retrievable. - -**Check for**: -- Visible options (not buried in hidden menus) -- Contextual help when needed (tooltips, inline hints) -- Recent items and history -- Autocomplete and suggestions -- Labels on icons (not icon-only navigation) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Heavy memorization; users must remember paths and commands | -| 1 | Mostly recall; many hidden features, few visible cues | -| 2 | Some aids; main actions visible, secondary features hidden | -| 3 | Good recognition; most things discoverable, few memory demands | -| 4 | Everything discoverable; users never need to memorize | - -### 7. Flexibility and Efficiency of Use - -Accelerators, invisible to novices, speed up expert interaction. - -**Check for**: -- Keyboard shortcuts for common actions -- Customizable interface elements -- Recent items and favorites -- Bulk/batch actions -- Power user features that don't complicate the basics - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | One rigid path; no shortcuts or alternatives | -| 1 | Limited flexibility; few alternatives to the main path | -| 2 | Some shortcuts; basic keyboard support, limited bulk actions | -| 3 | Good accelerators; keyboard nav, some customization | -| 4 | Highly flexible; multiple paths, power features, customizable | - -### 8. Aesthetic and Minimalist Design - -Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose. - -**Check for**: -- Only necessary information visible at each step -- Clear visual hierarchy directing attention -- Purposeful use of color and emphasis -- No decorative clutter competing for attention -- Focused, uncluttered layouts - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Overwhelming; everything competes for attention equally | -| 1 | Cluttered; too much noise, hard to find what matters | -| 2 | Some clutter; main content clear, periphery noisy | -| 3 | Mostly clean; focused design, minor visual noise | -| 4 | Perfectly minimal; every element earns its pixel | - -### 9. Help Users Recognize, Diagnose, and Recover from Errors - -Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution. - -**Check for**: -- Plain language error messages (no error codes for users) -- Specific problem identification ("Email is missing @" not "Invalid input") -- Actionable recovery suggestions -- Errors displayed near the source of the problem -- Non-blocking error handling (don't wipe the form) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Cryptic errors; codes, jargon, or no message at all | -| 1 | Vague errors; "Something went wrong" with no guidance | -| 2 | Clear but unhelpful; names the problem but not the fix | -| 3 | Clear with suggestions; identifies problem and offers next steps | -| 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work | - -### 10. Help and Documentation - -Even if the system is usable without docs, help should be easy to find, task-focused, and concise. - -**Check for**: -- Searchable help or documentation -- Contextual help (tooltips, inline hints, guided tours) -- Task-focused organization (not feature-organized) -- Concise, scannable content -- Easy access without leaving current context - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | No help available anywhere | -| 1 | Help exists but hard to find or irrelevant | -| 2 | Basic help; FAQ or docs exist, not contextual | -| 3 | Good documentation; searchable, mostly task-focused | -| 4 | Excellent contextual help; right info at the right moment | - ---- - -## Score Summary - -**Total possible**: 40 points (10 heuristics × 4 max) - -| Score Range | Rating | What It Means | -|-------------|--------|---------------| -| 36–40 | Excellent | Minor polish only; ship it | -| 28–35 | Good | Address weak areas, solid foundation | -| 20–27 | Acceptable | Significant improvements needed before users are happy | -| 12–19 | Poor | Major UX overhaul required; core experience broken | -| 0–11 | Critical | Redesign needed; unusable in current state | - ---- - -## Issue Severity (P0–P3) - -Tag each individual issue found during scoring with a priority level: - -| Priority | Name | Description | Action | -|----------|------|-------------|--------| -| **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper | -| **P1** | Major | Causes significant difficulty or confusion | Fix before release | -| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass | -| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits | - -**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1. diff --git a/.cursor/skills/impeccable/reference/teach.md b/.cursor/skills/impeccable/reference/init.md similarity index 58% rename from .cursor/skills/impeccable/reference/teach.md rename to .cursor/skills/impeccable/reference/init.md index c5d09b9f3..498f2d307 100644 --- a/.cursor/skills/impeccable/reference/teach.md +++ b/.cursor/skills/impeccable/reference/init.md @@ -1,21 +1,16 @@ -# Teach Flow +# Init Flow -Gathers design context for a project and writes two complementary files at the project root: +The setup command for a project. One codebase crawl feeds everything it writes: - **PRODUCT.md** (strategic): root project file for register, target users, product purpose, brand personality, anti-references, strategic design principles. Answers "who/what/why". - **DESIGN.md** (visual): root project file for visual theme, color palette, typography, components, layout. Follows the [Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/). Answers "how it looks". +- **`.impeccable/live/config.json`** (live mode): pre-configured so `/impeccable live` boots straight into variant mode with no first-time detour. -Every other impeccable command reads these files before doing any work. +It closes by pointing the user at the best command to run next. Every other impeccable command reads PRODUCT.md and DESIGN.md before doing any work. ## Step 1: Load current state -Run the shared loader first so you know what already exists: - -```bash -node .cursor/skills/impeccable/scripts/load-context.mjs -``` - -The output tells you whether PRODUCT.md and/or DESIGN.md already exist. If `migrated: true`, legacy `.impeccable.md` was auto-renamed to `PRODUCT.md`. Mention this once to the user. +Check what already exists. PRODUCT.md and DESIGN.md live at the project root, or under `.agents/context/` or `docs/` (case-insensitive). Read whichever are present with your native file tool. Also note whether `.impeccable/live/config.json` already exists (Step 6 leaves it untouched if so). Decision tree: - **Neither file exists (empty project or no context yet)**: do Steps 2-4 (write PRODUCT.md), then decide on DESIGN.md based on whether there's code to analyze. @@ -26,14 +21,14 @@ Decision tree: Never silently overwrite an existing file. Always confirm first. -If teach was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete teach, re-run the loader, then resume the original command with the freshly loaded context. For craft, resume into shape next; teach creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief. +If init was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete init, then resume the original command. Your own writes are the freshest source; no reload needed. For craft, resume into shape next; init creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief. ## Step 2: Explore the codebase -Before asking questions, thoroughly scan the project to discover what you can: +Before asking questions, thoroughly scan the project to discover what you can. This single crawl feeds PRODUCT.md, DESIGN.md, **and** the live-mode framework detection in Step 6, so be thorough once rather than re-scanning later: - **README and docs**: Project purpose, target audience, any stated goals -- **Package.json / config files**: Tech stack, dependencies, existing design libraries +- **Package.json / config files**: Tech stack, dependencies, existing design libraries, **and the framework** (Vite/SPA, Next.js, Nuxt, SvelteKit, Astro, multi-page static) plus the HTML entry the browser actually loads - **Existing components**: Current design patterns, spacing, typography in use - **Brand assets**: Logos, favicons, color values already defined - **Design tokens / CSS variables**: Existing color palettes, font stacks, spacing scales @@ -46,7 +41,7 @@ Also form a **register hypothesis** from what you find: Register is a hypothesis at this point, not a decision; Step 3 confirms it. -Note what you've learned and what remains unclear. This exploration feeds both PRODUCT.md and DESIGN.md. +Note what you've learned and what remains unclear. Also note any rough edges worth a follow-up command (thin hierarchy, flat or gray palette, missing error/empty states, dull copy); Step 7 turns these into concrete recommendations without re-analyzing. ## Step 3: Ask strategic questions (for PRODUCT.md) @@ -84,8 +79,7 @@ If the signal is genuinely split (e.g. a product with a big marketing landing), ### Brand & Personality - How would you describe the brand personality in 3 words? - Reference sites or apps that capture the right feel? What specifically about them? - - For brand, push for real-world references in the right lane (tech-minimal, editorial-magazine, consumer-warm, brutalist-grid, etc.), not generic "modern" adjectives. - - For product, push for category best-tool references (Linear, Figma, Notion, Raycast, Stripe). + - Push for specific named references with the *specific* thing about them that fits this brand, not generic "modern" adjectives or category-bucket lanes. - What should this explicitly NOT look like? Any anti-references? ### Accessibility & Inclusion @@ -141,16 +135,38 @@ If the user agrees, delegate to `/impeccable document` (it auto-detects scan vs If the user prefers to skip, mention they can run `/impeccable document` any time later. -## Step 6: Confirm and wrap up +## Step 6: Configure live mode (when code exists) -Summarize: +If the project has code with HTML entries and a dev server (the same "code exists" condition that puts `/impeccable document` in scan mode), pre-configure live mode now. You already identified the framework and the served HTML entry in Step 2, so this is nearly free, and it spares the user the first-time setup detour when they later run `/impeccable live`. + +**Skip this step for empty / pre-implementation projects** (nothing to inject into yet). Tell the user live mode will configure itself the first time they run it once there's code. + +**If `.impeccable/live/config.json` already exists, leave it untouched** and note that live mode is already configured. + +Otherwise: + +1. Write `.impeccable/live/config.json`. Choose `files` (the HTML entries the browser actually loads), `insertBefore`, and `commentSyntax` from the framework table in [live.md](live.md)'s **First-time setup** section, using the framework you found in Step 2. That table is canonical; do not restate it here. For multi-page static sites, prefer a glob (`["public/**/*.html"]`) over a literal list. +2. Run `node .cursor/skills/impeccable/scripts/detect-csp.mjs`. If it reports a patchable shape (`append-arrays` / `append-string`), use the **consent prompt template** from live.md before editing any source file. On decline, skip the patch. For `middleware` / `meta-tag` shapes, surface the detected files and ask the user to add `http://localhost:8400` to `script-src` and `connect-src` manually. For `null`, there's nothing to do. +3. Set `cspChecked: true` in the config once CSP is handled (patched, declined, manual, or not needed). The schema and per-shape patch details live in live.md's First-time setup; follow it rather than duplicating. + +Writing the config file is harmless and needs no consent; only the CSP **source-file patch** requires a yes. + +## Step 7: Recommend starting points, then wrap up + +Summarize tersely: - Register captured (brand / product) -- What was written (PRODUCT.md, DESIGN.md, or both) +- What was written (PRODUCT.md, DESIGN.md, live config, or a subset) - The 3-5 strategic principles from PRODUCT.md that will guide future work -- If DESIGN.md is pending, remind the user how to generate it later +- If DESIGN.md or live config is pending, one line on how to set it up later -**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node .cursor/skills/impeccable/scripts/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. +Then recommend the **best commands to run next**, drawn from what your Step 2 crawl already surfaced. Do not run a fresh analysis here; surface observations you already have. Tailor to register and to what you saw, offer the 2-4 most relevant (not a menu dump), and give the exact command to type. Group by intent: -If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context. +- **Build something new**: `/impeccable craft ` (shape, then build end-to-end) or `/impeccable shape ` (plan first). Lead with this for empty or early-stage projects. +- **Improve what's there**: name the specific surface. `/impeccable critique ` for a scored UX review; `/impeccable audit ` for a11y / perf / responsive checks; `/impeccable polish ` for a pre-ship pass. When the crawl flagged a specific weakness, point the matching command at it: thin hierarchy or spacing → `layout`, flat or gray palette → `colorize`, missing error / empty states → `harden` or `onboard`, dull or unclear copy → `clarify`. +- **Iterate visually**: `/impeccable live` (configured in Step 6) to pick elements in the browser and generate variants in place. + +The full command menu is one bare `/impeccable` away; keep this list short and pointed. + +If init was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now. Your own writes are the freshest source; no reload needed. Optionally ask the user directly to clarify what you cannot infer. Ask whether they'd like a brief summary of PRODUCT.md appended to .cursorrules for easier agent reference. If yes, append a short **Design Context** pointer section there. diff --git a/.cursor/skills/impeccable/reference/interaction-design.md b/.cursor/skills/impeccable/reference/interaction-design.md index 15aed5b29..c42c9d810 100644 --- a/.cursor/skills/impeccable/reference/interaction-design.md +++ b/.cursor/skills/impeccable/reference/interaction-design.md @@ -150,12 +150,6 @@ For browsers without anchor positioning support, `position: fixed` with manual c Check viewport boundaries before rendering. If the dropdown would overflow the bottom edge, flip it above the trigger. If it would overflow the right edge, align it to the trigger's right side instead. -### Anti-Patterns - -- **`position: absolute` inside `overflow: hidden`** - The dropdown will be clipped. Use `position: fixed` or the top layer instead. -- **Arbitrary z-index values** like `z-index: 9999` - Use a semantic z-index scale: `dropdown (100) -> sticky (200) -> modal-backdrop (300) -> modal (400) -> toast (500) -> tooltip (600)`. -- **Rendering dropdown markup inline** without an escape hatch from the parent's stacking context. Either use `popover` (top layer), a portal, or `position: fixed`. - ## Destructive Actions: Undo > Confirm **Undo is better than confirmation dialogs.** Users click through confirmations mindlessly. Remove from UI immediately, show undo toast, actually delete after toast expires. Use confirmation only for truly irreversible actions (account deletion), high-cost actions, or batch operations. diff --git a/.cursor/skills/impeccable/reference/layout.md b/.cursor/skills/impeccable/reference/layout.md index cad4c62a1..8765978b0 100644 --- a/.cursor/skills/impeccable/reference/layout.md +++ b/.cursor/skills/impeccable/reference/layout.md @@ -1,4 +1,4 @@ -Space is the most underused design tool. Find the layout's actual problem (monotone spacing, weak hierarchy, identical card grids, the centered-stack default) and fix the structure, not the surface. +Space is the most underused design tool. Find the layout's actual problem (monotone spacing, weak hierarchy, identical card grids) and fix the structure, not the surface. --- @@ -27,7 +27,6 @@ Analyze what's weak about the current spatial design: 3. **Grid & structure**: - Is there a clear underlying structure, or does the layout feel random? - Are identical card grids used everywhere? (Icon + heading + text, repeated endlessly) - - Is everything centered? (Left-aligned with asymmetric layouts feels more designed, but not a hard and fast rule) 4. **Rhythm & variety**: - Does the layout have visual rhythm? (Alternating tight/generous spacing) @@ -43,8 +42,6 @@ Analyze what's weak about the current spatial design: ## Plan Layout Improvements -Consult the [spatial design reference](spatial-design.md) for detailed guidance on grids, rhythm, and container queries. - Create a systematic plan: - **Spacing system**: Use a consistent scale (a framework's built-in scale like Tailwind's, rem-based tokens, or a custom system). The specific values matter less than consistency. @@ -57,6 +54,7 @@ Create a systematic plan: ### Establish a Spacing System - Use a consistent spacing scale (framework scales like Tailwind, rem-based tokens, or a custom scale all work). What matters is that values come from a defined set, not arbitrary numbers. +- Prefer a 4pt base scale (4, 8, 12, 16, 24, 32, 48, 64, 96px) over 8pt; 8pt is too coarse and you'll frequently need 12px between 8 and 16. - Name tokens semantically if using custom properties: `--space-xs` through `--space-xl`, not `--spacing-8` - Use `gap` for sibling spacing instead of margins; eliminates margin collapse hacks - Apply `clamp()` for fluid spacing that breathes on larger screens @@ -66,15 +64,22 @@ Create a systematic plan: - **Tight grouping** for related elements (8-12px between siblings) - **Generous separation** between distinct sections (48-96px) - **Varied spacing** within sections (not every row needs the same gap) -- **Asymmetric compositions**: break the predictable centered-content pattern when it makes sense +- **Asymmetric compositions**: a deliberate choice when the content invites it (not a default to chase). ### Choose the Right Layout Tool -- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. Flex is simpler and more appropriate for the majority of layout tasks. +- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. - **Use Grid for 2D layouts**: Page-level structure, dashboards, data-dense interfaces, anything where rows AND columns need coordinated control. -- **Don't default to Grid** when Flexbox with `flex-wrap` would be simpler and more flexible. -- Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. - Use named grid areas (`grid-template-areas`) for complex page layouts; redefine at breakpoints. +- Use **container queries** for components, viewport queries for page layouts. A card in a narrow sidebar can stay compact while the same card in a main content area expands automatically: + +```css +.card-container { container-type: inline-size; } +.card { display: grid; gap: var(--space-md); } +@container (min-width: 400px) { + .card { grid-template-columns: 120px 1fr; } +} +``` ### Break Card Grid Monotony @@ -85,18 +90,36 @@ Create a systematic plan: ### Strengthen Visual Hierarchy - Use the fewest dimensions needed for clear hierarchy. Space alone can be enough; generous whitespace around an element draws the eye. Some of the most polished designs achieve rhythm with just space and weight. Add color or size contrast only when simpler means aren't sufficient. +- The best hierarchy combines 2–3 dimensions at once. A heading that's larger, bolder, AND has more space above it reads as primary without trying: + +| Tool | Strong Hierarchy | Weak Hierarchy | +|------|------------------|----------------| +| **Size** | 3:1 ratio or more | <2:1 ratio | +| **Weight** | Bold vs Regular | Medium vs Regular | +| **Color** | High contrast | Similar tones | +| **Position** | Top/left (primary) | Bottom/right | +| **Space** | Surrounded by white space | Crowded | + - Be aware of reading flow: in LTR languages, the eye naturally scans top-left to bottom-right, but primary action placement depends on context (e.g., bottom-right in dialogs, top in navigation). - Create clear content groupings through proximity and separation. ### Manage Depth & Elevation -- Create a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip) - Build a consistent shadow scale (sm → md → lg → xl); shadows should be subtle - Use elevation to reinforce hierarchy, not as decoration ### Optical Adjustments - If an icon looks visually off-center despite being geometrically centered, nudge it. But only if you're confident it actually looks wrong. Don't adjust speculatively. +- Text at `margin-left: 0` looks slightly indented because of letterform whitespace; a negative margin (`-0.05em`) optically aligns it. Geometrically centered glyphs often look off-center (play icons need to shift right, arrows shift toward their direction). +- Touch targets must be 44×44px minimum even when the visual element is smaller. Expand the hit area with padding or a pseudo-element: + +```css +.icon-button { width: 24px; height: 24px; position: relative; } +.icon-button::before { + content: ''; position: absolute; inset: -10px; +} +``` **NEVER**: - Use arbitrary spacing values outside your scale @@ -104,10 +127,7 @@ Create a systematic plan: - Wrap everything in cards (not everything needs a container) - Nest cards inside cards (use spacing and dividers for hierarchy within) - Use identical card grids everywhere (icon + heading + text, repeated) -- Center everything (left-aligned with asymmetry feels more designed) - Default to the hero metric layout (big number, small label, stats, gradient) as a template. If showing real user data, a prominent metric can work, but it should display actual data, not decorative numbers. -- Default to CSS Grid when Flexbox would be simpler; use the simplest tool for the job -- Use arbitrary z-index values (999, 9999); build a semantic scale ## Verify Layout Improvements diff --git a/.cursor/skills/impeccable/reference/live.md b/.cursor/skills/impeccable/reference/live.md index 681a7caa1..9665d0243 100644 --- a/.cursor/skills/impeccable/reference/live.md +++ b/.cursor/skills/impeccable/reference/live.md @@ -9,18 +9,21 @@ A running dev server with hot module replacement (Vite, Next.js, Bun, etc.), OR Execute in order. No step skipped, no step reordered. 1. `live.mjs`: boot. -2. Navigate to the URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). If you can't infer it confidently, tell the user once to open their dev/preview URL. Never use `serverPort` as that URL; it's the helper, not the app. +2. Open the app URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). Never use `serverPort`; it's the helper, not the app. **Cursor:** `browser_navigate` to that URL before polling; do not skip. **Other harnesses:** use the available browser tool; if the URL is uncertain, ask the user once. 3. Poll loop with the default long timeout (600000 ms). After every event or `--reply`, run `live-poll.mjs` again immediately. Never pass a short `--timeout=`. + +The global bar **Impeccable mark** dims and shows a pulsing amber dot when no agent is long-polling `/poll`. Hover the mark for the hint; restart `live-poll.mjs` to reconnect. 4. On `generate`: read screenshot if present; load the action's reference; plan three distinct directions; write all variants in one edit; `--reply done`; poll again. -5. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts remain recoverable until you finish cleanup, run `live-complete.mjs --id EVENT_ID`, and only then poll again. -6. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart. -7. On `exit`: run the cleanup at the bottom. +5. On `steer`: read the message and `pageUrl`; do the work (page edits, navigation help, or a short reply in the `--reply` message); `--reply steer_done`; poll again. No pickup ack. The Steer bar unlocks when `steer_done` arrives over SSE. +6. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts remain recoverable until you finish cleanup, run `live-complete.mjs --id EVENT_ID`, and only then poll again. +7. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart. +8. On `exit`: run the cleanup at the bottom. Harness policy: - **Claude Code**: run the poll as a **background task** (no short timeout). The harness notifies you when it completes, so the main conversation stays free. Do not block the shell. -- **Cursor**: run the poll in the **foreground** (blocking shell; not a background terminal, not a subagent). Cursor background terminals and subagents do not reliably resume the chat with poll stdout. +- **Cursor**: run **one-shot** poll in a **background terminal** with notify on `"type":"(steer|generate|accept|discard|exit)"`. After each event the poll exits; handle it, `--reply`, then start `live-poll.mjs` again. Do **not** use `--stream` on Cursor: incremental stdout notify is slower in practice than exit-based notify (~5s vs sub-second in testing). - **Codex**: run the poll in the **foreground** (blocking shell; not a background task, not a subagent). Codex background exec sessions do not reliably surface poll stdout back into the conversation at the moment events arrive, so a "fire-and-forget" background poll will stall live mode. -- **Other harnesses**: foreground unless you know stdout reliably returns to this session. +- **Other harnesses**: one-shot foreground unless you know stdout reliably returns to this session when a shell exits. Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodies. Spend tokens on tools and edits; on failure, one or two short sentences. @@ -30,7 +33,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi node .cursor/skills/impeccable/scripts/live.mjs ``` -Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation; **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md. +Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation; **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. `serverPort` and `serverToken` belong to the small **Impeccable live helper** HTTP server (serves `/live.js`, SSE, and `/poll`). That port is **not** your dev server and is usually not the URL you open to view the app. The browser page is whatever origin serves one of the `pageFiles` entries (Vite / Next / Bun / tunnel / LAN hostname). @@ -38,12 +41,15 @@ If output is `{ ok: false, error: "config_missing" | "config_invalid", path }`, ## Poll loop +**Default (portable, all harnesses):** + ``` LOOP: node .cursor/skills/impeccable/scripts/live-poll.mjs # default long timeout; no --timeout= Read JSON; dispatch on "type" "generate" → Handle Generate; reply done; LOOP + "steer" → Handle Steer; reply steer_done; LOOP "accept" → Handle Accept; complete carbonize cleanup if required; LOOP "discard" → Handle Discard; LOOP "prefetch" → Handle Prefetch; LOOP @@ -51,6 +57,16 @@ LOOP: "exit" → break → Cleanup ``` +**Stream mode (experimental, not for Cursor):** + +``` +node .cursor/skills/impeccable/scripts/live-poll.mjs --stream # stays running; one JSON line per event + Handle event; run --reply in a separate command + Repeat until "exit" line → Cleanup +``` + +Stream keeps one process alive and waits for `--reply` ack before polling again. Useful only when the harness reads incremental stdout reliably and quickly. **Cursor is not one of those:** background pattern notify on a long-running shell was ~5s to pick up events vs sub-second for one-shot exit notify. Default to one-shot everywhere unless you have measured otherwise. + ## Recovery commands The live helper persists an append-only journal under `.impeccable/live/sessions/`. Browser checkpoints are advisory but durable; the journal is canonical. This is local durable recovery state, not project source. @@ -71,9 +87,32 @@ Server restart rule: start `live-server.mjs` again, then poll. Startup requeues ## Handle `generate` -Event: `{id, action, freeformPrompt?, count, pageUrl, element, screenshotPath?, comments?, strokes?}`. +**Replace mode** (default): `{id, action, freeformPrompt?, count, pageUrl, element, screenshotPath?, comments?, strokes?}`. -Speed matters; the user is watching a spinner. Minimize tool calls by using the `wrap` helper and writing all variants in a single edit. +**Insert mode** (`event.mode === "insert"`): `{id, mode: "insert", count, pageUrl, insert: { position, anchor }, placeholder: { width, height }, freeformPrompt?, screenshotPath?, comments?, strokes?}`. No `action`. Requires a non-empty `freeformPrompt` **or** annotations. Screenshot is sent only when annotations exist (same rule as replace). Use `placeholder` dimensions as a soft size hint for net-new content. + +Speed matters; the user is watching a spinner. Minimize tool calls by using the wrap/insert helper and writing all variants in a single edit. + +### Insert mode branch + +When `event.mode === "insert"`: + +1. Read the screenshot if `event.screenshotPath` is present (annotations only). +2. Run the insert helper instead of wrap: + +```bash +node .cursor/skills/impeccable/scripts/live-insert.mjs --id EVENT_ID --count EVENT_COUNT --position after \ + --element-id "ANCHOR_ID" --classes "class1,class2" --tag "section" --text "ANCHOR_TEXT" +``` + +- `--position` ← `event.insert.position` (`before` | `after`) +- Anchor flags ← `event.insert.anchor` (same mapping as wrap: id, classes, tag, text) + +The scaffold has **no** `data-impeccable-variant="original"`. Variants are net-new HTML+CSS inserted at `insertLine`. Load `brand.md` or `product.md` (freeform only, no action sub-command). Write all variants in one edit, then `--reply done`. + +On accept/discard, `live-accept.mjs` removes the wrapper block; the anchor element is untouched. + +### Replace mode (default) ### 1. Read the screenshot (if present) @@ -424,6 +463,28 @@ A background agent may be used for the rewrite, but the current thread is respon Event: `{id, _acceptResult, _completionAck}`. The poll script already restored the original, removed all variant markers, and acknowledged `discarded` durable completion. Nothing to do unless `_completionAck.ok !== true`; in that case run `live-complete.mjs --id EVENT_ID --discarded`, then poll again. +## Handle `steer` + +Event: `{id, message, pageUrl}`. The user typed or spoke into the global bar **Steer** control: page-level direction without picking an element or launching variant generation. + +The mic button uses the browser **Web Speech API** (MVP): click to start, speak, stop automatically when the utterance ends, then the transcript submits as a steer event. Click again while listening to cancel without submitting. + +This is lighter than `generate`: no screenshot, no element context, no variant cycling. Read `message` and inspect the live page or project files as needed, then either make edits or answer in prose. + +When finished: + +```bash +node .cursor/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID steer_done ["Optional short note for a browser toast"] +``` + +On failure: + +```bash +node .cursor/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID error "Short reason" +``` + +Then poll again immediately. Do not send a separate "picked up" reply. The Steer bar stays locked until `steer_done` or `error` arrives over SSE. + ## Handle `prefetch` Event: `{pageUrl}`. The browser fires this the first time the user selects an element on a given route, as a latency shortcut; it signals the user is likely about to Go on a page you haven't read yet. diff --git a/.cursor/skills/impeccable/reference/motion-design.md b/.cursor/skills/impeccable/reference/motion-design.md deleted file mode 100644 index 78b6fd260..000000000 --- a/.cursor/skills/impeccable/reference/motion-design.md +++ /dev/null @@ -1,109 +0,0 @@ -# Motion Design - -## Duration: The 100/300/500 Rule - -Timing matters more than easing. These durations feel right for most UI: - -| Duration | Use Case | Examples | -|----------|----------|----------| -| **100-150ms** | Instant feedback | Button press, toggle, color change | -| **200-300ms** | State changes | Menu open, tooltip, hover states | -| **300-500ms** | Layout changes | Accordion, modal, drawer | -| **500-800ms** | Entrance animations | Page load, hero reveals | - -**Exit animations are faster than entrances.** Use ~75% of enter duration. - -## Easing: Pick the Right Curve - -**Don't use `ease`.** It's a compromise that's rarely optimal. Instead: - -| Curve | Use For | CSS | -|-------|---------|-----| -| **ease-out** | Elements entering | `cubic-bezier(0.16, 1, 0.3, 1)` | -| **ease-in** | Elements leaving | `cubic-bezier(0.7, 0, 0.84, 0)` | -| **ease-in-out** | State toggles (there → back) | `cubic-bezier(0.65, 0, 0.35, 1)` | - -**For micro-interactions, use exponential curves.** They feel natural because they mimic real physics (friction, deceleration): - -```css -/* Quart out - smooth, refined (recommended default) */ ---ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1); - -/* Quint out - slightly more dramatic */ ---ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1); - -/* Expo out - snappy, confident */ ---ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); -``` - -**Avoid bounce and elastic curves.** They were trendy in 2015 but now feel tacky and amateurish. Real objects don't bounce when they stop; they decelerate smoothly. Overshoot effects draw attention to the animation itself rather than the content. - -## Premium Motion Materials - -Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties: blur reveals, backdrop-filter panels, saturation or brightness shifts, shadow bloom, SVG filters, masks, clip paths, gradient-position movement, and variable font or shader-driven effects. - -Use the right material for the effect: - -- **Transform / opacity**: movement, press feedback, simple reveals, list choreography. -- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances, atmospheric transitions. -- **Clip path / masks**: wipes, reveals, editorial cropping, product-like transitions. -- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state. -- **Grid-template rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly. - -The hard rule is not "transform and opacity only." The hard rule is: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify in-browser that the result is smooth on the target viewports. If blur/filter makes the interaction feel significantly more premium and remains smooth, use it. - -## Staggered Animations - -Use CSS custom properties for cleaner stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"` on each item. **Cap total stagger time**: 10 items at 50ms = 500ms total. For many items, reduce per-item delay or cap staggered count. - -## Reduced Motion - -This is not optional. Vestibular disorders affect ~35% of adults over 40. - -```css -/* Define animations normally */ -.card { - animation: slide-up 500ms ease-out; -} - -/* Provide alternative for reduced motion */ -@media (prefers-reduced-motion: reduce) { - .card { - animation: fade-in 200ms ease-out; /* Crossfade instead of motion */ - } -} - -/* Or disable entirely */ -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - transition-duration: 0.01ms !important; - } -} -``` - -**What to preserve**: Functional animations like progress bars, loading spinners (slowed down), and focus indicators should still work, just without spatial movement. - -## Perceived Performance - -**Nobody cares how fast your site is, just how fast it feels.** Perception can be as effective as actual performance. - -**The 80ms threshold**: Our brains buffer sensory input for ~80ms to synchronize perception. Anything under 80ms feels instant and simultaneous. This is your target for micro-interactions. - -**Active vs passive time**: Passive waiting (staring at a spinner) feels longer than active engagement. Strategies to shift the balance: - -- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening. -- **Early completion**: Show content progressively, don't wait for everything. Video buffering, progressive images, streaming HTML. -- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Instagram likes work offline; the UI updates instantly, syncs later. Use for low-stakes actions; avoid for payments or destructive operations. - -**Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances, but ease-in toward a task's end compresses perceived time. - -**Caution**: Too-fast responses can decrease perceived value. Users may distrust instant results for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening. - -## Performance - -Don't use `will-change` preemptively, only when animation is imminent (`:hover`, `.animating`). For scroll-triggered animations, use Intersection Observer instead of scroll events; unobserve after animating once. Create motion tokens for consistency (durations, easings, common transitions). - ---- - -**Avoid**: Animating everything (animation fatigue is real). Using >500ms for UI feedback. Ignoring `prefers-reduced-motion`. Using animation to hide slow loading. diff --git a/.cursor/skills/impeccable/reference/personas.md b/.cursor/skills/impeccable/reference/personas.md deleted file mode 100644 index d5e49f808..000000000 --- a/.cursor/skills/impeccable/reference/personas.md +++ /dev/null @@ -1,179 +0,0 @@ -# Persona-Based Design Testing - -Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss. - -**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns. - ---- - -## 1. Impatient Power User: "Alex" - - -**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave. - -**Behaviors**: -- Skips all onboarding and instructions -- Looks for keyboard shortcuts immediately -- Tries to bulk-select, batch-edit, and automate -- Gets frustrated by required steps that feel unnecessary -- Abandons if anything feels slow or patronizing - -**Test Questions**: -- Can Alex complete the core task in under 60 seconds? -- Are there keyboard shortcuts for common actions? -- Can onboarding be skipped entirely? -- Do modals have keyboard dismiss (Esc)? -- Is there a "power user" path (shortcuts, bulk actions)? - -**Red Flags** (report these specifically): -- Forced tutorials or unskippable onboarding -- No keyboard navigation for primary actions -- Slow animations that can't be skipped -- One-item-at-a-time workflows where batch would be natural -- Redundant confirmation steps for low-risk actions - ---- - -## 2. Confused First-Timer: "Jordan" - -**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out. - -**Behaviors**: -- Reads all instructions carefully -- Hesitates before clicking anything unfamiliar -- Looks for help or support constantly -- Misunderstands jargon and abbreviations -- Takes the most literal interpretation of any label - -**Test Questions**: -- Is the first action obviously clear within 5 seconds? -- Are all icons labeled with text? -- Is there contextual help at decision points? -- Does terminology assume prior knowledge? -- Is there a clear "back" or "undo" at every step? - -**Red Flags** (report these specifically): -- Icon-only navigation with no labels -- Technical jargon without explanation -- No visible help option or guidance -- Ambiguous next steps after completing an action -- No confirmation that an action succeeded - ---- - -## 3. Accessibility-Dependent User: "Sam" - -**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences. - -**Behaviors**: -- Tabs through the interface linearly -- Relies on ARIA labels and heading structure -- Cannot see hover states or visual-only indicators -- Needs adequate color contrast (4.5:1 minimum) -- May use browser zoom up to 200% - -**Test Questions**: -- Can the entire primary flow be completed keyboard-only? -- Are all interactive elements focusable with visible focus indicators? -- Do images have meaningful alt text? -- Is color contrast WCAG AA compliant (4.5:1 for text)? -- Does the screen reader announce state changes (loading, success, errors)? - -**Red Flags** (report these specifically): -- Click-only interactions with no keyboard alternative -- Missing or invisible focus indicators -- Meaning conveyed by color alone (red = error, green = success) -- Unlabeled form fields or buttons -- Time-limited actions without extension option -- Custom components that break screen reader flow - ---- - -## 4. Deliberate Stress Tester: "Riley" - -**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience. - -**Behaviors**: -- Tests edge cases intentionally (empty states, long strings, special characters) -- Submits forms with unexpected data (emoji, RTL text, very long values) -- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs -- Looks for inconsistencies between what the UI promises and what actually happens -- Documents problems methodically - -**Test Questions**: -- What happens at the edges (0 items, 1000 items, very long text)? -- Do error states recover gracefully or leave the UI in a broken state? -- What happens on refresh mid-workflow? Is state preserved? -- Are there features that appear to work but produce broken results? -- How does the UI handle unexpected input (emoji, special chars, paste from Excel)? - -**Red Flags** (report these specifically): -- Features that appear to work but silently fail or produce wrong results -- Error handling that exposes technical details or leaves UI in a broken state -- Empty states that show nothing useful ("No results" with no guidance) -- Workflows that lose user data on refresh or navigation -- Inconsistent behavior between similar interactions in different parts of the UI - ---- - -## 5. Distracted Mobile User: "Casey" - -**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection. - -**Behaviors**: -- Uses thumb only; prefers bottom-of-screen actions -- Gets interrupted mid-flow and returns later -- Switches between apps frequently -- Has limited attention span and low patience -- Types as little as possible, prefers taps and selections - -**Test Questions**: -- Are primary actions in the thumb zone (bottom half of screen)? -- Is state preserved if the user leaves and returns? -- Does it work on slow connections (3G)? -- Can forms use autocomplete and smart defaults? -- Are touch targets at least 44×44pt? - -**Red Flags** (report these specifically): -- Important actions positioned at the top of the screen (unreachable by thumb) -- No state persistence; progress lost on tab switch or interruption -- Large text inputs required where selection would work -- Heavy assets loading on every page (no lazy loading) -- Tiny tap targets or targets too close together - ---- - -## Selecting Personas - -Choose personas based on the interface type: - -| Interface Type | Primary Personas | Why | -|---------------|-----------------|-----| -| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile | -| Dashboard / admin | Alex, Sam | Power users, accessibility | -| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity | -| Onboarding flow | Jordan, Casey | Confusion, interruption | -| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav | -| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile | - ---- - -## Project-Specific Personas - -If `.cursorrules` contains a `## Design Context` section (generated by `impeccable teach`), derive 1–2 additional personas from the audience and brand information: - -1. Read the target audience description -2. Identify the primary user archetype not covered by the 5 predefined personas -3. Create a persona following this template: - -``` -### [Role]: "[Name]" - -**Profile**: [2-3 key characteristics derived from Design Context] - -**Behaviors**: [3-4 specific behaviors based on the described audience] - -**Red Flags**: [3-4 things that would alienate this specific user type] -``` - -Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists. diff --git a/.cursor/skills/impeccable/reference/polish.md b/.cursor/skills/impeccable/reference/polish.md index 96a410804..da5c541bc 100644 --- a/.cursor/skills/impeccable/reference/polish.md +++ b/.cursor/skills/impeccable/reference/polish.md @@ -91,7 +91,6 @@ Visual polish on a misshapen flow is wasted work. Match the *shape* of the exper - **Theme consistency**: Works in all theme variants - **Color meaning**: Same colors mean same things throughout - **Accessible focus**: Focus indicators visible with sufficient contrast -- **Tinted neutrals**: No pure gray or pure black; add subtle color tint (0.01 chroma) - **Gray on color**: Never put gray text on colored backgrounds; use a shade of that color or transparency ### Interaction States diff --git a/.cursor/skills/impeccable/reference/product.md b/.cursor/skills/impeccable/reference/product.md index 64b3b0169..2ca462350 100644 --- a/.cursor/skills/impeccable/reference/product.md +++ b/.cursor/skills/impeccable/reference/product.md @@ -10,7 +10,6 @@ Product UI's failure mode isn't flatness, it's strangeness without purpose: over ## Typography -- **System fonts are legitimate.** `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif` gives you native feel on every platform. Inter is the common cross-platform default for a reason. - **One family is often right.** Product UIs don't need display/body pairing. A well-tuned sans carries headings, buttons, labels, body, data. - **Fixed rem scale, not fluid.** Clamp-sized headings don't serve product UI. Users view at consistent DPI, and a fluid h1 that shrinks in a sidebar looks worse, not better. - **Tighter scale ratio.** 1.125–1.2 between steps is typical. More type elements here than on brand surfaces; exaggerated contrast creates noise. @@ -26,8 +25,6 @@ Product defaults to Restrained. A single surface can earn Committed (a dashboard ## Layout -- Predictable grids. Consistency IS an affordance; users navigate faster when the structure is expected. -- Familiar patterns are features. Standard navigation (top bar, side nav), breadcrumbs, tabs, and form layouts have established user expectations. Don't reinvent for flavor. - Responsive behavior is structural (collapse sidebar, responsive table, breakpoint-driven columns), not fluid typography. ## Components @@ -51,6 +48,7 @@ Every interactive component has: default, hover, focus, active, disabled, loadin - Display fonts in UI labels, buttons, data. - Reinventing standard affordances for flavor (custom scrollbars, weird form controls, non-standard modals). - Heavy color or full-saturation accents on inactive states. +- Modal as first thought. Modals are usually laziness. Exhaust inline / progressive alternatives first. ## Product permissions diff --git a/.cursor/skills/impeccable/reference/responsive-design.md b/.cursor/skills/impeccable/reference/responsive-design.md deleted file mode 100644 index f079e5e5d..000000000 --- a/.cursor/skills/impeccable/reference/responsive-design.md +++ /dev/null @@ -1,114 +0,0 @@ -# Responsive Design - -## Mobile-First: Write It Right - -Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first. - -## Breakpoints: Content-Driven - -Don't chase device sizes; let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints. - -## Detect Input Method, Not Just Screen Size - -**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard. Use pointer and hover queries: - -```css -/* Fine pointer (mouse, trackpad) */ -@media (pointer: fine) { - .button { padding: 8px 16px; } -} - -/* Coarse pointer (touch, stylus) */ -@media (pointer: coarse) { - .button { padding: 12px 20px; } /* Larger touch target */ -} - -/* Device supports hover */ -@media (hover: hover) { - .card:hover { transform: translateY(-2px); } -} - -/* Device doesn't support hover (touch) */ -@media (hover: none) { - .card { /* No hover state - use active instead */ } -} -``` - -**Critical**: Don't rely on hover for functionality. Touch users can't hover. - -## Safe Areas: Handle the Notch - -Modern phones have notches, rounded corners, and home indicators. Use `env()`: - -```css -body { - padding-top: env(safe-area-inset-top); - padding-bottom: env(safe-area-inset-bottom); - padding-left: env(safe-area-inset-left); - padding-right: env(safe-area-inset-right); -} - -/* With fallback */ -.footer { - padding-bottom: max(1rem, env(safe-area-inset-bottom)); -} -``` - -**Enable viewport-fit** in your meta tag: -```html - -``` - -## Responsive Images: Get It Right - -### srcset with Width Descriptors - -```html -Hero image -``` - -**How it works**: -- `srcset` lists available images with their actual widths (`w` descriptors) -- `sizes` tells the browser how wide the image will display -- Browser picks the best file based on viewport width AND device pixel ratio - -### Picture Element for Art Direction - -When you need different crops/compositions (not just resolutions): - -```html - - - - ... - -``` - -## Layout Adaptation Patterns - -**Navigation**: Three stages: hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `
/` for content that can collapse on mobile. - -## Testing: Don't Trust DevTools Alone - -DevTools device emulation is useful for layout but misses: - -- Actual touch interactions -- Real CPU/memory constraints -- Network latency patterns -- Font rendering differences -- Browser chrome/keyboard appearances - -**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators. - ---- - -**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful. diff --git a/.cursor/skills/impeccable/reference/shape.md b/.cursor/skills/impeccable/reference/shape.md index f7c4cd2f1..592def8a7 100644 --- a/.cursor/skills/impeccable/reference/shape.md +++ b/.cursor/skills/impeccable/reference/shape.md @@ -151,7 +151,7 @@ How users interact with this feature. What happens on click, hover, scroll? What 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., spatial-design.md for complex layouts, motion-design.md for animated features, interaction-design.md for form-heavy features). +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. diff --git a/.cursor/skills/impeccable/reference/spatial-design.md b/.cursor/skills/impeccable/reference/spatial-design.md deleted file mode 100644 index 4d4b83afc..000000000 --- a/.cursor/skills/impeccable/reference/spatial-design.md +++ /dev/null @@ -1,100 +0,0 @@ -# Spatial Design - -## Spacing Systems - -### Use 4pt Base, Not 8pt - -8pt systems are too coarse; you'll frequently need 12px (between 8 and 16). Use 4pt for granularity: 4, 8, 12, 16, 24, 32, 48, 64, 96px. - -### Name Tokens Semantically - -Name by relationship (`--space-sm`, `--space-lg`), not value (`--spacing-8`). Use `gap` instead of margins for sibling spacing; it eliminates margin collapse and cleanup hacks. - -## Grid Systems - -### The Self-Adjusting Grid - -Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. Columns are at least 280px, as many as fit per row, leftovers stretch. For complex layouts, use named grid areas (`grid-template-areas`) and redefine them at breakpoints. - -## Visual Hierarchy - -### The Squint Test - -Blur your eyes (or screenshot and blur). Can you still identify: -- The most important element? -- The second most important? -- Clear groupings? - -If everything looks the same weight blurred, you have a hierarchy problem. - -### Hierarchy Through Multiple Dimensions - -Don't rely on size alone. Combine: - -| Tool | Strong Hierarchy | Weak Hierarchy | -|------|------------------|----------------| -| **Size** | 3:1 ratio or more | <2:1 ratio | -| **Weight** | Bold vs Regular | Medium vs Regular | -| **Color** | High contrast | Similar tones | -| **Position** | Top/left (primary) | Bottom/right | -| **Space** | Surrounded by white space | Crowded | - -**The best hierarchy uses 2-3 dimensions at once**: A heading that's larger, bolder, AND has more space above it. - -### Cards Are Not Required - -Cards are overused. Spacing and alignment create visual grouping naturally. Use cards only when content is truly distinct and actionable, items need visual comparison in a grid, or content needs clear interaction boundaries. **Never nest cards inside cards.** Use spacing, typography, and subtle dividers for hierarchy within a card. - -## Container Queries - -Viewport queries are for page layouts. **Container queries are for components**: - -```css -.card-container { - container-type: inline-size; -} - -.card { - display: grid; - gap: var(--space-md); -} - -/* Card layout changes based on its container, not viewport */ -@container (min-width: 400px) { - .card { - grid-template-columns: 120px 1fr; - } -} -``` - -**Why this matters**: A card in a narrow sidebar stays compact, while the same card in a main content area expands automatically, without viewport hacks. - -## Optical Adjustments - -Text at `margin-left: 0` looks indented due to letterform whitespace; use negative margin (`-0.05em`) to optically align. Geometrically centered icons often look off-center; play icons need to shift right, arrows shift toward their direction. - -### Touch Targets vs Visual Size - -Buttons can look small but need large touch targets (44px minimum). Use padding or pseudo-elements: - -```css -.icon-button { - width: 24px; /* Visual size */ - height: 24px; - position: relative; -} - -.icon-button::before { - content: ''; - position: absolute; - inset: -10px; /* Expand tap target to 44px */ -} -``` - -## Depth & Elevation - -Create semantic z-index scales (dropdown → sticky → modal-backdrop → modal → toast → tooltip) instead of arbitrary numbers. For shadows, create a consistent elevation scale (sm → md → lg → xl). **Key insight**: Shadows should be subtle. If you can clearly see it, it's probably too strong. - ---- - -**Avoid**: Arbitrary spacing values outside your scale. Making all spacing equal (variety creates hierarchy). Creating hierarchy through size alone - combine size, weight, color, and space. diff --git a/.cursor/skills/impeccable/reference/typeset.md b/.cursor/skills/impeccable/reference/typeset.md index dd3e67ea3..5f5e0c2c0 100644 --- a/.cursor/skills/impeccable/reference/typeset.md +++ b/.cursor/skills/impeccable/reference/typeset.md @@ -4,7 +4,7 @@ Typography carries most of the information on the page. Replace generic defaults ## Register -Brand: run the font selection procedure in [brand.md](brand.md). Pairing follows the brand's lane (display serif + sans body for editorial/luxury, one committed sans for tech, etc.). Fluid `clamp()` scale, ≥1.25 ratio between steps. +Brand: run the font selection procedure in [brand.md](brand.md). Fluid `clamp()` scale, ≥1.25 ratio between steps. Product: system fonts and familiar sans stacks are legitimate here. One well-tuned family typically carries the whole UI. Fixed `rem` scale, 1.125–1.2 ratio between more closely-spaced steps. @@ -43,7 +43,7 @@ Analyze what's weak or generic about the current type: ## Plan Typography Improvements -Consult the [typography reference](typography.md) for detailed guidance on scales, pairing, and loading strategies. +Consult the [Reference Material](#reference-material) section below for detailed guidance on scales, pairing, and loading strategies. Create a systematic plan: @@ -122,3 +122,158 @@ Each variant MUST declare a `scale` param controlling the hierarchy ratio. Expre Where the variant riffs on a specific pairing, expose the pairing choice as a `steps` param (e.g. "serif display + sans body" vs. "mono display + sans body" vs. "all-sans"). Each branch routes through `:scope[data-p-pairing="X"]` selectors in scoped CSS. See `reference/live.md` for the full params contract. + +--- + +## Reference Material + +The sections below were previously `typography.md` and live inline now so the typeset flow has its deep typography reference in one place. `bolder.md` also references this section. + +### Typography + +#### Classic Typography Principles + +##### Vertical Rhythm + +Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony; text and space share a mathematical foundation. + +##### Modular Scale & Hierarchy + +The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy. + +**Use fewer sizes with more contrast.** A 5-size system covers most needs: + +| Role | Typical Ratio | Use Case | +|------|---------------|----------| +| xs | 0.75rem | Captions, legal | +| sm | 0.875rem | Secondary UI, metadata | +| base | 1rem | Body text | +| lg | 1.25-1.5rem | Subheadings, lead text | +| xl+ | 2-4rem | Headlines, hero text | + +Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit. + +##### Readability & Measure + +Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length: narrow columns need tighter leading, wide columns need more. + +**Non-obvious**: Light text on dark backgrounds needs compensation on three axes, not just one. Bump line-height by 0.05–0.1, add a touch of letter-spacing (0.01–0.02em), and optionally step the body weight up one notch (regular → medium). The perceived weight drops across all three; fix all three. + +**Paragraph rhythm**: Pick either space between paragraphs OR first-line indentation. Never both. Digital usually wants space; editorial/long-form can justify indent-only. + +#### Font Selection & Pairing + +The tactical selection procedure and the reflex-reject list live in [reference/brand.md](brand.md) under **Font selection procedure** and **Reflex-reject list** (loaded for brand-register tasks). The rest of this section covers the adjacent knowledge: anti-reflex corrections, system font use, and pairing rules. + +##### Anti-reflexes worth defending against + +- A technical/utilitarian brief does NOT need a serif "for warmth." Most tech tools should look like tech tools. +- An editorial/premium brief does NOT need the same expressive serif everyone is using right now. Premium can be Swiss-modern, can be neo-grotesque, can be a literal monospace, can be a quiet humanist sans. +- A children's product does NOT need a rounded display font. Kids' books use real type. +- A "modern" brief does NOT need a geometric sans. The most modern thing you can do is not use the font everyone else is using. + +**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality. + +##### Pairing Principles + +**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif). + +When pairing, contrast on multiple axes: +- Serif + Sans (structure contrast) +- Geometric + Humanist (personality contrast) +- Condensed display + Wide body (proportion contrast) + +##### Web Font Loading + +The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix: + +```css +/* 1. Use font-display: swap for visibility */ +@font-face { + font-family: 'CustomFont'; + src: url('font.woff2') format('woff2'); + font-display: swap; +} + +/* 2. Match fallback metrics to minimize shift */ +@font-face { + font-family: 'CustomFont-Fallback'; + src: local('Arial'); + size-adjust: 105%; /* Scale to match x-height */ + ascent-override: 90%; /* Match ascender height */ + descent-override: 20%; /* Match descender depth */ + line-gap-override: 10%; /* Match line spacing */ +} + +body { + font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; +} +``` + +Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically. + +**`swap` vs `optional`**: `swap` shows fallback text immediately and FOUT-swaps when the web font arrives. `optional` uses the fallback if the web font misses a small load budget (~100ms) and avoids the shift entirely. Pick `optional` when zero layout shift matters more than seeing the branded font on slow networks. + +**Preload the critical weight only**: typically the regular-weight body font used above the fold. Preloading every weight costs more bandwidth than it saves. + +**Variable fonts for 3+ weights or styles**: a single variable font file is usually smaller than three static weight files, gives fractional weight control, and pairs well with `font-optical-sizing: auto`. For 1–2 weights, static is fine. + +#### Modern Web Typography + +##### Fluid Type + +Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate (higher vw = faster scaling). Add a rem offset so it doesn't collapse to 0 on small screens. + +**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes. + +**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI; fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it. + +**Bound your clamp()**: keep `max-size ≤ ~2.5 × min-size`. Wider ratios break the browser's zoom and reflow behaviour and make large viewports feel like the page is shouting. + +**Scale container width and font-size together** so effective character measure stays in the 45–75ch band at every viewport. A heading that widens faster than its container drifts out of the comfortable measure at the top end. + +##### OpenType Features + +Most developers don't know these exist. Use them for polish: + +```css +/* Proper fractions */ +.recipe-amount { font-variant-numeric: diagonal-fractions; } + +/* Small caps for abbreviations */ +abbr { font-variant-caps: all-small-caps; } + +/* Disable ligatures in code */ +code { font-variant-ligatures: none; } + +/* Enable kerning (usually on by default, but be explicit) */ +body { font-kerning: normal; } +``` + +Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/). + +##### Rendering polish + +```css +/* Variable fonts: pick the right optical-size master automatically */ +body { font-optical-sizing: auto; } +``` + +**ALL-CAPS tracking**: capitals sit too close at default spacing. Add 5–12% letter-spacing (`letter-spacing: 0.05em` to `0.12em`) to short all-caps labels, eyebrows, and small headings. Real small caps (via `font-variant-caps`) need the same treatment, slightly gentler. + +#### Typography System Architecture + +Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system. + +#### Accessibility Considerations + +Beyond contrast ratios (which are well-documented), consider: + +- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout. +- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text. +- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile. +- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets. + +--- + +**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text. diff --git a/.cursor/skills/impeccable/reference/typography.md b/.cursor/skills/impeccable/reference/typography.md deleted file mode 100644 index ed5ca272a..000000000 --- a/.cursor/skills/impeccable/reference/typography.md +++ /dev/null @@ -1,159 +0,0 @@ -# Typography - -## Classic Typography Principles - -### Vertical Rhythm - -Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony; text and space share a mathematical foundation. - -### Modular Scale & Hierarchy - -The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy. - -**Use fewer sizes with more contrast.** A 5-size system covers most needs: - -| Role | Typical Ratio | Use Case | -|------|---------------|----------| -| xs | 0.75rem | Captions, legal | -| sm | 0.875rem | Secondary UI, metadata | -| base | 1rem | Body text | -| lg | 1.25-1.5rem | Subheadings, lead text | -| xl+ | 2-4rem | Headlines, hero text | - -Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit. - -### Readability & Measure - -Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length: narrow columns need tighter leading, wide columns need more. - -**Non-obvious**: Light text on dark backgrounds needs compensation on three axes, not just one. Bump line-height by 0.05–0.1, add a touch of letter-spacing (0.01–0.02em), and optionally step the body weight up one notch (regular → medium). The perceived weight drops across all three; fix all three. - -**Paragraph rhythm**: Pick either space between paragraphs OR first-line indentation. Never both. Digital usually wants space; editorial/long-form can justify indent-only. - -## Font Selection & Pairing - -The tactical selection procedure and the reflex-reject list live in [reference/brand.md](brand.md) under **Font selection procedure** and **Reflex-reject list** (loaded for brand-register tasks). The rest of this section covers the adjacent knowledge: anti-reflex corrections, system font use, and pairing rules. - -### Anti-reflexes worth defending against - -- A technical/utilitarian brief does NOT need a serif "for warmth." Most tech tools should look like tech tools. -- An editorial/premium brief does NOT need the same expressive serif everyone is using right now. Premium can be Swiss-modern, can be neo-grotesque, can be a literal monospace, can be a quiet humanist sans. -- A children's product does NOT need a rounded display font. Kids' books use real type. -- A "modern" brief does NOT need a geometric sans. The most modern thing you can do is not use the font everyone else is using. - -**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality. - -### Pairing Principles - -**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif). - -When pairing, contrast on multiple axes: -- Serif + Sans (structure contrast) -- Geometric + Humanist (personality contrast) -- Condensed display + Wide body (proportion contrast) - -**Never pair fonts that are similar but not identical** (e.g., two geometric sans-serifs). They create visual tension without clear hierarchy. - -### Web Font Loading - -The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix: - -```css -/* 1. Use font-display: swap for visibility */ -@font-face { - font-family: 'CustomFont'; - src: url('font.woff2') format('woff2'); - font-display: swap; -} - -/* 2. Match fallback metrics to minimize shift */ -@font-face { - font-family: 'CustomFont-Fallback'; - src: local('Arial'); - size-adjust: 105%; /* Scale to match x-height */ - ascent-override: 90%; /* Match ascender height */ - descent-override: 20%; /* Match descender depth */ - line-gap-override: 10%; /* Match line spacing */ -} - -body { - font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; -} -``` - -Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically. - -**`swap` vs `optional`**: `swap` shows fallback text immediately and FOUT-swaps when the web font arrives. `optional` uses the fallback if the web font misses a small load budget (~100ms) and avoids the shift entirely. Pick `optional` when zero layout shift matters more than seeing the branded font on slow networks. - -**Preload the critical weight only**: typically the regular-weight body font used above the fold. Preloading every weight costs more bandwidth than it saves. - -**Variable fonts for 3+ weights or styles**: a single variable font file is usually smaller than three static weight files, gives fractional weight control, and pairs well with `font-optical-sizing: auto`. For 1–2 weights, static is fine. - -## Modern Web Typography - -### Fluid Type - -Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate (higher vw = faster scaling). Add a rem offset so it doesn't collapse to 0 on small screens. - -**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes. - -**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI; fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it. - -**Bound your clamp()**: keep `max-size ≤ ~2.5 × min-size`. Wider ratios break the browser's zoom and reflow behaviour and make large viewports feel like the page is shouting. - -**Scale container width and font-size together** so effective character measure stays in the 45–75ch band at every viewport. A heading that widens faster than its container drifts out of the comfortable measure at the top end. - -### OpenType Features - -Most developers don't know these exist. Use them for polish: - -```css -/* Tabular numbers for data alignment */ -.data-table { font-variant-numeric: tabular-nums; } - -/* Proper fractions */ -.recipe-amount { font-variant-numeric: diagonal-fractions; } - -/* Small caps for abbreviations */ -abbr { font-variant-caps: all-small-caps; } - -/* Disable ligatures in code */ -code { font-variant-ligatures: none; } - -/* Enable kerning (usually on by default, but be explicit) */ -body { font-kerning: normal; } -``` - -Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/). - -### Rendering polish - -```css -/* Even out heading line lengths (browser picks better break points) */ -h1, h2, h3 { text-wrap: balance; } - -/* Reduce orphans and ragged endings in long prose */ -article p { text-wrap: pretty; } - -/* Variable fonts: pick the right optical-size master automatically */ -body { font-optical-sizing: auto; } -``` - -**ALL-CAPS tracking**: capitals sit too close at default spacing. Add 5–12% letter-spacing (`letter-spacing: 0.05em` to `0.12em`) to short all-caps labels, eyebrows, and small headings. Real small caps (via `font-variant-caps`) need the same treatment, slightly gentler. - -## Typography System Architecture - -Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system. - -## Accessibility Considerations - -Beyond contrast ratios (which are well-documented), consider: - -- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout. -- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text. -- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile. -- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets. - ---- - -**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text. diff --git a/.cursor/skills/impeccable/reference/ux-writing.md b/.cursor/skills/impeccable/reference/ux-writing.md deleted file mode 100644 index 417b5f56c..000000000 --- a/.cursor/skills/impeccable/reference/ux-writing.md +++ /dev/null @@ -1,107 +0,0 @@ -# UX Writing - -## The Button Label Problem - -**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns: - -| Bad | Good | Why | -|-----|------|-----| -| OK | Save changes | Says what will happen | -| Submit | Create account | Outcome-focused | -| Yes | Delete message | Confirms the action | -| Cancel | Keep editing | Clarifies what "cancel" means | -| Click here | Download PDF | Describes the destination | - -**For destructive actions**, name the destruction: -- "Delete" not "Remove" (delete is permanent, remove implies recoverable) -- "Delete 5 items" not "Delete selected" (show the count) - -## Error Messages: The Formula - -Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input". - -### Error Message Templates - -| Situation | Template | -|-----------|----------| -| **Format error** | "[Field] needs to be [format]. Example: [example]" | -| **Missing required** | "Please enter [what's missing]" | -| **Permission denied** | "You don't have access to [thing]. [What to do instead]" | -| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." | -| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" | - -### Don't Blame the User - -Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date". - -## Empty States Are Opportunities - -Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items". - -## Voice vs Tone - -**Voice** is your brand's personality, consistent everywhere. -**Tone** adapts to the moment. - -| Moment | Tone Shift | -|--------|------------| -| Success | Celebratory, brief: "Done! Your changes are live." | -| Error | Empathetic, helpful: "That didn't work. Here's what to try..." | -| Loading | Reassuring: "Saving your work..." | -| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | - -**Never use humor for errors.** Users are already frustrated. Be helpful, not cute. - -## Writing for Accessibility - -**Link text** must have standalone meaning: "View pricing plans" not "Click here". **Alt text** describes information, not the image: "Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context. - -## Writing for Translation - -### Plan for Expansion - -German text is ~30% longer than English. Allocate space: - -| Language | Expansion | -|----------|-----------| -| German | +30% | -| French | +20% | -| Finnish | +30-40% | -| Chinese | -30% (fewer chars, but same width) | - -### Translation-Friendly Patterns - -Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear. - -## Consistency: The Terminology Problem - -Pick one term and stick with it: - -| Inconsistent | Consistent | -|--------------|------------| -| Delete / Remove / Trash | Delete | -| Settings / Preferences / Options | Settings | -| Sign in / Log in / Enter | Sign in | -| Create / Add / New | Create | - -Build a terminology glossary and enforce it. Variety creates confusion. - -## Avoid Redundant Copy - -If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well. - -## Loading States - -Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress. - -## Confirmation Dialogs: Use Sparingly - -Most confirmation dialogs are design failures; consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No"). - -## Form Instructions - -Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking. - ---- - -**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors. diff --git a/.cursor/skills/impeccable/scripts/cleanup-deprecated.mjs b/.cursor/skills/impeccable/scripts/cleanup-deprecated.mjs index 6aee4710a..bc6400e90 100644 --- a/.cursor/skills/impeccable/scripts/cleanup-deprecated.mjs +++ b/.cursor/skills/impeccable/scripts/cleanup-deprecated.mjs @@ -25,7 +25,7 @@ import { join, resolve } from 'node:path'; const DEPRECATED_NAMES = [ // v2.0 renames 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable teach + 'teach-impeccable', // folded into /impeccable init // v2.1 merges 'arrange', // renamed to layout 'normalize', // merged into polish diff --git a/.cursor/skills/impeccable/scripts/command-metadata.json b/.cursor/skills/impeccable/scripts/command-metadata.json index 1488bd5f1..83796d54b 100644 --- a/.cursor/skills/impeccable/scripts/command-metadata.json +++ b/.cursor/skills/impeccable/scripts/command-metadata.json @@ -3,8 +3,8 @@ "description": "Full confirmed-brief-then-build flow. Runs multi-round shape discovery first, resolves visual probe and north-star mock gates when available, then builds and visually iterates. Use when building a new feature end-to-end.", "argumentHint": "[feature description]" }, - "teach": { - "description": "Gathers design context for a project. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.", + "init": { + "description": "Sets up a project for impeccable. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles); offers DESIGN.md (visual: colors, typography, components) when code exists; pre-configures live mode; then recommends the best commands to run next. Every other command reads these files before doing work. Use once per project.", "argumentHint": "" }, "document": { diff --git a/.cursor/skills/impeccable/scripts/context.mjs b/.cursor/skills/impeccable/scripts/context.mjs new file mode 100644 index 000000000..52263c883 --- /dev/null +++ b/.cursor/skills/impeccable/scripts/context.mjs @@ -0,0 +1,253 @@ +/** + * Context loader: prints PRODUCT.md (and DESIGN.md if present) as one + * markdown block on stdout, or exits with empty stdout when no PRODUCT.md + * is found anywhere. The skill keys off "empty stdout" to branch into the + * init flow. + * + * Path resolution (first match wins): + * 1. cwd, if PRODUCT.md or DESIGN.md is there + * 2. .agents/context/ then docs/ + * 3. $IMPECCABLE_CONTEXT_DIR (absolute or cwd-relative) — power-user + * escape hatch, only consulted when defaults are empty + * 4. cwd as a "nothing found" default + * + * `resolveContextDir()` and `loadContext()` are also exported for the + * server-side scripts (live.mjs, live-server.mjs) that need the structured + * shape rather than the markdown block. + */ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; +const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; +const FALLBACK_DIRS = ['.agents/context', 'docs']; + +// ─── Update check ────────────────────────────────────────────────────────── +// Piggyback a lightweight skill-version check on the once-per-session boot. +// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent +// can offer `npx impeccable skills update`. Everything here is best-effort and +// silent on failure: a network problem, sandbox, or missing cache must never +// block context output or print an error. + +const UPDATE_HOST = (process.env.IMPECCABLE_UPDATE_HOST || 'https://impeccable.style').replace(/\/$/, ''); +const UPDATE_CACHE_PATH = + process.env.IMPECCABLE_UPDATE_CACHE || path.join(os.homedir(), '.impeccable', 'update-check.json'); +const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // throttle the network poll to once a day +const RENOTIFY_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000; // don't re-surface the same version for a week +const FETCH_TIMEOUT_MS = 1200; + +export function resolveContextDir(cwd = process.cwd()) { + if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { + return cwd; + } + for (const rel of FALLBACK_DIRS) { + const candidate = path.resolve(cwd, rel); + if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { + return candidate; + } + } + const envDir = process.env.IMPECCABLE_CONTEXT_DIR; + if (envDir && envDir.trim()) { + const trimmed = envDir.trim(); + return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); + } + return cwd; +} + +export function loadContext(cwd = process.cwd()) { + const contextDir = resolveContextDir(cwd); + const productPath = firstExisting(contextDir, PRODUCT_NAMES); + const designPath = firstExisting(contextDir, DESIGN_NAMES); + const product = productPath ? safeRead(productPath) : null; + const design = designPath ? safeRead(designPath) : null; + return { + hasProduct: !!product, + product, + productPath: productPath ? path.relative(cwd, productPath) : null, + hasDesign: !!design, + design, + designPath: designPath ? path.relative(cwd, designPath) : null, + contextDir, + }; +} + +function firstExisting(dir, names) { + for (const name of names) { + const abs = path.join(dir, name); + if (fs.existsSync(abs)) return abs; + } + return null; +} + +function safeRead(p) { + try { + return fs.readFileSync(p, 'utf-8'); + } catch { + return null; + } +} + +/** + * Pull the register (`brand` or `product`) out of PRODUCT.md by looking + * for a `## Register` section and reading the first non-empty line that + * follows it. Returns null when the file is legacy / register-less. + */ +function extractRegister(product) { + if (!product) return null; + const lines = product.split('\n'); + for (let i = 0; i < lines.length; i++) { + if (/^##\s+Register\b/i.test(lines[i].trim())) { + for (let j = i + 1; j < lines.length; j++) { + const next = lines[j].trim(); + if (!next) continue; + const word = next.toLowerCase(); + if (word === 'brand' || word === 'product') return word; + return null; + } + } + } + return null; +} + +/** + * Read the installed skill's own version from the sibling SKILL.md frontmatter + * (this file lives at `/scripts/context.mjs`). Returns null when the + * frontmatter is missing or unreadable. + */ +function readLocalSkillVersion() { + try { + const here = path.dirname(fileURLToPath(import.meta.url)); + const skillMd = path.join(here, '..', 'SKILL.md'); + const content = fs.readFileSync(skillMd, 'utf-8'); + const match = content.match(/^version:\s*(.+)$/m); + return match ? match[1].trim().replace(/^["']|["']$/g, '') : null; + } catch { + return null; + } +} + +function readUpdateCache() { + try { + return JSON.parse(fs.readFileSync(UPDATE_CACHE_PATH, 'utf-8')); + } catch { + return {}; + } +} + +function writeUpdateCache(cache) { + try { + fs.mkdirSync(path.dirname(UPDATE_CACHE_PATH), { recursive: true }); + fs.writeFileSync(UPDATE_CACHE_PATH, JSON.stringify(cache)); + } catch { + // Best-effort: a read-only home dir just means we re-poll next session. + } +} + +/** Compare dotted numeric versions. Returns >0 when a is newer than b. */ +function compareSemver(a, b) { + const pa = String(a).split('.').map(n => parseInt(n, 10) || 0); + const pb = String(b).split('.').map(n => parseInt(n, 10) || 0); + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + const diff = (pa[i] || 0) - (pb[i] || 0); + if (diff !== 0) return diff; + } + return 0; +} + +async function fetchLatestSkillVersion() { + try { + const res = await fetch(`${UPDATE_HOST}/api/version`, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }); + if (!res.ok) return null; + const data = await res.json(); + return typeof data?.skills === 'string' ? data.skills : null; + } catch { + return null; // offline, sandboxed, timed out, or bad JSON: all non-fatal + } +} + +function buildUpdateDirective(localVersion, latestVersion) { + return ( + `UPDATE_AVAILABLE: A newer Impeccable skill is available ` + + `(installed v${localVersion}, latest v${latestVersion}). ` + + `Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` + + `Update now? It runs \`npx impeccable skills update\`." ` + + `If they agree, run \`npx impeccable skills update\` (the update applies to the next session, not this one). ` + + `Either way, continue the current task without waiting, and do not raise this again.` + ); +} + +/** + * Best-effort update directive for the boot output. Returns a string to append + * or null. Polls the version endpoint at most once per day (cached globally in + * the user's home dir) and re-surfaces a given version at most once per week so + * the agent never nags. Opt out entirely with IMPECCABLE_NO_UPDATE_CHECK=1. + */ +async function computeUpdateDirective(now = Date.now()) { + try { + if (process.env.IMPECCABLE_NO_UPDATE_CHECK) return null; + const localVersion = readLocalSkillVersion(); + if (!localVersion) return null; + + const cache = readUpdateCache(); + + // Poll the network only when the throttle window has elapsed. Stamp + // lastCheck even on failure so an offline machine doesn't poll every boot. + if (!cache.lastCheck || now - cache.lastCheck > CHECK_INTERVAL_MS) { + const latest = await fetchLatestSkillVersion(); + cache.lastCheck = now; + if (latest) cache.latestVersion = latest; + writeUpdateCache(cache); + } + + const latest = cache.latestVersion; + if (!latest || compareSemver(latest, localVersion) <= 0) return null; + + // Anti-nag: surface a given version at most once per RENOTIFY window. + if (cache.notifiedVersion === latest && cache.notifiedAt && now - cache.notifiedAt < RENOTIFY_INTERVAL_MS) { + return null; + } + cache.notifiedVersion = latest; + cache.notifiedAt = now; + writeUpdateCache(cache); + + return buildUpdateDirective(localVersion, latest); + } catch { + return null; + } +} + +async function cli() { + const ctx = loadContext(process.cwd()); + const updateDirective = await computeUpdateDirective(); + + if (!ctx.hasProduct) { + // Direct stdout message instead of relying on empty output as a signal + // — cheap models miss the empty case more often than the explicit one. + const parts = [ + 'NO_PRODUCT_MD: This project has no PRODUCT.md yet. ' + + 'Stop the current task, load reference/init.md, and follow its ' + + 'instructions to write PRODUCT.md before resuming.', + ]; + if (updateDirective) parts.push(updateDirective); + process.stdout.write(parts.join('\n\n---\n\n') + '\n'); + process.exit(0); + } + const parts = [`# PRODUCT.md\n\n${ctx.product.trim()}`]; + if (ctx.hasDesign) { + parts.push(`# DESIGN.md\n\n${ctx.design.trim()}`); + } + const register = extractRegister(ctx.product); + const next = register + ? `NEXT STEP: This project's register is \`${register}\`. You MUST now read \`reference/${register}.md\` before producing any design output.` + : `NEXT STEP: You MUST now read the matching register reference (\`reference/brand.md\` or \`reference/product.md\`) before producing any design output. Pick based on PRODUCT.md above.`; + parts.push(next); + if (updateDirective) parts.push(updateDirective); + process.stdout.write(parts.join('\n\n---\n\n') + '\n'); +} + +const _running = process.argv[1]; +if (_running?.endsWith('context.mjs') || _running?.endsWith('context.mjs/')) { + cli(); +} diff --git a/.cursor/skills/impeccable/scripts/design-parser.mjs b/.cursor/skills/impeccable/scripts/design-parser.mjs index b68756640..82a5cd7dc 100644 --- a/.cursor/skills/impeccable/scripts/design-parser.mjs +++ b/.cursor/skills/impeccable/scripts/design-parser.mjs @@ -63,7 +63,7 @@ function parseYamlSubset(yaml) { } const key = content.slice(0, colonIdx).trim(); - const rest = content.slice(colonIdx + 1).trim(); + const rest = stripInlineYamlComment(content.slice(colonIdx + 1).trim()); const parent = stack[stack.length - 1].obj; if (rest === '') { @@ -93,6 +93,21 @@ function findTopLevelColon(s) { return -1; } +function stripInlineYamlComment(s) { + let inQuote = null; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (inQuote) { + if (ch === inQuote && s[i - 1] !== '\\') inQuote = null; + } else if (ch === '"' || ch === "'") { + inQuote = ch; + } else if (ch === '#' && i > 0 && /\s/.test(s[i - 1])) { + return s.slice(0, i).trimEnd(); + } + } + return s; +} + function parseScalar(raw) { const s = raw.trim(); if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { diff --git a/.cursor/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.cursor/skills/impeccable/scripts/detector/browser/injected/index.mjs index c03a69caf..ceff16737 100644 --- a/.cursor/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/.cursor/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -9,8 +9,21 @@ if (IS_BROWSER) { const EXTENSION_MODE = (_myScript && _myScript.dataset.impeccableExtension === 'true') || document.documentElement.dataset.impeccableExtension === 'true'; - const BRAND_COLOR = 'oklch(55% 0.25 350)'; - const BRAND_COLOR_HOVER = 'oklch(45% 0.25 350)'; + // Kinpaku gold — pinned to the site's brand token (see + // site/styles/kinpaku-tokens.css --ks-kinpaku). Keep this in sync with + // the picker's C.brand in skill/scripts/live-browser.js and the kit's + // picker section in site/styles/kinpaku-kit.css. + // + // One color across both light and dark host pages. The outline is a + // 2px gesture pointing at an element + a labeled tag — it's a marker, + // not body text, so it doesn't need WCAG AA against the page. The + // label text inside the gold tag is dark (LABEL_INK) which has ~16:1 + // against the leaf gold, so reading the rule name is solid in both + // modes. Hover deepens the gold (preserves chroma — never drops it, + // dropping chroma washes the gold into a sand/olive tone). + const BRAND_COLOR = 'oklch(84% 0.19 80.46)'; + const BRAND_COLOR_HOVER = 'oklch(74% 0.18 80)'; + const LABEL_INK = 'oklch(4% 0.004 95)'; const LABEL_BG = BRAND_COLOR; const OUTLINE_COLOR = BRAND_COLOR; @@ -278,7 +291,7 @@ if (IS_BROWSER) { display: 'flex', alignItems: 'center', whiteSpace: 'nowrap', fontSize: '11px', fontWeight: '600', letterSpacing: '0.02em', - color: 'white', lineHeight: '14px', + color: LABEL_INK, lineHeight: '14px', background: LABEL_BG, fontFamily: 'system-ui, sans-serif', borderRadius: '4px 4px 0 0', @@ -398,7 +411,7 @@ if (IS_BROWSER) { banner.className = 'impeccable-overlay impeccable-banner'; Object.assign(banner.style, { position: 'fixed', top: '0', left: '0', right: '0', zIndex: '100000', - background: LABEL_BG, color: 'white', + background: LABEL_BG, color: LABEL_INK, fontFamily: 'system-ui, sans-serif', fontSize: '13px', display: 'flex', alignItems: 'center', pointerEvents: 'auto', height: '36px', overflow: 'hidden', maxWidth: '100vw', @@ -1223,12 +1236,12 @@ if (IS_BROWSER) { } console.group( `%c[impeccable] ${allFindings.length} anti-pattern${allFindings.length === 1 ? '' : 's'} found`, - 'color: oklch(60% 0.25 350); font-weight: bold' + 'color: oklch(84% 0.19 80.46); font-weight: bold' ); for (const { el, findings } of allFindings) { for (const f of findings) { console.log(`%c${f.type || f.id}%c ${f.detail || f.snippet}`, - 'color: oklch(55% 0.25 350); font-weight: bold', 'color: inherit', el); + 'color: oklch(84% 0.19 80.46); font-weight: bold', 'color: inherit', el); } } console.groupEnd(); @@ -1249,6 +1262,10 @@ if (IS_BROWSER) { const groupMap = new Map(); const _disabled = EXTENSION_MODE ? (window.__IMPECCABLE_CONFIG__?.disabledRules || []) : []; const _ruleOk = (id) => !_disabled.length || !_disabled.includes(id); + // Note: provider-gated rules (--gpt / --gemini) are NOT filtered here. In a + // real browser env (detector page, live overlay, extension) running every + // check is free, so we always surface them; the gating is purely a CLI + // output concern, applied in the Node engines' detect* return paths. for (const el of document.querySelectorAll('*')) { // Skip impeccable's own elements and any descendants (overlays, labels, banner, nav buttons) @@ -1270,11 +1287,23 @@ if (IS_BROWSER) { ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), - ...checkElementHeroEyebrowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementOversizedH1DOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementClippedOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementGptBorderShadowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementTextOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ].filter(f => _ruleOk(f.type)); addBrowserFindings(groupMap, el, findings); + + // Hero eyebrow: the offending element is the eyebrow above the heading, + // not the heading itself — highlight the previous sibling instead. + const eyebrowFindings = checkElementHeroEyebrowDOM(el) + .map(f => ({ type: f.id, detail: f.snippet })) + .filter(f => _ruleOk(f.type)); + if (eyebrowFindings.length > 0 && el.previousElementSibling) { + addBrowserFindings(groupMap, el.previousElementSibling, eyebrowFindings); + } } const pageLevelFindings = []; @@ -1306,6 +1335,14 @@ if (IS_BROWSER) { addBrowserFindings(groupMap, document.body, qualityFindings); } + const creamFindings = checkCreamPalette(document) + .map(f => ({ type: f.id, detail: f.snippet })) + .filter(f => _ruleOk(f.type)); + if (creamFindings.length > 0) { + pageLevelFindings.push(...creamFindings); + addBrowserFindings(groupMap, document.body, creamFindings); + } + // Regex-on-HTML checks (shared with Node) // Clone the document and strip impeccable-live overlay nodes before the // regex scan, so the inspector's own inline styles (transitions on top/ diff --git a/.cursor/skills/impeccable/scripts/detector/cli/main.mjs b/.cursor/skills/impeccable/scripts/detector/cli/main.mjs index 76da09f0c..94283e01b 100644 --- a/.cursor/skills/impeccable/scripts/detector/cli/main.mjs +++ b/.cursor/skills/impeccable/scripts/detector/cli/main.mjs @@ -41,7 +41,7 @@ function formatFindings(findings, jsonMode) { // Stdin handling // --------------------------------------------------------------------------- -async function handleStdin() { +async function handleStdin(options = {}) { const chunks = []; for await (const chunk of process.stdin) chunks.push(chunk); const input = Buffer.concat(chunks).toString('utf-8'); @@ -50,10 +50,10 @@ async function handleStdin() { const fp = parsed?.tool_input?.file_path; if (fp && fs.existsSync(fp)) { return HTML_EXTENSIONS.has(path.extname(fp).toLowerCase()) - ? detectHtml(fp) : detectText(fs.readFileSync(fp, 'utf-8'), fp); + ? detectHtml(fp, options) : detectText(fs.readFileSync(fp, 'utf-8'), fp, options); } } catch { /* not JSON */ } - return detectText(input, ''); + return detectText(input, '', options); } @@ -81,6 +81,8 @@ Scan files or URLs for UI anti-patterns and design quality issues. Options: --fast Regex-only mode (skip static HTML/CSS analysis, faster but misses linked stylesheets) --json Output results as JSON + --gpt Also report GPT-specific provider tells (off by default) + --gemini Also report Gemini-specific provider tells (off by default) --help Show this help message Detection modes: @@ -106,6 +108,10 @@ async function detectCli() { const jsonMode = args.includes('--json'); const helpMode = args.includes('--help'); const fastMode = args.includes('--fast'); + const providers = []; + if (args.includes('--gpt')) providers.push('gpt'); + if (args.includes('--gemini')) providers.push('gemini'); + const scanOptions = { providers }; const targets = args.filter(a => !a.startsWith('--')); if (helpMode) { printUsage(); process.exit(0); } @@ -113,7 +119,7 @@ async function detectCli() { let allFindings = []; if (!process.stdin.isTTY && targets.length === 0) { - allFindings = await handleStdin(); + allFindings = await handleStdin(scanOptions); } else { const paths = targets.length > 0 ? targets : [process.cwd()]; const urlTargetCount = paths.filter(target => /^https?:\/\//i.test(target)).length; @@ -124,8 +130,8 @@ async function detectCli() { if (/^https?:\/\//i.test(target)) { try { const scanner = browserDetector - ? (url) => browserDetector.detectUrl(url) - : (url) => detectUrl(url); + ? (url) => browserDetector.detectUrl(url, scanOptions) + : (url) => detectUrl(url, scanOptions); allFindings.push(...await scanner(target)); } catch (e) { process.stderr.write(`Error: ${e.message}\n`); } continue; @@ -192,9 +198,9 @@ async function detectCli() { const ext = path.extname(file).toLowerCase(); let fileFindings; if (!fastMode && HTML_EXTENSIONS.has(ext)) { - fileFindings = await detectHtml(file); + fileFindings = await detectHtml(file, scanOptions); } else { - fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file); + fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file, scanOptions); } // Annotate findings with import context const importers = importedByMap.get(file); @@ -209,9 +215,9 @@ async function detectCli() { } else if (stat.isFile()) { const ext = path.extname(resolved).toLowerCase(); if (!fastMode && HTML_EXTENSIONS.has(ext)) { - allFindings.push(...await detectHtml(resolved)); + allFindings.push(...await detectHtml(resolved, scanOptions)); } else { - allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved)); + allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved, scanOptions)); } } } diff --git a/.cursor/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.cursor/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 1afbe3f12..88439a5c3 100644 --- a/.cursor/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.cursor/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -38,7 +38,7 @@ const OVERUSED_FONTS = new Set([ // Older monoculture (still ubiquitous): 'inter', 'roboto', 'open sans', 'lato', 'montserrat', 'arial', 'helvetica', // Newer monoculture (the Anthropic-skill / Vercel / GitHub default wave): - 'fraunces', 'instrument sans', + 'fraunces', 'instrument sans', 'instrument serif', 'geist', 'geist sans', 'geist mono', 'mona sans', 'plus jakarta sans', 'space grotesk', 'recoleta', @@ -165,6 +165,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'AI color palette', }, + { + id: 'cream-palette', + category: 'slop', + name: 'Cream / beige palette', + description: + 'A warm cream or beige page background has become the default "tasteful" AI surface, reached for by reflex. Choose a background that comes from a deliberate palette, not the safe warm off-white.', + skillSection: 'Color & Contrast', + skillGuideline: 'cream and beige as the default surface', + }, { id: 'nested-cards', category: 'slop', @@ -183,15 +192,6 @@ const ANTIPATTERNS = [ skillSection: 'Layout & Space', skillGuideline: 'same spacing everywhere', }, - { - id: 'everything-centered', - category: 'slop', - name: 'Everything centered', - description: - 'Every text element is center-aligned. Left-aligned text with asymmetric layouts feels more designed. Center only hero sections and CTAs.', - skillSection: 'Layout & Space', - skillGuideline: 'Center everything', - }, { id: 'bounce-easing', category: 'slop', @@ -247,17 +247,72 @@ const ANTIPATTERNS = [ skillSection: 'Typography', skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding', }, + { + id: 'numbered-section-markers', + category: 'slop', + severity: 'advisory', + name: 'Numbered section markers (01 / 02 / 03)', + description: + 'Numbered display markers as section labels (01, 02, 03) are the AI editorial scaffold one tier deeper than tracked eyebrow chips. If you find yourself reaching for them, choose a different section cadence.', + skillSection: 'Layout & Space', + skillGuideline: 'numbered section markers', + }, + { + id: 'em-dash-overuse', + category: 'slop', + name: 'Em-dash overuse', + description: + 'More than two em-dashes (— or --) in body copy is an AI cadence tell. Use commas, colons, periods, or parentheses instead.', + skillSection: 'Copy', + skillGuideline: 'no em dashes', + }, + { + id: 'marketing-buzzword', + category: 'slop', + name: 'Marketing buzzword', + description: + 'Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does.', + skillSection: 'Copy', + skillGuideline: 'marketing buzzwords', + }, + { + id: 'aphoristic-cadence', + category: 'slop', + name: 'Aphoristic-cadence copy', + description: + 'Three or more sections landing on a short rebuttal sentence ("X. No Y." / "X. Just Y.") or a manufactured-contrast aphorism ("Not a feature. A platform.") reads as AI cadence, not voice. Once is fine; the pattern is the tell.', + skillSection: 'Copy', + skillGuideline: 'aphoristic cadence', + }, + { + id: 'oversized-h1', + category: 'slop', + name: 'Oversized hero headline', + description: + 'A full-sentence headline set at display size ends up dominating the viewport, leaving no room for anything else above the fold. A punchy one- or two-word headline at that size is fine — the problem is a long headline blown up too large. Set long headlines smaller, or tighten the copy.', + skillSection: 'Typography', + skillGuideline: 'long headline set at display size', + }, + { + id: 'extreme-negative-tracking', + category: 'slop', + name: 'Crushed letter spacing', + description: + 'Letter-spacing pulled tighter than the point where characters keep their own shapes costs legibility. Tighten display type optically, not destructively.', + skillSection: 'Typography', + skillGuideline: 'letter spacing crushed past legibility', + }, + { + id: 'broken-image', + category: 'quality', + name: 'Broken or placeholder image', + description: + ' tags with empty src, missing src, or placeholder values ship as broken-image boxes. Use real images, generated assets, or remove the tag.', + skillSection: 'Imagery', + skillGuideline: 'broken image references', + }, // ── Quality: general design and accessibility issues ── - { - id: 'pure-black-white', - category: 'quality', - name: 'Pure black background', - description: - 'Pure #000000 as a background color looks harsh and unnatural. Tint it slightly toward your brand hue (e.g., oklch(12% 0.01 250)) for a more refined feel.', - skillSection: 'Color & Contrast', - skillGuideline: 'pure black (#000)', - }, { id: 'gray-on-color', category: 'quality', @@ -297,7 +352,9 @@ const ANTIPATTERNS = [ category: 'quality', name: 'Cramped padding', description: - 'Text is too close to the edge of its container. Add at least 8px (ideally 12-16px) of padding inside bordered or colored containers.', + 'Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.', + skillSection: 'Layout & Space', + skillGuideline: 'inside bordered or colored containers', }, { id: 'body-text-viewport-edge', @@ -350,6 +407,70 @@ const ANTIPATTERNS = [ description: 'Letter spacing above 0.05em on body text disrupts natural character groupings and slows reading. Reserve wide tracking for short uppercase labels only.', }, + { + id: 'text-overflow', + category: 'quality', + name: 'Content overflowing its container', + description: + 'Content renders wider than its container, spilling out or forcing a horizontal scrollbar. Let text wrap, constrain widths, or give the region a deliberate scroll affordance.', + skillSection: 'Layout & Space', + skillGuideline: 'content wider than its container', + }, + { + id: 'clipped-overflow-container', + category: 'quality', + name: 'Positioned child clipped by overflow container', + description: + 'A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.', + skillSection: 'Layout & Space', + skillGuideline: 'overflow container clipping positioned children', + }, + + // ── Provider tells: opt-in via --gpt / --gemini (gated off by default) ── + { + id: 'gpt-thin-border-wide-shadow', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Hairline border with wide shadow', + description: + 'A hairline border paired with a wide, diffuse shadow is a recurring generated-UI signature. Commit to one — a defined edge or a soft elevation — rather than both at once.', + skillSection: 'Visual Details', + skillGuideline: 'hairline border plus wide diffuse shadow', + }, + { + id: 'repeating-stripes-gradient', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Repeating-gradient stripes', + description: + 'Repeating-gradient stripes used as surface decoration are a recurring generated-UI signature. Reach for a deliberate texture or leave the surface plain.', + skillSection: 'Visual Details', + skillGuideline: 'repeating-gradient decorative stripes', + }, + { + id: 'theater-slop-phrase', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Theater framing copy', + description: + 'Dismissing something as "theater" is a recurring generated-copy tic. Say plainly what the thing does or does not do.', + skillSection: 'Copy', + skillGuideline: 'theater framing copy', + }, + { + id: 'image-hover-transform', + category: 'slop', + severity: 'advisory', + gated: 'gemini', + name: 'Image hover transform', + description: + 'Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction.', + skillSection: 'Motion', + skillGuideline: 'image scale or rotate on hover', + }, ]; // --- cli/engine/shared/color.mjs --- @@ -527,11 +648,6 @@ function checkColors(opts) { } const findings = []; - // Pure black background (only solid or near-solid, not semi-transparent overlays) - if (bgColor && bgColor.a >= 0.9 && bgColor.r === 0 && bgColor.g === 0 && bgColor.b === 0) { - findings.push({ id: 'pure-black-white', snippet: '#000000 background' }); - } - if (hasDirectText && textColor && !isEmojiOnly) { // Run background-dependent checks against either a solid bg or, if the // ancestor is a gradient, against every gradient stop (use the worst case). @@ -587,9 +703,6 @@ function checkColors(opts) { // Tailwind class checks if (classList) { const classStr = typeof classList === 'string' ? classList : Array.from(classList).join(' '); - if (/\bbg-black\b(?!\/)/.test(classStr)) { - findings.push({ id: 'pure-black-white', snippet: 'bg-black' }); - } const grayMatch = classStr.match(/\btext-(?:gray|slate|zinc|neutral|stone)-\d+\b/); const colorBgMatch = classStr.match(/\bbg-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+\b/); @@ -905,12 +1018,6 @@ function checkHtmlPatterns(html) { // --- Color --- - // Pure black background - const pureBlackBgRe = /background(?:-color)?\s*:\s*(?:#000000|#000|rgb\(\s*0,\s*0,\s*0\s*\))\b/gi; - if (pureBlackBgRe.test(html)) { - findings.push({ id: 'pure-black-white', snippet: 'Pure #000 background' }); - } - // AI color palette: purple/violet const purpleHexRe = /#(?:7c3aed|8b5cf6|a855f7|9333ea|7e22ce|6d28d9|6366f1|764ba2|667eea)\b/gi; if (purpleHexRe.test(html)) { @@ -1027,6 +1134,39 @@ function checkHtmlPatterns(html) { } } + // --- Provider tells (gated): repeating-gradient stripes (GPT) --- + if (/repeating-(?:linear|radial|conic)-gradient\s*\(/i.test(html)) { + findings.push({ id: 'repeating-stripes-gradient', snippet: 'repeating-gradient decorative stripes' }); + } + + // --- Provider tells (gated): "X theater" framing copy (GPT) --- + // Lives here (regex-on-HTML) rather than in the text-content analyzers so it + // runs in the bundled browser path too, not just the CLI/static path. + { + const bodyText = html + .replace(/]*>[\s\S]*?<\/script>/gi, ' ') + .replace(/]*>[\s\S]*?<\/style>/gi, ' ') + .replace(/<[^>]+>/g, ' '); + const tm = /\b(\w+)\s+theater\b/i.exec(bodyText); + if (tm) findings.push({ id: 'theater-slop-phrase', snippet: `"${tm[0].trim()}"` }); + } + + // --- Provider tells (gated): image hover transform (Gemini) --- + // A CSS `img...:hover { transform: ... }` rule, or a Tailwind hover:scale / + // hover:rotate / hover:translate utility on an . Each distinct + // mechanism is its own finding. + const imgHoverCss = /\bimg\b[^,{}]*:hover\b[^{}]*\{[^}]*\btransform\s*:\s*(?:scale|rotate|translate|matrix|skew)/i; + if (imgHoverCss.test(html)) { + findings.push({ id: 'image-hover-transform', snippet: 'img:hover { transform } rule' }); + } + const imgTagRe = /]*\bclass\s*=\s*"([^"]*)"/gi; + let im; + while ((im = imgTagRe.exec(html)) !== null) { + if (/\bhover:(?:scale|rotate|translate|skew)-/.test(im[1])) { + findings.push({ id: 'image-hover-transform', snippet: 'Tailwind hover transform on ' }); + } + } + return findings; } @@ -1673,7 +1813,7 @@ function resolveLengthPx(value, fontSizePx) { // Both adapters resolve font-size, line-height and letter-spacing to pixels // before calling this so the pure function only deals with numbers. function checkQuality(opts) { - const { el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax = 80, viewportWidth = 0 } = opts; + const { el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax = 80, viewportWidth = 0, win = null } = opts; const findings = []; // Skip browser extension injected elements const elId = el.id || ''; @@ -1724,6 +1864,155 @@ function checkQuality(opts) { } } + // --- Flush against a visible boundary --- + // Fires when a container has a visible boundary (border, outline, OR a + // non-transparent background) AND near-zero padding on the bounded + // side(s) AND text-bearing children land flush against the boundary. + // + // Distinct from cramped-padding: that rule needs the element itself to + // have direct text (hasDirectText). This rule targets the OPPOSITE + // shape — a container with NO direct text, only children — which is + // exactly what cramped-padding misses (a section wrapping a label + + // list lands a free pass). + // + // The classic shape: agent writes `padding: 28px 0 0` shorthand on a + // section that also has a border, zeroing horizontal padding so the + // text-bearing children touch the side borders. Background and + // outline count too: a colored card with zero padding has the same + // visual failure mode. + { + const FLUSH_SKIP_TAGS = new Set(['HTML', 'BODY', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ARTICLE', 'ASIDE', 'BUTTON', 'A', 'LABEL', 'SUMMARY', 'CODE', 'PRE', 'INPUT', 'TEXTAREA', 'SELECT', 'FORM', 'FIGURE', 'TABLE', 'TBODY', 'THEAD', 'TR', 'TD', 'TH']); + const upperTag = tag ? tag.toUpperCase() : ''; + const elPosition = style.position || ''; + if ( + !FLUSH_SKIP_TAGS.has(upperTag) && + !hasDirectText && + !['fixed', 'absolute'].includes(elPosition) && + el.children && el.children.length > 0 + ) { + const isTransparent = (c) => + !c || c === 'transparent' || c === 'rgba(0, 0, 0, 0)' || + /^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*0(?:\.0+)?\s*\)$/.test(c); + + const borderW = { + top: parseFloat(style.borderTopWidth) || 0, + right: parseFloat(style.borderRightWidth) || 0, + bottom: parseFloat(style.borderBottomWidth) || 0, + left: parseFloat(style.borderLeftWidth) || 0, + }; + const borderVisible = { + top: borderW.top > 0 && !isTransparent(style.borderTopColor), + right: borderW.right > 0 && !isTransparent(style.borderRightColor), + bottom: borderW.bottom > 0 && !isTransparent(style.borderBottomColor), + left: borderW.left > 0 && !isTransparent(style.borderLeftColor), + }; + // Outline detection. jsdom decomposes `border` shorthand into + // border{Top,…}Width/Color but does NOT decompose `outline` — + // the longhands come back empty when the value was set via the + // shorthand. Fall back to parsing `style.outline` ourselves. + let outlineW = parseFloat(style.outlineWidth) || 0; + let outlineStyleVal = style.outlineStyle || ''; + let outlineColorVal = style.outlineColor || ''; + if (!outlineW && style.outline) { + const wMatch = style.outline.match(/(\d+(?:\.\d+)?)\s*px/); + if (wMatch) outlineW = parseFloat(wMatch[1]) || 0; + if (!outlineStyleVal) { + outlineStyleVal = /\b(solid|dashed|dotted|double|groove|ridge|inset|outset)\b/.test(style.outline) ? 'solid' : ''; + } + if (!outlineColorVal) { + const cMatch = style.outline.match(/(rgba?\([^)]+\)|#[0-9a-fA-F]{3,8}|[a-zA-Z]+)\s*$/); + if (cMatch) outlineColorVal = cMatch[1]; + } + } + const outlineVisible = outlineW > 0 && !isTransparent(outlineColorVal) && outlineStyleVal && outlineStyleVal !== 'none'; + const bgVisible = !isTransparent(style.backgroundColor); + + const anyVisible = borderVisible.top || borderVisible.right || borderVisible.bottom || borderVisible.left || outlineVisible || bgVisible; + if (anyVisible) { + // Resolve padding to px (jsdom returns raw "1.5rem" etc., not the + // computed px value; parseFloat would strip the unit and treat + // 1.5rem as 1.5px, false-flagging legitimate insets). + const pad = { + top: resolveLengthPx(style.paddingTop, fontSize) ?? 0, + right: resolveLengthPx(style.paddingRight, fontSize) ?? 0, + bottom: resolveLengthPx(style.paddingBottom, fontSize) ?? 0, + left: resolveLengthPx(style.paddingLeft, fontSize) ?? 0, + }; + const PAD_THRESHOLD = 2; + // Children-insulate-this-side: a side is insulated if ANY direct + // child has its own padding ≥ 4px on that side. Rationale: in + // typical flow, only the first/last (or leftmost/rightmost) + // children actually sit at the parent's edges. If even one of + // them has its own padding, the visual flush is broken on that + // side. Classic example: a column-flow card frame where the + // top child (header) has padding-top:12 and the bottom child + // (footer) has padding-bottom:8 — the parent's padding:0 doesn't + // matter; nothing is actually flush. The `any-child-insulates` + // heuristic accepts some false negatives (a card with one heavily + // padded middle child won't flag) for far fewer false positives. + const CHILD_INSULATE_THRESHOLD = 4; + const childrenInsulate = { top: false, right: false, bottom: false, left: false }; + for (const child of el.children) { + let childStyle = null; + if (win && typeof win.getComputedStyle === 'function') { + try { childStyle = win.getComputedStyle(child); } catch {} + } + if (!childStyle && typeof getComputedStyle === 'function') { + try { childStyle = getComputedStyle(child); } catch {} + } + if (!childStyle) continue; + const childPad = { + top: resolveLengthPx(childStyle.paddingTop, fontSize) ?? 0, + right: resolveLengthPx(childStyle.paddingRight, fontSize) ?? 0, + bottom: resolveLengthPx(childStyle.paddingBottom, fontSize) ?? 0, + left: resolveLengthPx(childStyle.paddingLeft, fontSize) ?? 0, + }; + for (const s of ['top', 'right', 'bottom', 'left']) { + if (childPad[s] >= CHILD_INSULATE_THRESHOLD) childrenInsulate[s] = true; + } + } + + const flushSides = []; + for (const side of ['top', 'right', 'bottom', 'left']) { + const sideBounded = borderVisible[side] || outlineVisible || bgVisible; + if (sideBounded && pad[side] <= PAD_THRESHOLD && !childrenInsulate[side]) { + flushSides.push(side); + } + } + + if (flushSides.length > 0) { + // Confirm at least one direct child has substantial text content + // (> 4 chars). Without this, the flush is harmless: e.g. an + // image-only card. + let hasTextChild = false; + for (const child of el.children) { + const childText = (child.textContent || '').trim(); + if (childText.length > 4) { hasTextChild = true; break; } + } + if (hasTextChild) { + const cls = (typeof el.className === 'string' && el.className.trim()) + ? el.className.trim().split(/\s+/)[0] + : ''; + const boundaryParts = []; + const borderSidesVisible = ['top', 'right', 'bottom', 'left'].filter(s => borderVisible[s]); + if (borderSidesVisible.length === 4) boundaryParts.push('border'); + else if (borderSidesVisible.length > 0) boundaryParts.push(`border-${borderSidesVisible.join('/')}`); + if (outlineVisible) boundaryParts.push('outline'); + if (bgVisible) boundaryParts.push('bg'); + const sidesLabel = flushSides.length === 4 ? 'all sides' : flushSides.join('/'); + const ident = cls + ? `<${tag.toLowerCase()}> "${cls}"` + : `<${tag.toLowerCase()}>`; + findings.push({ + id: 'cramped-padding', + snippet: `${ident}: children flush against ${boundaryParts.join('+')} on ${sidesLabel} (no inset)`, + }); + } + } + } + } + } + // --- Body text touching viewport edge --- (browser-only: needs rect) // Catches the failure mode where the agent ships body paragraphs // with NO container providing horizontal padding — text bleeds @@ -1804,6 +2093,20 @@ function checkQuality(opts) { } } + // --- Crushed letter spacing (mirror of wide-tracking) --- + // Tracking pulled tighter than ~-0.05em crushes characters into each other. + // Optical tightening that display type legitimately wants (around -0.02em) + // stays well above this floor. + if (hasDirectText && textLen > 20 && fontSize > 0) { + if (letterSpacingPx != null && letterSpacingPx < 0) { + const trackingEm = letterSpacingPx / fontSize; + if (trackingEm <= -0.05) { + const excerpt = (el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 40); + findings.push({ id: 'extreme-negative-tracking', snippet: `letter-spacing: ${trackingEm.toFixed(2)}em — "${excerpt}"` }); + } + } + } + return findings; } @@ -1820,7 +2123,7 @@ function checkElementQualityDOM(el) { const rect = el.getBoundingClientRect(); const lineMax = (typeof window !== 'undefined' && window.__IMPECCABLE_CONFIG__?.lineLengthMax) || 80; const viewportWidth = (typeof window !== 'undefined' ? window.innerWidth : 0) || 0; - return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax, viewportWidth }); + return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax, viewportWidth, win: typeof window !== 'undefined' ? window : null }); } // Pure page-level skipped-heading walk. Takes a Document so it works in both @@ -1862,7 +2165,7 @@ function checkElementQuality(el, style, tag, window) { const fontSize = resolveFontSizePx(el, window); const lineHeightPx = resolveLengthPx(style.lineHeight, fontSize); const letterSpacingPx = resolveLengthPx(style.letterSpacing, fontSize); - return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect: null }); + return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect: null, win: window }); } function checkElementBorders(tag, style, overrides, resolvedRadius) { @@ -2303,40 +2606,213 @@ function checkPageLayout(doc, win) { } } - // Everything centered - const textEls = doc.querySelectorAll('h1, h2, h3, h4, h5, h6, p, li, div, button'); - let centeredCount = 0; - let totalText = 0; - for (const el of textEls) { - const hasDirectText = [...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim().length >= 3); - if (!hasDirectText) continue; - totalText++; - - let cur = el; - let isCentered = false; - while (cur && cur.nodeType === 1) { - const rawStyle = cur.getAttribute?.('style') || ''; - const cls = cur.getAttribute?.('class') || ''; - if (/text-align\s*:\s*center/i.test(rawStyle) || /\btext-center\b/.test(cls)) { - isCentered = true; - break; - } - if (cur.tagName === 'BODY') break; - cur = cur.parentElement; - } - if (isCentered) centeredCount++; - } - - if (totalText >= 5 && centeredCount / totalText > 0.7) { - findings.push({ - id: 'everything-centered', - snippet: `${centeredCount}/${totalText} text elements centered (${Math.round(centeredCount / totalText * 100)}%)`, - }); - } - return findings; } +// ─── Cream / beige palette (the default "tasteful" AI surface) ──────────────── +// A warm, lightly-tinted off-white page background — light, with R≥G≥B and a +// small warm tint (not white, not a strong color). The current reflex surface. +function isCreamColor(rgb) { + if (!rgb) return false; + const { r, g, b } = rgb; + if (Math.min(r, g, b) < 209) return false; // must be light + if (!(r >= g && g >= b)) return false; // warm ordering + const warmth = r - b; + return warmth >= 6 && warmth <= 48; // tinted, not white, not strong +} + +// Tailwind background utilities that render as a warm off-white surface. The +// static engine doesn't fetch Tailwind's CSS, so a `bg-amber-50` on +// resolves to nothing in computed style — catch it from the class list +// instead. Candidate tokens map to their actual Tailwind hex and are still +// filtered through isCreamColor, so neutral grays (stone) and over-saturated +// shades drop out on their own. +const TAILWIND_BG_HEX = { + 'bg-amber-50': '#fffbeb', 'bg-amber-100': '#fef3c7', + 'bg-orange-50': '#fff7ed', 'bg-orange-100': '#ffedd5', + 'bg-yellow-50': '#fefce8', + 'bg-stone-50': '#fafaf9', 'bg-stone-100': '#f5f5f4', 'bg-stone-200': '#e7e5e4', +}; + +function creamFromClassList(cls) { + if (!cls) return null; + // Arbitrary value: bg-[#f5f0e6] / bg-[rgb(245_240_230)] (underscores = spaces). + const arb = cls.match(/\bbg-\[([^\]]+)\]/); + if (arb && isCreamColor(parseAnyColor(arb[1].replace(/_/g, ' ')))) return `bg-[${arb[1]}]`; + // Named warm-light utilities. + for (const [tok, hex] of Object.entries(TAILWIND_BG_HEX)) { + if (new RegExp(`(^|\\s)${tok}($|\\s)`).test(cls) && isCreamColor(parseAnyColor(hex))) return tok; + } + return null; +} + +function checkCreamPalette(doc, win) { + const findings = []; + const body = doc.body || (doc.querySelector ? doc.querySelector('body') : null); + if (!body) return findings; + const html = doc.documentElement; + const getCS = (el) => (win ? win.getComputedStyle(el) : getComputedStyle(el)); + + // 1. Computed background — covers inline / close.', 'Prefix every preview selector with the matching [data-impeccable-variant="N"] selector.', 'Keep selectors anchored to the generated variant wrapper; do not rely on component CSS scoping for preview rules.', ], forbidden: [ 'Do not use @scope for this styleMode.', + 'Do not wrap style content in a JSX/TSX template literal ({` ... `}); that syntax is for .tsx/.jsx only.', + 'Do not put { immediately after the style opening tag; Astro parses { as expression syntax.', ], }; } @@ -629,4 +632,15 @@ if (_running?.endsWith('live-wrap.mjs') || _running?.endsWith('live-wrap.mjs/')) } // Test exports (used by tests/live-wrap.test.mjs) -export { buildSearchQueries, findElement, findClosingLine, detectCommentSyntax }; +export { + buildSearchQueries, + findElement, + findClosingLine, + detectCommentSyntax, + findAllElements, + filterByText, + findFileWithQuery, + detectStyleMode, + buildCssAuthoring, + buildCssSelectorPrefixExamples, +}; diff --git a/.cursor/skills/impeccable/scripts/live.mjs b/.cursor/skills/impeccable/scripts/live.mjs index cafb0eca9..8acd300ed 100644 --- a/.cursor/skills/impeccable/scripts/live.mjs +++ b/.cursor/skills/impeccable/scripts/live.mjs @@ -21,7 +21,7 @@ import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { loadContext } from './load-context.mjs'; +import { loadContext } from './context.mjs'; import { resolveFiles } from './live-inject.mjs'; import { readLiveServerInfo } from './impeccable-paths.mjs'; @@ -80,7 +80,7 @@ The agent should then: process.exit(1); } - // 4. Load PRODUCT.md + DESIGN.md context (auto-migrates legacy .impeccable.md) + // 4. Load PRODUCT.md + DESIGN.md context. const ctx = loadContext(process.cwd()); // 5. Compute drift-heal: compare resolved inject targets against the @@ -102,7 +102,6 @@ The agent should then: hasDesign: ctx.hasDesign, design: ctx.design, designPath: ctx.designPath, - migrated: ctx.migrated, }, null, 2)); } diff --git a/.cursor/skills/impeccable/scripts/load-context.mjs b/.cursor/skills/impeccable/scripts/load-context.mjs deleted file mode 100644 index dc340bf16..000000000 --- a/.cursor/skills/impeccable/scripts/load-context.mjs +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Shared context loader for every impeccable command that needs to know - * "who is this for" and "what does this look like". - * - * Input: project root (process.cwd()). - * - * Output (JSON to stdout): - * { - * hasProduct: boolean, // PRODUCT.md found (or auto-migrated) - * product: string | null, // PRODUCT.md contents - * productPath: string | null, // relative path - * hasDesign: boolean, // DESIGN.md found - * design: string | null, // DESIGN.md contents - * designPath: string | null, - * migrated: boolean, // true if we auto-renamed .impeccable.md -> PRODUCT.md - * contextDir: string, // absolute path of the directory the files were found in - * } - * - * Filename matching is case-insensitive for PRODUCT.md and DESIGN.md. The - * Google DESIGN.md convention is uppercase at repo root; Kiro-style and - * lowercase variants are also matched so users don't get punished for case. - * - * Lookup directory resolution (first match wins): - * 1. process.env.IMPECCABLE_CONTEXT_DIR (absolute or relative to cwd) - * 2. cwd, if PRODUCT.md / DESIGN.md / .impeccable.md is there (back-compat) - * 3. Auto-fallback subdirectories of cwd: .agents/context/, then docs/ - * 4. cwd as a default "no context found" location - * - * Legacy `.impeccable.md` -> PRODUCT.md migration only fires at cwd root; - * fallback directories are read-only as far as auto-rename is concerned. - */ - -import fs from 'node:fs'; -import path from 'node:path'; - -const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; -const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; -const LEGACY_NAMES = ['.impeccable.md']; -const FALLBACK_DIRS = ['.agents/context', 'docs']; - -/** - * Resolve the directory that holds PRODUCT.md / DESIGN.md for - * this project. Exported so other scripts (e.g. live-server.mjs) can read the - * design files from the same location the loader uses. - */ -export function resolveContextDir(cwd = process.cwd()) { - // 1. Explicit override - const envDir = process.env.IMPECCABLE_CONTEXT_DIR; - if (envDir && envDir.trim()) { - const trimmed = envDir.trim(); - return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); - } - - // 2. cwd wins if any canonical or legacy file is there. We check legacy too - // so the auto-migration path in loadContext stays predictable. - if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES, ...LEGACY_NAMES])) { - return cwd; - } - - // 3. Auto-fallback subdirs. Match if PRODUCT.md or DESIGN.md is present; - // legacy `.impeccable.md` does not pull the lookup into a fallback dir. - for (const rel of FALLBACK_DIRS) { - const candidate = path.resolve(cwd, rel); - if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { - return candidate; - } - } - - // 4. Nothing found — keep the historical "default to cwd" behaviour so the - // caller's `hasProduct === false` branch still fires the same way. - return cwd; -} - -export function loadContext(cwd = process.cwd()) { - let migrated = false; - const contextDir = resolveContextDir(cwd); - - // 1. Look for PRODUCT.md (case-insensitive) in the resolved dir - let productPath = firstExisting(contextDir, PRODUCT_NAMES); - - // 2. Legacy: if no PRODUCT.md but .impeccable.md exists at cwd root, rename - // it in place. We only migrate at the root — fallback dirs are read-only - // so we don't surprise users by mutating files under docs/ or .agents/. - if (!productPath && contextDir === cwd) { - const legacyPath = firstExisting(cwd, LEGACY_NAMES); - if (legacyPath) { - const newPath = path.join(cwd, 'PRODUCT.md'); - try { - fs.renameSync(legacyPath, newPath); - productPath = newPath; - migrated = true; - } catch { - // Rename failed (permissions, etc.) — fall back to reading legacy in place - productPath = legacyPath; - } - } - } - - // 3. DESIGN.md (case-insensitive) - const designPath = firstExisting(contextDir, DESIGN_NAMES); - - const product = productPath ? safeRead(productPath) : null; - const design = designPath ? safeRead(designPath) : null; - - return { - hasProduct: !!product, - product, - productPath: productPath ? path.relative(cwd, productPath) : null, - hasDesign: !!design, - design, - designPath: designPath ? path.relative(cwd, designPath) : null, - migrated, - contextDir, - }; -} - -function firstExisting(dir, names) { - for (const name of names) { - const abs = path.join(dir, name); - if (fs.existsSync(abs)) return abs; - } - return null; -} - -function safeRead(p) { - try { return fs.readFileSync(p, 'utf-8'); } catch { return null; } -} - -// --------------------------------------------------------------------------- -// CLI mode — print the context as JSON -// --------------------------------------------------------------------------- - -function cli() { - const result = loadContext(process.cwd()); - console.log(JSON.stringify(result, null, 2)); -} - -const _running = process.argv[1]; -if (_running?.endsWith('load-context.mjs') || _running?.endsWith('load-context.mjs/')) { - cli(); -} diff --git a/.cursor/skills/impeccable/scripts/palette.mjs b/.cursor/skills/impeccable/scripts/palette.mjs new file mode 100644 index 000000000..4c3f1751a --- /dev/null +++ b/.cursor/skills/impeccable/scripts/palette.mjs @@ -0,0 +1,633 @@ +#!/usr/bin/env node +/** + * Brand-seed picker. Returns one OKLCH seed color + the mood it most + * naturally evokes, and teaches the model how to compose a full palette + * around it. + * + * The seed is the brand's anchor color. The 5-role palette (bg, surface, + * ink, accent, muted) is composed by the caller at runtime using their + * judgment + the brief (PRODUCT.md / DESIGN.md / user prompt), NOT picked + * from a frozen 4-color preset. + * + * Why: 4-color frozen palettes drift toward safe defaults (warm-cream bg, + * complementary accent on near-white) regardless of brief. A single seed + + * the model's own composition lets the same seed produce a dark-mode jazz + * club or a light-mode hospitality brand depending on what the brief calls + * for. Tested empirically against curated 4-color palettes; seed approach + * wins on mood-fit in 3 of 5 cases and ties on the rest. + * + * Usage: + * node scripts/palette.mjs # pick at random + * node scripts/palette.mjs --id seed-021 # pick a specific seed + * node scripts/palette.mjs --from # hash to a seed (deterministic) + * + * Env vars: + * IMPECCABLE_PALETTE_SEED — same as --from; useful for the eval harness + * to make runs reproducible. + */ + +import crypto from 'node:crypto'; + +// Seeds are inlined (129 entries, hand-curated via a tinder review of +// ~400 candidates from ColorHunt + synthesis + Radix/brand/Pantone anchors). +// Each carries a mood + strategy the judging model produced — surfaced as +// hints, not commands; the brief still drives composition. +const SEEDS = [ + { id: "seed-200", oklch: [0.360, 0.137, 0.0], + mood: "Aesop apothecary shelf — oxblood bottle glass against linen, considered and unhurried", + strategy: "Seed is a deep desaturated red-brown that reads as brand ink itself; I push primary darker toward bottle-glass oxblood, pair with a pure white surface so the red does the work, and use a clear pale-blush accent that can carry dark text in pills." }, + { id: "seed-000", oklch: [0.400, 0.130, 0.0], + mood: "oxblood leather banquette in a 1940s steakhouse — low lamplight on dark wood and burgundy", + strategy: "Near-black bg with the faintest red undertone lets the oxblood primary glow like lamplit leather; warm cream ink and a brass accent complete the chophouse register." }, + { id: "seed-002", oklch: [0.450, 0.150, 0.0], + mood: "darkroom red light — analog photography, blood-warm safelight glow on chemical trays", + strategy: "Near-black surface with a deep oxblood primary lets the seed function like a safelight in a darkroom — the bg disappears so the red becomes the only emotional signal." }, + { id: "seed-003", oklch: [0.500, 0.194, 0.0], + mood: "darkroom safelight — the deep oxblood glow of analog photography, chemical and contemplative", + strategy: "Anchored the seed as primary against pure near-black so the red reads like a single illuminated bulb in a developing room, with cool desaturated ink to evoke silver gelatin print tones." }, + { id: "seed-004", oklch: [0.546, 0.204, 3.4], + mood: "midnight boudoir — velvet rose under low lamplight, perfumed and intimate", + strategy: "Near-black surface lets the rose seed glow like silk in shadow; a warm champagne accent provides the candle-flame counterpoint without breaking the hush." }, + { id: "seed-005", oklch: [0.550, 0.180, 0.0], + mood: "smoldering vermillion at dusk — the last red ember in a blacksmith's forge, iron-rich and quietly violent", + strategy: "Near-black gallery surround lets the seed read as glowing forged metal; ink stays warm-off-white, accent shifts to a hotter ember orange so the primary feels like cooling steel against a fresh strike." }, + { id: "seed-201", oklch: [0.647, 0.262, 0.3], + mood: "Figma plugin marketplace red — confident product-brand crimson, the kind a modern dev tool uses for a 'live' indicator or a primary CTA on a pristine docs page", + strategy: "Pure white surface lets a high-chroma crimson primary do all the brand work, paired with a hue-shifted warm coral accent for hierarchy without competing saturation" }, + { id: "seed-006", oklch: [0.650, 0.160, 0.0], + mood: "1960s Italian cinema — Technicolor lipstick red against a darkened theater", + strategy: "Pure near-black surface lets a saturated cinematic red and its warm peach accent perform like film light projected in a dark room — the brand colors carry the drama, the bg disappears." }, + { id: "seed-008", oklch: [0.520, 0.200, 10.4], + mood: "Negroni hour at a Milanese bar — bittersweet crimson, vermouth and amaro under low tungsten", + strategy: "Seed is a saturated red-crimson with cinematic weight, so I sit it on near-black to let the primary glow like backlit liquor, with a warmer amber accent acting as the citrus twist against the bitter red." }, + { id: "seed-010", oklch: [0.563, 0.223, 11.0], + mood: "Negroni hour on a Milan rooftop — bittersweet crimson, aperitivo light, polished restraint", + strategy: "Seed is a vivid carmine-red with strong chroma, so the surface gets out of the way (pure white) and lets the primary do the aperitivo work, with a cooled garnet accent for tension." }, + { id: "seed-202", oklch: [0.643, 0.247, 7.0], + mood: "Glossier brand pink — modern beauty editorial, confident and current", + strategy: "Pure white bg lets a saturated rose-red primary do all the brand work, paired with a deeper crimson accent for hierarchy — the Stripe/Glossier move where the color carries the mood." }, + { id: "seed-013", oklch: [0.400, 0.130, 20.0], + mood: "Tuscan cellar at dusk — aged terracotta, oxidized iron, the deep red of decanted Sangiovese", + strategy: "Black surface lets the oxblood seed and copper accent glow like firelight on cellar stone; brand colors carry all the warmth while the room recedes." }, + { id: "seed-014", oklch: [0.450, 0.150, 20.0], + mood: "smoldering tannery — oxblood leather, cured under low workshop light", + strategy: "Anchor the deep oxblood seed as primary against a near-black architectural ground, then lift with a single warm ember accent so the leather reads burnished rather than bloody." }, + { id: "seed-016", oklch: [0.550, 0.180, 20.0], + mood: "Negroni hour on a Roman terrace — bitter campari red, vermouth, late golden light spilling on white linen", + strategy: "Pure white surface lets the campari-red primary do all the emotional work, paired with a deeper oxblood accent for bittersweet depth — Italian aperitivo restraint, not warmth-washed." }, + { id: "seed-205", oklch: [0.634, 0.254, 17.6], + mood: "Aesop apothecary bottle — considered red-coral on a clinical white surface, the kind of brand restraint where one saturated object does all the work", + strategy: "Default A pure white surface lets a single coral-red primary carry the entire brand voice; accent shifts to a deeper oxblood for hierarchy without competing chroma." }, + { id: "seed-011", oklch: [0.639, 0.207, 13.5], + mood: "Aperitivo hour in Milan — Campari glow on a white marble bar, crisp and effervescent", + strategy: "Pure white gallery backdrop lets the Campari-red primary ring like a single bitter note; ink is near-black with a whisper of warmth, accent shifts to a deeper oxblood for hierarchy without competing hues." }, + { id: "seed-015", oklch: [0.527, 0.202, 22.7], + mood: "Negroni hour on a Milanese terrace — bittersweet vermillion, aperitivo glassware catching low sun", + strategy: "Seed becomes a saturated aperitivo-red primary against pure white so the color carries the bittersweet warmth alone, paired with a deep oxblood accent for typographic gravitas." }, + { id: "seed-023", oklch: [0.427, 0.175, 29.2], + mood: "blacksmith's forge at dusk — iron heated to ember red, the deep glow of oxidized metal and quenching oil", + strategy: "Pure black bg lets the seed's ember-red glow radiate like hot iron in a dark forge; accent shifts to a copper-amber to suggest scaling metal and sparks, while ink stays near-white for tool-precise legibility." }, + { id: "seed-206", oklch: [0.614, 0.234, 28.2], + mood: "Aesop apothecary bottle — considered red-orange on lab-white, calm utility with a single confident pigment", + strategy: "Pure white surface lets a saturated vermilion primary do all the brand work, paired with a deep oxblood accent for hierarchy without introducing a second hue family" }, + { id: "seed-029", oklch: [0.665, 0.222, 25.7], + mood: "Negroni hour at a Milanese bar — bittersweet orange-red liqueur catching late afternoon light on polished marble", + strategy: "Pure white surface lets the seed's vermilion read like Campari in a glass; a deeper oxblood accent provides the bitter depth, with neutral graphite ink keeping the editorial restraint of Italian design." }, + { id: "seed-022", oklch: [0.418, 0.155, 27.2], + mood: "Pompeiian red fresco — oxidized cinnabar on a museum wall, archaeological gravity", + strategy: "Pure black gallery surface lets the seed's iron-oxide red read as a lit artifact; accent shifts to an aged terracotta amber, so primary and accent form a fired-clay duet against neutral void." }, + { id: "seed-024", oklch: [0.464, 0.169, 26.9], + mood: "Mid-century darkroom under the safelight — developer trays, oxblood leather, the quiet patience of a print emerging", + strategy: "Seed becomes a deep oxblood primary; surface stays pure black so the red glows like a safelight, with a warmer ember accent for hierarchy" }, + { id: "seed-026", oklch: [0.489, 0.190, 28.3], + mood: "smoldering ember in a blacksmith's forge — iron-hot rust, soot, and controlled fire", + strategy: "Near-black soot background lets the seed's red-orange glow like heated metal; ink is bone-white, accent is a cooler tempered-steel orange that creates internal heat gradient with the primary." }, + { id: "seed-027", oklch: [0.568, 0.208, 27.1], + mood: "Sicilian blood orange at golden hour — citrus rind, terracotta, sun on stucco", + strategy: "Seed reads as vivid blood-orange — picked pure white surface so the citrus-red primary and a deep oxblood accent do all the emotional work, like a Loro Piana editorial spread." }, + { id: "seed-028", oklch: [0.591, 0.172, 24.0], + mood: "Sienna-fired ceramic studio at dusk — terracotta cooling on a wheel, hands still dusted with slip", + strategy: "Pure black stage lets the fired-clay primary glow like a kiln ember, with a deeper oxblood accent providing tonal weight rather than hue contrast — a monochrome warm-axis play." }, + { id: "seed-033", oklch: [0.544, 0.169, 31.3], + mood: "1960s Italian terracotta workshop — fired clay, espresso, late-afternoon Mediterranean dust", + strategy: "Pure black ground lets the seed's burnt-sienna primary glow like a lit kiln, with a deeper oxblood accent for restrained warmth tension — the brand carries the heat, the surface stays out." }, + { id: "seed-207", oklch: [0.564, 0.231, 29.1], + mood: "Aesop apothecary bottle — considered red oxide, the calm authority of a well-made object on a white shelf", + strategy: "Seed becomes the singular brand voice against pure white, with a deeper oxblood accent for hierarchy — the surface disappears so the red does all the speaking." }, + { id: "seed-035", oklch: [0.663, 0.153, 32.1], + mood: "Aesop apothecary bottle — clay-fired warmth, considered retail", + strategy: "Pure white surface lets the terracotta primary do the brand work, paired with a deep umber ink and a cooler clay accent for editorial tension." }, + { id: "seed-037", oklch: [0.590, 0.188, 35.8], + mood: "Aesop apothecary bottle — considered terracotta, herbalist restraint, the warmth comes from the glass not the room", + strategy: "Seed becomes a muted terracotta primary against pure white so the brand's warmth carries entirely through the color itself; accent shifts to a deeper umber for quiet hierarchy." }, + { id: "seed-038", oklch: [0.652, 0.229, 34.8], + mood: "blown-glass furnace at dusk — molten orange iron pulled from the kiln, a craftsman's signature heat", + strategy: "Pure black stage so the seed reads as live ember; primary holds the seed's heat, accent shifts to a brass-amber a hue-step away for a 1.7+ contrast pairing without leaving the fire." }, + { id: "seed-039", oklch: [0.653, 0.185, 33.5], + mood: "Aesop apothecary bottle — considered terracotta, quiet retail craft", + strategy: "Seed becomes a grounded clay primary against pure white, paired with a deeper umber accent so the warmth lives entirely in the brand marks, not the surface." }, + { id: "seed-167", oklch: [0.495, 0.134, 36.0], + mood: "Aesop apothecary shelf — burnished terracotta on clinical white, considered craft pharmacy", + strategy: "Treat the seed as a brand-carrying burnt-sienna against a pure paper-white surface so the warmth lives entirely in the primary, with a deep umber accent pulled along the same warm axis for typographic gravity." }, + { id: "seed-147", oklch: [0.500, 0.151, 40.0], + mood: "Aesop apothecary shelf — considered terracotta, pharmacy restraint, the brand color does the work against clinical white", + strategy: "Anchor the seed's burnt-sienna primary against a pure white surface so the rust speaks alone, with a deep umber ink and a cooler clay accent to give the palette product-brand discipline rather than environmental warmth." }, + { id: "seed-040", oklch: [0.660, 0.201, 40.0], + mood: "Aesop apothecary bottle — amber glass on a clean dispensary shelf, considered and clinical-warm", + strategy: "Seed becomes a burnt-amber primary against pure white so the bottle-glass color does the emotional work; accent shifts to a deep olive-bronze for the apothecary-label pairing." }, + { id: "seed-041", oklch: [0.673, 0.217, 38.6], + mood: "Aesop apothecary shelf — considered orange glass, clinical retail restraint", + strategy: "Pure white surface lets the burnt-orange primary do all the brand work, with a deep ink-brown for editorial gravity and a muted clay accent that reads as a sibling, not a contrast." }, + { id: "seed-042", oklch: [0.688, 0.133, 35.8], + mood: "Aesop apothecary shelf — terracotta glass, considered retail", + strategy: "Seed becomes a warm clay primary against pure white so the bottle-on-marble retail feel comes from the brand color alone; a deeper umber accent gives the label-print contrast." }, + { id: "seed-043", oklch: [0.781, 0.119, 38.1], + mood: "Aesop apothecary catalogue — considered terracotta, dermatological restraint, the warm color doing all the work against clinical white", + strategy: "Pure white surface lets the seed's warm clay tone read as the entire brand voice, paired with a deeper umber accent for hierarchy without competing with the primary's warmth." }, + { id: "seed-168", oklch: [0.400, 0.103, 50.0], + mood: "Aesop apothecary bottle — amber glass on a clinical white shelf, considered and pharmaceutical", + strategy: "Pure white surface lets the deep amber primary act like tinted glass against a clean shelf; accent is a muted clay that complements without competing, keeping the brand quiet and product-led." }, + { id: "seed-044", oklch: [0.568, 0.149, 45.9], + mood: "1970s desert highway at golden hour — sun-faded terracotta, denim dust, the warmth of a Polaroid pulled from a glovebox", + strategy: "Seed becomes a burnt-sienna primary against pure white so the terracotta does all the emotional work; a deep indigo accent acts as the denim shadow opposing the sun, creating the era's signature warm/cool tension without tinting the page." }, + { id: "seed-045", oklch: [0.607, 0.163, 47.7], + mood: "Aesop apothecary shelf — considered amber glass, clinical restraint, craft pharmacy", + strategy: "Pure white bg lets the burnt-amber primary do the apothecary work alone, paired with a deeper umber accent and graphite ink for editorial calm." }, + { id: "seed-046", oklch: [0.653, 0.175, 45.0], + mood: "Aesop apothecary shelf — considered amber glass, quiet luxury, restrained craft", + strategy: "Pure black backdrop lets the warm amber primary glow like backlit apothecary glass, with a deeper rust accent providing tonal depth in the same hue family — monochromatic warm against neutral void." }, + { id: "seed-047", oklch: [0.695, 0.205, 43.2], + mood: "Aesop apothecary label — sun-warmed amber glass on a clinical countertop, restrained botanical pharmacy", + strategy: "Pure white surface lets the burnt-amber primary and a deeper sienna accent do all the brand work, like an apothecary bottle photographed under daylight." }, + { id: "seed-051", oklch: [0.704, 0.189, 49.0], + mood: "blacksmith's forge at dusk — glowing iron, hammered copper, ember light against cooling steel", + strategy: "Pure near-black surface lets the seed's molten orange burn like heated metal; accent shifts to a deeper amber-red to suggest the cooling end of the same iron, while ink stays a clean off-white so type reads like chalk on slate." }, + { id: "seed-171", oklch: [0.550, 0.124, 60.0], + mood: "Klim Type Foundry specimen page — considered ochre on paper, design-school-honest", + strategy: "Seed becomes a muted ochre primary on pure white; accent is a deep ink-navy pulled across the wheel for editorial contrast without warmth-pooling in the bg" }, + { id: "seed-148", oklch: [0.650, 0.146, 60.0], + mood: "Klim-style editorial gold — late-afternoon paper light on a serif specimen sheet, considered and dry", + strategy: "Hold the seed's amber as primary on a pure white page so the gold reads as ink rather than atmosphere, and pair with a deep aubergine accent for typographic contrast." }, + { id: "seed-052", oklch: [0.700, 0.130, 60.0], + mood: "late-afternoon terracotta studio — sun-warmed clay, hands-on craft, the hour before dusk", + strategy: "Seed is a saturated amber-ochre with strong environmental association (ceramics, adobe, sunlit plaster), so I lean into Exception (a) with a faintly warm bone surface that reads as lime-washed wall, then deepen the seed slightly for primary and pair it with a fired-clay rust accent for hand-thrown warmth." }, + { id: "seed-053", oklch: [0.773, 0.157, 56.6], + mood: "late-summer apricot orchard at golden hour — sun-warmed fruit, considered Californian craft", + strategy: "Seed is a juicy mid-warm orange at daylight luminance — leaning optimistic/editorial, so pure white surface lets the apricot primary glow without muddying it; a deep wine accent provides the bite." }, + { id: "seed-149", oklch: [0.600, 0.124, 70.0], + mood: "1970s desert highway — late-afternoon amber light on chrome and asphalt", + strategy: "Anchor the amber seed as primary against pure black so the warm hue reads as headlight glow against night; a cooler dusk-mauve accent provides the complementary tension of horizon vs. sun." }, + { id: "seed-054", oklch: [0.740, 0.162, 68.1], + mood: "late-afternoon honey on terracotta — Mediterranean stucco at golden hour, sun-baked amber", + strategy: "Seed is a saturated honey-amber at high lightness; pairing it with pure black lets the warmth read as luminous gold against gravity, like lamplight in a dark room." }, + { id: "seed-055", oklch: [0.774, 0.174, 65.1], + mood: "late-summer honey hour — amber light slanting through a west-facing window, optimistic and golden", + strategy: "Anchor a saturated honey-amber primary on pure white so the warmth radiates from the brand itself, then pair with a deep teak accent for grounded contrast rather than tinting the canvas." }, + { id: "seed-056", oklch: [0.691, 0.146, 74.6], + mood: "Klim-style modern publishing house — late-afternoon paper warmth, considered editorial gold", + strategy: "Pure white surface so the amber seed becomes the brand voice; ink stays near-black neutral and accent shifts to a deep ink-blue to give the gold something structural to lean on." }, + { id: "seed-150", oklch: [0.750, 0.148, 80.0], + mood: "Klim Type Foundry specimen page — late-summer editorial gold, considered and grown-up", + strategy: "Pure white surface lets a single restrained ochre primary do all the brand work, paired with a deep ink-blue accent for typographic contrast in the Klim/Commercial Type tradition." }, + { id: "seed-058", oklch: [0.764, 0.120, 77.1], + mood: "Klim Type Foundry specimen page — late-afternoon ochre, considered editorial typography", + strategy: "Pure white surface lets the ochre primary do the brand work, paired with a deep ink-blue accent for editorial contrast — the type-foundry move where one warm hue carries the whole feeling against neutral paper." }, + { id: "seed-059", oklch: [0.784, 0.144, 79.8], + mood: "late afternoon in a Tuscan limonaia — sun-cured amber on whitewashed plaster", + strategy: "Pure white surface lets the saffron-amber primary and a deep olive accent carry the Mediterranean warmth, with split-complementary tension between gold and a quiet evergreen." }, + { id: "seed-061", oklch: [0.817, 0.161, 75.1], + mood: "late-afternoon honey on Tuscan limestone — golden hour, slow and luminous", + strategy: "Pure white surface lets the amber primary glow like sunlight on a wall, paired with a deep terracotta accent for warm tonal contrast within the same hue family." }, + { id: "seed-063", oklch: [0.842, 0.165, 91.3], + mood: "late-afternoon Tuscan sun on limestone — golden hour, considered, optimistic", + strategy: "Pure white surface lets the amber-gold primary radiate as the mood-carrier, with a deep aubergine accent providing the long shadow that golden light needs to feel three-dimensional." }, + { id: "seed-174", oklch: [0.350, 0.075, 110.0], + mood: "olive grove at late afternoon — sun-cured leaves, dust, and quiet Mediterranean weight", + strategy: "Pure white surface lets a deep, sun-cured olive primary do the emotional work, with a burnt-terracotta accent providing the warm-earth counterpoint olive groves are known for." }, + { id: "seed-117", oklch: [0.650, 0.100, 110.0], + mood: "Klim-style editorial sage — late-summer foundry catalogue, considered olive-yellow on paper", + strategy: "Seed sits at olive-chartreuse; treating it as a quiet typographic primary on pure paper, with a deeper bronze-olive accent for hierarchy — the color does the work, the page disappears." }, + { id: "seed-118", oklch: [0.750, 0.090, 110.0], + mood: "Klim Type Foundry specimen page — late-summer olive light on a working specimen, the honesty of a type designer showing their work", + strategy: "Pure white bg lets a desaturated olive-yellow primary do the editorial work, with a deeper olive-bronze accent providing typographic emphasis the way a specimen uses one heavy weight against the body roman." }, + { id: "seed-065", oklch: [0.797, 0.166, 113.1], + mood: "late-summer olive grove at noon — sun-bleached leaves, dry stone, Mediterranean glare", + strategy: "Hold the seed as a luminous chartreuse-olive primary against pure white so the color reads as sunlit foliage, pairing it with a deep umber accent for the dry-stone contrast." }, + { id: "seed-176", oklch: [0.300, 0.071, 120.0], + mood: "moss-darkened apothecary jar — herbal, shadowed, mid-19th-century botanical study", + strategy: "Seed is a deep desaturated olive-green that reads as preserved botanical pigment; I anchor it on pure white so the dim moss-green primary feels like ink on a herbarium page, with a warm ochre accent supplying the aged-paper counterpoint." }, + { id: "seed-155", oklch: [0.550, 0.142, 130.0], + mood: "moss-bed forest floor at noon — chlorophyll, lichen, sunlit fern", + strategy: "Seed is a confident mid-olive green with strong chroma; mood is daylight botanical, so I let the brand greens do the work on a pure paper-white bg and pair with a warm umber accent for fern-against-bark contrast." }, + { id: "seed-119", oklch: [0.600, 0.154, 130.0], + mood: "moss garden at Saihō-ji — damp stone, filtered green light through old cedar", + strategy: "Pure near-black bg lets the seed's mossy green glow like wet lichen under low light; accent shifts to a pale ochre-gold like sun catching through canopy." }, + { id: "seed-179", oklch: [0.300, 0.096, 140.0], + mood: "moss on wet stone — forest floor at dusk, deep botanical hush", + strategy: "Kept the seed's deep moss green as primary against a near-black surface so the green reads as living shadow, with a pale lichen accent providing the single point of light." }, + { id: "seed-180", oklch: [0.350, 0.110, 140.0], + mood: "moss-darkened apothecary — herbal tinctures in amber glass, pressed botanicals, the deep green of a conservatory at dusk", + strategy: "Near-black bg with a whisper of green undertone lets the seed's deep moss read as luminous foliage; a warm parchment accent provides the apothecary-label counterpoint without breaking the herbal register." }, + { id: "seed-120", oklch: [0.650, 0.100, 140.0], + mood: "moss on weathered stone — quiet botanical garden conservatory at midday", + strategy: "Pure white bg lets the muted sage-green primary read as a considered botanical mark, with a deeper terracotta accent providing earthen counterpoint without breaking the gallery-like restraint." }, + { id: "seed-121", oklch: [0.750, 0.090, 140.0], + mood: "moss garden at Saihō-ji — diffuse green light filtered through wet stone and lichen", + strategy: "Pure near-black bg lets the muted sage-green primary glow like lichen under low light; a warm pale-bone accent acts as the single ray of sun cutting through canopy." }, + { id: "seed-182", oklch: [0.400, 0.106, 150.0], + mood: "moss garden at Saiho-ji — deep cultivated green under wet stone shadow, contemplative and damp", + strategy: "Near-black bg with the faintest cool-green undertone evokes shaded stone; primary holds the seed's moss tone while accent shifts to a lichen-yellow for organic counterpoint without breaking the hush." }, + { id: "seed-157", oklch: [0.550, 0.145, 150.0], + mood: "moss garden at Saiho-ji — damp stone, filtered green light through cedar canopy", + strategy: "Near-black bg with a faint green undertone evokes deep forest shadow; primary holds the seed's verdant register while accent shifts to a pale lichen-cream to mimic light catching moss." }, + { id: "seed-122", oklch: [0.600, 0.158, 150.0], + mood: "forest floor at first light — moss, lichen, and clean morning air", + strategy: "Seed reads as a living, daylight green; surface stays pure white so the green carries the freshness, with a cool teal accent pulling it toward dew rather than earth." }, + { id: "seed-195", oklch: [0.650, 0.150, 145.0], + mood: "Considered horticulture brand — botanical research lab, the green of a healthy stem photographed in clean daylight", + strategy: "Pure white surface lets the seed's vegetal green carry the entire brand voice, paired with a deep forest ink and a warm clay accent for editorial contrast." }, + { id: "seed-183", oklch: [0.350, 0.077, 160.0], + mood: "moss-stained apothecary — deep forest glass, herbal tinctures shelved in low candlelight", + strategy: "Anchored the seed as primary and built a near-black dark surface with whisper-tinted green to evoke aged apothecary glass, letting the green glow rather than shout." }, + { id: "seed-184", oklch: [0.400, 0.087, 160.0], + mood: "deep forest apothecary — moss, bottle glass, and herbal tincture under afternoon light", + strategy: "Seed becomes a botanical-bottle-green primary on pure white, paired with a warm clove-amber accent to evoke herbal pharmacy contrast without tinting the surface." }, + { id: "seed-158", oklch: [0.550, 0.119, 160.0], + mood: "moss on wet stone — forest floor after rain, mineral and quiet", + strategy: "Pure white surface lets the deep mossy green carry the entire mood; accent shifts to a damp slate-teal to sit beside primary like lichen on stone without competing." }, + { id: "seed-159", oklch: [0.600, 0.130, 160.0], + mood: "moss-covered forest apothecary — herbal tinctures in amber glass, eucalyptus shadow", + strategy: "Anchored the green seed in a near-black backdrop so it reads like botanical glassware lit from within, with a warm amber accent pulled across the wheel to evoke tincture bottles against dark wood." }, + { id: "seed-185", oklch: [0.450, 0.086, 170.0], + mood: "weathered copper patina on a Pacific Northwest greenhouse — oxidized teal, glass light, botanical hush", + strategy: "Seed sits as a deep oxidized-teal primary against pure white so the patina reads as pigment, not atmosphere; a rust-copper accent completes the verdigris/oxidation story across the warm-cool axis." }, + { id: "seed-124", oklch: [0.750, 0.080, 170.0], + mood: "sea-glass on a foggy Pacific shoreline — weathered, mineral, quietly oxidized", + strategy: "Seed is a soft desaturated teal-green; pairing it on pure white lets the mineral primary read as patinated copper-glass, with a deeper kelp-toned primary and a rusted coral accent to spark the muted teal against its complement." }, + { id: "seed-160", oklch: [0.550, 0.095, 180.0], + mood: "weathered copper patina on a museum bronze — oxidized teal, conservatorial quiet", + strategy: "Pure near-black gallery surround lets the patina-teal primary glow like a lit artifact, with a warm verdigris-adjacent accent providing the oxidation contrast against the cool seed." }, + { id: "seed-161", oklch: [0.720, 0.100, 188.0], + mood: "climate-tech dashboard — calm verdigris on plain paper, the quiet confidence of an instrument that just works", + strategy: "Seed teal carries the entire mood as a single considered brand color on pure white, with a desaturated copper accent providing warm signal against the cool primary without competing for attention." }, + { id: "seed-186", oklch: [0.450, 0.074, 200.0], + mood: "deep hydrothermal vent — mineral teal under pressure, the cold blue-green of oxidized copper in submerged light", + strategy: "Near-black surface lets the mineral teal glow as if lit from within; accent shifts toward verdigris-copper to suggest patina on submerged metal, while ink stays cool-neutral to keep the register austere rather than aquatic-cute." }, + { id: "seed-125", oklch: [0.650, 0.100, 200.0], + mood: "climate-tech dashboard — calm operational teal, the color of clean water data and atmospheric sensors", + strategy: "Pure white surface lets a single muted-teal primary do all the brand work, with a deeper marine accent providing hierarchy without competing chroma." }, + { id: "seed-126", oklch: [0.750, 0.080, 200.0], + mood: "climate-tech product brand — quiet competence, dashboards for hard infrastructure problems", + strategy: "Hold the seed's muted teal as primary, pair with a sharper cyan-leaning accent for interactive lift, and let a pure white surface do the disappearing act so the brand reads as a tool, not an atmosphere." }, + { id: "seed-162", oklch: [0.550, 0.091, 210.0], + mood: "weathered nautical instrument — patinated brass on oxidized steel, the cool blue-grey of a ship's chronometer at dawn", + strategy: "Pure white surface lets the muted teal-steel primary read as a precise instrument mark, with a warm brass accent providing the single point of patina against clinical white." }, + { id: "seed-163", oklch: [0.450, 0.086, 230.0], + mood: "deep harbor at dusk — weathered nautical instruments, brass dials on oxidized steel", + strategy: "Near-black background with subtle cool tint evokes the marine dusk; primary holds the seed's teal-blue while a warm brass accent creates the instrument-on-steel tension." }, + { id: "seed-164", oklch: [0.550, 0.105, 230.0], + mood: "deep harbor at dawn — cold steel water, fog-muted light, the quiet before the boats leave", + strategy: "Pure near-black bg lets the seed's cold marine blue read as a luminous beacon, while a pale frost-cyan accent evokes diffused dawn light cutting through fog." }, + { id: "seed-127", oklch: [0.650, 0.100, 230.0], + mood: "climate-tech dashboard — atmospheric sensor blue, calm operational clarity", + strategy: "Anchor the seed as a confident mid-blue primary on pure white so the brand color carries all the atmospheric feeling, with a deep navy accent for hierarchy and a soft slate muted for body text." }, + { id: "seed-128", oklch: [0.750, 0.080, 230.0], + mood: "climate-tech dashboard — calm atmospheric data, considered sky-blue", + strategy: "Pure white surface lets the muted sky-blue primary carry the meteorological calm, with a deep-navy accent providing readable weight against the soft primary." }, + { id: "seed-187", oklch: [0.350, 0.078, 240.0], + mood: "deep harbor at blue hour — wet stone, cold steel, the quiet before night fully lands", + strategy: "Near-black architectural bg with a hint of marine chroma lets the seed read as ambient atmosphere rather than UI chrome; a cooler steel accent sits opposite the warmer-shifted primary for navigational clarity." }, + { id: "seed-077", oklch: [0.578, 0.130, 241.7], + mood: "pre-dawn signal tower — cold blue solitude, instruments glowing against the dark", + strategy: "Pure near-black bg lets the seed's cold tower-light blue glow as the sole emotional source, with a frost-cyan accent acting as a secondary indicator light." }, + { id: "seed-188", oklch: [0.400, 0.110, 250.0], + mood: "Linear's considered indigo — the calm authority of a well-built developer tool, blueprint ink on a clean page", + strategy: "Held the seed as a deep indigo primary against pure white so the brand color carries all the gravity; accent shifts to a cooler, brighter cyan-blue to create a crisp hierarchy pair without warming the surface." }, + { id: "seed-165", oklch: [0.450, 0.123, 250.0], + mood: "blueprint room at dusk — drafting table, graphite, civic-engineering blue", + strategy: "Seed is a mid-deep architectural blue with real chroma and no environmental cue, so I stay out of the way with a pure white surface and let the primary do all the talking, pairing it with a burnt-ochre accent for drafting-pencil contrast." }, + { id: "seed-079", oklch: [0.478, 0.136, 251.8], + mood: "twilight cartography — the blue of deep dusk over open water, precise and navigational", + strategy: "Pure white surface lets the seed's oceanic blue act as a single navigational anchor, with a warm amber accent struck across it like a lighthouse beam at dusk." }, + { id: "seed-080", oklch: [0.541, 0.122, 248.2], + mood: "Linear-style considered tool blue — the calm, exact register of a modern engineering app where every pixel is intentional", + strategy: "Pure white surface lets the considered indigo-blue primary carry the entire brand; a deeper navy accent provides hierarchy without warmth, keeping the palette in a single cool family for that focused-software feel" }, + { id: "seed-166", oklch: [0.550, 0.149, 250.0], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and quietly intense", + strategy: "Near-black bg with the faintest cool tint reads like a darkened cockpit; the seed becomes a luminous instrument-blue primary, paired with a warm amber accent that mimics avionics readouts for unmistakable signal contrast." }, + { id: "seed-081", oklch: [0.650, 0.160, 250.0], + mood: "deep-sea research vessel at dawn — instrument glow against cold steel light", + strategy: "Pure near-white bg keeps the palette technical and instrument-like; the seed blue holds as primary while a desaturated steel-cyan accent reads like signal readouts on glass." }, + { id: "seed-082", oklch: [0.742, 0.140, 247.4], + mood: "high-altitude flight deck at dawn — cold cabin instruments glowing against a sky still holding night", + strategy: "Near-black cockpit ground with a faint blue cast lets the seed read as an illuminated instrument; primary holds the seed, accent shifts to cyan for signal/indicator contrast." }, + { id: "seed-210", oklch: [0.360, 0.140, 260.0], + mood: "Linear-style considered tool indigo — late-night focused work, the deep blue of a code editor at 2am where everything else falls away", + strategy: "Pure black bg lets the indigo primary carry all the cognitive-focus weight, with a slightly brighter periwinkle accent for interactive lift — the surface disappears so the tool feels weightless." }, + { id: "seed-189", oklch: [0.400, 0.130, 260.0], + mood: "pre-dawn observatory — cold instrument blue, star-chart precision", + strategy: "Seed becomes the primary on pure black so the deep instrument-blue glows like a calibration light, with a faint cyan accent reading as starlight against the void." }, + { id: "seed-211", oklch: [0.420, 0.161, 260.0], + mood: "Linear's considered indigo — the tool-for-thought blue of focused product work, calm authority without coldness", + strategy: "Hold the seed as a deep indigo primary against pure white, then pair with a slightly warmer, lighter periwinkle accent to create gentle hue separation without breaking the disciplined tool-brand register." }, + { id: "seed-129", oklch: [0.450, 0.150, 260.0], + mood: "pre-dawn observatory — deep cobalt sky just before astronomical twilight, instruments cool to the touch", + strategy: "Near-black surface lets the cobalt seed read as luminous starlight; a single warm amber accent acts as the calibration lamp against the cold blue field." }, + { id: "seed-084", oklch: [0.476, 0.207, 261.2], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and awake", + strategy: "Default B black bg lets the cobalt primary read as a luminous instrument signal, with a cyan accent striking the analogous 'cockpit display' relationship." }, + { id: "seed-085", oklch: [0.681, 0.132, 258.4], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky", + strategy: "Anchored the seed as a luminous primary against a near-black architectural ground, with a warm amber accent acting as the single instrument light cutting through cold blue." }, + { id: "seed-086", oklch: [0.767, 0.106, 255.9], + mood: "Scandinavian winter morning — quiet light through frost, pale sky over snow", + strategy: "Anchored a pure white editorial stage so the seed's cool sky-blue reads as crisp polar light, with a deeper navy primary providing the only saturated weight — like a single dark pine against snow." }, + { id: "seed-083", oklch: [0.340, 0.159, 262.4], + mood: "deep cobalt twilight — the moment after sunset when the sky goes electric blue and city windows start to glow", + strategy: "Pure black stage lets the cobalt seed act as a luminous neon-window glow, with a warm amber accent across the wheel for the lit-window contrast." }, + { id: "seed-212", oklch: [0.360, 0.219, 270.0], + mood: "Linear-grade tooling indigo — considered software for people who care about craft", + strategy: "Anchored the deep indigo seed as primary on a pure white surface so the brand color carries all the weight, with a slightly cooler violet-blue accent for hierarchy without competing chroma." }, + { id: "seed-130", oklch: [0.400, 0.150, 270.0], + mood: "Linear-grade indigo — considered productivity tool, ink on paper, no theatrics", + strategy: "Pure white surface lets a deep cool indigo carry all the brand weight, paired with a slightly warmer violet-blue accent for hierarchy without acid." }, + { id: "seed-213", oklch: [0.411, 0.241, 267.9], + mood: "Linear-style indigo — considered tool surface, the kind of blue-violet that sits behind a developer's keyboard at 11pm without shouting", + strategy: "Pure black canvas lets a saturated indigo primary do all the brand work, with a cooler cyan-violet accent providing UI signal without competing." }, + { id: "seed-131", oklch: [0.450, 0.180, 270.0], + mood: "monastic indigo dusk — vespers light through stained glass, contemplative and severe", + strategy: "Seed becomes a deep indigo primary against pure near-black so the violet reads as luminous stained-glass against architectural shadow, with a cooler iris accent for tonal lift." }, + { id: "seed-088", oklch: [0.476, 0.158, 268.5], + mood: "pre-dawn astronomer's notebook — deep indigo sky just before the stars fade, ink and graphite", + strategy: "Near-black bg with the faintest cool tint to evoke night sky without theatrics; primary holds the seed's indigo, accent shifts to a paler periwinkle for stellar contrast, keeping the palette monochromatic-cool and observational." }, + { id: "seed-196", oklch: [0.530, 0.130, 268.0], + mood: "Linear-style considered tool indigo — the deep-focus blue-violet of a thoughtfully built productivity surface, the color of a well-typeset keyboard shortcut", + strategy: "Pure white bg lets the indigo seed do all the brand work as primary, with a slightly darker, more saturated violet-shifted accent for hierarchy and interactive states — the surface disappears so the brand color reads as the entire identity." }, + { id: "seed-132", oklch: [0.700, 0.120, 270.0], + mood: "Linear-style considered tool indigo — the quiet violet of a focused product workspace, late-afternoon thinking", + strategy: "Pure white surface lets a muted indigo-violet primary and a slightly cooler accent do all the brand work, keeping the register calm and software-like rather than theatrical." }, + { id: "seed-090", oklch: [0.445, 0.206, 279.1], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, not a nightclub", + strategy: "Anchor the seed as a confident product primary on pure white, with a cooler indigo-shift accent that reads as a sibling tool color, so the brand violet does all the emotional work." }, + { id: "seed-133", oklch: [0.500, 0.160, 280.0], + mood: "Linear-adjacent indigo — considered productivity tool, the violet of a thinking workspace", + strategy: "Seed becomes a measured indigo primary on pure white; accent shifts to a cooler blue-violet to create hierarchy without nightclub saturation, letting the brand color do all the emotional work." }, + { id: "seed-094", oklch: [0.533, 0.125, 294.3], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, calm authority for a creative workspace", + strategy: "Pure white canvas lets the indigo-violet primary carry the entire brand voice; accent shifts hue slightly toward blue for a cool, tool-like duotone rather than warm decorative pairing." }, + { id: "seed-137", oklch: [0.700, 0.120, 290.0], + mood: "Linear-adjacent indigo — the considered tool, late-evening focus mode, software made for people who care about craft", + strategy: "Pure black surface lets a single restrained indigo-violet carry the brand, with a cooler periwinkle accent providing UI hierarchy without competing — Vercel/Linear dark-mode discipline." }, + { id: "seed-100", oklch: [0.450, 0.150, 330.0], + mood: "velvet boudoir at last call — bruised orchid and lipstick traces under low lamplight", + strategy: "Pure near-black surface lets a deep magenta-rose primary smolder while a warm peach accent acts like skin-lit lamplight — drama lives in the brand pair, not the room." }, + { id: "seed-103", oklch: [0.650, 0.160, 330.0], + mood: "1980s Memphis boudoir — powder-pink neon humming against lacquered black, lipstick and lacquer", + strategy: "Near-black gallery surface lets the magenta-pink seed read as lit neon; accent shifts to warm coral to create cinematic dichromatic tension without competing chroma." }, + { id: "seed-228", oklch: [0.360, 0.147, 340.0], + mood: "Figma-era creative tool plum — considered productivity software for designers, the inky violet of a serif wordmark on a marketing site", + strategy: "Held the seed as a deep plum primary against pure white so the brand color does the emotional work; paired with a muted rose accent for warmth without breaking the productivity-tool restraint." }, + { id: "seed-107", oklch: [0.500, 0.200, 340.0], + mood: "Figma plum — creative-tool confidence, considered magenta for a modern design product", + strategy: "Pure white surface lets a saturated magenta-plum primary carry all the brand voice, paired with a cooler violet-leaning accent for hierarchy without competing." }, + { id: "seed-198", oklch: [0.600, 0.210, 340.0], + mood: "Figma-era creative tool plum — confident, considered, made for makers", + strategy: "Anchor a saturated plum primary against pure white so the brand color does all the emotional work, with a deeper magenta-rose accent for hierarchy." }, + { id: "seed-112", oklch: [0.754, 0.193, 343.4], + mood: "Figma-era creative tool — confident pink primary doing the brand work on a clean canvas, the way Linear uses indigo or Stripe uses violet", + strategy: "Anchor the seed pink as a saturated brand primary on pure white so the color carries all the personality; pair with a cooler plum accent to give the pink something to push against without competing." }, + { id: "seed-229", oklch: [0.420, 0.163, 350.0], + mood: "considered fintech rose — the deep magenta of a modern product brand (think Stripe-adjacent, but rotated toward berry), confident and current", + strategy: "pure white surface lets a single deep berry-rose primary do all the brand work, paired with a cooler indigo accent for the contrast move you see in modern product marketing" }, + { id: "seed-113", oklch: [0.470, 0.173, 354.8], + mood: "1960s velvet rope nightclub — crushed magenta, low light, cigarette smoke catching a spotlight", + strategy: "Pure black stage so the seed's smoky magenta reads as a single hot spotlight, paired with a cooler violet accent for the second light cue." }, + { id: "seed-114", oklch: [0.570, 0.158, 353.3], + mood: "fin-de-siècle Parisian rose — velvet curtain, theatre program, lipstick blotted on linen", + strategy: "Drop bg to true black so the dusty-rose primary reads as stage-lit silk; accent shifts to a warmer coral-mauve at higher lightness to create gentle hue rotation without breaking the romance." }, + { id: "seed-199", oklch: [0.650, 0.180, 350.0], + mood: "modern fintech rose — the considered pink of a Series B brand mark, confident and current without nostalgia", + strategy: "Pure white surface lets a saturated rose primary do the brand work, paired with a deep plum accent for hierarchy — the Stripe move applied to a pink hue." }, + { id: "seed-115", oklch: [0.636, 0.218, 355.3], + mood: "backstage at a cabaret — velvet rope, lipstick mark on a champagne glass", + strategy: "Seed reads as a saturated stage-light magenta-red; I push it into pure black so the primary glows like a neon sign and the accent (a cold pearl-pink) acts as the spotlight rim — the room is dark, the color does the singing." }, + { id: "seed-230", oklch: [0.650, 0.249, 354.5], + mood: "Modern fintech rose — the considered pink of a contemporary payments brand: confident, alive, and clear-headed", + strategy: "Pure white bg lets a saturated rose-magenta primary carry all the brand energy, paired with a cooler indigo accent for trustworthy contrast — the Stripe move applied to a pink hue." }, + { id: "seed-231", oklch: [0.682, 0.241, 353.2], + mood: "Figma-era creative tool — a confident pink-magenta product brand, the kind a modern design platform uses to feel alive without shouting", + strategy: "Default A pure white bg lets the saturated pink-magenta primary do all the brand work, with a near-complementary cool teal accent for tool-like clarity and a neutral ink for editorial calm" }, + { id: "seed-116", oklch: [0.734, 0.183, 356.8], + mood: "modern beauty brand DTC — Glossier-adjacent pink, confident and current without being saccharine", + 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." }, +]; + +function parseArgs(argv) { + const args = { id: null, from: null }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === '--id' && argv[i + 1]) { args.id = argv[++i]; } + else if (a === '--from' && argv[i + 1]) { args.from = argv[++i]; } + } + return args; +} + +// Hash a key into a stable float in [0, 1) for deterministic weighted picks. +function hashUnit(key) { + const h = crypto.createHash('sha256').update(key).digest(); + return h.readUInt32BE(0) / 0x100000000; +} + +// The curated library is hue-skewed (more reds/oranges than teals/magentas) +// because that's where the source material + taste landed. Left uniform, a +// random pick would land on red ~1/3 of the time. Inverse-frequency weighting +// gives each seed a weight of 1/(count in its 30° hue bucket), so each hue +// ZONE is roughly equally likely to be chosen regardless of how many seeds it +// holds — fair rainbow exposure across runs without pruning the library. +function buildWeights(seeds) { + const bucketCount = {}; + const bucketOf = (s) => Math.floor(((s.oklch[2] % 360) + 360) % 360 / 30); + for (const s of seeds) { const b = bucketOf(s); bucketCount[b] = (bucketCount[b] || 0) + 1; } + const weights = seeds.map((s) => 1 / bucketCount[bucketOf(s)]); + const total = weights.reduce((a, b) => a + b, 0); + return { weights, total }; +} + +function weightedPick(seeds, unit) { + const { weights, total } = buildWeights(seeds); + let target = unit * total; + for (let i = 0; i < seeds.length; i++) { + target -= weights[i]; + if (target < 0) return seeds[i]; + } + return seeds[seeds.length - 1]; +} + +function pickSeed(seeds, { id, from }) { + if (id) { + const found = seeds.find(s => s.id === id); + if (!found) { console.error(`no seed with id "${id}"`); process.exit(2); } + return found; + } + const envFrom = process.env.IMPECCABLE_PALETTE_SEED; + const key = from || envFrom; + const unit = key ? hashUnit(key) : Math.random(); + return weightedPick(seeds, unit); +} + +function fmtOklch([L, C, H]) { + return `oklch(${L.toFixed(3)} ${C.toFixed(3)} ${H.toFixed(1)})`; +} + +function hueWord(H) { + if (H < 15 || H >= 345) return 'pure red'; + if (H < 35) return 'warm red / crimson'; + if (H < 55) return 'warm coral / burnt orange'; + if (H < 80) return 'orange / honey'; + if (H < 105) return 'warm amber / honey-gold'; + if (H < 135) return 'yellow-green / olive'; + if (H < 170) return 'green'; + if (H < 200) return 'teal'; + if (H < 230) return 'sky blue'; + if (H < 265) return 'cobalt / indigo'; + if (H < 295) return 'violet / purple'; + if (H < 330) return 'magenta / pink'; + return 'deep pink / rose'; +} + +// --------------------------------------------------------------- + +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}` : ''; + +// --------------------------------------------------------------- +// Fat tool-exit response — what the model sees on stdout. +// --------------------------------------------------------------- + +process.stdout.write(`BRAND SEED · ${seed.id} + +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. + +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. + +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é. + + 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 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. + + 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. + +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): + + - 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. + +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. + +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. +`); diff --git a/.cursor/skills/impeccable/scripts/pin.mjs b/.cursor/skills/impeccable/scripts/pin.mjs index ba02783fb..320d98ce1 100644 --- a/.cursor/skills/impeccable/scripts/pin.mjs +++ b/.cursor/skills/impeccable/scripts/pin.mjs @@ -27,7 +27,7 @@ const HARNESS_DIRS = [ // Valid sub-command names const VALID_COMMANDS = [ - 'craft', 'teach', 'extract', 'document', 'shape', + 'craft', 'init', 'extract', 'document', 'shape', 'critique', 'audit', 'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live', 'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive', diff --git a/.gemini/skills/impeccable/SKILL.md b/.gemini/skills/impeccable/SKILL.md index bbff6469e..9ae6bd7ae 100644 --- a/.gemini/skills/impeccable/SKILL.md +++ b/.gemini/skills/impeccable/SKILL.md @@ -1,92 +1,89 @@ --- name: impeccable description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks. -version: 3.1.1 +version: 3.5.0 --- Designs and iterates production-grade frontend interfaces. Real working code, committed design choices, exceptional craft. ## Setup -Before any design work or file edits: +You MUST do these steps before proceeding: -1. Load context (PRODUCT.md / DESIGN.md) via the loader script. -2. Identify the register and load the matching register reference (brand.md or product.md). -3. **If the user invoked a sub-command (e.g. `craft`, `shape`, `audit`), load its reference file too.** This is non-negotiable: `craft` without `craft.md` loaded means you'll skip the shape-and-confirm step the user expects. +1. Run `node .gemini/skills/impeccable/scripts/context.mjs` once per session. If you've already seen its output in this conversation, do not re-run it. The script either prints the project's PRODUCT.md (and DESIGN.md when present) as a markdown block, or tells you it's missing. Follow whatever it prints. **If it reports `NO_PRODUCT_MD`, stop and follow `reference/init.md` before doing anything else.** If the output ends with an `UPDATE_AVAILABLE` directive, follow it (ask the user once about updating, then continue). It never blocks the current task. +2. If the user invoked a sub-command (`craft`, `shape`, `audit`, `polish`, ...), you MUST read `reference/.md` next. Non-optional. The reference defines the command's flow; without it you will skip steps the user expects. +3. Familiarize yourself with any existing design system, conventions, and components in the code. Read at least one project file (CSS / tokens / theme / a representative component or page). **Required even when you've loaded a sub-command reference in step 2.** Don't reinvent the wheel; use what's there when it works, branch out when the UX wins. +4. Read the matching register reference. **This is non-optional; skipping it produces generic output.** If the project is marketing, a landing page, a campaign, long-form content, or a portfolio (design IS the product), read `reference/brand.md`. If it is app UI, admin, a dashboard, or a tool (design SERVES the product), read `reference/product.md`. Pick by first match: (1) task cue ("landing page" vs "dashboard"); (2) surface in focus (the page, file, or route being worked on); (3) `register` field in PRODUCT.md. +5. **If the project is brand-new (no existing CSS tokens / theme / committed brand colors found in step 3)**, run `node .gemini/skills/impeccable/scripts/palette.mjs` to receive a brand seed color and composition guidance. This is the anchor for your primary brand color. Compose the rest of the palette (bg, surface, ink, accent, muted) around it per the script's instructions. Use OKLCH throughout. **Skip this step only if step 3 found committed brand colors in existing tokens; in that case identity-preservation wins.** -Skipping these produces generic output that ignores the project. +## Design guidance -### 1. Context gathering +Produce ready-to-ship, production-grade code, not prototypes or starting points. Take no shortcuts unless the user asks for them (when in doubt, ask). Don't stop until arriving at a complete implementation (beautiful, responsive, fast, precise, bug-free, on brand). You take attention to detail seriously: every page, section or component crafted is battle tested using the tools available to you (browser screenshotting, computer use, etc). Gemini is capable of extraordinary work. Don't hold back. -Two files, case-insensitive. The loader looks at the project root by default and falls back to `.agents/context/` and `docs/` if the root is clean. Override with `IMPECCABLE_CONTEXT_DIR=path/to/dir` (absolute or relative to cwd). +### General rules -- **PRODUCT.md**: required. Users, brand, tone, anti-references, strategic principles. -- **DESIGN.md**: optional, strongly recommended. Colors, typography, elevation, components. +#### Color -Load both in one call: +- **Verify contrast.** Body text must hit ≥4.5:1 against its background; large text (≥18px or bold ≥14px) needs ≥3:1. Placeholder text needs the same 4.5:1, not the muted-gray default. The most common failure: muted gray body text on a tinted near-white. If the contrast is even close, bump the body color toward the ink end of the ramp; light gray "for elegance" is the single biggest reason AI designs feel hard to read. +- Gray text on a colored background looks washed out. Use a darker shade of the background's own hue, or a transparency of the text color. -```bash -node .gemini/skills/impeccable/scripts/load-context.mjs -``` +#### Typography -Consume the full JSON output. Never pipe through `head`, `tail`, `grep`, or `jq`. The output's `contextDir` field tells you where the files were resolved from. +- Cap body line length at 65–75ch. +- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales. +- Cap font-family count at 3 (display + body + optional mono). More than 3 reads as indecision, not richness. One well-tuned family with weight contrast usually beats three competing typefaces. +- Don't pair fonts that are similar but not identical (two geometric sans-serifs, two humanist sans-serifs). Pair on a contrast axis (serif + sans, geometric + humanist) or use one family in multiple weights. +- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. +- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. +- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". +- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. -If the output is already in this session's conversation history, don't re-run. Exceptions requiring a fresh load: you just ran `/impeccable teach` or `/impeccable document` (they rewrite the files), or the user manually edited one. +#### Layout -`/impeccable live` already warms context via `live.mjs`. If you've run `live.mjs`, don't also run `load-context.mjs` this session. +- Vary spacing for rhythm. +- Cards are the lazy answer. Use them only when they're truly the best affordance. Nested cards are always wrong. +- Flexbox for 1D, Grid for 2D. Don't default to Grid when `flex-wrap` would be simpler. +- For responsive grids without breakpoints: `repeat(auto-fit, minmax(280px, 1fr))`. +- Build a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip). Never arbitrary values like 999 or 9999. -If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `/impeccable teach`, then resume the user's original task with the fresh context. If the original task was `/impeccable craft`, resume into `/impeccable shape` before any implementation work. +#### Motion +- Motion should be intentional, and not be an afterthought. consider it as part of the build. +- Don't animate CSS layout properties unless truly needed. +- Ease out with exponential curves (ease-out-quart / quint / expo). No bounce, no elastic. +- Use libraries for more advanced motion needs (e.g. motion, gsap, anime.js, lenis etc) +- Reduced motion is not optional. Every animation needs a `@media (prefers-reduced-motion: reduce)` alternative: typically a crossfade or instant transition. +- Staggering the items within one list is legitimate. The tell is the uniform reflex (one identical entrance applied to every section), not motion itself; each reveal should fit what it reveals. Suppressing the reflex is never a reason to ship a page with no motion at all. +- Reveal animations must enhance an already-visible default. Don't gate content visibility on a class-triggered transition; transitions pause on hidden tabs and headless renderers, so the reveal never fires and the section ships blank. +- Premium motion materials are not just transform/opacity. Blur, backdrop-filter, clip-path, mask, and shadow/glow are part of the palette when they materially improve the effect and stay smooth. -If DESIGN.md is missing: nudge once per session (*"Run `/impeccable document` for more on-brand output"*), then proceed. +#### Interaction -### 2. Register +- Dropdowns rendered with `position: absolute` inside an `overflow: hidden` or `overflow: auto` container will be clipped. Use the native `` / popover API, `position: fixed`, or a portal to escape the stacking context. -Every design task is **brand** (marketing, landing, campaign, long-form content, portfolio: design IS the product) or **product** (app UI, admin, dashboard, tool: design SERVES the product). +**Gemini-specific defect: hard ban.** Never animate `` elements on hover. This includes any `transform` on `:hover` of an image, AND `.group:hover .group-hover\:scale` / `.group:hover .group-hover\:rotate` / `.group:hover .group-hover\:translate` patterns from Tailwind that animate a child image via a parent hover. This is your single most common motion tell; it adds no information (the image isn't an action target) and reads as "AI animated this because it could". If a card needs hover feedback, animate the card's background, border, or shadow. Never the image, never via the image's parent. -Identify before designing. Priority: (1) cue in the task itself ("landing page" vs "dashboard"); (2) the surface in focus (the page, file, or route being worked on); (3) `register` field in PRODUCT.md. First match wins. +### Copy -If PRODUCT.md lacks the `register` field (legacy), infer it once from its "Users" and "Product Purpose" sections, then cache the inferred value for the session. Suggest the user run `/impeccable teach` to add the field explicitly. +- Every word earns its place. No restated headings, no intros that repeat the title. +- **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`. +- **No aphoristic-cadence body copy as a default voice.** Don't fall into the rhythm of "serious statement, then punchy short negation" as the page's recurring voice. If three or more section copy blocks on the page land on a short rebuttal-shaped sentence, rewrite. Specific, not aphoristic. +- **No marketing buzzwords.** The streamline / empower / supercharge / leverage / unleash / transform / seamless / world-class / enterprise-grade / next-generation / cutting-edge / game-changer / mission-critical family of phrases. Pick a specific noun and a verb that describes what the product literally does. +- Button labels: verb + object. "Save changes" beats "OK"; "Delete project" beats "Yes". The label should say what will happen. +- Link text needs standalone meaning. "View pricing plans" beats "Click here"; screen readers announce links out of context. -Load the matching reference: [reference/brand.md](reference/brand.md) or [reference/product.md](reference/product.md). The shared design laws below apply to both. +### New projects only (when no prior work exists) -## Shared design laws +#### Color & Theme -Apply to every design, both registers. Match implementation complexity to the aesthetic vision: maximalism needs elaborate code, minimalism needs precision. Interpret creatively. Vary across projects; never converge on the same choices. Gemini is capable of extraordinary work. Don't hold back. - -### Color - -- Use OKLCH. Reduce chroma as lightness approaches 0 or 100; high chroma at extremes looks garish. -- Never use `#000` or `#fff`. Tint every neutral toward the brand hue (chroma 0.005–0.01 is enough). +- Use OKLCH. +- **The cream / sand / beige body bg is the saturated AI default of 2026.** The whole warm-neutral band (OKLCH L 0.84-0.97, C < 0.06, hue 40-100) reads as cream/sand/paper/parchment regardless of what you call it. Token names like `--paper`, `--cream`, `--sand`, `--bone`, `--flour`, `--linen`, `--parchment`, `--wheat`, `--biscuit`, `--ivory` are tells in themselves. If the brief is "warm, traditional, family-coastal-Italian" or "magazine-warm" or "editorial-restraint", DO NOT translate that into a near-white warm-tinted bg; that's the AI move. Pick: (a) a saturated brand color as the body (terracotta, oxblood, deep ochre, near-black), (b) a true off-white at chroma 0 (or chroma toward the brand's own hue, not toward warmth-by-default), or (c) a darker mid-tone tinted neutral that's clearly the brand's own. "Warmth" in the brand is carried by accent + typography + imagery, not by body bg. +- Tinted neutrals: add 0.005–0.015 chroma toward the brand's hue. Don't default-tint toward warm or cool "because the brand feels that way"; that's the cross-project monoculture move. +- When picking a theme: Dark vs. light is never a default. Not dark "because tools look cool dark." Not light "to be safe.".Before choosing, write one sentence of physical scene: who uses this, where, under what ambient light, in what mood. If the sentence doesn't force the answer, it's not concrete enough. Add detail until it does. - Pick a **color strategy** before picking colors. Four steps on the commitment axis: - **Restrained**: tinted neutrals + one accent ≤10%. Product default; brand minimalism. - **Committed**: one saturated color carries 30–60% of the surface. Brand default for identity-driven pages. - **Full palette**: 3–4 named roles, each used deliberately. Brand campaigns; product data viz. - **Drenched**: the surface IS the color. Brand heroes, campaign pages. -- The "one accent ≤10%" rule is Restrained only. Committed / Full palette / Drenched exceed it on purpose. Don't collapse every design to Restrained by reflex. - -### Theme - -Dark vs. light is never a default. Not dark "because tools look cool dark." Not light "to be safe." - -Before choosing, write one sentence of physical scene: who uses this, where, under what ambient light, in what mood. If the sentence doesn't force the answer, it's not concrete enough. Add detail until it does. - -"Observability dashboard" does not force an answer. "SRE glancing at incident severity on a 27-inch monitor at 2am in a dim room" does. Run the sentence, not the category. - -### Typography - -- Cap body line length at 65–75ch. -- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales. - -### Layout - -- Vary spacing for rhythm. Same padding everywhere is monotony. -- Cards are the lazy answer. Use them only when they're truly the best affordance. Nested cards are always wrong. -- Don't wrap everything in a container. Most things don't need one. - -### Motion - -- Don't animate CSS layout properties. -- Ease out with exponential curves (ease-out-quart / quint / expo). No bounce, no elastic. ### Absolute bans @@ -97,12 +94,9 @@ Match-and-refuse. If you're about to write any of these, rewrite the element wit - **Glassmorphism as default.** Blurs and glass cards used decoratively. Rare and purposeful, or nothing. - **The hero-metric template.** Big number, small label, supporting stats, gradient accent. SaaS cliché. - **Identical card grids.** Same-sized cards with icon + heading + text, repeated endlessly. -- **Modal as first thought.** Modals are usually laziness. Exhaust inline / progressive alternatives first. - -### Copy - -- Every word earns its place. No restated headings, no intros that repeat the title. -- **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`. +- **Tiny uppercase tracked eyebrow above every section.** The 2023-era kicker (small all-caps text with wide tracking, "ABOUT" "PROCESS" "PRICING" above each heading) is now the saturated AI scaffold; it appears on 55-95% of generations regardless of brief, which is the definition of a tell. One named kicker as a deliberate brand system is voice; an eyebrow on every section is AI grammar. Choose a different cadence. +- **Numbered section markers as default scaffolding (01 / 02 / 03).** Putting `01 · About / 02 · Process / 03 · Pricing` above every section is the eyebrow trope one tier deeper: reach for it because "landing pages do this" and you're scaffolding by reflex. Numbers earn their place when the section actually IS a sequence (a real 3-step process, an ordered flow, a typed timeline) and the order carries information the reader needs. One deliberate numbered sequence on one page is voice; numbered eyebrows on every section across the site is AI grammar. +- **Text that overflows its container.** Long heading words plus large clamp scales plus narrow grids cause headline overflow on tablet/mobile. Test the heading copy at every breakpoint; if it overflows, reduce the clamp max or rewrite the copy. The viewport is part of the design. ### The AI slop test @@ -110,7 +104,7 @@ If someone could look at this interface and say "AI made that" without doubt, it **Category-reflex check.** Run at two altitudes; the second one catches what the first one misses. -- **First-order:** if someone could guess the theme + palette from the category alone ("observability → dark blue", "healthcare → white + teal", "finance → navy + gold", "crypto → neon on black"), it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain. +- **First-order:** if someone could guess the theme + palette from the category alone, it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain. - **Second-order:** if someone could guess the aesthetic family from category-plus-anti-references ("AI workflow tool that's not SaaS-cream → editorial-typographic", "fintech that's not navy-and-gold → terminal-native dark mode"), it's the trap one tier deeper. The first reflex was avoided; the second wasn't. Rework until both answers are not obvious. The brand register's [reflex-reject aesthetic lanes](reference/brand.md) list catches the currently-saturated families. ## Commands @@ -119,7 +113,7 @@ If someone could look at this interface and say "AI made that" without doubt, it |---|---|---|---| | `craft [feature]` | Build | Shape, then build a feature end-to-end | [reference/craft.md](reference/craft.md) | | `shape [feature]` | Build | Plan UX/UI before writing code | [reference/shape.md](reference/shape.md) | -| `teach` | Build | Set up PRODUCT.md and DESIGN.md context | [reference/teach.md](reference/teach.md) | +| `init` | Build | Set up project context: PRODUCT.md, DESIGN.md, live config, next steps | [reference/init.md](reference/init.md) | | `document` | Build | Generate DESIGN.md from existing project code | [reference/document.md](reference/document.md) | | `extract [target]` | Build | Pull reusable tokens and components into design system | [reference/extract.md](reference/extract.md) | | `critique [target]` | Evaluate | UX design review with heuristic scoring | [reference/critique.md](reference/critique.md) | @@ -147,11 +141,14 @@ Plus two management commands: `pin ` and `unpin `, detailed be 1. **No argument**: render the table above as the user-facing command menu, grouped by category. Ask what they'd like to do. 2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. -3. **First word doesn't match**: general design invocation. Apply the setup steps, shared design laws, and the loaded register reference, using the full argument as context. +3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. +4. **No clear command match**: general design invocation. Apply the setup steps, the General rules, and the loaded register reference, using the full argument as context. Setup (context gathering, register) is already loaded by then; sub-commands don't re-invoke `/impeccable`. -If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `teach` as a blocker, finish teach, refresh context, then resume the original command and target. +If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `init` as a blocker, finish init, refresh context, then resume the original command and target. + +`teach` is a deprecated alias for `init`: if the user types it, load [reference/init.md](reference/init.md) and proceed as if they ran `init`. ## Pin / Unpin diff --git a/.gemini/skills/impeccable/reference/adapt.md b/.gemini/skills/impeccable/reference/adapt.md index 9d0673ea2..5af7606cb 100644 --- a/.gemini/skills/impeccable/reference/adapt.md +++ b/.gemini/skills/impeccable/reference/adapt.md @@ -188,3 +188,124 @@ Test thoroughly across contexts: - **Slow connections**: Test on throttled network When the adaptation feels native to each context, hand off to `/impeccable polish` for the final pass. + +--- + +## Reference Material + +The sections below were previously `responsive-design.md` and live inline now so the adapt flow has its deep responsive reference in one place. + +### Responsive Design + +#### Mobile-First: Write It Right + +Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first. + +#### Breakpoints: Content-Driven + +Don't chase device sizes; let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints. + +#### Detect Input Method, Not Just Screen Size + +**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard. Use pointer and hover queries: + +```css +/* Fine pointer (mouse, trackpad) */ +@media (pointer: fine) { + .button { padding: 8px 16px; } +} + +/* Coarse pointer (touch, stylus) */ +@media (pointer: coarse) { + .button { padding: 12px 20px; } /* Larger touch target */ +} + +/* Device supports hover */ +@media (hover: hover) { + .card:hover { transform: translateY(-2px); } +} + +/* Device doesn't support hover (touch) */ +@media (hover: none) { + .card { /* No hover state - use active instead */ } +} +``` + +**Critical**: Don't rely on hover for functionality. Touch users can't hover. + +#### Safe Areas: Handle the Notch + +Modern phones have notches, rounded corners, and home indicators. Use `env()`: + +```css +body { + padding-top: env(safe-area-inset-top); + padding-bottom: env(safe-area-inset-bottom); + padding-left: env(safe-area-inset-left); + padding-right: env(safe-area-inset-right); +} + +/* With fallback */ +.footer { + padding-bottom: max(1rem, env(safe-area-inset-bottom)); +} +``` + +**Enable viewport-fit** in your meta tag: +```html + +``` + +#### Responsive Images: Get It Right + +##### srcset with Width Descriptors + +```html +Hero image +``` + +**How it works**: +- `srcset` lists available images with their actual widths (`w` descriptors) +- `sizes` tells the browser how wide the image will display +- Browser picks the best file based on viewport width AND device pixel ratio + +##### Picture Element for Art Direction + +When you need different crops/compositions (not just resolutions): + +```html + + + + ... + +``` + +#### Layout Adaptation Patterns + +**Navigation**: Three stages: hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `
/` for content that can collapse on mobile. + +#### Testing: Don't Trust DevTools Alone + +DevTools device emulation is useful for layout but misses: + +- Actual touch interactions +- Real CPU/memory constraints +- Network latency patterns +- Font rendering differences +- Browser chrome/keyboard appearances + +**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators. + +--- + +**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful. diff --git a/.gemini/skills/impeccable/reference/animate.md b/.gemini/skills/impeccable/reference/animate.md index 20aeea659..15d9f9080 100644 --- a/.gemini/skills/impeccable/reference/animate.md +++ b/.gemini/skills/impeccable/reference/animate.md @@ -6,7 +6,7 @@ Add motion that conveys state, gives feedback, and clarifies hierarchy. Cut moti ## Register -Brand: orchestrated page-load sequences, staggered reveals, scroll-driven animation. Motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions. +Brand: motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions. The saturated AI default is fade-and-rise reveals on every scrolled section; that's a tell, not a choreography. Reserve scroll-triggered motion for moments that earn it. Product: 150–250 ms on most transitions. Motion conveys state: feedback, reveal, loading, transitions between views. No page-load choreography; users are in a task and won't wait for it. @@ -49,10 +49,11 @@ Create a purposeful animation plan: Add motion systematically across these categories: ### Entrance Animations -- **Page load choreography**: Stagger element reveals (100-150ms delays), fade + slide combinations - **Hero section**: Dramatic entrance for primary content (scale, parallax, or creative effects) -- **Content reveals**: Scroll-triggered animations using intersection observer - **Modal/drawer entry**: Smooth slide + fade, backdrop fade, focus management +- **List rhythm**: Sibling stagger is legitimate for cards-in-a-grid or list-items-appearing. Whole-section fade-on-scroll is not a list and is not legitimate. Cap total stagger time: 10 items at 50ms each = 500ms total. For more items, reduce per-item delay or cap the staggered count. + + Use CSS custom properties for clean stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"`, `style="--i: 1"`, etc. on each item. ### Micro-interactions - **Button feedback**: @@ -97,11 +98,14 @@ Use appropriate techniques for each animation: ### Timing & Easing -**Durations by purpose:** -- **100-150ms**: Instant feedback (button press, toggle) -- **200-300ms**: State changes (hover, menu open) -- **300-500ms**: Layout changes (accordion, modal) -- **500-800ms**: Entrance animations (page load) +**Duration: the 100/300/500 rule.** Timing matters more than easing for "feels right": + +| Duration | Use Case | Examples | +|----------|----------|----------| +| **100–150ms** | Instant feedback | Button press, toggle, color change | +| **200–300ms** | State changes | Menu open, tooltip, hover state | +| **300–500ms** | Layout changes | Accordion, modal, drawer | +| **500–800ms** | Entrance animations | Page load, hero reveal | **Easing curves (use these, not CSS defaults):** ```css @@ -134,13 +138,35 @@ Use appropriate techniques for each animation: - GSAP for complex sequences ``` +### Motion Materials + +Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties. Match material to effect: + +- **Transform / opacity**: movement, press feedback, simple reveals, list choreography +- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances +- **Clip-path / masks**: wipes, reveals, editorial cropping, product-like transitions +- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state +- **Grid-template-rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly + +The hard rule isn't "transform and opacity only." It's: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify smoothness in-browser on target viewports. + ### Performance -- **Motion materials**: Use transform/opacity for reliable movement, but use blur, filters, masks, shadows, and color shifts when they materially improve the effect - **Layout safety**: Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins) -- **will-change**: Add sparingly for known expensive animations +- **will-change**: Add sparingly for known expensive animations only (e.g. on `:hover` or an `.animating` class), never preemptively across the whole page +- **Scroll triggers**: Use Intersection Observer instead of scroll event listeners; unobserve after the animation fires once - **Bound expensive effects**: Keep blur/filter/shadow areas small or isolated, use `contain` where appropriate - **Monitor FPS**: Ensure 60fps on target devices +### Perceived Performance + +Nobody cares how fast your site *is*, only how fast it feels. The 80ms threshold: anything under ~80ms feels instant because our brains buffer sensory input for that long to synchronize perception. Target this for micro-interactions. + +- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening. +- **Early completion**: Show content progressively, don't wait for everything (progressive images, streaming HTML, skeleton fade-ins). +- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Use for low-stakes actions (likes, follows). Avoid for payments or destructive operations. +- **Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances. +- **Caution**: Too-fast responses can decrease perceived value for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening. + ### Accessibility ```css @media (prefers-reduced-motion: reduce) { diff --git a/.gemini/skills/impeccable/reference/bolder.md b/.gemini/skills/impeccable/reference/bolder.md index ff0ef2f14..30d4e852c 100644 --- a/.gemini/skills/impeccable/reference/bolder.md +++ b/.gemini/skills/impeccable/reference/bolder.md @@ -50,7 +50,7 @@ Create a strategy to increase impact while maintaining coherence: Systematically increase impact across these dimensions: ### Typography Amplification -- **Replace generic fonts**: Swap system fonts for distinctive choices (see the parent skill's typography guidelines and [typography.md](typography.md) for inspiration) +- **Replace generic fonts**: Swap system fonts for distinctive choices (see the parent skill's typography guidelines and the [Reference Material section of typeset.md](typeset.md#reference-material) for inspiration) - **Extreme scale**: Create dramatic size jumps (3x-5x differences, not 1.5x) - **Weight contrast**: Pair 900 weights with 200 weights, not 600 with 400 - **Unexpected choices**: Variable fonts, display fonts for headlines, condensed/extended widths, monospace as intentional accent (not as lazy "dev tool" default) @@ -78,10 +78,10 @@ Systematically increase impact across these dimensions: - **Custom elements**: Illustrative elements, custom icons, decorative details that reinforce brand ### Motion & Animation -- **Entrance choreography**: Staggered, dramatic page load animations with 50-100ms delays -- **Scroll effects**: Parallax, reveal animations, scroll-triggered sequences -- **Micro-interactions**: Satisfying hover effects, click feedback, state changes -- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic, which cheapen the effect) +- **Hero moment**: One signature entrance, once. Not on every visit and not on every section. +- **Micro-interactions**: Satisfying hover effects, click feedback, state changes. +- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic, which cheapen the effect). +- **Bolder ≠ scroll-fade-rise on every section.** That's the saturated AI default, the opposite of bold. ### Composition Boldness - **Hero moments**: Create clear focal points with dramatic treatment diff --git a/.gemini/skills/impeccable/reference/brand.md b/.gemini/skills/impeccable/reference/brand.md index 3d83a1cdc..194514e95 100644 --- a/.gemini/skills/impeccable/reference/brand.md +++ b/.gemini/skills/impeccable/reference/brand.md @@ -43,17 +43,10 @@ The reflex-reject lists apply to **new design choices**. When the existing brand ### Pairing and voice -Distinctive + refined is the goal. The specific shape depends on the brand: - -- **Editorial / long-form / luxury**: display serif + sans body (a magazine shape). -- **Tech / dev tools / fintech**: one committed sans, usually; custom-tight tracking, strong weight contrast inside a single family. -- **Consumer / food / travel**: warmer pairings, often a humanist sans plus a script or display serif. -- **Creative studios / agencies**: rule-breaking welcome. Mono-only, or display-only, or custom-drawn type as voice. +Distinctive + refined is the goal. The specific shape depends on the brand, not on the brand's category. A category ("restaurant", "dev tool", "magazine", "fintech") is not a recipe; treating it as one is the first-order reflex SKILL.md warns against. Two families minimum is the rule *only* when the voice needs it. A single well-chosen family with committed weight/size contrast is stronger than a timid display+body pair. -Vary across projects. If the last brief was a serif-display landing page, this one isn't. - ### Scale Modular scale, fluid `clamp()` for headings, ≥1.25 ratio between steps. Flat scales (1.1× apart) read as uncommitted. @@ -74,8 +67,7 @@ Brand surfaces have permission for Committed, Full palette, and Drenched strateg - Asymmetric compositions are one option. Break the grid intentionally for emphasis. - Fluid spacing with `clamp()` that breathes on larger viewports. Vary for rhythm: generous separations, tight groupings. -- Alternative: a strict, visible grid as the voice (brutalist / Swiss / tech-spec aesthetics). Either asymmetric or rigorously-gridded can be "designed"; the failure mode is splitting the difference into a generic centered stack. -- Don't default to centering everything. Left-aligned with asymmetric layouts feels more designed; a strict grid reads as confident structure. A centered-stack hero with icon-title-subtitle cards reads as template. +- For image-led briefs (hotels, restaurants, magazines, photography), full-bleed hero imagery with overlaid menu and centered headline is a canonical move; let the photograph be the design. - When cards ARE the right affordance, use `grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` for breakpoint-free responsiveness. ## Imagery @@ -93,8 +85,7 @@ Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landin ## Motion -- One well-orchestrated page-load with staggered reveals beats scattered micro-interactions, when the brand invites it. Tech-minimal brands often skip entrance motion entirely; the restraint is the voice. -- For collapsing/expanding sections, transition `grid-template-rows` rather than `height`. +- One well-orchestrated page-load beats scattered micro-interactions, when the brand invites it. Some brands skip entrance motion entirely; the restraint is the voice. ## Brand bans (on top of the shared absolute bans) @@ -111,8 +102,7 @@ Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landin Brand can afford things product can't. Take them. -- Ambitious first-load motion. Reveals, scroll-triggered transitions, typographic choreography. +- Ambitious first-load motion. Reveals and typographic choreography that earn their place; not fade-on-scroll for every section. - Single-purpose viewports. One dominant idea per fold, long scroll, deliberate pacing. -- Typographic risk. Enormous display type, unexpected italic cuts, mixed cases, hand-drawn headlines, a single oversize word as a hero. - Unexpected color strategies. Palette IS voice; a calm brand and a restless brand should not share palette mechanics. - Art direction per section. Different sections can have different visual worlds if the narrative demands it. Consistency of voice beats consistency of treatment. diff --git a/.gemini/skills/impeccable/reference/clarify.md b/.gemini/skills/impeccable/reference/clarify.md index 5f7d420f4..00ee978c3 100644 --- a/.gemini/skills/impeccable/reference/clarify.md +++ b/.gemini/skills/impeccable/reference/clarify.md @@ -172,3 +172,117 @@ Test that copy improvements work: - **Tone**: Is it appropriate for the situation? When the copy reads cleanly, hand off to `/impeccable polish` for the final pass. + +--- + +## Reference Material + +The sections below were previously `ux-writing.md` and live inline now so the clarify flow has its deep UX-writing reference in one place. + +### UX Writing + +#### The Button Label Problem + +**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns: + +| Bad | Good | Why | +|-----|------|-----| +| OK | Save changes | Says what will happen | +| Submit | Create account | Outcome-focused | +| Yes | Delete message | Confirms the action | +| Cancel | Keep editing | Clarifies what "cancel" means | +| Click here | Download PDF | Describes the destination | + +**For destructive actions**, name the destruction: +- "Delete" not "Remove" (delete is permanent, remove implies recoverable) +- "Delete 5 items" not "Delete selected" (show the count) + +#### Error Messages: The Formula + +Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input". + +##### Error Message Templates + +| Situation | Template | +|-----------|----------| +| **Format error** | "[Field] needs to be [format]. Example: [example]" | +| **Missing required** | "Please enter [what's missing]" | +| **Permission denied** | "You don't have access to [thing]. [What to do instead]" | +| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." | +| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" | + +##### Don't Blame the User + +Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date". + +#### Empty States Are Opportunities + +Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items". + +#### Voice vs Tone + +**Voice** is your brand's personality, consistent everywhere. +**Tone** adapts to the moment. + +| Moment | Tone Shift | +|--------|------------| +| Success | Celebratory, brief: "Done! Your changes are live." | +| Error | Empathetic, helpful: "That didn't work. Here's what to try..." | +| Loading | Reassuring: "Saving your work..." | +| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | + +**Never use humor for errors.** Users are already frustrated. Be helpful, not cute. + +#### Writing for Accessibility + +**Link text** must have standalone meaning: "View pricing plans" not "Click here". **Alt text** describes information, not the image: "Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context. + +#### Writing for Translation + +##### Plan for Expansion + +German text is ~30% longer than English. Allocate space: + +| Language | Expansion | +|----------|-----------| +| German | +30% | +| French | +20% | +| Finnish | +30-40% | +| Chinese | -30% (fewer chars, but same width) | + +##### Translation-Friendly Patterns + +Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear. + +#### Consistency: The Terminology Problem + +Pick one term and stick with it: + +| Inconsistent | Consistent | +|--------------|------------| +| Delete / Remove / Trash | Delete | +| Settings / Preferences / Options | Settings | +| Sign in / Log in / Enter | Sign in | +| Create / Add / New | Create | + +Build a terminology glossary and enforce it. Variety creates confusion. + +#### Avoid Redundant Copy + +If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well. + +#### Loading States + +Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress. + +#### Confirmation Dialogs: Use Sparingly + +Most confirmation dialogs are design failures; consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No"). + +#### Form Instructions + +Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking. + +--- + +**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors. diff --git a/.gemini/skills/impeccable/reference/codex.md b/.gemini/skills/impeccable/reference/codex.md index f40ec746f..d563af95b 100644 --- a/.gemini/skills/impeccable/reference/codex.md +++ b/.gemini/skills/impeccable/reference/codex.md @@ -23,8 +23,8 @@ Before generating anything, run a brief direction conversation grounded in the s Ask **2-3 targeted questions** about visual lane, color strategy, atmosphere, and named anchor references. Don't enumerate generic menus; tie each question to the shape brief's answers. Example shape-grounded questions: -- "Brief says 'editorial restraint, Klim-adjacent.' Are we closer to a quiet specimen page or a magazine-spread feel with hero imagery?" -- "Palette strategy from shape was 'Committed.' Want it warm-grounded (deep oxblood + cream) or cool-grounded (slate + paper white)?" +- "Brief says 'specimen-page restraint.' Are we closer to a quiet typographic page or a wider editorial spread with hero imagery?" +- "Palette strategy from shape was 'Committed.' Which one color carries the surface (a brand-driven pick rather than a default warm-or-cool framing)? (And no, the answer isn't a cream/sand body bg; that's the saturated AI default.)" **STOP and wait for answers.** These pin the palette before any pixel gets generated. Do not proceed to Step B until the user has responded. diff --git a/.gemini/skills/impeccable/reference/cognitive-load.md b/.gemini/skills/impeccable/reference/cognitive-load.md deleted file mode 100644 index 48f8ad58b..000000000 --- a/.gemini/skills/impeccable/reference/cognitive-load.md +++ /dev/null @@ -1,106 +0,0 @@ -# Cognitive Load Assessment - -Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload. - ---- - -## Three Types of Cognitive Load - -### Intrinsic Load: The Task Itself -Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it. - -**Manage it by**: -- Breaking complex tasks into discrete steps -- Providing scaffolding (templates, defaults, examples) -- Progressive disclosure: show what's needed now, hide the rest -- Grouping related decisions together - -### Extraneous Load: Bad Design -Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste. - -**Common sources**: -- Confusing navigation that requires mental mapping -- Unclear labels that force users to guess meaning -- Visual clutter competing for attention -- Inconsistent patterns that prevent learning -- Unnecessary steps between user intent and result - -### Germane Load: Learning Effort -Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery. - -**Support it by**: -- Progressive disclosure that reveals complexity gradually -- Consistent patterns that reward learning -- Feedback that confirms correct understanding -- Onboarding that teaches through action, not walls of text - ---- - -## Cognitive Load Checklist - -Evaluate the interface against these 8 items: - -- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements? -- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)? -- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)? -- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen? -- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next? -- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)? -- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one? -- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it? - -**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed). - ---- - -## The Working Memory Rule - -**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001). - -At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider: -- **≤4 items**: Within working memory limits, manageable -- **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure -- **8+ items**: Overloaded; users will skip, misclick, or abandon - -**Practical applications**: -- Navigation menus: ≤5 top-level items (group the rest under clear categories) -- Form sections: ≤4 fields visible per group before a visual break -- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu -- Dashboard widgets: ≤4 key metrics visible without scrolling -- Pricing tiers: ≤3 options (more causes analysis paralysis) - ---- - -## Common Cognitive Load Violations - -### 1. The Wall of Options -**Problem**: Presenting 10+ choices at once with no hierarchy. -**Fix**: Group into categories, highlight recommended, use progressive disclosure. - -### 2. The Memory Bridge -**Problem**: User must remember info from step 1 to complete step 3. -**Fix**: Keep relevant context visible, or repeat it where it's needed. - -### 3. The Hidden Navigation -**Problem**: User must build a mental map of where things are. -**Fix**: Always show current location (breadcrumbs, active states, progress indicators). - -### 4. The Jargon Barrier -**Problem**: Technical or domain language forces translation effort. -**Fix**: Use plain language. If domain terms are unavoidable, define them inline. - -### 5. The Visual Noise Floor -**Problem**: Every element has the same visual weight; nothing stands out. -**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted. - -### 6. The Inconsistent Pattern -**Problem**: Similar actions work differently in different places. -**Fix**: Standardize interaction patterns. Same type of action = same type of UI. - -### 7. The Multi-Task Demand -**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating). -**Fix**: Sequence the steps. Let the user do one thing at a time. - -### 8. The Context Switch -**Problem**: User must jump between screens/tabs/modals to gather info for a single decision. -**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth. diff --git a/.gemini/skills/impeccable/reference/color-and-contrast.md b/.gemini/skills/impeccable/reference/color-and-contrast.md deleted file mode 100644 index 110c2ee47..000000000 --- a/.gemini/skills/impeccable/reference/color-and-contrast.md +++ /dev/null @@ -1,105 +0,0 @@ -# Color & Contrast - -## Color Spaces: Use OKLCH - -**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal, unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark. - -The OKLCH function takes three components: `oklch(lightness chroma hue)` where lightness is 0-100%, chroma is roughly 0-0.4, and hue is 0-360. To build a primary color and its lighter / darker variants, hold the chroma+hue roughly constant and vary the lightness, but **reduce chroma as you approach white or black**, because high chroma at extreme lightness looks garish. - -The hue you pick is a brand decision and should not come from a default. Do not reach for blue (hue 250) or warm orange (hue 60) by reflex; those are the dominant AI-design defaults, not the right answer for any specific brand. - -## Building Functional Palettes - -### Tinted Neutrals - -**Pure gray is dead.** A neutral with zero chroma feels lifeless next to a colored brand. Add a tiny chroma value (0.005-0.015) to all your neutrals, hued toward whatever your brand color is. The chroma is small enough not to read as "tinted" consciously, but it creates subconscious cohesion between brand color and UI surfaces. - -The hue you tint toward should come from THIS project's brand, not from a "warm = friendly, cool = tech" formula. If your brand color is teal, your neutrals lean toward teal. If your brand color is amber, they lean toward amber. The point is cohesion with the SPECIFIC brand, not a stock palette. - -**Avoid** the trap of always tinting toward warm orange or always tinting toward cool blue. Those are the two laziest defaults and they create their own monoculture across projects. - -### Palette Structure - -A complete system needs: - -| Role | Purpose | Example | -|------|---------|---------| -| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades | -| **Neutral** | Text, backgrounds, borders | 9-11 shade scale | -| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each | -| **Surface** | Cards, modals, overlays | 2-3 elevation levels | - -**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise. - -### The 60-30-10 Rule (Applied Correctly) - -This rule is about **visual weight**, not pixel count: - -- **60%**: Neutral backgrounds, white space, base surfaces -- **30%**: Secondary colors: text, borders, inactive states -- **10%**: Accent: CTAs, highlights, focus states - -The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power. - -## Contrast & Accessibility - -### WCAG Requirements - -| Content Type | AA Minimum | AAA Target | -|--------------|------------|------------| -| Body text | 4.5:1 | 7:1 | -| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | -| UI components, icons | 3:1 | 4.5:1 | -| Non-essential decorations | None | None | - -**The gotcha**: Placeholder text still needs 4.5:1. That light gray placeholder you see everywhere? Usually fails WCAG. - -### Dangerous Color Combinations - -These commonly fail contrast or cause readability issues: - -- Light gray text on white (the #1 accessibility fail) -- **Gray text on any colored background**: gray looks washed out and dead on color. Use a darker shade of the background color, or transparency -- Red text on green background (or vice versa): 8% of men can't distinguish these -- Blue text on red background (vibrates visually) -- Yellow text on white (almost always fails) -- Thin light text on images (unpredictable contrast) - -### Never Use Pure Gray or Pure Black - -Pure gray (`oklch(50% 0 0)`) and pure black (`#000`) don't exist in nature; real shadows and surfaces always have a color cast. Even a chroma of 0.005-0.01 is enough to feel natural without being obviously tinted. (See tinted neutrals example above.) - -### Testing - -Don't trust your eyes. Use tools: - -- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) -- Browser DevTools → Rendering → Emulate vision deficiencies -- [Polypane](https://polypane.app/) for real-time testing - -## Theming: Light & Dark Mode - -### Dark Mode Is Not Inverted Light Mode - -You can't just swap colors. Dark mode requires different design decisions: - -| Light Mode | Dark Mode | -|------------|-----------| -| Shadows for depth | Lighter surfaces for depth (no shadows) | -| Dark text on light | Light text on dark (reduce font weight) | -| Vibrant accents | Desaturate accents slightly | -| White backgrounds | Never pure black; use dark gray (oklch 12-18%) | - -In dark mode, depth comes from surface lightness, not shadow. Build a 3-step surface scale where higher elevations are lighter (e.g. 15% / 20% / 25% lightness). Use the SAME hue and chroma as your brand color (whatever it is for THIS project; do not reach for blue) and only vary the lightness. Reduce body text weight slightly (e.g. 350 instead of 400) because light text on dark reads as heavier than dark text on light. - -### Token Hierarchy - -Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer; primitives stay the same. - -## Alpha Is A Design Smell - -Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed. - ---- - -**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Using pure black (#000) for large areas. Skipping color blindness testing (8% of men affected). diff --git a/.gemini/skills/impeccable/reference/colorize.md b/.gemini/skills/impeccable/reference/colorize.md index 9e96eef59..2457f608c 100644 --- a/.gemini/skills/impeccable/reference/colorize.md +++ b/.gemini/skills/impeccable/reference/colorize.md @@ -68,10 +68,10 @@ Add color systematically across these dimensions: - **Hover states**: Introduce color on interaction ### Background & Surfaces -- **Tinted backgrounds**: Replace pure gray (`#f5f5f5`) with warm neutrals (`oklch(97% 0.01 60)`) or cool tints (`oklch(97% 0.01 250)`) +- **Tinted backgrounds**: If you replace pure gray, tint toward the brand hue, not toward a generic-warm-or-cool pair. The default-warm-tint (`oklch(97% 0.01 60)` and its neighbors) is now the AI cream/sand giveaway. Be specific to the brand or stay neutral. - **Colored sections**: Use subtle background colors to separate areas - **Gradient backgrounds**: Add depth with subtle, intentional gradients (not generic purple-blue) -- **Cards & surfaces**: Tint cards or surfaces slightly for warmth +- **Cards & surfaces**: Tint cards or surfaces toward the brand, not "for warmth" by reflex **Use OKLCH for color**: It's perceptually uniform, meaning equal steps in lightness *look* equal. Great for generating harmonious scales. @@ -124,8 +124,6 @@ Ensure color addition improves rather than overwhelms: - Use every color in the rainbow (choose 2-4 colors beyond neutrals) - Apply color randomly without semantic meaning - Put gray text on colored backgrounds. It looks washed out; use a darker shade of the background color or transparency instead -- Use pure gray for neutrals. Add subtle color tint (warm or cool) for depth -- Use pure black (`#000`) or pure white (`#fff`) for large areas - Violate WCAG contrast requirements - Use color as the only indicator (accessibility issue) - Make everything colorful (defeats the purpose) @@ -152,3 +150,108 @@ When invoked from live mode, each variant MUST declare a `color-amount` param so ``` Layer 1-2 variant-specific params on top: palette selection (`steps` with named options), temperature warmth, or tint vs. true color. See `reference/live.md` for the full params contract. + +--- + +## Reference Material + +The sections below were previously `color-and-contrast.md` and live inline now so the colorize flow has its deep color reference in one place. + +### Color & Contrast + +#### Color Spaces: Use OKLCH + +**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal, unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark. + +The OKLCH function takes three components: `oklch(lightness chroma hue)` where lightness is 0-100%, chroma is roughly 0-0.4, and hue is 0-360. To build a primary color and its lighter / darker variants, hold the chroma+hue roughly constant and vary the lightness, but **reduce chroma as you approach white or black**, because high chroma at extreme lightness looks garish. + +The hue you pick is a brand decision and should not come from a default. Do not reach for blue (hue 250) or warm orange (hue 60) by reflex; those are the dominant AI-design defaults, not the right answer for any specific brand. + +#### Building Functional Palettes + +##### Tinted Neutrals + +**Pure gray is dead.** A neutral with zero chroma feels lifeless next to a colored brand. Add a tiny chroma value (0.005-0.015) to all your neutrals, hued toward whatever your brand color is. The chroma is small enough not to read as "tinted" consciously, but it creates subconscious cohesion between brand color and UI surfaces. + +The hue you tint toward should come from THIS project's brand, not from a "warm = friendly, cool = tech" formula. If your brand color is teal, your neutrals lean toward teal. If your brand color is amber, they lean toward amber. The point is cohesion with the SPECIFIC brand, not a stock palette. + +**Avoid** the trap of always tinting toward warm orange or always tinting toward cool blue. Those are the two laziest defaults and they create their own monoculture across projects. + +##### Palette Structure + +A complete system needs: + +| Role | Purpose | Example | +|------|---------|---------| +| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades | +| **Neutral** | Text, backgrounds, borders | 9-11 shade scale | +| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each | +| **Surface** | Cards, modals, overlays | 2-3 elevation levels | + +**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise. + +##### The 60-30-10 Rule (Applied Correctly) + +This rule is about **visual weight**, not pixel count: + +- **60%**: Neutral backgrounds, white space, base surfaces +- **30%**: Secondary colors: text, borders, inactive states +- **10%**: Accent: CTAs, highlights, focus states + +The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power. + +#### Contrast & Accessibility + +##### WCAG Requirements + +| Content Type | AA Minimum | AAA Target | +|--------------|------------|------------| +| Body text | 4.5:1 | 7:1 | +| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | +| UI components, icons | 3:1 | 4.5:1 | +| Non-essential decorations | None | None | + +##### Dangerous Color Combinations + +These commonly fail contrast or cause readability issues: + +- Light gray text on white (the #1 accessibility fail) +- Red text on green background (or vice versa): 8% of men can't distinguish these +- Blue text on red background (vibrates visually) +- Yellow text on white (almost always fails) +- Thin light text on images (unpredictable contrast) + +##### Testing + +Don't trust your eyes. Use tools: + +- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) +- Browser DevTools → Rendering → Emulate vision deficiencies +- [Polypane](https://polypane.app/) for real-time testing + +#### Theming: Light & Dark Mode + +##### Dark Mode Is Not Inverted Light Mode + +You can't just swap colors. Dark mode requires different design decisions: + +| Light Mode | Dark Mode | +|------------|-----------| +| Shadows for depth | Lighter surfaces for depth (no shadows) | +| Dark text on light | Light text on dark (reduce font weight) | +| Vibrant accents | Desaturate accents slightly | +| White backgrounds | Either pure black or a deep surface that fits the brand (a brand-tinted near-black at oklch 12-18% works too) | + +In dark mode, depth comes from surface lightness, not shadow. Build a 3-step surface scale where higher elevations are lighter (e.g. 15% / 20% / 25% lightness). Use the SAME hue and chroma as your brand color (whatever it is for THIS project; do not reach for blue) and only vary the lightness. Reduce body text weight slightly (e.g. 350 instead of 400) because light text on dark reads as heavier than dark text on light. + +##### Token Hierarchy + +Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer; primitives stay the same. + +#### Alpha Is A Design Smell + +Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed. + +--- + +**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Skipping color blindness testing (8% of men affected). diff --git a/.gemini/skills/impeccable/reference/craft.md b/.gemini/skills/impeccable/reference/craft.md index 06094b1dd..2c7bd99b1 100644 --- a/.gemini/skills/impeccable/reference/craft.md +++ b/.gemini/skills/impeccable/reference/craft.md @@ -56,15 +56,15 @@ If the harness has native image generation (Codex), a compact shape's "confirm o Based on the design brief's "Recommended References" section, consult the relevant impeccable reference files. At minimum, always consult: -- [spatial-design.md](spatial-design.md) for layout and spacing -- [typography.md](typography.md) for type hierarchy +- [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 [motion-design.md](motion-design.md) -- Color-heavy or themed? Consult [color-and-contrast.md](color-and-contrast.md) -- Responsive requirements? Consult [responsive-design.md](responsive-design.md) -- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.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) diff --git a/.gemini/skills/impeccable/reference/critique.md b/.gemini/skills/impeccable/reference/critique.md index 62d4180fe..607e1f033 100644 --- a/.gemini/skills/impeccable/reference/critique.md +++ b/.gemini/skills/impeccable/reference/critique.md @@ -38,9 +38,9 @@ Read relevant source files and visually inspect the live page when browser autom Evaluate: - **AI slop**: Would someone believe "AI made this" immediately? Check all DON'T guidance from the parent Impeccable skill. - **Holistic design**: hierarchy, IA, emotional fit, discoverability, composition, typography, color, accessibility, states, copy, and edge cases. -- **Cognitive load**: consult [cognitive-load](cognitive-load.md); report checklist failures and decision points with >4 visible options. +- **Cognitive load**: consult the [Cognitive Load Assessment](#cognitive-load-assessment) section below; report checklist failures and decision points with >4 visible options. - **Emotional journey**: peak-end rule, emotional valleys, reassurance at high-stakes moments. -- **Nielsen heuristics**: consult [heuristics-scoring](heuristics-scoring.md); score all 10 heuristics 0-4. +- **Nielsen heuristics**: consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below; score all 10 heuristics 0-4. Return: AI slop verdict, heuristic scores, cognitive load, emotional journey, 2-3 strengths, 3-5 priority issues, persona red flags, minor observations, and provocative questions. @@ -80,7 +80,7 @@ The chat response is the primary user-facing deliverable. Present the full struc Structure your feedback as a design director would: #### Design Health Score -> *Consult [heuristics-scoring](heuristics-scoring.md)* +> *Consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below.* Present the Nielsen's 10 heuristics scores as a table: @@ -119,16 +119,16 @@ Highlight 2-3 things done well. Be specific about why they work. #### Priority Issues The 3-5 most impactful design problems, ordered by importance. -For each issue, tag with **P0-P3 severity** (consult [heuristics-scoring](heuristics-scoring.md) for severity definitions): +For each issue, tag with **P0-P3 severity** (see [Issue Severity below](#issue-severity-p0p3) for definitions): - **[P?] What**: Name the problem clearly - **Why it matters**: How this hurts users or undermines goals - **Fix**: What to do about it (be concrete) - **Suggested command**: Which command could address this (from: /impeccable adapt, /impeccable animate, /impeccable audit, /impeccable bolder, /impeccable clarify, /impeccable colorize, /impeccable critique, /impeccable delight, /impeccable distill, /impeccable document, /impeccable harden, /impeccable layout, /impeccable onboard, /impeccable optimize, /impeccable overdrive, /impeccable polish, /impeccable quieter, /impeccable shape, /impeccable typeset) #### Persona Red Flags -> *Consult [personas](personas.md)* +> *Consult the [Personas reference](#persona-based-design-testing) below.* -Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `GEMINI.md` contains a `## Design Context` section from `impeccable teach`, also generate 1-2 project-specific personas from the audience/brand info. +Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `GEMINI.md` contains a `## Design Context` section from `impeccable init`, also generate 1-2 project-specific personas from the audience/brand info. For each selected persona, walk through the primary user action and list specific red flags found: @@ -234,3 +234,534 @@ After presenting the summary, tell the user: > You can ask me to run these one at a time, all at once, or in any order you prefer. > > Re-run `/impeccable critique` after fixes to see your score improve. + +--- + +## Reference Material + +The sections below were previously separate reference files (`cognitive-load.md`, `heuristics-scoring.md`, `personas.md`). They live inline now so the critique flow has all its deep context in one place. + +### Cognitive Load Assessment + +Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload. + +--- + +#### Three Types of Cognitive Load + +##### Intrinsic Load: The Task Itself +Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it. + +**Manage it by**: +- Breaking complex tasks into discrete steps +- Providing scaffolding (templates, defaults, examples) +- Progressive disclosure: show what's needed now, hide the rest +- Grouping related decisions together + +##### Extraneous Load: Bad Design +Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste. + +**Common sources**: +- Confusing navigation that requires mental mapping +- Unclear labels that force users to guess meaning +- Visual clutter competing for attention +- Inconsistent patterns that prevent learning +- Unnecessary steps between user intent and result + +##### Germane Load: Learning Effort +Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery. + +**Support it by**: +- Progressive disclosure that reveals complexity gradually +- Consistent patterns that reward learning +- Feedback that confirms correct understanding +- Onboarding that teaches through action, not walls of text + +--- + +#### Cognitive Load Checklist + +Evaluate the interface against these 8 items: + +- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements? +- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)? +- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)? +- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen? +- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next? +- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)? +- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one? +- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it? + +**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed). + +--- + +#### The Working Memory Rule + +**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001). + +At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider: +- **≤4 items**: Within working memory limits, manageable +- **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure +- **8+ items**: Overloaded; users will skip, misclick, or abandon + +**Practical applications**: +- Navigation menus: ≤5 top-level items (group the rest under clear categories) +- Form sections: ≤4 fields visible per group before a visual break +- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu +- Dashboard widgets: ≤4 key metrics visible without scrolling +- Pricing tiers: ≤3 options (more causes analysis paralysis) + +--- + +#### Common Cognitive Load Violations + +##### 1. The Wall of Options +**Problem**: Presenting 10+ choices at once with no hierarchy. +**Fix**: Group into categories, highlight recommended, use progressive disclosure. + +##### 2. The Memory Bridge +**Problem**: User must remember info from step 1 to complete step 3. +**Fix**: Keep relevant context visible, or repeat it where it's needed. + +##### 3. The Hidden Navigation +**Problem**: User must build a mental map of where things are. +**Fix**: Always show current location (breadcrumbs, active states, progress indicators). + +##### 4. The Jargon Barrier +**Problem**: Technical or domain language forces translation effort. +**Fix**: Use plain language. If domain terms are unavoidable, define them inline. + +##### 5. The Visual Noise Floor +**Problem**: Every element has the same visual weight; nothing stands out. +**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted. + +##### 6. The Inconsistent Pattern +**Problem**: Similar actions work differently in different places. +**Fix**: Standardize interaction patterns. Same type of action = same type of UI. + +##### 7. The Multi-Task Demand +**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating). +**Fix**: Sequence the steps. Let the user do one thing at a time. + +##### 8. The Context Switch +**Problem**: User must jump between screens/tabs/modals to gather info for a single decision. +**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth. + +--- + +### Heuristics Scoring Guide + +Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough." + +#### Nielsen's 10 Heuristics + +##### 1. Visibility of System Status + +Keep users informed about what's happening through timely, appropriate feedback. + +**Check for**: +- Loading indicators during async operations +- Confirmation of user actions (save, submit, delete) +- Progress indicators for multi-step processes +- Current location in navigation (breadcrumbs, active states) +- Form validation feedback (inline, not just on submit) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | No feedback; user is guessing what happened | +| 1 | Rare feedback; most actions produce no visible response | +| 2 | Partial; some states communicated, major gaps remain | +| 3 | Good; most operations give clear feedback, minor gaps | +| 4 | Excellent; every action confirms, progress is always visible | + +##### 2. Match Between System and Real World + +Speak the user's language. Follow real-world conventions. Information appears in natural, logical order. + +**Check for**: +- Familiar terminology (no unexplained jargon) +- Logical information order matching user expectations +- Recognizable icons and metaphors +- Domain-appropriate language for the target audience +- Natural reading flow (left-to-right, top-to-bottom priority) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Pure tech jargon, alien to users | +| 1 | Mostly confusing; requires domain expertise to navigate | +| 2 | Mixed; some plain language, some jargon leaks through | +| 3 | Mostly natural; occasional term needs context | +| 4 | Speaks the user's language fluently throughout | + +##### 3. User Control and Freedom + +Users need a clear "emergency exit" from unwanted states without extended dialogue. + +**Check for**: +- Undo/redo functionality +- Cancel buttons on forms and modals +- Clear navigation back to safety (home, previous) +- Easy way to clear filters, search, selections +- Escape from long or multi-step processes + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Users get trapped; no way out without refreshing | +| 1 | Difficult exits; must find obscure paths to escape | +| 2 | Some exits; main flows have escape, edge cases don't | +| 3 | Good control; users can exit and undo most actions | +| 4 | Full control; undo, cancel, back, and escape everywhere | + +##### 4. Consistency and Standards + +Users shouldn't wonder whether different words, situations, or actions mean the same thing. + +**Check for**: +- Consistent terminology throughout the interface +- Same actions produce same results everywhere +- Platform conventions followed (standard UI patterns) +- Visual consistency (colors, typography, spacing, components) +- Consistent interaction patterns (same gesture = same behavior) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Inconsistent everywhere; feels like different products stitched together | +| 1 | Many inconsistencies; similar things look/behave differently | +| 2 | Partially consistent; main flows match, details diverge | +| 3 | Mostly consistent; occasional deviation, nothing confusing | +| 4 | Fully consistent; cohesive system, predictable behavior | + +##### 5. Error Prevention + +Better than good error messages is a design that prevents problems in the first place. + +**Check for**: +- Confirmation before destructive actions (delete, overwrite) +- Constraints preventing invalid input (date pickers, dropdowns) +- Smart defaults that reduce errors +- Clear labels that prevent misunderstanding +- Autosave and draft recovery + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Errors easy to make; no guardrails anywhere | +| 1 | Few safeguards; some inputs validated, most aren't | +| 2 | Partial prevention; common errors caught, edge cases slip | +| 3 | Good prevention; most error paths blocked proactively | +| 4 | Excellent; errors nearly impossible through smart constraints | + +##### 6. Recognition Rather Than Recall + +Minimize memory load. Make objects, actions, and options visible or easily retrievable. + +**Check for**: +- Visible options (not buried in hidden menus) +- Contextual help when needed (tooltips, inline hints) +- Recent items and history +- Autocomplete and suggestions +- Labels on icons (not icon-only navigation) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Heavy memorization; users must remember paths and commands | +| 1 | Mostly recall; many hidden features, few visible cues | +| 2 | Some aids; main actions visible, secondary features hidden | +| 3 | Good recognition; most things discoverable, few memory demands | +| 4 | Everything discoverable; users never need to memorize | + +##### 7. Flexibility and Efficiency of Use + +Accelerators, invisible to novices, speed up expert interaction. + +**Check for**: +- Keyboard shortcuts for common actions +- Customizable interface elements +- Recent items and favorites +- Bulk/batch actions +- Power user features that don't complicate the basics + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | One rigid path; no shortcuts or alternatives | +| 1 | Limited flexibility; few alternatives to the main path | +| 2 | Some shortcuts; basic keyboard support, limited bulk actions | +| 3 | Good accelerators; keyboard nav, some customization | +| 4 | Highly flexible; multiple paths, power features, customizable | + +##### 8. Aesthetic and Minimalist Design + +Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose. + +**Check for**: +- Only necessary information visible at each step +- Clear visual hierarchy directing attention +- Purposeful use of color and emphasis +- No decorative clutter competing for attention +- Focused, uncluttered layouts + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Overwhelming; everything competes for attention equally | +| 1 | Cluttered; too much noise, hard to find what matters | +| 2 | Some clutter; main content clear, periphery noisy | +| 3 | Mostly clean; focused design, minor visual noise | +| 4 | Perfectly minimal; every element earns its pixel | + +##### 9. Help Users Recognize, Diagnose, and Recover from Errors + +Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution. + +**Check for**: +- Plain language error messages (no error codes for users) +- Specific problem identification ("Email is missing @" not "Invalid input") +- Actionable recovery suggestions +- Errors displayed near the source of the problem +- Non-blocking error handling (don't wipe the form) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Cryptic errors; codes, jargon, or no message at all | +| 1 | Vague errors; "Something went wrong" with no guidance | +| 2 | Clear but unhelpful; names the problem but not the fix | +| 3 | Clear with suggestions; identifies problem and offers next steps | +| 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work | + +##### 10. Help and Documentation + +Even if the system is usable without docs, help should be easy to find, task-focused, and concise. + +**Check for**: +- Searchable help or documentation +- Contextual help (tooltips, inline hints, guided tours) +- Task-focused organization (not feature-organized) +- Concise, scannable content +- Easy access without leaving current context + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | No help available anywhere | +| 1 | Help exists but hard to find or irrelevant | +| 2 | Basic help; FAQ or docs exist, not contextual | +| 3 | Good documentation; searchable, mostly task-focused | +| 4 | Excellent contextual help; right info at the right moment | + +--- + +#### Score Summary + +**Total possible**: 40 points (10 heuristics × 4 max) + +| Score Range | Rating | What It Means | +|-------------|--------|---------------| +| 36–40 | Excellent | Minor polish only; ship it | +| 28–35 | Good | Address weak areas, solid foundation | +| 20–27 | Acceptable | Significant improvements needed before users are happy | +| 12–19 | Poor | Major UX overhaul required; core experience broken | +| 0–11 | Critical | Redesign needed; unusable in current state | + +--- + +#### Issue Severity (P0–P3) + +Tag each individual issue found during scoring with a priority level: + +| Priority | Name | Description | Action | +|----------|------|-------------|--------| +| **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper | +| **P1** | Major | Causes significant difficulty or confusion | Fix before release | +| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass | +| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits | + +**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1. + +--- + +### Persona-Based Design Testing + +Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss. + +**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns. + +--- + +#### 1. Impatient Power User: "Alex" + +**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave. + +**Behaviors**: +- Skips all onboarding and instructions +- Looks for keyboard shortcuts immediately +- Tries to bulk-select, batch-edit, and automate +- Gets frustrated by required steps that feel unnecessary +- Abandons if anything feels slow or patronizing + +**Test Questions**: +- Can Alex complete the core task in under 60 seconds? +- Are there keyboard shortcuts for common actions? +- Can onboarding be skipped entirely? +- Do modals have keyboard dismiss (Esc)? +- Is there a "power user" path (shortcuts, bulk actions)? + +**Red Flags** (report these specifically): +- Forced tutorials or unskippable onboarding +- No keyboard navigation for primary actions +- Slow animations that can't be skipped +- One-item-at-a-time workflows where batch would be natural +- Redundant confirmation steps for low-risk actions + +--- + +#### 2. Confused First-Timer: "Jordan" + +**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out. + +**Behaviors**: +- Reads all instructions carefully +- Hesitates before clicking anything unfamiliar +- Looks for help or support constantly +- Misunderstands jargon and abbreviations +- Takes the most literal interpretation of any label + +**Test Questions**: +- Is the first action obviously clear within 5 seconds? +- Are all icons labeled with text? +- Is there contextual help at decision points? +- Does terminology assume prior knowledge? +- Is there a clear "back" or "undo" at every step? + +**Red Flags** (report these specifically): +- Icon-only navigation with no labels +- Technical jargon without explanation +- No visible help option or guidance +- Ambiguous next steps after completing an action +- No confirmation that an action succeeded + +--- + +#### 3. Accessibility-Dependent User: "Sam" + +**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences. + +**Behaviors**: +- Tabs through the interface linearly +- Relies on ARIA labels and heading structure +- Cannot see hover states or visual-only indicators +- Needs adequate color contrast (4.5:1 minimum) +- May use browser zoom up to 200% + +**Test Questions**: +- Can the entire primary flow be completed keyboard-only? +- Are all interactive elements focusable with visible focus indicators? +- Do images have meaningful alt text? +- Is color contrast WCAG AA compliant (4.5:1 for text)? +- Does the screen reader announce state changes (loading, success, errors)? + +**Red Flags** (report these specifically): +- Click-only interactions with no keyboard alternative +- Missing or invisible focus indicators +- Meaning conveyed by color alone (red = error, green = success) +- Unlabeled form fields or buttons +- Time-limited actions without extension option +- Custom components that break screen reader flow + +--- + +#### 4. Deliberate Stress Tester: "Riley" + +**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience. + +**Behaviors**: +- Tests edge cases intentionally (empty states, long strings, special characters) +- Submits forms with unexpected data (emoji, RTL text, very long values) +- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs +- Looks for inconsistencies between what the UI promises and what actually happens +- Documents problems methodically + +**Test Questions**: +- What happens at the edges (0 items, 1000 items, very long text)? +- Do error states recover gracefully or leave the UI in a broken state? +- What happens on refresh mid-workflow? Is state preserved? +- Are there features that appear to work but produce broken results? +- How does the UI handle unexpected input (emoji, special chars, paste from Excel)? + +**Red Flags** (report these specifically): +- Features that appear to work but silently fail or produce wrong results +- Error handling that exposes technical details or leaves UI in a broken state +- Empty states that show nothing useful ("No results" with no guidance) +- Workflows that lose user data on refresh or navigation +- Inconsistent behavior between similar interactions in different parts of the UI + +--- + +#### 5. Distracted Mobile User: "Casey" + +**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection. + +**Behaviors**: +- Uses thumb only; prefers bottom-of-screen actions +- Gets interrupted mid-flow and returns later +- Switches between apps frequently +- Has limited attention span and low patience +- Types as little as possible, prefers taps and selections + +**Test Questions**: +- Are primary actions in the thumb zone (bottom half of screen)? +- Is state preserved if the user leaves and returns? +- Does it work on slow connections (3G)? +- Can forms use autocomplete and smart defaults? +- Are touch targets at least 44×44pt? + +**Red Flags** (report these specifically): +- Important actions positioned at the top of the screen (unreachable by thumb) +- No state persistence; progress lost on tab switch or interruption +- Large text inputs required where selection would work +- Heavy assets loading on every page (no lazy loading) +- Tiny tap targets or targets too close together + +--- + +#### Selecting Personas + +Choose personas based on the interface type: + +| Interface Type | Primary Personas | Why | +|---------------|-----------------|-----| +| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile | +| Dashboard / admin | Alex, Sam | Power users, accessibility | +| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity | +| Onboarding flow | Jordan, Casey | Confusion, interruption | +| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav | +| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile | + +--- + +#### Project-Specific Personas + +If `GEMINI.md` contains a `## Design Context` section (generated by `impeccable init`), derive 1–2 additional personas from the audience and brand information: + +1. Read the target audience description +2. Identify the primary user archetype not covered by the 5 predefined personas +3. Create a persona following this template: + +``` +##### [Role]: "[Name]" + +**Profile**: [2-3 key characteristics derived from Design Context] + +**Behaviors**: [3-4 specific behaviors based on the described audience] + +**Red Flags**: [3-4 things that would alienate this specific user type] +``` + +Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists. diff --git a/.gemini/skills/impeccable/reference/document.md b/.gemini/skills/impeccable/reference/document.md index 9dbd30435..60075688a 100644 --- a/.gemini/skills/impeccable/reference/document.md +++ b/.gemini/skills/impeccable/reference/document.md @@ -45,7 +45,7 @@ Rules that matter: - **Token refs** use `{path.to.token}` (e.g. `{colors.primary}`, `{rounded.md}`). Components may reference primitives; primitives may not reference each other. - **Stitch validates colors as hex sRGB only** (`#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`); OKLCH/HSL/P3 trigger a linter warning, not a hard error. YAML accepts the string either way and our own parser is format-agnostic. Choose based on project posture: (a) if the project has an "OKLCH-only" doctrine or uses Display-P3 values that don't round-trip through sRGB, put OKLCH directly in the frontmatter and accept the Stitch linter warning; (b) if the project wants strict Stitch compliance or plans to use their Tailwind/DTCG export pipeline, put hex in the frontmatter and keep OKLCH in prose as the canonical reference. Never split the source of truth without explicit reason. - **Component sub-tokens** are limited to 8 props: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, `width`. Shadows, motion, focus rings, backdrop-filter: none of those fit. Carry them in the sidecar (Step 4b). -- **Scale keys are open-ended.** Use whatever names the project already uses (`warm-ash-cream`, `surface-container-low`). Don't rename to Material defaults. +- **Scale keys are open-ended.** Use whatever names the project already uses (`oxblood-deep`, `surface-container-low`). Don't rename to Material defaults. - **Variants are naming convention, not schema.** `button-primary` / `button-primary-hover` / `button-primary-active` as sibling keys. ## The markdown body: six sections (exact order) @@ -61,7 +61,7 @@ Optional evocative subtitles are allowed in the form `## 2. Colors: The [Name] P ## When to run -- The user just ran `/impeccable teach` and needs the visual side documented. +- The user just ran `/impeccable init` and needs the visual side documented. - The skill noticed no `DESIGN.md` exists and nudged the user to create one. - An existing `DESIGN.md` is stale (the design has drifted). - Before a large redesign, to capture the current state as a reference. @@ -71,7 +71,7 @@ If a `DESIGN.md` already exists, **do not silently overwrite it**. Show the user ## Two paths - **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 teach, nothing built yet). Interview for five high-level answers, write a minimal DESIGN.md marked ``. Re-run in scan mode once there's code. +- **Seed mode**: the project is pre-implementation (fresh init, nothing built yet). Interview for five high-level answers, 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` forces seed mode regardless of code presence. @@ -103,7 +103,7 @@ Build a structured draft from the discovered tokens. For each token class: From the auto-extracted tokens, draft the YAML frontmatter now (you'll write it at the top of DESIGN.md in Step 4). This is the machine-readable layer: what the live panel and Stitch's linter consume. -- **Colors**: one entry per extracted color. Key = descriptive slug (`warm-ash-cream`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. +- **Colors**: one entry per extracted color. Key = descriptive slug (`oxblood-deep`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. - **Typography**: one entry per role (`display`, `headline`, `title`, `body`, `label`). Typography is an object; include only the props that are real for the project (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`). - **Rounded / Spacing**: whatever scale steps the project actually uses, keyed by whatever scale name the project uses (`sm` / `md` / `lg`, or `surface-sm`, or numeric steps). - **Components**: one entry per variant (`button-primary`, `button-primary-hover`, `button-ghost`). Reference primitives via `{colors.X}`, `{rounded.Y}`. If a variant needs a property Stitch's 8-prop set doesn't cover (shadow, focus ring, backdrop-filter), carry the full snippet in the sidecar instead. @@ -253,7 +253,7 @@ Regenerate the sidecar whenever you regenerate root `DESIGN.md`. If the user onl "extensions": { "colorMeta": { "primary": { "role": "primary", "displayName": "Editorial Magenta", "canonical": "oklch(60% 0.25 350)", "tonalRamp": ["...", "...", "..."] }, - "warm-ash-cream": { "role": "neutral", "displayName": "Warm Ash Cream", "canonical": "oklch(96% 0.005 350)", "tonalRamp": ["...", "...", "..."] } + "cool-paper": { "role": "neutral", "displayName": "Cool Paper", "canonical": "oklch(96% 0.005 230)", "tonalRamp": ["...", "...", "..."] } }, "typographyMeta": { "display": { "displayName": "Display", "purpose": "Hero headlines only." } @@ -328,12 +328,13 @@ Pull directly from the DESIGN.md you just wrote: Do not reword. The panel shows these as secondary collapsible context; the same voice that's in the Markdown carries through. -### Step 5: Confirm, refine, and refresh session cache +### Step 5: Confirm and refine 1. Show the user the full DESIGN.md you wrote. Briefly highlight the non-obvious creative choices (descriptive color names, atmosphere language, named rules). 2. Mention that `.impeccable/design.json` was also written alongside; the live panel will now render this project's actual button/input/nav primitives instead of generic approximations. 3. Offer to refine any section: "Want me to revise a section, add component patterns I missed, or adjust the atmosphere language?" -4. **Refresh the session cache.** Run `node .gemini/skills/impeccable/scripts/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. + +Your own write is the freshest source; subsequent commands in this session don't need a reload. ## Seed mode @@ -394,11 +395,12 @@ Per-section guidance in seed mode: 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. -### Step 4: Confirm and refresh session cache +### Step 4: Confirm 1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment). 2. Tell the user: "Re-run `/impeccable document` once you have some code. That pass will extract real tokens and generate the sidecar." -3. Run `node .gemini/skills/impeccable/scripts/load-context.mjs` once so the seed lands in conversation for the rest of the session. + +Your own write is the freshest source; no reload needed. ## Style guidelines diff --git a/.gemini/skills/impeccable/reference/heuristics-scoring.md b/.gemini/skills/impeccable/reference/heuristics-scoring.md deleted file mode 100644 index edbe5028d..000000000 --- a/.gemini/skills/impeccable/reference/heuristics-scoring.md +++ /dev/null @@ -1,234 +0,0 @@ -# Heuristics Scoring Guide - -Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough." - -## Nielsen's 10 Heuristics - -### 1. Visibility of System Status - -Keep users informed about what's happening through timely, appropriate feedback. - -**Check for**: -- Loading indicators during async operations -- Confirmation of user actions (save, submit, delete) -- Progress indicators for multi-step processes -- Current location in navigation (breadcrumbs, active states) -- Form validation feedback (inline, not just on submit) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | No feedback; user is guessing what happened | -| 1 | Rare feedback; most actions produce no visible response | -| 2 | Partial; some states communicated, major gaps remain | -| 3 | Good; most operations give clear feedback, minor gaps | -| 4 | Excellent; every action confirms, progress is always visible | - -### 2. Match Between System and Real World - -Speak the user's language. Follow real-world conventions. Information appears in natural, logical order. - -**Check for**: -- Familiar terminology (no unexplained jargon) -- Logical information order matching user expectations -- Recognizable icons and metaphors -- Domain-appropriate language for the target audience -- Natural reading flow (left-to-right, top-to-bottom priority) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Pure tech jargon, alien to users | -| 1 | Mostly confusing; requires domain expertise to navigate | -| 2 | Mixed; some plain language, some jargon leaks through | -| 3 | Mostly natural; occasional term needs context | -| 4 | Speaks the user's language fluently throughout | - -### 3. User Control and Freedom - -Users need a clear "emergency exit" from unwanted states without extended dialogue. - -**Check for**: -- Undo/redo functionality -- Cancel buttons on forms and modals -- Clear navigation back to safety (home, previous) -- Easy way to clear filters, search, selections -- Escape from long or multi-step processes - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Users get trapped; no way out without refreshing | -| 1 | Difficult exits; must find obscure paths to escape | -| 2 | Some exits; main flows have escape, edge cases don't | -| 3 | Good control; users can exit and undo most actions | -| 4 | Full control; undo, cancel, back, and escape everywhere | - -### 4. Consistency and Standards - -Users shouldn't wonder whether different words, situations, or actions mean the same thing. - -**Check for**: -- Consistent terminology throughout the interface -- Same actions produce same results everywhere -- Platform conventions followed (standard UI patterns) -- Visual consistency (colors, typography, spacing, components) -- Consistent interaction patterns (same gesture = same behavior) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Inconsistent everywhere; feels like different products stitched together | -| 1 | Many inconsistencies; similar things look/behave differently | -| 2 | Partially consistent; main flows match, details diverge | -| 3 | Mostly consistent; occasional deviation, nothing confusing | -| 4 | Fully consistent; cohesive system, predictable behavior | - -### 5. Error Prevention - -Better than good error messages is a design that prevents problems in the first place. - -**Check for**: -- Confirmation before destructive actions (delete, overwrite) -- Constraints preventing invalid input (date pickers, dropdowns) -- Smart defaults that reduce errors -- Clear labels that prevent misunderstanding -- Autosave and draft recovery - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Errors easy to make; no guardrails anywhere | -| 1 | Few safeguards; some inputs validated, most aren't | -| 2 | Partial prevention; common errors caught, edge cases slip | -| 3 | Good prevention; most error paths blocked proactively | -| 4 | Excellent; errors nearly impossible through smart constraints | - -### 6. Recognition Rather Than Recall - -Minimize memory load. Make objects, actions, and options visible or easily retrievable. - -**Check for**: -- Visible options (not buried in hidden menus) -- Contextual help when needed (tooltips, inline hints) -- Recent items and history -- Autocomplete and suggestions -- Labels on icons (not icon-only navigation) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Heavy memorization; users must remember paths and commands | -| 1 | Mostly recall; many hidden features, few visible cues | -| 2 | Some aids; main actions visible, secondary features hidden | -| 3 | Good recognition; most things discoverable, few memory demands | -| 4 | Everything discoverable; users never need to memorize | - -### 7. Flexibility and Efficiency of Use - -Accelerators, invisible to novices, speed up expert interaction. - -**Check for**: -- Keyboard shortcuts for common actions -- Customizable interface elements -- Recent items and favorites -- Bulk/batch actions -- Power user features that don't complicate the basics - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | One rigid path; no shortcuts or alternatives | -| 1 | Limited flexibility; few alternatives to the main path | -| 2 | Some shortcuts; basic keyboard support, limited bulk actions | -| 3 | Good accelerators; keyboard nav, some customization | -| 4 | Highly flexible; multiple paths, power features, customizable | - -### 8. Aesthetic and Minimalist Design - -Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose. - -**Check for**: -- Only necessary information visible at each step -- Clear visual hierarchy directing attention -- Purposeful use of color and emphasis -- No decorative clutter competing for attention -- Focused, uncluttered layouts - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Overwhelming; everything competes for attention equally | -| 1 | Cluttered; too much noise, hard to find what matters | -| 2 | Some clutter; main content clear, periphery noisy | -| 3 | Mostly clean; focused design, minor visual noise | -| 4 | Perfectly minimal; every element earns its pixel | - -### 9. Help Users Recognize, Diagnose, and Recover from Errors - -Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution. - -**Check for**: -- Plain language error messages (no error codes for users) -- Specific problem identification ("Email is missing @" not "Invalid input") -- Actionable recovery suggestions -- Errors displayed near the source of the problem -- Non-blocking error handling (don't wipe the form) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Cryptic errors; codes, jargon, or no message at all | -| 1 | Vague errors; "Something went wrong" with no guidance | -| 2 | Clear but unhelpful; names the problem but not the fix | -| 3 | Clear with suggestions; identifies problem and offers next steps | -| 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work | - -### 10. Help and Documentation - -Even if the system is usable without docs, help should be easy to find, task-focused, and concise. - -**Check for**: -- Searchable help or documentation -- Contextual help (tooltips, inline hints, guided tours) -- Task-focused organization (not feature-organized) -- Concise, scannable content -- Easy access without leaving current context - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | No help available anywhere | -| 1 | Help exists but hard to find or irrelevant | -| 2 | Basic help; FAQ or docs exist, not contextual | -| 3 | Good documentation; searchable, mostly task-focused | -| 4 | Excellent contextual help; right info at the right moment | - ---- - -## Score Summary - -**Total possible**: 40 points (10 heuristics × 4 max) - -| Score Range | Rating | What It Means | -|-------------|--------|---------------| -| 36–40 | Excellent | Minor polish only; ship it | -| 28–35 | Good | Address weak areas, solid foundation | -| 20–27 | Acceptable | Significant improvements needed before users are happy | -| 12–19 | Poor | Major UX overhaul required; core experience broken | -| 0–11 | Critical | Redesign needed; unusable in current state | - ---- - -## Issue Severity (P0–P3) - -Tag each individual issue found during scoring with a priority level: - -| Priority | Name | Description | Action | -|----------|------|-------------|--------| -| **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper | -| **P1** | Major | Causes significant difficulty or confusion | Fix before release | -| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass | -| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits | - -**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1. diff --git a/.gemini/skills/impeccable/reference/teach.md b/.gemini/skills/impeccable/reference/init.md similarity index 58% rename from .gemini/skills/impeccable/reference/teach.md rename to .gemini/skills/impeccable/reference/init.md index 4857a9b1c..a2b019164 100644 --- a/.gemini/skills/impeccable/reference/teach.md +++ b/.gemini/skills/impeccable/reference/init.md @@ -1,21 +1,16 @@ -# Teach Flow +# Init Flow -Gathers design context for a project and writes two complementary files at the project root: +The setup command for a project. One codebase crawl feeds everything it writes: - **PRODUCT.md** (strategic): root project file for register, target users, product purpose, brand personality, anti-references, strategic design principles. Answers "who/what/why". - **DESIGN.md** (visual): root project file for visual theme, color palette, typography, components, layout. Follows the [Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/). Answers "how it looks". +- **`.impeccable/live/config.json`** (live mode): pre-configured so `/impeccable live` boots straight into variant mode with no first-time detour. -Every other impeccable command reads these files before doing any work. +It closes by pointing the user at the best command to run next. Every other impeccable command reads PRODUCT.md and DESIGN.md before doing any work. ## Step 1: Load current state -Run the shared loader first so you know what already exists: - -```bash -node .gemini/skills/impeccable/scripts/load-context.mjs -``` - -The output tells you whether PRODUCT.md and/or DESIGN.md already exist. If `migrated: true`, legacy `.impeccable.md` was auto-renamed to `PRODUCT.md`. Mention this once to the user. +Check what already exists. PRODUCT.md and DESIGN.md live at the project root, or under `.agents/context/` or `docs/` (case-insensitive). Read whichever are present with your native file tool. Also note whether `.impeccable/live/config.json` already exists (Step 6 leaves it untouched if so). Decision tree: - **Neither file exists (empty project or no context yet)**: do Steps 2-4 (write PRODUCT.md), then decide on DESIGN.md based on whether there's code to analyze. @@ -26,14 +21,14 @@ Decision tree: Never silently overwrite an existing file. Always confirm first. -If teach was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete teach, re-run the loader, then resume the original command with the freshly loaded context. For craft, resume into shape next; teach creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief. +If init was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete init, then resume the original command. Your own writes are the freshest source; no reload needed. For craft, resume into shape next; init creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief. ## Step 2: Explore the codebase -Before asking questions, thoroughly scan the project to discover what you can: +Before asking questions, thoroughly scan the project to discover what you can. This single crawl feeds PRODUCT.md, DESIGN.md, **and** the live-mode framework detection in Step 6, so be thorough once rather than re-scanning later: - **README and docs**: Project purpose, target audience, any stated goals -- **Package.json / config files**: Tech stack, dependencies, existing design libraries +- **Package.json / config files**: Tech stack, dependencies, existing design libraries, **and the framework** (Vite/SPA, Next.js, Nuxt, SvelteKit, Astro, multi-page static) plus the HTML entry the browser actually loads - **Existing components**: Current design patterns, spacing, typography in use - **Brand assets**: Logos, favicons, color values already defined - **Design tokens / CSS variables**: Existing color palettes, font stacks, spacing scales @@ -46,7 +41,7 @@ Also form a **register hypothesis** from what you find: Register is a hypothesis at this point, not a decision; Step 3 confirms it. -Note what you've learned and what remains unclear. This exploration feeds both PRODUCT.md and DESIGN.md. +Note what you've learned and what remains unclear. Also note any rough edges worth a follow-up command (thin hierarchy, flat or gray palette, missing error/empty states, dull copy); Step 7 turns these into concrete recommendations without re-analyzing. ## Step 3: Ask strategic questions (for PRODUCT.md) @@ -84,8 +79,7 @@ If the signal is genuinely split (e.g. a product with a big marketing landing), ### Brand & Personality - How would you describe the brand personality in 3 words? - Reference sites or apps that capture the right feel? What specifically about them? - - For brand, push for real-world references in the right lane (tech-minimal, editorial-magazine, consumer-warm, brutalist-grid, etc.), not generic "modern" adjectives. - - For product, push for category best-tool references (Linear, Figma, Notion, Raycast, Stripe). + - Push for specific named references with the *specific* thing about them that fits this brand, not generic "modern" adjectives or category-bucket lanes. - What should this explicitly NOT look like? Any anti-references? ### Accessibility & Inclusion @@ -141,16 +135,38 @@ If the user agrees, delegate to `/impeccable document` (it auto-detects scan vs If the user prefers to skip, mention they can run `/impeccable document` any time later. -## Step 6: Confirm and wrap up +## Step 6: Configure live mode (when code exists) -Summarize: +If the project has code with HTML entries and a dev server (the same "code exists" condition that puts `/impeccable document` in scan mode), pre-configure live mode now. You already identified the framework and the served HTML entry in Step 2, so this is nearly free, and it spares the user the first-time setup detour when they later run `/impeccable live`. + +**Skip this step for empty / pre-implementation projects** (nothing to inject into yet). Tell the user live mode will configure itself the first time they run it once there's code. + +**If `.impeccable/live/config.json` already exists, leave it untouched** and note that live mode is already configured. + +Otherwise: + +1. Write `.impeccable/live/config.json`. Choose `files` (the HTML entries the browser actually loads), `insertBefore`, and `commentSyntax` from the framework table in [live.md](live.md)'s **First-time setup** section, using the framework you found in Step 2. That table is canonical; do not restate it here. For multi-page static sites, prefer a glob (`["public/**/*.html"]`) over a literal list. +2. Run `node .gemini/skills/impeccable/scripts/detect-csp.mjs`. If it reports a patchable shape (`append-arrays` / `append-string`), use the **consent prompt template** from live.md before editing any source file. On decline, skip the patch. For `middleware` / `meta-tag` shapes, surface the detected files and ask the user to add `http://localhost:8400` to `script-src` and `connect-src` manually. For `null`, there's nothing to do. +3. Set `cspChecked: true` in the config once CSP is handled (patched, declined, manual, or not needed). The schema and per-shape patch details live in live.md's First-time setup; follow it rather than duplicating. + +Writing the config file is harmless and needs no consent; only the CSP **source-file patch** requires a yes. + +## Step 7: Recommend starting points, then wrap up + +Summarize tersely: - Register captured (brand / product) -- What was written (PRODUCT.md, DESIGN.md, or both) +- What was written (PRODUCT.md, DESIGN.md, live config, or a subset) - The 3-5 strategic principles from PRODUCT.md that will guide future work -- If DESIGN.md is pending, remind the user how to generate it later +- If DESIGN.md or live config is pending, one line on how to set it up later -**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node .gemini/skills/impeccable/scripts/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. +Then recommend the **best commands to run next**, drawn from what your Step 2 crawl already surfaced. Do not run a fresh analysis here; surface observations you already have. Tailor to register and to what you saw, offer the 2-4 most relevant (not a menu dump), and give the exact command to type. Group by intent: -If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context. +- **Build something new**: `/impeccable craft ` (shape, then build end-to-end) or `/impeccable shape ` (plan first). Lead with this for empty or early-stage projects. +- **Improve what's there**: name the specific surface. `/impeccable critique ` for a scored UX review; `/impeccable audit ` for a11y / perf / responsive checks; `/impeccable polish ` for a pre-ship pass. When the crawl flagged a specific weakness, point the matching command at it: thin hierarchy or spacing → `layout`, flat or gray palette → `colorize`, missing error / empty states → `harden` or `onboard`, dull or unclear copy → `clarify`. +- **Iterate visually**: `/impeccable live` (configured in Step 6) to pick elements in the browser and generate variants in place. + +The full command menu is one bare `/impeccable` away; keep this list short and pointed. + +If init was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now. Your own writes are the freshest source; no reload needed. Optionally ask the user directly to clarify what you cannot infer. Ask whether they'd like a brief summary of PRODUCT.md appended to GEMINI.md for easier agent reference. If yes, append a short **Design Context** pointer section there. diff --git a/.gemini/skills/impeccable/reference/interaction-design.md b/.gemini/skills/impeccable/reference/interaction-design.md index 15aed5b29..c42c9d810 100644 --- a/.gemini/skills/impeccable/reference/interaction-design.md +++ b/.gemini/skills/impeccable/reference/interaction-design.md @@ -150,12 +150,6 @@ For browsers without anchor positioning support, `position: fixed` with manual c Check viewport boundaries before rendering. If the dropdown would overflow the bottom edge, flip it above the trigger. If it would overflow the right edge, align it to the trigger's right side instead. -### Anti-Patterns - -- **`position: absolute` inside `overflow: hidden`** - The dropdown will be clipped. Use `position: fixed` or the top layer instead. -- **Arbitrary z-index values** like `z-index: 9999` - Use a semantic z-index scale: `dropdown (100) -> sticky (200) -> modal-backdrop (300) -> modal (400) -> toast (500) -> tooltip (600)`. -- **Rendering dropdown markup inline** without an escape hatch from the parent's stacking context. Either use `popover` (top layer), a portal, or `position: fixed`. - ## Destructive Actions: Undo > Confirm **Undo is better than confirmation dialogs.** Users click through confirmations mindlessly. Remove from UI immediately, show undo toast, actually delete after toast expires. Use confirmation only for truly irreversible actions (account deletion), high-cost actions, or batch operations. diff --git a/.gemini/skills/impeccable/reference/layout.md b/.gemini/skills/impeccable/reference/layout.md index cad4c62a1..8765978b0 100644 --- a/.gemini/skills/impeccable/reference/layout.md +++ b/.gemini/skills/impeccable/reference/layout.md @@ -1,4 +1,4 @@ -Space is the most underused design tool. Find the layout's actual problem (monotone spacing, weak hierarchy, identical card grids, the centered-stack default) and fix the structure, not the surface. +Space is the most underused design tool. Find the layout's actual problem (monotone spacing, weak hierarchy, identical card grids) and fix the structure, not the surface. --- @@ -27,7 +27,6 @@ Analyze what's weak about the current spatial design: 3. **Grid & structure**: - Is there a clear underlying structure, or does the layout feel random? - Are identical card grids used everywhere? (Icon + heading + text, repeated endlessly) - - Is everything centered? (Left-aligned with asymmetric layouts feels more designed, but not a hard and fast rule) 4. **Rhythm & variety**: - Does the layout have visual rhythm? (Alternating tight/generous spacing) @@ -43,8 +42,6 @@ Analyze what's weak about the current spatial design: ## Plan Layout Improvements -Consult the [spatial design reference](spatial-design.md) for detailed guidance on grids, rhythm, and container queries. - Create a systematic plan: - **Spacing system**: Use a consistent scale (a framework's built-in scale like Tailwind's, rem-based tokens, or a custom system). The specific values matter less than consistency. @@ -57,6 +54,7 @@ Create a systematic plan: ### Establish a Spacing System - Use a consistent spacing scale (framework scales like Tailwind, rem-based tokens, or a custom scale all work). What matters is that values come from a defined set, not arbitrary numbers. +- Prefer a 4pt base scale (4, 8, 12, 16, 24, 32, 48, 64, 96px) over 8pt; 8pt is too coarse and you'll frequently need 12px between 8 and 16. - Name tokens semantically if using custom properties: `--space-xs` through `--space-xl`, not `--spacing-8` - Use `gap` for sibling spacing instead of margins; eliminates margin collapse hacks - Apply `clamp()` for fluid spacing that breathes on larger screens @@ -66,15 +64,22 @@ Create a systematic plan: - **Tight grouping** for related elements (8-12px between siblings) - **Generous separation** between distinct sections (48-96px) - **Varied spacing** within sections (not every row needs the same gap) -- **Asymmetric compositions**: break the predictable centered-content pattern when it makes sense +- **Asymmetric compositions**: a deliberate choice when the content invites it (not a default to chase). ### Choose the Right Layout Tool -- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. Flex is simpler and more appropriate for the majority of layout tasks. +- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. - **Use Grid for 2D layouts**: Page-level structure, dashboards, data-dense interfaces, anything where rows AND columns need coordinated control. -- **Don't default to Grid** when Flexbox with `flex-wrap` would be simpler and more flexible. -- Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. - Use named grid areas (`grid-template-areas`) for complex page layouts; redefine at breakpoints. +- Use **container queries** for components, viewport queries for page layouts. A card in a narrow sidebar can stay compact while the same card in a main content area expands automatically: + +```css +.card-container { container-type: inline-size; } +.card { display: grid; gap: var(--space-md); } +@container (min-width: 400px) { + .card { grid-template-columns: 120px 1fr; } +} +``` ### Break Card Grid Monotony @@ -85,18 +90,36 @@ Create a systematic plan: ### Strengthen Visual Hierarchy - Use the fewest dimensions needed for clear hierarchy. Space alone can be enough; generous whitespace around an element draws the eye. Some of the most polished designs achieve rhythm with just space and weight. Add color or size contrast only when simpler means aren't sufficient. +- The best hierarchy combines 2–3 dimensions at once. A heading that's larger, bolder, AND has more space above it reads as primary without trying: + +| Tool | Strong Hierarchy | Weak Hierarchy | +|------|------------------|----------------| +| **Size** | 3:1 ratio or more | <2:1 ratio | +| **Weight** | Bold vs Regular | Medium vs Regular | +| **Color** | High contrast | Similar tones | +| **Position** | Top/left (primary) | Bottom/right | +| **Space** | Surrounded by white space | Crowded | + - Be aware of reading flow: in LTR languages, the eye naturally scans top-left to bottom-right, but primary action placement depends on context (e.g., bottom-right in dialogs, top in navigation). - Create clear content groupings through proximity and separation. ### Manage Depth & Elevation -- Create a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip) - Build a consistent shadow scale (sm → md → lg → xl); shadows should be subtle - Use elevation to reinforce hierarchy, not as decoration ### Optical Adjustments - If an icon looks visually off-center despite being geometrically centered, nudge it. But only if you're confident it actually looks wrong. Don't adjust speculatively. +- Text at `margin-left: 0` looks slightly indented because of letterform whitespace; a negative margin (`-0.05em`) optically aligns it. Geometrically centered glyphs often look off-center (play icons need to shift right, arrows shift toward their direction). +- Touch targets must be 44×44px minimum even when the visual element is smaller. Expand the hit area with padding or a pseudo-element: + +```css +.icon-button { width: 24px; height: 24px; position: relative; } +.icon-button::before { + content: ''; position: absolute; inset: -10px; +} +``` **NEVER**: - Use arbitrary spacing values outside your scale @@ -104,10 +127,7 @@ Create a systematic plan: - Wrap everything in cards (not everything needs a container) - Nest cards inside cards (use spacing and dividers for hierarchy within) - Use identical card grids everywhere (icon + heading + text, repeated) -- Center everything (left-aligned with asymmetry feels more designed) - Default to the hero metric layout (big number, small label, stats, gradient) as a template. If showing real user data, a prominent metric can work, but it should display actual data, not decorative numbers. -- Default to CSS Grid when Flexbox would be simpler; use the simplest tool for the job -- Use arbitrary z-index values (999, 9999); build a semantic scale ## Verify Layout Improvements diff --git a/.gemini/skills/impeccable/reference/live.md b/.gemini/skills/impeccable/reference/live.md index 6a71a250e..4a30cf27e 100644 --- a/.gemini/skills/impeccable/reference/live.md +++ b/.gemini/skills/impeccable/reference/live.md @@ -9,18 +9,21 @@ A running dev server with hot module replacement (Vite, Next.js, Bun, etc.), OR Execute in order. No step skipped, no step reordered. 1. `live.mjs`: boot. -2. Navigate to the URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). If you can't infer it confidently, tell the user once to open their dev/preview URL. Never use `serverPort` as that URL; it's the helper, not the app. +2. Open the app URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). Never use `serverPort`; it's the helper, not the app. **Cursor:** `browser_navigate` to that URL before polling; do not skip. **Other harnesses:** use the available browser tool; if the URL is uncertain, ask the user once. 3. Poll loop with the default long timeout (600000 ms). After every event or `--reply`, run `live-poll.mjs` again immediately. Never pass a short `--timeout=`. + +The global bar **Impeccable mark** dims and shows a pulsing amber dot when no agent is long-polling `/poll`. Hover the mark for the hint; restart `live-poll.mjs` to reconnect. 4. On `generate`: read screenshot if present; load the action's reference; plan three distinct directions; write all variants in one edit; `--reply done`; poll again. -5. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts remain recoverable until you finish cleanup, run `live-complete.mjs --id EVENT_ID`, and only then poll again. -6. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart. -7. On `exit`: run the cleanup at the bottom. +5. On `steer`: read the message and `pageUrl`; do the work (page edits, navigation help, or a short reply in the `--reply` message); `--reply steer_done`; poll again. No pickup ack. The Steer bar unlocks when `steer_done` arrives over SSE. +6. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts remain recoverable until you finish cleanup, run `live-complete.mjs --id EVENT_ID`, and only then poll again. +7. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart. +8. On `exit`: run the cleanup at the bottom. Harness policy: - **Claude Code**: run the poll as a **background task** (no short timeout). The harness notifies you when it completes, so the main conversation stays free. Do not block the shell. -- **Cursor**: run the poll in the **foreground** (blocking shell; not a background terminal, not a subagent). Cursor background terminals and subagents do not reliably resume the chat with poll stdout. +- **Cursor**: run **one-shot** poll in a **background terminal** with notify on `"type":"(steer|generate|accept|discard|exit)"`. After each event the poll exits; handle it, `--reply`, then start `live-poll.mjs` again. Do **not** use `--stream` on Cursor: incremental stdout notify is slower in practice than exit-based notify (~5s vs sub-second in testing). - **Codex**: run the poll in the **foreground** (blocking shell; not a background task, not a subagent). Codex background exec sessions do not reliably surface poll stdout back into the conversation at the moment events arrive, so a "fire-and-forget" background poll will stall live mode. -- **Other harnesses**: foreground unless you know stdout reliably returns to this session. +- **Other harnesses**: one-shot foreground unless you know stdout reliably returns to this session when a shell exits. Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodies. Spend tokens on tools and edits; on failure, one or two short sentences. @@ -30,7 +33,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi node .gemini/skills/impeccable/scripts/live.mjs ``` -Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation; **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md. +Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation; **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. `serverPort` and `serverToken` belong to the small **Impeccable live helper** HTTP server (serves `/live.js`, SSE, and `/poll`). That port is **not** your dev server and is usually not the URL you open to view the app. The browser page is whatever origin serves one of the `pageFiles` entries (Vite / Next / Bun / tunnel / LAN hostname). @@ -38,12 +41,15 @@ If output is `{ ok: false, error: "config_missing" | "config_invalid", path }`, ## Poll loop +**Default (portable, all harnesses):** + ``` LOOP: node .gemini/skills/impeccable/scripts/live-poll.mjs # default long timeout; no --timeout= Read JSON; dispatch on "type" "generate" → Handle Generate; reply done; LOOP + "steer" → Handle Steer; reply steer_done; LOOP "accept" → Handle Accept; complete carbonize cleanup if required; LOOP "discard" → Handle Discard; LOOP "prefetch" → Handle Prefetch; LOOP @@ -51,6 +57,16 @@ LOOP: "exit" → break → Cleanup ``` +**Stream mode (experimental, not for Cursor):** + +``` +node .gemini/skills/impeccable/scripts/live-poll.mjs --stream # stays running; one JSON line per event + Handle event; run --reply in a separate command + Repeat until "exit" line → Cleanup +``` + +Stream keeps one process alive and waits for `--reply` ack before polling again. Useful only when the harness reads incremental stdout reliably and quickly. **Cursor is not one of those:** background pattern notify on a long-running shell was ~5s to pick up events vs sub-second for one-shot exit notify. Default to one-shot everywhere unless you have measured otherwise. + ## Recovery commands The live helper persists an append-only journal under `.impeccable/live/sessions/`. Browser checkpoints are advisory but durable; the journal is canonical. This is local durable recovery state, not project source. @@ -71,9 +87,32 @@ Server restart rule: start `live-server.mjs` again, then poll. Startup requeues ## Handle `generate` -Event: `{id, action, freeformPrompt?, count, pageUrl, element, screenshotPath?, comments?, strokes?}`. +**Replace mode** (default): `{id, action, freeformPrompt?, count, pageUrl, element, screenshotPath?, comments?, strokes?}`. -Speed matters; the user is watching a spinner. Minimize tool calls by using the `wrap` helper and writing all variants in a single edit. +**Insert mode** (`event.mode === "insert"`): `{id, mode: "insert", count, pageUrl, insert: { position, anchor }, placeholder: { width, height }, freeformPrompt?, screenshotPath?, comments?, strokes?}`. No `action`. Requires a non-empty `freeformPrompt` **or** annotations. Screenshot is sent only when annotations exist (same rule as replace). Use `placeholder` dimensions as a soft size hint for net-new content. + +Speed matters; the user is watching a spinner. Minimize tool calls by using the wrap/insert helper and writing all variants in a single edit. + +### Insert mode branch + +When `event.mode === "insert"`: + +1. Read the screenshot if `event.screenshotPath` is present (annotations only). +2. Run the insert helper instead of wrap: + +```bash +node .gemini/skills/impeccable/scripts/live-insert.mjs --id EVENT_ID --count EVENT_COUNT --position after \ + --element-id "ANCHOR_ID" --classes "class1,class2" --tag "section" --text "ANCHOR_TEXT" +``` + +- `--position` ← `event.insert.position` (`before` | `after`) +- Anchor flags ← `event.insert.anchor` (same mapping as wrap: id, classes, tag, text) + +The scaffold has **no** `data-impeccable-variant="original"`. Variants are net-new HTML+CSS inserted at `insertLine`. Load `brand.md` or `product.md` (freeform only, no action sub-command). Write all variants in one edit, then `--reply done`. + +On accept/discard, `live-accept.mjs` removes the wrapper block; the anchor element is untouched. + +### Replace mode (default) ### 1. Read the screenshot (if present) @@ -424,6 +463,28 @@ A background agent may be used for the rewrite, but the current thread is respon Event: `{id, _acceptResult, _completionAck}`. The poll script already restored the original, removed all variant markers, and acknowledged `discarded` durable completion. Nothing to do unless `_completionAck.ok !== true`; in that case run `live-complete.mjs --id EVENT_ID --discarded`, then poll again. +## Handle `steer` + +Event: `{id, message, pageUrl}`. The user typed or spoke into the global bar **Steer** control: page-level direction without picking an element or launching variant generation. + +The mic button uses the browser **Web Speech API** (MVP): click to start, speak, stop automatically when the utterance ends, then the transcript submits as a steer event. Click again while listening to cancel without submitting. + +This is lighter than `generate`: no screenshot, no element context, no variant cycling. Read `message` and inspect the live page or project files as needed, then either make edits or answer in prose. + +When finished: + +```bash +node .gemini/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID steer_done ["Optional short note for a browser toast"] +``` + +On failure: + +```bash +node .gemini/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID error "Short reason" +``` + +Then poll again immediately. Do not send a separate "picked up" reply. The Steer bar stays locked until `steer_done` or `error` arrives over SSE. + ## Handle `prefetch` Event: `{pageUrl}`. The browser fires this the first time the user selects an element on a given route, as a latency shortcut; it signals the user is likely about to Go on a page you haven't read yet. diff --git a/.gemini/skills/impeccable/reference/motion-design.md b/.gemini/skills/impeccable/reference/motion-design.md deleted file mode 100644 index 78b6fd260..000000000 --- a/.gemini/skills/impeccable/reference/motion-design.md +++ /dev/null @@ -1,109 +0,0 @@ -# Motion Design - -## Duration: The 100/300/500 Rule - -Timing matters more than easing. These durations feel right for most UI: - -| Duration | Use Case | Examples | -|----------|----------|----------| -| **100-150ms** | Instant feedback | Button press, toggle, color change | -| **200-300ms** | State changes | Menu open, tooltip, hover states | -| **300-500ms** | Layout changes | Accordion, modal, drawer | -| **500-800ms** | Entrance animations | Page load, hero reveals | - -**Exit animations are faster than entrances.** Use ~75% of enter duration. - -## Easing: Pick the Right Curve - -**Don't use `ease`.** It's a compromise that's rarely optimal. Instead: - -| Curve | Use For | CSS | -|-------|---------|-----| -| **ease-out** | Elements entering | `cubic-bezier(0.16, 1, 0.3, 1)` | -| **ease-in** | Elements leaving | `cubic-bezier(0.7, 0, 0.84, 0)` | -| **ease-in-out** | State toggles (there → back) | `cubic-bezier(0.65, 0, 0.35, 1)` | - -**For micro-interactions, use exponential curves.** They feel natural because they mimic real physics (friction, deceleration): - -```css -/* Quart out - smooth, refined (recommended default) */ ---ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1); - -/* Quint out - slightly more dramatic */ ---ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1); - -/* Expo out - snappy, confident */ ---ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); -``` - -**Avoid bounce and elastic curves.** They were trendy in 2015 but now feel tacky and amateurish. Real objects don't bounce when they stop; they decelerate smoothly. Overshoot effects draw attention to the animation itself rather than the content. - -## Premium Motion Materials - -Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties: blur reveals, backdrop-filter panels, saturation or brightness shifts, shadow bloom, SVG filters, masks, clip paths, gradient-position movement, and variable font or shader-driven effects. - -Use the right material for the effect: - -- **Transform / opacity**: movement, press feedback, simple reveals, list choreography. -- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances, atmospheric transitions. -- **Clip path / masks**: wipes, reveals, editorial cropping, product-like transitions. -- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state. -- **Grid-template rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly. - -The hard rule is not "transform and opacity only." The hard rule is: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify in-browser that the result is smooth on the target viewports. If blur/filter makes the interaction feel significantly more premium and remains smooth, use it. - -## Staggered Animations - -Use CSS custom properties for cleaner stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"` on each item. **Cap total stagger time**: 10 items at 50ms = 500ms total. For many items, reduce per-item delay or cap staggered count. - -## Reduced Motion - -This is not optional. Vestibular disorders affect ~35% of adults over 40. - -```css -/* Define animations normally */ -.card { - animation: slide-up 500ms ease-out; -} - -/* Provide alternative for reduced motion */ -@media (prefers-reduced-motion: reduce) { - .card { - animation: fade-in 200ms ease-out; /* Crossfade instead of motion */ - } -} - -/* Or disable entirely */ -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - transition-duration: 0.01ms !important; - } -} -``` - -**What to preserve**: Functional animations like progress bars, loading spinners (slowed down), and focus indicators should still work, just without spatial movement. - -## Perceived Performance - -**Nobody cares how fast your site is, just how fast it feels.** Perception can be as effective as actual performance. - -**The 80ms threshold**: Our brains buffer sensory input for ~80ms to synchronize perception. Anything under 80ms feels instant and simultaneous. This is your target for micro-interactions. - -**Active vs passive time**: Passive waiting (staring at a spinner) feels longer than active engagement. Strategies to shift the balance: - -- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening. -- **Early completion**: Show content progressively, don't wait for everything. Video buffering, progressive images, streaming HTML. -- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Instagram likes work offline; the UI updates instantly, syncs later. Use for low-stakes actions; avoid for payments or destructive operations. - -**Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances, but ease-in toward a task's end compresses perceived time. - -**Caution**: Too-fast responses can decrease perceived value. Users may distrust instant results for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening. - -## Performance - -Don't use `will-change` preemptively, only when animation is imminent (`:hover`, `.animating`). For scroll-triggered animations, use Intersection Observer instead of scroll events; unobserve after animating once. Create motion tokens for consistency (durations, easings, common transitions). - ---- - -**Avoid**: Animating everything (animation fatigue is real). Using >500ms for UI feedback. Ignoring `prefers-reduced-motion`. Using animation to hide slow loading. diff --git a/.gemini/skills/impeccable/reference/personas.md b/.gemini/skills/impeccable/reference/personas.md deleted file mode 100644 index 627f79b1a..000000000 --- a/.gemini/skills/impeccable/reference/personas.md +++ /dev/null @@ -1,179 +0,0 @@ -# Persona-Based Design Testing - -Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss. - -**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns. - ---- - -## 1. Impatient Power User: "Alex" - - -**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave. - -**Behaviors**: -- Skips all onboarding and instructions -- Looks for keyboard shortcuts immediately -- Tries to bulk-select, batch-edit, and automate -- Gets frustrated by required steps that feel unnecessary -- Abandons if anything feels slow or patronizing - -**Test Questions**: -- Can Alex complete the core task in under 60 seconds? -- Are there keyboard shortcuts for common actions? -- Can onboarding be skipped entirely? -- Do modals have keyboard dismiss (Esc)? -- Is there a "power user" path (shortcuts, bulk actions)? - -**Red Flags** (report these specifically): -- Forced tutorials or unskippable onboarding -- No keyboard navigation for primary actions -- Slow animations that can't be skipped -- One-item-at-a-time workflows where batch would be natural -- Redundant confirmation steps for low-risk actions - ---- - -## 2. Confused First-Timer: "Jordan" - -**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out. - -**Behaviors**: -- Reads all instructions carefully -- Hesitates before clicking anything unfamiliar -- Looks for help or support constantly -- Misunderstands jargon and abbreviations -- Takes the most literal interpretation of any label - -**Test Questions**: -- Is the first action obviously clear within 5 seconds? -- Are all icons labeled with text? -- Is there contextual help at decision points? -- Does terminology assume prior knowledge? -- Is there a clear "back" or "undo" at every step? - -**Red Flags** (report these specifically): -- Icon-only navigation with no labels -- Technical jargon without explanation -- No visible help option or guidance -- Ambiguous next steps after completing an action -- No confirmation that an action succeeded - ---- - -## 3. Accessibility-Dependent User: "Sam" - -**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences. - -**Behaviors**: -- Tabs through the interface linearly -- Relies on ARIA labels and heading structure -- Cannot see hover states or visual-only indicators -- Needs adequate color contrast (4.5:1 minimum) -- May use browser zoom up to 200% - -**Test Questions**: -- Can the entire primary flow be completed keyboard-only? -- Are all interactive elements focusable with visible focus indicators? -- Do images have meaningful alt text? -- Is color contrast WCAG AA compliant (4.5:1 for text)? -- Does the screen reader announce state changes (loading, success, errors)? - -**Red Flags** (report these specifically): -- Click-only interactions with no keyboard alternative -- Missing or invisible focus indicators -- Meaning conveyed by color alone (red = error, green = success) -- Unlabeled form fields or buttons -- Time-limited actions without extension option -- Custom components that break screen reader flow - ---- - -## 4. Deliberate Stress Tester: "Riley" - -**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience. - -**Behaviors**: -- Tests edge cases intentionally (empty states, long strings, special characters) -- Submits forms with unexpected data (emoji, RTL text, very long values) -- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs -- Looks for inconsistencies between what the UI promises and what actually happens -- Documents problems methodically - -**Test Questions**: -- What happens at the edges (0 items, 1000 items, very long text)? -- Do error states recover gracefully or leave the UI in a broken state? -- What happens on refresh mid-workflow? Is state preserved? -- Are there features that appear to work but produce broken results? -- How does the UI handle unexpected input (emoji, special chars, paste from Excel)? - -**Red Flags** (report these specifically): -- Features that appear to work but silently fail or produce wrong results -- Error handling that exposes technical details or leaves UI in a broken state -- Empty states that show nothing useful ("No results" with no guidance) -- Workflows that lose user data on refresh or navigation -- Inconsistent behavior between similar interactions in different parts of the UI - ---- - -## 5. Distracted Mobile User: "Casey" - -**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection. - -**Behaviors**: -- Uses thumb only; prefers bottom-of-screen actions -- Gets interrupted mid-flow and returns later -- Switches between apps frequently -- Has limited attention span and low patience -- Types as little as possible, prefers taps and selections - -**Test Questions**: -- Are primary actions in the thumb zone (bottom half of screen)? -- Is state preserved if the user leaves and returns? -- Does it work on slow connections (3G)? -- Can forms use autocomplete and smart defaults? -- Are touch targets at least 44×44pt? - -**Red Flags** (report these specifically): -- Important actions positioned at the top of the screen (unreachable by thumb) -- No state persistence; progress lost on tab switch or interruption -- Large text inputs required where selection would work -- Heavy assets loading on every page (no lazy loading) -- Tiny tap targets or targets too close together - ---- - -## Selecting Personas - -Choose personas based on the interface type: - -| Interface Type | Primary Personas | Why | -|---------------|-----------------|-----| -| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile | -| Dashboard / admin | Alex, Sam | Power users, accessibility | -| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity | -| Onboarding flow | Jordan, Casey | Confusion, interruption | -| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav | -| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile | - ---- - -## Project-Specific Personas - -If `GEMINI.md` contains a `## Design Context` section (generated by `impeccable teach`), derive 1–2 additional personas from the audience and brand information: - -1. Read the target audience description -2. Identify the primary user archetype not covered by the 5 predefined personas -3. Create a persona following this template: - -``` -### [Role]: "[Name]" - -**Profile**: [2-3 key characteristics derived from Design Context] - -**Behaviors**: [3-4 specific behaviors based on the described audience] - -**Red Flags**: [3-4 things that would alienate this specific user type] -``` - -Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists. diff --git a/.gemini/skills/impeccable/reference/polish.md b/.gemini/skills/impeccable/reference/polish.md index 17c93a0d3..6af417659 100644 --- a/.gemini/skills/impeccable/reference/polish.md +++ b/.gemini/skills/impeccable/reference/polish.md @@ -91,7 +91,6 @@ Visual polish on a misshapen flow is wasted work. Match the *shape* of the exper - **Theme consistency**: Works in all theme variants - **Color meaning**: Same colors mean same things throughout - **Accessible focus**: Focus indicators visible with sufficient contrast -- **Tinted neutrals**: No pure gray or pure black; add subtle color tint (0.01 chroma) - **Gray on color**: Never put gray text on colored backgrounds; use a shade of that color or transparency ### Interaction States diff --git a/.gemini/skills/impeccable/reference/product.md b/.gemini/skills/impeccable/reference/product.md index 64b3b0169..2ca462350 100644 --- a/.gemini/skills/impeccable/reference/product.md +++ b/.gemini/skills/impeccable/reference/product.md @@ -10,7 +10,6 @@ Product UI's failure mode isn't flatness, it's strangeness without purpose: over ## Typography -- **System fonts are legitimate.** `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif` gives you native feel on every platform. Inter is the common cross-platform default for a reason. - **One family is often right.** Product UIs don't need display/body pairing. A well-tuned sans carries headings, buttons, labels, body, data. - **Fixed rem scale, not fluid.** Clamp-sized headings don't serve product UI. Users view at consistent DPI, and a fluid h1 that shrinks in a sidebar looks worse, not better. - **Tighter scale ratio.** 1.125–1.2 between steps is typical. More type elements here than on brand surfaces; exaggerated contrast creates noise. @@ -26,8 +25,6 @@ Product defaults to Restrained. A single surface can earn Committed (a dashboard ## Layout -- Predictable grids. Consistency IS an affordance; users navigate faster when the structure is expected. -- Familiar patterns are features. Standard navigation (top bar, side nav), breadcrumbs, tabs, and form layouts have established user expectations. Don't reinvent for flavor. - Responsive behavior is structural (collapse sidebar, responsive table, breakpoint-driven columns), not fluid typography. ## Components @@ -51,6 +48,7 @@ Every interactive component has: default, hover, focus, active, disabled, loadin - Display fonts in UI labels, buttons, data. - Reinventing standard affordances for flavor (custom scrollbars, weird form controls, non-standard modals). - Heavy color or full-saturation accents on inactive states. +- Modal as first thought. Modals are usually laziness. Exhaust inline / progressive alternatives first. ## Product permissions diff --git a/.gemini/skills/impeccable/reference/responsive-design.md b/.gemini/skills/impeccable/reference/responsive-design.md deleted file mode 100644 index f079e5e5d..000000000 --- a/.gemini/skills/impeccable/reference/responsive-design.md +++ /dev/null @@ -1,114 +0,0 @@ -# Responsive Design - -## Mobile-First: Write It Right - -Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first. - -## Breakpoints: Content-Driven - -Don't chase device sizes; let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints. - -## Detect Input Method, Not Just Screen Size - -**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard. Use pointer and hover queries: - -```css -/* Fine pointer (mouse, trackpad) */ -@media (pointer: fine) { - .button { padding: 8px 16px; } -} - -/* Coarse pointer (touch, stylus) */ -@media (pointer: coarse) { - .button { padding: 12px 20px; } /* Larger touch target */ -} - -/* Device supports hover */ -@media (hover: hover) { - .card:hover { transform: translateY(-2px); } -} - -/* Device doesn't support hover (touch) */ -@media (hover: none) { - .card { /* No hover state - use active instead */ } -} -``` - -**Critical**: Don't rely on hover for functionality. Touch users can't hover. - -## Safe Areas: Handle the Notch - -Modern phones have notches, rounded corners, and home indicators. Use `env()`: - -```css -body { - padding-top: env(safe-area-inset-top); - padding-bottom: env(safe-area-inset-bottom); - padding-left: env(safe-area-inset-left); - padding-right: env(safe-area-inset-right); -} - -/* With fallback */ -.footer { - padding-bottom: max(1rem, env(safe-area-inset-bottom)); -} -``` - -**Enable viewport-fit** in your meta tag: -```html - -``` - -## Responsive Images: Get It Right - -### srcset with Width Descriptors - -```html -Hero image -``` - -**How it works**: -- `srcset` lists available images with their actual widths (`w` descriptors) -- `sizes` tells the browser how wide the image will display -- Browser picks the best file based on viewport width AND device pixel ratio - -### Picture Element for Art Direction - -When you need different crops/compositions (not just resolutions): - -```html - - - - ... - -``` - -## Layout Adaptation Patterns - -**Navigation**: Three stages: hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `
/` for content that can collapse on mobile. - -## Testing: Don't Trust DevTools Alone - -DevTools device emulation is useful for layout but misses: - -- Actual touch interactions -- Real CPU/memory constraints -- Network latency patterns -- Font rendering differences -- Browser chrome/keyboard appearances - -**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators. - ---- - -**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful. diff --git a/.gemini/skills/impeccable/reference/shape.md b/.gemini/skills/impeccable/reference/shape.md index f7c4cd2f1..592def8a7 100644 --- a/.gemini/skills/impeccable/reference/shape.md +++ b/.gemini/skills/impeccable/reference/shape.md @@ -151,7 +151,7 @@ How users interact with this feature. What happens on click, hover, scroll? What 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., spatial-design.md for complex layouts, motion-design.md for animated features, interaction-design.md for form-heavy features). +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. diff --git a/.gemini/skills/impeccable/reference/spatial-design.md b/.gemini/skills/impeccable/reference/spatial-design.md deleted file mode 100644 index 4d4b83afc..000000000 --- a/.gemini/skills/impeccable/reference/spatial-design.md +++ /dev/null @@ -1,100 +0,0 @@ -# Spatial Design - -## Spacing Systems - -### Use 4pt Base, Not 8pt - -8pt systems are too coarse; you'll frequently need 12px (between 8 and 16). Use 4pt for granularity: 4, 8, 12, 16, 24, 32, 48, 64, 96px. - -### Name Tokens Semantically - -Name by relationship (`--space-sm`, `--space-lg`), not value (`--spacing-8`). Use `gap` instead of margins for sibling spacing; it eliminates margin collapse and cleanup hacks. - -## Grid Systems - -### The Self-Adjusting Grid - -Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. Columns are at least 280px, as many as fit per row, leftovers stretch. For complex layouts, use named grid areas (`grid-template-areas`) and redefine them at breakpoints. - -## Visual Hierarchy - -### The Squint Test - -Blur your eyes (or screenshot and blur). Can you still identify: -- The most important element? -- The second most important? -- Clear groupings? - -If everything looks the same weight blurred, you have a hierarchy problem. - -### Hierarchy Through Multiple Dimensions - -Don't rely on size alone. Combine: - -| Tool | Strong Hierarchy | Weak Hierarchy | -|------|------------------|----------------| -| **Size** | 3:1 ratio or more | <2:1 ratio | -| **Weight** | Bold vs Regular | Medium vs Regular | -| **Color** | High contrast | Similar tones | -| **Position** | Top/left (primary) | Bottom/right | -| **Space** | Surrounded by white space | Crowded | - -**The best hierarchy uses 2-3 dimensions at once**: A heading that's larger, bolder, AND has more space above it. - -### Cards Are Not Required - -Cards are overused. Spacing and alignment create visual grouping naturally. Use cards only when content is truly distinct and actionable, items need visual comparison in a grid, or content needs clear interaction boundaries. **Never nest cards inside cards.** Use spacing, typography, and subtle dividers for hierarchy within a card. - -## Container Queries - -Viewport queries are for page layouts. **Container queries are for components**: - -```css -.card-container { - container-type: inline-size; -} - -.card { - display: grid; - gap: var(--space-md); -} - -/* Card layout changes based on its container, not viewport */ -@container (min-width: 400px) { - .card { - grid-template-columns: 120px 1fr; - } -} -``` - -**Why this matters**: A card in a narrow sidebar stays compact, while the same card in a main content area expands automatically, without viewport hacks. - -## Optical Adjustments - -Text at `margin-left: 0` looks indented due to letterform whitespace; use negative margin (`-0.05em`) to optically align. Geometrically centered icons often look off-center; play icons need to shift right, arrows shift toward their direction. - -### Touch Targets vs Visual Size - -Buttons can look small but need large touch targets (44px minimum). Use padding or pseudo-elements: - -```css -.icon-button { - width: 24px; /* Visual size */ - height: 24px; - position: relative; -} - -.icon-button::before { - content: ''; - position: absolute; - inset: -10px; /* Expand tap target to 44px */ -} -``` - -## Depth & Elevation - -Create semantic z-index scales (dropdown → sticky → modal-backdrop → modal → toast → tooltip) instead of arbitrary numbers. For shadows, create a consistent elevation scale (sm → md → lg → xl). **Key insight**: Shadows should be subtle. If you can clearly see it, it's probably too strong. - ---- - -**Avoid**: Arbitrary spacing values outside your scale. Making all spacing equal (variety creates hierarchy). Creating hierarchy through size alone - combine size, weight, color, and space. diff --git a/.gemini/skills/impeccable/reference/typeset.md b/.gemini/skills/impeccable/reference/typeset.md index dd3e67ea3..5f5e0c2c0 100644 --- a/.gemini/skills/impeccable/reference/typeset.md +++ b/.gemini/skills/impeccable/reference/typeset.md @@ -4,7 +4,7 @@ Typography carries most of the information on the page. Replace generic defaults ## Register -Brand: run the font selection procedure in [brand.md](brand.md). Pairing follows the brand's lane (display serif + sans body for editorial/luxury, one committed sans for tech, etc.). Fluid `clamp()` scale, ≥1.25 ratio between steps. +Brand: run the font selection procedure in [brand.md](brand.md). Fluid `clamp()` scale, ≥1.25 ratio between steps. Product: system fonts and familiar sans stacks are legitimate here. One well-tuned family typically carries the whole UI. Fixed `rem` scale, 1.125–1.2 ratio between more closely-spaced steps. @@ -43,7 +43,7 @@ Analyze what's weak or generic about the current type: ## Plan Typography Improvements -Consult the [typography reference](typography.md) for detailed guidance on scales, pairing, and loading strategies. +Consult the [Reference Material](#reference-material) section below for detailed guidance on scales, pairing, and loading strategies. Create a systematic plan: @@ -122,3 +122,158 @@ Each variant MUST declare a `scale` param controlling the hierarchy ratio. Expre Where the variant riffs on a specific pairing, expose the pairing choice as a `steps` param (e.g. "serif display + sans body" vs. "mono display + sans body" vs. "all-sans"). Each branch routes through `:scope[data-p-pairing="X"]` selectors in scoped CSS. See `reference/live.md` for the full params contract. + +--- + +## Reference Material + +The sections below were previously `typography.md` and live inline now so the typeset flow has its deep typography reference in one place. `bolder.md` also references this section. + +### Typography + +#### Classic Typography Principles + +##### Vertical Rhythm + +Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony; text and space share a mathematical foundation. + +##### Modular Scale & Hierarchy + +The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy. + +**Use fewer sizes with more contrast.** A 5-size system covers most needs: + +| Role | Typical Ratio | Use Case | +|------|---------------|----------| +| xs | 0.75rem | Captions, legal | +| sm | 0.875rem | Secondary UI, metadata | +| base | 1rem | Body text | +| lg | 1.25-1.5rem | Subheadings, lead text | +| xl+ | 2-4rem | Headlines, hero text | + +Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit. + +##### Readability & Measure + +Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length: narrow columns need tighter leading, wide columns need more. + +**Non-obvious**: Light text on dark backgrounds needs compensation on three axes, not just one. Bump line-height by 0.05–0.1, add a touch of letter-spacing (0.01–0.02em), and optionally step the body weight up one notch (regular → medium). The perceived weight drops across all three; fix all three. + +**Paragraph rhythm**: Pick either space between paragraphs OR first-line indentation. Never both. Digital usually wants space; editorial/long-form can justify indent-only. + +#### Font Selection & Pairing + +The tactical selection procedure and the reflex-reject list live in [reference/brand.md](brand.md) under **Font selection procedure** and **Reflex-reject list** (loaded for brand-register tasks). The rest of this section covers the adjacent knowledge: anti-reflex corrections, system font use, and pairing rules. + +##### Anti-reflexes worth defending against + +- A technical/utilitarian brief does NOT need a serif "for warmth." Most tech tools should look like tech tools. +- An editorial/premium brief does NOT need the same expressive serif everyone is using right now. Premium can be Swiss-modern, can be neo-grotesque, can be a literal monospace, can be a quiet humanist sans. +- A children's product does NOT need a rounded display font. Kids' books use real type. +- A "modern" brief does NOT need a geometric sans. The most modern thing you can do is not use the font everyone else is using. + +**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality. + +##### Pairing Principles + +**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif). + +When pairing, contrast on multiple axes: +- Serif + Sans (structure contrast) +- Geometric + Humanist (personality contrast) +- Condensed display + Wide body (proportion contrast) + +##### Web Font Loading + +The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix: + +```css +/* 1. Use font-display: swap for visibility */ +@font-face { + font-family: 'CustomFont'; + src: url('font.woff2') format('woff2'); + font-display: swap; +} + +/* 2. Match fallback metrics to minimize shift */ +@font-face { + font-family: 'CustomFont-Fallback'; + src: local('Arial'); + size-adjust: 105%; /* Scale to match x-height */ + ascent-override: 90%; /* Match ascender height */ + descent-override: 20%; /* Match descender depth */ + line-gap-override: 10%; /* Match line spacing */ +} + +body { + font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; +} +``` + +Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically. + +**`swap` vs `optional`**: `swap` shows fallback text immediately and FOUT-swaps when the web font arrives. `optional` uses the fallback if the web font misses a small load budget (~100ms) and avoids the shift entirely. Pick `optional` when zero layout shift matters more than seeing the branded font on slow networks. + +**Preload the critical weight only**: typically the regular-weight body font used above the fold. Preloading every weight costs more bandwidth than it saves. + +**Variable fonts for 3+ weights or styles**: a single variable font file is usually smaller than three static weight files, gives fractional weight control, and pairs well with `font-optical-sizing: auto`. For 1–2 weights, static is fine. + +#### Modern Web Typography + +##### Fluid Type + +Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate (higher vw = faster scaling). Add a rem offset so it doesn't collapse to 0 on small screens. + +**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes. + +**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI; fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it. + +**Bound your clamp()**: keep `max-size ≤ ~2.5 × min-size`. Wider ratios break the browser's zoom and reflow behaviour and make large viewports feel like the page is shouting. + +**Scale container width and font-size together** so effective character measure stays in the 45–75ch band at every viewport. A heading that widens faster than its container drifts out of the comfortable measure at the top end. + +##### OpenType Features + +Most developers don't know these exist. Use them for polish: + +```css +/* Proper fractions */ +.recipe-amount { font-variant-numeric: diagonal-fractions; } + +/* Small caps for abbreviations */ +abbr { font-variant-caps: all-small-caps; } + +/* Disable ligatures in code */ +code { font-variant-ligatures: none; } + +/* Enable kerning (usually on by default, but be explicit) */ +body { font-kerning: normal; } +``` + +Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/). + +##### Rendering polish + +```css +/* Variable fonts: pick the right optical-size master automatically */ +body { font-optical-sizing: auto; } +``` + +**ALL-CAPS tracking**: capitals sit too close at default spacing. Add 5–12% letter-spacing (`letter-spacing: 0.05em` to `0.12em`) to short all-caps labels, eyebrows, and small headings. Real small caps (via `font-variant-caps`) need the same treatment, slightly gentler. + +#### Typography System Architecture + +Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system. + +#### Accessibility Considerations + +Beyond contrast ratios (which are well-documented), consider: + +- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout. +- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text. +- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile. +- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets. + +--- + +**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text. diff --git a/.gemini/skills/impeccable/reference/typography.md b/.gemini/skills/impeccable/reference/typography.md deleted file mode 100644 index ed5ca272a..000000000 --- a/.gemini/skills/impeccable/reference/typography.md +++ /dev/null @@ -1,159 +0,0 @@ -# Typography - -## Classic Typography Principles - -### Vertical Rhythm - -Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony; text and space share a mathematical foundation. - -### Modular Scale & Hierarchy - -The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy. - -**Use fewer sizes with more contrast.** A 5-size system covers most needs: - -| Role | Typical Ratio | Use Case | -|------|---------------|----------| -| xs | 0.75rem | Captions, legal | -| sm | 0.875rem | Secondary UI, metadata | -| base | 1rem | Body text | -| lg | 1.25-1.5rem | Subheadings, lead text | -| xl+ | 2-4rem | Headlines, hero text | - -Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit. - -### Readability & Measure - -Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length: narrow columns need tighter leading, wide columns need more. - -**Non-obvious**: Light text on dark backgrounds needs compensation on three axes, not just one. Bump line-height by 0.05–0.1, add a touch of letter-spacing (0.01–0.02em), and optionally step the body weight up one notch (regular → medium). The perceived weight drops across all three; fix all three. - -**Paragraph rhythm**: Pick either space between paragraphs OR first-line indentation. Never both. Digital usually wants space; editorial/long-form can justify indent-only. - -## Font Selection & Pairing - -The tactical selection procedure and the reflex-reject list live in [reference/brand.md](brand.md) under **Font selection procedure** and **Reflex-reject list** (loaded for brand-register tasks). The rest of this section covers the adjacent knowledge: anti-reflex corrections, system font use, and pairing rules. - -### Anti-reflexes worth defending against - -- A technical/utilitarian brief does NOT need a serif "for warmth." Most tech tools should look like tech tools. -- An editorial/premium brief does NOT need the same expressive serif everyone is using right now. Premium can be Swiss-modern, can be neo-grotesque, can be a literal monospace, can be a quiet humanist sans. -- A children's product does NOT need a rounded display font. Kids' books use real type. -- A "modern" brief does NOT need a geometric sans. The most modern thing you can do is not use the font everyone else is using. - -**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality. - -### Pairing Principles - -**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif). - -When pairing, contrast on multiple axes: -- Serif + Sans (structure contrast) -- Geometric + Humanist (personality contrast) -- Condensed display + Wide body (proportion contrast) - -**Never pair fonts that are similar but not identical** (e.g., two geometric sans-serifs). They create visual tension without clear hierarchy. - -### Web Font Loading - -The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix: - -```css -/* 1. Use font-display: swap for visibility */ -@font-face { - font-family: 'CustomFont'; - src: url('font.woff2') format('woff2'); - font-display: swap; -} - -/* 2. Match fallback metrics to minimize shift */ -@font-face { - font-family: 'CustomFont-Fallback'; - src: local('Arial'); - size-adjust: 105%; /* Scale to match x-height */ - ascent-override: 90%; /* Match ascender height */ - descent-override: 20%; /* Match descender depth */ - line-gap-override: 10%; /* Match line spacing */ -} - -body { - font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; -} -``` - -Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically. - -**`swap` vs `optional`**: `swap` shows fallback text immediately and FOUT-swaps when the web font arrives. `optional` uses the fallback if the web font misses a small load budget (~100ms) and avoids the shift entirely. Pick `optional` when zero layout shift matters more than seeing the branded font on slow networks. - -**Preload the critical weight only**: typically the regular-weight body font used above the fold. Preloading every weight costs more bandwidth than it saves. - -**Variable fonts for 3+ weights or styles**: a single variable font file is usually smaller than three static weight files, gives fractional weight control, and pairs well with `font-optical-sizing: auto`. For 1–2 weights, static is fine. - -## Modern Web Typography - -### Fluid Type - -Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate (higher vw = faster scaling). Add a rem offset so it doesn't collapse to 0 on small screens. - -**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes. - -**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI; fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it. - -**Bound your clamp()**: keep `max-size ≤ ~2.5 × min-size`. Wider ratios break the browser's zoom and reflow behaviour and make large viewports feel like the page is shouting. - -**Scale container width and font-size together** so effective character measure stays in the 45–75ch band at every viewport. A heading that widens faster than its container drifts out of the comfortable measure at the top end. - -### OpenType Features - -Most developers don't know these exist. Use them for polish: - -```css -/* Tabular numbers for data alignment */ -.data-table { font-variant-numeric: tabular-nums; } - -/* Proper fractions */ -.recipe-amount { font-variant-numeric: diagonal-fractions; } - -/* Small caps for abbreviations */ -abbr { font-variant-caps: all-small-caps; } - -/* Disable ligatures in code */ -code { font-variant-ligatures: none; } - -/* Enable kerning (usually on by default, but be explicit) */ -body { font-kerning: normal; } -``` - -Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/). - -### Rendering polish - -```css -/* Even out heading line lengths (browser picks better break points) */ -h1, h2, h3 { text-wrap: balance; } - -/* Reduce orphans and ragged endings in long prose */ -article p { text-wrap: pretty; } - -/* Variable fonts: pick the right optical-size master automatically */ -body { font-optical-sizing: auto; } -``` - -**ALL-CAPS tracking**: capitals sit too close at default spacing. Add 5–12% letter-spacing (`letter-spacing: 0.05em` to `0.12em`) to short all-caps labels, eyebrows, and small headings. Real small caps (via `font-variant-caps`) need the same treatment, slightly gentler. - -## Typography System Architecture - -Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system. - -## Accessibility Considerations - -Beyond contrast ratios (which are well-documented), consider: - -- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout. -- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text. -- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile. -- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets. - ---- - -**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text. diff --git a/.gemini/skills/impeccable/reference/ux-writing.md b/.gemini/skills/impeccable/reference/ux-writing.md deleted file mode 100644 index 417b5f56c..000000000 --- a/.gemini/skills/impeccable/reference/ux-writing.md +++ /dev/null @@ -1,107 +0,0 @@ -# UX Writing - -## The Button Label Problem - -**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns: - -| Bad | Good | Why | -|-----|------|-----| -| OK | Save changes | Says what will happen | -| Submit | Create account | Outcome-focused | -| Yes | Delete message | Confirms the action | -| Cancel | Keep editing | Clarifies what "cancel" means | -| Click here | Download PDF | Describes the destination | - -**For destructive actions**, name the destruction: -- "Delete" not "Remove" (delete is permanent, remove implies recoverable) -- "Delete 5 items" not "Delete selected" (show the count) - -## Error Messages: The Formula - -Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input". - -### Error Message Templates - -| Situation | Template | -|-----------|----------| -| **Format error** | "[Field] needs to be [format]. Example: [example]" | -| **Missing required** | "Please enter [what's missing]" | -| **Permission denied** | "You don't have access to [thing]. [What to do instead]" | -| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." | -| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" | - -### Don't Blame the User - -Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date". - -## Empty States Are Opportunities - -Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items". - -## Voice vs Tone - -**Voice** is your brand's personality, consistent everywhere. -**Tone** adapts to the moment. - -| Moment | Tone Shift | -|--------|------------| -| Success | Celebratory, brief: "Done! Your changes are live." | -| Error | Empathetic, helpful: "That didn't work. Here's what to try..." | -| Loading | Reassuring: "Saving your work..." | -| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | - -**Never use humor for errors.** Users are already frustrated. Be helpful, not cute. - -## Writing for Accessibility - -**Link text** must have standalone meaning: "View pricing plans" not "Click here". **Alt text** describes information, not the image: "Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context. - -## Writing for Translation - -### Plan for Expansion - -German text is ~30% longer than English. Allocate space: - -| Language | Expansion | -|----------|-----------| -| German | +30% | -| French | +20% | -| Finnish | +30-40% | -| Chinese | -30% (fewer chars, but same width) | - -### Translation-Friendly Patterns - -Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear. - -## Consistency: The Terminology Problem - -Pick one term and stick with it: - -| Inconsistent | Consistent | -|--------------|------------| -| Delete / Remove / Trash | Delete | -| Settings / Preferences / Options | Settings | -| Sign in / Log in / Enter | Sign in | -| Create / Add / New | Create | - -Build a terminology glossary and enforce it. Variety creates confusion. - -## Avoid Redundant Copy - -If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well. - -## Loading States - -Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress. - -## Confirmation Dialogs: Use Sparingly - -Most confirmation dialogs are design failures; consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No"). - -## Form Instructions - -Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking. - ---- - -**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors. diff --git a/.gemini/skills/impeccable/scripts/cleanup-deprecated.mjs b/.gemini/skills/impeccable/scripts/cleanup-deprecated.mjs index 6aee4710a..bc6400e90 100644 --- a/.gemini/skills/impeccable/scripts/cleanup-deprecated.mjs +++ b/.gemini/skills/impeccable/scripts/cleanup-deprecated.mjs @@ -25,7 +25,7 @@ import { join, resolve } from 'node:path'; const DEPRECATED_NAMES = [ // v2.0 renames 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable teach + 'teach-impeccable', // folded into /impeccable init // v2.1 merges 'arrange', // renamed to layout 'normalize', // merged into polish diff --git a/.gemini/skills/impeccable/scripts/command-metadata.json b/.gemini/skills/impeccable/scripts/command-metadata.json index 1488bd5f1..83796d54b 100644 --- a/.gemini/skills/impeccable/scripts/command-metadata.json +++ b/.gemini/skills/impeccable/scripts/command-metadata.json @@ -3,8 +3,8 @@ "description": "Full confirmed-brief-then-build flow. Runs multi-round shape discovery first, resolves visual probe and north-star mock gates when available, then builds and visually iterates. Use when building a new feature end-to-end.", "argumentHint": "[feature description]" }, - "teach": { - "description": "Gathers design context for a project. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.", + "init": { + "description": "Sets up a project for impeccable. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles); offers DESIGN.md (visual: colors, typography, components) when code exists; pre-configures live mode; then recommends the best commands to run next. Every other command reads these files before doing work. Use once per project.", "argumentHint": "" }, "document": { diff --git a/.gemini/skills/impeccable/scripts/context.mjs b/.gemini/skills/impeccable/scripts/context.mjs new file mode 100644 index 000000000..52263c883 --- /dev/null +++ b/.gemini/skills/impeccable/scripts/context.mjs @@ -0,0 +1,253 @@ +/** + * Context loader: prints PRODUCT.md (and DESIGN.md if present) as one + * markdown block on stdout, or exits with empty stdout when no PRODUCT.md + * is found anywhere. The skill keys off "empty stdout" to branch into the + * init flow. + * + * Path resolution (first match wins): + * 1. cwd, if PRODUCT.md or DESIGN.md is there + * 2. .agents/context/ then docs/ + * 3. $IMPECCABLE_CONTEXT_DIR (absolute or cwd-relative) — power-user + * escape hatch, only consulted when defaults are empty + * 4. cwd as a "nothing found" default + * + * `resolveContextDir()` and `loadContext()` are also exported for the + * server-side scripts (live.mjs, live-server.mjs) that need the structured + * shape rather than the markdown block. + */ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; +const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; +const FALLBACK_DIRS = ['.agents/context', 'docs']; + +// ─── Update check ────────────────────────────────────────────────────────── +// Piggyback a lightweight skill-version check on the once-per-session boot. +// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent +// can offer `npx impeccable skills update`. Everything here is best-effort and +// silent on failure: a network problem, sandbox, or missing cache must never +// block context output or print an error. + +const UPDATE_HOST = (process.env.IMPECCABLE_UPDATE_HOST || 'https://impeccable.style').replace(/\/$/, ''); +const UPDATE_CACHE_PATH = + process.env.IMPECCABLE_UPDATE_CACHE || path.join(os.homedir(), '.impeccable', 'update-check.json'); +const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // throttle the network poll to once a day +const RENOTIFY_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000; // don't re-surface the same version for a week +const FETCH_TIMEOUT_MS = 1200; + +export function resolveContextDir(cwd = process.cwd()) { + if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { + return cwd; + } + for (const rel of FALLBACK_DIRS) { + const candidate = path.resolve(cwd, rel); + if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { + return candidate; + } + } + const envDir = process.env.IMPECCABLE_CONTEXT_DIR; + if (envDir && envDir.trim()) { + const trimmed = envDir.trim(); + return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); + } + return cwd; +} + +export function loadContext(cwd = process.cwd()) { + const contextDir = resolveContextDir(cwd); + const productPath = firstExisting(contextDir, PRODUCT_NAMES); + const designPath = firstExisting(contextDir, DESIGN_NAMES); + const product = productPath ? safeRead(productPath) : null; + const design = designPath ? safeRead(designPath) : null; + return { + hasProduct: !!product, + product, + productPath: productPath ? path.relative(cwd, productPath) : null, + hasDesign: !!design, + design, + designPath: designPath ? path.relative(cwd, designPath) : null, + contextDir, + }; +} + +function firstExisting(dir, names) { + for (const name of names) { + const abs = path.join(dir, name); + if (fs.existsSync(abs)) return abs; + } + return null; +} + +function safeRead(p) { + try { + return fs.readFileSync(p, 'utf-8'); + } catch { + return null; + } +} + +/** + * Pull the register (`brand` or `product`) out of PRODUCT.md by looking + * for a `## Register` section and reading the first non-empty line that + * follows it. Returns null when the file is legacy / register-less. + */ +function extractRegister(product) { + if (!product) return null; + const lines = product.split('\n'); + for (let i = 0; i < lines.length; i++) { + if (/^##\s+Register\b/i.test(lines[i].trim())) { + for (let j = i + 1; j < lines.length; j++) { + const next = lines[j].trim(); + if (!next) continue; + const word = next.toLowerCase(); + if (word === 'brand' || word === 'product') return word; + return null; + } + } + } + return null; +} + +/** + * Read the installed skill's own version from the sibling SKILL.md frontmatter + * (this file lives at `/scripts/context.mjs`). Returns null when the + * frontmatter is missing or unreadable. + */ +function readLocalSkillVersion() { + try { + const here = path.dirname(fileURLToPath(import.meta.url)); + const skillMd = path.join(here, '..', 'SKILL.md'); + const content = fs.readFileSync(skillMd, 'utf-8'); + const match = content.match(/^version:\s*(.+)$/m); + return match ? match[1].trim().replace(/^["']|["']$/g, '') : null; + } catch { + return null; + } +} + +function readUpdateCache() { + try { + return JSON.parse(fs.readFileSync(UPDATE_CACHE_PATH, 'utf-8')); + } catch { + return {}; + } +} + +function writeUpdateCache(cache) { + try { + fs.mkdirSync(path.dirname(UPDATE_CACHE_PATH), { recursive: true }); + fs.writeFileSync(UPDATE_CACHE_PATH, JSON.stringify(cache)); + } catch { + // Best-effort: a read-only home dir just means we re-poll next session. + } +} + +/** Compare dotted numeric versions. Returns >0 when a is newer than b. */ +function compareSemver(a, b) { + const pa = String(a).split('.').map(n => parseInt(n, 10) || 0); + const pb = String(b).split('.').map(n => parseInt(n, 10) || 0); + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + const diff = (pa[i] || 0) - (pb[i] || 0); + if (diff !== 0) return diff; + } + return 0; +} + +async function fetchLatestSkillVersion() { + try { + const res = await fetch(`${UPDATE_HOST}/api/version`, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }); + if (!res.ok) return null; + const data = await res.json(); + return typeof data?.skills === 'string' ? data.skills : null; + } catch { + return null; // offline, sandboxed, timed out, or bad JSON: all non-fatal + } +} + +function buildUpdateDirective(localVersion, latestVersion) { + return ( + `UPDATE_AVAILABLE: A newer Impeccable skill is available ` + + `(installed v${localVersion}, latest v${latestVersion}). ` + + `Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` + + `Update now? It runs \`npx impeccable skills update\`." ` + + `If they agree, run \`npx impeccable skills update\` (the update applies to the next session, not this one). ` + + `Either way, continue the current task without waiting, and do not raise this again.` + ); +} + +/** + * Best-effort update directive for the boot output. Returns a string to append + * or null. Polls the version endpoint at most once per day (cached globally in + * the user's home dir) and re-surfaces a given version at most once per week so + * the agent never nags. Opt out entirely with IMPECCABLE_NO_UPDATE_CHECK=1. + */ +async function computeUpdateDirective(now = Date.now()) { + try { + if (process.env.IMPECCABLE_NO_UPDATE_CHECK) return null; + const localVersion = readLocalSkillVersion(); + if (!localVersion) return null; + + const cache = readUpdateCache(); + + // Poll the network only when the throttle window has elapsed. Stamp + // lastCheck even on failure so an offline machine doesn't poll every boot. + if (!cache.lastCheck || now - cache.lastCheck > CHECK_INTERVAL_MS) { + const latest = await fetchLatestSkillVersion(); + cache.lastCheck = now; + if (latest) cache.latestVersion = latest; + writeUpdateCache(cache); + } + + const latest = cache.latestVersion; + if (!latest || compareSemver(latest, localVersion) <= 0) return null; + + // Anti-nag: surface a given version at most once per RENOTIFY window. + if (cache.notifiedVersion === latest && cache.notifiedAt && now - cache.notifiedAt < RENOTIFY_INTERVAL_MS) { + return null; + } + cache.notifiedVersion = latest; + cache.notifiedAt = now; + writeUpdateCache(cache); + + return buildUpdateDirective(localVersion, latest); + } catch { + return null; + } +} + +async function cli() { + const ctx = loadContext(process.cwd()); + const updateDirective = await computeUpdateDirective(); + + if (!ctx.hasProduct) { + // Direct stdout message instead of relying on empty output as a signal + // — cheap models miss the empty case more often than the explicit one. + const parts = [ + 'NO_PRODUCT_MD: This project has no PRODUCT.md yet. ' + + 'Stop the current task, load reference/init.md, and follow its ' + + 'instructions to write PRODUCT.md before resuming.', + ]; + if (updateDirective) parts.push(updateDirective); + process.stdout.write(parts.join('\n\n---\n\n') + '\n'); + process.exit(0); + } + const parts = [`# PRODUCT.md\n\n${ctx.product.trim()}`]; + if (ctx.hasDesign) { + parts.push(`# DESIGN.md\n\n${ctx.design.trim()}`); + } + const register = extractRegister(ctx.product); + const next = register + ? `NEXT STEP: This project's register is \`${register}\`. You MUST now read \`reference/${register}.md\` before producing any design output.` + : `NEXT STEP: You MUST now read the matching register reference (\`reference/brand.md\` or \`reference/product.md\`) before producing any design output. Pick based on PRODUCT.md above.`; + parts.push(next); + if (updateDirective) parts.push(updateDirective); + process.stdout.write(parts.join('\n\n---\n\n') + '\n'); +} + +const _running = process.argv[1]; +if (_running?.endsWith('context.mjs') || _running?.endsWith('context.mjs/')) { + cli(); +} diff --git a/.gemini/skills/impeccable/scripts/design-parser.mjs b/.gemini/skills/impeccable/scripts/design-parser.mjs index b68756640..82a5cd7dc 100644 --- a/.gemini/skills/impeccable/scripts/design-parser.mjs +++ b/.gemini/skills/impeccable/scripts/design-parser.mjs @@ -63,7 +63,7 @@ function parseYamlSubset(yaml) { } const key = content.slice(0, colonIdx).trim(); - const rest = content.slice(colonIdx + 1).trim(); + const rest = stripInlineYamlComment(content.slice(colonIdx + 1).trim()); const parent = stack[stack.length - 1].obj; if (rest === '') { @@ -93,6 +93,21 @@ function findTopLevelColon(s) { return -1; } +function stripInlineYamlComment(s) { + let inQuote = null; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (inQuote) { + if (ch === inQuote && s[i - 1] !== '\\') inQuote = null; + } else if (ch === '"' || ch === "'") { + inQuote = ch; + } else if (ch === '#' && i > 0 && /\s/.test(s[i - 1])) { + return s.slice(0, i).trimEnd(); + } + } + return s; +} + function parseScalar(raw) { const s = raw.trim(); if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { diff --git a/.gemini/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.gemini/skills/impeccable/scripts/detector/browser/injected/index.mjs index c03a69caf..ceff16737 100644 --- a/.gemini/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/.gemini/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -9,8 +9,21 @@ if (IS_BROWSER) { const EXTENSION_MODE = (_myScript && _myScript.dataset.impeccableExtension === 'true') || document.documentElement.dataset.impeccableExtension === 'true'; - const BRAND_COLOR = 'oklch(55% 0.25 350)'; - const BRAND_COLOR_HOVER = 'oklch(45% 0.25 350)'; + // Kinpaku gold — pinned to the site's brand token (see + // site/styles/kinpaku-tokens.css --ks-kinpaku). Keep this in sync with + // the picker's C.brand in skill/scripts/live-browser.js and the kit's + // picker section in site/styles/kinpaku-kit.css. + // + // One color across both light and dark host pages. The outline is a + // 2px gesture pointing at an element + a labeled tag — it's a marker, + // not body text, so it doesn't need WCAG AA against the page. The + // label text inside the gold tag is dark (LABEL_INK) which has ~16:1 + // against the leaf gold, so reading the rule name is solid in both + // modes. Hover deepens the gold (preserves chroma — never drops it, + // dropping chroma washes the gold into a sand/olive tone). + const BRAND_COLOR = 'oklch(84% 0.19 80.46)'; + const BRAND_COLOR_HOVER = 'oklch(74% 0.18 80)'; + const LABEL_INK = 'oklch(4% 0.004 95)'; const LABEL_BG = BRAND_COLOR; const OUTLINE_COLOR = BRAND_COLOR; @@ -278,7 +291,7 @@ if (IS_BROWSER) { display: 'flex', alignItems: 'center', whiteSpace: 'nowrap', fontSize: '11px', fontWeight: '600', letterSpacing: '0.02em', - color: 'white', lineHeight: '14px', + color: LABEL_INK, lineHeight: '14px', background: LABEL_BG, fontFamily: 'system-ui, sans-serif', borderRadius: '4px 4px 0 0', @@ -398,7 +411,7 @@ if (IS_BROWSER) { banner.className = 'impeccable-overlay impeccable-banner'; Object.assign(banner.style, { position: 'fixed', top: '0', left: '0', right: '0', zIndex: '100000', - background: LABEL_BG, color: 'white', + background: LABEL_BG, color: LABEL_INK, fontFamily: 'system-ui, sans-serif', fontSize: '13px', display: 'flex', alignItems: 'center', pointerEvents: 'auto', height: '36px', overflow: 'hidden', maxWidth: '100vw', @@ -1223,12 +1236,12 @@ if (IS_BROWSER) { } console.group( `%c[impeccable] ${allFindings.length} anti-pattern${allFindings.length === 1 ? '' : 's'} found`, - 'color: oklch(60% 0.25 350); font-weight: bold' + 'color: oklch(84% 0.19 80.46); font-weight: bold' ); for (const { el, findings } of allFindings) { for (const f of findings) { console.log(`%c${f.type || f.id}%c ${f.detail || f.snippet}`, - 'color: oklch(55% 0.25 350); font-weight: bold', 'color: inherit', el); + 'color: oklch(84% 0.19 80.46); font-weight: bold', 'color: inherit', el); } } console.groupEnd(); @@ -1249,6 +1262,10 @@ if (IS_BROWSER) { const groupMap = new Map(); const _disabled = EXTENSION_MODE ? (window.__IMPECCABLE_CONFIG__?.disabledRules || []) : []; const _ruleOk = (id) => !_disabled.length || !_disabled.includes(id); + // Note: provider-gated rules (--gpt / --gemini) are NOT filtered here. In a + // real browser env (detector page, live overlay, extension) running every + // check is free, so we always surface them; the gating is purely a CLI + // output concern, applied in the Node engines' detect* return paths. for (const el of document.querySelectorAll('*')) { // Skip impeccable's own elements and any descendants (overlays, labels, banner, nav buttons) @@ -1270,11 +1287,23 @@ if (IS_BROWSER) { ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), - ...checkElementHeroEyebrowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementOversizedH1DOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementClippedOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementGptBorderShadowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementTextOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ].filter(f => _ruleOk(f.type)); addBrowserFindings(groupMap, el, findings); + + // Hero eyebrow: the offending element is the eyebrow above the heading, + // not the heading itself — highlight the previous sibling instead. + const eyebrowFindings = checkElementHeroEyebrowDOM(el) + .map(f => ({ type: f.id, detail: f.snippet })) + .filter(f => _ruleOk(f.type)); + if (eyebrowFindings.length > 0 && el.previousElementSibling) { + addBrowserFindings(groupMap, el.previousElementSibling, eyebrowFindings); + } } const pageLevelFindings = []; @@ -1306,6 +1335,14 @@ if (IS_BROWSER) { addBrowserFindings(groupMap, document.body, qualityFindings); } + const creamFindings = checkCreamPalette(document) + .map(f => ({ type: f.id, detail: f.snippet })) + .filter(f => _ruleOk(f.type)); + if (creamFindings.length > 0) { + pageLevelFindings.push(...creamFindings); + addBrowserFindings(groupMap, document.body, creamFindings); + } + // Regex-on-HTML checks (shared with Node) // Clone the document and strip impeccable-live overlay nodes before the // regex scan, so the inspector's own inline styles (transitions on top/ diff --git a/.gemini/skills/impeccable/scripts/detector/cli/main.mjs b/.gemini/skills/impeccable/scripts/detector/cli/main.mjs index 76da09f0c..94283e01b 100644 --- a/.gemini/skills/impeccable/scripts/detector/cli/main.mjs +++ b/.gemini/skills/impeccable/scripts/detector/cli/main.mjs @@ -41,7 +41,7 @@ function formatFindings(findings, jsonMode) { // Stdin handling // --------------------------------------------------------------------------- -async function handleStdin() { +async function handleStdin(options = {}) { const chunks = []; for await (const chunk of process.stdin) chunks.push(chunk); const input = Buffer.concat(chunks).toString('utf-8'); @@ -50,10 +50,10 @@ async function handleStdin() { const fp = parsed?.tool_input?.file_path; if (fp && fs.existsSync(fp)) { return HTML_EXTENSIONS.has(path.extname(fp).toLowerCase()) - ? detectHtml(fp) : detectText(fs.readFileSync(fp, 'utf-8'), fp); + ? detectHtml(fp, options) : detectText(fs.readFileSync(fp, 'utf-8'), fp, options); } } catch { /* not JSON */ } - return detectText(input, ''); + return detectText(input, '', options); } @@ -81,6 +81,8 @@ Scan files or URLs for UI anti-patterns and design quality issues. Options: --fast Regex-only mode (skip static HTML/CSS analysis, faster but misses linked stylesheets) --json Output results as JSON + --gpt Also report GPT-specific provider tells (off by default) + --gemini Also report Gemini-specific provider tells (off by default) --help Show this help message Detection modes: @@ -106,6 +108,10 @@ async function detectCli() { const jsonMode = args.includes('--json'); const helpMode = args.includes('--help'); const fastMode = args.includes('--fast'); + const providers = []; + if (args.includes('--gpt')) providers.push('gpt'); + if (args.includes('--gemini')) providers.push('gemini'); + const scanOptions = { providers }; const targets = args.filter(a => !a.startsWith('--')); if (helpMode) { printUsage(); process.exit(0); } @@ -113,7 +119,7 @@ async function detectCli() { let allFindings = []; if (!process.stdin.isTTY && targets.length === 0) { - allFindings = await handleStdin(); + allFindings = await handleStdin(scanOptions); } else { const paths = targets.length > 0 ? targets : [process.cwd()]; const urlTargetCount = paths.filter(target => /^https?:\/\//i.test(target)).length; @@ -124,8 +130,8 @@ async function detectCli() { if (/^https?:\/\//i.test(target)) { try { const scanner = browserDetector - ? (url) => browserDetector.detectUrl(url) - : (url) => detectUrl(url); + ? (url) => browserDetector.detectUrl(url, scanOptions) + : (url) => detectUrl(url, scanOptions); allFindings.push(...await scanner(target)); } catch (e) { process.stderr.write(`Error: ${e.message}\n`); } continue; @@ -192,9 +198,9 @@ async function detectCli() { const ext = path.extname(file).toLowerCase(); let fileFindings; if (!fastMode && HTML_EXTENSIONS.has(ext)) { - fileFindings = await detectHtml(file); + fileFindings = await detectHtml(file, scanOptions); } else { - fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file); + fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file, scanOptions); } // Annotate findings with import context const importers = importedByMap.get(file); @@ -209,9 +215,9 @@ async function detectCli() { } else if (stat.isFile()) { const ext = path.extname(resolved).toLowerCase(); if (!fastMode && HTML_EXTENSIONS.has(ext)) { - allFindings.push(...await detectHtml(resolved)); + allFindings.push(...await detectHtml(resolved, scanOptions)); } else { - allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved)); + allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved, scanOptions)); } } } diff --git a/.gemini/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.gemini/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 1afbe3f12..88439a5c3 100644 --- a/.gemini/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.gemini/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -38,7 +38,7 @@ const OVERUSED_FONTS = new Set([ // Older monoculture (still ubiquitous): 'inter', 'roboto', 'open sans', 'lato', 'montserrat', 'arial', 'helvetica', // Newer monoculture (the Anthropic-skill / Vercel / GitHub default wave): - 'fraunces', 'instrument sans', + 'fraunces', 'instrument sans', 'instrument serif', 'geist', 'geist sans', 'geist mono', 'mona sans', 'plus jakarta sans', 'space grotesk', 'recoleta', @@ -165,6 +165,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'AI color palette', }, + { + id: 'cream-palette', + category: 'slop', + name: 'Cream / beige palette', + description: + 'A warm cream or beige page background has become the default "tasteful" AI surface, reached for by reflex. Choose a background that comes from a deliberate palette, not the safe warm off-white.', + skillSection: 'Color & Contrast', + skillGuideline: 'cream and beige as the default surface', + }, { id: 'nested-cards', category: 'slop', @@ -183,15 +192,6 @@ const ANTIPATTERNS = [ skillSection: 'Layout & Space', skillGuideline: 'same spacing everywhere', }, - { - id: 'everything-centered', - category: 'slop', - name: 'Everything centered', - description: - 'Every text element is center-aligned. Left-aligned text with asymmetric layouts feels more designed. Center only hero sections and CTAs.', - skillSection: 'Layout & Space', - skillGuideline: 'Center everything', - }, { id: 'bounce-easing', category: 'slop', @@ -247,17 +247,72 @@ const ANTIPATTERNS = [ skillSection: 'Typography', skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding', }, + { + id: 'numbered-section-markers', + category: 'slop', + severity: 'advisory', + name: 'Numbered section markers (01 / 02 / 03)', + description: + 'Numbered display markers as section labels (01, 02, 03) are the AI editorial scaffold one tier deeper than tracked eyebrow chips. If you find yourself reaching for them, choose a different section cadence.', + skillSection: 'Layout & Space', + skillGuideline: 'numbered section markers', + }, + { + id: 'em-dash-overuse', + category: 'slop', + name: 'Em-dash overuse', + description: + 'More than two em-dashes (— or --) in body copy is an AI cadence tell. Use commas, colons, periods, or parentheses instead.', + skillSection: 'Copy', + skillGuideline: 'no em dashes', + }, + { + id: 'marketing-buzzword', + category: 'slop', + name: 'Marketing buzzword', + description: + 'Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does.', + skillSection: 'Copy', + skillGuideline: 'marketing buzzwords', + }, + { + id: 'aphoristic-cadence', + category: 'slop', + name: 'Aphoristic-cadence copy', + description: + 'Three or more sections landing on a short rebuttal sentence ("X. No Y." / "X. Just Y.") or a manufactured-contrast aphorism ("Not a feature. A platform.") reads as AI cadence, not voice. Once is fine; the pattern is the tell.', + skillSection: 'Copy', + skillGuideline: 'aphoristic cadence', + }, + { + id: 'oversized-h1', + category: 'slop', + name: 'Oversized hero headline', + description: + 'A full-sentence headline set at display size ends up dominating the viewport, leaving no room for anything else above the fold. A punchy one- or two-word headline at that size is fine — the problem is a long headline blown up too large. Set long headlines smaller, or tighten the copy.', + skillSection: 'Typography', + skillGuideline: 'long headline set at display size', + }, + { + id: 'extreme-negative-tracking', + category: 'slop', + name: 'Crushed letter spacing', + description: + 'Letter-spacing pulled tighter than the point where characters keep their own shapes costs legibility. Tighten display type optically, not destructively.', + skillSection: 'Typography', + skillGuideline: 'letter spacing crushed past legibility', + }, + { + id: 'broken-image', + category: 'quality', + name: 'Broken or placeholder image', + description: + ' tags with empty src, missing src, or placeholder values ship as broken-image boxes. Use real images, generated assets, or remove the tag.', + skillSection: 'Imagery', + skillGuideline: 'broken image references', + }, // ── Quality: general design and accessibility issues ── - { - id: 'pure-black-white', - category: 'quality', - name: 'Pure black background', - description: - 'Pure #000000 as a background color looks harsh and unnatural. Tint it slightly toward your brand hue (e.g., oklch(12% 0.01 250)) for a more refined feel.', - skillSection: 'Color & Contrast', - skillGuideline: 'pure black (#000)', - }, { id: 'gray-on-color', category: 'quality', @@ -297,7 +352,9 @@ const ANTIPATTERNS = [ category: 'quality', name: 'Cramped padding', description: - 'Text is too close to the edge of its container. Add at least 8px (ideally 12-16px) of padding inside bordered or colored containers.', + 'Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.', + skillSection: 'Layout & Space', + skillGuideline: 'inside bordered or colored containers', }, { id: 'body-text-viewport-edge', @@ -350,6 +407,70 @@ const ANTIPATTERNS = [ description: 'Letter spacing above 0.05em on body text disrupts natural character groupings and slows reading. Reserve wide tracking for short uppercase labels only.', }, + { + id: 'text-overflow', + category: 'quality', + name: 'Content overflowing its container', + description: + 'Content renders wider than its container, spilling out or forcing a horizontal scrollbar. Let text wrap, constrain widths, or give the region a deliberate scroll affordance.', + skillSection: 'Layout & Space', + skillGuideline: 'content wider than its container', + }, + { + id: 'clipped-overflow-container', + category: 'quality', + name: 'Positioned child clipped by overflow container', + description: + 'A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.', + skillSection: 'Layout & Space', + skillGuideline: 'overflow container clipping positioned children', + }, + + // ── Provider tells: opt-in via --gpt / --gemini (gated off by default) ── + { + id: 'gpt-thin-border-wide-shadow', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Hairline border with wide shadow', + description: + 'A hairline border paired with a wide, diffuse shadow is a recurring generated-UI signature. Commit to one — a defined edge or a soft elevation — rather than both at once.', + skillSection: 'Visual Details', + skillGuideline: 'hairline border plus wide diffuse shadow', + }, + { + id: 'repeating-stripes-gradient', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Repeating-gradient stripes', + description: + 'Repeating-gradient stripes used as surface decoration are a recurring generated-UI signature. Reach for a deliberate texture or leave the surface plain.', + skillSection: 'Visual Details', + skillGuideline: 'repeating-gradient decorative stripes', + }, + { + id: 'theater-slop-phrase', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Theater framing copy', + description: + 'Dismissing something as "theater" is a recurring generated-copy tic. Say plainly what the thing does or does not do.', + skillSection: 'Copy', + skillGuideline: 'theater framing copy', + }, + { + id: 'image-hover-transform', + category: 'slop', + severity: 'advisory', + gated: 'gemini', + name: 'Image hover transform', + description: + 'Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction.', + skillSection: 'Motion', + skillGuideline: 'image scale or rotate on hover', + }, ]; // --- cli/engine/shared/color.mjs --- @@ -527,11 +648,6 @@ function checkColors(opts) { } const findings = []; - // Pure black background (only solid or near-solid, not semi-transparent overlays) - if (bgColor && bgColor.a >= 0.9 && bgColor.r === 0 && bgColor.g === 0 && bgColor.b === 0) { - findings.push({ id: 'pure-black-white', snippet: '#000000 background' }); - } - if (hasDirectText && textColor && !isEmojiOnly) { // Run background-dependent checks against either a solid bg or, if the // ancestor is a gradient, against every gradient stop (use the worst case). @@ -587,9 +703,6 @@ function checkColors(opts) { // Tailwind class checks if (classList) { const classStr = typeof classList === 'string' ? classList : Array.from(classList).join(' '); - if (/\bbg-black\b(?!\/)/.test(classStr)) { - findings.push({ id: 'pure-black-white', snippet: 'bg-black' }); - } const grayMatch = classStr.match(/\btext-(?:gray|slate|zinc|neutral|stone)-\d+\b/); const colorBgMatch = classStr.match(/\bbg-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+\b/); @@ -905,12 +1018,6 @@ function checkHtmlPatterns(html) { // --- Color --- - // Pure black background - const pureBlackBgRe = /background(?:-color)?\s*:\s*(?:#000000|#000|rgb\(\s*0,\s*0,\s*0\s*\))\b/gi; - if (pureBlackBgRe.test(html)) { - findings.push({ id: 'pure-black-white', snippet: 'Pure #000 background' }); - } - // AI color palette: purple/violet const purpleHexRe = /#(?:7c3aed|8b5cf6|a855f7|9333ea|7e22ce|6d28d9|6366f1|764ba2|667eea)\b/gi; if (purpleHexRe.test(html)) { @@ -1027,6 +1134,39 @@ function checkHtmlPatterns(html) { } } + // --- Provider tells (gated): repeating-gradient stripes (GPT) --- + if (/repeating-(?:linear|radial|conic)-gradient\s*\(/i.test(html)) { + findings.push({ id: 'repeating-stripes-gradient', snippet: 'repeating-gradient decorative stripes' }); + } + + // --- Provider tells (gated): "X theater" framing copy (GPT) --- + // Lives here (regex-on-HTML) rather than in the text-content analyzers so it + // runs in the bundled browser path too, not just the CLI/static path. + { + const bodyText = html + .replace(/]*>[\s\S]*?<\/script>/gi, ' ') + .replace(/]*>[\s\S]*?<\/style>/gi, ' ') + .replace(/<[^>]+>/g, ' '); + const tm = /\b(\w+)\s+theater\b/i.exec(bodyText); + if (tm) findings.push({ id: 'theater-slop-phrase', snippet: `"${tm[0].trim()}"` }); + } + + // --- Provider tells (gated): image hover transform (Gemini) --- + // A CSS `img...:hover { transform: ... }` rule, or a Tailwind hover:scale / + // hover:rotate / hover:translate utility on an . Each distinct + // mechanism is its own finding. + const imgHoverCss = /\bimg\b[^,{}]*:hover\b[^{}]*\{[^}]*\btransform\s*:\s*(?:scale|rotate|translate|matrix|skew)/i; + if (imgHoverCss.test(html)) { + findings.push({ id: 'image-hover-transform', snippet: 'img:hover { transform } rule' }); + } + const imgTagRe = /]*\bclass\s*=\s*"([^"]*)"/gi; + let im; + while ((im = imgTagRe.exec(html)) !== null) { + if (/\bhover:(?:scale|rotate|translate|skew)-/.test(im[1])) { + findings.push({ id: 'image-hover-transform', snippet: 'Tailwind hover transform on ' }); + } + } + return findings; } @@ -1673,7 +1813,7 @@ function resolveLengthPx(value, fontSizePx) { // Both adapters resolve font-size, line-height and letter-spacing to pixels // before calling this so the pure function only deals with numbers. function checkQuality(opts) { - const { el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax = 80, viewportWidth = 0 } = opts; + const { el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax = 80, viewportWidth = 0, win = null } = opts; const findings = []; // Skip browser extension injected elements const elId = el.id || ''; @@ -1724,6 +1864,155 @@ function checkQuality(opts) { } } + // --- Flush against a visible boundary --- + // Fires when a container has a visible boundary (border, outline, OR a + // non-transparent background) AND near-zero padding on the bounded + // side(s) AND text-bearing children land flush against the boundary. + // + // Distinct from cramped-padding: that rule needs the element itself to + // have direct text (hasDirectText). This rule targets the OPPOSITE + // shape — a container with NO direct text, only children — which is + // exactly what cramped-padding misses (a section wrapping a label + + // list lands a free pass). + // + // The classic shape: agent writes `padding: 28px 0 0` shorthand on a + // section that also has a border, zeroing horizontal padding so the + // text-bearing children touch the side borders. Background and + // outline count too: a colored card with zero padding has the same + // visual failure mode. + { + const FLUSH_SKIP_TAGS = new Set(['HTML', 'BODY', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ARTICLE', 'ASIDE', 'BUTTON', 'A', 'LABEL', 'SUMMARY', 'CODE', 'PRE', 'INPUT', 'TEXTAREA', 'SELECT', 'FORM', 'FIGURE', 'TABLE', 'TBODY', 'THEAD', 'TR', 'TD', 'TH']); + const upperTag = tag ? tag.toUpperCase() : ''; + const elPosition = style.position || ''; + if ( + !FLUSH_SKIP_TAGS.has(upperTag) && + !hasDirectText && + !['fixed', 'absolute'].includes(elPosition) && + el.children && el.children.length > 0 + ) { + const isTransparent = (c) => + !c || c === 'transparent' || c === 'rgba(0, 0, 0, 0)' || + /^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*0(?:\.0+)?\s*\)$/.test(c); + + const borderW = { + top: parseFloat(style.borderTopWidth) || 0, + right: parseFloat(style.borderRightWidth) || 0, + bottom: parseFloat(style.borderBottomWidth) || 0, + left: parseFloat(style.borderLeftWidth) || 0, + }; + const borderVisible = { + top: borderW.top > 0 && !isTransparent(style.borderTopColor), + right: borderW.right > 0 && !isTransparent(style.borderRightColor), + bottom: borderW.bottom > 0 && !isTransparent(style.borderBottomColor), + left: borderW.left > 0 && !isTransparent(style.borderLeftColor), + }; + // Outline detection. jsdom decomposes `border` shorthand into + // border{Top,…}Width/Color but does NOT decompose `outline` — + // the longhands come back empty when the value was set via the + // shorthand. Fall back to parsing `style.outline` ourselves. + let outlineW = parseFloat(style.outlineWidth) || 0; + let outlineStyleVal = style.outlineStyle || ''; + let outlineColorVal = style.outlineColor || ''; + if (!outlineW && style.outline) { + const wMatch = style.outline.match(/(\d+(?:\.\d+)?)\s*px/); + if (wMatch) outlineW = parseFloat(wMatch[1]) || 0; + if (!outlineStyleVal) { + outlineStyleVal = /\b(solid|dashed|dotted|double|groove|ridge|inset|outset)\b/.test(style.outline) ? 'solid' : ''; + } + if (!outlineColorVal) { + const cMatch = style.outline.match(/(rgba?\([^)]+\)|#[0-9a-fA-F]{3,8}|[a-zA-Z]+)\s*$/); + if (cMatch) outlineColorVal = cMatch[1]; + } + } + const outlineVisible = outlineW > 0 && !isTransparent(outlineColorVal) && outlineStyleVal && outlineStyleVal !== 'none'; + const bgVisible = !isTransparent(style.backgroundColor); + + const anyVisible = borderVisible.top || borderVisible.right || borderVisible.bottom || borderVisible.left || outlineVisible || bgVisible; + if (anyVisible) { + // Resolve padding to px (jsdom returns raw "1.5rem" etc., not the + // computed px value; parseFloat would strip the unit and treat + // 1.5rem as 1.5px, false-flagging legitimate insets). + const pad = { + top: resolveLengthPx(style.paddingTop, fontSize) ?? 0, + right: resolveLengthPx(style.paddingRight, fontSize) ?? 0, + bottom: resolveLengthPx(style.paddingBottom, fontSize) ?? 0, + left: resolveLengthPx(style.paddingLeft, fontSize) ?? 0, + }; + const PAD_THRESHOLD = 2; + // Children-insulate-this-side: a side is insulated if ANY direct + // child has its own padding ≥ 4px on that side. Rationale: in + // typical flow, only the first/last (or leftmost/rightmost) + // children actually sit at the parent's edges. If even one of + // them has its own padding, the visual flush is broken on that + // side. Classic example: a column-flow card frame where the + // top child (header) has padding-top:12 and the bottom child + // (footer) has padding-bottom:8 — the parent's padding:0 doesn't + // matter; nothing is actually flush. The `any-child-insulates` + // heuristic accepts some false negatives (a card with one heavily + // padded middle child won't flag) for far fewer false positives. + const CHILD_INSULATE_THRESHOLD = 4; + const childrenInsulate = { top: false, right: false, bottom: false, left: false }; + for (const child of el.children) { + let childStyle = null; + if (win && typeof win.getComputedStyle === 'function') { + try { childStyle = win.getComputedStyle(child); } catch {} + } + if (!childStyle && typeof getComputedStyle === 'function') { + try { childStyle = getComputedStyle(child); } catch {} + } + if (!childStyle) continue; + const childPad = { + top: resolveLengthPx(childStyle.paddingTop, fontSize) ?? 0, + right: resolveLengthPx(childStyle.paddingRight, fontSize) ?? 0, + bottom: resolveLengthPx(childStyle.paddingBottom, fontSize) ?? 0, + left: resolveLengthPx(childStyle.paddingLeft, fontSize) ?? 0, + }; + for (const s of ['top', 'right', 'bottom', 'left']) { + if (childPad[s] >= CHILD_INSULATE_THRESHOLD) childrenInsulate[s] = true; + } + } + + const flushSides = []; + for (const side of ['top', 'right', 'bottom', 'left']) { + const sideBounded = borderVisible[side] || outlineVisible || bgVisible; + if (sideBounded && pad[side] <= PAD_THRESHOLD && !childrenInsulate[side]) { + flushSides.push(side); + } + } + + if (flushSides.length > 0) { + // Confirm at least one direct child has substantial text content + // (> 4 chars). Without this, the flush is harmless: e.g. an + // image-only card. + let hasTextChild = false; + for (const child of el.children) { + const childText = (child.textContent || '').trim(); + if (childText.length > 4) { hasTextChild = true; break; } + } + if (hasTextChild) { + const cls = (typeof el.className === 'string' && el.className.trim()) + ? el.className.trim().split(/\s+/)[0] + : ''; + const boundaryParts = []; + const borderSidesVisible = ['top', 'right', 'bottom', 'left'].filter(s => borderVisible[s]); + if (borderSidesVisible.length === 4) boundaryParts.push('border'); + else if (borderSidesVisible.length > 0) boundaryParts.push(`border-${borderSidesVisible.join('/')}`); + if (outlineVisible) boundaryParts.push('outline'); + if (bgVisible) boundaryParts.push('bg'); + const sidesLabel = flushSides.length === 4 ? 'all sides' : flushSides.join('/'); + const ident = cls + ? `<${tag.toLowerCase()}> "${cls}"` + : `<${tag.toLowerCase()}>`; + findings.push({ + id: 'cramped-padding', + snippet: `${ident}: children flush against ${boundaryParts.join('+')} on ${sidesLabel} (no inset)`, + }); + } + } + } + } + } + // --- Body text touching viewport edge --- (browser-only: needs rect) // Catches the failure mode where the agent ships body paragraphs // with NO container providing horizontal padding — text bleeds @@ -1804,6 +2093,20 @@ function checkQuality(opts) { } } + // --- Crushed letter spacing (mirror of wide-tracking) --- + // Tracking pulled tighter than ~-0.05em crushes characters into each other. + // Optical tightening that display type legitimately wants (around -0.02em) + // stays well above this floor. + if (hasDirectText && textLen > 20 && fontSize > 0) { + if (letterSpacingPx != null && letterSpacingPx < 0) { + const trackingEm = letterSpacingPx / fontSize; + if (trackingEm <= -0.05) { + const excerpt = (el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 40); + findings.push({ id: 'extreme-negative-tracking', snippet: `letter-spacing: ${trackingEm.toFixed(2)}em — "${excerpt}"` }); + } + } + } + return findings; } @@ -1820,7 +2123,7 @@ function checkElementQualityDOM(el) { const rect = el.getBoundingClientRect(); const lineMax = (typeof window !== 'undefined' && window.__IMPECCABLE_CONFIG__?.lineLengthMax) || 80; const viewportWidth = (typeof window !== 'undefined' ? window.innerWidth : 0) || 0; - return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax, viewportWidth }); + return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax, viewportWidth, win: typeof window !== 'undefined' ? window : null }); } // Pure page-level skipped-heading walk. Takes a Document so it works in both @@ -1862,7 +2165,7 @@ function checkElementQuality(el, style, tag, window) { const fontSize = resolveFontSizePx(el, window); const lineHeightPx = resolveLengthPx(style.lineHeight, fontSize); const letterSpacingPx = resolveLengthPx(style.letterSpacing, fontSize); - return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect: null }); + return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect: null, win: window }); } function checkElementBorders(tag, style, overrides, resolvedRadius) { @@ -2303,40 +2606,213 @@ function checkPageLayout(doc, win) { } } - // Everything centered - const textEls = doc.querySelectorAll('h1, h2, h3, h4, h5, h6, p, li, div, button'); - let centeredCount = 0; - let totalText = 0; - for (const el of textEls) { - const hasDirectText = [...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim().length >= 3); - if (!hasDirectText) continue; - totalText++; - - let cur = el; - let isCentered = false; - while (cur && cur.nodeType === 1) { - const rawStyle = cur.getAttribute?.('style') || ''; - const cls = cur.getAttribute?.('class') || ''; - if (/text-align\s*:\s*center/i.test(rawStyle) || /\btext-center\b/.test(cls)) { - isCentered = true; - break; - } - if (cur.tagName === 'BODY') break; - cur = cur.parentElement; - } - if (isCentered) centeredCount++; - } - - if (totalText >= 5 && centeredCount / totalText > 0.7) { - findings.push({ - id: 'everything-centered', - snippet: `${centeredCount}/${totalText} text elements centered (${Math.round(centeredCount / totalText * 100)}%)`, - }); - } - return findings; } +// ─── Cream / beige palette (the default "tasteful" AI surface) ──────────────── +// A warm, lightly-tinted off-white page background — light, with R≥G≥B and a +// small warm tint (not white, not a strong color). The current reflex surface. +function isCreamColor(rgb) { + if (!rgb) return false; + const { r, g, b } = rgb; + if (Math.min(r, g, b) < 209) return false; // must be light + if (!(r >= g && g >= b)) return false; // warm ordering + const warmth = r - b; + return warmth >= 6 && warmth <= 48; // tinted, not white, not strong +} + +// Tailwind background utilities that render as a warm off-white surface. The +// static engine doesn't fetch Tailwind's CSS, so a `bg-amber-50` on +// resolves to nothing in computed style — catch it from the class list +// instead. Candidate tokens map to their actual Tailwind hex and are still +// filtered through isCreamColor, so neutral grays (stone) and over-saturated +// shades drop out on their own. +const TAILWIND_BG_HEX = { + 'bg-amber-50': '#fffbeb', 'bg-amber-100': '#fef3c7', + 'bg-orange-50': '#fff7ed', 'bg-orange-100': '#ffedd5', + 'bg-yellow-50': '#fefce8', + 'bg-stone-50': '#fafaf9', 'bg-stone-100': '#f5f5f4', 'bg-stone-200': '#e7e5e4', +}; + +function creamFromClassList(cls) { + if (!cls) return null; + // Arbitrary value: bg-[#f5f0e6] / bg-[rgb(245_240_230)] (underscores = spaces). + const arb = cls.match(/\bbg-\[([^\]]+)\]/); + if (arb && isCreamColor(parseAnyColor(arb[1].replace(/_/g, ' ')))) return `bg-[${arb[1]}]`; + // Named warm-light utilities. + for (const [tok, hex] of Object.entries(TAILWIND_BG_HEX)) { + if (new RegExp(`(^|\\s)${tok}($|\\s)`).test(cls) && isCreamColor(parseAnyColor(hex))) return tok; + } + return null; +} + +function checkCreamPalette(doc, win) { + const findings = []; + const body = doc.body || (doc.querySelector ? doc.querySelector('body') : null); + if (!body) return findings; + const html = doc.documentElement; + const getCS = (el) => (win ? win.getComputedStyle(el) : getComputedStyle(el)); + + // 1. Computed background — covers inline / close.', 'Prefix every preview selector with the matching [data-impeccable-variant="N"] selector.', 'Keep selectors anchored to the generated variant wrapper; do not rely on component CSS scoping for preview rules.', ], forbidden: [ 'Do not use @scope for this styleMode.', + 'Do not wrap style content in a JSX/TSX template literal ({` ... `}); that syntax is for .tsx/.jsx only.', + 'Do not put { immediately after the style opening tag; Astro parses { as expression syntax.', ], }; } @@ -629,4 +632,15 @@ if (_running?.endsWith('live-wrap.mjs') || _running?.endsWith('live-wrap.mjs/')) } // Test exports (used by tests/live-wrap.test.mjs) -export { buildSearchQueries, findElement, findClosingLine, detectCommentSyntax }; +export { + buildSearchQueries, + findElement, + findClosingLine, + detectCommentSyntax, + findAllElements, + filterByText, + findFileWithQuery, + detectStyleMode, + buildCssAuthoring, + buildCssSelectorPrefixExamples, +}; diff --git a/.gemini/skills/impeccable/scripts/live.mjs b/.gemini/skills/impeccable/scripts/live.mjs index cafb0eca9..8acd300ed 100644 --- a/.gemini/skills/impeccable/scripts/live.mjs +++ b/.gemini/skills/impeccable/scripts/live.mjs @@ -21,7 +21,7 @@ import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { loadContext } from './load-context.mjs'; +import { loadContext } from './context.mjs'; import { resolveFiles } from './live-inject.mjs'; import { readLiveServerInfo } from './impeccable-paths.mjs'; @@ -80,7 +80,7 @@ The agent should then: process.exit(1); } - // 4. Load PRODUCT.md + DESIGN.md context (auto-migrates legacy .impeccable.md) + // 4. Load PRODUCT.md + DESIGN.md context. const ctx = loadContext(process.cwd()); // 5. Compute drift-heal: compare resolved inject targets against the @@ -102,7 +102,6 @@ The agent should then: hasDesign: ctx.hasDesign, design: ctx.design, designPath: ctx.designPath, - migrated: ctx.migrated, }, null, 2)); } diff --git a/.gemini/skills/impeccable/scripts/load-context.mjs b/.gemini/skills/impeccable/scripts/load-context.mjs deleted file mode 100644 index dc340bf16..000000000 --- a/.gemini/skills/impeccable/scripts/load-context.mjs +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Shared context loader for every impeccable command that needs to know - * "who is this for" and "what does this look like". - * - * Input: project root (process.cwd()). - * - * Output (JSON to stdout): - * { - * hasProduct: boolean, // PRODUCT.md found (or auto-migrated) - * product: string | null, // PRODUCT.md contents - * productPath: string | null, // relative path - * hasDesign: boolean, // DESIGN.md found - * design: string | null, // DESIGN.md contents - * designPath: string | null, - * migrated: boolean, // true if we auto-renamed .impeccable.md -> PRODUCT.md - * contextDir: string, // absolute path of the directory the files were found in - * } - * - * Filename matching is case-insensitive for PRODUCT.md and DESIGN.md. The - * Google DESIGN.md convention is uppercase at repo root; Kiro-style and - * lowercase variants are also matched so users don't get punished for case. - * - * Lookup directory resolution (first match wins): - * 1. process.env.IMPECCABLE_CONTEXT_DIR (absolute or relative to cwd) - * 2. cwd, if PRODUCT.md / DESIGN.md / .impeccable.md is there (back-compat) - * 3. Auto-fallback subdirectories of cwd: .agents/context/, then docs/ - * 4. cwd as a default "no context found" location - * - * Legacy `.impeccable.md` -> PRODUCT.md migration only fires at cwd root; - * fallback directories are read-only as far as auto-rename is concerned. - */ - -import fs from 'node:fs'; -import path from 'node:path'; - -const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; -const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; -const LEGACY_NAMES = ['.impeccable.md']; -const FALLBACK_DIRS = ['.agents/context', 'docs']; - -/** - * Resolve the directory that holds PRODUCT.md / DESIGN.md for - * this project. Exported so other scripts (e.g. live-server.mjs) can read the - * design files from the same location the loader uses. - */ -export function resolveContextDir(cwd = process.cwd()) { - // 1. Explicit override - const envDir = process.env.IMPECCABLE_CONTEXT_DIR; - if (envDir && envDir.trim()) { - const trimmed = envDir.trim(); - return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); - } - - // 2. cwd wins if any canonical or legacy file is there. We check legacy too - // so the auto-migration path in loadContext stays predictable. - if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES, ...LEGACY_NAMES])) { - return cwd; - } - - // 3. Auto-fallback subdirs. Match if PRODUCT.md or DESIGN.md is present; - // legacy `.impeccable.md` does not pull the lookup into a fallback dir. - for (const rel of FALLBACK_DIRS) { - const candidate = path.resolve(cwd, rel); - if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { - return candidate; - } - } - - // 4. Nothing found — keep the historical "default to cwd" behaviour so the - // caller's `hasProduct === false` branch still fires the same way. - return cwd; -} - -export function loadContext(cwd = process.cwd()) { - let migrated = false; - const contextDir = resolveContextDir(cwd); - - // 1. Look for PRODUCT.md (case-insensitive) in the resolved dir - let productPath = firstExisting(contextDir, PRODUCT_NAMES); - - // 2. Legacy: if no PRODUCT.md but .impeccable.md exists at cwd root, rename - // it in place. We only migrate at the root — fallback dirs are read-only - // so we don't surprise users by mutating files under docs/ or .agents/. - if (!productPath && contextDir === cwd) { - const legacyPath = firstExisting(cwd, LEGACY_NAMES); - if (legacyPath) { - const newPath = path.join(cwd, 'PRODUCT.md'); - try { - fs.renameSync(legacyPath, newPath); - productPath = newPath; - migrated = true; - } catch { - // Rename failed (permissions, etc.) — fall back to reading legacy in place - productPath = legacyPath; - } - } - } - - // 3. DESIGN.md (case-insensitive) - const designPath = firstExisting(contextDir, DESIGN_NAMES); - - const product = productPath ? safeRead(productPath) : null; - const design = designPath ? safeRead(designPath) : null; - - return { - hasProduct: !!product, - product, - productPath: productPath ? path.relative(cwd, productPath) : null, - hasDesign: !!design, - design, - designPath: designPath ? path.relative(cwd, designPath) : null, - migrated, - contextDir, - }; -} - -function firstExisting(dir, names) { - for (const name of names) { - const abs = path.join(dir, name); - if (fs.existsSync(abs)) return abs; - } - return null; -} - -function safeRead(p) { - try { return fs.readFileSync(p, 'utf-8'); } catch { return null; } -} - -// --------------------------------------------------------------------------- -// CLI mode — print the context as JSON -// --------------------------------------------------------------------------- - -function cli() { - const result = loadContext(process.cwd()); - console.log(JSON.stringify(result, null, 2)); -} - -const _running = process.argv[1]; -if (_running?.endsWith('load-context.mjs') || _running?.endsWith('load-context.mjs/')) { - cli(); -} diff --git a/.gemini/skills/impeccable/scripts/palette.mjs b/.gemini/skills/impeccable/scripts/palette.mjs new file mode 100644 index 000000000..4c3f1751a --- /dev/null +++ b/.gemini/skills/impeccable/scripts/palette.mjs @@ -0,0 +1,633 @@ +#!/usr/bin/env node +/** + * Brand-seed picker. Returns one OKLCH seed color + the mood it most + * naturally evokes, and teaches the model how to compose a full palette + * around it. + * + * The seed is the brand's anchor color. The 5-role palette (bg, surface, + * ink, accent, muted) is composed by the caller at runtime using their + * judgment + the brief (PRODUCT.md / DESIGN.md / user prompt), NOT picked + * from a frozen 4-color preset. + * + * Why: 4-color frozen palettes drift toward safe defaults (warm-cream bg, + * complementary accent on near-white) regardless of brief. A single seed + + * the model's own composition lets the same seed produce a dark-mode jazz + * club or a light-mode hospitality brand depending on what the brief calls + * for. Tested empirically against curated 4-color palettes; seed approach + * wins on mood-fit in 3 of 5 cases and ties on the rest. + * + * Usage: + * node scripts/palette.mjs # pick at random + * node scripts/palette.mjs --id seed-021 # pick a specific seed + * node scripts/palette.mjs --from # hash to a seed (deterministic) + * + * Env vars: + * IMPECCABLE_PALETTE_SEED — same as --from; useful for the eval harness + * to make runs reproducible. + */ + +import crypto from 'node:crypto'; + +// Seeds are inlined (129 entries, hand-curated via a tinder review of +// ~400 candidates from ColorHunt + synthesis + Radix/brand/Pantone anchors). +// Each carries a mood + strategy the judging model produced — surfaced as +// hints, not commands; the brief still drives composition. +const SEEDS = [ + { id: "seed-200", oklch: [0.360, 0.137, 0.0], + mood: "Aesop apothecary shelf — oxblood bottle glass against linen, considered and unhurried", + strategy: "Seed is a deep desaturated red-brown that reads as brand ink itself; I push primary darker toward bottle-glass oxblood, pair with a pure white surface so the red does the work, and use a clear pale-blush accent that can carry dark text in pills." }, + { id: "seed-000", oklch: [0.400, 0.130, 0.0], + mood: "oxblood leather banquette in a 1940s steakhouse — low lamplight on dark wood and burgundy", + strategy: "Near-black bg with the faintest red undertone lets the oxblood primary glow like lamplit leather; warm cream ink and a brass accent complete the chophouse register." }, + { id: "seed-002", oklch: [0.450, 0.150, 0.0], + mood: "darkroom red light — analog photography, blood-warm safelight glow on chemical trays", + strategy: "Near-black surface with a deep oxblood primary lets the seed function like a safelight in a darkroom — the bg disappears so the red becomes the only emotional signal." }, + { id: "seed-003", oklch: [0.500, 0.194, 0.0], + mood: "darkroom safelight — the deep oxblood glow of analog photography, chemical and contemplative", + strategy: "Anchored the seed as primary against pure near-black so the red reads like a single illuminated bulb in a developing room, with cool desaturated ink to evoke silver gelatin print tones." }, + { id: "seed-004", oklch: [0.546, 0.204, 3.4], + mood: "midnight boudoir — velvet rose under low lamplight, perfumed and intimate", + strategy: "Near-black surface lets the rose seed glow like silk in shadow; a warm champagne accent provides the candle-flame counterpoint without breaking the hush." }, + { id: "seed-005", oklch: [0.550, 0.180, 0.0], + mood: "smoldering vermillion at dusk — the last red ember in a blacksmith's forge, iron-rich and quietly violent", + strategy: "Near-black gallery surround lets the seed read as glowing forged metal; ink stays warm-off-white, accent shifts to a hotter ember orange so the primary feels like cooling steel against a fresh strike." }, + { id: "seed-201", oklch: [0.647, 0.262, 0.3], + mood: "Figma plugin marketplace red — confident product-brand crimson, the kind a modern dev tool uses for a 'live' indicator or a primary CTA on a pristine docs page", + strategy: "Pure white surface lets a high-chroma crimson primary do all the brand work, paired with a hue-shifted warm coral accent for hierarchy without competing saturation" }, + { id: "seed-006", oklch: [0.650, 0.160, 0.0], + mood: "1960s Italian cinema — Technicolor lipstick red against a darkened theater", + strategy: "Pure near-black surface lets a saturated cinematic red and its warm peach accent perform like film light projected in a dark room — the brand colors carry the drama, the bg disappears." }, + { id: "seed-008", oklch: [0.520, 0.200, 10.4], + mood: "Negroni hour at a Milanese bar — bittersweet crimson, vermouth and amaro under low tungsten", + strategy: "Seed is a saturated red-crimson with cinematic weight, so I sit it on near-black to let the primary glow like backlit liquor, with a warmer amber accent acting as the citrus twist against the bitter red." }, + { id: "seed-010", oklch: [0.563, 0.223, 11.0], + mood: "Negroni hour on a Milan rooftop — bittersweet crimson, aperitivo light, polished restraint", + strategy: "Seed is a vivid carmine-red with strong chroma, so the surface gets out of the way (pure white) and lets the primary do the aperitivo work, with a cooled garnet accent for tension." }, + { id: "seed-202", oklch: [0.643, 0.247, 7.0], + mood: "Glossier brand pink — modern beauty editorial, confident and current", + strategy: "Pure white bg lets a saturated rose-red primary do all the brand work, paired with a deeper crimson accent for hierarchy — the Stripe/Glossier move where the color carries the mood." }, + { id: "seed-013", oklch: [0.400, 0.130, 20.0], + mood: "Tuscan cellar at dusk — aged terracotta, oxidized iron, the deep red of decanted Sangiovese", + strategy: "Black surface lets the oxblood seed and copper accent glow like firelight on cellar stone; brand colors carry all the warmth while the room recedes." }, + { id: "seed-014", oklch: [0.450, 0.150, 20.0], + mood: "smoldering tannery — oxblood leather, cured under low workshop light", + strategy: "Anchor the deep oxblood seed as primary against a near-black architectural ground, then lift with a single warm ember accent so the leather reads burnished rather than bloody." }, + { id: "seed-016", oklch: [0.550, 0.180, 20.0], + mood: "Negroni hour on a Roman terrace — bitter campari red, vermouth, late golden light spilling on white linen", + strategy: "Pure white surface lets the campari-red primary do all the emotional work, paired with a deeper oxblood accent for bittersweet depth — Italian aperitivo restraint, not warmth-washed." }, + { id: "seed-205", oklch: [0.634, 0.254, 17.6], + mood: "Aesop apothecary bottle — considered red-coral on a clinical white surface, the kind of brand restraint where one saturated object does all the work", + strategy: "Default A pure white surface lets a single coral-red primary carry the entire brand voice; accent shifts to a deeper oxblood for hierarchy without competing chroma." }, + { id: "seed-011", oklch: [0.639, 0.207, 13.5], + mood: "Aperitivo hour in Milan — Campari glow on a white marble bar, crisp and effervescent", + strategy: "Pure white gallery backdrop lets the Campari-red primary ring like a single bitter note; ink is near-black with a whisper of warmth, accent shifts to a deeper oxblood for hierarchy without competing hues." }, + { id: "seed-015", oklch: [0.527, 0.202, 22.7], + mood: "Negroni hour on a Milanese terrace — bittersweet vermillion, aperitivo glassware catching low sun", + strategy: "Seed becomes a saturated aperitivo-red primary against pure white so the color carries the bittersweet warmth alone, paired with a deep oxblood accent for typographic gravitas." }, + { id: "seed-023", oklch: [0.427, 0.175, 29.2], + mood: "blacksmith's forge at dusk — iron heated to ember red, the deep glow of oxidized metal and quenching oil", + strategy: "Pure black bg lets the seed's ember-red glow radiate like hot iron in a dark forge; accent shifts to a copper-amber to suggest scaling metal and sparks, while ink stays near-white for tool-precise legibility." }, + { id: "seed-206", oklch: [0.614, 0.234, 28.2], + mood: "Aesop apothecary bottle — considered red-orange on lab-white, calm utility with a single confident pigment", + strategy: "Pure white surface lets a saturated vermilion primary do all the brand work, paired with a deep oxblood accent for hierarchy without introducing a second hue family" }, + { id: "seed-029", oklch: [0.665, 0.222, 25.7], + mood: "Negroni hour at a Milanese bar — bittersweet orange-red liqueur catching late afternoon light on polished marble", + strategy: "Pure white surface lets the seed's vermilion read like Campari in a glass; a deeper oxblood accent provides the bitter depth, with neutral graphite ink keeping the editorial restraint of Italian design." }, + { id: "seed-022", oklch: [0.418, 0.155, 27.2], + mood: "Pompeiian red fresco — oxidized cinnabar on a museum wall, archaeological gravity", + strategy: "Pure black gallery surface lets the seed's iron-oxide red read as a lit artifact; accent shifts to an aged terracotta amber, so primary and accent form a fired-clay duet against neutral void." }, + { id: "seed-024", oklch: [0.464, 0.169, 26.9], + mood: "Mid-century darkroom under the safelight — developer trays, oxblood leather, the quiet patience of a print emerging", + strategy: "Seed becomes a deep oxblood primary; surface stays pure black so the red glows like a safelight, with a warmer ember accent for hierarchy" }, + { id: "seed-026", oklch: [0.489, 0.190, 28.3], + mood: "smoldering ember in a blacksmith's forge — iron-hot rust, soot, and controlled fire", + strategy: "Near-black soot background lets the seed's red-orange glow like heated metal; ink is bone-white, accent is a cooler tempered-steel orange that creates internal heat gradient with the primary." }, + { id: "seed-027", oklch: [0.568, 0.208, 27.1], + mood: "Sicilian blood orange at golden hour — citrus rind, terracotta, sun on stucco", + strategy: "Seed reads as vivid blood-orange — picked pure white surface so the citrus-red primary and a deep oxblood accent do all the emotional work, like a Loro Piana editorial spread." }, + { id: "seed-028", oklch: [0.591, 0.172, 24.0], + mood: "Sienna-fired ceramic studio at dusk — terracotta cooling on a wheel, hands still dusted with slip", + strategy: "Pure black stage lets the fired-clay primary glow like a kiln ember, with a deeper oxblood accent providing tonal weight rather than hue contrast — a monochrome warm-axis play." }, + { id: "seed-033", oklch: [0.544, 0.169, 31.3], + mood: "1960s Italian terracotta workshop — fired clay, espresso, late-afternoon Mediterranean dust", + strategy: "Pure black ground lets the seed's burnt-sienna primary glow like a lit kiln, with a deeper oxblood accent for restrained warmth tension — the brand carries the heat, the surface stays out." }, + { id: "seed-207", oklch: [0.564, 0.231, 29.1], + mood: "Aesop apothecary bottle — considered red oxide, the calm authority of a well-made object on a white shelf", + strategy: "Seed becomes the singular brand voice against pure white, with a deeper oxblood accent for hierarchy — the surface disappears so the red does all the speaking." }, + { id: "seed-035", oklch: [0.663, 0.153, 32.1], + mood: "Aesop apothecary bottle — clay-fired warmth, considered retail", + strategy: "Pure white surface lets the terracotta primary do the brand work, paired with a deep umber ink and a cooler clay accent for editorial tension." }, + { id: "seed-037", oklch: [0.590, 0.188, 35.8], + mood: "Aesop apothecary bottle — considered terracotta, herbalist restraint, the warmth comes from the glass not the room", + strategy: "Seed becomes a muted terracotta primary against pure white so the brand's warmth carries entirely through the color itself; accent shifts to a deeper umber for quiet hierarchy." }, + { id: "seed-038", oklch: [0.652, 0.229, 34.8], + mood: "blown-glass furnace at dusk — molten orange iron pulled from the kiln, a craftsman's signature heat", + strategy: "Pure black stage so the seed reads as live ember; primary holds the seed's heat, accent shifts to a brass-amber a hue-step away for a 1.7+ contrast pairing without leaving the fire." }, + { id: "seed-039", oklch: [0.653, 0.185, 33.5], + mood: "Aesop apothecary bottle — considered terracotta, quiet retail craft", + strategy: "Seed becomes a grounded clay primary against pure white, paired with a deeper umber accent so the warmth lives entirely in the brand marks, not the surface." }, + { id: "seed-167", oklch: [0.495, 0.134, 36.0], + mood: "Aesop apothecary shelf — burnished terracotta on clinical white, considered craft pharmacy", + strategy: "Treat the seed as a brand-carrying burnt-sienna against a pure paper-white surface so the warmth lives entirely in the primary, with a deep umber accent pulled along the same warm axis for typographic gravity." }, + { id: "seed-147", oklch: [0.500, 0.151, 40.0], + mood: "Aesop apothecary shelf — considered terracotta, pharmacy restraint, the brand color does the work against clinical white", + strategy: "Anchor the seed's burnt-sienna primary against a pure white surface so the rust speaks alone, with a deep umber ink and a cooler clay accent to give the palette product-brand discipline rather than environmental warmth." }, + { id: "seed-040", oklch: [0.660, 0.201, 40.0], + mood: "Aesop apothecary bottle — amber glass on a clean dispensary shelf, considered and clinical-warm", + strategy: "Seed becomes a burnt-amber primary against pure white so the bottle-glass color does the emotional work; accent shifts to a deep olive-bronze for the apothecary-label pairing." }, + { id: "seed-041", oklch: [0.673, 0.217, 38.6], + mood: "Aesop apothecary shelf — considered orange glass, clinical retail restraint", + strategy: "Pure white surface lets the burnt-orange primary do all the brand work, with a deep ink-brown for editorial gravity and a muted clay accent that reads as a sibling, not a contrast." }, + { id: "seed-042", oklch: [0.688, 0.133, 35.8], + mood: "Aesop apothecary shelf — terracotta glass, considered retail", + strategy: "Seed becomes a warm clay primary against pure white so the bottle-on-marble retail feel comes from the brand color alone; a deeper umber accent gives the label-print contrast." }, + { id: "seed-043", oklch: [0.781, 0.119, 38.1], + mood: "Aesop apothecary catalogue — considered terracotta, dermatological restraint, the warm color doing all the work against clinical white", + strategy: "Pure white surface lets the seed's warm clay tone read as the entire brand voice, paired with a deeper umber accent for hierarchy without competing with the primary's warmth." }, + { id: "seed-168", oklch: [0.400, 0.103, 50.0], + mood: "Aesop apothecary bottle — amber glass on a clinical white shelf, considered and pharmaceutical", + strategy: "Pure white surface lets the deep amber primary act like tinted glass against a clean shelf; accent is a muted clay that complements without competing, keeping the brand quiet and product-led." }, + { id: "seed-044", oklch: [0.568, 0.149, 45.9], + mood: "1970s desert highway at golden hour — sun-faded terracotta, denim dust, the warmth of a Polaroid pulled from a glovebox", + strategy: "Seed becomes a burnt-sienna primary against pure white so the terracotta does all the emotional work; a deep indigo accent acts as the denim shadow opposing the sun, creating the era's signature warm/cool tension without tinting the page." }, + { id: "seed-045", oklch: [0.607, 0.163, 47.7], + mood: "Aesop apothecary shelf — considered amber glass, clinical restraint, craft pharmacy", + strategy: "Pure white bg lets the burnt-amber primary do the apothecary work alone, paired with a deeper umber accent and graphite ink for editorial calm." }, + { id: "seed-046", oklch: [0.653, 0.175, 45.0], + mood: "Aesop apothecary shelf — considered amber glass, quiet luxury, restrained craft", + strategy: "Pure black backdrop lets the warm amber primary glow like backlit apothecary glass, with a deeper rust accent providing tonal depth in the same hue family — monochromatic warm against neutral void." }, + { id: "seed-047", oklch: [0.695, 0.205, 43.2], + mood: "Aesop apothecary label — sun-warmed amber glass on a clinical countertop, restrained botanical pharmacy", + strategy: "Pure white surface lets the burnt-amber primary and a deeper sienna accent do all the brand work, like an apothecary bottle photographed under daylight." }, + { id: "seed-051", oklch: [0.704, 0.189, 49.0], + mood: "blacksmith's forge at dusk — glowing iron, hammered copper, ember light against cooling steel", + strategy: "Pure near-black surface lets the seed's molten orange burn like heated metal; accent shifts to a deeper amber-red to suggest the cooling end of the same iron, while ink stays a clean off-white so type reads like chalk on slate." }, + { id: "seed-171", oklch: [0.550, 0.124, 60.0], + mood: "Klim Type Foundry specimen page — considered ochre on paper, design-school-honest", + strategy: "Seed becomes a muted ochre primary on pure white; accent is a deep ink-navy pulled across the wheel for editorial contrast without warmth-pooling in the bg" }, + { id: "seed-148", oklch: [0.650, 0.146, 60.0], + mood: "Klim-style editorial gold — late-afternoon paper light on a serif specimen sheet, considered and dry", + strategy: "Hold the seed's amber as primary on a pure white page so the gold reads as ink rather than atmosphere, and pair with a deep aubergine accent for typographic contrast." }, + { id: "seed-052", oklch: [0.700, 0.130, 60.0], + mood: "late-afternoon terracotta studio — sun-warmed clay, hands-on craft, the hour before dusk", + strategy: "Seed is a saturated amber-ochre with strong environmental association (ceramics, adobe, sunlit plaster), so I lean into Exception (a) with a faintly warm bone surface that reads as lime-washed wall, then deepen the seed slightly for primary and pair it with a fired-clay rust accent for hand-thrown warmth." }, + { id: "seed-053", oklch: [0.773, 0.157, 56.6], + mood: "late-summer apricot orchard at golden hour — sun-warmed fruit, considered Californian craft", + strategy: "Seed is a juicy mid-warm orange at daylight luminance — leaning optimistic/editorial, so pure white surface lets the apricot primary glow without muddying it; a deep wine accent provides the bite." }, + { id: "seed-149", oklch: [0.600, 0.124, 70.0], + mood: "1970s desert highway — late-afternoon amber light on chrome and asphalt", + strategy: "Anchor the amber seed as primary against pure black so the warm hue reads as headlight glow against night; a cooler dusk-mauve accent provides the complementary tension of horizon vs. sun." }, + { id: "seed-054", oklch: [0.740, 0.162, 68.1], + mood: "late-afternoon honey on terracotta — Mediterranean stucco at golden hour, sun-baked amber", + strategy: "Seed is a saturated honey-amber at high lightness; pairing it with pure black lets the warmth read as luminous gold against gravity, like lamplight in a dark room." }, + { id: "seed-055", oklch: [0.774, 0.174, 65.1], + mood: "late-summer honey hour — amber light slanting through a west-facing window, optimistic and golden", + strategy: "Anchor a saturated honey-amber primary on pure white so the warmth radiates from the brand itself, then pair with a deep teak accent for grounded contrast rather than tinting the canvas." }, + { id: "seed-056", oklch: [0.691, 0.146, 74.6], + mood: "Klim-style modern publishing house — late-afternoon paper warmth, considered editorial gold", + strategy: "Pure white surface so the amber seed becomes the brand voice; ink stays near-black neutral and accent shifts to a deep ink-blue to give the gold something structural to lean on." }, + { id: "seed-150", oklch: [0.750, 0.148, 80.0], + mood: "Klim Type Foundry specimen page — late-summer editorial gold, considered and grown-up", + strategy: "Pure white surface lets a single restrained ochre primary do all the brand work, paired with a deep ink-blue accent for typographic contrast in the Klim/Commercial Type tradition." }, + { id: "seed-058", oklch: [0.764, 0.120, 77.1], + mood: "Klim Type Foundry specimen page — late-afternoon ochre, considered editorial typography", + strategy: "Pure white surface lets the ochre primary do the brand work, paired with a deep ink-blue accent for editorial contrast — the type-foundry move where one warm hue carries the whole feeling against neutral paper." }, + { id: "seed-059", oklch: [0.784, 0.144, 79.8], + mood: "late afternoon in a Tuscan limonaia — sun-cured amber on whitewashed plaster", + strategy: "Pure white surface lets the saffron-amber primary and a deep olive accent carry the Mediterranean warmth, with split-complementary tension between gold and a quiet evergreen." }, + { id: "seed-061", oklch: [0.817, 0.161, 75.1], + mood: "late-afternoon honey on Tuscan limestone — golden hour, slow and luminous", + strategy: "Pure white surface lets the amber primary glow like sunlight on a wall, paired with a deep terracotta accent for warm tonal contrast within the same hue family." }, + { id: "seed-063", oklch: [0.842, 0.165, 91.3], + mood: "late-afternoon Tuscan sun on limestone — golden hour, considered, optimistic", + strategy: "Pure white surface lets the amber-gold primary radiate as the mood-carrier, with a deep aubergine accent providing the long shadow that golden light needs to feel three-dimensional." }, + { id: "seed-174", oklch: [0.350, 0.075, 110.0], + mood: "olive grove at late afternoon — sun-cured leaves, dust, and quiet Mediterranean weight", + strategy: "Pure white surface lets a deep, sun-cured olive primary do the emotional work, with a burnt-terracotta accent providing the warm-earth counterpoint olive groves are known for." }, + { id: "seed-117", oklch: [0.650, 0.100, 110.0], + mood: "Klim-style editorial sage — late-summer foundry catalogue, considered olive-yellow on paper", + strategy: "Seed sits at olive-chartreuse; treating it as a quiet typographic primary on pure paper, with a deeper bronze-olive accent for hierarchy — the color does the work, the page disappears." }, + { id: "seed-118", oklch: [0.750, 0.090, 110.0], + mood: "Klim Type Foundry specimen page — late-summer olive light on a working specimen, the honesty of a type designer showing their work", + strategy: "Pure white bg lets a desaturated olive-yellow primary do the editorial work, with a deeper olive-bronze accent providing typographic emphasis the way a specimen uses one heavy weight against the body roman." }, + { id: "seed-065", oklch: [0.797, 0.166, 113.1], + mood: "late-summer olive grove at noon — sun-bleached leaves, dry stone, Mediterranean glare", + strategy: "Hold the seed as a luminous chartreuse-olive primary against pure white so the color reads as sunlit foliage, pairing it with a deep umber accent for the dry-stone contrast." }, + { id: "seed-176", oklch: [0.300, 0.071, 120.0], + mood: "moss-darkened apothecary jar — herbal, shadowed, mid-19th-century botanical study", + strategy: "Seed is a deep desaturated olive-green that reads as preserved botanical pigment; I anchor it on pure white so the dim moss-green primary feels like ink on a herbarium page, with a warm ochre accent supplying the aged-paper counterpoint." }, + { id: "seed-155", oklch: [0.550, 0.142, 130.0], + mood: "moss-bed forest floor at noon — chlorophyll, lichen, sunlit fern", + strategy: "Seed is a confident mid-olive green with strong chroma; mood is daylight botanical, so I let the brand greens do the work on a pure paper-white bg and pair with a warm umber accent for fern-against-bark contrast." }, + { id: "seed-119", oklch: [0.600, 0.154, 130.0], + mood: "moss garden at Saihō-ji — damp stone, filtered green light through old cedar", + strategy: "Pure near-black bg lets the seed's mossy green glow like wet lichen under low light; accent shifts to a pale ochre-gold like sun catching through canopy." }, + { id: "seed-179", oklch: [0.300, 0.096, 140.0], + mood: "moss on wet stone — forest floor at dusk, deep botanical hush", + strategy: "Kept the seed's deep moss green as primary against a near-black surface so the green reads as living shadow, with a pale lichen accent providing the single point of light." }, + { id: "seed-180", oklch: [0.350, 0.110, 140.0], + mood: "moss-darkened apothecary — herbal tinctures in amber glass, pressed botanicals, the deep green of a conservatory at dusk", + strategy: "Near-black bg with a whisper of green undertone lets the seed's deep moss read as luminous foliage; a warm parchment accent provides the apothecary-label counterpoint without breaking the herbal register." }, + { id: "seed-120", oklch: [0.650, 0.100, 140.0], + mood: "moss on weathered stone — quiet botanical garden conservatory at midday", + strategy: "Pure white bg lets the muted sage-green primary read as a considered botanical mark, with a deeper terracotta accent providing earthen counterpoint without breaking the gallery-like restraint." }, + { id: "seed-121", oklch: [0.750, 0.090, 140.0], + mood: "moss garden at Saihō-ji — diffuse green light filtered through wet stone and lichen", + strategy: "Pure near-black bg lets the muted sage-green primary glow like lichen under low light; a warm pale-bone accent acts as the single ray of sun cutting through canopy." }, + { id: "seed-182", oklch: [0.400, 0.106, 150.0], + mood: "moss garden at Saiho-ji — deep cultivated green under wet stone shadow, contemplative and damp", + strategy: "Near-black bg with the faintest cool-green undertone evokes shaded stone; primary holds the seed's moss tone while accent shifts to a lichen-yellow for organic counterpoint without breaking the hush." }, + { id: "seed-157", oklch: [0.550, 0.145, 150.0], + mood: "moss garden at Saiho-ji — damp stone, filtered green light through cedar canopy", + strategy: "Near-black bg with a faint green undertone evokes deep forest shadow; primary holds the seed's verdant register while accent shifts to a pale lichen-cream to mimic light catching moss." }, + { id: "seed-122", oklch: [0.600, 0.158, 150.0], + mood: "forest floor at first light — moss, lichen, and clean morning air", + strategy: "Seed reads as a living, daylight green; surface stays pure white so the green carries the freshness, with a cool teal accent pulling it toward dew rather than earth." }, + { id: "seed-195", oklch: [0.650, 0.150, 145.0], + mood: "Considered horticulture brand — botanical research lab, the green of a healthy stem photographed in clean daylight", + strategy: "Pure white surface lets the seed's vegetal green carry the entire brand voice, paired with a deep forest ink and a warm clay accent for editorial contrast." }, + { id: "seed-183", oklch: [0.350, 0.077, 160.0], + mood: "moss-stained apothecary — deep forest glass, herbal tinctures shelved in low candlelight", + strategy: "Anchored the seed as primary and built a near-black dark surface with whisper-tinted green to evoke aged apothecary glass, letting the green glow rather than shout." }, + { id: "seed-184", oklch: [0.400, 0.087, 160.0], + mood: "deep forest apothecary — moss, bottle glass, and herbal tincture under afternoon light", + strategy: "Seed becomes a botanical-bottle-green primary on pure white, paired with a warm clove-amber accent to evoke herbal pharmacy contrast without tinting the surface." }, + { id: "seed-158", oklch: [0.550, 0.119, 160.0], + mood: "moss on wet stone — forest floor after rain, mineral and quiet", + strategy: "Pure white surface lets the deep mossy green carry the entire mood; accent shifts to a damp slate-teal to sit beside primary like lichen on stone without competing." }, + { id: "seed-159", oklch: [0.600, 0.130, 160.0], + mood: "moss-covered forest apothecary — herbal tinctures in amber glass, eucalyptus shadow", + strategy: "Anchored the green seed in a near-black backdrop so it reads like botanical glassware lit from within, with a warm amber accent pulled across the wheel to evoke tincture bottles against dark wood." }, + { id: "seed-185", oklch: [0.450, 0.086, 170.0], + mood: "weathered copper patina on a Pacific Northwest greenhouse — oxidized teal, glass light, botanical hush", + strategy: "Seed sits as a deep oxidized-teal primary against pure white so the patina reads as pigment, not atmosphere; a rust-copper accent completes the verdigris/oxidation story across the warm-cool axis." }, + { id: "seed-124", oklch: [0.750, 0.080, 170.0], + mood: "sea-glass on a foggy Pacific shoreline — weathered, mineral, quietly oxidized", + strategy: "Seed is a soft desaturated teal-green; pairing it on pure white lets the mineral primary read as patinated copper-glass, with a deeper kelp-toned primary and a rusted coral accent to spark the muted teal against its complement." }, + { id: "seed-160", oklch: [0.550, 0.095, 180.0], + mood: "weathered copper patina on a museum bronze — oxidized teal, conservatorial quiet", + strategy: "Pure near-black gallery surround lets the patina-teal primary glow like a lit artifact, with a warm verdigris-adjacent accent providing the oxidation contrast against the cool seed." }, + { id: "seed-161", oklch: [0.720, 0.100, 188.0], + mood: "climate-tech dashboard — calm verdigris on plain paper, the quiet confidence of an instrument that just works", + strategy: "Seed teal carries the entire mood as a single considered brand color on pure white, with a desaturated copper accent providing warm signal against the cool primary without competing for attention." }, + { id: "seed-186", oklch: [0.450, 0.074, 200.0], + mood: "deep hydrothermal vent — mineral teal under pressure, the cold blue-green of oxidized copper in submerged light", + strategy: "Near-black surface lets the mineral teal glow as if lit from within; accent shifts toward verdigris-copper to suggest patina on submerged metal, while ink stays cool-neutral to keep the register austere rather than aquatic-cute." }, + { id: "seed-125", oklch: [0.650, 0.100, 200.0], + mood: "climate-tech dashboard — calm operational teal, the color of clean water data and atmospheric sensors", + strategy: "Pure white surface lets a single muted-teal primary do all the brand work, with a deeper marine accent providing hierarchy without competing chroma." }, + { id: "seed-126", oklch: [0.750, 0.080, 200.0], + mood: "climate-tech product brand — quiet competence, dashboards for hard infrastructure problems", + strategy: "Hold the seed's muted teal as primary, pair with a sharper cyan-leaning accent for interactive lift, and let a pure white surface do the disappearing act so the brand reads as a tool, not an atmosphere." }, + { id: "seed-162", oklch: [0.550, 0.091, 210.0], + mood: "weathered nautical instrument — patinated brass on oxidized steel, the cool blue-grey of a ship's chronometer at dawn", + strategy: "Pure white surface lets the muted teal-steel primary read as a precise instrument mark, with a warm brass accent providing the single point of patina against clinical white." }, + { id: "seed-163", oklch: [0.450, 0.086, 230.0], + mood: "deep harbor at dusk — weathered nautical instruments, brass dials on oxidized steel", + strategy: "Near-black background with subtle cool tint evokes the marine dusk; primary holds the seed's teal-blue while a warm brass accent creates the instrument-on-steel tension." }, + { id: "seed-164", oklch: [0.550, 0.105, 230.0], + mood: "deep harbor at dawn — cold steel water, fog-muted light, the quiet before the boats leave", + strategy: "Pure near-black bg lets the seed's cold marine blue read as a luminous beacon, while a pale frost-cyan accent evokes diffused dawn light cutting through fog." }, + { id: "seed-127", oklch: [0.650, 0.100, 230.0], + mood: "climate-tech dashboard — atmospheric sensor blue, calm operational clarity", + strategy: "Anchor the seed as a confident mid-blue primary on pure white so the brand color carries all the atmospheric feeling, with a deep navy accent for hierarchy and a soft slate muted for body text." }, + { id: "seed-128", oklch: [0.750, 0.080, 230.0], + mood: "climate-tech dashboard — calm atmospheric data, considered sky-blue", + strategy: "Pure white surface lets the muted sky-blue primary carry the meteorological calm, with a deep-navy accent providing readable weight against the soft primary." }, + { id: "seed-187", oklch: [0.350, 0.078, 240.0], + mood: "deep harbor at blue hour — wet stone, cold steel, the quiet before night fully lands", + strategy: "Near-black architectural bg with a hint of marine chroma lets the seed read as ambient atmosphere rather than UI chrome; a cooler steel accent sits opposite the warmer-shifted primary for navigational clarity." }, + { id: "seed-077", oklch: [0.578, 0.130, 241.7], + mood: "pre-dawn signal tower — cold blue solitude, instruments glowing against the dark", + strategy: "Pure near-black bg lets the seed's cold tower-light blue glow as the sole emotional source, with a frost-cyan accent acting as a secondary indicator light." }, + { id: "seed-188", oklch: [0.400, 0.110, 250.0], + mood: "Linear's considered indigo — the calm authority of a well-built developer tool, blueprint ink on a clean page", + strategy: "Held the seed as a deep indigo primary against pure white so the brand color carries all the gravity; accent shifts to a cooler, brighter cyan-blue to create a crisp hierarchy pair without warming the surface." }, + { id: "seed-165", oklch: [0.450, 0.123, 250.0], + mood: "blueprint room at dusk — drafting table, graphite, civic-engineering blue", + strategy: "Seed is a mid-deep architectural blue with real chroma and no environmental cue, so I stay out of the way with a pure white surface and let the primary do all the talking, pairing it with a burnt-ochre accent for drafting-pencil contrast." }, + { id: "seed-079", oklch: [0.478, 0.136, 251.8], + mood: "twilight cartography — the blue of deep dusk over open water, precise and navigational", + strategy: "Pure white surface lets the seed's oceanic blue act as a single navigational anchor, with a warm amber accent struck across it like a lighthouse beam at dusk." }, + { id: "seed-080", oklch: [0.541, 0.122, 248.2], + mood: "Linear-style considered tool blue — the calm, exact register of a modern engineering app where every pixel is intentional", + strategy: "Pure white surface lets the considered indigo-blue primary carry the entire brand; a deeper navy accent provides hierarchy without warmth, keeping the palette in a single cool family for that focused-software feel" }, + { id: "seed-166", oklch: [0.550, 0.149, 250.0], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and quietly intense", + strategy: "Near-black bg with the faintest cool tint reads like a darkened cockpit; the seed becomes a luminous instrument-blue primary, paired with a warm amber accent that mimics avionics readouts for unmistakable signal contrast." }, + { id: "seed-081", oklch: [0.650, 0.160, 250.0], + mood: "deep-sea research vessel at dawn — instrument glow against cold steel light", + strategy: "Pure near-white bg keeps the palette technical and instrument-like; the seed blue holds as primary while a desaturated steel-cyan accent reads like signal readouts on glass." }, + { id: "seed-082", oklch: [0.742, 0.140, 247.4], + mood: "high-altitude flight deck at dawn — cold cabin instruments glowing against a sky still holding night", + strategy: "Near-black cockpit ground with a faint blue cast lets the seed read as an illuminated instrument; primary holds the seed, accent shifts to cyan for signal/indicator contrast." }, + { id: "seed-210", oklch: [0.360, 0.140, 260.0], + mood: "Linear-style considered tool indigo — late-night focused work, the deep blue of a code editor at 2am where everything else falls away", + strategy: "Pure black bg lets the indigo primary carry all the cognitive-focus weight, with a slightly brighter periwinkle accent for interactive lift — the surface disappears so the tool feels weightless." }, + { id: "seed-189", oklch: [0.400, 0.130, 260.0], + mood: "pre-dawn observatory — cold instrument blue, star-chart precision", + strategy: "Seed becomes the primary on pure black so the deep instrument-blue glows like a calibration light, with a faint cyan accent reading as starlight against the void." }, + { id: "seed-211", oklch: [0.420, 0.161, 260.0], + mood: "Linear's considered indigo — the tool-for-thought blue of focused product work, calm authority without coldness", + strategy: "Hold the seed as a deep indigo primary against pure white, then pair with a slightly warmer, lighter periwinkle accent to create gentle hue separation without breaking the disciplined tool-brand register." }, + { id: "seed-129", oklch: [0.450, 0.150, 260.0], + mood: "pre-dawn observatory — deep cobalt sky just before astronomical twilight, instruments cool to the touch", + strategy: "Near-black surface lets the cobalt seed read as luminous starlight; a single warm amber accent acts as the calibration lamp against the cold blue field." }, + { id: "seed-084", oklch: [0.476, 0.207, 261.2], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and awake", + strategy: "Default B black bg lets the cobalt primary read as a luminous instrument signal, with a cyan accent striking the analogous 'cockpit display' relationship." }, + { id: "seed-085", oklch: [0.681, 0.132, 258.4], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky", + strategy: "Anchored the seed as a luminous primary against a near-black architectural ground, with a warm amber accent acting as the single instrument light cutting through cold blue." }, + { id: "seed-086", oklch: [0.767, 0.106, 255.9], + mood: "Scandinavian winter morning — quiet light through frost, pale sky over snow", + strategy: "Anchored a pure white editorial stage so the seed's cool sky-blue reads as crisp polar light, with a deeper navy primary providing the only saturated weight — like a single dark pine against snow." }, + { id: "seed-083", oklch: [0.340, 0.159, 262.4], + mood: "deep cobalt twilight — the moment after sunset when the sky goes electric blue and city windows start to glow", + strategy: "Pure black stage lets the cobalt seed act as a luminous neon-window glow, with a warm amber accent across the wheel for the lit-window contrast." }, + { id: "seed-212", oklch: [0.360, 0.219, 270.0], + mood: "Linear-grade tooling indigo — considered software for people who care about craft", + strategy: "Anchored the deep indigo seed as primary on a pure white surface so the brand color carries all the weight, with a slightly cooler violet-blue accent for hierarchy without competing chroma." }, + { id: "seed-130", oklch: [0.400, 0.150, 270.0], + mood: "Linear-grade indigo — considered productivity tool, ink on paper, no theatrics", + strategy: "Pure white surface lets a deep cool indigo carry all the brand weight, paired with a slightly warmer violet-blue accent for hierarchy without acid." }, + { id: "seed-213", oklch: [0.411, 0.241, 267.9], + mood: "Linear-style indigo — considered tool surface, the kind of blue-violet that sits behind a developer's keyboard at 11pm without shouting", + strategy: "Pure black canvas lets a saturated indigo primary do all the brand work, with a cooler cyan-violet accent providing UI signal without competing." }, + { id: "seed-131", oklch: [0.450, 0.180, 270.0], + mood: "monastic indigo dusk — vespers light through stained glass, contemplative and severe", + strategy: "Seed becomes a deep indigo primary against pure near-black so the violet reads as luminous stained-glass against architectural shadow, with a cooler iris accent for tonal lift." }, + { id: "seed-088", oklch: [0.476, 0.158, 268.5], + mood: "pre-dawn astronomer's notebook — deep indigo sky just before the stars fade, ink and graphite", + strategy: "Near-black bg with the faintest cool tint to evoke night sky without theatrics; primary holds the seed's indigo, accent shifts to a paler periwinkle for stellar contrast, keeping the palette monochromatic-cool and observational." }, + { id: "seed-196", oklch: [0.530, 0.130, 268.0], + mood: "Linear-style considered tool indigo — the deep-focus blue-violet of a thoughtfully built productivity surface, the color of a well-typeset keyboard shortcut", + strategy: "Pure white bg lets the indigo seed do all the brand work as primary, with a slightly darker, more saturated violet-shifted accent for hierarchy and interactive states — the surface disappears so the brand color reads as the entire identity." }, + { id: "seed-132", oklch: [0.700, 0.120, 270.0], + mood: "Linear-style considered tool indigo — the quiet violet of a focused product workspace, late-afternoon thinking", + strategy: "Pure white surface lets a muted indigo-violet primary and a slightly cooler accent do all the brand work, keeping the register calm and software-like rather than theatrical." }, + { id: "seed-090", oklch: [0.445, 0.206, 279.1], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, not a nightclub", + strategy: "Anchor the seed as a confident product primary on pure white, with a cooler indigo-shift accent that reads as a sibling tool color, so the brand violet does all the emotional work." }, + { id: "seed-133", oklch: [0.500, 0.160, 280.0], + mood: "Linear-adjacent indigo — considered productivity tool, the violet of a thinking workspace", + strategy: "Seed becomes a measured indigo primary on pure white; accent shifts to a cooler blue-violet to create hierarchy without nightclub saturation, letting the brand color do all the emotional work." }, + { id: "seed-094", oklch: [0.533, 0.125, 294.3], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, calm authority for a creative workspace", + strategy: "Pure white canvas lets the indigo-violet primary carry the entire brand voice; accent shifts hue slightly toward blue for a cool, tool-like duotone rather than warm decorative pairing." }, + { id: "seed-137", oklch: [0.700, 0.120, 290.0], + mood: "Linear-adjacent indigo — the considered tool, late-evening focus mode, software made for people who care about craft", + strategy: "Pure black surface lets a single restrained indigo-violet carry the brand, with a cooler periwinkle accent providing UI hierarchy without competing — Vercel/Linear dark-mode discipline." }, + { id: "seed-100", oklch: [0.450, 0.150, 330.0], + mood: "velvet boudoir at last call — bruised orchid and lipstick traces under low lamplight", + strategy: "Pure near-black surface lets a deep magenta-rose primary smolder while a warm peach accent acts like skin-lit lamplight — drama lives in the brand pair, not the room." }, + { id: "seed-103", oklch: [0.650, 0.160, 330.0], + mood: "1980s Memphis boudoir — powder-pink neon humming against lacquered black, lipstick and lacquer", + strategy: "Near-black gallery surface lets the magenta-pink seed read as lit neon; accent shifts to warm coral to create cinematic dichromatic tension without competing chroma." }, + { id: "seed-228", oklch: [0.360, 0.147, 340.0], + mood: "Figma-era creative tool plum — considered productivity software for designers, the inky violet of a serif wordmark on a marketing site", + strategy: "Held the seed as a deep plum primary against pure white so the brand color does the emotional work; paired with a muted rose accent for warmth without breaking the productivity-tool restraint." }, + { id: "seed-107", oklch: [0.500, 0.200, 340.0], + mood: "Figma plum — creative-tool confidence, considered magenta for a modern design product", + strategy: "Pure white surface lets a saturated magenta-plum primary carry all the brand voice, paired with a cooler violet-leaning accent for hierarchy without competing." }, + { id: "seed-198", oklch: [0.600, 0.210, 340.0], + mood: "Figma-era creative tool plum — confident, considered, made for makers", + strategy: "Anchor a saturated plum primary against pure white so the brand color does all the emotional work, with a deeper magenta-rose accent for hierarchy." }, + { id: "seed-112", oklch: [0.754, 0.193, 343.4], + mood: "Figma-era creative tool — confident pink primary doing the brand work on a clean canvas, the way Linear uses indigo or Stripe uses violet", + strategy: "Anchor the seed pink as a saturated brand primary on pure white so the color carries all the personality; pair with a cooler plum accent to give the pink something to push against without competing." }, + { id: "seed-229", oklch: [0.420, 0.163, 350.0], + mood: "considered fintech rose — the deep magenta of a modern product brand (think Stripe-adjacent, but rotated toward berry), confident and current", + strategy: "pure white surface lets a single deep berry-rose primary do all the brand work, paired with a cooler indigo accent for the contrast move you see in modern product marketing" }, + { id: "seed-113", oklch: [0.470, 0.173, 354.8], + mood: "1960s velvet rope nightclub — crushed magenta, low light, cigarette smoke catching a spotlight", + strategy: "Pure black stage so the seed's smoky magenta reads as a single hot spotlight, paired with a cooler violet accent for the second light cue." }, + { id: "seed-114", oklch: [0.570, 0.158, 353.3], + mood: "fin-de-siècle Parisian rose — velvet curtain, theatre program, lipstick blotted on linen", + strategy: "Drop bg to true black so the dusty-rose primary reads as stage-lit silk; accent shifts to a warmer coral-mauve at higher lightness to create gentle hue rotation without breaking the romance." }, + { id: "seed-199", oklch: [0.650, 0.180, 350.0], + mood: "modern fintech rose — the considered pink of a Series B brand mark, confident and current without nostalgia", + strategy: "Pure white surface lets a saturated rose primary do the brand work, paired with a deep plum accent for hierarchy — the Stripe move applied to a pink hue." }, + { id: "seed-115", oklch: [0.636, 0.218, 355.3], + mood: "backstage at a cabaret — velvet rope, lipstick mark on a champagne glass", + strategy: "Seed reads as a saturated stage-light magenta-red; I push it into pure black so the primary glows like a neon sign and the accent (a cold pearl-pink) acts as the spotlight rim — the room is dark, the color does the singing." }, + { id: "seed-230", oklch: [0.650, 0.249, 354.5], + mood: "Modern fintech rose — the considered pink of a contemporary payments brand: confident, alive, and clear-headed", + strategy: "Pure white bg lets a saturated rose-magenta primary carry all the brand energy, paired with a cooler indigo accent for trustworthy contrast — the Stripe move applied to a pink hue." }, + { id: "seed-231", oklch: [0.682, 0.241, 353.2], + mood: "Figma-era creative tool — a confident pink-magenta product brand, the kind a modern design platform uses to feel alive without shouting", + strategy: "Default A pure white bg lets the saturated pink-magenta primary do all the brand work, with a near-complementary cool teal accent for tool-like clarity and a neutral ink for editorial calm" }, + { id: "seed-116", oklch: [0.734, 0.183, 356.8], + mood: "modern beauty brand DTC — Glossier-adjacent pink, confident and current without being saccharine", + 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." }, +]; + +function parseArgs(argv) { + const args = { id: null, from: null }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === '--id' && argv[i + 1]) { args.id = argv[++i]; } + else if (a === '--from' && argv[i + 1]) { args.from = argv[++i]; } + } + return args; +} + +// Hash a key into a stable float in [0, 1) for deterministic weighted picks. +function hashUnit(key) { + const h = crypto.createHash('sha256').update(key).digest(); + return h.readUInt32BE(0) / 0x100000000; +} + +// The curated library is hue-skewed (more reds/oranges than teals/magentas) +// because that's where the source material + taste landed. Left uniform, a +// random pick would land on red ~1/3 of the time. Inverse-frequency weighting +// gives each seed a weight of 1/(count in its 30° hue bucket), so each hue +// ZONE is roughly equally likely to be chosen regardless of how many seeds it +// holds — fair rainbow exposure across runs without pruning the library. +function buildWeights(seeds) { + const bucketCount = {}; + const bucketOf = (s) => Math.floor(((s.oklch[2] % 360) + 360) % 360 / 30); + for (const s of seeds) { const b = bucketOf(s); bucketCount[b] = (bucketCount[b] || 0) + 1; } + const weights = seeds.map((s) => 1 / bucketCount[bucketOf(s)]); + const total = weights.reduce((a, b) => a + b, 0); + return { weights, total }; +} + +function weightedPick(seeds, unit) { + const { weights, total } = buildWeights(seeds); + let target = unit * total; + for (let i = 0; i < seeds.length; i++) { + target -= weights[i]; + if (target < 0) return seeds[i]; + } + return seeds[seeds.length - 1]; +} + +function pickSeed(seeds, { id, from }) { + if (id) { + const found = seeds.find(s => s.id === id); + if (!found) { console.error(`no seed with id "${id}"`); process.exit(2); } + return found; + } + const envFrom = process.env.IMPECCABLE_PALETTE_SEED; + const key = from || envFrom; + const unit = key ? hashUnit(key) : Math.random(); + return weightedPick(seeds, unit); +} + +function fmtOklch([L, C, H]) { + return `oklch(${L.toFixed(3)} ${C.toFixed(3)} ${H.toFixed(1)})`; +} + +function hueWord(H) { + if (H < 15 || H >= 345) return 'pure red'; + if (H < 35) return 'warm red / crimson'; + if (H < 55) return 'warm coral / burnt orange'; + if (H < 80) return 'orange / honey'; + if (H < 105) return 'warm amber / honey-gold'; + if (H < 135) return 'yellow-green / olive'; + if (H < 170) return 'green'; + if (H < 200) return 'teal'; + if (H < 230) return 'sky blue'; + if (H < 265) return 'cobalt / indigo'; + if (H < 295) return 'violet / purple'; + if (H < 330) return 'magenta / pink'; + return 'deep pink / rose'; +} + +// --------------------------------------------------------------- + +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}` : ''; + +// --------------------------------------------------------------- +// Fat tool-exit response — what the model sees on stdout. +// --------------------------------------------------------------- + +process.stdout.write(`BRAND SEED · ${seed.id} + +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. + +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. + +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é. + + 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 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. + + 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. + +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): + + - 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. + +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. + +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. +`); diff --git a/.gemini/skills/impeccable/scripts/pin.mjs b/.gemini/skills/impeccable/scripts/pin.mjs index ba02783fb..320d98ce1 100644 --- a/.gemini/skills/impeccable/scripts/pin.mjs +++ b/.gemini/skills/impeccable/scripts/pin.mjs @@ -27,7 +27,7 @@ const HARNESS_DIRS = [ // Valid sub-command names const VALID_COMMANDS = [ - 'craft', 'teach', 'extract', 'document', 'shape', + 'craft', 'init', 'extract', 'document', 'shape', 'critique', 'audit', 'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live', 'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive', diff --git a/.github/skills/impeccable/SKILL.md b/.github/skills/impeccable/SKILL.md index 28fc885b6..2ea882afe 100644 --- a/.github/skills/impeccable/SKILL.md +++ b/.github/skills/impeccable/SKILL.md @@ -1,95 +1,90 @@ --- name: impeccable description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks. -version: 3.1.1 +version: 3.5.0 user-invocable: true -argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · teach|document|extract|live] [target]" -license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution. +argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]" +license: Apache 2.0 --- Designs and iterates production-grade frontend interfaces. Real working code, committed design choices, exceptional craft. ## Setup -Before any design work or file edits: +You MUST do these steps before proceeding: -1. Load context (PRODUCT.md / DESIGN.md) via the loader script. -2. Identify the register and load the matching register reference (brand.md or product.md). -3. **If the user invoked a sub-command (e.g. `craft`, `shape`, `audit`), load its reference file too.** This is non-negotiable: `craft` without `craft.md` loaded means you'll skip the shape-and-confirm step the user expects. +1. Run `node .github/skills/impeccable/scripts/context.mjs` once per session. If you've already seen its output in this conversation, do not re-run it. The script either prints the project's PRODUCT.md (and DESIGN.md when present) as a markdown block, or tells you it's missing. Follow whatever it prints. **If it reports `NO_PRODUCT_MD`, stop and follow `reference/init.md` before doing anything else.** If the output ends with an `UPDATE_AVAILABLE` directive, follow it (ask the user once about updating, then continue). It never blocks the current task. +2. If the user invoked a sub-command (`craft`, `shape`, `audit`, `polish`, ...), you MUST read `reference/.md` next. Non-optional. The reference defines the command's flow; without it you will skip steps the user expects. +3. Familiarize yourself with any existing design system, conventions, and components in the code. Read at least one project file (CSS / tokens / theme / a representative component or page). **Required even when you've loaded a sub-command reference in step 2.** Don't reinvent the wheel; use what's there when it works, branch out when the UX wins. +4. Read the matching register reference. **This is non-optional; skipping it produces generic output.** If the project is marketing, a landing page, a campaign, long-form content, or a portfolio (design IS the product), read `reference/brand.md`. If it is app UI, admin, a dashboard, or a tool (design SERVES the product), read `reference/product.md`. Pick by first match: (1) task cue ("landing page" vs "dashboard"); (2) surface in focus (the page, file, or route being worked on); (3) `register` field in PRODUCT.md. +5. **If the project is brand-new (no existing CSS tokens / theme / committed brand colors found in step 3)**, run `node .github/skills/impeccable/scripts/palette.mjs` to receive a brand seed color and composition guidance. This is the anchor for your primary brand color. Compose the rest of the palette (bg, surface, ink, accent, muted) around it per the script's instructions. Use OKLCH throughout. **Skip this step only if step 3 found committed brand colors in existing tokens; in that case identity-preservation wins.** -Skipping these produces generic output that ignores the project. +## Design guidance -### 1. Context gathering +Produce ready-to-ship, production-grade code, not prototypes or starting points. Take no shortcuts unless the user asks for them (when in doubt, ask). Don't stop until arriving at a complete implementation (beautiful, responsive, fast, precise, bug-free, on brand). You take attention to detail seriously: every page, section or component crafted is battle tested using the tools available to you (browser screenshotting, computer use, etc). the model is capable of extraordinary work. Don't hold back. -Two files, case-insensitive. The loader looks at the project root by default and falls back to `.agents/context/` and `docs/` if the root is clean. Override with `IMPECCABLE_CONTEXT_DIR=path/to/dir` (absolute or relative to cwd). +### General rules -- **PRODUCT.md**: required. Users, brand, tone, anti-references, strategic principles. -- **DESIGN.md**: optional, strongly recommended. Colors, typography, elevation, components. +#### Color -Load both in one call: +- **Verify contrast.** Body text must hit ≥4.5:1 against its background; large text (≥18px or bold ≥14px) needs ≥3:1. Placeholder text needs the same 4.5:1, not the muted-gray default. The most common failure: muted gray body text on a tinted near-white. If the contrast is even close, bump the body color toward the ink end of the ramp; light gray "for elegance" is the single biggest reason AI designs feel hard to read. +- Gray text on a colored background looks washed out. Use a darker shade of the background's own hue, or a transparency of the text color. -```bash -node .github/skills/impeccable/scripts/load-context.mjs -``` +#### Typography -Consume the full JSON output. Never pipe through `head`, `tail`, `grep`, or `jq`. The output's `contextDir` field tells you where the files were resolved from. +- Cap body line length at 65–75ch. +- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales. +- Cap font-family count at 3 (display + body + optional mono). More than 3 reads as indecision, not richness. One well-tuned family with weight contrast usually beats three competing typefaces. +- Don't pair fonts that are similar but not identical (two geometric sans-serifs, two humanist sans-serifs). Pair on a contrast axis (serif + sans, geometric + humanist) or use one family in multiple weights. +- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. +- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. +- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". +- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. -If the output is already in this session's conversation history, don't re-run. Exceptions requiring a fresh load: you just ran `/impeccable teach` or `/impeccable document` (they rewrite the files), or the user manually edited one. +#### Layout -`/impeccable live` already warms context via `live.mjs`. If you've run `live.mjs`, don't also run `load-context.mjs` this session. +- Vary spacing for rhythm. +- Cards are the lazy answer. Use them only when they're truly the best affordance. Nested cards are always wrong. +- Flexbox for 1D, Grid for 2D. Don't default to Grid when `flex-wrap` would be simpler. +- For responsive grids without breakpoints: `repeat(auto-fit, minmax(280px, 1fr))`. +- Build a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip). Never arbitrary values like 999 or 9999. -If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `/impeccable teach`, then resume the user's original task with the fresh context. If the original task was `/impeccable craft`, resume into `/impeccable shape` before any implementation work. +#### Motion +- Motion should be intentional, and not be an afterthought. consider it as part of the build. +- Don't animate CSS layout properties unless truly needed. +- Ease out with exponential curves (ease-out-quart / quint / expo). No bounce, no elastic. +- Use libraries for more advanced motion needs (e.g. motion, gsap, anime.js, lenis etc) +- Reduced motion is not optional. Every animation needs a `@media (prefers-reduced-motion: reduce)` alternative: typically a crossfade or instant transition. +- Staggering the items within one list is legitimate. The tell is the uniform reflex (one identical entrance applied to every section), not motion itself; each reveal should fit what it reveals. Suppressing the reflex is never a reason to ship a page with no motion at all. +- Reveal animations must enhance an already-visible default. Don't gate content visibility on a class-triggered transition; transitions pause on hidden tabs and headless renderers, so the reveal never fires and the section ships blank. +- Premium motion materials are not just transform/opacity. Blur, backdrop-filter, clip-path, mask, and shadow/glow are part of the palette when they materially improve the effect and stay smooth. -If DESIGN.md is missing: nudge once per session (*"Run `/impeccable document` for more on-brand output"*), then proceed. +#### Interaction -### 2. Register +- Dropdowns rendered with `position: absolute` inside an `overflow: hidden` or `overflow: auto` container will be clipped. Use the native `` / popover API, `position: fixed`, or a portal to escape the stacking context. -Every design task is **brand** (marketing, landing, campaign, long-form content, portfolio: design IS the product) or **product** (app UI, admin, dashboard, tool: design SERVES the product). +### Copy -Identify before designing. Priority: (1) cue in the task itself ("landing page" vs "dashboard"); (2) the surface in focus (the page, file, or route being worked on); (3) `register` field in PRODUCT.md. First match wins. +- Every word earns its place. No restated headings, no intros that repeat the title. +- **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`. +- **No aphoristic-cadence body copy as a default voice.** Don't fall into the rhythm of "serious statement, then punchy short negation" as the page's recurring voice. If three or more section copy blocks on the page land on a short rebuttal-shaped sentence, rewrite. Specific, not aphoristic. +- **No marketing buzzwords.** The streamline / empower / supercharge / leverage / unleash / transform / seamless / world-class / enterprise-grade / next-generation / cutting-edge / game-changer / mission-critical family of phrases. Pick a specific noun and a verb that describes what the product literally does. +- Button labels: verb + object. "Save changes" beats "OK"; "Delete project" beats "Yes". The label should say what will happen. +- Link text needs standalone meaning. "View pricing plans" beats "Click here"; screen readers announce links out of context. -If PRODUCT.md lacks the `register` field (legacy), infer it once from its "Users" and "Product Purpose" sections, then cache the inferred value for the session. Suggest the user run `/impeccable teach` to add the field explicitly. +### New projects only (when no prior work exists) -Load the matching reference: [reference/brand.md](reference/brand.md) or [reference/product.md](reference/product.md). The shared design laws below apply to both. +#### Color & Theme -## Shared design laws - -Apply to every design, both registers. Match implementation complexity to the aesthetic vision: maximalism needs elaborate code, minimalism needs precision. Interpret creatively. Vary across projects; never converge on the same choices. the model is capable of extraordinary work. Don't hold back. - -### Color - -- Use OKLCH. Reduce chroma as lightness approaches 0 or 100; high chroma at extremes looks garish. -- Never use `#000` or `#fff`. Tint every neutral toward the brand hue (chroma 0.005–0.01 is enough). +- Use OKLCH. +- **The cream / sand / beige body bg is the saturated AI default of 2026.** The whole warm-neutral band (OKLCH L 0.84-0.97, C < 0.06, hue 40-100) reads as cream/sand/paper/parchment regardless of what you call it. Token names like `--paper`, `--cream`, `--sand`, `--bone`, `--flour`, `--linen`, `--parchment`, `--wheat`, `--biscuit`, `--ivory` are tells in themselves. If the brief is "warm, traditional, family-coastal-Italian" or "magazine-warm" or "editorial-restraint", DO NOT translate that into a near-white warm-tinted bg; that's the AI move. Pick: (a) a saturated brand color as the body (terracotta, oxblood, deep ochre, near-black), (b) a true off-white at chroma 0 (or chroma toward the brand's own hue, not toward warmth-by-default), or (c) a darker mid-tone tinted neutral that's clearly the brand's own. "Warmth" in the brand is carried by accent + typography + imagery, not by body bg. +- Tinted neutrals: add 0.005–0.015 chroma toward the brand's hue. Don't default-tint toward warm or cool "because the brand feels that way"; that's the cross-project monoculture move. +- When picking a theme: Dark vs. light is never a default. Not dark "because tools look cool dark." Not light "to be safe.".Before choosing, write one sentence of physical scene: who uses this, where, under what ambient light, in what mood. If the sentence doesn't force the answer, it's not concrete enough. Add detail until it does. - Pick a **color strategy** before picking colors. Four steps on the commitment axis: - **Restrained**: tinted neutrals + one accent ≤10%. Product default; brand minimalism. - **Committed**: one saturated color carries 30–60% of the surface. Brand default for identity-driven pages. - **Full palette**: 3–4 named roles, each used deliberately. Brand campaigns; product data viz. - **Drenched**: the surface IS the color. Brand heroes, campaign pages. -- The "one accent ≤10%" rule is Restrained only. Committed / Full palette / Drenched exceed it on purpose. Don't collapse every design to Restrained by reflex. - -### Theme - -Dark vs. light is never a default. Not dark "because tools look cool dark." Not light "to be safe." - -Before choosing, write one sentence of physical scene: who uses this, where, under what ambient light, in what mood. If the sentence doesn't force the answer, it's not concrete enough. Add detail until it does. - -"Observability dashboard" does not force an answer. "SRE glancing at incident severity on a 27-inch monitor at 2am in a dim room" does. Run the sentence, not the category. - -### Typography - -- Cap body line length at 65–75ch. -- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales. - -### Layout - -- Vary spacing for rhythm. Same padding everywhere is monotony. -- Cards are the lazy answer. Use them only when they're truly the best affordance. Nested cards are always wrong. -- Don't wrap everything in a container. Most things don't need one. - -### Motion - -- Don't animate CSS layout properties. -- Ease out with exponential curves (ease-out-quart / quint / expo). No bounce, no elastic. ### Absolute bans @@ -100,12 +95,9 @@ Match-and-refuse. If you're about to write any of these, rewrite the element wit - **Glassmorphism as default.** Blurs and glass cards used decoratively. Rare and purposeful, or nothing. - **The hero-metric template.** Big number, small label, supporting stats, gradient accent. SaaS cliché. - **Identical card grids.** Same-sized cards with icon + heading + text, repeated endlessly. -- **Modal as first thought.** Modals are usually laziness. Exhaust inline / progressive alternatives first. - -### Copy - -- Every word earns its place. No restated headings, no intros that repeat the title. -- **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`. +- **Tiny uppercase tracked eyebrow above every section.** The 2023-era kicker (small all-caps text with wide tracking, "ABOUT" "PROCESS" "PRICING" above each heading) is now the saturated AI scaffold; it appears on 55-95% of generations regardless of brief, which is the definition of a tell. One named kicker as a deliberate brand system is voice; an eyebrow on every section is AI grammar. Choose a different cadence. +- **Numbered section markers as default scaffolding (01 / 02 / 03).** Putting `01 · About / 02 · Process / 03 · Pricing` above every section is the eyebrow trope one tier deeper: reach for it because "landing pages do this" and you're scaffolding by reflex. Numbers earn their place when the section actually IS a sequence (a real 3-step process, an ordered flow, a typed timeline) and the order carries information the reader needs. One deliberate numbered sequence on one page is voice; numbered eyebrows on every section across the site is AI grammar. +- **Text that overflows its container.** Long heading words plus large clamp scales plus narrow grids cause headline overflow on tablet/mobile. Test the heading copy at every breakpoint; if it overflows, reduce the clamp max or rewrite the copy. The viewport is part of the design. ### The AI slop test @@ -113,7 +105,7 @@ If someone could look at this interface and say "AI made that" without doubt, it **Category-reflex check.** Run at two altitudes; the second one catches what the first one misses. -- **First-order:** if someone could guess the theme + palette from the category alone ("observability → dark blue", "healthcare → white + teal", "finance → navy + gold", "crypto → neon on black"), it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain. +- **First-order:** if someone could guess the theme + palette from the category alone, it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain. - **Second-order:** if someone could guess the aesthetic family from category-plus-anti-references ("AI workflow tool that's not SaaS-cream → editorial-typographic", "fintech that's not navy-and-gold → terminal-native dark mode"), it's the trap one tier deeper. The first reflex was avoided; the second wasn't. Rework until both answers are not obvious. The brand register's [reflex-reject aesthetic lanes](reference/brand.md) list catches the currently-saturated families. ## Commands @@ -122,7 +114,7 @@ If someone could look at this interface and say "AI made that" without doubt, it |---|---|---|---| | `craft [feature]` | Build | Shape, then build a feature end-to-end | [reference/craft.md](reference/craft.md) | | `shape [feature]` | Build | Plan UX/UI before writing code | [reference/shape.md](reference/shape.md) | -| `teach` | Build | Set up PRODUCT.md and DESIGN.md context | [reference/teach.md](reference/teach.md) | +| `init` | Build | Set up project context: PRODUCT.md, DESIGN.md, live config, next steps | [reference/init.md](reference/init.md) | | `document` | Build | Generate DESIGN.md from existing project code | [reference/document.md](reference/document.md) | | `extract [target]` | Build | Pull reusable tokens and components into design system | [reference/extract.md](reference/extract.md) | | `critique [target]` | Evaluate | UX design review with heuristic scoring | [reference/critique.md](reference/critique.md) | @@ -150,11 +142,14 @@ Plus two management commands: `pin ` and `unpin `, detailed be 1. **No argument**: render the table above as the user-facing command menu, grouped by category. Ask what they'd like to do. 2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. -3. **First word doesn't match**: general design invocation. Apply the setup steps, shared design laws, and the loaded register reference, using the full argument as context. +3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. +4. **No clear command match**: general design invocation. Apply the setup steps, the General rules, and the loaded register reference, using the full argument as context. Setup (context gathering, register) is already loaded by then; sub-commands don't re-invoke `/impeccable`. -If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `teach` as a blocker, finish teach, refresh context, then resume the original command and target. +If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `init` as a blocker, finish init, refresh context, then resume the original command and target. + +`teach` is a deprecated alias for `init`: if the user types it, load [reference/init.md](reference/init.md) and proceed as if they ran `init`. ## Pin / Unpin diff --git a/.github/skills/impeccable/reference/adapt.md b/.github/skills/impeccable/reference/adapt.md index 9d0673ea2..5af7606cb 100644 --- a/.github/skills/impeccable/reference/adapt.md +++ b/.github/skills/impeccable/reference/adapt.md @@ -188,3 +188,124 @@ Test thoroughly across contexts: - **Slow connections**: Test on throttled network When the adaptation feels native to each context, hand off to `/impeccable polish` for the final pass. + +--- + +## Reference Material + +The sections below were previously `responsive-design.md` and live inline now so the adapt flow has its deep responsive reference in one place. + +### Responsive Design + +#### Mobile-First: Write It Right + +Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first. + +#### Breakpoints: Content-Driven + +Don't chase device sizes; let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints. + +#### Detect Input Method, Not Just Screen Size + +**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard. Use pointer and hover queries: + +```css +/* Fine pointer (mouse, trackpad) */ +@media (pointer: fine) { + .button { padding: 8px 16px; } +} + +/* Coarse pointer (touch, stylus) */ +@media (pointer: coarse) { + .button { padding: 12px 20px; } /* Larger touch target */ +} + +/* Device supports hover */ +@media (hover: hover) { + .card:hover { transform: translateY(-2px); } +} + +/* Device doesn't support hover (touch) */ +@media (hover: none) { + .card { /* No hover state - use active instead */ } +} +``` + +**Critical**: Don't rely on hover for functionality. Touch users can't hover. + +#### Safe Areas: Handle the Notch + +Modern phones have notches, rounded corners, and home indicators. Use `env()`: + +```css +body { + padding-top: env(safe-area-inset-top); + padding-bottom: env(safe-area-inset-bottom); + padding-left: env(safe-area-inset-left); + padding-right: env(safe-area-inset-right); +} + +/* With fallback */ +.footer { + padding-bottom: max(1rem, env(safe-area-inset-bottom)); +} +``` + +**Enable viewport-fit** in your meta tag: +```html + +``` + +#### Responsive Images: Get It Right + +##### srcset with Width Descriptors + +```html +Hero image +``` + +**How it works**: +- `srcset` lists available images with their actual widths (`w` descriptors) +- `sizes` tells the browser how wide the image will display +- Browser picks the best file based on viewport width AND device pixel ratio + +##### Picture Element for Art Direction + +When you need different crops/compositions (not just resolutions): + +```html + + + + ... + +``` + +#### Layout Adaptation Patterns + +**Navigation**: Three stages: hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `
/` for content that can collapse on mobile. + +#### Testing: Don't Trust DevTools Alone + +DevTools device emulation is useful for layout but misses: + +- Actual touch interactions +- Real CPU/memory constraints +- Network latency patterns +- Font rendering differences +- Browser chrome/keyboard appearances + +**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators. + +--- + +**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful. diff --git a/.github/skills/impeccable/reference/animate.md b/.github/skills/impeccable/reference/animate.md index 20aeea659..15d9f9080 100644 --- a/.github/skills/impeccable/reference/animate.md +++ b/.github/skills/impeccable/reference/animate.md @@ -6,7 +6,7 @@ Add motion that conveys state, gives feedback, and clarifies hierarchy. Cut moti ## Register -Brand: orchestrated page-load sequences, staggered reveals, scroll-driven animation. Motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions. +Brand: motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions. The saturated AI default is fade-and-rise reveals on every scrolled section; that's a tell, not a choreography. Reserve scroll-triggered motion for moments that earn it. Product: 150–250 ms on most transitions. Motion conveys state: feedback, reveal, loading, transitions between views. No page-load choreography; users are in a task and won't wait for it. @@ -49,10 +49,11 @@ Create a purposeful animation plan: Add motion systematically across these categories: ### Entrance Animations -- **Page load choreography**: Stagger element reveals (100-150ms delays), fade + slide combinations - **Hero section**: Dramatic entrance for primary content (scale, parallax, or creative effects) -- **Content reveals**: Scroll-triggered animations using intersection observer - **Modal/drawer entry**: Smooth slide + fade, backdrop fade, focus management +- **List rhythm**: Sibling stagger is legitimate for cards-in-a-grid or list-items-appearing. Whole-section fade-on-scroll is not a list and is not legitimate. Cap total stagger time: 10 items at 50ms each = 500ms total. For more items, reduce per-item delay or cap the staggered count. + + Use CSS custom properties for clean stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"`, `style="--i: 1"`, etc. on each item. ### Micro-interactions - **Button feedback**: @@ -97,11 +98,14 @@ Use appropriate techniques for each animation: ### Timing & Easing -**Durations by purpose:** -- **100-150ms**: Instant feedback (button press, toggle) -- **200-300ms**: State changes (hover, menu open) -- **300-500ms**: Layout changes (accordion, modal) -- **500-800ms**: Entrance animations (page load) +**Duration: the 100/300/500 rule.** Timing matters more than easing for "feels right": + +| Duration | Use Case | Examples | +|----------|----------|----------| +| **100–150ms** | Instant feedback | Button press, toggle, color change | +| **200–300ms** | State changes | Menu open, tooltip, hover state | +| **300–500ms** | Layout changes | Accordion, modal, drawer | +| **500–800ms** | Entrance animations | Page load, hero reveal | **Easing curves (use these, not CSS defaults):** ```css @@ -134,13 +138,35 @@ Use appropriate techniques for each animation: - GSAP for complex sequences ``` +### Motion Materials + +Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties. Match material to effect: + +- **Transform / opacity**: movement, press feedback, simple reveals, list choreography +- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances +- **Clip-path / masks**: wipes, reveals, editorial cropping, product-like transitions +- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state +- **Grid-template-rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly + +The hard rule isn't "transform and opacity only." It's: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify smoothness in-browser on target viewports. + ### Performance -- **Motion materials**: Use transform/opacity for reliable movement, but use blur, filters, masks, shadows, and color shifts when they materially improve the effect - **Layout safety**: Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins) -- **will-change**: Add sparingly for known expensive animations +- **will-change**: Add sparingly for known expensive animations only (e.g. on `:hover` or an `.animating` class), never preemptively across the whole page +- **Scroll triggers**: Use Intersection Observer instead of scroll event listeners; unobserve after the animation fires once - **Bound expensive effects**: Keep blur/filter/shadow areas small or isolated, use `contain` where appropriate - **Monitor FPS**: Ensure 60fps on target devices +### Perceived Performance + +Nobody cares how fast your site *is*, only how fast it feels. The 80ms threshold: anything under ~80ms feels instant because our brains buffer sensory input for that long to synchronize perception. Target this for micro-interactions. + +- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening. +- **Early completion**: Show content progressively, don't wait for everything (progressive images, streaming HTML, skeleton fade-ins). +- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Use for low-stakes actions (likes, follows). Avoid for payments or destructive operations. +- **Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances. +- **Caution**: Too-fast responses can decrease perceived value for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening. + ### Accessibility ```css @media (prefers-reduced-motion: reduce) { diff --git a/.github/skills/impeccable/reference/bolder.md b/.github/skills/impeccable/reference/bolder.md index ff0ef2f14..30d4e852c 100644 --- a/.github/skills/impeccable/reference/bolder.md +++ b/.github/skills/impeccable/reference/bolder.md @@ -50,7 +50,7 @@ Create a strategy to increase impact while maintaining coherence: Systematically increase impact across these dimensions: ### Typography Amplification -- **Replace generic fonts**: Swap system fonts for distinctive choices (see the parent skill's typography guidelines and [typography.md](typography.md) for inspiration) +- **Replace generic fonts**: Swap system fonts for distinctive choices (see the parent skill's typography guidelines and the [Reference Material section of typeset.md](typeset.md#reference-material) for inspiration) - **Extreme scale**: Create dramatic size jumps (3x-5x differences, not 1.5x) - **Weight contrast**: Pair 900 weights with 200 weights, not 600 with 400 - **Unexpected choices**: Variable fonts, display fonts for headlines, condensed/extended widths, monospace as intentional accent (not as lazy "dev tool" default) @@ -78,10 +78,10 @@ Systematically increase impact across these dimensions: - **Custom elements**: Illustrative elements, custom icons, decorative details that reinforce brand ### Motion & Animation -- **Entrance choreography**: Staggered, dramatic page load animations with 50-100ms delays -- **Scroll effects**: Parallax, reveal animations, scroll-triggered sequences -- **Micro-interactions**: Satisfying hover effects, click feedback, state changes -- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic, which cheapen the effect) +- **Hero moment**: One signature entrance, once. Not on every visit and not on every section. +- **Micro-interactions**: Satisfying hover effects, click feedback, state changes. +- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic, which cheapen the effect). +- **Bolder ≠ scroll-fade-rise on every section.** That's the saturated AI default, the opposite of bold. ### Composition Boldness - **Hero moments**: Create clear focal points with dramatic treatment diff --git a/.github/skills/impeccable/reference/brand.md b/.github/skills/impeccable/reference/brand.md index 3d83a1cdc..194514e95 100644 --- a/.github/skills/impeccable/reference/brand.md +++ b/.github/skills/impeccable/reference/brand.md @@ -43,17 +43,10 @@ The reflex-reject lists apply to **new design choices**. When the existing brand ### Pairing and voice -Distinctive + refined is the goal. The specific shape depends on the brand: - -- **Editorial / long-form / luxury**: display serif + sans body (a magazine shape). -- **Tech / dev tools / fintech**: one committed sans, usually; custom-tight tracking, strong weight contrast inside a single family. -- **Consumer / food / travel**: warmer pairings, often a humanist sans plus a script or display serif. -- **Creative studios / agencies**: rule-breaking welcome. Mono-only, or display-only, or custom-drawn type as voice. +Distinctive + refined is the goal. The specific shape depends on the brand, not on the brand's category. A category ("restaurant", "dev tool", "magazine", "fintech") is not a recipe; treating it as one is the first-order reflex SKILL.md warns against. Two families minimum is the rule *only* when the voice needs it. A single well-chosen family with committed weight/size contrast is stronger than a timid display+body pair. -Vary across projects. If the last brief was a serif-display landing page, this one isn't. - ### Scale Modular scale, fluid `clamp()` for headings, ≥1.25 ratio between steps. Flat scales (1.1× apart) read as uncommitted. @@ -74,8 +67,7 @@ Brand surfaces have permission for Committed, Full palette, and Drenched strateg - Asymmetric compositions are one option. Break the grid intentionally for emphasis. - Fluid spacing with `clamp()` that breathes on larger viewports. Vary for rhythm: generous separations, tight groupings. -- Alternative: a strict, visible grid as the voice (brutalist / Swiss / tech-spec aesthetics). Either asymmetric or rigorously-gridded can be "designed"; the failure mode is splitting the difference into a generic centered stack. -- Don't default to centering everything. Left-aligned with asymmetric layouts feels more designed; a strict grid reads as confident structure. A centered-stack hero with icon-title-subtitle cards reads as template. +- For image-led briefs (hotels, restaurants, magazines, photography), full-bleed hero imagery with overlaid menu and centered headline is a canonical move; let the photograph be the design. - When cards ARE the right affordance, use `grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` for breakpoint-free responsiveness. ## Imagery @@ -93,8 +85,7 @@ Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landin ## Motion -- One well-orchestrated page-load with staggered reveals beats scattered micro-interactions, when the brand invites it. Tech-minimal brands often skip entrance motion entirely; the restraint is the voice. -- For collapsing/expanding sections, transition `grid-template-rows` rather than `height`. +- One well-orchestrated page-load beats scattered micro-interactions, when the brand invites it. Some brands skip entrance motion entirely; the restraint is the voice. ## Brand bans (on top of the shared absolute bans) @@ -111,8 +102,7 @@ Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landin Brand can afford things product can't. Take them. -- Ambitious first-load motion. Reveals, scroll-triggered transitions, typographic choreography. +- Ambitious first-load motion. Reveals and typographic choreography that earn their place; not fade-on-scroll for every section. - Single-purpose viewports. One dominant idea per fold, long scroll, deliberate pacing. -- Typographic risk. Enormous display type, unexpected italic cuts, mixed cases, hand-drawn headlines, a single oversize word as a hero. - Unexpected color strategies. Palette IS voice; a calm brand and a restless brand should not share palette mechanics. - Art direction per section. Different sections can have different visual worlds if the narrative demands it. Consistency of voice beats consistency of treatment. diff --git a/.github/skills/impeccable/reference/clarify.md b/.github/skills/impeccable/reference/clarify.md index 5f7d420f4..00ee978c3 100644 --- a/.github/skills/impeccable/reference/clarify.md +++ b/.github/skills/impeccable/reference/clarify.md @@ -172,3 +172,117 @@ Test that copy improvements work: - **Tone**: Is it appropriate for the situation? When the copy reads cleanly, hand off to `/impeccable polish` for the final pass. + +--- + +## Reference Material + +The sections below were previously `ux-writing.md` and live inline now so the clarify flow has its deep UX-writing reference in one place. + +### UX Writing + +#### The Button Label Problem + +**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns: + +| Bad | Good | Why | +|-----|------|-----| +| OK | Save changes | Says what will happen | +| Submit | Create account | Outcome-focused | +| Yes | Delete message | Confirms the action | +| Cancel | Keep editing | Clarifies what "cancel" means | +| Click here | Download PDF | Describes the destination | + +**For destructive actions**, name the destruction: +- "Delete" not "Remove" (delete is permanent, remove implies recoverable) +- "Delete 5 items" not "Delete selected" (show the count) + +#### Error Messages: The Formula + +Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input". + +##### Error Message Templates + +| Situation | Template | +|-----------|----------| +| **Format error** | "[Field] needs to be [format]. Example: [example]" | +| **Missing required** | "Please enter [what's missing]" | +| **Permission denied** | "You don't have access to [thing]. [What to do instead]" | +| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." | +| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" | + +##### Don't Blame the User + +Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date". + +#### Empty States Are Opportunities + +Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items". + +#### Voice vs Tone + +**Voice** is your brand's personality, consistent everywhere. +**Tone** adapts to the moment. + +| Moment | Tone Shift | +|--------|------------| +| Success | Celebratory, brief: "Done! Your changes are live." | +| Error | Empathetic, helpful: "That didn't work. Here's what to try..." | +| Loading | Reassuring: "Saving your work..." | +| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | + +**Never use humor for errors.** Users are already frustrated. Be helpful, not cute. + +#### Writing for Accessibility + +**Link text** must have standalone meaning: "View pricing plans" not "Click here". **Alt text** describes information, not the image: "Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context. + +#### Writing for Translation + +##### Plan for Expansion + +German text is ~30% longer than English. Allocate space: + +| Language | Expansion | +|----------|-----------| +| German | +30% | +| French | +20% | +| Finnish | +30-40% | +| Chinese | -30% (fewer chars, but same width) | + +##### Translation-Friendly Patterns + +Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear. + +#### Consistency: The Terminology Problem + +Pick one term and stick with it: + +| Inconsistent | Consistent | +|--------------|------------| +| Delete / Remove / Trash | Delete | +| Settings / Preferences / Options | Settings | +| Sign in / Log in / Enter | Sign in | +| Create / Add / New | Create | + +Build a terminology glossary and enforce it. Variety creates confusion. + +#### Avoid Redundant Copy + +If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well. + +#### Loading States + +Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress. + +#### Confirmation Dialogs: Use Sparingly + +Most confirmation dialogs are design failures; consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No"). + +#### Form Instructions + +Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking. + +--- + +**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors. diff --git a/.github/skills/impeccable/reference/codex.md b/.github/skills/impeccable/reference/codex.md index f40ec746f..d563af95b 100644 --- a/.github/skills/impeccable/reference/codex.md +++ b/.github/skills/impeccable/reference/codex.md @@ -23,8 +23,8 @@ Before generating anything, run a brief direction conversation grounded in the s Ask **2-3 targeted questions** about visual lane, color strategy, atmosphere, and named anchor references. Don't enumerate generic menus; tie each question to the shape brief's answers. Example shape-grounded questions: -- "Brief says 'editorial restraint, Klim-adjacent.' Are we closer to a quiet specimen page or a magazine-spread feel with hero imagery?" -- "Palette strategy from shape was 'Committed.' Want it warm-grounded (deep oxblood + cream) or cool-grounded (slate + paper white)?" +- "Brief says 'specimen-page restraint.' Are we closer to a quiet typographic page or a wider editorial spread with hero imagery?" +- "Palette strategy from shape was 'Committed.' Which one color carries the surface (a brand-driven pick rather than a default warm-or-cool framing)? (And no, the answer isn't a cream/sand body bg; that's the saturated AI default.)" **STOP and wait for answers.** These pin the palette before any pixel gets generated. Do not proceed to Step B until the user has responded. diff --git a/.github/skills/impeccable/reference/cognitive-load.md b/.github/skills/impeccable/reference/cognitive-load.md deleted file mode 100644 index 48f8ad58b..000000000 --- a/.github/skills/impeccable/reference/cognitive-load.md +++ /dev/null @@ -1,106 +0,0 @@ -# Cognitive Load Assessment - -Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload. - ---- - -## Three Types of Cognitive Load - -### Intrinsic Load: The Task Itself -Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it. - -**Manage it by**: -- Breaking complex tasks into discrete steps -- Providing scaffolding (templates, defaults, examples) -- Progressive disclosure: show what's needed now, hide the rest -- Grouping related decisions together - -### Extraneous Load: Bad Design -Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste. - -**Common sources**: -- Confusing navigation that requires mental mapping -- Unclear labels that force users to guess meaning -- Visual clutter competing for attention -- Inconsistent patterns that prevent learning -- Unnecessary steps between user intent and result - -### Germane Load: Learning Effort -Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery. - -**Support it by**: -- Progressive disclosure that reveals complexity gradually -- Consistent patterns that reward learning -- Feedback that confirms correct understanding -- Onboarding that teaches through action, not walls of text - ---- - -## Cognitive Load Checklist - -Evaluate the interface against these 8 items: - -- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements? -- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)? -- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)? -- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen? -- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next? -- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)? -- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one? -- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it? - -**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed). - ---- - -## The Working Memory Rule - -**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001). - -At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider: -- **≤4 items**: Within working memory limits, manageable -- **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure -- **8+ items**: Overloaded; users will skip, misclick, or abandon - -**Practical applications**: -- Navigation menus: ≤5 top-level items (group the rest under clear categories) -- Form sections: ≤4 fields visible per group before a visual break -- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu -- Dashboard widgets: ≤4 key metrics visible without scrolling -- Pricing tiers: ≤3 options (more causes analysis paralysis) - ---- - -## Common Cognitive Load Violations - -### 1. The Wall of Options -**Problem**: Presenting 10+ choices at once with no hierarchy. -**Fix**: Group into categories, highlight recommended, use progressive disclosure. - -### 2. The Memory Bridge -**Problem**: User must remember info from step 1 to complete step 3. -**Fix**: Keep relevant context visible, or repeat it where it's needed. - -### 3. The Hidden Navigation -**Problem**: User must build a mental map of where things are. -**Fix**: Always show current location (breadcrumbs, active states, progress indicators). - -### 4. The Jargon Barrier -**Problem**: Technical or domain language forces translation effort. -**Fix**: Use plain language. If domain terms are unavoidable, define them inline. - -### 5. The Visual Noise Floor -**Problem**: Every element has the same visual weight; nothing stands out. -**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted. - -### 6. The Inconsistent Pattern -**Problem**: Similar actions work differently in different places. -**Fix**: Standardize interaction patterns. Same type of action = same type of UI. - -### 7. The Multi-Task Demand -**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating). -**Fix**: Sequence the steps. Let the user do one thing at a time. - -### 8. The Context Switch -**Problem**: User must jump between screens/tabs/modals to gather info for a single decision. -**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth. diff --git a/.github/skills/impeccable/reference/color-and-contrast.md b/.github/skills/impeccable/reference/color-and-contrast.md deleted file mode 100644 index 110c2ee47..000000000 --- a/.github/skills/impeccable/reference/color-and-contrast.md +++ /dev/null @@ -1,105 +0,0 @@ -# Color & Contrast - -## Color Spaces: Use OKLCH - -**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal, unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark. - -The OKLCH function takes three components: `oklch(lightness chroma hue)` where lightness is 0-100%, chroma is roughly 0-0.4, and hue is 0-360. To build a primary color and its lighter / darker variants, hold the chroma+hue roughly constant and vary the lightness, but **reduce chroma as you approach white or black**, because high chroma at extreme lightness looks garish. - -The hue you pick is a brand decision and should not come from a default. Do not reach for blue (hue 250) or warm orange (hue 60) by reflex; those are the dominant AI-design defaults, not the right answer for any specific brand. - -## Building Functional Palettes - -### Tinted Neutrals - -**Pure gray is dead.** A neutral with zero chroma feels lifeless next to a colored brand. Add a tiny chroma value (0.005-0.015) to all your neutrals, hued toward whatever your brand color is. The chroma is small enough not to read as "tinted" consciously, but it creates subconscious cohesion between brand color and UI surfaces. - -The hue you tint toward should come from THIS project's brand, not from a "warm = friendly, cool = tech" formula. If your brand color is teal, your neutrals lean toward teal. If your brand color is amber, they lean toward amber. The point is cohesion with the SPECIFIC brand, not a stock palette. - -**Avoid** the trap of always tinting toward warm orange or always tinting toward cool blue. Those are the two laziest defaults and they create their own monoculture across projects. - -### Palette Structure - -A complete system needs: - -| Role | Purpose | Example | -|------|---------|---------| -| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades | -| **Neutral** | Text, backgrounds, borders | 9-11 shade scale | -| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each | -| **Surface** | Cards, modals, overlays | 2-3 elevation levels | - -**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise. - -### The 60-30-10 Rule (Applied Correctly) - -This rule is about **visual weight**, not pixel count: - -- **60%**: Neutral backgrounds, white space, base surfaces -- **30%**: Secondary colors: text, borders, inactive states -- **10%**: Accent: CTAs, highlights, focus states - -The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power. - -## Contrast & Accessibility - -### WCAG Requirements - -| Content Type | AA Minimum | AAA Target | -|--------------|------------|------------| -| Body text | 4.5:1 | 7:1 | -| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | -| UI components, icons | 3:1 | 4.5:1 | -| Non-essential decorations | None | None | - -**The gotcha**: Placeholder text still needs 4.5:1. That light gray placeholder you see everywhere? Usually fails WCAG. - -### Dangerous Color Combinations - -These commonly fail contrast or cause readability issues: - -- Light gray text on white (the #1 accessibility fail) -- **Gray text on any colored background**: gray looks washed out and dead on color. Use a darker shade of the background color, or transparency -- Red text on green background (or vice versa): 8% of men can't distinguish these -- Blue text on red background (vibrates visually) -- Yellow text on white (almost always fails) -- Thin light text on images (unpredictable contrast) - -### Never Use Pure Gray or Pure Black - -Pure gray (`oklch(50% 0 0)`) and pure black (`#000`) don't exist in nature; real shadows and surfaces always have a color cast. Even a chroma of 0.005-0.01 is enough to feel natural without being obviously tinted. (See tinted neutrals example above.) - -### Testing - -Don't trust your eyes. Use tools: - -- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) -- Browser DevTools → Rendering → Emulate vision deficiencies -- [Polypane](https://polypane.app/) for real-time testing - -## Theming: Light & Dark Mode - -### Dark Mode Is Not Inverted Light Mode - -You can't just swap colors. Dark mode requires different design decisions: - -| Light Mode | Dark Mode | -|------------|-----------| -| Shadows for depth | Lighter surfaces for depth (no shadows) | -| Dark text on light | Light text on dark (reduce font weight) | -| Vibrant accents | Desaturate accents slightly | -| White backgrounds | Never pure black; use dark gray (oklch 12-18%) | - -In dark mode, depth comes from surface lightness, not shadow. Build a 3-step surface scale where higher elevations are lighter (e.g. 15% / 20% / 25% lightness). Use the SAME hue and chroma as your brand color (whatever it is for THIS project; do not reach for blue) and only vary the lightness. Reduce body text weight slightly (e.g. 350 instead of 400) because light text on dark reads as heavier than dark text on light. - -### Token Hierarchy - -Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer; primitives stay the same. - -## Alpha Is A Design Smell - -Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed. - ---- - -**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Using pure black (#000) for large areas. Skipping color blindness testing (8% of men affected). diff --git a/.github/skills/impeccable/reference/colorize.md b/.github/skills/impeccable/reference/colorize.md index 9e96eef59..2457f608c 100644 --- a/.github/skills/impeccable/reference/colorize.md +++ b/.github/skills/impeccable/reference/colorize.md @@ -68,10 +68,10 @@ Add color systematically across these dimensions: - **Hover states**: Introduce color on interaction ### Background & Surfaces -- **Tinted backgrounds**: Replace pure gray (`#f5f5f5`) with warm neutrals (`oklch(97% 0.01 60)`) or cool tints (`oklch(97% 0.01 250)`) +- **Tinted backgrounds**: If you replace pure gray, tint toward the brand hue, not toward a generic-warm-or-cool pair. The default-warm-tint (`oklch(97% 0.01 60)` and its neighbors) is now the AI cream/sand giveaway. Be specific to the brand or stay neutral. - **Colored sections**: Use subtle background colors to separate areas - **Gradient backgrounds**: Add depth with subtle, intentional gradients (not generic purple-blue) -- **Cards & surfaces**: Tint cards or surfaces slightly for warmth +- **Cards & surfaces**: Tint cards or surfaces toward the brand, not "for warmth" by reflex **Use OKLCH for color**: It's perceptually uniform, meaning equal steps in lightness *look* equal. Great for generating harmonious scales. @@ -124,8 +124,6 @@ Ensure color addition improves rather than overwhelms: - Use every color in the rainbow (choose 2-4 colors beyond neutrals) - Apply color randomly without semantic meaning - Put gray text on colored backgrounds. It looks washed out; use a darker shade of the background color or transparency instead -- Use pure gray for neutrals. Add subtle color tint (warm or cool) for depth -- Use pure black (`#000`) or pure white (`#fff`) for large areas - Violate WCAG contrast requirements - Use color as the only indicator (accessibility issue) - Make everything colorful (defeats the purpose) @@ -152,3 +150,108 @@ When invoked from live mode, each variant MUST declare a `color-amount` param so ``` Layer 1-2 variant-specific params on top: palette selection (`steps` with named options), temperature warmth, or tint vs. true color. See `reference/live.md` for the full params contract. + +--- + +## Reference Material + +The sections below were previously `color-and-contrast.md` and live inline now so the colorize flow has its deep color reference in one place. + +### Color & Contrast + +#### Color Spaces: Use OKLCH + +**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal, unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark. + +The OKLCH function takes three components: `oklch(lightness chroma hue)` where lightness is 0-100%, chroma is roughly 0-0.4, and hue is 0-360. To build a primary color and its lighter / darker variants, hold the chroma+hue roughly constant and vary the lightness, but **reduce chroma as you approach white or black**, because high chroma at extreme lightness looks garish. + +The hue you pick is a brand decision and should not come from a default. Do not reach for blue (hue 250) or warm orange (hue 60) by reflex; those are the dominant AI-design defaults, not the right answer for any specific brand. + +#### Building Functional Palettes + +##### Tinted Neutrals + +**Pure gray is dead.** A neutral with zero chroma feels lifeless next to a colored brand. Add a tiny chroma value (0.005-0.015) to all your neutrals, hued toward whatever your brand color is. The chroma is small enough not to read as "tinted" consciously, but it creates subconscious cohesion between brand color and UI surfaces. + +The hue you tint toward should come from THIS project's brand, not from a "warm = friendly, cool = tech" formula. If your brand color is teal, your neutrals lean toward teal. If your brand color is amber, they lean toward amber. The point is cohesion with the SPECIFIC brand, not a stock palette. + +**Avoid** the trap of always tinting toward warm orange or always tinting toward cool blue. Those are the two laziest defaults and they create their own monoculture across projects. + +##### Palette Structure + +A complete system needs: + +| Role | Purpose | Example | +|------|---------|---------| +| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades | +| **Neutral** | Text, backgrounds, borders | 9-11 shade scale | +| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each | +| **Surface** | Cards, modals, overlays | 2-3 elevation levels | + +**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise. + +##### The 60-30-10 Rule (Applied Correctly) + +This rule is about **visual weight**, not pixel count: + +- **60%**: Neutral backgrounds, white space, base surfaces +- **30%**: Secondary colors: text, borders, inactive states +- **10%**: Accent: CTAs, highlights, focus states + +The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power. + +#### Contrast & Accessibility + +##### WCAG Requirements + +| Content Type | AA Minimum | AAA Target | +|--------------|------------|------------| +| Body text | 4.5:1 | 7:1 | +| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | +| UI components, icons | 3:1 | 4.5:1 | +| Non-essential decorations | None | None | + +##### Dangerous Color Combinations + +These commonly fail contrast or cause readability issues: + +- Light gray text on white (the #1 accessibility fail) +- Red text on green background (or vice versa): 8% of men can't distinguish these +- Blue text on red background (vibrates visually) +- Yellow text on white (almost always fails) +- Thin light text on images (unpredictable contrast) + +##### Testing + +Don't trust your eyes. Use tools: + +- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) +- Browser DevTools → Rendering → Emulate vision deficiencies +- [Polypane](https://polypane.app/) for real-time testing + +#### Theming: Light & Dark Mode + +##### Dark Mode Is Not Inverted Light Mode + +You can't just swap colors. Dark mode requires different design decisions: + +| Light Mode | Dark Mode | +|------------|-----------| +| Shadows for depth | Lighter surfaces for depth (no shadows) | +| Dark text on light | Light text on dark (reduce font weight) | +| Vibrant accents | Desaturate accents slightly | +| White backgrounds | Either pure black or a deep surface that fits the brand (a brand-tinted near-black at oklch 12-18% works too) | + +In dark mode, depth comes from surface lightness, not shadow. Build a 3-step surface scale where higher elevations are lighter (e.g. 15% / 20% / 25% lightness). Use the SAME hue and chroma as your brand color (whatever it is for THIS project; do not reach for blue) and only vary the lightness. Reduce body text weight slightly (e.g. 350 instead of 400) because light text on dark reads as heavier than dark text on light. + +##### Token Hierarchy + +Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer; primitives stay the same. + +#### Alpha Is A Design Smell + +Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed. + +--- + +**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Skipping color blindness testing (8% of men affected). diff --git a/.github/skills/impeccable/reference/craft.md b/.github/skills/impeccable/reference/craft.md index 06094b1dd..2c7bd99b1 100644 --- a/.github/skills/impeccable/reference/craft.md +++ b/.github/skills/impeccable/reference/craft.md @@ -56,15 +56,15 @@ If the harness has native image generation (Codex), a compact shape's "confirm o Based on the design brief's "Recommended References" section, consult the relevant impeccable reference files. At minimum, always consult: -- [spatial-design.md](spatial-design.md) for layout and spacing -- [typography.md](typography.md) for type hierarchy +- [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 [motion-design.md](motion-design.md) -- Color-heavy or themed? Consult [color-and-contrast.md](color-and-contrast.md) -- Responsive requirements? Consult [responsive-design.md](responsive-design.md) -- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.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) diff --git a/.github/skills/impeccable/reference/critique.md b/.github/skills/impeccable/reference/critique.md index aee3412e2..29d6057fe 100644 --- a/.github/skills/impeccable/reference/critique.md +++ b/.github/skills/impeccable/reference/critique.md @@ -38,9 +38,9 @@ Read relevant source files and visually inspect the live page when browser autom Evaluate: - **AI slop**: Would someone believe "AI made this" immediately? Check all DON'T guidance from the parent Impeccable skill. - **Holistic design**: hierarchy, IA, emotional fit, discoverability, composition, typography, color, accessibility, states, copy, and edge cases. -- **Cognitive load**: consult [cognitive-load](cognitive-load.md); report checklist failures and decision points with >4 visible options. +- **Cognitive load**: consult the [Cognitive Load Assessment](#cognitive-load-assessment) section below; report checklist failures and decision points with >4 visible options. - **Emotional journey**: peak-end rule, emotional valleys, reassurance at high-stakes moments. -- **Nielsen heuristics**: consult [heuristics-scoring](heuristics-scoring.md); score all 10 heuristics 0-4. +- **Nielsen heuristics**: consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below; score all 10 heuristics 0-4. Return: AI slop verdict, heuristic scores, cognitive load, emotional journey, 2-3 strengths, 3-5 priority issues, persona red flags, minor observations, and provocative questions. @@ -80,7 +80,7 @@ The chat response is the primary user-facing deliverable. Present the full struc Structure your feedback as a design director would: #### Design Health Score -> *Consult [heuristics-scoring](heuristics-scoring.md)* +> *Consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below.* Present the Nielsen's 10 heuristics scores as a table: @@ -119,16 +119,16 @@ Highlight 2-3 things done well. Be specific about why they work. #### Priority Issues The 3-5 most impactful design problems, ordered by importance. -For each issue, tag with **P0-P3 severity** (consult [heuristics-scoring](heuristics-scoring.md) for severity definitions): +For each issue, tag with **P0-P3 severity** (see [Issue Severity below](#issue-severity-p0p3) for definitions): - **[P?] What**: Name the problem clearly - **Why it matters**: How this hurts users or undermines goals - **Fix**: What to do about it (be concrete) - **Suggested command**: Which command could address this (from: /impeccable adapt, /impeccable animate, /impeccable audit, /impeccable bolder, /impeccable clarify, /impeccable colorize, /impeccable critique, /impeccable delight, /impeccable distill, /impeccable document, /impeccable harden, /impeccable layout, /impeccable onboard, /impeccable optimize, /impeccable overdrive, /impeccable polish, /impeccable quieter, /impeccable shape, /impeccable typeset) #### Persona Red Flags -> *Consult [personas](personas.md)* +> *Consult the [Personas reference](#persona-based-design-testing) below.* -Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `.github/copilot-instructions.md` contains a `## Design Context` section from `impeccable teach`, also generate 1-2 project-specific personas from the audience/brand info. +Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `.github/copilot-instructions.md` contains a `## Design Context` section from `impeccable init`, also generate 1-2 project-specific personas from the audience/brand info. For each selected persona, walk through the primary user action and list specific red flags found: @@ -234,3 +234,534 @@ After presenting the summary, tell the user: > You can ask me to run these one at a time, all at once, or in any order you prefer. > > Re-run `/impeccable critique` after fixes to see your score improve. + +--- + +## Reference Material + +The sections below were previously separate reference files (`cognitive-load.md`, `heuristics-scoring.md`, `personas.md`). They live inline now so the critique flow has all its deep context in one place. + +### Cognitive Load Assessment + +Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload. + +--- + +#### Three Types of Cognitive Load + +##### Intrinsic Load: The Task Itself +Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it. + +**Manage it by**: +- Breaking complex tasks into discrete steps +- Providing scaffolding (templates, defaults, examples) +- Progressive disclosure: show what's needed now, hide the rest +- Grouping related decisions together + +##### Extraneous Load: Bad Design +Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste. + +**Common sources**: +- Confusing navigation that requires mental mapping +- Unclear labels that force users to guess meaning +- Visual clutter competing for attention +- Inconsistent patterns that prevent learning +- Unnecessary steps between user intent and result + +##### Germane Load: Learning Effort +Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery. + +**Support it by**: +- Progressive disclosure that reveals complexity gradually +- Consistent patterns that reward learning +- Feedback that confirms correct understanding +- Onboarding that teaches through action, not walls of text + +--- + +#### Cognitive Load Checklist + +Evaluate the interface against these 8 items: + +- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements? +- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)? +- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)? +- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen? +- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next? +- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)? +- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one? +- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it? + +**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed). + +--- + +#### The Working Memory Rule + +**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001). + +At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider: +- **≤4 items**: Within working memory limits, manageable +- **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure +- **8+ items**: Overloaded; users will skip, misclick, or abandon + +**Practical applications**: +- Navigation menus: ≤5 top-level items (group the rest under clear categories) +- Form sections: ≤4 fields visible per group before a visual break +- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu +- Dashboard widgets: ≤4 key metrics visible without scrolling +- Pricing tiers: ≤3 options (more causes analysis paralysis) + +--- + +#### Common Cognitive Load Violations + +##### 1. The Wall of Options +**Problem**: Presenting 10+ choices at once with no hierarchy. +**Fix**: Group into categories, highlight recommended, use progressive disclosure. + +##### 2. The Memory Bridge +**Problem**: User must remember info from step 1 to complete step 3. +**Fix**: Keep relevant context visible, or repeat it where it's needed. + +##### 3. The Hidden Navigation +**Problem**: User must build a mental map of where things are. +**Fix**: Always show current location (breadcrumbs, active states, progress indicators). + +##### 4. The Jargon Barrier +**Problem**: Technical or domain language forces translation effort. +**Fix**: Use plain language. If domain terms are unavoidable, define them inline. + +##### 5. The Visual Noise Floor +**Problem**: Every element has the same visual weight; nothing stands out. +**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted. + +##### 6. The Inconsistent Pattern +**Problem**: Similar actions work differently in different places. +**Fix**: Standardize interaction patterns. Same type of action = same type of UI. + +##### 7. The Multi-Task Demand +**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating). +**Fix**: Sequence the steps. Let the user do one thing at a time. + +##### 8. The Context Switch +**Problem**: User must jump between screens/tabs/modals to gather info for a single decision. +**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth. + +--- + +### Heuristics Scoring Guide + +Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough." + +#### Nielsen's 10 Heuristics + +##### 1. Visibility of System Status + +Keep users informed about what's happening through timely, appropriate feedback. + +**Check for**: +- Loading indicators during async operations +- Confirmation of user actions (save, submit, delete) +- Progress indicators for multi-step processes +- Current location in navigation (breadcrumbs, active states) +- Form validation feedback (inline, not just on submit) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | No feedback; user is guessing what happened | +| 1 | Rare feedback; most actions produce no visible response | +| 2 | Partial; some states communicated, major gaps remain | +| 3 | Good; most operations give clear feedback, minor gaps | +| 4 | Excellent; every action confirms, progress is always visible | + +##### 2. Match Between System and Real World + +Speak the user's language. Follow real-world conventions. Information appears in natural, logical order. + +**Check for**: +- Familiar terminology (no unexplained jargon) +- Logical information order matching user expectations +- Recognizable icons and metaphors +- Domain-appropriate language for the target audience +- Natural reading flow (left-to-right, top-to-bottom priority) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Pure tech jargon, alien to users | +| 1 | Mostly confusing; requires domain expertise to navigate | +| 2 | Mixed; some plain language, some jargon leaks through | +| 3 | Mostly natural; occasional term needs context | +| 4 | Speaks the user's language fluently throughout | + +##### 3. User Control and Freedom + +Users need a clear "emergency exit" from unwanted states without extended dialogue. + +**Check for**: +- Undo/redo functionality +- Cancel buttons on forms and modals +- Clear navigation back to safety (home, previous) +- Easy way to clear filters, search, selections +- Escape from long or multi-step processes + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Users get trapped; no way out without refreshing | +| 1 | Difficult exits; must find obscure paths to escape | +| 2 | Some exits; main flows have escape, edge cases don't | +| 3 | Good control; users can exit and undo most actions | +| 4 | Full control; undo, cancel, back, and escape everywhere | + +##### 4. Consistency and Standards + +Users shouldn't wonder whether different words, situations, or actions mean the same thing. + +**Check for**: +- Consistent terminology throughout the interface +- Same actions produce same results everywhere +- Platform conventions followed (standard UI patterns) +- Visual consistency (colors, typography, spacing, components) +- Consistent interaction patterns (same gesture = same behavior) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Inconsistent everywhere; feels like different products stitched together | +| 1 | Many inconsistencies; similar things look/behave differently | +| 2 | Partially consistent; main flows match, details diverge | +| 3 | Mostly consistent; occasional deviation, nothing confusing | +| 4 | Fully consistent; cohesive system, predictable behavior | + +##### 5. Error Prevention + +Better than good error messages is a design that prevents problems in the first place. + +**Check for**: +- Confirmation before destructive actions (delete, overwrite) +- Constraints preventing invalid input (date pickers, dropdowns) +- Smart defaults that reduce errors +- Clear labels that prevent misunderstanding +- Autosave and draft recovery + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Errors easy to make; no guardrails anywhere | +| 1 | Few safeguards; some inputs validated, most aren't | +| 2 | Partial prevention; common errors caught, edge cases slip | +| 3 | Good prevention; most error paths blocked proactively | +| 4 | Excellent; errors nearly impossible through smart constraints | + +##### 6. Recognition Rather Than Recall + +Minimize memory load. Make objects, actions, and options visible or easily retrievable. + +**Check for**: +- Visible options (not buried in hidden menus) +- Contextual help when needed (tooltips, inline hints) +- Recent items and history +- Autocomplete and suggestions +- Labels on icons (not icon-only navigation) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Heavy memorization; users must remember paths and commands | +| 1 | Mostly recall; many hidden features, few visible cues | +| 2 | Some aids; main actions visible, secondary features hidden | +| 3 | Good recognition; most things discoverable, few memory demands | +| 4 | Everything discoverable; users never need to memorize | + +##### 7. Flexibility and Efficiency of Use + +Accelerators, invisible to novices, speed up expert interaction. + +**Check for**: +- Keyboard shortcuts for common actions +- Customizable interface elements +- Recent items and favorites +- Bulk/batch actions +- Power user features that don't complicate the basics + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | One rigid path; no shortcuts or alternatives | +| 1 | Limited flexibility; few alternatives to the main path | +| 2 | Some shortcuts; basic keyboard support, limited bulk actions | +| 3 | Good accelerators; keyboard nav, some customization | +| 4 | Highly flexible; multiple paths, power features, customizable | + +##### 8. Aesthetic and Minimalist Design + +Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose. + +**Check for**: +- Only necessary information visible at each step +- Clear visual hierarchy directing attention +- Purposeful use of color and emphasis +- No decorative clutter competing for attention +- Focused, uncluttered layouts + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Overwhelming; everything competes for attention equally | +| 1 | Cluttered; too much noise, hard to find what matters | +| 2 | Some clutter; main content clear, periphery noisy | +| 3 | Mostly clean; focused design, minor visual noise | +| 4 | Perfectly minimal; every element earns its pixel | + +##### 9. Help Users Recognize, Diagnose, and Recover from Errors + +Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution. + +**Check for**: +- Plain language error messages (no error codes for users) +- Specific problem identification ("Email is missing @" not "Invalid input") +- Actionable recovery suggestions +- Errors displayed near the source of the problem +- Non-blocking error handling (don't wipe the form) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Cryptic errors; codes, jargon, or no message at all | +| 1 | Vague errors; "Something went wrong" with no guidance | +| 2 | Clear but unhelpful; names the problem but not the fix | +| 3 | Clear with suggestions; identifies problem and offers next steps | +| 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work | + +##### 10. Help and Documentation + +Even if the system is usable without docs, help should be easy to find, task-focused, and concise. + +**Check for**: +- Searchable help or documentation +- Contextual help (tooltips, inline hints, guided tours) +- Task-focused organization (not feature-organized) +- Concise, scannable content +- Easy access without leaving current context + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | No help available anywhere | +| 1 | Help exists but hard to find or irrelevant | +| 2 | Basic help; FAQ or docs exist, not contextual | +| 3 | Good documentation; searchable, mostly task-focused | +| 4 | Excellent contextual help; right info at the right moment | + +--- + +#### Score Summary + +**Total possible**: 40 points (10 heuristics × 4 max) + +| Score Range | Rating | What It Means | +|-------------|--------|---------------| +| 36–40 | Excellent | Minor polish only; ship it | +| 28–35 | Good | Address weak areas, solid foundation | +| 20–27 | Acceptable | Significant improvements needed before users are happy | +| 12–19 | Poor | Major UX overhaul required; core experience broken | +| 0–11 | Critical | Redesign needed; unusable in current state | + +--- + +#### Issue Severity (P0–P3) + +Tag each individual issue found during scoring with a priority level: + +| Priority | Name | Description | Action | +|----------|------|-------------|--------| +| **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper | +| **P1** | Major | Causes significant difficulty or confusion | Fix before release | +| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass | +| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits | + +**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1. + +--- + +### Persona-Based Design Testing + +Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss. + +**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns. + +--- + +#### 1. Impatient Power User: "Alex" + +**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave. + +**Behaviors**: +- Skips all onboarding and instructions +- Looks for keyboard shortcuts immediately +- Tries to bulk-select, batch-edit, and automate +- Gets frustrated by required steps that feel unnecessary +- Abandons if anything feels slow or patronizing + +**Test Questions**: +- Can Alex complete the core task in under 60 seconds? +- Are there keyboard shortcuts for common actions? +- Can onboarding be skipped entirely? +- Do modals have keyboard dismiss (Esc)? +- Is there a "power user" path (shortcuts, bulk actions)? + +**Red Flags** (report these specifically): +- Forced tutorials or unskippable onboarding +- No keyboard navigation for primary actions +- Slow animations that can't be skipped +- One-item-at-a-time workflows where batch would be natural +- Redundant confirmation steps for low-risk actions + +--- + +#### 2. Confused First-Timer: "Jordan" + +**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out. + +**Behaviors**: +- Reads all instructions carefully +- Hesitates before clicking anything unfamiliar +- Looks for help or support constantly +- Misunderstands jargon and abbreviations +- Takes the most literal interpretation of any label + +**Test Questions**: +- Is the first action obviously clear within 5 seconds? +- Are all icons labeled with text? +- Is there contextual help at decision points? +- Does terminology assume prior knowledge? +- Is there a clear "back" or "undo" at every step? + +**Red Flags** (report these specifically): +- Icon-only navigation with no labels +- Technical jargon without explanation +- No visible help option or guidance +- Ambiguous next steps after completing an action +- No confirmation that an action succeeded + +--- + +#### 3. Accessibility-Dependent User: "Sam" + +**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences. + +**Behaviors**: +- Tabs through the interface linearly +- Relies on ARIA labels and heading structure +- Cannot see hover states or visual-only indicators +- Needs adequate color contrast (4.5:1 minimum) +- May use browser zoom up to 200% + +**Test Questions**: +- Can the entire primary flow be completed keyboard-only? +- Are all interactive elements focusable with visible focus indicators? +- Do images have meaningful alt text? +- Is color contrast WCAG AA compliant (4.5:1 for text)? +- Does the screen reader announce state changes (loading, success, errors)? + +**Red Flags** (report these specifically): +- Click-only interactions with no keyboard alternative +- Missing or invisible focus indicators +- Meaning conveyed by color alone (red = error, green = success) +- Unlabeled form fields or buttons +- Time-limited actions without extension option +- Custom components that break screen reader flow + +--- + +#### 4. Deliberate Stress Tester: "Riley" + +**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience. + +**Behaviors**: +- Tests edge cases intentionally (empty states, long strings, special characters) +- Submits forms with unexpected data (emoji, RTL text, very long values) +- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs +- Looks for inconsistencies between what the UI promises and what actually happens +- Documents problems methodically + +**Test Questions**: +- What happens at the edges (0 items, 1000 items, very long text)? +- Do error states recover gracefully or leave the UI in a broken state? +- What happens on refresh mid-workflow? Is state preserved? +- Are there features that appear to work but produce broken results? +- How does the UI handle unexpected input (emoji, special chars, paste from Excel)? + +**Red Flags** (report these specifically): +- Features that appear to work but silently fail or produce wrong results +- Error handling that exposes technical details or leaves UI in a broken state +- Empty states that show nothing useful ("No results" with no guidance) +- Workflows that lose user data on refresh or navigation +- Inconsistent behavior between similar interactions in different parts of the UI + +--- + +#### 5. Distracted Mobile User: "Casey" + +**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection. + +**Behaviors**: +- Uses thumb only; prefers bottom-of-screen actions +- Gets interrupted mid-flow and returns later +- Switches between apps frequently +- Has limited attention span and low patience +- Types as little as possible, prefers taps and selections + +**Test Questions**: +- Are primary actions in the thumb zone (bottom half of screen)? +- Is state preserved if the user leaves and returns? +- Does it work on slow connections (3G)? +- Can forms use autocomplete and smart defaults? +- Are touch targets at least 44×44pt? + +**Red Flags** (report these specifically): +- Important actions positioned at the top of the screen (unreachable by thumb) +- No state persistence; progress lost on tab switch or interruption +- Large text inputs required where selection would work +- Heavy assets loading on every page (no lazy loading) +- Tiny tap targets or targets too close together + +--- + +#### Selecting Personas + +Choose personas based on the interface type: + +| Interface Type | Primary Personas | Why | +|---------------|-----------------|-----| +| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile | +| Dashboard / admin | Alex, Sam | Power users, accessibility | +| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity | +| Onboarding flow | Jordan, Casey | Confusion, interruption | +| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav | +| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile | + +--- + +#### Project-Specific Personas + +If `.github/copilot-instructions.md` contains a `## Design Context` section (generated by `impeccable init`), derive 1–2 additional personas from the audience and brand information: + +1. Read the target audience description +2. Identify the primary user archetype not covered by the 5 predefined personas +3. Create a persona following this template: + +``` +##### [Role]: "[Name]" + +**Profile**: [2-3 key characteristics derived from Design Context] + +**Behaviors**: [3-4 specific behaviors based on the described audience] + +**Red Flags**: [3-4 things that would alienate this specific user type] +``` + +Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists. diff --git a/.github/skills/impeccable/reference/document.md b/.github/skills/impeccable/reference/document.md index d27533278..60075688a 100644 --- a/.github/skills/impeccable/reference/document.md +++ b/.github/skills/impeccable/reference/document.md @@ -45,7 +45,7 @@ Rules that matter: - **Token refs** use `{path.to.token}` (e.g. `{colors.primary}`, `{rounded.md}`). Components may reference primitives; primitives may not reference each other. - **Stitch validates colors as hex sRGB only** (`#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`); OKLCH/HSL/P3 trigger a linter warning, not a hard error. YAML accepts the string either way and our own parser is format-agnostic. Choose based on project posture: (a) if the project has an "OKLCH-only" doctrine or uses Display-P3 values that don't round-trip through sRGB, put OKLCH directly in the frontmatter and accept the Stitch linter warning; (b) if the project wants strict Stitch compliance or plans to use their Tailwind/DTCG export pipeline, put hex in the frontmatter and keep OKLCH in prose as the canonical reference. Never split the source of truth without explicit reason. - **Component sub-tokens** are limited to 8 props: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, `width`. Shadows, motion, focus rings, backdrop-filter: none of those fit. Carry them in the sidecar (Step 4b). -- **Scale keys are open-ended.** Use whatever names the project already uses (`warm-ash-cream`, `surface-container-low`). Don't rename to Material defaults. +- **Scale keys are open-ended.** Use whatever names the project already uses (`oxblood-deep`, `surface-container-low`). Don't rename to Material defaults. - **Variants are naming convention, not schema.** `button-primary` / `button-primary-hover` / `button-primary-active` as sibling keys. ## The markdown body: six sections (exact order) @@ -61,7 +61,7 @@ Optional evocative subtitles are allowed in the form `## 2. Colors: The [Name] P ## When to run -- The user just ran `/impeccable teach` and needs the visual side documented. +- The user just ran `/impeccable init` and needs the visual side documented. - The skill noticed no `DESIGN.md` exists and nudged the user to create one. - An existing `DESIGN.md` is stale (the design has drifted). - Before a large redesign, to capture the current state as a reference. @@ -71,7 +71,7 @@ If a `DESIGN.md` already exists, **do not silently overwrite it**. Show the user ## Two paths - **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 teach, nothing built yet). Interview for five high-level answers, write a minimal DESIGN.md marked ``. Re-run in scan mode once there's code. +- **Seed mode**: the project is pre-implementation (fresh init, nothing built yet). Interview for five high-level answers, 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` forces seed mode regardless of code presence. @@ -103,7 +103,7 @@ Build a structured draft from the discovered tokens. For each token class: From the auto-extracted tokens, draft the YAML frontmatter now (you'll write it at the top of DESIGN.md in Step 4). This is the machine-readable layer: what the live panel and Stitch's linter consume. -- **Colors**: one entry per extracted color. Key = descriptive slug (`warm-ash-cream`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. +- **Colors**: one entry per extracted color. Key = descriptive slug (`oxblood-deep`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. - **Typography**: one entry per role (`display`, `headline`, `title`, `body`, `label`). Typography is an object; include only the props that are real for the project (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`). - **Rounded / Spacing**: whatever scale steps the project actually uses, keyed by whatever scale name the project uses (`sm` / `md` / `lg`, or `surface-sm`, or numeric steps). - **Components**: one entry per variant (`button-primary`, `button-primary-hover`, `button-ghost`). Reference primitives via `{colors.X}`, `{rounded.Y}`. If a variant needs a property Stitch's 8-prop set doesn't cover (shadow, focus ring, backdrop-filter), carry the full snippet in the sidecar instead. @@ -253,7 +253,7 @@ Regenerate the sidecar whenever you regenerate root `DESIGN.md`. If the user onl "extensions": { "colorMeta": { "primary": { "role": "primary", "displayName": "Editorial Magenta", "canonical": "oklch(60% 0.25 350)", "tonalRamp": ["...", "...", "..."] }, - "warm-ash-cream": { "role": "neutral", "displayName": "Warm Ash Cream", "canonical": "oklch(96% 0.005 350)", "tonalRamp": ["...", "...", "..."] } + "cool-paper": { "role": "neutral", "displayName": "Cool Paper", "canonical": "oklch(96% 0.005 230)", "tonalRamp": ["...", "...", "..."] } }, "typographyMeta": { "display": { "displayName": "Display", "purpose": "Hero headlines only." } @@ -328,12 +328,13 @@ Pull directly from the DESIGN.md you just wrote: Do not reword. The panel shows these as secondary collapsible context; the same voice that's in the Markdown carries through. -### Step 5: Confirm, refine, and refresh session cache +### Step 5: Confirm and refine 1. Show the user the full DESIGN.md you wrote. Briefly highlight the non-obvious creative choices (descriptive color names, atmosphere language, named rules). 2. Mention that `.impeccable/design.json` was also written alongside; the live panel will now render this project's actual button/input/nav primitives instead of generic approximations. 3. Offer to refine any section: "Want me to revise a section, add component patterns I missed, or adjust the atmosphere language?" -4. **Refresh the session cache.** Run `node .github/skills/impeccable/scripts/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. + +Your own write is the freshest source; subsequent commands in this session don't need a reload. ## Seed mode @@ -394,11 +395,12 @@ Per-section guidance in seed mode: 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. -### Step 4: Confirm and refresh session cache +### Step 4: Confirm 1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment). 2. Tell the user: "Re-run `/impeccable document` once you have some code. That pass will extract real tokens and generate the sidecar." -3. Run `node .github/skills/impeccable/scripts/load-context.mjs` once so the seed lands in conversation for the rest of the session. + +Your own write is the freshest source; no reload needed. ## Style guidelines diff --git a/.github/skills/impeccable/reference/heuristics-scoring.md b/.github/skills/impeccable/reference/heuristics-scoring.md deleted file mode 100644 index edbe5028d..000000000 --- a/.github/skills/impeccable/reference/heuristics-scoring.md +++ /dev/null @@ -1,234 +0,0 @@ -# Heuristics Scoring Guide - -Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough." - -## Nielsen's 10 Heuristics - -### 1. Visibility of System Status - -Keep users informed about what's happening through timely, appropriate feedback. - -**Check for**: -- Loading indicators during async operations -- Confirmation of user actions (save, submit, delete) -- Progress indicators for multi-step processes -- Current location in navigation (breadcrumbs, active states) -- Form validation feedback (inline, not just on submit) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | No feedback; user is guessing what happened | -| 1 | Rare feedback; most actions produce no visible response | -| 2 | Partial; some states communicated, major gaps remain | -| 3 | Good; most operations give clear feedback, minor gaps | -| 4 | Excellent; every action confirms, progress is always visible | - -### 2. Match Between System and Real World - -Speak the user's language. Follow real-world conventions. Information appears in natural, logical order. - -**Check for**: -- Familiar terminology (no unexplained jargon) -- Logical information order matching user expectations -- Recognizable icons and metaphors -- Domain-appropriate language for the target audience -- Natural reading flow (left-to-right, top-to-bottom priority) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Pure tech jargon, alien to users | -| 1 | Mostly confusing; requires domain expertise to navigate | -| 2 | Mixed; some plain language, some jargon leaks through | -| 3 | Mostly natural; occasional term needs context | -| 4 | Speaks the user's language fluently throughout | - -### 3. User Control and Freedom - -Users need a clear "emergency exit" from unwanted states without extended dialogue. - -**Check for**: -- Undo/redo functionality -- Cancel buttons on forms and modals -- Clear navigation back to safety (home, previous) -- Easy way to clear filters, search, selections -- Escape from long or multi-step processes - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Users get trapped; no way out without refreshing | -| 1 | Difficult exits; must find obscure paths to escape | -| 2 | Some exits; main flows have escape, edge cases don't | -| 3 | Good control; users can exit and undo most actions | -| 4 | Full control; undo, cancel, back, and escape everywhere | - -### 4. Consistency and Standards - -Users shouldn't wonder whether different words, situations, or actions mean the same thing. - -**Check for**: -- Consistent terminology throughout the interface -- Same actions produce same results everywhere -- Platform conventions followed (standard UI patterns) -- Visual consistency (colors, typography, spacing, components) -- Consistent interaction patterns (same gesture = same behavior) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Inconsistent everywhere; feels like different products stitched together | -| 1 | Many inconsistencies; similar things look/behave differently | -| 2 | Partially consistent; main flows match, details diverge | -| 3 | Mostly consistent; occasional deviation, nothing confusing | -| 4 | Fully consistent; cohesive system, predictable behavior | - -### 5. Error Prevention - -Better than good error messages is a design that prevents problems in the first place. - -**Check for**: -- Confirmation before destructive actions (delete, overwrite) -- Constraints preventing invalid input (date pickers, dropdowns) -- Smart defaults that reduce errors -- Clear labels that prevent misunderstanding -- Autosave and draft recovery - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Errors easy to make; no guardrails anywhere | -| 1 | Few safeguards; some inputs validated, most aren't | -| 2 | Partial prevention; common errors caught, edge cases slip | -| 3 | Good prevention; most error paths blocked proactively | -| 4 | Excellent; errors nearly impossible through smart constraints | - -### 6. Recognition Rather Than Recall - -Minimize memory load. Make objects, actions, and options visible or easily retrievable. - -**Check for**: -- Visible options (not buried in hidden menus) -- Contextual help when needed (tooltips, inline hints) -- Recent items and history -- Autocomplete and suggestions -- Labels on icons (not icon-only navigation) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Heavy memorization; users must remember paths and commands | -| 1 | Mostly recall; many hidden features, few visible cues | -| 2 | Some aids; main actions visible, secondary features hidden | -| 3 | Good recognition; most things discoverable, few memory demands | -| 4 | Everything discoverable; users never need to memorize | - -### 7. Flexibility and Efficiency of Use - -Accelerators, invisible to novices, speed up expert interaction. - -**Check for**: -- Keyboard shortcuts for common actions -- Customizable interface elements -- Recent items and favorites -- Bulk/batch actions -- Power user features that don't complicate the basics - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | One rigid path; no shortcuts or alternatives | -| 1 | Limited flexibility; few alternatives to the main path | -| 2 | Some shortcuts; basic keyboard support, limited bulk actions | -| 3 | Good accelerators; keyboard nav, some customization | -| 4 | Highly flexible; multiple paths, power features, customizable | - -### 8. Aesthetic and Minimalist Design - -Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose. - -**Check for**: -- Only necessary information visible at each step -- Clear visual hierarchy directing attention -- Purposeful use of color and emphasis -- No decorative clutter competing for attention -- Focused, uncluttered layouts - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Overwhelming; everything competes for attention equally | -| 1 | Cluttered; too much noise, hard to find what matters | -| 2 | Some clutter; main content clear, periphery noisy | -| 3 | Mostly clean; focused design, minor visual noise | -| 4 | Perfectly minimal; every element earns its pixel | - -### 9. Help Users Recognize, Diagnose, and Recover from Errors - -Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution. - -**Check for**: -- Plain language error messages (no error codes for users) -- Specific problem identification ("Email is missing @" not "Invalid input") -- Actionable recovery suggestions -- Errors displayed near the source of the problem -- Non-blocking error handling (don't wipe the form) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Cryptic errors; codes, jargon, or no message at all | -| 1 | Vague errors; "Something went wrong" with no guidance | -| 2 | Clear but unhelpful; names the problem but not the fix | -| 3 | Clear with suggestions; identifies problem and offers next steps | -| 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work | - -### 10. Help and Documentation - -Even if the system is usable without docs, help should be easy to find, task-focused, and concise. - -**Check for**: -- Searchable help or documentation -- Contextual help (tooltips, inline hints, guided tours) -- Task-focused organization (not feature-organized) -- Concise, scannable content -- Easy access without leaving current context - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | No help available anywhere | -| 1 | Help exists but hard to find or irrelevant | -| 2 | Basic help; FAQ or docs exist, not contextual | -| 3 | Good documentation; searchable, mostly task-focused | -| 4 | Excellent contextual help; right info at the right moment | - ---- - -## Score Summary - -**Total possible**: 40 points (10 heuristics × 4 max) - -| Score Range | Rating | What It Means | -|-------------|--------|---------------| -| 36–40 | Excellent | Minor polish only; ship it | -| 28–35 | Good | Address weak areas, solid foundation | -| 20–27 | Acceptable | Significant improvements needed before users are happy | -| 12–19 | Poor | Major UX overhaul required; core experience broken | -| 0–11 | Critical | Redesign needed; unusable in current state | - ---- - -## Issue Severity (P0–P3) - -Tag each individual issue found during scoring with a priority level: - -| Priority | Name | Description | Action | -|----------|------|-------------|--------| -| **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper | -| **P1** | Major | Causes significant difficulty or confusion | Fix before release | -| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass | -| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits | - -**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1. diff --git a/.github/skills/impeccable/reference/teach.md b/.github/skills/impeccable/reference/init.md similarity index 58% rename from .github/skills/impeccable/reference/teach.md rename to .github/skills/impeccable/reference/init.md index 003e436e3..67af1fa32 100644 --- a/.github/skills/impeccable/reference/teach.md +++ b/.github/skills/impeccable/reference/init.md @@ -1,21 +1,16 @@ -# Teach Flow +# Init Flow -Gathers design context for a project and writes two complementary files at the project root: +The setup command for a project. One codebase crawl feeds everything it writes: - **PRODUCT.md** (strategic): root project file for register, target users, product purpose, brand personality, anti-references, strategic design principles. Answers "who/what/why". - **DESIGN.md** (visual): root project file for visual theme, color palette, typography, components, layout. Follows the [Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/). Answers "how it looks". +- **`.impeccable/live/config.json`** (live mode): pre-configured so `/impeccable live` boots straight into variant mode with no first-time detour. -Every other impeccable command reads these files before doing any work. +It closes by pointing the user at the best command to run next. Every other impeccable command reads PRODUCT.md and DESIGN.md before doing any work. ## Step 1: Load current state -Run the shared loader first so you know what already exists: - -```bash -node .github/skills/impeccable/scripts/load-context.mjs -``` - -The output tells you whether PRODUCT.md and/or DESIGN.md already exist. If `migrated: true`, legacy `.impeccable.md` was auto-renamed to `PRODUCT.md`. Mention this once to the user. +Check what already exists. PRODUCT.md and DESIGN.md live at the project root, or under `.agents/context/` or `docs/` (case-insensitive). Read whichever are present with your native file tool. Also note whether `.impeccable/live/config.json` already exists (Step 6 leaves it untouched if so). Decision tree: - **Neither file exists (empty project or no context yet)**: do Steps 2-4 (write PRODUCT.md), then decide on DESIGN.md based on whether there's code to analyze. @@ -26,14 +21,14 @@ Decision tree: Never silently overwrite an existing file. Always confirm first. -If teach was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete teach, re-run the loader, then resume the original command with the freshly loaded context. For craft, resume into shape next; teach creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief. +If init was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete init, then resume the original command. Your own writes are the freshest source; no reload needed. For craft, resume into shape next; init creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief. ## Step 2: Explore the codebase -Before asking questions, thoroughly scan the project to discover what you can: +Before asking questions, thoroughly scan the project to discover what you can. This single crawl feeds PRODUCT.md, DESIGN.md, **and** the live-mode framework detection in Step 6, so be thorough once rather than re-scanning later: - **README and docs**: Project purpose, target audience, any stated goals -- **Package.json / config files**: Tech stack, dependencies, existing design libraries +- **Package.json / config files**: Tech stack, dependencies, existing design libraries, **and the framework** (Vite/SPA, Next.js, Nuxt, SvelteKit, Astro, multi-page static) plus the HTML entry the browser actually loads - **Existing components**: Current design patterns, spacing, typography in use - **Brand assets**: Logos, favicons, color values already defined - **Design tokens / CSS variables**: Existing color palettes, font stacks, spacing scales @@ -46,7 +41,7 @@ Also form a **register hypothesis** from what you find: Register is a hypothesis at this point, not a decision; Step 3 confirms it. -Note what you've learned and what remains unclear. This exploration feeds both PRODUCT.md and DESIGN.md. +Note what you've learned and what remains unclear. Also note any rough edges worth a follow-up command (thin hierarchy, flat or gray palette, missing error/empty states, dull copy); Step 7 turns these into concrete recommendations without re-analyzing. ## Step 3: Ask strategic questions (for PRODUCT.md) @@ -84,8 +79,7 @@ If the signal is genuinely split (e.g. a product with a big marketing landing), ### Brand & Personality - How would you describe the brand personality in 3 words? - Reference sites or apps that capture the right feel? What specifically about them? - - For brand, push for real-world references in the right lane (tech-minimal, editorial-magazine, consumer-warm, brutalist-grid, etc.), not generic "modern" adjectives. - - For product, push for category best-tool references (Linear, Figma, Notion, Raycast, Stripe). + - Push for specific named references with the *specific* thing about them that fits this brand, not generic "modern" adjectives or category-bucket lanes. - What should this explicitly NOT look like? Any anti-references? ### Accessibility & Inclusion @@ -141,16 +135,38 @@ If the user agrees, delegate to `/impeccable document` (it auto-detects scan vs If the user prefers to skip, mention they can run `/impeccable document` any time later. -## Step 6: Confirm and wrap up +## Step 6: Configure live mode (when code exists) -Summarize: +If the project has code with HTML entries and a dev server (the same "code exists" condition that puts `/impeccable document` in scan mode), pre-configure live mode now. You already identified the framework and the served HTML entry in Step 2, so this is nearly free, and it spares the user the first-time setup detour when they later run `/impeccable live`. + +**Skip this step for empty / pre-implementation projects** (nothing to inject into yet). Tell the user live mode will configure itself the first time they run it once there's code. + +**If `.impeccable/live/config.json` already exists, leave it untouched** and note that live mode is already configured. + +Otherwise: + +1. Write `.impeccable/live/config.json`. Choose `files` (the HTML entries the browser actually loads), `insertBefore`, and `commentSyntax` from the framework table in [live.md](live.md)'s **First-time setup** section, using the framework you found in Step 2. That table is canonical; do not restate it here. For multi-page static sites, prefer a glob (`["public/**/*.html"]`) over a literal list. +2. Run `node .github/skills/impeccable/scripts/detect-csp.mjs`. If it reports a patchable shape (`append-arrays` / `append-string`), use the **consent prompt template** from live.md before editing any source file. On decline, skip the patch. For `middleware` / `meta-tag` shapes, surface the detected files and ask the user to add `http://localhost:8400` to `script-src` and `connect-src` manually. For `null`, there's nothing to do. +3. Set `cspChecked: true` in the config once CSP is handled (patched, declined, manual, or not needed). The schema and per-shape patch details live in live.md's First-time setup; follow it rather than duplicating. + +Writing the config file is harmless and needs no consent; only the CSP **source-file patch** requires a yes. + +## Step 7: Recommend starting points, then wrap up + +Summarize tersely: - Register captured (brand / product) -- What was written (PRODUCT.md, DESIGN.md, or both) +- What was written (PRODUCT.md, DESIGN.md, live config, or a subset) - The 3-5 strategic principles from PRODUCT.md that will guide future work -- If DESIGN.md is pending, remind the user how to generate it later +- If DESIGN.md or live config is pending, one line on how to set it up later -**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node .github/skills/impeccable/scripts/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. +Then recommend the **best commands to run next**, drawn from what your Step 2 crawl already surfaced. Do not run a fresh analysis here; surface observations you already have. Tailor to register and to what you saw, offer the 2-4 most relevant (not a menu dump), and give the exact command to type. Group by intent: -If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context. +- **Build something new**: `/impeccable craft ` (shape, then build end-to-end) or `/impeccable shape ` (plan first). Lead with this for empty or early-stage projects. +- **Improve what's there**: name the specific surface. `/impeccable critique ` for a scored UX review; `/impeccable audit ` for a11y / perf / responsive checks; `/impeccable polish ` for a pre-ship pass. When the crawl flagged a specific weakness, point the matching command at it: thin hierarchy or spacing → `layout`, flat or gray palette → `colorize`, missing error / empty states → `harden` or `onboard`, dull or unclear copy → `clarify`. +- **Iterate visually**: `/impeccable live` (configured in Step 6) to pick elements in the browser and generate variants in place. + +The full command menu is one bare `/impeccable` away; keep this list short and pointed. + +If init was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now. Your own writes are the freshest source; no reload needed. Optionally ask the user directly to clarify what you cannot infer. Ask whether they'd like a brief summary of PRODUCT.md appended to .github/copilot-instructions.md for easier agent reference. If yes, append a short **Design Context** pointer section there. diff --git a/.github/skills/impeccable/reference/interaction-design.md b/.github/skills/impeccable/reference/interaction-design.md index 15aed5b29..c42c9d810 100644 --- a/.github/skills/impeccable/reference/interaction-design.md +++ b/.github/skills/impeccable/reference/interaction-design.md @@ -150,12 +150,6 @@ For browsers without anchor positioning support, `position: fixed` with manual c Check viewport boundaries before rendering. If the dropdown would overflow the bottom edge, flip it above the trigger. If it would overflow the right edge, align it to the trigger's right side instead. -### Anti-Patterns - -- **`position: absolute` inside `overflow: hidden`** - The dropdown will be clipped. Use `position: fixed` or the top layer instead. -- **Arbitrary z-index values** like `z-index: 9999` - Use a semantic z-index scale: `dropdown (100) -> sticky (200) -> modal-backdrop (300) -> modal (400) -> toast (500) -> tooltip (600)`. -- **Rendering dropdown markup inline** without an escape hatch from the parent's stacking context. Either use `popover` (top layer), a portal, or `position: fixed`. - ## Destructive Actions: Undo > Confirm **Undo is better than confirmation dialogs.** Users click through confirmations mindlessly. Remove from UI immediately, show undo toast, actually delete after toast expires. Use confirmation only for truly irreversible actions (account deletion), high-cost actions, or batch operations. diff --git a/.github/skills/impeccable/reference/layout.md b/.github/skills/impeccable/reference/layout.md index cad4c62a1..8765978b0 100644 --- a/.github/skills/impeccable/reference/layout.md +++ b/.github/skills/impeccable/reference/layout.md @@ -1,4 +1,4 @@ -Space is the most underused design tool. Find the layout's actual problem (monotone spacing, weak hierarchy, identical card grids, the centered-stack default) and fix the structure, not the surface. +Space is the most underused design tool. Find the layout's actual problem (monotone spacing, weak hierarchy, identical card grids) and fix the structure, not the surface. --- @@ -27,7 +27,6 @@ Analyze what's weak about the current spatial design: 3. **Grid & structure**: - Is there a clear underlying structure, or does the layout feel random? - Are identical card grids used everywhere? (Icon + heading + text, repeated endlessly) - - Is everything centered? (Left-aligned with asymmetric layouts feels more designed, but not a hard and fast rule) 4. **Rhythm & variety**: - Does the layout have visual rhythm? (Alternating tight/generous spacing) @@ -43,8 +42,6 @@ Analyze what's weak about the current spatial design: ## Plan Layout Improvements -Consult the [spatial design reference](spatial-design.md) for detailed guidance on grids, rhythm, and container queries. - Create a systematic plan: - **Spacing system**: Use a consistent scale (a framework's built-in scale like Tailwind's, rem-based tokens, or a custom system). The specific values matter less than consistency. @@ -57,6 +54,7 @@ Create a systematic plan: ### Establish a Spacing System - Use a consistent spacing scale (framework scales like Tailwind, rem-based tokens, or a custom scale all work). What matters is that values come from a defined set, not arbitrary numbers. +- Prefer a 4pt base scale (4, 8, 12, 16, 24, 32, 48, 64, 96px) over 8pt; 8pt is too coarse and you'll frequently need 12px between 8 and 16. - Name tokens semantically if using custom properties: `--space-xs` through `--space-xl`, not `--spacing-8` - Use `gap` for sibling spacing instead of margins; eliminates margin collapse hacks - Apply `clamp()` for fluid spacing that breathes on larger screens @@ -66,15 +64,22 @@ Create a systematic plan: - **Tight grouping** for related elements (8-12px between siblings) - **Generous separation** between distinct sections (48-96px) - **Varied spacing** within sections (not every row needs the same gap) -- **Asymmetric compositions**: break the predictable centered-content pattern when it makes sense +- **Asymmetric compositions**: a deliberate choice when the content invites it (not a default to chase). ### Choose the Right Layout Tool -- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. Flex is simpler and more appropriate for the majority of layout tasks. +- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. - **Use Grid for 2D layouts**: Page-level structure, dashboards, data-dense interfaces, anything where rows AND columns need coordinated control. -- **Don't default to Grid** when Flexbox with `flex-wrap` would be simpler and more flexible. -- Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. - Use named grid areas (`grid-template-areas`) for complex page layouts; redefine at breakpoints. +- Use **container queries** for components, viewport queries for page layouts. A card in a narrow sidebar can stay compact while the same card in a main content area expands automatically: + +```css +.card-container { container-type: inline-size; } +.card { display: grid; gap: var(--space-md); } +@container (min-width: 400px) { + .card { grid-template-columns: 120px 1fr; } +} +``` ### Break Card Grid Monotony @@ -85,18 +90,36 @@ Create a systematic plan: ### Strengthen Visual Hierarchy - Use the fewest dimensions needed for clear hierarchy. Space alone can be enough; generous whitespace around an element draws the eye. Some of the most polished designs achieve rhythm with just space and weight. Add color or size contrast only when simpler means aren't sufficient. +- The best hierarchy combines 2–3 dimensions at once. A heading that's larger, bolder, AND has more space above it reads as primary without trying: + +| Tool | Strong Hierarchy | Weak Hierarchy | +|------|------------------|----------------| +| **Size** | 3:1 ratio or more | <2:1 ratio | +| **Weight** | Bold vs Regular | Medium vs Regular | +| **Color** | High contrast | Similar tones | +| **Position** | Top/left (primary) | Bottom/right | +| **Space** | Surrounded by white space | Crowded | + - Be aware of reading flow: in LTR languages, the eye naturally scans top-left to bottom-right, but primary action placement depends on context (e.g., bottom-right in dialogs, top in navigation). - Create clear content groupings through proximity and separation. ### Manage Depth & Elevation -- Create a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip) - Build a consistent shadow scale (sm → md → lg → xl); shadows should be subtle - Use elevation to reinforce hierarchy, not as decoration ### Optical Adjustments - If an icon looks visually off-center despite being geometrically centered, nudge it. But only if you're confident it actually looks wrong. Don't adjust speculatively. +- Text at `margin-left: 0` looks slightly indented because of letterform whitespace; a negative margin (`-0.05em`) optically aligns it. Geometrically centered glyphs often look off-center (play icons need to shift right, arrows shift toward their direction). +- Touch targets must be 44×44px minimum even when the visual element is smaller. Expand the hit area with padding or a pseudo-element: + +```css +.icon-button { width: 24px; height: 24px; position: relative; } +.icon-button::before { + content: ''; position: absolute; inset: -10px; +} +``` **NEVER**: - Use arbitrary spacing values outside your scale @@ -104,10 +127,7 @@ Create a systematic plan: - Wrap everything in cards (not everything needs a container) - Nest cards inside cards (use spacing and dividers for hierarchy within) - Use identical card grids everywhere (icon + heading + text, repeated) -- Center everything (left-aligned with asymmetry feels more designed) - Default to the hero metric layout (big number, small label, stats, gradient) as a template. If showing real user data, a prominent metric can work, but it should display actual data, not decorative numbers. -- Default to CSS Grid when Flexbox would be simpler; use the simplest tool for the job -- Use arbitrary z-index values (999, 9999); build a semantic scale ## Verify Layout Improvements diff --git a/.github/skills/impeccable/reference/live.md b/.github/skills/impeccable/reference/live.md index 0d92cd5c6..8e24c5506 100644 --- a/.github/skills/impeccable/reference/live.md +++ b/.github/skills/impeccable/reference/live.md @@ -9,18 +9,21 @@ A running dev server with hot module replacement (Vite, Next.js, Bun, etc.), OR Execute in order. No step skipped, no step reordered. 1. `live.mjs`: boot. -2. Navigate to the URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). If you can't infer it confidently, tell the user once to open their dev/preview URL. Never use `serverPort` as that URL; it's the helper, not the app. +2. Open the app URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). Never use `serverPort`; it's the helper, not the app. **Cursor:** `browser_navigate` to that URL before polling; do not skip. **Other harnesses:** use the available browser tool; if the URL is uncertain, ask the user once. 3. Poll loop with the default long timeout (600000 ms). After every event or `--reply`, run `live-poll.mjs` again immediately. Never pass a short `--timeout=`. + +The global bar **Impeccable mark** dims and shows a pulsing amber dot when no agent is long-polling `/poll`. Hover the mark for the hint; restart `live-poll.mjs` to reconnect. 4. On `generate`: read screenshot if present; load the action's reference; plan three distinct directions; write all variants in one edit; `--reply done`; poll again. -5. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts remain recoverable until you finish cleanup, run `live-complete.mjs --id EVENT_ID`, and only then poll again. -6. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart. -7. On `exit`: run the cleanup at the bottom. +5. On `steer`: read the message and `pageUrl`; do the work (page edits, navigation help, or a short reply in the `--reply` message); `--reply steer_done`; poll again. No pickup ack. The Steer bar unlocks when `steer_done` arrives over SSE. +6. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts remain recoverable until you finish cleanup, run `live-complete.mjs --id EVENT_ID`, and only then poll again. +7. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart. +8. On `exit`: run the cleanup at the bottom. Harness policy: - **Claude Code**: run the poll as a **background task** (no short timeout). The harness notifies you when it completes, so the main conversation stays free. Do not block the shell. -- **Cursor**: run the poll in the **foreground** (blocking shell; not a background terminal, not a subagent). Cursor background terminals and subagents do not reliably resume the chat with poll stdout. +- **Cursor**: run **one-shot** poll in a **background terminal** with notify on `"type":"(steer|generate|accept|discard|exit)"`. After each event the poll exits; handle it, `--reply`, then start `live-poll.mjs` again. Do **not** use `--stream` on Cursor: incremental stdout notify is slower in practice than exit-based notify (~5s vs sub-second in testing). - **Codex**: run the poll in the **foreground** (blocking shell; not a background task, not a subagent). Codex background exec sessions do not reliably surface poll stdout back into the conversation at the moment events arrive, so a "fire-and-forget" background poll will stall live mode. -- **Other harnesses**: foreground unless you know stdout reliably returns to this session. +- **Other harnesses**: one-shot foreground unless you know stdout reliably returns to this session when a shell exits. Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodies. Spend tokens on tools and edits; on failure, one or two short sentences. @@ -30,7 +33,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi node .github/skills/impeccable/scripts/live.mjs ``` -Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation; **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md. +Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation; **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. `serverPort` and `serverToken` belong to the small **Impeccable live helper** HTTP server (serves `/live.js`, SSE, and `/poll`). That port is **not** your dev server and is usually not the URL you open to view the app. The browser page is whatever origin serves one of the `pageFiles` entries (Vite / Next / Bun / tunnel / LAN hostname). @@ -38,12 +41,15 @@ If output is `{ ok: false, error: "config_missing" | "config_invalid", path }`, ## Poll loop +**Default (portable, all harnesses):** + ``` LOOP: node .github/skills/impeccable/scripts/live-poll.mjs # default long timeout; no --timeout= Read JSON; dispatch on "type" "generate" → Handle Generate; reply done; LOOP + "steer" → Handle Steer; reply steer_done; LOOP "accept" → Handle Accept; complete carbonize cleanup if required; LOOP "discard" → Handle Discard; LOOP "prefetch" → Handle Prefetch; LOOP @@ -51,6 +57,16 @@ LOOP: "exit" → break → Cleanup ``` +**Stream mode (experimental, not for Cursor):** + +``` +node .github/skills/impeccable/scripts/live-poll.mjs --stream # stays running; one JSON line per event + Handle event; run --reply in a separate command + Repeat until "exit" line → Cleanup +``` + +Stream keeps one process alive and waits for `--reply` ack before polling again. Useful only when the harness reads incremental stdout reliably and quickly. **Cursor is not one of those:** background pattern notify on a long-running shell was ~5s to pick up events vs sub-second for one-shot exit notify. Default to one-shot everywhere unless you have measured otherwise. + ## Recovery commands The live helper persists an append-only journal under `.impeccable/live/sessions/`. Browser checkpoints are advisory but durable; the journal is canonical. This is local durable recovery state, not project source. @@ -71,9 +87,32 @@ Server restart rule: start `live-server.mjs` again, then poll. Startup requeues ## Handle `generate` -Event: `{id, action, freeformPrompt?, count, pageUrl, element, screenshotPath?, comments?, strokes?}`. +**Replace mode** (default): `{id, action, freeformPrompt?, count, pageUrl, element, screenshotPath?, comments?, strokes?}`. -Speed matters; the user is watching a spinner. Minimize tool calls by using the `wrap` helper and writing all variants in a single edit. +**Insert mode** (`event.mode === "insert"`): `{id, mode: "insert", count, pageUrl, insert: { position, anchor }, placeholder: { width, height }, freeformPrompt?, screenshotPath?, comments?, strokes?}`. No `action`. Requires a non-empty `freeformPrompt` **or** annotations. Screenshot is sent only when annotations exist (same rule as replace). Use `placeholder` dimensions as a soft size hint for net-new content. + +Speed matters; the user is watching a spinner. Minimize tool calls by using the wrap/insert helper and writing all variants in a single edit. + +### Insert mode branch + +When `event.mode === "insert"`: + +1. Read the screenshot if `event.screenshotPath` is present (annotations only). +2. Run the insert helper instead of wrap: + +```bash +node .github/skills/impeccable/scripts/live-insert.mjs --id EVENT_ID --count EVENT_COUNT --position after \ + --element-id "ANCHOR_ID" --classes "class1,class2" --tag "section" --text "ANCHOR_TEXT" +``` + +- `--position` ← `event.insert.position` (`before` | `after`) +- Anchor flags ← `event.insert.anchor` (same mapping as wrap: id, classes, tag, text) + +The scaffold has **no** `data-impeccable-variant="original"`. Variants are net-new HTML+CSS inserted at `insertLine`. Load `brand.md` or `product.md` (freeform only, no action sub-command). Write all variants in one edit, then `--reply done`. + +On accept/discard, `live-accept.mjs` removes the wrapper block; the anchor element is untouched. + +### Replace mode (default) ### 1. Read the screenshot (if present) @@ -424,6 +463,28 @@ A background agent may be used for the rewrite, but the current thread is respon Event: `{id, _acceptResult, _completionAck}`. The poll script already restored the original, removed all variant markers, and acknowledged `discarded` durable completion. Nothing to do unless `_completionAck.ok !== true`; in that case run `live-complete.mjs --id EVENT_ID --discarded`, then poll again. +## Handle `steer` + +Event: `{id, message, pageUrl}`. The user typed or spoke into the global bar **Steer** control: page-level direction without picking an element or launching variant generation. + +The mic button uses the browser **Web Speech API** (MVP): click to start, speak, stop automatically when the utterance ends, then the transcript submits as a steer event. Click again while listening to cancel without submitting. + +This is lighter than `generate`: no screenshot, no element context, no variant cycling. Read `message` and inspect the live page or project files as needed, then either make edits or answer in prose. + +When finished: + +```bash +node .github/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID steer_done ["Optional short note for a browser toast"] +``` + +On failure: + +```bash +node .github/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID error "Short reason" +``` + +Then poll again immediately. Do not send a separate "picked up" reply. The Steer bar stays locked until `steer_done` or `error` arrives over SSE. + ## Handle `prefetch` Event: `{pageUrl}`. The browser fires this the first time the user selects an element on a given route, as a latency shortcut; it signals the user is likely about to Go on a page you haven't read yet. diff --git a/.github/skills/impeccable/reference/motion-design.md b/.github/skills/impeccable/reference/motion-design.md deleted file mode 100644 index 78b6fd260..000000000 --- a/.github/skills/impeccable/reference/motion-design.md +++ /dev/null @@ -1,109 +0,0 @@ -# Motion Design - -## Duration: The 100/300/500 Rule - -Timing matters more than easing. These durations feel right for most UI: - -| Duration | Use Case | Examples | -|----------|----------|----------| -| **100-150ms** | Instant feedback | Button press, toggle, color change | -| **200-300ms** | State changes | Menu open, tooltip, hover states | -| **300-500ms** | Layout changes | Accordion, modal, drawer | -| **500-800ms** | Entrance animations | Page load, hero reveals | - -**Exit animations are faster than entrances.** Use ~75% of enter duration. - -## Easing: Pick the Right Curve - -**Don't use `ease`.** It's a compromise that's rarely optimal. Instead: - -| Curve | Use For | CSS | -|-------|---------|-----| -| **ease-out** | Elements entering | `cubic-bezier(0.16, 1, 0.3, 1)` | -| **ease-in** | Elements leaving | `cubic-bezier(0.7, 0, 0.84, 0)` | -| **ease-in-out** | State toggles (there → back) | `cubic-bezier(0.65, 0, 0.35, 1)` | - -**For micro-interactions, use exponential curves.** They feel natural because they mimic real physics (friction, deceleration): - -```css -/* Quart out - smooth, refined (recommended default) */ ---ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1); - -/* Quint out - slightly more dramatic */ ---ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1); - -/* Expo out - snappy, confident */ ---ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); -``` - -**Avoid bounce and elastic curves.** They were trendy in 2015 but now feel tacky and amateurish. Real objects don't bounce when they stop; they decelerate smoothly. Overshoot effects draw attention to the animation itself rather than the content. - -## Premium Motion Materials - -Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties: blur reveals, backdrop-filter panels, saturation or brightness shifts, shadow bloom, SVG filters, masks, clip paths, gradient-position movement, and variable font or shader-driven effects. - -Use the right material for the effect: - -- **Transform / opacity**: movement, press feedback, simple reveals, list choreography. -- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances, atmospheric transitions. -- **Clip path / masks**: wipes, reveals, editorial cropping, product-like transitions. -- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state. -- **Grid-template rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly. - -The hard rule is not "transform and opacity only." The hard rule is: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify in-browser that the result is smooth on the target viewports. If blur/filter makes the interaction feel significantly more premium and remains smooth, use it. - -## Staggered Animations - -Use CSS custom properties for cleaner stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"` on each item. **Cap total stagger time**: 10 items at 50ms = 500ms total. For many items, reduce per-item delay or cap staggered count. - -## Reduced Motion - -This is not optional. Vestibular disorders affect ~35% of adults over 40. - -```css -/* Define animations normally */ -.card { - animation: slide-up 500ms ease-out; -} - -/* Provide alternative for reduced motion */ -@media (prefers-reduced-motion: reduce) { - .card { - animation: fade-in 200ms ease-out; /* Crossfade instead of motion */ - } -} - -/* Or disable entirely */ -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - transition-duration: 0.01ms !important; - } -} -``` - -**What to preserve**: Functional animations like progress bars, loading spinners (slowed down), and focus indicators should still work, just without spatial movement. - -## Perceived Performance - -**Nobody cares how fast your site is, just how fast it feels.** Perception can be as effective as actual performance. - -**The 80ms threshold**: Our brains buffer sensory input for ~80ms to synchronize perception. Anything under 80ms feels instant and simultaneous. This is your target for micro-interactions. - -**Active vs passive time**: Passive waiting (staring at a spinner) feels longer than active engagement. Strategies to shift the balance: - -- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening. -- **Early completion**: Show content progressively, don't wait for everything. Video buffering, progressive images, streaming HTML. -- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Instagram likes work offline; the UI updates instantly, syncs later. Use for low-stakes actions; avoid for payments or destructive operations. - -**Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances, but ease-in toward a task's end compresses perceived time. - -**Caution**: Too-fast responses can decrease perceived value. Users may distrust instant results for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening. - -## Performance - -Don't use `will-change` preemptively, only when animation is imminent (`:hover`, `.animating`). For scroll-triggered animations, use Intersection Observer instead of scroll events; unobserve after animating once. Create motion tokens for consistency (durations, easings, common transitions). - ---- - -**Avoid**: Animating everything (animation fatigue is real). Using >500ms for UI feedback. Ignoring `prefers-reduced-motion`. Using animation to hide slow loading. diff --git a/.github/skills/impeccable/reference/personas.md b/.github/skills/impeccable/reference/personas.md deleted file mode 100644 index e51a4cfe7..000000000 --- a/.github/skills/impeccable/reference/personas.md +++ /dev/null @@ -1,179 +0,0 @@ -# Persona-Based Design Testing - -Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss. - -**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns. - ---- - -## 1. Impatient Power User: "Alex" - - -**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave. - -**Behaviors**: -- Skips all onboarding and instructions -- Looks for keyboard shortcuts immediately -- Tries to bulk-select, batch-edit, and automate -- Gets frustrated by required steps that feel unnecessary -- Abandons if anything feels slow or patronizing - -**Test Questions**: -- Can Alex complete the core task in under 60 seconds? -- Are there keyboard shortcuts for common actions? -- Can onboarding be skipped entirely? -- Do modals have keyboard dismiss (Esc)? -- Is there a "power user" path (shortcuts, bulk actions)? - -**Red Flags** (report these specifically): -- Forced tutorials or unskippable onboarding -- No keyboard navigation for primary actions -- Slow animations that can't be skipped -- One-item-at-a-time workflows where batch would be natural -- Redundant confirmation steps for low-risk actions - ---- - -## 2. Confused First-Timer: "Jordan" - -**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out. - -**Behaviors**: -- Reads all instructions carefully -- Hesitates before clicking anything unfamiliar -- Looks for help or support constantly -- Misunderstands jargon and abbreviations -- Takes the most literal interpretation of any label - -**Test Questions**: -- Is the first action obviously clear within 5 seconds? -- Are all icons labeled with text? -- Is there contextual help at decision points? -- Does terminology assume prior knowledge? -- Is there a clear "back" or "undo" at every step? - -**Red Flags** (report these specifically): -- Icon-only navigation with no labels -- Technical jargon without explanation -- No visible help option or guidance -- Ambiguous next steps after completing an action -- No confirmation that an action succeeded - ---- - -## 3. Accessibility-Dependent User: "Sam" - -**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences. - -**Behaviors**: -- Tabs through the interface linearly -- Relies on ARIA labels and heading structure -- Cannot see hover states or visual-only indicators -- Needs adequate color contrast (4.5:1 minimum) -- May use browser zoom up to 200% - -**Test Questions**: -- Can the entire primary flow be completed keyboard-only? -- Are all interactive elements focusable with visible focus indicators? -- Do images have meaningful alt text? -- Is color contrast WCAG AA compliant (4.5:1 for text)? -- Does the screen reader announce state changes (loading, success, errors)? - -**Red Flags** (report these specifically): -- Click-only interactions with no keyboard alternative -- Missing or invisible focus indicators -- Meaning conveyed by color alone (red = error, green = success) -- Unlabeled form fields or buttons -- Time-limited actions without extension option -- Custom components that break screen reader flow - ---- - -## 4. Deliberate Stress Tester: "Riley" - -**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience. - -**Behaviors**: -- Tests edge cases intentionally (empty states, long strings, special characters) -- Submits forms with unexpected data (emoji, RTL text, very long values) -- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs -- Looks for inconsistencies between what the UI promises and what actually happens -- Documents problems methodically - -**Test Questions**: -- What happens at the edges (0 items, 1000 items, very long text)? -- Do error states recover gracefully or leave the UI in a broken state? -- What happens on refresh mid-workflow? Is state preserved? -- Are there features that appear to work but produce broken results? -- How does the UI handle unexpected input (emoji, special chars, paste from Excel)? - -**Red Flags** (report these specifically): -- Features that appear to work but silently fail or produce wrong results -- Error handling that exposes technical details or leaves UI in a broken state -- Empty states that show nothing useful ("No results" with no guidance) -- Workflows that lose user data on refresh or navigation -- Inconsistent behavior between similar interactions in different parts of the UI - ---- - -## 5. Distracted Mobile User: "Casey" - -**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection. - -**Behaviors**: -- Uses thumb only; prefers bottom-of-screen actions -- Gets interrupted mid-flow and returns later -- Switches between apps frequently -- Has limited attention span and low patience -- Types as little as possible, prefers taps and selections - -**Test Questions**: -- Are primary actions in the thumb zone (bottom half of screen)? -- Is state preserved if the user leaves and returns? -- Does it work on slow connections (3G)? -- Can forms use autocomplete and smart defaults? -- Are touch targets at least 44×44pt? - -**Red Flags** (report these specifically): -- Important actions positioned at the top of the screen (unreachable by thumb) -- No state persistence; progress lost on tab switch or interruption -- Large text inputs required where selection would work -- Heavy assets loading on every page (no lazy loading) -- Tiny tap targets or targets too close together - ---- - -## Selecting Personas - -Choose personas based on the interface type: - -| Interface Type | Primary Personas | Why | -|---------------|-----------------|-----| -| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile | -| Dashboard / admin | Alex, Sam | Power users, accessibility | -| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity | -| Onboarding flow | Jordan, Casey | Confusion, interruption | -| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav | -| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile | - ---- - -## Project-Specific Personas - -If `.github/copilot-instructions.md` contains a `## Design Context` section (generated by `impeccable teach`), derive 1–2 additional personas from the audience and brand information: - -1. Read the target audience description -2. Identify the primary user archetype not covered by the 5 predefined personas -3. Create a persona following this template: - -``` -### [Role]: "[Name]" - -**Profile**: [2-3 key characteristics derived from Design Context] - -**Behaviors**: [3-4 specific behaviors based on the described audience] - -**Red Flags**: [3-4 things that would alienate this specific user type] -``` - -Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists. diff --git a/.github/skills/impeccable/reference/polish.md b/.github/skills/impeccable/reference/polish.md index 836f0d8e4..d19851ac2 100644 --- a/.github/skills/impeccable/reference/polish.md +++ b/.github/skills/impeccable/reference/polish.md @@ -91,7 +91,6 @@ Visual polish on a misshapen flow is wasted work. Match the *shape* of the exper - **Theme consistency**: Works in all theme variants - **Color meaning**: Same colors mean same things throughout - **Accessible focus**: Focus indicators visible with sufficient contrast -- **Tinted neutrals**: No pure gray or pure black; add subtle color tint (0.01 chroma) - **Gray on color**: Never put gray text on colored backgrounds; use a shade of that color or transparency ### Interaction States diff --git a/.github/skills/impeccable/reference/product.md b/.github/skills/impeccable/reference/product.md index 64b3b0169..2ca462350 100644 --- a/.github/skills/impeccable/reference/product.md +++ b/.github/skills/impeccable/reference/product.md @@ -10,7 +10,6 @@ Product UI's failure mode isn't flatness, it's strangeness without purpose: over ## Typography -- **System fonts are legitimate.** `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif` gives you native feel on every platform. Inter is the common cross-platform default for a reason. - **One family is often right.** Product UIs don't need display/body pairing. A well-tuned sans carries headings, buttons, labels, body, data. - **Fixed rem scale, not fluid.** Clamp-sized headings don't serve product UI. Users view at consistent DPI, and a fluid h1 that shrinks in a sidebar looks worse, not better. - **Tighter scale ratio.** 1.125–1.2 between steps is typical. More type elements here than on brand surfaces; exaggerated contrast creates noise. @@ -26,8 +25,6 @@ Product defaults to Restrained. A single surface can earn Committed (a dashboard ## Layout -- Predictable grids. Consistency IS an affordance; users navigate faster when the structure is expected. -- Familiar patterns are features. Standard navigation (top bar, side nav), breadcrumbs, tabs, and form layouts have established user expectations. Don't reinvent for flavor. - Responsive behavior is structural (collapse sidebar, responsive table, breakpoint-driven columns), not fluid typography. ## Components @@ -51,6 +48,7 @@ Every interactive component has: default, hover, focus, active, disabled, loadin - Display fonts in UI labels, buttons, data. - Reinventing standard affordances for flavor (custom scrollbars, weird form controls, non-standard modals). - Heavy color or full-saturation accents on inactive states. +- Modal as first thought. Modals are usually laziness. Exhaust inline / progressive alternatives first. ## Product permissions diff --git a/.github/skills/impeccable/reference/responsive-design.md b/.github/skills/impeccable/reference/responsive-design.md deleted file mode 100644 index f079e5e5d..000000000 --- a/.github/skills/impeccable/reference/responsive-design.md +++ /dev/null @@ -1,114 +0,0 @@ -# Responsive Design - -## Mobile-First: Write It Right - -Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first. - -## Breakpoints: Content-Driven - -Don't chase device sizes; let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints. - -## Detect Input Method, Not Just Screen Size - -**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard. Use pointer and hover queries: - -```css -/* Fine pointer (mouse, trackpad) */ -@media (pointer: fine) { - .button { padding: 8px 16px; } -} - -/* Coarse pointer (touch, stylus) */ -@media (pointer: coarse) { - .button { padding: 12px 20px; } /* Larger touch target */ -} - -/* Device supports hover */ -@media (hover: hover) { - .card:hover { transform: translateY(-2px); } -} - -/* Device doesn't support hover (touch) */ -@media (hover: none) { - .card { /* No hover state - use active instead */ } -} -``` - -**Critical**: Don't rely on hover for functionality. Touch users can't hover. - -## Safe Areas: Handle the Notch - -Modern phones have notches, rounded corners, and home indicators. Use `env()`: - -```css -body { - padding-top: env(safe-area-inset-top); - padding-bottom: env(safe-area-inset-bottom); - padding-left: env(safe-area-inset-left); - padding-right: env(safe-area-inset-right); -} - -/* With fallback */ -.footer { - padding-bottom: max(1rem, env(safe-area-inset-bottom)); -} -``` - -**Enable viewport-fit** in your meta tag: -```html - -``` - -## Responsive Images: Get It Right - -### srcset with Width Descriptors - -```html -Hero image -``` - -**How it works**: -- `srcset` lists available images with their actual widths (`w` descriptors) -- `sizes` tells the browser how wide the image will display -- Browser picks the best file based on viewport width AND device pixel ratio - -### Picture Element for Art Direction - -When you need different crops/compositions (not just resolutions): - -```html - - - - ... - -``` - -## Layout Adaptation Patterns - -**Navigation**: Three stages: hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `
/` for content that can collapse on mobile. - -## Testing: Don't Trust DevTools Alone - -DevTools device emulation is useful for layout but misses: - -- Actual touch interactions -- Real CPU/memory constraints -- Network latency patterns -- Font rendering differences -- Browser chrome/keyboard appearances - -**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators. - ---- - -**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful. diff --git a/.github/skills/impeccable/reference/shape.md b/.github/skills/impeccable/reference/shape.md index f7c4cd2f1..592def8a7 100644 --- a/.github/skills/impeccable/reference/shape.md +++ b/.github/skills/impeccable/reference/shape.md @@ -151,7 +151,7 @@ How users interact with this feature. What happens on click, hover, scroll? What 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., spatial-design.md for complex layouts, motion-design.md for animated features, interaction-design.md for form-heavy features). +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. diff --git a/.github/skills/impeccable/reference/spatial-design.md b/.github/skills/impeccable/reference/spatial-design.md deleted file mode 100644 index 4d4b83afc..000000000 --- a/.github/skills/impeccable/reference/spatial-design.md +++ /dev/null @@ -1,100 +0,0 @@ -# Spatial Design - -## Spacing Systems - -### Use 4pt Base, Not 8pt - -8pt systems are too coarse; you'll frequently need 12px (between 8 and 16). Use 4pt for granularity: 4, 8, 12, 16, 24, 32, 48, 64, 96px. - -### Name Tokens Semantically - -Name by relationship (`--space-sm`, `--space-lg`), not value (`--spacing-8`). Use `gap` instead of margins for sibling spacing; it eliminates margin collapse and cleanup hacks. - -## Grid Systems - -### The Self-Adjusting Grid - -Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. Columns are at least 280px, as many as fit per row, leftovers stretch. For complex layouts, use named grid areas (`grid-template-areas`) and redefine them at breakpoints. - -## Visual Hierarchy - -### The Squint Test - -Blur your eyes (or screenshot and blur). Can you still identify: -- The most important element? -- The second most important? -- Clear groupings? - -If everything looks the same weight blurred, you have a hierarchy problem. - -### Hierarchy Through Multiple Dimensions - -Don't rely on size alone. Combine: - -| Tool | Strong Hierarchy | Weak Hierarchy | -|------|------------------|----------------| -| **Size** | 3:1 ratio or more | <2:1 ratio | -| **Weight** | Bold vs Regular | Medium vs Regular | -| **Color** | High contrast | Similar tones | -| **Position** | Top/left (primary) | Bottom/right | -| **Space** | Surrounded by white space | Crowded | - -**The best hierarchy uses 2-3 dimensions at once**: A heading that's larger, bolder, AND has more space above it. - -### Cards Are Not Required - -Cards are overused. Spacing and alignment create visual grouping naturally. Use cards only when content is truly distinct and actionable, items need visual comparison in a grid, or content needs clear interaction boundaries. **Never nest cards inside cards.** Use spacing, typography, and subtle dividers for hierarchy within a card. - -## Container Queries - -Viewport queries are for page layouts. **Container queries are for components**: - -```css -.card-container { - container-type: inline-size; -} - -.card { - display: grid; - gap: var(--space-md); -} - -/* Card layout changes based on its container, not viewport */ -@container (min-width: 400px) { - .card { - grid-template-columns: 120px 1fr; - } -} -``` - -**Why this matters**: A card in a narrow sidebar stays compact, while the same card in a main content area expands automatically, without viewport hacks. - -## Optical Adjustments - -Text at `margin-left: 0` looks indented due to letterform whitespace; use negative margin (`-0.05em`) to optically align. Geometrically centered icons often look off-center; play icons need to shift right, arrows shift toward their direction. - -### Touch Targets vs Visual Size - -Buttons can look small but need large touch targets (44px minimum). Use padding or pseudo-elements: - -```css -.icon-button { - width: 24px; /* Visual size */ - height: 24px; - position: relative; -} - -.icon-button::before { - content: ''; - position: absolute; - inset: -10px; /* Expand tap target to 44px */ -} -``` - -## Depth & Elevation - -Create semantic z-index scales (dropdown → sticky → modal-backdrop → modal → toast → tooltip) instead of arbitrary numbers. For shadows, create a consistent elevation scale (sm → md → lg → xl). **Key insight**: Shadows should be subtle. If you can clearly see it, it's probably too strong. - ---- - -**Avoid**: Arbitrary spacing values outside your scale. Making all spacing equal (variety creates hierarchy). Creating hierarchy through size alone - combine size, weight, color, and space. diff --git a/.github/skills/impeccable/reference/typeset.md b/.github/skills/impeccable/reference/typeset.md index dd3e67ea3..5f5e0c2c0 100644 --- a/.github/skills/impeccable/reference/typeset.md +++ b/.github/skills/impeccable/reference/typeset.md @@ -4,7 +4,7 @@ Typography carries most of the information on the page. Replace generic defaults ## Register -Brand: run the font selection procedure in [brand.md](brand.md). Pairing follows the brand's lane (display serif + sans body for editorial/luxury, one committed sans for tech, etc.). Fluid `clamp()` scale, ≥1.25 ratio between steps. +Brand: run the font selection procedure in [brand.md](brand.md). Fluid `clamp()` scale, ≥1.25 ratio between steps. Product: system fonts and familiar sans stacks are legitimate here. One well-tuned family typically carries the whole UI. Fixed `rem` scale, 1.125–1.2 ratio between more closely-spaced steps. @@ -43,7 +43,7 @@ Analyze what's weak or generic about the current type: ## Plan Typography Improvements -Consult the [typography reference](typography.md) for detailed guidance on scales, pairing, and loading strategies. +Consult the [Reference Material](#reference-material) section below for detailed guidance on scales, pairing, and loading strategies. Create a systematic plan: @@ -122,3 +122,158 @@ Each variant MUST declare a `scale` param controlling the hierarchy ratio. Expre Where the variant riffs on a specific pairing, expose the pairing choice as a `steps` param (e.g. "serif display + sans body" vs. "mono display + sans body" vs. "all-sans"). Each branch routes through `:scope[data-p-pairing="X"]` selectors in scoped CSS. See `reference/live.md` for the full params contract. + +--- + +## Reference Material + +The sections below were previously `typography.md` and live inline now so the typeset flow has its deep typography reference in one place. `bolder.md` also references this section. + +### Typography + +#### Classic Typography Principles + +##### Vertical Rhythm + +Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony; text and space share a mathematical foundation. + +##### Modular Scale & Hierarchy + +The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy. + +**Use fewer sizes with more contrast.** A 5-size system covers most needs: + +| Role | Typical Ratio | Use Case | +|------|---------------|----------| +| xs | 0.75rem | Captions, legal | +| sm | 0.875rem | Secondary UI, metadata | +| base | 1rem | Body text | +| lg | 1.25-1.5rem | Subheadings, lead text | +| xl+ | 2-4rem | Headlines, hero text | + +Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit. + +##### Readability & Measure + +Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length: narrow columns need tighter leading, wide columns need more. + +**Non-obvious**: Light text on dark backgrounds needs compensation on three axes, not just one. Bump line-height by 0.05–0.1, add a touch of letter-spacing (0.01–0.02em), and optionally step the body weight up one notch (regular → medium). The perceived weight drops across all three; fix all three. + +**Paragraph rhythm**: Pick either space between paragraphs OR first-line indentation. Never both. Digital usually wants space; editorial/long-form can justify indent-only. + +#### Font Selection & Pairing + +The tactical selection procedure and the reflex-reject list live in [reference/brand.md](brand.md) under **Font selection procedure** and **Reflex-reject list** (loaded for brand-register tasks). The rest of this section covers the adjacent knowledge: anti-reflex corrections, system font use, and pairing rules. + +##### Anti-reflexes worth defending against + +- A technical/utilitarian brief does NOT need a serif "for warmth." Most tech tools should look like tech tools. +- An editorial/premium brief does NOT need the same expressive serif everyone is using right now. Premium can be Swiss-modern, can be neo-grotesque, can be a literal monospace, can be a quiet humanist sans. +- A children's product does NOT need a rounded display font. Kids' books use real type. +- A "modern" brief does NOT need a geometric sans. The most modern thing you can do is not use the font everyone else is using. + +**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality. + +##### Pairing Principles + +**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif). + +When pairing, contrast on multiple axes: +- Serif + Sans (structure contrast) +- Geometric + Humanist (personality contrast) +- Condensed display + Wide body (proportion contrast) + +##### Web Font Loading + +The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix: + +```css +/* 1. Use font-display: swap for visibility */ +@font-face { + font-family: 'CustomFont'; + src: url('font.woff2') format('woff2'); + font-display: swap; +} + +/* 2. Match fallback metrics to minimize shift */ +@font-face { + font-family: 'CustomFont-Fallback'; + src: local('Arial'); + size-adjust: 105%; /* Scale to match x-height */ + ascent-override: 90%; /* Match ascender height */ + descent-override: 20%; /* Match descender depth */ + line-gap-override: 10%; /* Match line spacing */ +} + +body { + font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; +} +``` + +Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically. + +**`swap` vs `optional`**: `swap` shows fallback text immediately and FOUT-swaps when the web font arrives. `optional` uses the fallback if the web font misses a small load budget (~100ms) and avoids the shift entirely. Pick `optional` when zero layout shift matters more than seeing the branded font on slow networks. + +**Preload the critical weight only**: typically the regular-weight body font used above the fold. Preloading every weight costs more bandwidth than it saves. + +**Variable fonts for 3+ weights or styles**: a single variable font file is usually smaller than three static weight files, gives fractional weight control, and pairs well with `font-optical-sizing: auto`. For 1–2 weights, static is fine. + +#### Modern Web Typography + +##### Fluid Type + +Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate (higher vw = faster scaling). Add a rem offset so it doesn't collapse to 0 on small screens. + +**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes. + +**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI; fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it. + +**Bound your clamp()**: keep `max-size ≤ ~2.5 × min-size`. Wider ratios break the browser's zoom and reflow behaviour and make large viewports feel like the page is shouting. + +**Scale container width and font-size together** so effective character measure stays in the 45–75ch band at every viewport. A heading that widens faster than its container drifts out of the comfortable measure at the top end. + +##### OpenType Features + +Most developers don't know these exist. Use them for polish: + +```css +/* Proper fractions */ +.recipe-amount { font-variant-numeric: diagonal-fractions; } + +/* Small caps for abbreviations */ +abbr { font-variant-caps: all-small-caps; } + +/* Disable ligatures in code */ +code { font-variant-ligatures: none; } + +/* Enable kerning (usually on by default, but be explicit) */ +body { font-kerning: normal; } +``` + +Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/). + +##### Rendering polish + +```css +/* Variable fonts: pick the right optical-size master automatically */ +body { font-optical-sizing: auto; } +``` + +**ALL-CAPS tracking**: capitals sit too close at default spacing. Add 5–12% letter-spacing (`letter-spacing: 0.05em` to `0.12em`) to short all-caps labels, eyebrows, and small headings. Real small caps (via `font-variant-caps`) need the same treatment, slightly gentler. + +#### Typography System Architecture + +Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system. + +#### Accessibility Considerations + +Beyond contrast ratios (which are well-documented), consider: + +- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout. +- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text. +- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile. +- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets. + +--- + +**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text. diff --git a/.github/skills/impeccable/reference/typography.md b/.github/skills/impeccable/reference/typography.md deleted file mode 100644 index ed5ca272a..000000000 --- a/.github/skills/impeccable/reference/typography.md +++ /dev/null @@ -1,159 +0,0 @@ -# Typography - -## Classic Typography Principles - -### Vertical Rhythm - -Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony; text and space share a mathematical foundation. - -### Modular Scale & Hierarchy - -The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy. - -**Use fewer sizes with more contrast.** A 5-size system covers most needs: - -| Role | Typical Ratio | Use Case | -|------|---------------|----------| -| xs | 0.75rem | Captions, legal | -| sm | 0.875rem | Secondary UI, metadata | -| base | 1rem | Body text | -| lg | 1.25-1.5rem | Subheadings, lead text | -| xl+ | 2-4rem | Headlines, hero text | - -Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit. - -### Readability & Measure - -Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length: narrow columns need tighter leading, wide columns need more. - -**Non-obvious**: Light text on dark backgrounds needs compensation on three axes, not just one. Bump line-height by 0.05–0.1, add a touch of letter-spacing (0.01–0.02em), and optionally step the body weight up one notch (regular → medium). The perceived weight drops across all three; fix all three. - -**Paragraph rhythm**: Pick either space between paragraphs OR first-line indentation. Never both. Digital usually wants space; editorial/long-form can justify indent-only. - -## Font Selection & Pairing - -The tactical selection procedure and the reflex-reject list live in [reference/brand.md](brand.md) under **Font selection procedure** and **Reflex-reject list** (loaded for brand-register tasks). The rest of this section covers the adjacent knowledge: anti-reflex corrections, system font use, and pairing rules. - -### Anti-reflexes worth defending against - -- A technical/utilitarian brief does NOT need a serif "for warmth." Most tech tools should look like tech tools. -- An editorial/premium brief does NOT need the same expressive serif everyone is using right now. Premium can be Swiss-modern, can be neo-grotesque, can be a literal monospace, can be a quiet humanist sans. -- A children's product does NOT need a rounded display font. Kids' books use real type. -- A "modern" brief does NOT need a geometric sans. The most modern thing you can do is not use the font everyone else is using. - -**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality. - -### Pairing Principles - -**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif). - -When pairing, contrast on multiple axes: -- Serif + Sans (structure contrast) -- Geometric + Humanist (personality contrast) -- Condensed display + Wide body (proportion contrast) - -**Never pair fonts that are similar but not identical** (e.g., two geometric sans-serifs). They create visual tension without clear hierarchy. - -### Web Font Loading - -The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix: - -```css -/* 1. Use font-display: swap for visibility */ -@font-face { - font-family: 'CustomFont'; - src: url('font.woff2') format('woff2'); - font-display: swap; -} - -/* 2. Match fallback metrics to minimize shift */ -@font-face { - font-family: 'CustomFont-Fallback'; - src: local('Arial'); - size-adjust: 105%; /* Scale to match x-height */ - ascent-override: 90%; /* Match ascender height */ - descent-override: 20%; /* Match descender depth */ - line-gap-override: 10%; /* Match line spacing */ -} - -body { - font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; -} -``` - -Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically. - -**`swap` vs `optional`**: `swap` shows fallback text immediately and FOUT-swaps when the web font arrives. `optional` uses the fallback if the web font misses a small load budget (~100ms) and avoids the shift entirely. Pick `optional` when zero layout shift matters more than seeing the branded font on slow networks. - -**Preload the critical weight only**: typically the regular-weight body font used above the fold. Preloading every weight costs more bandwidth than it saves. - -**Variable fonts for 3+ weights or styles**: a single variable font file is usually smaller than three static weight files, gives fractional weight control, and pairs well with `font-optical-sizing: auto`. For 1–2 weights, static is fine. - -## Modern Web Typography - -### Fluid Type - -Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate (higher vw = faster scaling). Add a rem offset so it doesn't collapse to 0 on small screens. - -**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes. - -**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI; fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it. - -**Bound your clamp()**: keep `max-size ≤ ~2.5 × min-size`. Wider ratios break the browser's zoom and reflow behaviour and make large viewports feel like the page is shouting. - -**Scale container width and font-size together** so effective character measure stays in the 45–75ch band at every viewport. A heading that widens faster than its container drifts out of the comfortable measure at the top end. - -### OpenType Features - -Most developers don't know these exist. Use them for polish: - -```css -/* Tabular numbers for data alignment */ -.data-table { font-variant-numeric: tabular-nums; } - -/* Proper fractions */ -.recipe-amount { font-variant-numeric: diagonal-fractions; } - -/* Small caps for abbreviations */ -abbr { font-variant-caps: all-small-caps; } - -/* Disable ligatures in code */ -code { font-variant-ligatures: none; } - -/* Enable kerning (usually on by default, but be explicit) */ -body { font-kerning: normal; } -``` - -Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/). - -### Rendering polish - -```css -/* Even out heading line lengths (browser picks better break points) */ -h1, h2, h3 { text-wrap: balance; } - -/* Reduce orphans and ragged endings in long prose */ -article p { text-wrap: pretty; } - -/* Variable fonts: pick the right optical-size master automatically */ -body { font-optical-sizing: auto; } -``` - -**ALL-CAPS tracking**: capitals sit too close at default spacing. Add 5–12% letter-spacing (`letter-spacing: 0.05em` to `0.12em`) to short all-caps labels, eyebrows, and small headings. Real small caps (via `font-variant-caps`) need the same treatment, slightly gentler. - -## Typography System Architecture - -Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system. - -## Accessibility Considerations - -Beyond contrast ratios (which are well-documented), consider: - -- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout. -- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text. -- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile. -- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets. - ---- - -**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text. diff --git a/.github/skills/impeccable/reference/ux-writing.md b/.github/skills/impeccable/reference/ux-writing.md deleted file mode 100644 index 417b5f56c..000000000 --- a/.github/skills/impeccable/reference/ux-writing.md +++ /dev/null @@ -1,107 +0,0 @@ -# UX Writing - -## The Button Label Problem - -**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns: - -| Bad | Good | Why | -|-----|------|-----| -| OK | Save changes | Says what will happen | -| Submit | Create account | Outcome-focused | -| Yes | Delete message | Confirms the action | -| Cancel | Keep editing | Clarifies what "cancel" means | -| Click here | Download PDF | Describes the destination | - -**For destructive actions**, name the destruction: -- "Delete" not "Remove" (delete is permanent, remove implies recoverable) -- "Delete 5 items" not "Delete selected" (show the count) - -## Error Messages: The Formula - -Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input". - -### Error Message Templates - -| Situation | Template | -|-----------|----------| -| **Format error** | "[Field] needs to be [format]. Example: [example]" | -| **Missing required** | "Please enter [what's missing]" | -| **Permission denied** | "You don't have access to [thing]. [What to do instead]" | -| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." | -| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" | - -### Don't Blame the User - -Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date". - -## Empty States Are Opportunities - -Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items". - -## Voice vs Tone - -**Voice** is your brand's personality, consistent everywhere. -**Tone** adapts to the moment. - -| Moment | Tone Shift | -|--------|------------| -| Success | Celebratory, brief: "Done! Your changes are live." | -| Error | Empathetic, helpful: "That didn't work. Here's what to try..." | -| Loading | Reassuring: "Saving your work..." | -| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | - -**Never use humor for errors.** Users are already frustrated. Be helpful, not cute. - -## Writing for Accessibility - -**Link text** must have standalone meaning: "View pricing plans" not "Click here". **Alt text** describes information, not the image: "Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context. - -## Writing for Translation - -### Plan for Expansion - -German text is ~30% longer than English. Allocate space: - -| Language | Expansion | -|----------|-----------| -| German | +30% | -| French | +20% | -| Finnish | +30-40% | -| Chinese | -30% (fewer chars, but same width) | - -### Translation-Friendly Patterns - -Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear. - -## Consistency: The Terminology Problem - -Pick one term and stick with it: - -| Inconsistent | Consistent | -|--------------|------------| -| Delete / Remove / Trash | Delete | -| Settings / Preferences / Options | Settings | -| Sign in / Log in / Enter | Sign in | -| Create / Add / New | Create | - -Build a terminology glossary and enforce it. Variety creates confusion. - -## Avoid Redundant Copy - -If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well. - -## Loading States - -Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress. - -## Confirmation Dialogs: Use Sparingly - -Most confirmation dialogs are design failures; consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No"). - -## Form Instructions - -Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking. - ---- - -**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors. diff --git a/.github/skills/impeccable/scripts/cleanup-deprecated.mjs b/.github/skills/impeccable/scripts/cleanup-deprecated.mjs index 6aee4710a..bc6400e90 100644 --- a/.github/skills/impeccable/scripts/cleanup-deprecated.mjs +++ b/.github/skills/impeccable/scripts/cleanup-deprecated.mjs @@ -25,7 +25,7 @@ import { join, resolve } from 'node:path'; const DEPRECATED_NAMES = [ // v2.0 renames 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable teach + 'teach-impeccable', // folded into /impeccable init // v2.1 merges 'arrange', // renamed to layout 'normalize', // merged into polish diff --git a/.github/skills/impeccable/scripts/command-metadata.json b/.github/skills/impeccable/scripts/command-metadata.json index 1488bd5f1..83796d54b 100644 --- a/.github/skills/impeccable/scripts/command-metadata.json +++ b/.github/skills/impeccable/scripts/command-metadata.json @@ -3,8 +3,8 @@ "description": "Full confirmed-brief-then-build flow. Runs multi-round shape discovery first, resolves visual probe and north-star mock gates when available, then builds and visually iterates. Use when building a new feature end-to-end.", "argumentHint": "[feature description]" }, - "teach": { - "description": "Gathers design context for a project. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.", + "init": { + "description": "Sets up a project for impeccable. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles); offers DESIGN.md (visual: colors, typography, components) when code exists; pre-configures live mode; then recommends the best commands to run next. Every other command reads these files before doing work. Use once per project.", "argumentHint": "" }, "document": { diff --git a/.github/skills/impeccable/scripts/context.mjs b/.github/skills/impeccable/scripts/context.mjs new file mode 100644 index 000000000..52263c883 --- /dev/null +++ b/.github/skills/impeccable/scripts/context.mjs @@ -0,0 +1,253 @@ +/** + * Context loader: prints PRODUCT.md (and DESIGN.md if present) as one + * markdown block on stdout, or exits with empty stdout when no PRODUCT.md + * is found anywhere. The skill keys off "empty stdout" to branch into the + * init flow. + * + * Path resolution (first match wins): + * 1. cwd, if PRODUCT.md or DESIGN.md is there + * 2. .agents/context/ then docs/ + * 3. $IMPECCABLE_CONTEXT_DIR (absolute or cwd-relative) — power-user + * escape hatch, only consulted when defaults are empty + * 4. cwd as a "nothing found" default + * + * `resolveContextDir()` and `loadContext()` are also exported for the + * server-side scripts (live.mjs, live-server.mjs) that need the structured + * shape rather than the markdown block. + */ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; +const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; +const FALLBACK_DIRS = ['.agents/context', 'docs']; + +// ─── Update check ────────────────────────────────────────────────────────── +// Piggyback a lightweight skill-version check on the once-per-session boot. +// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent +// can offer `npx impeccable skills update`. Everything here is best-effort and +// silent on failure: a network problem, sandbox, or missing cache must never +// block context output or print an error. + +const UPDATE_HOST = (process.env.IMPECCABLE_UPDATE_HOST || 'https://impeccable.style').replace(/\/$/, ''); +const UPDATE_CACHE_PATH = + process.env.IMPECCABLE_UPDATE_CACHE || path.join(os.homedir(), '.impeccable', 'update-check.json'); +const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // throttle the network poll to once a day +const RENOTIFY_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000; // don't re-surface the same version for a week +const FETCH_TIMEOUT_MS = 1200; + +export function resolveContextDir(cwd = process.cwd()) { + if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { + return cwd; + } + for (const rel of FALLBACK_DIRS) { + const candidate = path.resolve(cwd, rel); + if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { + return candidate; + } + } + const envDir = process.env.IMPECCABLE_CONTEXT_DIR; + if (envDir && envDir.trim()) { + const trimmed = envDir.trim(); + return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); + } + return cwd; +} + +export function loadContext(cwd = process.cwd()) { + const contextDir = resolveContextDir(cwd); + const productPath = firstExisting(contextDir, PRODUCT_NAMES); + const designPath = firstExisting(contextDir, DESIGN_NAMES); + const product = productPath ? safeRead(productPath) : null; + const design = designPath ? safeRead(designPath) : null; + return { + hasProduct: !!product, + product, + productPath: productPath ? path.relative(cwd, productPath) : null, + hasDesign: !!design, + design, + designPath: designPath ? path.relative(cwd, designPath) : null, + contextDir, + }; +} + +function firstExisting(dir, names) { + for (const name of names) { + const abs = path.join(dir, name); + if (fs.existsSync(abs)) return abs; + } + return null; +} + +function safeRead(p) { + try { + return fs.readFileSync(p, 'utf-8'); + } catch { + return null; + } +} + +/** + * Pull the register (`brand` or `product`) out of PRODUCT.md by looking + * for a `## Register` section and reading the first non-empty line that + * follows it. Returns null when the file is legacy / register-less. + */ +function extractRegister(product) { + if (!product) return null; + const lines = product.split('\n'); + for (let i = 0; i < lines.length; i++) { + if (/^##\s+Register\b/i.test(lines[i].trim())) { + for (let j = i + 1; j < lines.length; j++) { + const next = lines[j].trim(); + if (!next) continue; + const word = next.toLowerCase(); + if (word === 'brand' || word === 'product') return word; + return null; + } + } + } + return null; +} + +/** + * Read the installed skill's own version from the sibling SKILL.md frontmatter + * (this file lives at `/scripts/context.mjs`). Returns null when the + * frontmatter is missing or unreadable. + */ +function readLocalSkillVersion() { + try { + const here = path.dirname(fileURLToPath(import.meta.url)); + const skillMd = path.join(here, '..', 'SKILL.md'); + const content = fs.readFileSync(skillMd, 'utf-8'); + const match = content.match(/^version:\s*(.+)$/m); + return match ? match[1].trim().replace(/^["']|["']$/g, '') : null; + } catch { + return null; + } +} + +function readUpdateCache() { + try { + return JSON.parse(fs.readFileSync(UPDATE_CACHE_PATH, 'utf-8')); + } catch { + return {}; + } +} + +function writeUpdateCache(cache) { + try { + fs.mkdirSync(path.dirname(UPDATE_CACHE_PATH), { recursive: true }); + fs.writeFileSync(UPDATE_CACHE_PATH, JSON.stringify(cache)); + } catch { + // Best-effort: a read-only home dir just means we re-poll next session. + } +} + +/** Compare dotted numeric versions. Returns >0 when a is newer than b. */ +function compareSemver(a, b) { + const pa = String(a).split('.').map(n => parseInt(n, 10) || 0); + const pb = String(b).split('.').map(n => parseInt(n, 10) || 0); + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + const diff = (pa[i] || 0) - (pb[i] || 0); + if (diff !== 0) return diff; + } + return 0; +} + +async function fetchLatestSkillVersion() { + try { + const res = await fetch(`${UPDATE_HOST}/api/version`, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }); + if (!res.ok) return null; + const data = await res.json(); + return typeof data?.skills === 'string' ? data.skills : null; + } catch { + return null; // offline, sandboxed, timed out, or bad JSON: all non-fatal + } +} + +function buildUpdateDirective(localVersion, latestVersion) { + return ( + `UPDATE_AVAILABLE: A newer Impeccable skill is available ` + + `(installed v${localVersion}, latest v${latestVersion}). ` + + `Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` + + `Update now? It runs \`npx impeccable skills update\`." ` + + `If they agree, run \`npx impeccable skills update\` (the update applies to the next session, not this one). ` + + `Either way, continue the current task without waiting, and do not raise this again.` + ); +} + +/** + * Best-effort update directive for the boot output. Returns a string to append + * or null. Polls the version endpoint at most once per day (cached globally in + * the user's home dir) and re-surfaces a given version at most once per week so + * the agent never nags. Opt out entirely with IMPECCABLE_NO_UPDATE_CHECK=1. + */ +async function computeUpdateDirective(now = Date.now()) { + try { + if (process.env.IMPECCABLE_NO_UPDATE_CHECK) return null; + const localVersion = readLocalSkillVersion(); + if (!localVersion) return null; + + const cache = readUpdateCache(); + + // Poll the network only when the throttle window has elapsed. Stamp + // lastCheck even on failure so an offline machine doesn't poll every boot. + if (!cache.lastCheck || now - cache.lastCheck > CHECK_INTERVAL_MS) { + const latest = await fetchLatestSkillVersion(); + cache.lastCheck = now; + if (latest) cache.latestVersion = latest; + writeUpdateCache(cache); + } + + const latest = cache.latestVersion; + if (!latest || compareSemver(latest, localVersion) <= 0) return null; + + // Anti-nag: surface a given version at most once per RENOTIFY window. + if (cache.notifiedVersion === latest && cache.notifiedAt && now - cache.notifiedAt < RENOTIFY_INTERVAL_MS) { + return null; + } + cache.notifiedVersion = latest; + cache.notifiedAt = now; + writeUpdateCache(cache); + + return buildUpdateDirective(localVersion, latest); + } catch { + return null; + } +} + +async function cli() { + const ctx = loadContext(process.cwd()); + const updateDirective = await computeUpdateDirective(); + + if (!ctx.hasProduct) { + // Direct stdout message instead of relying on empty output as a signal + // — cheap models miss the empty case more often than the explicit one. + const parts = [ + 'NO_PRODUCT_MD: This project has no PRODUCT.md yet. ' + + 'Stop the current task, load reference/init.md, and follow its ' + + 'instructions to write PRODUCT.md before resuming.', + ]; + if (updateDirective) parts.push(updateDirective); + process.stdout.write(parts.join('\n\n---\n\n') + '\n'); + process.exit(0); + } + const parts = [`# PRODUCT.md\n\n${ctx.product.trim()}`]; + if (ctx.hasDesign) { + parts.push(`# DESIGN.md\n\n${ctx.design.trim()}`); + } + const register = extractRegister(ctx.product); + const next = register + ? `NEXT STEP: This project's register is \`${register}\`. You MUST now read \`reference/${register}.md\` before producing any design output.` + : `NEXT STEP: You MUST now read the matching register reference (\`reference/brand.md\` or \`reference/product.md\`) before producing any design output. Pick based on PRODUCT.md above.`; + parts.push(next); + if (updateDirective) parts.push(updateDirective); + process.stdout.write(parts.join('\n\n---\n\n') + '\n'); +} + +const _running = process.argv[1]; +if (_running?.endsWith('context.mjs') || _running?.endsWith('context.mjs/')) { + cli(); +} diff --git a/.github/skills/impeccable/scripts/design-parser.mjs b/.github/skills/impeccable/scripts/design-parser.mjs index b68756640..82a5cd7dc 100644 --- a/.github/skills/impeccable/scripts/design-parser.mjs +++ b/.github/skills/impeccable/scripts/design-parser.mjs @@ -63,7 +63,7 @@ function parseYamlSubset(yaml) { } const key = content.slice(0, colonIdx).trim(); - const rest = content.slice(colonIdx + 1).trim(); + const rest = stripInlineYamlComment(content.slice(colonIdx + 1).trim()); const parent = stack[stack.length - 1].obj; if (rest === '') { @@ -93,6 +93,21 @@ function findTopLevelColon(s) { return -1; } +function stripInlineYamlComment(s) { + let inQuote = null; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (inQuote) { + if (ch === inQuote && s[i - 1] !== '\\') inQuote = null; + } else if (ch === '"' || ch === "'") { + inQuote = ch; + } else if (ch === '#' && i > 0 && /\s/.test(s[i - 1])) { + return s.slice(0, i).trimEnd(); + } + } + return s; +} + function parseScalar(raw) { const s = raw.trim(); if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { diff --git a/.github/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.github/skills/impeccable/scripts/detector/browser/injected/index.mjs index c03a69caf..ceff16737 100644 --- a/.github/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/.github/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -9,8 +9,21 @@ if (IS_BROWSER) { const EXTENSION_MODE = (_myScript && _myScript.dataset.impeccableExtension === 'true') || document.documentElement.dataset.impeccableExtension === 'true'; - const BRAND_COLOR = 'oklch(55% 0.25 350)'; - const BRAND_COLOR_HOVER = 'oklch(45% 0.25 350)'; + // Kinpaku gold — pinned to the site's brand token (see + // site/styles/kinpaku-tokens.css --ks-kinpaku). Keep this in sync with + // the picker's C.brand in skill/scripts/live-browser.js and the kit's + // picker section in site/styles/kinpaku-kit.css. + // + // One color across both light and dark host pages. The outline is a + // 2px gesture pointing at an element + a labeled tag — it's a marker, + // not body text, so it doesn't need WCAG AA against the page. The + // label text inside the gold tag is dark (LABEL_INK) which has ~16:1 + // against the leaf gold, so reading the rule name is solid in both + // modes. Hover deepens the gold (preserves chroma — never drops it, + // dropping chroma washes the gold into a sand/olive tone). + const BRAND_COLOR = 'oklch(84% 0.19 80.46)'; + const BRAND_COLOR_HOVER = 'oklch(74% 0.18 80)'; + const LABEL_INK = 'oklch(4% 0.004 95)'; const LABEL_BG = BRAND_COLOR; const OUTLINE_COLOR = BRAND_COLOR; @@ -278,7 +291,7 @@ if (IS_BROWSER) { display: 'flex', alignItems: 'center', whiteSpace: 'nowrap', fontSize: '11px', fontWeight: '600', letterSpacing: '0.02em', - color: 'white', lineHeight: '14px', + color: LABEL_INK, lineHeight: '14px', background: LABEL_BG, fontFamily: 'system-ui, sans-serif', borderRadius: '4px 4px 0 0', @@ -398,7 +411,7 @@ if (IS_BROWSER) { banner.className = 'impeccable-overlay impeccable-banner'; Object.assign(banner.style, { position: 'fixed', top: '0', left: '0', right: '0', zIndex: '100000', - background: LABEL_BG, color: 'white', + background: LABEL_BG, color: LABEL_INK, fontFamily: 'system-ui, sans-serif', fontSize: '13px', display: 'flex', alignItems: 'center', pointerEvents: 'auto', height: '36px', overflow: 'hidden', maxWidth: '100vw', @@ -1223,12 +1236,12 @@ if (IS_BROWSER) { } console.group( `%c[impeccable] ${allFindings.length} anti-pattern${allFindings.length === 1 ? '' : 's'} found`, - 'color: oklch(60% 0.25 350); font-weight: bold' + 'color: oklch(84% 0.19 80.46); font-weight: bold' ); for (const { el, findings } of allFindings) { for (const f of findings) { console.log(`%c${f.type || f.id}%c ${f.detail || f.snippet}`, - 'color: oklch(55% 0.25 350); font-weight: bold', 'color: inherit', el); + 'color: oklch(84% 0.19 80.46); font-weight: bold', 'color: inherit', el); } } console.groupEnd(); @@ -1249,6 +1262,10 @@ if (IS_BROWSER) { const groupMap = new Map(); const _disabled = EXTENSION_MODE ? (window.__IMPECCABLE_CONFIG__?.disabledRules || []) : []; const _ruleOk = (id) => !_disabled.length || !_disabled.includes(id); + // Note: provider-gated rules (--gpt / --gemini) are NOT filtered here. In a + // real browser env (detector page, live overlay, extension) running every + // check is free, so we always surface them; the gating is purely a CLI + // output concern, applied in the Node engines' detect* return paths. for (const el of document.querySelectorAll('*')) { // Skip impeccable's own elements and any descendants (overlays, labels, banner, nav buttons) @@ -1270,11 +1287,23 @@ if (IS_BROWSER) { ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), - ...checkElementHeroEyebrowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementOversizedH1DOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementClippedOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementGptBorderShadowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementTextOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ].filter(f => _ruleOk(f.type)); addBrowserFindings(groupMap, el, findings); + + // Hero eyebrow: the offending element is the eyebrow above the heading, + // not the heading itself — highlight the previous sibling instead. + const eyebrowFindings = checkElementHeroEyebrowDOM(el) + .map(f => ({ type: f.id, detail: f.snippet })) + .filter(f => _ruleOk(f.type)); + if (eyebrowFindings.length > 0 && el.previousElementSibling) { + addBrowserFindings(groupMap, el.previousElementSibling, eyebrowFindings); + } } const pageLevelFindings = []; @@ -1306,6 +1335,14 @@ if (IS_BROWSER) { addBrowserFindings(groupMap, document.body, qualityFindings); } + const creamFindings = checkCreamPalette(document) + .map(f => ({ type: f.id, detail: f.snippet })) + .filter(f => _ruleOk(f.type)); + if (creamFindings.length > 0) { + pageLevelFindings.push(...creamFindings); + addBrowserFindings(groupMap, document.body, creamFindings); + } + // Regex-on-HTML checks (shared with Node) // Clone the document and strip impeccable-live overlay nodes before the // regex scan, so the inspector's own inline styles (transitions on top/ diff --git a/.github/skills/impeccable/scripts/detector/cli/main.mjs b/.github/skills/impeccable/scripts/detector/cli/main.mjs index 76da09f0c..94283e01b 100644 --- a/.github/skills/impeccable/scripts/detector/cli/main.mjs +++ b/.github/skills/impeccable/scripts/detector/cli/main.mjs @@ -41,7 +41,7 @@ function formatFindings(findings, jsonMode) { // Stdin handling // --------------------------------------------------------------------------- -async function handleStdin() { +async function handleStdin(options = {}) { const chunks = []; for await (const chunk of process.stdin) chunks.push(chunk); const input = Buffer.concat(chunks).toString('utf-8'); @@ -50,10 +50,10 @@ async function handleStdin() { const fp = parsed?.tool_input?.file_path; if (fp && fs.existsSync(fp)) { return HTML_EXTENSIONS.has(path.extname(fp).toLowerCase()) - ? detectHtml(fp) : detectText(fs.readFileSync(fp, 'utf-8'), fp); + ? detectHtml(fp, options) : detectText(fs.readFileSync(fp, 'utf-8'), fp, options); } } catch { /* not JSON */ } - return detectText(input, ''); + return detectText(input, '', options); } @@ -81,6 +81,8 @@ Scan files or URLs for UI anti-patterns and design quality issues. Options: --fast Regex-only mode (skip static HTML/CSS analysis, faster but misses linked stylesheets) --json Output results as JSON + --gpt Also report GPT-specific provider tells (off by default) + --gemini Also report Gemini-specific provider tells (off by default) --help Show this help message Detection modes: @@ -106,6 +108,10 @@ async function detectCli() { const jsonMode = args.includes('--json'); const helpMode = args.includes('--help'); const fastMode = args.includes('--fast'); + const providers = []; + if (args.includes('--gpt')) providers.push('gpt'); + if (args.includes('--gemini')) providers.push('gemini'); + const scanOptions = { providers }; const targets = args.filter(a => !a.startsWith('--')); if (helpMode) { printUsage(); process.exit(0); } @@ -113,7 +119,7 @@ async function detectCli() { let allFindings = []; if (!process.stdin.isTTY && targets.length === 0) { - allFindings = await handleStdin(); + allFindings = await handleStdin(scanOptions); } else { const paths = targets.length > 0 ? targets : [process.cwd()]; const urlTargetCount = paths.filter(target => /^https?:\/\//i.test(target)).length; @@ -124,8 +130,8 @@ async function detectCli() { if (/^https?:\/\//i.test(target)) { try { const scanner = browserDetector - ? (url) => browserDetector.detectUrl(url) - : (url) => detectUrl(url); + ? (url) => browserDetector.detectUrl(url, scanOptions) + : (url) => detectUrl(url, scanOptions); allFindings.push(...await scanner(target)); } catch (e) { process.stderr.write(`Error: ${e.message}\n`); } continue; @@ -192,9 +198,9 @@ async function detectCli() { const ext = path.extname(file).toLowerCase(); let fileFindings; if (!fastMode && HTML_EXTENSIONS.has(ext)) { - fileFindings = await detectHtml(file); + fileFindings = await detectHtml(file, scanOptions); } else { - fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file); + fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file, scanOptions); } // Annotate findings with import context const importers = importedByMap.get(file); @@ -209,9 +215,9 @@ async function detectCli() { } else if (stat.isFile()) { const ext = path.extname(resolved).toLowerCase(); if (!fastMode && HTML_EXTENSIONS.has(ext)) { - allFindings.push(...await detectHtml(resolved)); + allFindings.push(...await detectHtml(resolved, scanOptions)); } else { - allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved)); + allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved, scanOptions)); } } } diff --git a/.github/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.github/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 1afbe3f12..88439a5c3 100644 --- a/.github/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.github/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -38,7 +38,7 @@ const OVERUSED_FONTS = new Set([ // Older monoculture (still ubiquitous): 'inter', 'roboto', 'open sans', 'lato', 'montserrat', 'arial', 'helvetica', // Newer monoculture (the Anthropic-skill / Vercel / GitHub default wave): - 'fraunces', 'instrument sans', + 'fraunces', 'instrument sans', 'instrument serif', 'geist', 'geist sans', 'geist mono', 'mona sans', 'plus jakarta sans', 'space grotesk', 'recoleta', @@ -165,6 +165,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'AI color palette', }, + { + id: 'cream-palette', + category: 'slop', + name: 'Cream / beige palette', + description: + 'A warm cream or beige page background has become the default "tasteful" AI surface, reached for by reflex. Choose a background that comes from a deliberate palette, not the safe warm off-white.', + skillSection: 'Color & Contrast', + skillGuideline: 'cream and beige as the default surface', + }, { id: 'nested-cards', category: 'slop', @@ -183,15 +192,6 @@ const ANTIPATTERNS = [ skillSection: 'Layout & Space', skillGuideline: 'same spacing everywhere', }, - { - id: 'everything-centered', - category: 'slop', - name: 'Everything centered', - description: - 'Every text element is center-aligned. Left-aligned text with asymmetric layouts feels more designed. Center only hero sections and CTAs.', - skillSection: 'Layout & Space', - skillGuideline: 'Center everything', - }, { id: 'bounce-easing', category: 'slop', @@ -247,17 +247,72 @@ const ANTIPATTERNS = [ skillSection: 'Typography', skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding', }, + { + id: 'numbered-section-markers', + category: 'slop', + severity: 'advisory', + name: 'Numbered section markers (01 / 02 / 03)', + description: + 'Numbered display markers as section labels (01, 02, 03) are the AI editorial scaffold one tier deeper than tracked eyebrow chips. If you find yourself reaching for them, choose a different section cadence.', + skillSection: 'Layout & Space', + skillGuideline: 'numbered section markers', + }, + { + id: 'em-dash-overuse', + category: 'slop', + name: 'Em-dash overuse', + description: + 'More than two em-dashes (— or --) in body copy is an AI cadence tell. Use commas, colons, periods, or parentheses instead.', + skillSection: 'Copy', + skillGuideline: 'no em dashes', + }, + { + id: 'marketing-buzzword', + category: 'slop', + name: 'Marketing buzzword', + description: + 'Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does.', + skillSection: 'Copy', + skillGuideline: 'marketing buzzwords', + }, + { + id: 'aphoristic-cadence', + category: 'slop', + name: 'Aphoristic-cadence copy', + description: + 'Three or more sections landing on a short rebuttal sentence ("X. No Y." / "X. Just Y.") or a manufactured-contrast aphorism ("Not a feature. A platform.") reads as AI cadence, not voice. Once is fine; the pattern is the tell.', + skillSection: 'Copy', + skillGuideline: 'aphoristic cadence', + }, + { + id: 'oversized-h1', + category: 'slop', + name: 'Oversized hero headline', + description: + 'A full-sentence headline set at display size ends up dominating the viewport, leaving no room for anything else above the fold. A punchy one- or two-word headline at that size is fine — the problem is a long headline blown up too large. Set long headlines smaller, or tighten the copy.', + skillSection: 'Typography', + skillGuideline: 'long headline set at display size', + }, + { + id: 'extreme-negative-tracking', + category: 'slop', + name: 'Crushed letter spacing', + description: + 'Letter-spacing pulled tighter than the point where characters keep their own shapes costs legibility. Tighten display type optically, not destructively.', + skillSection: 'Typography', + skillGuideline: 'letter spacing crushed past legibility', + }, + { + id: 'broken-image', + category: 'quality', + name: 'Broken or placeholder image', + description: + ' tags with empty src, missing src, or placeholder values ship as broken-image boxes. Use real images, generated assets, or remove the tag.', + skillSection: 'Imagery', + skillGuideline: 'broken image references', + }, // ── Quality: general design and accessibility issues ── - { - id: 'pure-black-white', - category: 'quality', - name: 'Pure black background', - description: - 'Pure #000000 as a background color looks harsh and unnatural. Tint it slightly toward your brand hue (e.g., oklch(12% 0.01 250)) for a more refined feel.', - skillSection: 'Color & Contrast', - skillGuideline: 'pure black (#000)', - }, { id: 'gray-on-color', category: 'quality', @@ -297,7 +352,9 @@ const ANTIPATTERNS = [ category: 'quality', name: 'Cramped padding', description: - 'Text is too close to the edge of its container. Add at least 8px (ideally 12-16px) of padding inside bordered or colored containers.', + 'Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.', + skillSection: 'Layout & Space', + skillGuideline: 'inside bordered or colored containers', }, { id: 'body-text-viewport-edge', @@ -350,6 +407,70 @@ const ANTIPATTERNS = [ description: 'Letter spacing above 0.05em on body text disrupts natural character groupings and slows reading. Reserve wide tracking for short uppercase labels only.', }, + { + id: 'text-overflow', + category: 'quality', + name: 'Content overflowing its container', + description: + 'Content renders wider than its container, spilling out or forcing a horizontal scrollbar. Let text wrap, constrain widths, or give the region a deliberate scroll affordance.', + skillSection: 'Layout & Space', + skillGuideline: 'content wider than its container', + }, + { + id: 'clipped-overflow-container', + category: 'quality', + name: 'Positioned child clipped by overflow container', + description: + 'A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.', + skillSection: 'Layout & Space', + skillGuideline: 'overflow container clipping positioned children', + }, + + // ── Provider tells: opt-in via --gpt / --gemini (gated off by default) ── + { + id: 'gpt-thin-border-wide-shadow', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Hairline border with wide shadow', + description: + 'A hairline border paired with a wide, diffuse shadow is a recurring generated-UI signature. Commit to one — a defined edge or a soft elevation — rather than both at once.', + skillSection: 'Visual Details', + skillGuideline: 'hairline border plus wide diffuse shadow', + }, + { + id: 'repeating-stripes-gradient', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Repeating-gradient stripes', + description: + 'Repeating-gradient stripes used as surface decoration are a recurring generated-UI signature. Reach for a deliberate texture or leave the surface plain.', + skillSection: 'Visual Details', + skillGuideline: 'repeating-gradient decorative stripes', + }, + { + id: 'theater-slop-phrase', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Theater framing copy', + description: + 'Dismissing something as "theater" is a recurring generated-copy tic. Say plainly what the thing does or does not do.', + skillSection: 'Copy', + skillGuideline: 'theater framing copy', + }, + { + id: 'image-hover-transform', + category: 'slop', + severity: 'advisory', + gated: 'gemini', + name: 'Image hover transform', + description: + 'Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction.', + skillSection: 'Motion', + skillGuideline: 'image scale or rotate on hover', + }, ]; // --- cli/engine/shared/color.mjs --- @@ -527,11 +648,6 @@ function checkColors(opts) { } const findings = []; - // Pure black background (only solid or near-solid, not semi-transparent overlays) - if (bgColor && bgColor.a >= 0.9 && bgColor.r === 0 && bgColor.g === 0 && bgColor.b === 0) { - findings.push({ id: 'pure-black-white', snippet: '#000000 background' }); - } - if (hasDirectText && textColor && !isEmojiOnly) { // Run background-dependent checks against either a solid bg or, if the // ancestor is a gradient, against every gradient stop (use the worst case). @@ -587,9 +703,6 @@ function checkColors(opts) { // Tailwind class checks if (classList) { const classStr = typeof classList === 'string' ? classList : Array.from(classList).join(' '); - if (/\bbg-black\b(?!\/)/.test(classStr)) { - findings.push({ id: 'pure-black-white', snippet: 'bg-black' }); - } const grayMatch = classStr.match(/\btext-(?:gray|slate|zinc|neutral|stone)-\d+\b/); const colorBgMatch = classStr.match(/\bbg-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+\b/); @@ -905,12 +1018,6 @@ function checkHtmlPatterns(html) { // --- Color --- - // Pure black background - const pureBlackBgRe = /background(?:-color)?\s*:\s*(?:#000000|#000|rgb\(\s*0,\s*0,\s*0\s*\))\b/gi; - if (pureBlackBgRe.test(html)) { - findings.push({ id: 'pure-black-white', snippet: 'Pure #000 background' }); - } - // AI color palette: purple/violet const purpleHexRe = /#(?:7c3aed|8b5cf6|a855f7|9333ea|7e22ce|6d28d9|6366f1|764ba2|667eea)\b/gi; if (purpleHexRe.test(html)) { @@ -1027,6 +1134,39 @@ function checkHtmlPatterns(html) { } } + // --- Provider tells (gated): repeating-gradient stripes (GPT) --- + if (/repeating-(?:linear|radial|conic)-gradient\s*\(/i.test(html)) { + findings.push({ id: 'repeating-stripes-gradient', snippet: 'repeating-gradient decorative stripes' }); + } + + // --- Provider tells (gated): "X theater" framing copy (GPT) --- + // Lives here (regex-on-HTML) rather than in the text-content analyzers so it + // runs in the bundled browser path too, not just the CLI/static path. + { + const bodyText = html + .replace(/]*>[\s\S]*?<\/script>/gi, ' ') + .replace(/]*>[\s\S]*?<\/style>/gi, ' ') + .replace(/<[^>]+>/g, ' '); + const tm = /\b(\w+)\s+theater\b/i.exec(bodyText); + if (tm) findings.push({ id: 'theater-slop-phrase', snippet: `"${tm[0].trim()}"` }); + } + + // --- Provider tells (gated): image hover transform (Gemini) --- + // A CSS `img...:hover { transform: ... }` rule, or a Tailwind hover:scale / + // hover:rotate / hover:translate utility on an . Each distinct + // mechanism is its own finding. + const imgHoverCss = /\bimg\b[^,{}]*:hover\b[^{}]*\{[^}]*\btransform\s*:\s*(?:scale|rotate|translate|matrix|skew)/i; + if (imgHoverCss.test(html)) { + findings.push({ id: 'image-hover-transform', snippet: 'img:hover { transform } rule' }); + } + const imgTagRe = /]*\bclass\s*=\s*"([^"]*)"/gi; + let im; + while ((im = imgTagRe.exec(html)) !== null) { + if (/\bhover:(?:scale|rotate|translate|skew)-/.test(im[1])) { + findings.push({ id: 'image-hover-transform', snippet: 'Tailwind hover transform on ' }); + } + } + return findings; } @@ -1673,7 +1813,7 @@ function resolveLengthPx(value, fontSizePx) { // Both adapters resolve font-size, line-height and letter-spacing to pixels // before calling this so the pure function only deals with numbers. function checkQuality(opts) { - const { el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax = 80, viewportWidth = 0 } = opts; + const { el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax = 80, viewportWidth = 0, win = null } = opts; const findings = []; // Skip browser extension injected elements const elId = el.id || ''; @@ -1724,6 +1864,155 @@ function checkQuality(opts) { } } + // --- Flush against a visible boundary --- + // Fires when a container has a visible boundary (border, outline, OR a + // non-transparent background) AND near-zero padding on the bounded + // side(s) AND text-bearing children land flush against the boundary. + // + // Distinct from cramped-padding: that rule needs the element itself to + // have direct text (hasDirectText). This rule targets the OPPOSITE + // shape — a container with NO direct text, only children — which is + // exactly what cramped-padding misses (a section wrapping a label + + // list lands a free pass). + // + // The classic shape: agent writes `padding: 28px 0 0` shorthand on a + // section that also has a border, zeroing horizontal padding so the + // text-bearing children touch the side borders. Background and + // outline count too: a colored card with zero padding has the same + // visual failure mode. + { + const FLUSH_SKIP_TAGS = new Set(['HTML', 'BODY', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ARTICLE', 'ASIDE', 'BUTTON', 'A', 'LABEL', 'SUMMARY', 'CODE', 'PRE', 'INPUT', 'TEXTAREA', 'SELECT', 'FORM', 'FIGURE', 'TABLE', 'TBODY', 'THEAD', 'TR', 'TD', 'TH']); + const upperTag = tag ? tag.toUpperCase() : ''; + const elPosition = style.position || ''; + if ( + !FLUSH_SKIP_TAGS.has(upperTag) && + !hasDirectText && + !['fixed', 'absolute'].includes(elPosition) && + el.children && el.children.length > 0 + ) { + const isTransparent = (c) => + !c || c === 'transparent' || c === 'rgba(0, 0, 0, 0)' || + /^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*0(?:\.0+)?\s*\)$/.test(c); + + const borderW = { + top: parseFloat(style.borderTopWidth) || 0, + right: parseFloat(style.borderRightWidth) || 0, + bottom: parseFloat(style.borderBottomWidth) || 0, + left: parseFloat(style.borderLeftWidth) || 0, + }; + const borderVisible = { + top: borderW.top > 0 && !isTransparent(style.borderTopColor), + right: borderW.right > 0 && !isTransparent(style.borderRightColor), + bottom: borderW.bottom > 0 && !isTransparent(style.borderBottomColor), + left: borderW.left > 0 && !isTransparent(style.borderLeftColor), + }; + // Outline detection. jsdom decomposes `border` shorthand into + // border{Top,…}Width/Color but does NOT decompose `outline` — + // the longhands come back empty when the value was set via the + // shorthand. Fall back to parsing `style.outline` ourselves. + let outlineW = parseFloat(style.outlineWidth) || 0; + let outlineStyleVal = style.outlineStyle || ''; + let outlineColorVal = style.outlineColor || ''; + if (!outlineW && style.outline) { + const wMatch = style.outline.match(/(\d+(?:\.\d+)?)\s*px/); + if (wMatch) outlineW = parseFloat(wMatch[1]) || 0; + if (!outlineStyleVal) { + outlineStyleVal = /\b(solid|dashed|dotted|double|groove|ridge|inset|outset)\b/.test(style.outline) ? 'solid' : ''; + } + if (!outlineColorVal) { + const cMatch = style.outline.match(/(rgba?\([^)]+\)|#[0-9a-fA-F]{3,8}|[a-zA-Z]+)\s*$/); + if (cMatch) outlineColorVal = cMatch[1]; + } + } + const outlineVisible = outlineW > 0 && !isTransparent(outlineColorVal) && outlineStyleVal && outlineStyleVal !== 'none'; + const bgVisible = !isTransparent(style.backgroundColor); + + const anyVisible = borderVisible.top || borderVisible.right || borderVisible.bottom || borderVisible.left || outlineVisible || bgVisible; + if (anyVisible) { + // Resolve padding to px (jsdom returns raw "1.5rem" etc., not the + // computed px value; parseFloat would strip the unit and treat + // 1.5rem as 1.5px, false-flagging legitimate insets). + const pad = { + top: resolveLengthPx(style.paddingTop, fontSize) ?? 0, + right: resolveLengthPx(style.paddingRight, fontSize) ?? 0, + bottom: resolveLengthPx(style.paddingBottom, fontSize) ?? 0, + left: resolveLengthPx(style.paddingLeft, fontSize) ?? 0, + }; + const PAD_THRESHOLD = 2; + // Children-insulate-this-side: a side is insulated if ANY direct + // child has its own padding ≥ 4px on that side. Rationale: in + // typical flow, only the first/last (or leftmost/rightmost) + // children actually sit at the parent's edges. If even one of + // them has its own padding, the visual flush is broken on that + // side. Classic example: a column-flow card frame where the + // top child (header) has padding-top:12 and the bottom child + // (footer) has padding-bottom:8 — the parent's padding:0 doesn't + // matter; nothing is actually flush. The `any-child-insulates` + // heuristic accepts some false negatives (a card with one heavily + // padded middle child won't flag) for far fewer false positives. + const CHILD_INSULATE_THRESHOLD = 4; + const childrenInsulate = { top: false, right: false, bottom: false, left: false }; + for (const child of el.children) { + let childStyle = null; + if (win && typeof win.getComputedStyle === 'function') { + try { childStyle = win.getComputedStyle(child); } catch {} + } + if (!childStyle && typeof getComputedStyle === 'function') { + try { childStyle = getComputedStyle(child); } catch {} + } + if (!childStyle) continue; + const childPad = { + top: resolveLengthPx(childStyle.paddingTop, fontSize) ?? 0, + right: resolveLengthPx(childStyle.paddingRight, fontSize) ?? 0, + bottom: resolveLengthPx(childStyle.paddingBottom, fontSize) ?? 0, + left: resolveLengthPx(childStyle.paddingLeft, fontSize) ?? 0, + }; + for (const s of ['top', 'right', 'bottom', 'left']) { + if (childPad[s] >= CHILD_INSULATE_THRESHOLD) childrenInsulate[s] = true; + } + } + + const flushSides = []; + for (const side of ['top', 'right', 'bottom', 'left']) { + const sideBounded = borderVisible[side] || outlineVisible || bgVisible; + if (sideBounded && pad[side] <= PAD_THRESHOLD && !childrenInsulate[side]) { + flushSides.push(side); + } + } + + if (flushSides.length > 0) { + // Confirm at least one direct child has substantial text content + // (> 4 chars). Without this, the flush is harmless: e.g. an + // image-only card. + let hasTextChild = false; + for (const child of el.children) { + const childText = (child.textContent || '').trim(); + if (childText.length > 4) { hasTextChild = true; break; } + } + if (hasTextChild) { + const cls = (typeof el.className === 'string' && el.className.trim()) + ? el.className.trim().split(/\s+/)[0] + : ''; + const boundaryParts = []; + const borderSidesVisible = ['top', 'right', 'bottom', 'left'].filter(s => borderVisible[s]); + if (borderSidesVisible.length === 4) boundaryParts.push('border'); + else if (borderSidesVisible.length > 0) boundaryParts.push(`border-${borderSidesVisible.join('/')}`); + if (outlineVisible) boundaryParts.push('outline'); + if (bgVisible) boundaryParts.push('bg'); + const sidesLabel = flushSides.length === 4 ? 'all sides' : flushSides.join('/'); + const ident = cls + ? `<${tag.toLowerCase()}> "${cls}"` + : `<${tag.toLowerCase()}>`; + findings.push({ + id: 'cramped-padding', + snippet: `${ident}: children flush against ${boundaryParts.join('+')} on ${sidesLabel} (no inset)`, + }); + } + } + } + } + } + // --- Body text touching viewport edge --- (browser-only: needs rect) // Catches the failure mode where the agent ships body paragraphs // with NO container providing horizontal padding — text bleeds @@ -1804,6 +2093,20 @@ function checkQuality(opts) { } } + // --- Crushed letter spacing (mirror of wide-tracking) --- + // Tracking pulled tighter than ~-0.05em crushes characters into each other. + // Optical tightening that display type legitimately wants (around -0.02em) + // stays well above this floor. + if (hasDirectText && textLen > 20 && fontSize > 0) { + if (letterSpacingPx != null && letterSpacingPx < 0) { + const trackingEm = letterSpacingPx / fontSize; + if (trackingEm <= -0.05) { + const excerpt = (el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 40); + findings.push({ id: 'extreme-negative-tracking', snippet: `letter-spacing: ${trackingEm.toFixed(2)}em — "${excerpt}"` }); + } + } + } + return findings; } @@ -1820,7 +2123,7 @@ function checkElementQualityDOM(el) { const rect = el.getBoundingClientRect(); const lineMax = (typeof window !== 'undefined' && window.__IMPECCABLE_CONFIG__?.lineLengthMax) || 80; const viewportWidth = (typeof window !== 'undefined' ? window.innerWidth : 0) || 0; - return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax, viewportWidth }); + return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax, viewportWidth, win: typeof window !== 'undefined' ? window : null }); } // Pure page-level skipped-heading walk. Takes a Document so it works in both @@ -1862,7 +2165,7 @@ function checkElementQuality(el, style, tag, window) { const fontSize = resolveFontSizePx(el, window); const lineHeightPx = resolveLengthPx(style.lineHeight, fontSize); const letterSpacingPx = resolveLengthPx(style.letterSpacing, fontSize); - return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect: null }); + return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect: null, win: window }); } function checkElementBorders(tag, style, overrides, resolvedRadius) { @@ -2303,40 +2606,213 @@ function checkPageLayout(doc, win) { } } - // Everything centered - const textEls = doc.querySelectorAll('h1, h2, h3, h4, h5, h6, p, li, div, button'); - let centeredCount = 0; - let totalText = 0; - for (const el of textEls) { - const hasDirectText = [...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim().length >= 3); - if (!hasDirectText) continue; - totalText++; - - let cur = el; - let isCentered = false; - while (cur && cur.nodeType === 1) { - const rawStyle = cur.getAttribute?.('style') || ''; - const cls = cur.getAttribute?.('class') || ''; - if (/text-align\s*:\s*center/i.test(rawStyle) || /\btext-center\b/.test(cls)) { - isCentered = true; - break; - } - if (cur.tagName === 'BODY') break; - cur = cur.parentElement; - } - if (isCentered) centeredCount++; - } - - if (totalText >= 5 && centeredCount / totalText > 0.7) { - findings.push({ - id: 'everything-centered', - snippet: `${centeredCount}/${totalText} text elements centered (${Math.round(centeredCount / totalText * 100)}%)`, - }); - } - return findings; } +// ─── Cream / beige palette (the default "tasteful" AI surface) ──────────────── +// A warm, lightly-tinted off-white page background — light, with R≥G≥B and a +// small warm tint (not white, not a strong color). The current reflex surface. +function isCreamColor(rgb) { + if (!rgb) return false; + const { r, g, b } = rgb; + if (Math.min(r, g, b) < 209) return false; // must be light + if (!(r >= g && g >= b)) return false; // warm ordering + const warmth = r - b; + return warmth >= 6 && warmth <= 48; // tinted, not white, not strong +} + +// Tailwind background utilities that render as a warm off-white surface. The +// static engine doesn't fetch Tailwind's CSS, so a `bg-amber-50` on +// resolves to nothing in computed style — catch it from the class list +// instead. Candidate tokens map to their actual Tailwind hex and are still +// filtered through isCreamColor, so neutral grays (stone) and over-saturated +// shades drop out on their own. +const TAILWIND_BG_HEX = { + 'bg-amber-50': '#fffbeb', 'bg-amber-100': '#fef3c7', + 'bg-orange-50': '#fff7ed', 'bg-orange-100': '#ffedd5', + 'bg-yellow-50': '#fefce8', + 'bg-stone-50': '#fafaf9', 'bg-stone-100': '#f5f5f4', 'bg-stone-200': '#e7e5e4', +}; + +function creamFromClassList(cls) { + if (!cls) return null; + // Arbitrary value: bg-[#f5f0e6] / bg-[rgb(245_240_230)] (underscores = spaces). + const arb = cls.match(/\bbg-\[([^\]]+)\]/); + if (arb && isCreamColor(parseAnyColor(arb[1].replace(/_/g, ' ')))) return `bg-[${arb[1]}]`; + // Named warm-light utilities. + for (const [tok, hex] of Object.entries(TAILWIND_BG_HEX)) { + if (new RegExp(`(^|\\s)${tok}($|\\s)`).test(cls) && isCreamColor(parseAnyColor(hex))) return tok; + } + return null; +} + +function checkCreamPalette(doc, win) { + const findings = []; + const body = doc.body || (doc.querySelector ? doc.querySelector('body') : null); + if (!body) return findings; + const html = doc.documentElement; + const getCS = (el) => (win ? win.getComputedStyle(el) : getComputedStyle(el)); + + // 1. Computed background — covers inline / close.', 'Prefix every preview selector with the matching [data-impeccable-variant="N"] selector.', 'Keep selectors anchored to the generated variant wrapper; do not rely on component CSS scoping for preview rules.', ], forbidden: [ 'Do not use @scope for this styleMode.', + 'Do not wrap style content in a JSX/TSX template literal ({` ... `}); that syntax is for .tsx/.jsx only.', + 'Do not put { immediately after the style opening tag; Astro parses { as expression syntax.', ], }; } @@ -629,4 +632,15 @@ if (_running?.endsWith('live-wrap.mjs') || _running?.endsWith('live-wrap.mjs/')) } // Test exports (used by tests/live-wrap.test.mjs) -export { buildSearchQueries, findElement, findClosingLine, detectCommentSyntax }; +export { + buildSearchQueries, + findElement, + findClosingLine, + detectCommentSyntax, + findAllElements, + filterByText, + findFileWithQuery, + detectStyleMode, + buildCssAuthoring, + buildCssSelectorPrefixExamples, +}; diff --git a/.github/skills/impeccable/scripts/live.mjs b/.github/skills/impeccable/scripts/live.mjs index cafb0eca9..8acd300ed 100644 --- a/.github/skills/impeccable/scripts/live.mjs +++ b/.github/skills/impeccable/scripts/live.mjs @@ -21,7 +21,7 @@ import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { loadContext } from './load-context.mjs'; +import { loadContext } from './context.mjs'; import { resolveFiles } from './live-inject.mjs'; import { readLiveServerInfo } from './impeccable-paths.mjs'; @@ -80,7 +80,7 @@ The agent should then: process.exit(1); } - // 4. Load PRODUCT.md + DESIGN.md context (auto-migrates legacy .impeccable.md) + // 4. Load PRODUCT.md + DESIGN.md context. const ctx = loadContext(process.cwd()); // 5. Compute drift-heal: compare resolved inject targets against the @@ -102,7 +102,6 @@ The agent should then: hasDesign: ctx.hasDesign, design: ctx.design, designPath: ctx.designPath, - migrated: ctx.migrated, }, null, 2)); } diff --git a/.github/skills/impeccable/scripts/load-context.mjs b/.github/skills/impeccable/scripts/load-context.mjs deleted file mode 100644 index dc340bf16..000000000 --- a/.github/skills/impeccable/scripts/load-context.mjs +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Shared context loader for every impeccable command that needs to know - * "who is this for" and "what does this look like". - * - * Input: project root (process.cwd()). - * - * Output (JSON to stdout): - * { - * hasProduct: boolean, // PRODUCT.md found (or auto-migrated) - * product: string | null, // PRODUCT.md contents - * productPath: string | null, // relative path - * hasDesign: boolean, // DESIGN.md found - * design: string | null, // DESIGN.md contents - * designPath: string | null, - * migrated: boolean, // true if we auto-renamed .impeccable.md -> PRODUCT.md - * contextDir: string, // absolute path of the directory the files were found in - * } - * - * Filename matching is case-insensitive for PRODUCT.md and DESIGN.md. The - * Google DESIGN.md convention is uppercase at repo root; Kiro-style and - * lowercase variants are also matched so users don't get punished for case. - * - * Lookup directory resolution (first match wins): - * 1. process.env.IMPECCABLE_CONTEXT_DIR (absolute or relative to cwd) - * 2. cwd, if PRODUCT.md / DESIGN.md / .impeccable.md is there (back-compat) - * 3. Auto-fallback subdirectories of cwd: .agents/context/, then docs/ - * 4. cwd as a default "no context found" location - * - * Legacy `.impeccable.md` -> PRODUCT.md migration only fires at cwd root; - * fallback directories are read-only as far as auto-rename is concerned. - */ - -import fs from 'node:fs'; -import path from 'node:path'; - -const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; -const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; -const LEGACY_NAMES = ['.impeccable.md']; -const FALLBACK_DIRS = ['.agents/context', 'docs']; - -/** - * Resolve the directory that holds PRODUCT.md / DESIGN.md for - * this project. Exported so other scripts (e.g. live-server.mjs) can read the - * design files from the same location the loader uses. - */ -export function resolveContextDir(cwd = process.cwd()) { - // 1. Explicit override - const envDir = process.env.IMPECCABLE_CONTEXT_DIR; - if (envDir && envDir.trim()) { - const trimmed = envDir.trim(); - return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); - } - - // 2. cwd wins if any canonical or legacy file is there. We check legacy too - // so the auto-migration path in loadContext stays predictable. - if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES, ...LEGACY_NAMES])) { - return cwd; - } - - // 3. Auto-fallback subdirs. Match if PRODUCT.md or DESIGN.md is present; - // legacy `.impeccable.md` does not pull the lookup into a fallback dir. - for (const rel of FALLBACK_DIRS) { - const candidate = path.resolve(cwd, rel); - if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { - return candidate; - } - } - - // 4. Nothing found — keep the historical "default to cwd" behaviour so the - // caller's `hasProduct === false` branch still fires the same way. - return cwd; -} - -export function loadContext(cwd = process.cwd()) { - let migrated = false; - const contextDir = resolveContextDir(cwd); - - // 1. Look for PRODUCT.md (case-insensitive) in the resolved dir - let productPath = firstExisting(contextDir, PRODUCT_NAMES); - - // 2. Legacy: if no PRODUCT.md but .impeccable.md exists at cwd root, rename - // it in place. We only migrate at the root — fallback dirs are read-only - // so we don't surprise users by mutating files under docs/ or .agents/. - if (!productPath && contextDir === cwd) { - const legacyPath = firstExisting(cwd, LEGACY_NAMES); - if (legacyPath) { - const newPath = path.join(cwd, 'PRODUCT.md'); - try { - fs.renameSync(legacyPath, newPath); - productPath = newPath; - migrated = true; - } catch { - // Rename failed (permissions, etc.) — fall back to reading legacy in place - productPath = legacyPath; - } - } - } - - // 3. DESIGN.md (case-insensitive) - const designPath = firstExisting(contextDir, DESIGN_NAMES); - - const product = productPath ? safeRead(productPath) : null; - const design = designPath ? safeRead(designPath) : null; - - return { - hasProduct: !!product, - product, - productPath: productPath ? path.relative(cwd, productPath) : null, - hasDesign: !!design, - design, - designPath: designPath ? path.relative(cwd, designPath) : null, - migrated, - contextDir, - }; -} - -function firstExisting(dir, names) { - for (const name of names) { - const abs = path.join(dir, name); - if (fs.existsSync(abs)) return abs; - } - return null; -} - -function safeRead(p) { - try { return fs.readFileSync(p, 'utf-8'); } catch { return null; } -} - -// --------------------------------------------------------------------------- -// CLI mode — print the context as JSON -// --------------------------------------------------------------------------- - -function cli() { - const result = loadContext(process.cwd()); - console.log(JSON.stringify(result, null, 2)); -} - -const _running = process.argv[1]; -if (_running?.endsWith('load-context.mjs') || _running?.endsWith('load-context.mjs/')) { - cli(); -} diff --git a/.github/skills/impeccable/scripts/palette.mjs b/.github/skills/impeccable/scripts/palette.mjs new file mode 100644 index 000000000..4c3f1751a --- /dev/null +++ b/.github/skills/impeccable/scripts/palette.mjs @@ -0,0 +1,633 @@ +#!/usr/bin/env node +/** + * Brand-seed picker. Returns one OKLCH seed color + the mood it most + * naturally evokes, and teaches the model how to compose a full palette + * around it. + * + * The seed is the brand's anchor color. The 5-role palette (bg, surface, + * ink, accent, muted) is composed by the caller at runtime using their + * judgment + the brief (PRODUCT.md / DESIGN.md / user prompt), NOT picked + * from a frozen 4-color preset. + * + * Why: 4-color frozen palettes drift toward safe defaults (warm-cream bg, + * complementary accent on near-white) regardless of brief. A single seed + + * the model's own composition lets the same seed produce a dark-mode jazz + * club or a light-mode hospitality brand depending on what the brief calls + * for. Tested empirically against curated 4-color palettes; seed approach + * wins on mood-fit in 3 of 5 cases and ties on the rest. + * + * Usage: + * node scripts/palette.mjs # pick at random + * node scripts/palette.mjs --id seed-021 # pick a specific seed + * node scripts/palette.mjs --from # hash to a seed (deterministic) + * + * Env vars: + * IMPECCABLE_PALETTE_SEED — same as --from; useful for the eval harness + * to make runs reproducible. + */ + +import crypto from 'node:crypto'; + +// Seeds are inlined (129 entries, hand-curated via a tinder review of +// ~400 candidates from ColorHunt + synthesis + Radix/brand/Pantone anchors). +// Each carries a mood + strategy the judging model produced — surfaced as +// hints, not commands; the brief still drives composition. +const SEEDS = [ + { id: "seed-200", oklch: [0.360, 0.137, 0.0], + mood: "Aesop apothecary shelf — oxblood bottle glass against linen, considered and unhurried", + strategy: "Seed is a deep desaturated red-brown that reads as brand ink itself; I push primary darker toward bottle-glass oxblood, pair with a pure white surface so the red does the work, and use a clear pale-blush accent that can carry dark text in pills." }, + { id: "seed-000", oklch: [0.400, 0.130, 0.0], + mood: "oxblood leather banquette in a 1940s steakhouse — low lamplight on dark wood and burgundy", + strategy: "Near-black bg with the faintest red undertone lets the oxblood primary glow like lamplit leather; warm cream ink and a brass accent complete the chophouse register." }, + { id: "seed-002", oklch: [0.450, 0.150, 0.0], + mood: "darkroom red light — analog photography, blood-warm safelight glow on chemical trays", + strategy: "Near-black surface with a deep oxblood primary lets the seed function like a safelight in a darkroom — the bg disappears so the red becomes the only emotional signal." }, + { id: "seed-003", oklch: [0.500, 0.194, 0.0], + mood: "darkroom safelight — the deep oxblood glow of analog photography, chemical and contemplative", + strategy: "Anchored the seed as primary against pure near-black so the red reads like a single illuminated bulb in a developing room, with cool desaturated ink to evoke silver gelatin print tones." }, + { id: "seed-004", oklch: [0.546, 0.204, 3.4], + mood: "midnight boudoir — velvet rose under low lamplight, perfumed and intimate", + strategy: "Near-black surface lets the rose seed glow like silk in shadow; a warm champagne accent provides the candle-flame counterpoint without breaking the hush." }, + { id: "seed-005", oklch: [0.550, 0.180, 0.0], + mood: "smoldering vermillion at dusk — the last red ember in a blacksmith's forge, iron-rich and quietly violent", + strategy: "Near-black gallery surround lets the seed read as glowing forged metal; ink stays warm-off-white, accent shifts to a hotter ember orange so the primary feels like cooling steel against a fresh strike." }, + { id: "seed-201", oklch: [0.647, 0.262, 0.3], + mood: "Figma plugin marketplace red — confident product-brand crimson, the kind a modern dev tool uses for a 'live' indicator or a primary CTA on a pristine docs page", + strategy: "Pure white surface lets a high-chroma crimson primary do all the brand work, paired with a hue-shifted warm coral accent for hierarchy without competing saturation" }, + { id: "seed-006", oklch: [0.650, 0.160, 0.0], + mood: "1960s Italian cinema — Technicolor lipstick red against a darkened theater", + strategy: "Pure near-black surface lets a saturated cinematic red and its warm peach accent perform like film light projected in a dark room — the brand colors carry the drama, the bg disappears." }, + { id: "seed-008", oklch: [0.520, 0.200, 10.4], + mood: "Negroni hour at a Milanese bar — bittersweet crimson, vermouth and amaro under low tungsten", + strategy: "Seed is a saturated red-crimson with cinematic weight, so I sit it on near-black to let the primary glow like backlit liquor, with a warmer amber accent acting as the citrus twist against the bitter red." }, + { id: "seed-010", oklch: [0.563, 0.223, 11.0], + mood: "Negroni hour on a Milan rooftop — bittersweet crimson, aperitivo light, polished restraint", + strategy: "Seed is a vivid carmine-red with strong chroma, so the surface gets out of the way (pure white) and lets the primary do the aperitivo work, with a cooled garnet accent for tension." }, + { id: "seed-202", oklch: [0.643, 0.247, 7.0], + mood: "Glossier brand pink — modern beauty editorial, confident and current", + strategy: "Pure white bg lets a saturated rose-red primary do all the brand work, paired with a deeper crimson accent for hierarchy — the Stripe/Glossier move where the color carries the mood." }, + { id: "seed-013", oklch: [0.400, 0.130, 20.0], + mood: "Tuscan cellar at dusk — aged terracotta, oxidized iron, the deep red of decanted Sangiovese", + strategy: "Black surface lets the oxblood seed and copper accent glow like firelight on cellar stone; brand colors carry all the warmth while the room recedes." }, + { id: "seed-014", oklch: [0.450, 0.150, 20.0], + mood: "smoldering tannery — oxblood leather, cured under low workshop light", + strategy: "Anchor the deep oxblood seed as primary against a near-black architectural ground, then lift with a single warm ember accent so the leather reads burnished rather than bloody." }, + { id: "seed-016", oklch: [0.550, 0.180, 20.0], + mood: "Negroni hour on a Roman terrace — bitter campari red, vermouth, late golden light spilling on white linen", + strategy: "Pure white surface lets the campari-red primary do all the emotional work, paired with a deeper oxblood accent for bittersweet depth — Italian aperitivo restraint, not warmth-washed." }, + { id: "seed-205", oklch: [0.634, 0.254, 17.6], + mood: "Aesop apothecary bottle — considered red-coral on a clinical white surface, the kind of brand restraint where one saturated object does all the work", + strategy: "Default A pure white surface lets a single coral-red primary carry the entire brand voice; accent shifts to a deeper oxblood for hierarchy without competing chroma." }, + { id: "seed-011", oklch: [0.639, 0.207, 13.5], + mood: "Aperitivo hour in Milan — Campari glow on a white marble bar, crisp and effervescent", + strategy: "Pure white gallery backdrop lets the Campari-red primary ring like a single bitter note; ink is near-black with a whisper of warmth, accent shifts to a deeper oxblood for hierarchy without competing hues." }, + { id: "seed-015", oklch: [0.527, 0.202, 22.7], + mood: "Negroni hour on a Milanese terrace — bittersweet vermillion, aperitivo glassware catching low sun", + strategy: "Seed becomes a saturated aperitivo-red primary against pure white so the color carries the bittersweet warmth alone, paired with a deep oxblood accent for typographic gravitas." }, + { id: "seed-023", oklch: [0.427, 0.175, 29.2], + mood: "blacksmith's forge at dusk — iron heated to ember red, the deep glow of oxidized metal and quenching oil", + strategy: "Pure black bg lets the seed's ember-red glow radiate like hot iron in a dark forge; accent shifts to a copper-amber to suggest scaling metal and sparks, while ink stays near-white for tool-precise legibility." }, + { id: "seed-206", oklch: [0.614, 0.234, 28.2], + mood: "Aesop apothecary bottle — considered red-orange on lab-white, calm utility with a single confident pigment", + strategy: "Pure white surface lets a saturated vermilion primary do all the brand work, paired with a deep oxblood accent for hierarchy without introducing a second hue family" }, + { id: "seed-029", oklch: [0.665, 0.222, 25.7], + mood: "Negroni hour at a Milanese bar — bittersweet orange-red liqueur catching late afternoon light on polished marble", + strategy: "Pure white surface lets the seed's vermilion read like Campari in a glass; a deeper oxblood accent provides the bitter depth, with neutral graphite ink keeping the editorial restraint of Italian design." }, + { id: "seed-022", oklch: [0.418, 0.155, 27.2], + mood: "Pompeiian red fresco — oxidized cinnabar on a museum wall, archaeological gravity", + strategy: "Pure black gallery surface lets the seed's iron-oxide red read as a lit artifact; accent shifts to an aged terracotta amber, so primary and accent form a fired-clay duet against neutral void." }, + { id: "seed-024", oklch: [0.464, 0.169, 26.9], + mood: "Mid-century darkroom under the safelight — developer trays, oxblood leather, the quiet patience of a print emerging", + strategy: "Seed becomes a deep oxblood primary; surface stays pure black so the red glows like a safelight, with a warmer ember accent for hierarchy" }, + { id: "seed-026", oklch: [0.489, 0.190, 28.3], + mood: "smoldering ember in a blacksmith's forge — iron-hot rust, soot, and controlled fire", + strategy: "Near-black soot background lets the seed's red-orange glow like heated metal; ink is bone-white, accent is a cooler tempered-steel orange that creates internal heat gradient with the primary." }, + { id: "seed-027", oklch: [0.568, 0.208, 27.1], + mood: "Sicilian blood orange at golden hour — citrus rind, terracotta, sun on stucco", + strategy: "Seed reads as vivid blood-orange — picked pure white surface so the citrus-red primary and a deep oxblood accent do all the emotional work, like a Loro Piana editorial spread." }, + { id: "seed-028", oklch: [0.591, 0.172, 24.0], + mood: "Sienna-fired ceramic studio at dusk — terracotta cooling on a wheel, hands still dusted with slip", + strategy: "Pure black stage lets the fired-clay primary glow like a kiln ember, with a deeper oxblood accent providing tonal weight rather than hue contrast — a monochrome warm-axis play." }, + { id: "seed-033", oklch: [0.544, 0.169, 31.3], + mood: "1960s Italian terracotta workshop — fired clay, espresso, late-afternoon Mediterranean dust", + strategy: "Pure black ground lets the seed's burnt-sienna primary glow like a lit kiln, with a deeper oxblood accent for restrained warmth tension — the brand carries the heat, the surface stays out." }, + { id: "seed-207", oklch: [0.564, 0.231, 29.1], + mood: "Aesop apothecary bottle — considered red oxide, the calm authority of a well-made object on a white shelf", + strategy: "Seed becomes the singular brand voice against pure white, with a deeper oxblood accent for hierarchy — the surface disappears so the red does all the speaking." }, + { id: "seed-035", oklch: [0.663, 0.153, 32.1], + mood: "Aesop apothecary bottle — clay-fired warmth, considered retail", + strategy: "Pure white surface lets the terracotta primary do the brand work, paired with a deep umber ink and a cooler clay accent for editorial tension." }, + { id: "seed-037", oklch: [0.590, 0.188, 35.8], + mood: "Aesop apothecary bottle — considered terracotta, herbalist restraint, the warmth comes from the glass not the room", + strategy: "Seed becomes a muted terracotta primary against pure white so the brand's warmth carries entirely through the color itself; accent shifts to a deeper umber for quiet hierarchy." }, + { id: "seed-038", oklch: [0.652, 0.229, 34.8], + mood: "blown-glass furnace at dusk — molten orange iron pulled from the kiln, a craftsman's signature heat", + strategy: "Pure black stage so the seed reads as live ember; primary holds the seed's heat, accent shifts to a brass-amber a hue-step away for a 1.7+ contrast pairing without leaving the fire." }, + { id: "seed-039", oklch: [0.653, 0.185, 33.5], + mood: "Aesop apothecary bottle — considered terracotta, quiet retail craft", + strategy: "Seed becomes a grounded clay primary against pure white, paired with a deeper umber accent so the warmth lives entirely in the brand marks, not the surface." }, + { id: "seed-167", oklch: [0.495, 0.134, 36.0], + mood: "Aesop apothecary shelf — burnished terracotta on clinical white, considered craft pharmacy", + strategy: "Treat the seed as a brand-carrying burnt-sienna against a pure paper-white surface so the warmth lives entirely in the primary, with a deep umber accent pulled along the same warm axis for typographic gravity." }, + { id: "seed-147", oklch: [0.500, 0.151, 40.0], + mood: "Aesop apothecary shelf — considered terracotta, pharmacy restraint, the brand color does the work against clinical white", + strategy: "Anchor the seed's burnt-sienna primary against a pure white surface so the rust speaks alone, with a deep umber ink and a cooler clay accent to give the palette product-brand discipline rather than environmental warmth." }, + { id: "seed-040", oklch: [0.660, 0.201, 40.0], + mood: "Aesop apothecary bottle — amber glass on a clean dispensary shelf, considered and clinical-warm", + strategy: "Seed becomes a burnt-amber primary against pure white so the bottle-glass color does the emotional work; accent shifts to a deep olive-bronze for the apothecary-label pairing." }, + { id: "seed-041", oklch: [0.673, 0.217, 38.6], + mood: "Aesop apothecary shelf — considered orange glass, clinical retail restraint", + strategy: "Pure white surface lets the burnt-orange primary do all the brand work, with a deep ink-brown for editorial gravity and a muted clay accent that reads as a sibling, not a contrast." }, + { id: "seed-042", oklch: [0.688, 0.133, 35.8], + mood: "Aesop apothecary shelf — terracotta glass, considered retail", + strategy: "Seed becomes a warm clay primary against pure white so the bottle-on-marble retail feel comes from the brand color alone; a deeper umber accent gives the label-print contrast." }, + { id: "seed-043", oklch: [0.781, 0.119, 38.1], + mood: "Aesop apothecary catalogue — considered terracotta, dermatological restraint, the warm color doing all the work against clinical white", + strategy: "Pure white surface lets the seed's warm clay tone read as the entire brand voice, paired with a deeper umber accent for hierarchy without competing with the primary's warmth." }, + { id: "seed-168", oklch: [0.400, 0.103, 50.0], + mood: "Aesop apothecary bottle — amber glass on a clinical white shelf, considered and pharmaceutical", + strategy: "Pure white surface lets the deep amber primary act like tinted glass against a clean shelf; accent is a muted clay that complements without competing, keeping the brand quiet and product-led." }, + { id: "seed-044", oklch: [0.568, 0.149, 45.9], + mood: "1970s desert highway at golden hour — sun-faded terracotta, denim dust, the warmth of a Polaroid pulled from a glovebox", + strategy: "Seed becomes a burnt-sienna primary against pure white so the terracotta does all the emotional work; a deep indigo accent acts as the denim shadow opposing the sun, creating the era's signature warm/cool tension without tinting the page." }, + { id: "seed-045", oklch: [0.607, 0.163, 47.7], + mood: "Aesop apothecary shelf — considered amber glass, clinical restraint, craft pharmacy", + strategy: "Pure white bg lets the burnt-amber primary do the apothecary work alone, paired with a deeper umber accent and graphite ink for editorial calm." }, + { id: "seed-046", oklch: [0.653, 0.175, 45.0], + mood: "Aesop apothecary shelf — considered amber glass, quiet luxury, restrained craft", + strategy: "Pure black backdrop lets the warm amber primary glow like backlit apothecary glass, with a deeper rust accent providing tonal depth in the same hue family — monochromatic warm against neutral void." }, + { id: "seed-047", oklch: [0.695, 0.205, 43.2], + mood: "Aesop apothecary label — sun-warmed amber glass on a clinical countertop, restrained botanical pharmacy", + strategy: "Pure white surface lets the burnt-amber primary and a deeper sienna accent do all the brand work, like an apothecary bottle photographed under daylight." }, + { id: "seed-051", oklch: [0.704, 0.189, 49.0], + mood: "blacksmith's forge at dusk — glowing iron, hammered copper, ember light against cooling steel", + strategy: "Pure near-black surface lets the seed's molten orange burn like heated metal; accent shifts to a deeper amber-red to suggest the cooling end of the same iron, while ink stays a clean off-white so type reads like chalk on slate." }, + { id: "seed-171", oklch: [0.550, 0.124, 60.0], + mood: "Klim Type Foundry specimen page — considered ochre on paper, design-school-honest", + strategy: "Seed becomes a muted ochre primary on pure white; accent is a deep ink-navy pulled across the wheel for editorial contrast without warmth-pooling in the bg" }, + { id: "seed-148", oklch: [0.650, 0.146, 60.0], + mood: "Klim-style editorial gold — late-afternoon paper light on a serif specimen sheet, considered and dry", + strategy: "Hold the seed's amber as primary on a pure white page so the gold reads as ink rather than atmosphere, and pair with a deep aubergine accent for typographic contrast." }, + { id: "seed-052", oklch: [0.700, 0.130, 60.0], + mood: "late-afternoon terracotta studio — sun-warmed clay, hands-on craft, the hour before dusk", + strategy: "Seed is a saturated amber-ochre with strong environmental association (ceramics, adobe, sunlit plaster), so I lean into Exception (a) with a faintly warm bone surface that reads as lime-washed wall, then deepen the seed slightly for primary and pair it with a fired-clay rust accent for hand-thrown warmth." }, + { id: "seed-053", oklch: [0.773, 0.157, 56.6], + mood: "late-summer apricot orchard at golden hour — sun-warmed fruit, considered Californian craft", + strategy: "Seed is a juicy mid-warm orange at daylight luminance — leaning optimistic/editorial, so pure white surface lets the apricot primary glow without muddying it; a deep wine accent provides the bite." }, + { id: "seed-149", oklch: [0.600, 0.124, 70.0], + mood: "1970s desert highway — late-afternoon amber light on chrome and asphalt", + strategy: "Anchor the amber seed as primary against pure black so the warm hue reads as headlight glow against night; a cooler dusk-mauve accent provides the complementary tension of horizon vs. sun." }, + { id: "seed-054", oklch: [0.740, 0.162, 68.1], + mood: "late-afternoon honey on terracotta — Mediterranean stucco at golden hour, sun-baked amber", + strategy: "Seed is a saturated honey-amber at high lightness; pairing it with pure black lets the warmth read as luminous gold against gravity, like lamplight in a dark room." }, + { id: "seed-055", oklch: [0.774, 0.174, 65.1], + mood: "late-summer honey hour — amber light slanting through a west-facing window, optimistic and golden", + strategy: "Anchor a saturated honey-amber primary on pure white so the warmth radiates from the brand itself, then pair with a deep teak accent for grounded contrast rather than tinting the canvas." }, + { id: "seed-056", oklch: [0.691, 0.146, 74.6], + mood: "Klim-style modern publishing house — late-afternoon paper warmth, considered editorial gold", + strategy: "Pure white surface so the amber seed becomes the brand voice; ink stays near-black neutral and accent shifts to a deep ink-blue to give the gold something structural to lean on." }, + { id: "seed-150", oklch: [0.750, 0.148, 80.0], + mood: "Klim Type Foundry specimen page — late-summer editorial gold, considered and grown-up", + strategy: "Pure white surface lets a single restrained ochre primary do all the brand work, paired with a deep ink-blue accent for typographic contrast in the Klim/Commercial Type tradition." }, + { id: "seed-058", oklch: [0.764, 0.120, 77.1], + mood: "Klim Type Foundry specimen page — late-afternoon ochre, considered editorial typography", + strategy: "Pure white surface lets the ochre primary do the brand work, paired with a deep ink-blue accent for editorial contrast — the type-foundry move where one warm hue carries the whole feeling against neutral paper." }, + { id: "seed-059", oklch: [0.784, 0.144, 79.8], + mood: "late afternoon in a Tuscan limonaia — sun-cured amber on whitewashed plaster", + strategy: "Pure white surface lets the saffron-amber primary and a deep olive accent carry the Mediterranean warmth, with split-complementary tension between gold and a quiet evergreen." }, + { id: "seed-061", oklch: [0.817, 0.161, 75.1], + mood: "late-afternoon honey on Tuscan limestone — golden hour, slow and luminous", + strategy: "Pure white surface lets the amber primary glow like sunlight on a wall, paired with a deep terracotta accent for warm tonal contrast within the same hue family." }, + { id: "seed-063", oklch: [0.842, 0.165, 91.3], + mood: "late-afternoon Tuscan sun on limestone — golden hour, considered, optimistic", + strategy: "Pure white surface lets the amber-gold primary radiate as the mood-carrier, with a deep aubergine accent providing the long shadow that golden light needs to feel three-dimensional." }, + { id: "seed-174", oklch: [0.350, 0.075, 110.0], + mood: "olive grove at late afternoon — sun-cured leaves, dust, and quiet Mediterranean weight", + strategy: "Pure white surface lets a deep, sun-cured olive primary do the emotional work, with a burnt-terracotta accent providing the warm-earth counterpoint olive groves are known for." }, + { id: "seed-117", oklch: [0.650, 0.100, 110.0], + mood: "Klim-style editorial sage — late-summer foundry catalogue, considered olive-yellow on paper", + strategy: "Seed sits at olive-chartreuse; treating it as a quiet typographic primary on pure paper, with a deeper bronze-olive accent for hierarchy — the color does the work, the page disappears." }, + { id: "seed-118", oklch: [0.750, 0.090, 110.0], + mood: "Klim Type Foundry specimen page — late-summer olive light on a working specimen, the honesty of a type designer showing their work", + strategy: "Pure white bg lets a desaturated olive-yellow primary do the editorial work, with a deeper olive-bronze accent providing typographic emphasis the way a specimen uses one heavy weight against the body roman." }, + { id: "seed-065", oklch: [0.797, 0.166, 113.1], + mood: "late-summer olive grove at noon — sun-bleached leaves, dry stone, Mediterranean glare", + strategy: "Hold the seed as a luminous chartreuse-olive primary against pure white so the color reads as sunlit foliage, pairing it with a deep umber accent for the dry-stone contrast." }, + { id: "seed-176", oklch: [0.300, 0.071, 120.0], + mood: "moss-darkened apothecary jar — herbal, shadowed, mid-19th-century botanical study", + strategy: "Seed is a deep desaturated olive-green that reads as preserved botanical pigment; I anchor it on pure white so the dim moss-green primary feels like ink on a herbarium page, with a warm ochre accent supplying the aged-paper counterpoint." }, + { id: "seed-155", oklch: [0.550, 0.142, 130.0], + mood: "moss-bed forest floor at noon — chlorophyll, lichen, sunlit fern", + strategy: "Seed is a confident mid-olive green with strong chroma; mood is daylight botanical, so I let the brand greens do the work on a pure paper-white bg and pair with a warm umber accent for fern-against-bark contrast." }, + { id: "seed-119", oklch: [0.600, 0.154, 130.0], + mood: "moss garden at Saihō-ji — damp stone, filtered green light through old cedar", + strategy: "Pure near-black bg lets the seed's mossy green glow like wet lichen under low light; accent shifts to a pale ochre-gold like sun catching through canopy." }, + { id: "seed-179", oklch: [0.300, 0.096, 140.0], + mood: "moss on wet stone — forest floor at dusk, deep botanical hush", + strategy: "Kept the seed's deep moss green as primary against a near-black surface so the green reads as living shadow, with a pale lichen accent providing the single point of light." }, + { id: "seed-180", oklch: [0.350, 0.110, 140.0], + mood: "moss-darkened apothecary — herbal tinctures in amber glass, pressed botanicals, the deep green of a conservatory at dusk", + strategy: "Near-black bg with a whisper of green undertone lets the seed's deep moss read as luminous foliage; a warm parchment accent provides the apothecary-label counterpoint without breaking the herbal register." }, + { id: "seed-120", oklch: [0.650, 0.100, 140.0], + mood: "moss on weathered stone — quiet botanical garden conservatory at midday", + strategy: "Pure white bg lets the muted sage-green primary read as a considered botanical mark, with a deeper terracotta accent providing earthen counterpoint without breaking the gallery-like restraint." }, + { id: "seed-121", oklch: [0.750, 0.090, 140.0], + mood: "moss garden at Saihō-ji — diffuse green light filtered through wet stone and lichen", + strategy: "Pure near-black bg lets the muted sage-green primary glow like lichen under low light; a warm pale-bone accent acts as the single ray of sun cutting through canopy." }, + { id: "seed-182", oklch: [0.400, 0.106, 150.0], + mood: "moss garden at Saiho-ji — deep cultivated green under wet stone shadow, contemplative and damp", + strategy: "Near-black bg with the faintest cool-green undertone evokes shaded stone; primary holds the seed's moss tone while accent shifts to a lichen-yellow for organic counterpoint without breaking the hush." }, + { id: "seed-157", oklch: [0.550, 0.145, 150.0], + mood: "moss garden at Saiho-ji — damp stone, filtered green light through cedar canopy", + strategy: "Near-black bg with a faint green undertone evokes deep forest shadow; primary holds the seed's verdant register while accent shifts to a pale lichen-cream to mimic light catching moss." }, + { id: "seed-122", oklch: [0.600, 0.158, 150.0], + mood: "forest floor at first light — moss, lichen, and clean morning air", + strategy: "Seed reads as a living, daylight green; surface stays pure white so the green carries the freshness, with a cool teal accent pulling it toward dew rather than earth." }, + { id: "seed-195", oklch: [0.650, 0.150, 145.0], + mood: "Considered horticulture brand — botanical research lab, the green of a healthy stem photographed in clean daylight", + strategy: "Pure white surface lets the seed's vegetal green carry the entire brand voice, paired with a deep forest ink and a warm clay accent for editorial contrast." }, + { id: "seed-183", oklch: [0.350, 0.077, 160.0], + mood: "moss-stained apothecary — deep forest glass, herbal tinctures shelved in low candlelight", + strategy: "Anchored the seed as primary and built a near-black dark surface with whisper-tinted green to evoke aged apothecary glass, letting the green glow rather than shout." }, + { id: "seed-184", oklch: [0.400, 0.087, 160.0], + mood: "deep forest apothecary — moss, bottle glass, and herbal tincture under afternoon light", + strategy: "Seed becomes a botanical-bottle-green primary on pure white, paired with a warm clove-amber accent to evoke herbal pharmacy contrast without tinting the surface." }, + { id: "seed-158", oklch: [0.550, 0.119, 160.0], + mood: "moss on wet stone — forest floor after rain, mineral and quiet", + strategy: "Pure white surface lets the deep mossy green carry the entire mood; accent shifts to a damp slate-teal to sit beside primary like lichen on stone without competing." }, + { id: "seed-159", oklch: [0.600, 0.130, 160.0], + mood: "moss-covered forest apothecary — herbal tinctures in amber glass, eucalyptus shadow", + strategy: "Anchored the green seed in a near-black backdrop so it reads like botanical glassware lit from within, with a warm amber accent pulled across the wheel to evoke tincture bottles against dark wood." }, + { id: "seed-185", oklch: [0.450, 0.086, 170.0], + mood: "weathered copper patina on a Pacific Northwest greenhouse — oxidized teal, glass light, botanical hush", + strategy: "Seed sits as a deep oxidized-teal primary against pure white so the patina reads as pigment, not atmosphere; a rust-copper accent completes the verdigris/oxidation story across the warm-cool axis." }, + { id: "seed-124", oklch: [0.750, 0.080, 170.0], + mood: "sea-glass on a foggy Pacific shoreline — weathered, mineral, quietly oxidized", + strategy: "Seed is a soft desaturated teal-green; pairing it on pure white lets the mineral primary read as patinated copper-glass, with a deeper kelp-toned primary and a rusted coral accent to spark the muted teal against its complement." }, + { id: "seed-160", oklch: [0.550, 0.095, 180.0], + mood: "weathered copper patina on a museum bronze — oxidized teal, conservatorial quiet", + strategy: "Pure near-black gallery surround lets the patina-teal primary glow like a lit artifact, with a warm verdigris-adjacent accent providing the oxidation contrast against the cool seed." }, + { id: "seed-161", oklch: [0.720, 0.100, 188.0], + mood: "climate-tech dashboard — calm verdigris on plain paper, the quiet confidence of an instrument that just works", + strategy: "Seed teal carries the entire mood as a single considered brand color on pure white, with a desaturated copper accent providing warm signal against the cool primary without competing for attention." }, + { id: "seed-186", oklch: [0.450, 0.074, 200.0], + mood: "deep hydrothermal vent — mineral teal under pressure, the cold blue-green of oxidized copper in submerged light", + strategy: "Near-black surface lets the mineral teal glow as if lit from within; accent shifts toward verdigris-copper to suggest patina on submerged metal, while ink stays cool-neutral to keep the register austere rather than aquatic-cute." }, + { id: "seed-125", oklch: [0.650, 0.100, 200.0], + mood: "climate-tech dashboard — calm operational teal, the color of clean water data and atmospheric sensors", + strategy: "Pure white surface lets a single muted-teal primary do all the brand work, with a deeper marine accent providing hierarchy without competing chroma." }, + { id: "seed-126", oklch: [0.750, 0.080, 200.0], + mood: "climate-tech product brand — quiet competence, dashboards for hard infrastructure problems", + strategy: "Hold the seed's muted teal as primary, pair with a sharper cyan-leaning accent for interactive lift, and let a pure white surface do the disappearing act so the brand reads as a tool, not an atmosphere." }, + { id: "seed-162", oklch: [0.550, 0.091, 210.0], + mood: "weathered nautical instrument — patinated brass on oxidized steel, the cool blue-grey of a ship's chronometer at dawn", + strategy: "Pure white surface lets the muted teal-steel primary read as a precise instrument mark, with a warm brass accent providing the single point of patina against clinical white." }, + { id: "seed-163", oklch: [0.450, 0.086, 230.0], + mood: "deep harbor at dusk — weathered nautical instruments, brass dials on oxidized steel", + strategy: "Near-black background with subtle cool tint evokes the marine dusk; primary holds the seed's teal-blue while a warm brass accent creates the instrument-on-steel tension." }, + { id: "seed-164", oklch: [0.550, 0.105, 230.0], + mood: "deep harbor at dawn — cold steel water, fog-muted light, the quiet before the boats leave", + strategy: "Pure near-black bg lets the seed's cold marine blue read as a luminous beacon, while a pale frost-cyan accent evokes diffused dawn light cutting through fog." }, + { id: "seed-127", oklch: [0.650, 0.100, 230.0], + mood: "climate-tech dashboard — atmospheric sensor blue, calm operational clarity", + strategy: "Anchor the seed as a confident mid-blue primary on pure white so the brand color carries all the atmospheric feeling, with a deep navy accent for hierarchy and a soft slate muted for body text." }, + { id: "seed-128", oklch: [0.750, 0.080, 230.0], + mood: "climate-tech dashboard — calm atmospheric data, considered sky-blue", + strategy: "Pure white surface lets the muted sky-blue primary carry the meteorological calm, with a deep-navy accent providing readable weight against the soft primary." }, + { id: "seed-187", oklch: [0.350, 0.078, 240.0], + mood: "deep harbor at blue hour — wet stone, cold steel, the quiet before night fully lands", + strategy: "Near-black architectural bg with a hint of marine chroma lets the seed read as ambient atmosphere rather than UI chrome; a cooler steel accent sits opposite the warmer-shifted primary for navigational clarity." }, + { id: "seed-077", oklch: [0.578, 0.130, 241.7], + mood: "pre-dawn signal tower — cold blue solitude, instruments glowing against the dark", + strategy: "Pure near-black bg lets the seed's cold tower-light blue glow as the sole emotional source, with a frost-cyan accent acting as a secondary indicator light." }, + { id: "seed-188", oklch: [0.400, 0.110, 250.0], + mood: "Linear's considered indigo — the calm authority of a well-built developer tool, blueprint ink on a clean page", + strategy: "Held the seed as a deep indigo primary against pure white so the brand color carries all the gravity; accent shifts to a cooler, brighter cyan-blue to create a crisp hierarchy pair without warming the surface." }, + { id: "seed-165", oklch: [0.450, 0.123, 250.0], + mood: "blueprint room at dusk — drafting table, graphite, civic-engineering blue", + strategy: "Seed is a mid-deep architectural blue with real chroma and no environmental cue, so I stay out of the way with a pure white surface and let the primary do all the talking, pairing it with a burnt-ochre accent for drafting-pencil contrast." }, + { id: "seed-079", oklch: [0.478, 0.136, 251.8], + mood: "twilight cartography — the blue of deep dusk over open water, precise and navigational", + strategy: "Pure white surface lets the seed's oceanic blue act as a single navigational anchor, with a warm amber accent struck across it like a lighthouse beam at dusk." }, + { id: "seed-080", oklch: [0.541, 0.122, 248.2], + mood: "Linear-style considered tool blue — the calm, exact register of a modern engineering app where every pixel is intentional", + strategy: "Pure white surface lets the considered indigo-blue primary carry the entire brand; a deeper navy accent provides hierarchy without warmth, keeping the palette in a single cool family for that focused-software feel" }, + { id: "seed-166", oklch: [0.550, 0.149, 250.0], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and quietly intense", + strategy: "Near-black bg with the faintest cool tint reads like a darkened cockpit; the seed becomes a luminous instrument-blue primary, paired with a warm amber accent that mimics avionics readouts for unmistakable signal contrast." }, + { id: "seed-081", oklch: [0.650, 0.160, 250.0], + mood: "deep-sea research vessel at dawn — instrument glow against cold steel light", + strategy: "Pure near-white bg keeps the palette technical and instrument-like; the seed blue holds as primary while a desaturated steel-cyan accent reads like signal readouts on glass." }, + { id: "seed-082", oklch: [0.742, 0.140, 247.4], + mood: "high-altitude flight deck at dawn — cold cabin instruments glowing against a sky still holding night", + strategy: "Near-black cockpit ground with a faint blue cast lets the seed read as an illuminated instrument; primary holds the seed, accent shifts to cyan for signal/indicator contrast." }, + { id: "seed-210", oklch: [0.360, 0.140, 260.0], + mood: "Linear-style considered tool indigo — late-night focused work, the deep blue of a code editor at 2am where everything else falls away", + strategy: "Pure black bg lets the indigo primary carry all the cognitive-focus weight, with a slightly brighter periwinkle accent for interactive lift — the surface disappears so the tool feels weightless." }, + { id: "seed-189", oklch: [0.400, 0.130, 260.0], + mood: "pre-dawn observatory — cold instrument blue, star-chart precision", + strategy: "Seed becomes the primary on pure black so the deep instrument-blue glows like a calibration light, with a faint cyan accent reading as starlight against the void." }, + { id: "seed-211", oklch: [0.420, 0.161, 260.0], + mood: "Linear's considered indigo — the tool-for-thought blue of focused product work, calm authority without coldness", + strategy: "Hold the seed as a deep indigo primary against pure white, then pair with a slightly warmer, lighter periwinkle accent to create gentle hue separation without breaking the disciplined tool-brand register." }, + { id: "seed-129", oklch: [0.450, 0.150, 260.0], + mood: "pre-dawn observatory — deep cobalt sky just before astronomical twilight, instruments cool to the touch", + strategy: "Near-black surface lets the cobalt seed read as luminous starlight; a single warm amber accent acts as the calibration lamp against the cold blue field." }, + { id: "seed-084", oklch: [0.476, 0.207, 261.2], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and awake", + strategy: "Default B black bg lets the cobalt primary read as a luminous instrument signal, with a cyan accent striking the analogous 'cockpit display' relationship." }, + { id: "seed-085", oklch: [0.681, 0.132, 258.4], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky", + strategy: "Anchored the seed as a luminous primary against a near-black architectural ground, with a warm amber accent acting as the single instrument light cutting through cold blue." }, + { id: "seed-086", oklch: [0.767, 0.106, 255.9], + mood: "Scandinavian winter morning — quiet light through frost, pale sky over snow", + strategy: "Anchored a pure white editorial stage so the seed's cool sky-blue reads as crisp polar light, with a deeper navy primary providing the only saturated weight — like a single dark pine against snow." }, + { id: "seed-083", oklch: [0.340, 0.159, 262.4], + mood: "deep cobalt twilight — the moment after sunset when the sky goes electric blue and city windows start to glow", + strategy: "Pure black stage lets the cobalt seed act as a luminous neon-window glow, with a warm amber accent across the wheel for the lit-window contrast." }, + { id: "seed-212", oklch: [0.360, 0.219, 270.0], + mood: "Linear-grade tooling indigo — considered software for people who care about craft", + strategy: "Anchored the deep indigo seed as primary on a pure white surface so the brand color carries all the weight, with a slightly cooler violet-blue accent for hierarchy without competing chroma." }, + { id: "seed-130", oklch: [0.400, 0.150, 270.0], + mood: "Linear-grade indigo — considered productivity tool, ink on paper, no theatrics", + strategy: "Pure white surface lets a deep cool indigo carry all the brand weight, paired with a slightly warmer violet-blue accent for hierarchy without acid." }, + { id: "seed-213", oklch: [0.411, 0.241, 267.9], + mood: "Linear-style indigo — considered tool surface, the kind of blue-violet that sits behind a developer's keyboard at 11pm without shouting", + strategy: "Pure black canvas lets a saturated indigo primary do all the brand work, with a cooler cyan-violet accent providing UI signal without competing." }, + { id: "seed-131", oklch: [0.450, 0.180, 270.0], + mood: "monastic indigo dusk — vespers light through stained glass, contemplative and severe", + strategy: "Seed becomes a deep indigo primary against pure near-black so the violet reads as luminous stained-glass against architectural shadow, with a cooler iris accent for tonal lift." }, + { id: "seed-088", oklch: [0.476, 0.158, 268.5], + mood: "pre-dawn astronomer's notebook — deep indigo sky just before the stars fade, ink and graphite", + strategy: "Near-black bg with the faintest cool tint to evoke night sky without theatrics; primary holds the seed's indigo, accent shifts to a paler periwinkle for stellar contrast, keeping the palette monochromatic-cool and observational." }, + { id: "seed-196", oklch: [0.530, 0.130, 268.0], + mood: "Linear-style considered tool indigo — the deep-focus blue-violet of a thoughtfully built productivity surface, the color of a well-typeset keyboard shortcut", + strategy: "Pure white bg lets the indigo seed do all the brand work as primary, with a slightly darker, more saturated violet-shifted accent for hierarchy and interactive states — the surface disappears so the brand color reads as the entire identity." }, + { id: "seed-132", oklch: [0.700, 0.120, 270.0], + mood: "Linear-style considered tool indigo — the quiet violet of a focused product workspace, late-afternoon thinking", + strategy: "Pure white surface lets a muted indigo-violet primary and a slightly cooler accent do all the brand work, keeping the register calm and software-like rather than theatrical." }, + { id: "seed-090", oklch: [0.445, 0.206, 279.1], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, not a nightclub", + strategy: "Anchor the seed as a confident product primary on pure white, with a cooler indigo-shift accent that reads as a sibling tool color, so the brand violet does all the emotional work." }, + { id: "seed-133", oklch: [0.500, 0.160, 280.0], + mood: "Linear-adjacent indigo — considered productivity tool, the violet of a thinking workspace", + strategy: "Seed becomes a measured indigo primary on pure white; accent shifts to a cooler blue-violet to create hierarchy without nightclub saturation, letting the brand color do all the emotional work." }, + { id: "seed-094", oklch: [0.533, 0.125, 294.3], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, calm authority for a creative workspace", + strategy: "Pure white canvas lets the indigo-violet primary carry the entire brand voice; accent shifts hue slightly toward blue for a cool, tool-like duotone rather than warm decorative pairing." }, + { id: "seed-137", oklch: [0.700, 0.120, 290.0], + mood: "Linear-adjacent indigo — the considered tool, late-evening focus mode, software made for people who care about craft", + strategy: "Pure black surface lets a single restrained indigo-violet carry the brand, with a cooler periwinkle accent providing UI hierarchy without competing — Vercel/Linear dark-mode discipline." }, + { id: "seed-100", oklch: [0.450, 0.150, 330.0], + mood: "velvet boudoir at last call — bruised orchid and lipstick traces under low lamplight", + strategy: "Pure near-black surface lets a deep magenta-rose primary smolder while a warm peach accent acts like skin-lit lamplight — drama lives in the brand pair, not the room." }, + { id: "seed-103", oklch: [0.650, 0.160, 330.0], + mood: "1980s Memphis boudoir — powder-pink neon humming against lacquered black, lipstick and lacquer", + strategy: "Near-black gallery surface lets the magenta-pink seed read as lit neon; accent shifts to warm coral to create cinematic dichromatic tension without competing chroma." }, + { id: "seed-228", oklch: [0.360, 0.147, 340.0], + mood: "Figma-era creative tool plum — considered productivity software for designers, the inky violet of a serif wordmark on a marketing site", + strategy: "Held the seed as a deep plum primary against pure white so the brand color does the emotional work; paired with a muted rose accent for warmth without breaking the productivity-tool restraint." }, + { id: "seed-107", oklch: [0.500, 0.200, 340.0], + mood: "Figma plum — creative-tool confidence, considered magenta for a modern design product", + strategy: "Pure white surface lets a saturated magenta-plum primary carry all the brand voice, paired with a cooler violet-leaning accent for hierarchy without competing." }, + { id: "seed-198", oklch: [0.600, 0.210, 340.0], + mood: "Figma-era creative tool plum — confident, considered, made for makers", + strategy: "Anchor a saturated plum primary against pure white so the brand color does all the emotional work, with a deeper magenta-rose accent for hierarchy." }, + { id: "seed-112", oklch: [0.754, 0.193, 343.4], + mood: "Figma-era creative tool — confident pink primary doing the brand work on a clean canvas, the way Linear uses indigo or Stripe uses violet", + strategy: "Anchor the seed pink as a saturated brand primary on pure white so the color carries all the personality; pair with a cooler plum accent to give the pink something to push against without competing." }, + { id: "seed-229", oklch: [0.420, 0.163, 350.0], + mood: "considered fintech rose — the deep magenta of a modern product brand (think Stripe-adjacent, but rotated toward berry), confident and current", + strategy: "pure white surface lets a single deep berry-rose primary do all the brand work, paired with a cooler indigo accent for the contrast move you see in modern product marketing" }, + { id: "seed-113", oklch: [0.470, 0.173, 354.8], + mood: "1960s velvet rope nightclub — crushed magenta, low light, cigarette smoke catching a spotlight", + strategy: "Pure black stage so the seed's smoky magenta reads as a single hot spotlight, paired with a cooler violet accent for the second light cue." }, + { id: "seed-114", oklch: [0.570, 0.158, 353.3], + mood: "fin-de-siècle Parisian rose — velvet curtain, theatre program, lipstick blotted on linen", + strategy: "Drop bg to true black so the dusty-rose primary reads as stage-lit silk; accent shifts to a warmer coral-mauve at higher lightness to create gentle hue rotation without breaking the romance." }, + { id: "seed-199", oklch: [0.650, 0.180, 350.0], + mood: "modern fintech rose — the considered pink of a Series B brand mark, confident and current without nostalgia", + strategy: "Pure white surface lets a saturated rose primary do the brand work, paired with a deep plum accent for hierarchy — the Stripe move applied to a pink hue." }, + { id: "seed-115", oklch: [0.636, 0.218, 355.3], + mood: "backstage at a cabaret — velvet rope, lipstick mark on a champagne glass", + strategy: "Seed reads as a saturated stage-light magenta-red; I push it into pure black so the primary glows like a neon sign and the accent (a cold pearl-pink) acts as the spotlight rim — the room is dark, the color does the singing." }, + { id: "seed-230", oklch: [0.650, 0.249, 354.5], + mood: "Modern fintech rose — the considered pink of a contemporary payments brand: confident, alive, and clear-headed", + strategy: "Pure white bg lets a saturated rose-magenta primary carry all the brand energy, paired with a cooler indigo accent for trustworthy contrast — the Stripe move applied to a pink hue." }, + { id: "seed-231", oklch: [0.682, 0.241, 353.2], + mood: "Figma-era creative tool — a confident pink-magenta product brand, the kind a modern design platform uses to feel alive without shouting", + strategy: "Default A pure white bg lets the saturated pink-magenta primary do all the brand work, with a near-complementary cool teal accent for tool-like clarity and a neutral ink for editorial calm" }, + { id: "seed-116", oklch: [0.734, 0.183, 356.8], + mood: "modern beauty brand DTC — Glossier-adjacent pink, confident and current without being saccharine", + 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." }, +]; + +function parseArgs(argv) { + const args = { id: null, from: null }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === '--id' && argv[i + 1]) { args.id = argv[++i]; } + else if (a === '--from' && argv[i + 1]) { args.from = argv[++i]; } + } + return args; +} + +// Hash a key into a stable float in [0, 1) for deterministic weighted picks. +function hashUnit(key) { + const h = crypto.createHash('sha256').update(key).digest(); + return h.readUInt32BE(0) / 0x100000000; +} + +// The curated library is hue-skewed (more reds/oranges than teals/magentas) +// because that's where the source material + taste landed. Left uniform, a +// random pick would land on red ~1/3 of the time. Inverse-frequency weighting +// gives each seed a weight of 1/(count in its 30° hue bucket), so each hue +// ZONE is roughly equally likely to be chosen regardless of how many seeds it +// holds — fair rainbow exposure across runs without pruning the library. +function buildWeights(seeds) { + const bucketCount = {}; + const bucketOf = (s) => Math.floor(((s.oklch[2] % 360) + 360) % 360 / 30); + for (const s of seeds) { const b = bucketOf(s); bucketCount[b] = (bucketCount[b] || 0) + 1; } + const weights = seeds.map((s) => 1 / bucketCount[bucketOf(s)]); + const total = weights.reduce((a, b) => a + b, 0); + return { weights, total }; +} + +function weightedPick(seeds, unit) { + const { weights, total } = buildWeights(seeds); + let target = unit * total; + for (let i = 0; i < seeds.length; i++) { + target -= weights[i]; + if (target < 0) return seeds[i]; + } + return seeds[seeds.length - 1]; +} + +function pickSeed(seeds, { id, from }) { + if (id) { + const found = seeds.find(s => s.id === id); + if (!found) { console.error(`no seed with id "${id}"`); process.exit(2); } + return found; + } + const envFrom = process.env.IMPECCABLE_PALETTE_SEED; + const key = from || envFrom; + const unit = key ? hashUnit(key) : Math.random(); + return weightedPick(seeds, unit); +} + +function fmtOklch([L, C, H]) { + return `oklch(${L.toFixed(3)} ${C.toFixed(3)} ${H.toFixed(1)})`; +} + +function hueWord(H) { + if (H < 15 || H >= 345) return 'pure red'; + if (H < 35) return 'warm red / crimson'; + if (H < 55) return 'warm coral / burnt orange'; + if (H < 80) return 'orange / honey'; + if (H < 105) return 'warm amber / honey-gold'; + if (H < 135) return 'yellow-green / olive'; + if (H < 170) return 'green'; + if (H < 200) return 'teal'; + if (H < 230) return 'sky blue'; + if (H < 265) return 'cobalt / indigo'; + if (H < 295) return 'violet / purple'; + if (H < 330) return 'magenta / pink'; + return 'deep pink / rose'; +} + +// --------------------------------------------------------------- + +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}` : ''; + +// --------------------------------------------------------------- +// Fat tool-exit response — what the model sees on stdout. +// --------------------------------------------------------------- + +process.stdout.write(`BRAND SEED · ${seed.id} + +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. + +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. + +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é. + + 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 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. + + 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. + +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): + + - 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. + +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. + +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. +`); diff --git a/.github/skills/impeccable/scripts/pin.mjs b/.github/skills/impeccable/scripts/pin.mjs index ba02783fb..320d98ce1 100644 --- a/.github/skills/impeccable/scripts/pin.mjs +++ b/.github/skills/impeccable/scripts/pin.mjs @@ -27,7 +27,7 @@ const HARNESS_DIRS = [ // Valid sub-command names const VALID_COMMANDS = [ - 'craft', 'teach', 'extract', 'document', 'shape', + 'craft', 'init', 'extract', 'document', 'shape', 'critique', 'audit', 'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live', 'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive', diff --git a/.gitignore b/.gitignore index 1bd57c6b1..385f91464 100644 --- a/.gitignore +++ b/.gitignore @@ -103,3 +103,6 @@ site/public/js/generated/ !.codex/agents/ !.codex/agents/** .astro/ + +# Local-only scratch for exploratory scripts, parked pages, and unused asset candidates. +tmp/ diff --git a/.impeccable/design.json b/.impeccable/design.json index 68497d70f..9a16a8bb7 100644 --- a/.impeccable/design.json +++ b/.impeccable/design.json @@ -1,281 +1,253 @@ { "schemaVersion": 2, - "generatedAt": "2026-04-23T00:00:00Z", + "generatedAt": "2026-05-26T00:00:00Z", "title": "Design System: Impeccable", "extensions": { "colorMeta": { - "editorial-magenta": { + "kinpaku-gold": { "role": "primary", - "displayName": "Editorial Magenta", - "description": "The one vibrant voice. Primary CTAs, active navigation, live-state indicators. Rarity is the design choice.", + "displayName": "Kinpaku Gold", + "canonical": "oklch(84% 0.19 80.46)", + "description": "Primary accent. CTAs, wordmark, active state, live picker borders, key rules.", "tonalRamp": [ - "oklch(22% 0.12 350)", - "oklch(32% 0.18 350)", - "oklch(42% 0.22 350)", - "oklch(52% 0.25 350)", - "oklch(60% 0.25 350)", - "oklch(72% 0.18 350)", - "oklch(84% 0.10 350)", - "oklch(94% 0.04 350)" + "oklch(18% 0.04 78)", + "oklch(28% 0.07 79)", + "oklch(38% 0.10 80)", + "oklch(48% 0.12 80)", + "oklch(58% 0.14 80)", + "oklch(68% 0.16 80)", + "oklch(78% 0.18 80)", + "oklch(88% 0.12 82)" ] }, - "editorial-magenta-deep": { - "role": "accent", - "displayName": "Editorial Magenta Deep", - "description": "Hover/active state for Editorial Magenta. Small darkening, confirms interaction without shouting." - }, - "warm-ash-cream": { - "role": "neutral", - "displayName": "Warm Ash Cream", - "description": "Primary page background. Almost-imperceptible magenta tint that creates subconscious cohesion with the accent.", + "verdigris-patina": { + "role": "secondary", + "displayName": "Verdigris Patina", + "canonical": "oklch(70% 0.12 188)", + "description": "Secondary accent. Improved states, live indicators, focus rings, contrast against gold.", "tonalRamp": [ - "oklch(15% 0.005 350)", - "oklch(25% 0.005 350)", - "oklch(40% 0.005 350)", - "oklch(55% 0.005 350)", - "oklch(70% 0.005 350)", - "oklch(82% 0.005 350)", - "oklch(92% 0.005 350)", - "oklch(96% 0.005 350)" + "oklch(18% 0.04 188)", + "oklch(28% 0.06 188)", + "oklch(38% 0.08 188)", + "oklch(48% 0.09 188)", + "oklch(58% 0.10 188)", + "oklch(68% 0.11 188)", + "oklch(78% 0.10 188)", + "oklch(88% 0.06 188)" ] }, - "crisp-paper-white": { + "lacquer-black": { "role": "neutral", - "displayName": "Crisp Paper White", - "description": "Pure background for inverted text moments (white-on-dark CTAs) and maximum-contrast surfaces." - }, - "deep-graphite": { - "role": "neutral", - "displayName": "Deep Graphite", - "description": "Primary text and primary-CTA background. Softer than pure black, reads as confident-but-not-aggressive on warm paper.", + "displayName": "Lacquer Black", + "canonical": "oklch(7% 0.006 95)", + "description": "Default page ground. Warm mineral black, never pure #000.", "tonalRamp": [ - "oklch(10% 0 0)", - "oklch(25% 0 0)", - "oklch(40% 0 0)", - "oklch(55% 0 0)", - "oklch(70% 0 0)", - "oklch(85% 0 0)", - "oklch(92% 0 0)", - "oklch(98% 0 0)" + "oklch(4% 0.004 95)", + "oklch(7% 0.006 95)", + "oklch(11% 0.006 95)", + "oklch(15% 0.008 95)", + "oklch(19% 0.008 95)", + "oklch(28% 0.010 95)", + "oklch(40% 0.012 82)", + "oklch(55% 0.014 82)" ] }, - "soft-charcoal": { + "lacquer-deep": { "role": "neutral", - "displayName": "Soft Charcoal", - "description": "Secondary text — taglines, hook paragraphs, supporting copy." + "displayName": "Lacquer Deep", + "canonical": "oklch(4% 0.004 95)", + "description": "Deepest inset surfaces, live picker bar fill, CTA text on gold." }, - "mid-ash": { + "raised-lacquer": { "role": "neutral", - "displayName": "Mid Ash", - "description": "Tertiary text — micro-labels, captions, meta lines. Reads as intentionally recessed metadata." + "displayName": "Raised Lacquer", + "canonical": "oklch(11% 0.006 95)", + "description": "Panels, cards, inputs, demo frames." }, - "paper-mist": { + "champagne": { "role": "neutral", - "displayName": "Paper Mist", - "description": "Hairline borders, section dividers, barely-visible structural seams." + "displayName": "Champagne", + "canonical": "oklch(84% 0.035 82)", + "description": "Headlines, strong labels, picker bar default text." }, - "magenta-whisper": { - "role": "accent", - "displayName": "Magenta Whisper", - "description": "Diffuse glow backdrop under accent elements on hover; subtle selection highlights." + "text-warm": { + "role": "neutral", + "displayName": "Warm Text", + "canonical": "oklch(81% 0.03 82)", + "description": "Body copy on dark lacquer surfaces." }, - "magenta-veil": { - "role": "accent", - "displayName": "Magenta Veil", - "description": "Stronger translucent tint for focus rings and emphasis shells." + "text-muted": { + "role": "neutral", + "displayName": "Muted Text", + "canonical": "oklch(63% 0.024 82)", + "description": "Captions, meta, inactive picker controls." + }, + "vermilion-warning": { + "role": "state", + "displayName": "Vermilion Warning", + "canonical": "oklch(58% 0.15 35)", + "description": "Errors, anti-pattern warnings, live picker exit hover." } }, "typographyMeta": { + "wordmark": { + "displayName": "Wordmark", + "purpose": "IMPECCABLE lockup only. Solid Alumni Sans, not pinstripe." + }, "display": { "displayName": "Display", - "purpose": "Hero title only. Light italic for an author-signature feel.", - "style": "italic" + "purpose": "Hero h1. Alumni Sans Pinstripe, weight 300." }, "headline": { "displayName": "Headline", - "purpose": "Section headings. Larger editorial moments." + "purpose": "Section h2. Pinstripe at weight 600, heavier than display." }, "title": { "displayName": "Title", - "purpose": "Hero tagline / section leads. A quieter second display voice.", - "style": "italic" + "purpose": "Panel and card headings (h3). Albert Sans." }, "body": { "displayName": "Body", - "purpose": "Paragraph copy. Capped at 65–75ch for readability." + "purpose": "Long copy. Albert Sans, line-height 1.8, max 65–75ch." }, - "body-lead": { - "displayName": "Body Lead", - "purpose": "The one or two lead paragraphs on each page. Slightly relaxed leading." - }, - "supporting": { - "displayName": "Supporting", - "purpose": "Captions, footnotes, supporting context." - }, - "label": { - "displayName": "Label", - "purpose": "CTA labels. Short, declarative. Uppercase, letter-tracked." - }, - "micro-label": { - "displayName": "Micro-Label", - "purpose": "\"Works with\", \"What's included\", \"v3.0 Changelog\"." + "eyebrow": { + "displayName": "Eyebrow", + "purpose": "Short mono labels above titles. Uppercase, tracked." }, "mono": { - "displayName": "Monospace Meta", - "purpose": "Command names in inline prose, periodic-table tile labels." + "displayName": "Mono Label", + "purpose": "Command names, audit lines, table headers." } }, "shadows": [ { - "name": "Soft Hover Lift", - "value": "0 4px 24px -4px rgba(0,0,0,0.12), 0 1px 3px rgba(0,0,0,0.06)", - "purpose": "Default hover response on cards and interactive surfaces." + "name": "Panel Setback", + "value": "0 24px 70px oklch(2% 0.004 95 / 0.42)", + "purpose": "Large framed modules only." }, { - "name": "Lifted Card", - "value": "0 20px 40px rgba(0,0,0,0.08)", - "purpose": "Deliberately elevated content (featured cards, install blocks)." + "name": "CTA Lift", + "value": "0 18px 48px oklch(2% 0.004 95 / 0.4)", + "purpose": "Primary button hover lift on large CTAs." }, { - "name": "Accent Glow", - "value": "0 20px 60px oklch(60% 0.25 350 / 0.15)", - "purpose": "Magenta-tinted ambient shadow under the rare magnetic moment. Used sparingly." + "name": "Patina Glow", + "value": "0 0 22px oklch(70% 0.105 190 / 0.24)", + "purpose": "Tiny live indicators only." + }, + { + "name": "Picker Halo", + "value": "0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)", + "purpose": "Live mode global and contextual picker bars." } ], "motion": [ { - "name": "ease-out", - "value": "cubic-bezier(0.16, 1, 0.3, 1)", - "purpose": "Primary curve. Expo-out feel — objects decelerate smoothly." - }, - { - "name": "ease-out-quint", - "value": "cubic-bezier(0.22, 1, 0.36, 1)", - "purpose": "Slightly sharper alternative. Use for transforms on compact elements." - }, - { - "name": "duration-fast", - "value": "0.15s", - "purpose": "State transitions (color, opacity)." - }, - { - "name": "duration-base", - "value": "0.3s", - "purpose": "Default for transforms and non-color changes." - }, - { - "name": "duration-slow", - "value": "0.6s", - "purpose": "Orchestrated entrances." + "name": "ks-ease", + "value": "cubic-bezier(0.2, 0.8, 0.2, 1)", + "purpose": "Default kit easing for color, border, and transform transitions." } ], - "breakpoints": [] + "breakpoints": [ + { "name": "md", "value": "980px" }, + { "name": "lg", "value": "1080px" } + ] }, "components": [ { - "name": "Primary CTA", + "name": "Primary Button", "kind": "button", "refersTo": "button-primary", - "description": "Sharp, squared, uppercase. The editorial signature.", - "html": "", - "css": ".ds-btn-primary { display: inline-block; padding: 16px 48px; font-family: 'Instrument Sans', system-ui, sans-serif; font-size: 0.9rem; font-weight: 500; letter-spacing: 0.05em; text-transform: uppercase; color: oklch(98% 0 0); background: oklch(10% 0 0); border: none; border-radius: 0; cursor: pointer; transition: transform 0.2s ease, background 0.2s ease; } .ds-btn-primary:hover { transform: translateY(-2px); background: oklch(60% 0.25 350); }" + "description": "Filled kinpaku CTA on lacquer. Both .ks-button and .ks-button-primary required.", + "html": "", + "css": ".ds-btn-primary { display: inline-flex; align-items: center; justify-content: center; min-height: 52px; padding: 0 28px; font-family: \"Albert Sans\", \"Avenir Next\", \"Helvetica Neue\", Arial, system-ui, sans-serif; font-size: 0.96rem; font-weight: 500; line-height: 1; color: oklch(4% 0.004 95); background: oklch(84% 0.19 80.46); border: 1px solid oklch(84% 0.19 80.46); border-radius: 2px; cursor: pointer; transition: transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1), background-color 180ms cubic-bezier(0.2, 0.8, 0.2, 1), border-color 180ms cubic-bezier(0.2, 0.8, 0.2, 1); } .ds-btn-primary:hover { background: oklch(86% 0.07 84); border-color: oklch(86% 0.07 84); transform: translateY(-1px); } .ds-btn-primary:active { transform: translateY(0); background: oklch(77% 0.13 82); border-color: oklch(77% 0.13 82); } .ds-btn-primary:focus-visible { outline: 2px solid oklch(70% 0.12 188); outline-offset: 3px; }" }, { - "name": "Inline Text Link", + "name": "Secondary Button", "kind": "button", - "description": "Secondary action. No boxed secondary button exists in the system.", - "html": "Read the docs", - "css": ".ds-link { color: oklch(10% 0 0); font-family: 'Instrument Sans', system-ui, sans-serif; font-weight: 500; text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px; transition: color 0.2s ease; } .ds-link:hover { color: oklch(60% 0.25 350); }" + "refersTo": "button-secondary", + "description": "Outlined kinpaku on transparent lacquer.", + "html": "", + "css": ".ds-btn-secondary { display: inline-flex; align-items: center; justify-content: center; min-height: 52px; padding: 0 28px; font-family: \"Albert Sans\", \"Avenir Next\", \"Helvetica Neue\", Arial, system-ui, sans-serif; font-size: 0.96rem; font-weight: 500; color: oklch(84% 0.19 80.46); background: transparent; border: 1px solid oklch(84% 0.19 80.46); border-radius: 2px; cursor: pointer; transition: transform 180ms cubic-bezier(0.2, 0.8, 0.2, 1), background-color 180ms cubic-bezier(0.2, 0.8, 0.2, 1); } .ds-btn-secondary:hover { background: oklch(77% 0.14 82 / 0.08); transform: translateY(-1px); } .ds-btn-secondary:focus-visible { outline: 2px solid oklch(70% 0.12 188); outline-offset: 3px; }" }, { - "name": "Email Input", + "name": "Text Input", "kind": "input", "refersTo": "input-text", - "description": "Hairline border, transparent background, magenta focus glow.", - "html": "", - "css": ".ds-input-email { width: 100%; padding: 10px 14px; font-family: 'Instrument Sans', system-ui, sans-serif; font-size: 0.9rem; color: oklch(10% 0 0); background: transparent; border: 1px solid oklch(92% 0 0); border-radius: 6px; outline: none; transition: border-color 0.15s ease, box-shadow 0.15s ease; } .ds-input-email::placeholder { color: oklch(55% 0 0); } .ds-input-email:focus { border-color: oklch(60% 0.25 350); box-shadow: 0 0 0 3px oklch(60% 0.25 350 / 0.15); }" - }, - { - "name": "Micro-Label", - "kind": "chip", - "description": "'What's included' style. Uppercase mono, wide tracking.", - "html": "What's included", - "css": ".ds-microlabel { display: inline-block; font-family: 'Space Grotesk', ui-monospace, monospace; font-size: 0.6875rem; font-weight: 500; letter-spacing: 0.1em; text-transform: uppercase; color: oklch(55% 0 0); }" + "description": "Raised lacquer field with gold hairline border, patina focus.", + "html": "", + "css": ".ds-input-text { width: 100%; min-height: 46px; padding: 0 14px; font-family: \"Albert Sans\", \"Avenir Next\", \"Helvetica Neue\", Arial, system-ui, sans-serif; font-size: 0.92rem; color: oklch(84% 0.035 82); background: oklch(11% 0.006 95); border: 1px solid oklch(58% 0.065 82 / 0.32); border-radius: 2px; outline: none; transition: border-color 180ms cubic-bezier(0.2, 0.8, 0.2, 1); } .ds-input-text::placeholder { color: oklch(63% 0.024 82); } .ds-input-text:focus { border-color: oklch(70% 0.12 188); }" }, { "name": "Site Navigation", "kind": "nav", "refersTo": "nav-link", - "description": "62px compact bar. No underline at rest; accent underline on active.", - "html": "", - "css": ".ds-nav { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 12px 20px; background: oklch(96% 0.005 350); font-family: 'Instrument Sans', system-ui, sans-serif; } .ds-nav-brand { font-family: 'Cormorant Garamond', Georgia, serif; font-style: italic; font-size: 1.05rem; color: oklch(10% 0 0); } .ds-nav-links { display: flex; gap: 16px; } .ds-nav-link { font-size: 0.85rem; font-weight: 500; color: oklch(10% 0 0); text-decoration: none; padding-bottom: 3px; border-bottom: 1px solid transparent; transition: color 0.2s ease, border-color 0.2s ease; } .ds-nav-link:hover { color: oklch(60% 0.25 350); } .ds-nav-active { border-bottom-color: oklch(60% 0.25 350); }" + "description": "Dark header nav. Champagne at rest, kinpaku on hover/active.", + "html": "", + "css": ".ds-nav { display: flex; gap: clamp(22px, 3vw, 48px); font-family: \"Albert Sans\", \"Avenir Next\", \"Helvetica Neue\", Arial, system-ui, sans-serif; font-size: 0.95rem; } .ds-nav-link { color: oklch(84% 0.035 82); text-decoration: none; transition: color 180ms cubic-bezier(0.2, 0.8, 0.2, 1); } .ds-nav-link:hover { color: oklch(84% 0.19 80.46); } .ds-nav-active { color: oklch(84% 0.19 80.46); }" }, { - "name": "Feature Card", + "name": "Bento Tile", "kind": "card", - "refersTo": "card-feature", - "description": "Standard card: 12px radius, hairline border, hover lift with soft shadow.", - "html": "

/impeccable polish

Sweep a site for AI-tool tells and refine typography, spacing, color in one pass.

", - "css": ".ds-card { padding: 20px 22px; background: oklch(98% 0 0); border: 1px solid oklch(92% 0 0); border-radius: 12px; transition: transform 0.2s ease, box-shadow 0.2s ease; font-family: 'Instrument Sans', system-ui, sans-serif; } .ds-card:hover { transform: translateY(-2px); box-shadow: 0 4px 24px -4px rgba(0,0,0,0.12), 0 1px 3px rgba(0,0,0,0.06); } .ds-card-title { font-family: 'Space Grotesk', ui-monospace, monospace; font-size: 0.85rem; font-weight: 500; color: oklch(10% 0 0); margin: 0 0 6px; } .ds-card-body { font-size: 0.85rem; line-height: 1.55; color: oklch(25% 0 0); margin: 0; }" + "refersTo": "card", + "description": "Flat lacquer tile inside the 12-column bento grid. No nested cards.", + "html": "
01

Respects your design system.

Inherits tokens and conventions instead of overwriting them.

", + "css": ".ds-bento-tile { display: flex; flex-direction: column; gap: 14px; padding: 48px; background: oklch(7% 0.006 95); border: 0; border-radius: 0; font-family: \"Albert Sans\", \"Avenir Next\", \"Helvetica Neue\", Arial, system-ui, sans-serif; } .ds-bento-num { font-family: \"SFMono-Regular\", \"Roboto Mono\", Consolas, monospace; font-size: 0.62rem; letter-spacing: 0.28em; text-transform: uppercase; color: oklch(84% 0.19 80.46); } .ds-bento-title { margin: 0; font-size: 1.18rem; font-weight: 500; color: oklch(84% 0.035 82); } .ds-bento-body { margin: 0; font-size: 1.02rem; line-height: 1.8; color: oklch(81% 0.03 82); max-width: 65ch; }" }, { - "name": "Command Category Tile", + "name": "Live Picker Bar", "kind": "custom", - "description": "Signature component: periodic-table tile used in the commands visualization.", - "html": "
07Popolish
", - "css": ".ds-tile { display: grid; grid-template-rows: auto 1fr auto; align-items: center; justify-items: center; width: 56px; height: 64px; padding: 4px; background: #fdf2f8; border: 1px solid #ec4899; border-radius: 6px; color: #be185d; cursor: pointer; transition: transform 0.2s ease, box-shadow 0.2s ease; } .ds-tile:hover { transform: translateY(-2px); box-shadow: 0 6px 16px rgba(236, 72, 153, 0.18); } .ds-tile-num { font-family: 'Space Grotesk', ui-monospace, monospace; font-size: 7px; font-weight: 500; letter-spacing: 0.05em; justify-self: start; } .ds-tile-sym { font-family: 'Cormorant Garamond', Georgia, serif; font-style: italic; font-size: 20px; font-weight: 500; line-height: 1; } .ds-tile-lbl { font-family: 'Space Grotesk', ui-monospace, monospace; font-size: 7px; font-weight: 500; letter-spacing: 0.05em; text-transform: lowercase; }" + "refersTo": "live-picker-bar", + "description": "Global live mode bar. Lacquer-deep fill, kinpaku border, carved-tile mark.", + "html": "
", + "css": ".ds-live-bar { display: inline-flex; align-items: center; gap: 2px; padding: 4px 5px; background: oklch(4% 0.004 95); border: 1.5px solid oklch(84% 0.19 80.46); border-radius: 10px; box-shadow: 0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28); font-family: system-ui, -apple-system, sans-serif; font-size: 12px; color: oklch(84% 0.035 82); } .ds-live-mark { display: inline-flex; align-items: center; padding: 0 12px 0 14px; color: oklch(84% 0.19 80.46); } .ds-live-toggle { padding: 6px 8px; border: none; border-radius: 7px; background: transparent; color: oklch(63% 0.024 82); font-family: inherit; font-size: 11.5px; font-weight: 500; cursor: pointer; transition: background 0.15s ease, color 0.15s ease; } .ds-live-toggle:hover { color: oklch(84% 0.035 82); } .ds-live-toggle.is-active { background: oklch(78% 0.12 82 / 0.18); color: oklch(84% 0.19 80.46); } .ds-live-toggle:focus-visible { outline: none; box-shadow: 0 0 0 2px oklch(84% 0.19 80.46 / 0.18), 0 0 0 3px oklch(84% 0.19 80.46); }" } ], "narrative": { - "northStar": "The Editorial Sanctuary", - "overview": "The Impeccable site reads more like a printed design publication than a SaaS landing page. Committed typography, generous breathing room, a single decisive accent. The aesthetic philosophy is restraint in service of craft. Every element earns its place.", + "northStar": "Neo Kinpaku", + "overview": "Impeccable is a dark lacquer interface marked by Japanese gold leaf and precise technical geometry. The brand should feel like a crafted object: black urushi, irregular kinpaku seams, quiet measurement marks, circuit traces, and controlled verdigris oxidation. It is refined, technical, and physical. This direction replaces the old warm-paper editorial system.", "keyCharacteristics": [ - "Warm off-white paper tones with an almost-imperceptible magenta tint.", - "A single decisive magenta accent used on ≤10% of any screen.", - "Italic serif for display type; clean neutral sans for body at 1.6 line-height.", - "Sharp, uppercase, letter-tracked primary CTAs.", - "Flat surfaces at rest. Shadows appear only on state change.", - "Magazine-scale spacing (8 / 16 / 24 / 32 / 48 / 80 / 120px)." + "Dark mineral and lacquer surfaces, never pure black.", + "Kinpaku gold as the primary accent, with true leaf texture where an element carries brand weight.", + "Verdigris patina as the secondary accent for state, contrast, and improved signals.", + "A geometric sans voice with a widely tracked wordmark, not a serif editorial voice.", + "Thin calibration lines, circuit geometry, and gold seams used as functional structure.", + "Small radii, restrained borders, and almost no decorative shadow." ], "rules": [ - { "name": "The One Voice Rule", "section": "colors", "body": "Editorial Magenta is the only vibrant color. No supporting accent, ever. If a second emphasis point is needed, use scale or weight, never a second hue." }, - { "name": "The Paper-Not-White Rule", "section": "colors", "body": "Page background is Warm Ash Cream, never Crisp Paper White. Warmth is load-bearing." }, - { "name": "The OKLCH-Only Rule", "section": "colors", "body": "All new colors declared in OKLCH. Hex is reserved for the fenced Command Category Tints." }, - { "name": "The Italic-Is-Voice Rule", "section": "typography", "body": "Italic is a voice choice for display type, not emphasis inside body copy. Body emphasis is carried by weight." }, - { "name": "The 1.6 Leading Rule", "section": "typography", "body": "Body line-height is 1.6 everywhere. Not 1.5, not 1.7. This is the load-bearing readability decision." }, - { "name": "The Fluid-Headlines-Only Rule", "section": "typography", "body": "Headings use clamp() fluid sizing. Body copy uses fixed rem values." }, - { "name": "The Flat-By-Default Rule", "section": "elevation", "body": "Surfaces are flat at rest. Reach for shadows only on hover or deliberate elevation." }, - { "name": "The Low-Alpha Rule", "section": "elevation", "body": "Every shadow uses ≤0.15 alpha on its strongest blur. Higher alphas read as 2014 Material drop shadows." }, - { "name": "The Tinted-Shadow-Only-For-Accent Rule", "section": "elevation", "body": "Neutral shadows for structure. Magenta-tinted shadows only for deliberate accent-glow moments." } + { "name": "The Gold Carries Brand Rule", "section": "colors", "body": "Kinpaku gold is the primary brand signal. If a single accent must represent Impeccable, use gold, not magenta or cyan." }, + { "name": "The Patina Has Meaning Rule", "section": "colors", "body": "Verdigris is secondary. It marks improvement, live state, or contrast against gold. Do not use it as a generic decoration field." }, + { "name": "The Texture Budget Rule", "section": "colors", "body": "Leaf and patina textures are for brand-bearing moments: hero seams, CTA fills, dividers, major swatches, and select system modules. Generic cards stay mostly flat." }, + { "name": "The OKLCH-Only Rule", "section": "colors", "body": "New colors are declared in OKLCH. Hex appears only inside third-party examples or imported assets." }, + { "name": "The Weight-Inversion Rule", "section": "typography", "body": "Section h2s read heavier (600) than the hero h1 (300). This is deliberate: the hero is elegant and thin so the page can breathe; section anchors carry more weight to ground each block." }, + { "name": "The Two-Face Rule", "section": "typography", "body": "Display sizes use Alumni Sans Pinstripe. Anything sized below 1.2rem uses Albert Sans. Pinstripe at small sizes loses its identity and reads as a bad rendering." }, + { "name": "The Tracked Labels Are Short Rule", "section": "typography", "body": "Tracked uppercase labels are for short system markers. Do not write full sentences in tracked caps." }, + { "name": "The Dark Type Needs Air Rule", "section": "typography", "body": "Body text on lacquer uses line-height 1.65 to 1.8 and a max width of 65 to 75ch." }, + { "name": "The Hairline First Rule", "section": "elevation", "body": "Use 1px gold hairlines before adding shadow." }, + { "name": "The No Glass Rule", "section": "elevation", "body": "Translucency can exist in overlays, but decorative blur/glass panels are not part of this system." }, + { "name": "The Picker Is Brand Rule", "section": "components", "body": "Live mode UI is Impeccable product chrome, not host-page chrome. It always ships the full kinpaku border, lacquer-deep fill, and carved-tile mark." } ], "dos": [ - "Do treat Warm Ash Cream as the default page background.", - "Do use Editorial Magenta on ≤10% of any given screen.", - "Do set all new colors in OKLCH.", - "Do use italic display type as a voice, not as emphasis inside paragraphs.", - "Do use clamp() fluid sizing for headings; use fixed rem for body.", - "Do keep the primary CTA sharp and squared. border-radius: 0, uppercase, letter-tracked.", - "Do use --ease-out (cubic-bezier(0.16, 1, 0.3, 1)) or --ease-out-quint on transitions.", - "Do leave surfaces flat at rest. Reach for shadows only on hover or deliberate elevation.", - "Do respect prefers-reduced-motion on every animation.", - "Do cap body line length at 65–75ch via max-width." + "Do use kinpaku gold as the primary brand color.", + "Do use verdigris patina for secondary state and contrast.", + "Do keep surfaces dark, warm, and mineral.", + "Do use real texture assets for gold leaf and oxidation when the element is brand-bearing.", + "Do use circuit/calibration geometry as structure, especially around product proof.", + "Do keep cards compact, flat, and sharply bounded.", + "Do preserve utility: demos, sliders, audit tables, command examples, docs, and pricing must remain understandable.", + "Do keep live mode picker bars on lacquer-deep with kinpaku gold borders, regardless of host page theme." ], "donts": [ - "Don't use pure black (#000) or pure white (#fff).", - "Don't use border-left or border-right greater than 1px as a colored stripe. The AI-dashboard tell.", - "Don't use background-clip: text with a gradient. Gradient text is banned.", - "Don't default to dark mode.", - "Don't use glassmorphism.", - "Don't add a second accent color.", - "Don't use rounded rectangles with generic drop shadows.", - "Don't use bounce or elastic easing.", - "Don't animate layout properties (width, height, padding, margin). Use transform and opacity.", - "Don't nest cards inside cards.", - "Don't use identical card grids.", - "Don't use the hero-metric layout template.", - "Don't extend the Command Category Tints vocabulary.", - "Don't hedge in UI copy.", - "Don't introduce a new spacing token outside the 8/16/24/32/48/80/120 scale." + "Do not use editorial magenta as a brand accent.", + "Do not use italic serif display typography.", + "Do not use purple gradients, neon cyan fields, glassmorphism, or generic AI-tool glow.", + "Do not put gold texture under long text.", + "Do not use beige, paper, or cream as the page ground.", + "Do not add decorative calibration marks that do not align with real content.", + "Do not use wide rounded cards or nested cards.", + "Do not use pure black or pure white.", + "Do not let the visual system hide the product proof.", + "Do not theme-adapt live mode picker chrome to match arbitrary host pages. The picker is always neo-kinpaku." ] } } diff --git a/.kiro/skills/impeccable/SKILL.md b/.kiro/skills/impeccable/SKILL.md index 7374744c8..47a323a3d 100644 --- a/.kiro/skills/impeccable/SKILL.md +++ b/.kiro/skills/impeccable/SKILL.md @@ -1,93 +1,88 @@ --- name: impeccable description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks. -version: 3.1.1 -license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution. +version: 3.5.0 +license: Apache 2.0 --- Designs and iterates production-grade frontend interfaces. Real working code, committed design choices, exceptional craft. ## Setup -Before any design work or file edits: +You MUST do these steps before proceeding: -1. Load context (PRODUCT.md / DESIGN.md) via the loader script. -2. Identify the register and load the matching register reference (brand.md or product.md). -3. **If the user invoked a sub-command (e.g. `craft`, `shape`, `audit`), load its reference file too.** This is non-negotiable: `craft` without `craft.md` loaded means you'll skip the shape-and-confirm step the user expects. +1. Run `node .kiro/skills/impeccable/scripts/context.mjs` once per session. If you've already seen its output in this conversation, do not re-run it. The script either prints the project's PRODUCT.md (and DESIGN.md when present) as a markdown block, or tells you it's missing. Follow whatever it prints. **If it reports `NO_PRODUCT_MD`, stop and follow `reference/init.md` before doing anything else.** If the output ends with an `UPDATE_AVAILABLE` directive, follow it (ask the user once about updating, then continue). It never blocks the current task. +2. If the user invoked a sub-command (`craft`, `shape`, `audit`, `polish`, ...), you MUST read `reference/.md` next. Non-optional. The reference defines the command's flow; without it you will skip steps the user expects. +3. Familiarize yourself with any existing design system, conventions, and components in the code. Read at least one project file (CSS / tokens / theme / a representative component or page). **Required even when you've loaded a sub-command reference in step 2.** Don't reinvent the wheel; use what's there when it works, branch out when the UX wins. +4. Read the matching register reference. **This is non-optional; skipping it produces generic output.** If the project is marketing, a landing page, a campaign, long-form content, or a portfolio (design IS the product), read `reference/brand.md`. If it is app UI, admin, a dashboard, or a tool (design SERVES the product), read `reference/product.md`. Pick by first match: (1) task cue ("landing page" vs "dashboard"); (2) surface in focus (the page, file, or route being worked on); (3) `register` field in PRODUCT.md. +5. **If the project is brand-new (no existing CSS tokens / theme / committed brand colors found in step 3)**, run `node .kiro/skills/impeccable/scripts/palette.mjs` to receive a brand seed color and composition guidance. This is the anchor for your primary brand color. Compose the rest of the palette (bg, surface, ink, accent, muted) around it per the script's instructions. Use OKLCH throughout. **Skip this step only if step 3 found committed brand colors in existing tokens; in that case identity-preservation wins.** -Skipping these produces generic output that ignores the project. +## Design guidance -### 1. Context gathering +Produce ready-to-ship, production-grade code, not prototypes or starting points. Take no shortcuts unless the user asks for them (when in doubt, ask). Don't stop until arriving at a complete implementation (beautiful, responsive, fast, precise, bug-free, on brand). You take attention to detail seriously: every page, section or component crafted is battle tested using the tools available to you (browser screenshotting, computer use, etc). Claude is capable of extraordinary work. Don't hold back. -Two files, case-insensitive. The loader looks at the project root by default and falls back to `.agents/context/` and `docs/` if the root is clean. Override with `IMPECCABLE_CONTEXT_DIR=path/to/dir` (absolute or relative to cwd). +### General rules -- **PRODUCT.md**: required. Users, brand, tone, anti-references, strategic principles. -- **DESIGN.md**: optional, strongly recommended. Colors, typography, elevation, components. +#### Color -Load both in one call: +- **Verify contrast.** Body text must hit ≥4.5:1 against its background; large text (≥18px or bold ≥14px) needs ≥3:1. Placeholder text needs the same 4.5:1, not the muted-gray default. The most common failure: muted gray body text on a tinted near-white. If the contrast is even close, bump the body color toward the ink end of the ramp; light gray "for elegance" is the single biggest reason AI designs feel hard to read. +- Gray text on a colored background looks washed out. Use a darker shade of the background's own hue, or a transparency of the text color. -```bash -node .kiro/skills/impeccable/scripts/load-context.mjs -``` +#### Typography -Consume the full JSON output. Never pipe through `head`, `tail`, `grep`, or `jq`. The output's `contextDir` field tells you where the files were resolved from. +- Cap body line length at 65–75ch. +- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales. +- Cap font-family count at 3 (display + body + optional mono). More than 3 reads as indecision, not richness. One well-tuned family with weight contrast usually beats three competing typefaces. +- Don't pair fonts that are similar but not identical (two geometric sans-serifs, two humanist sans-serifs). Pair on a contrast axis (serif + sans, geometric + humanist) or use one family in multiple weights. +- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. +- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. +- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". +- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. -If the output is already in this session's conversation history, don't re-run. Exceptions requiring a fresh load: you just ran `/impeccable teach` or `/impeccable document` (they rewrite the files), or the user manually edited one. +#### Layout -`/impeccable live` already warms context via `live.mjs`. If you've run `live.mjs`, don't also run `load-context.mjs` this session. +- Vary spacing for rhythm. +- Cards are the lazy answer. Use them only when they're truly the best affordance. Nested cards are always wrong. +- Flexbox for 1D, Grid for 2D. Don't default to Grid when `flex-wrap` would be simpler. +- For responsive grids without breakpoints: `repeat(auto-fit, minmax(280px, 1fr))`. +- Build a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip). Never arbitrary values like 999 or 9999. -If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `/impeccable teach`, then resume the user's original task with the fresh context. If the original task was `/impeccable craft`, resume into `/impeccable shape` before any implementation work. +#### Motion +- Motion should be intentional, and not be an afterthought. consider it as part of the build. +- Don't animate CSS layout properties unless truly needed. +- Ease out with exponential curves (ease-out-quart / quint / expo). No bounce, no elastic. +- Use libraries for more advanced motion needs (e.g. motion, gsap, anime.js, lenis etc) +- Reduced motion is not optional. Every animation needs a `@media (prefers-reduced-motion: reduce)` alternative: typically a crossfade or instant transition. +- Staggering the items within one list is legitimate. The tell is the uniform reflex (one identical entrance applied to every section), not motion itself; each reveal should fit what it reveals. Suppressing the reflex is never a reason to ship a page with no motion at all. +- Reveal animations must enhance an already-visible default. Don't gate content visibility on a class-triggered transition; transitions pause on hidden tabs and headless renderers, so the reveal never fires and the section ships blank. +- Premium motion materials are not just transform/opacity. Blur, backdrop-filter, clip-path, mask, and shadow/glow are part of the palette when they materially improve the effect and stay smooth. -If DESIGN.md is missing: nudge once per session (*"Run `/impeccable document` for more on-brand output"*), then proceed. +#### Interaction -### 2. Register +- Dropdowns rendered with `position: absolute` inside an `overflow: hidden` or `overflow: auto` container will be clipped. Use the native `` / popover API, `position: fixed`, or a portal to escape the stacking context. -Every design task is **brand** (marketing, landing, campaign, long-form content, portfolio: design IS the product) or **product** (app UI, admin, dashboard, tool: design SERVES the product). +### Copy -Identify before designing. Priority: (1) cue in the task itself ("landing page" vs "dashboard"); (2) the surface in focus (the page, file, or route being worked on); (3) `register` field in PRODUCT.md. First match wins. +- Every word earns its place. No restated headings, no intros that repeat the title. +- **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`. +- **No aphoristic-cadence body copy as a default voice.** Don't fall into the rhythm of "serious statement, then punchy short negation" as the page's recurring voice. If three or more section copy blocks on the page land on a short rebuttal-shaped sentence, rewrite. Specific, not aphoristic. +- **No marketing buzzwords.** The streamline / empower / supercharge / leverage / unleash / transform / seamless / world-class / enterprise-grade / next-generation / cutting-edge / game-changer / mission-critical family of phrases. Pick a specific noun and a verb that describes what the product literally does. +- Button labels: verb + object. "Save changes" beats "OK"; "Delete project" beats "Yes". The label should say what will happen. +- Link text needs standalone meaning. "View pricing plans" beats "Click here"; screen readers announce links out of context. -If PRODUCT.md lacks the `register` field (legacy), infer it once from its "Users" and "Product Purpose" sections, then cache the inferred value for the session. Suggest the user run `/impeccable teach` to add the field explicitly. +### New projects only (when no prior work exists) -Load the matching reference: [reference/brand.md](reference/brand.md) or [reference/product.md](reference/product.md). The shared design laws below apply to both. +#### Color & Theme -## Shared design laws - -Apply to every design, both registers. Match implementation complexity to the aesthetic vision: maximalism needs elaborate code, minimalism needs precision. Interpret creatively. Vary across projects; never converge on the same choices. Claude is capable of extraordinary work. Don't hold back. - -### Color - -- Use OKLCH. Reduce chroma as lightness approaches 0 or 100; high chroma at extremes looks garish. -- Never use `#000` or `#fff`. Tint every neutral toward the brand hue (chroma 0.005–0.01 is enough). +- Use OKLCH. +- **The cream / sand / beige body bg is the saturated AI default of 2026.** The whole warm-neutral band (OKLCH L 0.84-0.97, C < 0.06, hue 40-100) reads as cream/sand/paper/parchment regardless of what you call it. Token names like `--paper`, `--cream`, `--sand`, `--bone`, `--flour`, `--linen`, `--parchment`, `--wheat`, `--biscuit`, `--ivory` are tells in themselves. If the brief is "warm, traditional, family-coastal-Italian" or "magazine-warm" or "editorial-restraint", DO NOT translate that into a near-white warm-tinted bg; that's the AI move. Pick: (a) a saturated brand color as the body (terracotta, oxblood, deep ochre, near-black), (b) a true off-white at chroma 0 (or chroma toward the brand's own hue, not toward warmth-by-default), or (c) a darker mid-tone tinted neutral that's clearly the brand's own. "Warmth" in the brand is carried by accent + typography + imagery, not by body bg. +- Tinted neutrals: add 0.005–0.015 chroma toward the brand's hue. Don't default-tint toward warm or cool "because the brand feels that way"; that's the cross-project monoculture move. +- When picking a theme: Dark vs. light is never a default. Not dark "because tools look cool dark." Not light "to be safe.".Before choosing, write one sentence of physical scene: who uses this, where, under what ambient light, in what mood. If the sentence doesn't force the answer, it's not concrete enough. Add detail until it does. - Pick a **color strategy** before picking colors. Four steps on the commitment axis: - **Restrained**: tinted neutrals + one accent ≤10%. Product default; brand minimalism. - **Committed**: one saturated color carries 30–60% of the surface. Brand default for identity-driven pages. - **Full palette**: 3–4 named roles, each used deliberately. Brand campaigns; product data viz. - **Drenched**: the surface IS the color. Brand heroes, campaign pages. -- The "one accent ≤10%" rule is Restrained only. Committed / Full palette / Drenched exceed it on purpose. Don't collapse every design to Restrained by reflex. - -### Theme - -Dark vs. light is never a default. Not dark "because tools look cool dark." Not light "to be safe." - -Before choosing, write one sentence of physical scene: who uses this, where, under what ambient light, in what mood. If the sentence doesn't force the answer, it's not concrete enough. Add detail until it does. - -"Observability dashboard" does not force an answer. "SRE glancing at incident severity on a 27-inch monitor at 2am in a dim room" does. Run the sentence, not the category. - -### Typography - -- Cap body line length at 65–75ch. -- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales. - -### Layout - -- Vary spacing for rhythm. Same padding everywhere is monotony. -- Cards are the lazy answer. Use them only when they're truly the best affordance. Nested cards are always wrong. -- Don't wrap everything in a container. Most things don't need one. - -### Motion - -- Don't animate CSS layout properties. -- Ease out with exponential curves (ease-out-quart / quint / expo). No bounce, no elastic. ### Absolute bans @@ -98,12 +93,9 @@ Match-and-refuse. If you're about to write any of these, rewrite the element wit - **Glassmorphism as default.** Blurs and glass cards used decoratively. Rare and purposeful, or nothing. - **The hero-metric template.** Big number, small label, supporting stats, gradient accent. SaaS cliché. - **Identical card grids.** Same-sized cards with icon + heading + text, repeated endlessly. -- **Modal as first thought.** Modals are usually laziness. Exhaust inline / progressive alternatives first. - -### Copy - -- Every word earns its place. No restated headings, no intros that repeat the title. -- **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`. +- **Tiny uppercase tracked eyebrow above every section.** The 2023-era kicker (small all-caps text with wide tracking, "ABOUT" "PROCESS" "PRICING" above each heading) is now the saturated AI scaffold; it appears on 55-95% of generations regardless of brief, which is the definition of a tell. One named kicker as a deliberate brand system is voice; an eyebrow on every section is AI grammar. Choose a different cadence. +- **Numbered section markers as default scaffolding (01 / 02 / 03).** Putting `01 · About / 02 · Process / 03 · Pricing` above every section is the eyebrow trope one tier deeper: reach for it because "landing pages do this" and you're scaffolding by reflex. Numbers earn their place when the section actually IS a sequence (a real 3-step process, an ordered flow, a typed timeline) and the order carries information the reader needs. One deliberate numbered sequence on one page is voice; numbered eyebrows on every section across the site is AI grammar. +- **Text that overflows its container.** Long heading words plus large clamp scales plus narrow grids cause headline overflow on tablet/mobile. Test the heading copy at every breakpoint; if it overflows, reduce the clamp max or rewrite the copy. The viewport is part of the design. ### The AI slop test @@ -111,7 +103,7 @@ If someone could look at this interface and say "AI made that" without doubt, it **Category-reflex check.** Run at two altitudes; the second one catches what the first one misses. -- **First-order:** if someone could guess the theme + palette from the category alone ("observability → dark blue", "healthcare → white + teal", "finance → navy + gold", "crypto → neon on black"), it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain. +- **First-order:** if someone could guess the theme + palette from the category alone, it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain. - **Second-order:** if someone could guess the aesthetic family from category-plus-anti-references ("AI workflow tool that's not SaaS-cream → editorial-typographic", "fintech that's not navy-and-gold → terminal-native dark mode"), it's the trap one tier deeper. The first reflex was avoided; the second wasn't. Rework until both answers are not obvious. The brand register's [reflex-reject aesthetic lanes](reference/brand.md) list catches the currently-saturated families. ## Commands @@ -120,7 +112,7 @@ If someone could look at this interface and say "AI made that" without doubt, it |---|---|---|---| | `craft [feature]` | Build | Shape, then build a feature end-to-end | [reference/craft.md](reference/craft.md) | | `shape [feature]` | Build | Plan UX/UI before writing code | [reference/shape.md](reference/shape.md) | -| `teach` | Build | Set up PRODUCT.md and DESIGN.md context | [reference/teach.md](reference/teach.md) | +| `init` | Build | Set up project context: PRODUCT.md, DESIGN.md, live config, next steps | [reference/init.md](reference/init.md) | | `document` | Build | Generate DESIGN.md from existing project code | [reference/document.md](reference/document.md) | | `extract [target]` | Build | Pull reusable tokens and components into design system | [reference/extract.md](reference/extract.md) | | `critique [target]` | Evaluate | UX design review with heuristic scoring | [reference/critique.md](reference/critique.md) | @@ -148,11 +140,14 @@ Plus two management commands: `pin ` and `unpin `, detailed be 1. **No argument**: render the table above as the user-facing command menu, grouped by category. Ask what they'd like to do. 2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. -3. **First word doesn't match**: general design invocation. Apply the setup steps, shared design laws, and the loaded register reference, using the full argument as context. +3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. +4. **No clear command match**: general design invocation. Apply the setup steps, the General rules, and the loaded register reference, using the full argument as context. Setup (context gathering, register) is already loaded by then; sub-commands don't re-invoke `/impeccable`. -If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `teach` as a blocker, finish teach, refresh context, then resume the original command and target. +If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `init` as a blocker, finish init, refresh context, then resume the original command and target. + +`teach` is a deprecated alias for `init`: if the user types it, load [reference/init.md](reference/init.md) and proceed as if they ran `init`. ## Pin / Unpin diff --git a/.kiro/skills/impeccable/reference/adapt.md b/.kiro/skills/impeccable/reference/adapt.md index 9d0673ea2..5af7606cb 100644 --- a/.kiro/skills/impeccable/reference/adapt.md +++ b/.kiro/skills/impeccable/reference/adapt.md @@ -188,3 +188,124 @@ Test thoroughly across contexts: - **Slow connections**: Test on throttled network When the adaptation feels native to each context, hand off to `/impeccable polish` for the final pass. + +--- + +## Reference Material + +The sections below were previously `responsive-design.md` and live inline now so the adapt flow has its deep responsive reference in one place. + +### Responsive Design + +#### Mobile-First: Write It Right + +Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first. + +#### Breakpoints: Content-Driven + +Don't chase device sizes; let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints. + +#### Detect Input Method, Not Just Screen Size + +**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard. Use pointer and hover queries: + +```css +/* Fine pointer (mouse, trackpad) */ +@media (pointer: fine) { + .button { padding: 8px 16px; } +} + +/* Coarse pointer (touch, stylus) */ +@media (pointer: coarse) { + .button { padding: 12px 20px; } /* Larger touch target */ +} + +/* Device supports hover */ +@media (hover: hover) { + .card:hover { transform: translateY(-2px); } +} + +/* Device doesn't support hover (touch) */ +@media (hover: none) { + .card { /* No hover state - use active instead */ } +} +``` + +**Critical**: Don't rely on hover for functionality. Touch users can't hover. + +#### Safe Areas: Handle the Notch + +Modern phones have notches, rounded corners, and home indicators. Use `env()`: + +```css +body { + padding-top: env(safe-area-inset-top); + padding-bottom: env(safe-area-inset-bottom); + padding-left: env(safe-area-inset-left); + padding-right: env(safe-area-inset-right); +} + +/* With fallback */ +.footer { + padding-bottom: max(1rem, env(safe-area-inset-bottom)); +} +``` + +**Enable viewport-fit** in your meta tag: +```html + +``` + +#### Responsive Images: Get It Right + +##### srcset with Width Descriptors + +```html +Hero image +``` + +**How it works**: +- `srcset` lists available images with their actual widths (`w` descriptors) +- `sizes` tells the browser how wide the image will display +- Browser picks the best file based on viewport width AND device pixel ratio + +##### Picture Element for Art Direction + +When you need different crops/compositions (not just resolutions): + +```html + + + + ... + +``` + +#### Layout Adaptation Patterns + +**Navigation**: Three stages: hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `
/` for content that can collapse on mobile. + +#### Testing: Don't Trust DevTools Alone + +DevTools device emulation is useful for layout but misses: + +- Actual touch interactions +- Real CPU/memory constraints +- Network latency patterns +- Font rendering differences +- Browser chrome/keyboard appearances + +**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators. + +--- + +**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful. diff --git a/.kiro/skills/impeccable/reference/animate.md b/.kiro/skills/impeccable/reference/animate.md index 20aeea659..15d9f9080 100644 --- a/.kiro/skills/impeccable/reference/animate.md +++ b/.kiro/skills/impeccable/reference/animate.md @@ -6,7 +6,7 @@ Add motion that conveys state, gives feedback, and clarifies hierarchy. Cut moti ## Register -Brand: orchestrated page-load sequences, staggered reveals, scroll-driven animation. Motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions. +Brand: motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions. The saturated AI default is fade-and-rise reveals on every scrolled section; that's a tell, not a choreography. Reserve scroll-triggered motion for moments that earn it. Product: 150–250 ms on most transitions. Motion conveys state: feedback, reveal, loading, transitions between views. No page-load choreography; users are in a task and won't wait for it. @@ -49,10 +49,11 @@ Create a purposeful animation plan: Add motion systematically across these categories: ### Entrance Animations -- **Page load choreography**: Stagger element reveals (100-150ms delays), fade + slide combinations - **Hero section**: Dramatic entrance for primary content (scale, parallax, or creative effects) -- **Content reveals**: Scroll-triggered animations using intersection observer - **Modal/drawer entry**: Smooth slide + fade, backdrop fade, focus management +- **List rhythm**: Sibling stagger is legitimate for cards-in-a-grid or list-items-appearing. Whole-section fade-on-scroll is not a list and is not legitimate. Cap total stagger time: 10 items at 50ms each = 500ms total. For more items, reduce per-item delay or cap the staggered count. + + Use CSS custom properties for clean stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"`, `style="--i: 1"`, etc. on each item. ### Micro-interactions - **Button feedback**: @@ -97,11 +98,14 @@ Use appropriate techniques for each animation: ### Timing & Easing -**Durations by purpose:** -- **100-150ms**: Instant feedback (button press, toggle) -- **200-300ms**: State changes (hover, menu open) -- **300-500ms**: Layout changes (accordion, modal) -- **500-800ms**: Entrance animations (page load) +**Duration: the 100/300/500 rule.** Timing matters more than easing for "feels right": + +| Duration | Use Case | Examples | +|----------|----------|----------| +| **100–150ms** | Instant feedback | Button press, toggle, color change | +| **200–300ms** | State changes | Menu open, tooltip, hover state | +| **300–500ms** | Layout changes | Accordion, modal, drawer | +| **500–800ms** | Entrance animations | Page load, hero reveal | **Easing curves (use these, not CSS defaults):** ```css @@ -134,13 +138,35 @@ Use appropriate techniques for each animation: - GSAP for complex sequences ``` +### Motion Materials + +Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties. Match material to effect: + +- **Transform / opacity**: movement, press feedback, simple reveals, list choreography +- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances +- **Clip-path / masks**: wipes, reveals, editorial cropping, product-like transitions +- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state +- **Grid-template-rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly + +The hard rule isn't "transform and opacity only." It's: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify smoothness in-browser on target viewports. + ### Performance -- **Motion materials**: Use transform/opacity for reliable movement, but use blur, filters, masks, shadows, and color shifts when they materially improve the effect - **Layout safety**: Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins) -- **will-change**: Add sparingly for known expensive animations +- **will-change**: Add sparingly for known expensive animations only (e.g. on `:hover` or an `.animating` class), never preemptively across the whole page +- **Scroll triggers**: Use Intersection Observer instead of scroll event listeners; unobserve after the animation fires once - **Bound expensive effects**: Keep blur/filter/shadow areas small or isolated, use `contain` where appropriate - **Monitor FPS**: Ensure 60fps on target devices +### Perceived Performance + +Nobody cares how fast your site *is*, only how fast it feels. The 80ms threshold: anything under ~80ms feels instant because our brains buffer sensory input for that long to synchronize perception. Target this for micro-interactions. + +- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening. +- **Early completion**: Show content progressively, don't wait for everything (progressive images, streaming HTML, skeleton fade-ins). +- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Use for low-stakes actions (likes, follows). Avoid for payments or destructive operations. +- **Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances. +- **Caution**: Too-fast responses can decrease perceived value for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening. + ### Accessibility ```css @media (prefers-reduced-motion: reduce) { diff --git a/.kiro/skills/impeccable/reference/bolder.md b/.kiro/skills/impeccable/reference/bolder.md index ff0ef2f14..30d4e852c 100644 --- a/.kiro/skills/impeccable/reference/bolder.md +++ b/.kiro/skills/impeccable/reference/bolder.md @@ -50,7 +50,7 @@ Create a strategy to increase impact while maintaining coherence: Systematically increase impact across these dimensions: ### Typography Amplification -- **Replace generic fonts**: Swap system fonts for distinctive choices (see the parent skill's typography guidelines and [typography.md](typography.md) for inspiration) +- **Replace generic fonts**: Swap system fonts for distinctive choices (see the parent skill's typography guidelines and the [Reference Material section of typeset.md](typeset.md#reference-material) for inspiration) - **Extreme scale**: Create dramatic size jumps (3x-5x differences, not 1.5x) - **Weight contrast**: Pair 900 weights with 200 weights, not 600 with 400 - **Unexpected choices**: Variable fonts, display fonts for headlines, condensed/extended widths, monospace as intentional accent (not as lazy "dev tool" default) @@ -78,10 +78,10 @@ Systematically increase impact across these dimensions: - **Custom elements**: Illustrative elements, custom icons, decorative details that reinforce brand ### Motion & Animation -- **Entrance choreography**: Staggered, dramatic page load animations with 50-100ms delays -- **Scroll effects**: Parallax, reveal animations, scroll-triggered sequences -- **Micro-interactions**: Satisfying hover effects, click feedback, state changes -- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic, which cheapen the effect) +- **Hero moment**: One signature entrance, once. Not on every visit and not on every section. +- **Micro-interactions**: Satisfying hover effects, click feedback, state changes. +- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic, which cheapen the effect). +- **Bolder ≠ scroll-fade-rise on every section.** That's the saturated AI default, the opposite of bold. ### Composition Boldness - **Hero moments**: Create clear focal points with dramatic treatment diff --git a/.kiro/skills/impeccable/reference/brand.md b/.kiro/skills/impeccable/reference/brand.md index 3d83a1cdc..194514e95 100644 --- a/.kiro/skills/impeccable/reference/brand.md +++ b/.kiro/skills/impeccable/reference/brand.md @@ -43,17 +43,10 @@ The reflex-reject lists apply to **new design choices**. When the existing brand ### Pairing and voice -Distinctive + refined is the goal. The specific shape depends on the brand: - -- **Editorial / long-form / luxury**: display serif + sans body (a magazine shape). -- **Tech / dev tools / fintech**: one committed sans, usually; custom-tight tracking, strong weight contrast inside a single family. -- **Consumer / food / travel**: warmer pairings, often a humanist sans plus a script or display serif. -- **Creative studios / agencies**: rule-breaking welcome. Mono-only, or display-only, or custom-drawn type as voice. +Distinctive + refined is the goal. The specific shape depends on the brand, not on the brand's category. A category ("restaurant", "dev tool", "magazine", "fintech") is not a recipe; treating it as one is the first-order reflex SKILL.md warns against. Two families minimum is the rule *only* when the voice needs it. A single well-chosen family with committed weight/size contrast is stronger than a timid display+body pair. -Vary across projects. If the last brief was a serif-display landing page, this one isn't. - ### Scale Modular scale, fluid `clamp()` for headings, ≥1.25 ratio between steps. Flat scales (1.1× apart) read as uncommitted. @@ -74,8 +67,7 @@ Brand surfaces have permission for Committed, Full palette, and Drenched strateg - Asymmetric compositions are one option. Break the grid intentionally for emphasis. - Fluid spacing with `clamp()` that breathes on larger viewports. Vary for rhythm: generous separations, tight groupings. -- Alternative: a strict, visible grid as the voice (brutalist / Swiss / tech-spec aesthetics). Either asymmetric or rigorously-gridded can be "designed"; the failure mode is splitting the difference into a generic centered stack. -- Don't default to centering everything. Left-aligned with asymmetric layouts feels more designed; a strict grid reads as confident structure. A centered-stack hero with icon-title-subtitle cards reads as template. +- For image-led briefs (hotels, restaurants, magazines, photography), full-bleed hero imagery with overlaid menu and centered headline is a canonical move; let the photograph be the design. - When cards ARE the right affordance, use `grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` for breakpoint-free responsiveness. ## Imagery @@ -93,8 +85,7 @@ Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landin ## Motion -- One well-orchestrated page-load with staggered reveals beats scattered micro-interactions, when the brand invites it. Tech-minimal brands often skip entrance motion entirely; the restraint is the voice. -- For collapsing/expanding sections, transition `grid-template-rows` rather than `height`. +- One well-orchestrated page-load beats scattered micro-interactions, when the brand invites it. Some brands skip entrance motion entirely; the restraint is the voice. ## Brand bans (on top of the shared absolute bans) @@ -111,8 +102,7 @@ Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landin Brand can afford things product can't. Take them. -- Ambitious first-load motion. Reveals, scroll-triggered transitions, typographic choreography. +- Ambitious first-load motion. Reveals and typographic choreography that earn their place; not fade-on-scroll for every section. - Single-purpose viewports. One dominant idea per fold, long scroll, deliberate pacing. -- Typographic risk. Enormous display type, unexpected italic cuts, mixed cases, hand-drawn headlines, a single oversize word as a hero. - Unexpected color strategies. Palette IS voice; a calm brand and a restless brand should not share palette mechanics. - Art direction per section. Different sections can have different visual worlds if the narrative demands it. Consistency of voice beats consistency of treatment. diff --git a/.kiro/skills/impeccable/reference/clarify.md b/.kiro/skills/impeccable/reference/clarify.md index 5f7d420f4..00ee978c3 100644 --- a/.kiro/skills/impeccable/reference/clarify.md +++ b/.kiro/skills/impeccable/reference/clarify.md @@ -172,3 +172,117 @@ Test that copy improvements work: - **Tone**: Is it appropriate for the situation? When the copy reads cleanly, hand off to `/impeccable polish` for the final pass. + +--- + +## Reference Material + +The sections below were previously `ux-writing.md` and live inline now so the clarify flow has its deep UX-writing reference in one place. + +### UX Writing + +#### The Button Label Problem + +**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns: + +| Bad | Good | Why | +|-----|------|-----| +| OK | Save changes | Says what will happen | +| Submit | Create account | Outcome-focused | +| Yes | Delete message | Confirms the action | +| Cancel | Keep editing | Clarifies what "cancel" means | +| Click here | Download PDF | Describes the destination | + +**For destructive actions**, name the destruction: +- "Delete" not "Remove" (delete is permanent, remove implies recoverable) +- "Delete 5 items" not "Delete selected" (show the count) + +#### Error Messages: The Formula + +Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input". + +##### Error Message Templates + +| Situation | Template | +|-----------|----------| +| **Format error** | "[Field] needs to be [format]. Example: [example]" | +| **Missing required** | "Please enter [what's missing]" | +| **Permission denied** | "You don't have access to [thing]. [What to do instead]" | +| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." | +| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" | + +##### Don't Blame the User + +Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date". + +#### Empty States Are Opportunities + +Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items". + +#### Voice vs Tone + +**Voice** is your brand's personality, consistent everywhere. +**Tone** adapts to the moment. + +| Moment | Tone Shift | +|--------|------------| +| Success | Celebratory, brief: "Done! Your changes are live." | +| Error | Empathetic, helpful: "That didn't work. Here's what to try..." | +| Loading | Reassuring: "Saving your work..." | +| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | + +**Never use humor for errors.** Users are already frustrated. Be helpful, not cute. + +#### Writing for Accessibility + +**Link text** must have standalone meaning: "View pricing plans" not "Click here". **Alt text** describes information, not the image: "Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context. + +#### Writing for Translation + +##### Plan for Expansion + +German text is ~30% longer than English. Allocate space: + +| Language | Expansion | +|----------|-----------| +| German | +30% | +| French | +20% | +| Finnish | +30-40% | +| Chinese | -30% (fewer chars, but same width) | + +##### Translation-Friendly Patterns + +Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear. + +#### Consistency: The Terminology Problem + +Pick one term and stick with it: + +| Inconsistent | Consistent | +|--------------|------------| +| Delete / Remove / Trash | Delete | +| Settings / Preferences / Options | Settings | +| Sign in / Log in / Enter | Sign in | +| Create / Add / New | Create | + +Build a terminology glossary and enforce it. Variety creates confusion. + +#### Avoid Redundant Copy + +If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well. + +#### Loading States + +Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress. + +#### Confirmation Dialogs: Use Sparingly + +Most confirmation dialogs are design failures; consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No"). + +#### Form Instructions + +Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking. + +--- + +**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors. diff --git a/.kiro/skills/impeccable/reference/codex.md b/.kiro/skills/impeccable/reference/codex.md index f40ec746f..d563af95b 100644 --- a/.kiro/skills/impeccable/reference/codex.md +++ b/.kiro/skills/impeccable/reference/codex.md @@ -23,8 +23,8 @@ Before generating anything, run a brief direction conversation grounded in the s Ask **2-3 targeted questions** about visual lane, color strategy, atmosphere, and named anchor references. Don't enumerate generic menus; tie each question to the shape brief's answers. Example shape-grounded questions: -- "Brief says 'editorial restraint, Klim-adjacent.' Are we closer to a quiet specimen page or a magazine-spread feel with hero imagery?" -- "Palette strategy from shape was 'Committed.' Want it warm-grounded (deep oxblood + cream) or cool-grounded (slate + paper white)?" +- "Brief says 'specimen-page restraint.' Are we closer to a quiet typographic page or a wider editorial spread with hero imagery?" +- "Palette strategy from shape was 'Committed.' Which one color carries the surface (a brand-driven pick rather than a default warm-or-cool framing)? (And no, the answer isn't a cream/sand body bg; that's the saturated AI default.)" **STOP and wait for answers.** These pin the palette before any pixel gets generated. Do not proceed to Step B until the user has responded. diff --git a/.kiro/skills/impeccable/reference/cognitive-load.md b/.kiro/skills/impeccable/reference/cognitive-load.md deleted file mode 100644 index 48f8ad58b..000000000 --- a/.kiro/skills/impeccable/reference/cognitive-load.md +++ /dev/null @@ -1,106 +0,0 @@ -# Cognitive Load Assessment - -Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload. - ---- - -## Three Types of Cognitive Load - -### Intrinsic Load: The Task Itself -Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it. - -**Manage it by**: -- Breaking complex tasks into discrete steps -- Providing scaffolding (templates, defaults, examples) -- Progressive disclosure: show what's needed now, hide the rest -- Grouping related decisions together - -### Extraneous Load: Bad Design -Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste. - -**Common sources**: -- Confusing navigation that requires mental mapping -- Unclear labels that force users to guess meaning -- Visual clutter competing for attention -- Inconsistent patterns that prevent learning -- Unnecessary steps between user intent and result - -### Germane Load: Learning Effort -Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery. - -**Support it by**: -- Progressive disclosure that reveals complexity gradually -- Consistent patterns that reward learning -- Feedback that confirms correct understanding -- Onboarding that teaches through action, not walls of text - ---- - -## Cognitive Load Checklist - -Evaluate the interface against these 8 items: - -- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements? -- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)? -- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)? -- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen? -- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next? -- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)? -- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one? -- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it? - -**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed). - ---- - -## The Working Memory Rule - -**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001). - -At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider: -- **≤4 items**: Within working memory limits, manageable -- **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure -- **8+ items**: Overloaded; users will skip, misclick, or abandon - -**Practical applications**: -- Navigation menus: ≤5 top-level items (group the rest under clear categories) -- Form sections: ≤4 fields visible per group before a visual break -- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu -- Dashboard widgets: ≤4 key metrics visible without scrolling -- Pricing tiers: ≤3 options (more causes analysis paralysis) - ---- - -## Common Cognitive Load Violations - -### 1. The Wall of Options -**Problem**: Presenting 10+ choices at once with no hierarchy. -**Fix**: Group into categories, highlight recommended, use progressive disclosure. - -### 2. The Memory Bridge -**Problem**: User must remember info from step 1 to complete step 3. -**Fix**: Keep relevant context visible, or repeat it where it's needed. - -### 3. The Hidden Navigation -**Problem**: User must build a mental map of where things are. -**Fix**: Always show current location (breadcrumbs, active states, progress indicators). - -### 4. The Jargon Barrier -**Problem**: Technical or domain language forces translation effort. -**Fix**: Use plain language. If domain terms are unavoidable, define them inline. - -### 5. The Visual Noise Floor -**Problem**: Every element has the same visual weight; nothing stands out. -**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted. - -### 6. The Inconsistent Pattern -**Problem**: Similar actions work differently in different places. -**Fix**: Standardize interaction patterns. Same type of action = same type of UI. - -### 7. The Multi-Task Demand -**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating). -**Fix**: Sequence the steps. Let the user do one thing at a time. - -### 8. The Context Switch -**Problem**: User must jump between screens/tabs/modals to gather info for a single decision. -**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth. diff --git a/.kiro/skills/impeccable/reference/color-and-contrast.md b/.kiro/skills/impeccable/reference/color-and-contrast.md deleted file mode 100644 index 110c2ee47..000000000 --- a/.kiro/skills/impeccable/reference/color-and-contrast.md +++ /dev/null @@ -1,105 +0,0 @@ -# Color & Contrast - -## Color Spaces: Use OKLCH - -**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal, unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark. - -The OKLCH function takes three components: `oklch(lightness chroma hue)` where lightness is 0-100%, chroma is roughly 0-0.4, and hue is 0-360. To build a primary color and its lighter / darker variants, hold the chroma+hue roughly constant and vary the lightness, but **reduce chroma as you approach white or black**, because high chroma at extreme lightness looks garish. - -The hue you pick is a brand decision and should not come from a default. Do not reach for blue (hue 250) or warm orange (hue 60) by reflex; those are the dominant AI-design defaults, not the right answer for any specific brand. - -## Building Functional Palettes - -### Tinted Neutrals - -**Pure gray is dead.** A neutral with zero chroma feels lifeless next to a colored brand. Add a tiny chroma value (0.005-0.015) to all your neutrals, hued toward whatever your brand color is. The chroma is small enough not to read as "tinted" consciously, but it creates subconscious cohesion between brand color and UI surfaces. - -The hue you tint toward should come from THIS project's brand, not from a "warm = friendly, cool = tech" formula. If your brand color is teal, your neutrals lean toward teal. If your brand color is amber, they lean toward amber. The point is cohesion with the SPECIFIC brand, not a stock palette. - -**Avoid** the trap of always tinting toward warm orange or always tinting toward cool blue. Those are the two laziest defaults and they create their own monoculture across projects. - -### Palette Structure - -A complete system needs: - -| Role | Purpose | Example | -|------|---------|---------| -| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades | -| **Neutral** | Text, backgrounds, borders | 9-11 shade scale | -| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each | -| **Surface** | Cards, modals, overlays | 2-3 elevation levels | - -**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise. - -### The 60-30-10 Rule (Applied Correctly) - -This rule is about **visual weight**, not pixel count: - -- **60%**: Neutral backgrounds, white space, base surfaces -- **30%**: Secondary colors: text, borders, inactive states -- **10%**: Accent: CTAs, highlights, focus states - -The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power. - -## Contrast & Accessibility - -### WCAG Requirements - -| Content Type | AA Minimum | AAA Target | -|--------------|------------|------------| -| Body text | 4.5:1 | 7:1 | -| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | -| UI components, icons | 3:1 | 4.5:1 | -| Non-essential decorations | None | None | - -**The gotcha**: Placeholder text still needs 4.5:1. That light gray placeholder you see everywhere? Usually fails WCAG. - -### Dangerous Color Combinations - -These commonly fail contrast or cause readability issues: - -- Light gray text on white (the #1 accessibility fail) -- **Gray text on any colored background**: gray looks washed out and dead on color. Use a darker shade of the background color, or transparency -- Red text on green background (or vice versa): 8% of men can't distinguish these -- Blue text on red background (vibrates visually) -- Yellow text on white (almost always fails) -- Thin light text on images (unpredictable contrast) - -### Never Use Pure Gray or Pure Black - -Pure gray (`oklch(50% 0 0)`) and pure black (`#000`) don't exist in nature; real shadows and surfaces always have a color cast. Even a chroma of 0.005-0.01 is enough to feel natural without being obviously tinted. (See tinted neutrals example above.) - -### Testing - -Don't trust your eyes. Use tools: - -- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) -- Browser DevTools → Rendering → Emulate vision deficiencies -- [Polypane](https://polypane.app/) for real-time testing - -## Theming: Light & Dark Mode - -### Dark Mode Is Not Inverted Light Mode - -You can't just swap colors. Dark mode requires different design decisions: - -| Light Mode | Dark Mode | -|------------|-----------| -| Shadows for depth | Lighter surfaces for depth (no shadows) | -| Dark text on light | Light text on dark (reduce font weight) | -| Vibrant accents | Desaturate accents slightly | -| White backgrounds | Never pure black; use dark gray (oklch 12-18%) | - -In dark mode, depth comes from surface lightness, not shadow. Build a 3-step surface scale where higher elevations are lighter (e.g. 15% / 20% / 25% lightness). Use the SAME hue and chroma as your brand color (whatever it is for THIS project; do not reach for blue) and only vary the lightness. Reduce body text weight slightly (e.g. 350 instead of 400) because light text on dark reads as heavier than dark text on light. - -### Token Hierarchy - -Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer; primitives stay the same. - -## Alpha Is A Design Smell - -Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed. - ---- - -**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Using pure black (#000) for large areas. Skipping color blindness testing (8% of men affected). diff --git a/.kiro/skills/impeccable/reference/colorize.md b/.kiro/skills/impeccable/reference/colorize.md index 9e96eef59..2457f608c 100644 --- a/.kiro/skills/impeccable/reference/colorize.md +++ b/.kiro/skills/impeccable/reference/colorize.md @@ -68,10 +68,10 @@ Add color systematically across these dimensions: - **Hover states**: Introduce color on interaction ### Background & Surfaces -- **Tinted backgrounds**: Replace pure gray (`#f5f5f5`) with warm neutrals (`oklch(97% 0.01 60)`) or cool tints (`oklch(97% 0.01 250)`) +- **Tinted backgrounds**: If you replace pure gray, tint toward the brand hue, not toward a generic-warm-or-cool pair. The default-warm-tint (`oklch(97% 0.01 60)` and its neighbors) is now the AI cream/sand giveaway. Be specific to the brand or stay neutral. - **Colored sections**: Use subtle background colors to separate areas - **Gradient backgrounds**: Add depth with subtle, intentional gradients (not generic purple-blue) -- **Cards & surfaces**: Tint cards or surfaces slightly for warmth +- **Cards & surfaces**: Tint cards or surfaces toward the brand, not "for warmth" by reflex **Use OKLCH for color**: It's perceptually uniform, meaning equal steps in lightness *look* equal. Great for generating harmonious scales. @@ -124,8 +124,6 @@ Ensure color addition improves rather than overwhelms: - Use every color in the rainbow (choose 2-4 colors beyond neutrals) - Apply color randomly without semantic meaning - Put gray text on colored backgrounds. It looks washed out; use a darker shade of the background color or transparency instead -- Use pure gray for neutrals. Add subtle color tint (warm or cool) for depth -- Use pure black (`#000`) or pure white (`#fff`) for large areas - Violate WCAG contrast requirements - Use color as the only indicator (accessibility issue) - Make everything colorful (defeats the purpose) @@ -152,3 +150,108 @@ When invoked from live mode, each variant MUST declare a `color-amount` param so ``` Layer 1-2 variant-specific params on top: palette selection (`steps` with named options), temperature warmth, or tint vs. true color. See `reference/live.md` for the full params contract. + +--- + +## Reference Material + +The sections below were previously `color-and-contrast.md` and live inline now so the colorize flow has its deep color reference in one place. + +### Color & Contrast + +#### Color Spaces: Use OKLCH + +**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal, unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark. + +The OKLCH function takes three components: `oklch(lightness chroma hue)` where lightness is 0-100%, chroma is roughly 0-0.4, and hue is 0-360. To build a primary color and its lighter / darker variants, hold the chroma+hue roughly constant and vary the lightness, but **reduce chroma as you approach white or black**, because high chroma at extreme lightness looks garish. + +The hue you pick is a brand decision and should not come from a default. Do not reach for blue (hue 250) or warm orange (hue 60) by reflex; those are the dominant AI-design defaults, not the right answer for any specific brand. + +#### Building Functional Palettes + +##### Tinted Neutrals + +**Pure gray is dead.** A neutral with zero chroma feels lifeless next to a colored brand. Add a tiny chroma value (0.005-0.015) to all your neutrals, hued toward whatever your brand color is. The chroma is small enough not to read as "tinted" consciously, but it creates subconscious cohesion between brand color and UI surfaces. + +The hue you tint toward should come from THIS project's brand, not from a "warm = friendly, cool = tech" formula. If your brand color is teal, your neutrals lean toward teal. If your brand color is amber, they lean toward amber. The point is cohesion with the SPECIFIC brand, not a stock palette. + +**Avoid** the trap of always tinting toward warm orange or always tinting toward cool blue. Those are the two laziest defaults and they create their own monoculture across projects. + +##### Palette Structure + +A complete system needs: + +| Role | Purpose | Example | +|------|---------|---------| +| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades | +| **Neutral** | Text, backgrounds, borders | 9-11 shade scale | +| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each | +| **Surface** | Cards, modals, overlays | 2-3 elevation levels | + +**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise. + +##### The 60-30-10 Rule (Applied Correctly) + +This rule is about **visual weight**, not pixel count: + +- **60%**: Neutral backgrounds, white space, base surfaces +- **30%**: Secondary colors: text, borders, inactive states +- **10%**: Accent: CTAs, highlights, focus states + +The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power. + +#### Contrast & Accessibility + +##### WCAG Requirements + +| Content Type | AA Minimum | AAA Target | +|--------------|------------|------------| +| Body text | 4.5:1 | 7:1 | +| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | +| UI components, icons | 3:1 | 4.5:1 | +| Non-essential decorations | None | None | + +##### Dangerous Color Combinations + +These commonly fail contrast or cause readability issues: + +- Light gray text on white (the #1 accessibility fail) +- Red text on green background (or vice versa): 8% of men can't distinguish these +- Blue text on red background (vibrates visually) +- Yellow text on white (almost always fails) +- Thin light text on images (unpredictable contrast) + +##### Testing + +Don't trust your eyes. Use tools: + +- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) +- Browser DevTools → Rendering → Emulate vision deficiencies +- [Polypane](https://polypane.app/) for real-time testing + +#### Theming: Light & Dark Mode + +##### Dark Mode Is Not Inverted Light Mode + +You can't just swap colors. Dark mode requires different design decisions: + +| Light Mode | Dark Mode | +|------------|-----------| +| Shadows for depth | Lighter surfaces for depth (no shadows) | +| Dark text on light | Light text on dark (reduce font weight) | +| Vibrant accents | Desaturate accents slightly | +| White backgrounds | Either pure black or a deep surface that fits the brand (a brand-tinted near-black at oklch 12-18% works too) | + +In dark mode, depth comes from surface lightness, not shadow. Build a 3-step surface scale where higher elevations are lighter (e.g. 15% / 20% / 25% lightness). Use the SAME hue and chroma as your brand color (whatever it is for THIS project; do not reach for blue) and only vary the lightness. Reduce body text weight slightly (e.g. 350 instead of 400) because light text on dark reads as heavier than dark text on light. + +##### Token Hierarchy + +Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer; primitives stay the same. + +#### Alpha Is A Design Smell + +Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed. + +--- + +**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Skipping color blindness testing (8% of men affected). diff --git a/.kiro/skills/impeccable/reference/craft.md b/.kiro/skills/impeccable/reference/craft.md index 06094b1dd..2c7bd99b1 100644 --- a/.kiro/skills/impeccable/reference/craft.md +++ b/.kiro/skills/impeccable/reference/craft.md @@ -56,15 +56,15 @@ If the harness has native image generation (Codex), a compact shape's "confirm o Based on the design brief's "Recommended References" section, consult the relevant impeccable reference files. At minimum, always consult: -- [spatial-design.md](spatial-design.md) for layout and spacing -- [typography.md](typography.md) for type hierarchy +- [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 [motion-design.md](motion-design.md) -- Color-heavy or themed? Consult [color-and-contrast.md](color-and-contrast.md) -- Responsive requirements? Consult [responsive-design.md](responsive-design.md) -- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.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) diff --git a/.kiro/skills/impeccable/reference/critique.md b/.kiro/skills/impeccable/reference/critique.md index 569d8abde..0655e2483 100644 --- a/.kiro/skills/impeccable/reference/critique.md +++ b/.kiro/skills/impeccable/reference/critique.md @@ -38,9 +38,9 @@ Read relevant source files and visually inspect the live page when browser autom Evaluate: - **AI slop**: Would someone believe "AI made this" immediately? Check all DON'T guidance from the parent Impeccable skill. - **Holistic design**: hierarchy, IA, emotional fit, discoverability, composition, typography, color, accessibility, states, copy, and edge cases. -- **Cognitive load**: consult [cognitive-load](cognitive-load.md); report checklist failures and decision points with >4 visible options. +- **Cognitive load**: consult the [Cognitive Load Assessment](#cognitive-load-assessment) section below; report checklist failures and decision points with >4 visible options. - **Emotional journey**: peak-end rule, emotional valleys, reassurance at high-stakes moments. -- **Nielsen heuristics**: consult [heuristics-scoring](heuristics-scoring.md); score all 10 heuristics 0-4. +- **Nielsen heuristics**: consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below; score all 10 heuristics 0-4. Return: AI slop verdict, heuristic scores, cognitive load, emotional journey, 2-3 strengths, 3-5 priority issues, persona red flags, minor observations, and provocative questions. @@ -80,7 +80,7 @@ The chat response is the primary user-facing deliverable. Present the full struc Structure your feedback as a design director would: #### Design Health Score -> *Consult [heuristics-scoring](heuristics-scoring.md)* +> *Consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below.* Present the Nielsen's 10 heuristics scores as a table: @@ -119,16 +119,16 @@ Highlight 2-3 things done well. Be specific about why they work. #### Priority Issues The 3-5 most impactful design problems, ordered by importance. -For each issue, tag with **P0-P3 severity** (consult [heuristics-scoring](heuristics-scoring.md) for severity definitions): +For each issue, tag with **P0-P3 severity** (see [Issue Severity below](#issue-severity-p0p3) for definitions): - **[P?] What**: Name the problem clearly - **Why it matters**: How this hurts users or undermines goals - **Fix**: What to do about it (be concrete) - **Suggested command**: Which command could address this (from: /impeccable adapt, /impeccable animate, /impeccable audit, /impeccable bolder, /impeccable clarify, /impeccable colorize, /impeccable critique, /impeccable delight, /impeccable distill, /impeccable document, /impeccable harden, /impeccable layout, /impeccable onboard, /impeccable optimize, /impeccable overdrive, /impeccable polish, /impeccable quieter, /impeccable shape, /impeccable typeset) #### Persona Red Flags -> *Consult [personas](personas.md)* +> *Consult the [Personas reference](#persona-based-design-testing) below.* -Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `.kiro/settings.json` contains a `## Design Context` section from `impeccable teach`, also generate 1-2 project-specific personas from the audience/brand info. +Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `.kiro/settings.json` contains a `## Design Context` section from `impeccable init`, also generate 1-2 project-specific personas from the audience/brand info. For each selected persona, walk through the primary user action and list specific red flags found: @@ -234,3 +234,534 @@ After presenting the summary, tell the user: > You can ask me to run these one at a time, all at once, or in any order you prefer. > > Re-run `/impeccable critique` after fixes to see your score improve. + +--- + +## Reference Material + +The sections below were previously separate reference files (`cognitive-load.md`, `heuristics-scoring.md`, `personas.md`). They live inline now so the critique flow has all its deep context in one place. + +### Cognitive Load Assessment + +Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload. + +--- + +#### Three Types of Cognitive Load + +##### Intrinsic Load: The Task Itself +Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it. + +**Manage it by**: +- Breaking complex tasks into discrete steps +- Providing scaffolding (templates, defaults, examples) +- Progressive disclosure: show what's needed now, hide the rest +- Grouping related decisions together + +##### Extraneous Load: Bad Design +Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste. + +**Common sources**: +- Confusing navigation that requires mental mapping +- Unclear labels that force users to guess meaning +- Visual clutter competing for attention +- Inconsistent patterns that prevent learning +- Unnecessary steps between user intent and result + +##### Germane Load: Learning Effort +Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery. + +**Support it by**: +- Progressive disclosure that reveals complexity gradually +- Consistent patterns that reward learning +- Feedback that confirms correct understanding +- Onboarding that teaches through action, not walls of text + +--- + +#### Cognitive Load Checklist + +Evaluate the interface against these 8 items: + +- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements? +- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)? +- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)? +- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen? +- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next? +- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)? +- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one? +- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it? + +**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed). + +--- + +#### The Working Memory Rule + +**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001). + +At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider: +- **≤4 items**: Within working memory limits, manageable +- **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure +- **8+ items**: Overloaded; users will skip, misclick, or abandon + +**Practical applications**: +- Navigation menus: ≤5 top-level items (group the rest under clear categories) +- Form sections: ≤4 fields visible per group before a visual break +- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu +- Dashboard widgets: ≤4 key metrics visible without scrolling +- Pricing tiers: ≤3 options (more causes analysis paralysis) + +--- + +#### Common Cognitive Load Violations + +##### 1. The Wall of Options +**Problem**: Presenting 10+ choices at once with no hierarchy. +**Fix**: Group into categories, highlight recommended, use progressive disclosure. + +##### 2. The Memory Bridge +**Problem**: User must remember info from step 1 to complete step 3. +**Fix**: Keep relevant context visible, or repeat it where it's needed. + +##### 3. The Hidden Navigation +**Problem**: User must build a mental map of where things are. +**Fix**: Always show current location (breadcrumbs, active states, progress indicators). + +##### 4. The Jargon Barrier +**Problem**: Technical or domain language forces translation effort. +**Fix**: Use plain language. If domain terms are unavoidable, define them inline. + +##### 5. The Visual Noise Floor +**Problem**: Every element has the same visual weight; nothing stands out. +**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted. + +##### 6. The Inconsistent Pattern +**Problem**: Similar actions work differently in different places. +**Fix**: Standardize interaction patterns. Same type of action = same type of UI. + +##### 7. The Multi-Task Demand +**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating). +**Fix**: Sequence the steps. Let the user do one thing at a time. + +##### 8. The Context Switch +**Problem**: User must jump between screens/tabs/modals to gather info for a single decision. +**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth. + +--- + +### Heuristics Scoring Guide + +Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough." + +#### Nielsen's 10 Heuristics + +##### 1. Visibility of System Status + +Keep users informed about what's happening through timely, appropriate feedback. + +**Check for**: +- Loading indicators during async operations +- Confirmation of user actions (save, submit, delete) +- Progress indicators for multi-step processes +- Current location in navigation (breadcrumbs, active states) +- Form validation feedback (inline, not just on submit) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | No feedback; user is guessing what happened | +| 1 | Rare feedback; most actions produce no visible response | +| 2 | Partial; some states communicated, major gaps remain | +| 3 | Good; most operations give clear feedback, minor gaps | +| 4 | Excellent; every action confirms, progress is always visible | + +##### 2. Match Between System and Real World + +Speak the user's language. Follow real-world conventions. Information appears in natural, logical order. + +**Check for**: +- Familiar terminology (no unexplained jargon) +- Logical information order matching user expectations +- Recognizable icons and metaphors +- Domain-appropriate language for the target audience +- Natural reading flow (left-to-right, top-to-bottom priority) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Pure tech jargon, alien to users | +| 1 | Mostly confusing; requires domain expertise to navigate | +| 2 | Mixed; some plain language, some jargon leaks through | +| 3 | Mostly natural; occasional term needs context | +| 4 | Speaks the user's language fluently throughout | + +##### 3. User Control and Freedom + +Users need a clear "emergency exit" from unwanted states without extended dialogue. + +**Check for**: +- Undo/redo functionality +- Cancel buttons on forms and modals +- Clear navigation back to safety (home, previous) +- Easy way to clear filters, search, selections +- Escape from long or multi-step processes + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Users get trapped; no way out without refreshing | +| 1 | Difficult exits; must find obscure paths to escape | +| 2 | Some exits; main flows have escape, edge cases don't | +| 3 | Good control; users can exit and undo most actions | +| 4 | Full control; undo, cancel, back, and escape everywhere | + +##### 4. Consistency and Standards + +Users shouldn't wonder whether different words, situations, or actions mean the same thing. + +**Check for**: +- Consistent terminology throughout the interface +- Same actions produce same results everywhere +- Platform conventions followed (standard UI patterns) +- Visual consistency (colors, typography, spacing, components) +- Consistent interaction patterns (same gesture = same behavior) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Inconsistent everywhere; feels like different products stitched together | +| 1 | Many inconsistencies; similar things look/behave differently | +| 2 | Partially consistent; main flows match, details diverge | +| 3 | Mostly consistent; occasional deviation, nothing confusing | +| 4 | Fully consistent; cohesive system, predictable behavior | + +##### 5. Error Prevention + +Better than good error messages is a design that prevents problems in the first place. + +**Check for**: +- Confirmation before destructive actions (delete, overwrite) +- Constraints preventing invalid input (date pickers, dropdowns) +- Smart defaults that reduce errors +- Clear labels that prevent misunderstanding +- Autosave and draft recovery + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Errors easy to make; no guardrails anywhere | +| 1 | Few safeguards; some inputs validated, most aren't | +| 2 | Partial prevention; common errors caught, edge cases slip | +| 3 | Good prevention; most error paths blocked proactively | +| 4 | Excellent; errors nearly impossible through smart constraints | + +##### 6. Recognition Rather Than Recall + +Minimize memory load. Make objects, actions, and options visible or easily retrievable. + +**Check for**: +- Visible options (not buried in hidden menus) +- Contextual help when needed (tooltips, inline hints) +- Recent items and history +- Autocomplete and suggestions +- Labels on icons (not icon-only navigation) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Heavy memorization; users must remember paths and commands | +| 1 | Mostly recall; many hidden features, few visible cues | +| 2 | Some aids; main actions visible, secondary features hidden | +| 3 | Good recognition; most things discoverable, few memory demands | +| 4 | Everything discoverable; users never need to memorize | + +##### 7. Flexibility and Efficiency of Use + +Accelerators, invisible to novices, speed up expert interaction. + +**Check for**: +- Keyboard shortcuts for common actions +- Customizable interface elements +- Recent items and favorites +- Bulk/batch actions +- Power user features that don't complicate the basics + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | One rigid path; no shortcuts or alternatives | +| 1 | Limited flexibility; few alternatives to the main path | +| 2 | Some shortcuts; basic keyboard support, limited bulk actions | +| 3 | Good accelerators; keyboard nav, some customization | +| 4 | Highly flexible; multiple paths, power features, customizable | + +##### 8. Aesthetic and Minimalist Design + +Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose. + +**Check for**: +- Only necessary information visible at each step +- Clear visual hierarchy directing attention +- Purposeful use of color and emphasis +- No decorative clutter competing for attention +- Focused, uncluttered layouts + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Overwhelming; everything competes for attention equally | +| 1 | Cluttered; too much noise, hard to find what matters | +| 2 | Some clutter; main content clear, periphery noisy | +| 3 | Mostly clean; focused design, minor visual noise | +| 4 | Perfectly minimal; every element earns its pixel | + +##### 9. Help Users Recognize, Diagnose, and Recover from Errors + +Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution. + +**Check for**: +- Plain language error messages (no error codes for users) +- Specific problem identification ("Email is missing @" not "Invalid input") +- Actionable recovery suggestions +- Errors displayed near the source of the problem +- Non-blocking error handling (don't wipe the form) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Cryptic errors; codes, jargon, or no message at all | +| 1 | Vague errors; "Something went wrong" with no guidance | +| 2 | Clear but unhelpful; names the problem but not the fix | +| 3 | Clear with suggestions; identifies problem and offers next steps | +| 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work | + +##### 10. Help and Documentation + +Even if the system is usable without docs, help should be easy to find, task-focused, and concise. + +**Check for**: +- Searchable help or documentation +- Contextual help (tooltips, inline hints, guided tours) +- Task-focused organization (not feature-organized) +- Concise, scannable content +- Easy access without leaving current context + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | No help available anywhere | +| 1 | Help exists but hard to find or irrelevant | +| 2 | Basic help; FAQ or docs exist, not contextual | +| 3 | Good documentation; searchable, mostly task-focused | +| 4 | Excellent contextual help; right info at the right moment | + +--- + +#### Score Summary + +**Total possible**: 40 points (10 heuristics × 4 max) + +| Score Range | Rating | What It Means | +|-------------|--------|---------------| +| 36–40 | Excellent | Minor polish only; ship it | +| 28–35 | Good | Address weak areas, solid foundation | +| 20–27 | Acceptable | Significant improvements needed before users are happy | +| 12–19 | Poor | Major UX overhaul required; core experience broken | +| 0–11 | Critical | Redesign needed; unusable in current state | + +--- + +#### Issue Severity (P0–P3) + +Tag each individual issue found during scoring with a priority level: + +| Priority | Name | Description | Action | +|----------|------|-------------|--------| +| **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper | +| **P1** | Major | Causes significant difficulty or confusion | Fix before release | +| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass | +| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits | + +**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1. + +--- + +### Persona-Based Design Testing + +Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss. + +**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns. + +--- + +#### 1. Impatient Power User: "Alex" + +**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave. + +**Behaviors**: +- Skips all onboarding and instructions +- Looks for keyboard shortcuts immediately +- Tries to bulk-select, batch-edit, and automate +- Gets frustrated by required steps that feel unnecessary +- Abandons if anything feels slow or patronizing + +**Test Questions**: +- Can Alex complete the core task in under 60 seconds? +- Are there keyboard shortcuts for common actions? +- Can onboarding be skipped entirely? +- Do modals have keyboard dismiss (Esc)? +- Is there a "power user" path (shortcuts, bulk actions)? + +**Red Flags** (report these specifically): +- Forced tutorials or unskippable onboarding +- No keyboard navigation for primary actions +- Slow animations that can't be skipped +- One-item-at-a-time workflows where batch would be natural +- Redundant confirmation steps for low-risk actions + +--- + +#### 2. Confused First-Timer: "Jordan" + +**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out. + +**Behaviors**: +- Reads all instructions carefully +- Hesitates before clicking anything unfamiliar +- Looks for help or support constantly +- Misunderstands jargon and abbreviations +- Takes the most literal interpretation of any label + +**Test Questions**: +- Is the first action obviously clear within 5 seconds? +- Are all icons labeled with text? +- Is there contextual help at decision points? +- Does terminology assume prior knowledge? +- Is there a clear "back" or "undo" at every step? + +**Red Flags** (report these specifically): +- Icon-only navigation with no labels +- Technical jargon without explanation +- No visible help option or guidance +- Ambiguous next steps after completing an action +- No confirmation that an action succeeded + +--- + +#### 3. Accessibility-Dependent User: "Sam" + +**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences. + +**Behaviors**: +- Tabs through the interface linearly +- Relies on ARIA labels and heading structure +- Cannot see hover states or visual-only indicators +- Needs adequate color contrast (4.5:1 minimum) +- May use browser zoom up to 200% + +**Test Questions**: +- Can the entire primary flow be completed keyboard-only? +- Are all interactive elements focusable with visible focus indicators? +- Do images have meaningful alt text? +- Is color contrast WCAG AA compliant (4.5:1 for text)? +- Does the screen reader announce state changes (loading, success, errors)? + +**Red Flags** (report these specifically): +- Click-only interactions with no keyboard alternative +- Missing or invisible focus indicators +- Meaning conveyed by color alone (red = error, green = success) +- Unlabeled form fields or buttons +- Time-limited actions without extension option +- Custom components that break screen reader flow + +--- + +#### 4. Deliberate Stress Tester: "Riley" + +**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience. + +**Behaviors**: +- Tests edge cases intentionally (empty states, long strings, special characters) +- Submits forms with unexpected data (emoji, RTL text, very long values) +- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs +- Looks for inconsistencies between what the UI promises and what actually happens +- Documents problems methodically + +**Test Questions**: +- What happens at the edges (0 items, 1000 items, very long text)? +- Do error states recover gracefully or leave the UI in a broken state? +- What happens on refresh mid-workflow? Is state preserved? +- Are there features that appear to work but produce broken results? +- How does the UI handle unexpected input (emoji, special chars, paste from Excel)? + +**Red Flags** (report these specifically): +- Features that appear to work but silently fail or produce wrong results +- Error handling that exposes technical details or leaves UI in a broken state +- Empty states that show nothing useful ("No results" with no guidance) +- Workflows that lose user data on refresh or navigation +- Inconsistent behavior between similar interactions in different parts of the UI + +--- + +#### 5. Distracted Mobile User: "Casey" + +**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection. + +**Behaviors**: +- Uses thumb only; prefers bottom-of-screen actions +- Gets interrupted mid-flow and returns later +- Switches between apps frequently +- Has limited attention span and low patience +- Types as little as possible, prefers taps and selections + +**Test Questions**: +- Are primary actions in the thumb zone (bottom half of screen)? +- Is state preserved if the user leaves and returns? +- Does it work on slow connections (3G)? +- Can forms use autocomplete and smart defaults? +- Are touch targets at least 44×44pt? + +**Red Flags** (report these specifically): +- Important actions positioned at the top of the screen (unreachable by thumb) +- No state persistence; progress lost on tab switch or interruption +- Large text inputs required where selection would work +- Heavy assets loading on every page (no lazy loading) +- Tiny tap targets or targets too close together + +--- + +#### Selecting Personas + +Choose personas based on the interface type: + +| Interface Type | Primary Personas | Why | +|---------------|-----------------|-----| +| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile | +| Dashboard / admin | Alex, Sam | Power users, accessibility | +| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity | +| Onboarding flow | Jordan, Casey | Confusion, interruption | +| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav | +| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile | + +--- + +#### Project-Specific Personas + +If `.kiro/settings.json` contains a `## Design Context` section (generated by `impeccable init`), derive 1–2 additional personas from the audience and brand information: + +1. Read the target audience description +2. Identify the primary user archetype not covered by the 5 predefined personas +3. Create a persona following this template: + +``` +##### [Role]: "[Name]" + +**Profile**: [2-3 key characteristics derived from Design Context] + +**Behaviors**: [3-4 specific behaviors based on the described audience] + +**Red Flags**: [3-4 things that would alienate this specific user type] +``` + +Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists. diff --git a/.kiro/skills/impeccable/reference/document.md b/.kiro/skills/impeccable/reference/document.md index f9f04356d..60075688a 100644 --- a/.kiro/skills/impeccable/reference/document.md +++ b/.kiro/skills/impeccable/reference/document.md @@ -45,7 +45,7 @@ Rules that matter: - **Token refs** use `{path.to.token}` (e.g. `{colors.primary}`, `{rounded.md}`). Components may reference primitives; primitives may not reference each other. - **Stitch validates colors as hex sRGB only** (`#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`); OKLCH/HSL/P3 trigger a linter warning, not a hard error. YAML accepts the string either way and our own parser is format-agnostic. Choose based on project posture: (a) if the project has an "OKLCH-only" doctrine or uses Display-P3 values that don't round-trip through sRGB, put OKLCH directly in the frontmatter and accept the Stitch linter warning; (b) if the project wants strict Stitch compliance or plans to use their Tailwind/DTCG export pipeline, put hex in the frontmatter and keep OKLCH in prose as the canonical reference. Never split the source of truth without explicit reason. - **Component sub-tokens** are limited to 8 props: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, `width`. Shadows, motion, focus rings, backdrop-filter: none of those fit. Carry them in the sidecar (Step 4b). -- **Scale keys are open-ended.** Use whatever names the project already uses (`warm-ash-cream`, `surface-container-low`). Don't rename to Material defaults. +- **Scale keys are open-ended.** Use whatever names the project already uses (`oxblood-deep`, `surface-container-low`). Don't rename to Material defaults. - **Variants are naming convention, not schema.** `button-primary` / `button-primary-hover` / `button-primary-active` as sibling keys. ## The markdown body: six sections (exact order) @@ -61,7 +61,7 @@ Optional evocative subtitles are allowed in the form `## 2. Colors: The [Name] P ## When to run -- The user just ran `/impeccable teach` and needs the visual side documented. +- The user just ran `/impeccable init` and needs the visual side documented. - The skill noticed no `DESIGN.md` exists and nudged the user to create one. - An existing `DESIGN.md` is stale (the design has drifted). - Before a large redesign, to capture the current state as a reference. @@ -71,7 +71,7 @@ If a `DESIGN.md` already exists, **do not silently overwrite it**. Show the user ## Two paths - **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 teach, nothing built yet). Interview for five high-level answers, write a minimal DESIGN.md marked ``. Re-run in scan mode once there's code. +- **Seed mode**: the project is pre-implementation (fresh init, nothing built yet). Interview for five high-level answers, 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` forces seed mode regardless of code presence. @@ -103,7 +103,7 @@ Build a structured draft from the discovered tokens. For each token class: From the auto-extracted tokens, draft the YAML frontmatter now (you'll write it at the top of DESIGN.md in Step 4). This is the machine-readable layer: what the live panel and Stitch's linter consume. -- **Colors**: one entry per extracted color. Key = descriptive slug (`warm-ash-cream`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. +- **Colors**: one entry per extracted color. Key = descriptive slug (`oxblood-deep`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. - **Typography**: one entry per role (`display`, `headline`, `title`, `body`, `label`). Typography is an object; include only the props that are real for the project (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`). - **Rounded / Spacing**: whatever scale steps the project actually uses, keyed by whatever scale name the project uses (`sm` / `md` / `lg`, or `surface-sm`, or numeric steps). - **Components**: one entry per variant (`button-primary`, `button-primary-hover`, `button-ghost`). Reference primitives via `{colors.X}`, `{rounded.Y}`. If a variant needs a property Stitch's 8-prop set doesn't cover (shadow, focus ring, backdrop-filter), carry the full snippet in the sidecar instead. @@ -253,7 +253,7 @@ Regenerate the sidecar whenever you regenerate root `DESIGN.md`. If the user onl "extensions": { "colorMeta": { "primary": { "role": "primary", "displayName": "Editorial Magenta", "canonical": "oklch(60% 0.25 350)", "tonalRamp": ["...", "...", "..."] }, - "warm-ash-cream": { "role": "neutral", "displayName": "Warm Ash Cream", "canonical": "oklch(96% 0.005 350)", "tonalRamp": ["...", "...", "..."] } + "cool-paper": { "role": "neutral", "displayName": "Cool Paper", "canonical": "oklch(96% 0.005 230)", "tonalRamp": ["...", "...", "..."] } }, "typographyMeta": { "display": { "displayName": "Display", "purpose": "Hero headlines only." } @@ -328,12 +328,13 @@ Pull directly from the DESIGN.md you just wrote: Do not reword. The panel shows these as secondary collapsible context; the same voice that's in the Markdown carries through. -### Step 5: Confirm, refine, and refresh session cache +### Step 5: Confirm and refine 1. Show the user the full DESIGN.md you wrote. Briefly highlight the non-obvious creative choices (descriptive color names, atmosphere language, named rules). 2. Mention that `.impeccable/design.json` was also written alongside; the live panel will now render this project's actual button/input/nav primitives instead of generic approximations. 3. Offer to refine any section: "Want me to revise a section, add component patterns I missed, or adjust the atmosphere language?" -4. **Refresh the session cache.** Run `node .kiro/skills/impeccable/scripts/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. + +Your own write is the freshest source; subsequent commands in this session don't need a reload. ## Seed mode @@ -394,11 +395,12 @@ Per-section guidance in seed mode: 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. -### Step 4: Confirm and refresh session cache +### Step 4: Confirm 1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment). 2. Tell the user: "Re-run `/impeccable document` once you have some code. That pass will extract real tokens and generate the sidecar." -3. Run `node .kiro/skills/impeccable/scripts/load-context.mjs` once so the seed lands in conversation for the rest of the session. + +Your own write is the freshest source; no reload needed. ## Style guidelines diff --git a/.kiro/skills/impeccable/reference/heuristics-scoring.md b/.kiro/skills/impeccable/reference/heuristics-scoring.md deleted file mode 100644 index edbe5028d..000000000 --- a/.kiro/skills/impeccable/reference/heuristics-scoring.md +++ /dev/null @@ -1,234 +0,0 @@ -# Heuristics Scoring Guide - -Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough." - -## Nielsen's 10 Heuristics - -### 1. Visibility of System Status - -Keep users informed about what's happening through timely, appropriate feedback. - -**Check for**: -- Loading indicators during async operations -- Confirmation of user actions (save, submit, delete) -- Progress indicators for multi-step processes -- Current location in navigation (breadcrumbs, active states) -- Form validation feedback (inline, not just on submit) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | No feedback; user is guessing what happened | -| 1 | Rare feedback; most actions produce no visible response | -| 2 | Partial; some states communicated, major gaps remain | -| 3 | Good; most operations give clear feedback, minor gaps | -| 4 | Excellent; every action confirms, progress is always visible | - -### 2. Match Between System and Real World - -Speak the user's language. Follow real-world conventions. Information appears in natural, logical order. - -**Check for**: -- Familiar terminology (no unexplained jargon) -- Logical information order matching user expectations -- Recognizable icons and metaphors -- Domain-appropriate language for the target audience -- Natural reading flow (left-to-right, top-to-bottom priority) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Pure tech jargon, alien to users | -| 1 | Mostly confusing; requires domain expertise to navigate | -| 2 | Mixed; some plain language, some jargon leaks through | -| 3 | Mostly natural; occasional term needs context | -| 4 | Speaks the user's language fluently throughout | - -### 3. User Control and Freedom - -Users need a clear "emergency exit" from unwanted states without extended dialogue. - -**Check for**: -- Undo/redo functionality -- Cancel buttons on forms and modals -- Clear navigation back to safety (home, previous) -- Easy way to clear filters, search, selections -- Escape from long or multi-step processes - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Users get trapped; no way out without refreshing | -| 1 | Difficult exits; must find obscure paths to escape | -| 2 | Some exits; main flows have escape, edge cases don't | -| 3 | Good control; users can exit and undo most actions | -| 4 | Full control; undo, cancel, back, and escape everywhere | - -### 4. Consistency and Standards - -Users shouldn't wonder whether different words, situations, or actions mean the same thing. - -**Check for**: -- Consistent terminology throughout the interface -- Same actions produce same results everywhere -- Platform conventions followed (standard UI patterns) -- Visual consistency (colors, typography, spacing, components) -- Consistent interaction patterns (same gesture = same behavior) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Inconsistent everywhere; feels like different products stitched together | -| 1 | Many inconsistencies; similar things look/behave differently | -| 2 | Partially consistent; main flows match, details diverge | -| 3 | Mostly consistent; occasional deviation, nothing confusing | -| 4 | Fully consistent; cohesive system, predictable behavior | - -### 5. Error Prevention - -Better than good error messages is a design that prevents problems in the first place. - -**Check for**: -- Confirmation before destructive actions (delete, overwrite) -- Constraints preventing invalid input (date pickers, dropdowns) -- Smart defaults that reduce errors -- Clear labels that prevent misunderstanding -- Autosave and draft recovery - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Errors easy to make; no guardrails anywhere | -| 1 | Few safeguards; some inputs validated, most aren't | -| 2 | Partial prevention; common errors caught, edge cases slip | -| 3 | Good prevention; most error paths blocked proactively | -| 4 | Excellent; errors nearly impossible through smart constraints | - -### 6. Recognition Rather Than Recall - -Minimize memory load. Make objects, actions, and options visible or easily retrievable. - -**Check for**: -- Visible options (not buried in hidden menus) -- Contextual help when needed (tooltips, inline hints) -- Recent items and history -- Autocomplete and suggestions -- Labels on icons (not icon-only navigation) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Heavy memorization; users must remember paths and commands | -| 1 | Mostly recall; many hidden features, few visible cues | -| 2 | Some aids; main actions visible, secondary features hidden | -| 3 | Good recognition; most things discoverable, few memory demands | -| 4 | Everything discoverable; users never need to memorize | - -### 7. Flexibility and Efficiency of Use - -Accelerators, invisible to novices, speed up expert interaction. - -**Check for**: -- Keyboard shortcuts for common actions -- Customizable interface elements -- Recent items and favorites -- Bulk/batch actions -- Power user features that don't complicate the basics - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | One rigid path; no shortcuts or alternatives | -| 1 | Limited flexibility; few alternatives to the main path | -| 2 | Some shortcuts; basic keyboard support, limited bulk actions | -| 3 | Good accelerators; keyboard nav, some customization | -| 4 | Highly flexible; multiple paths, power features, customizable | - -### 8. Aesthetic and Minimalist Design - -Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose. - -**Check for**: -- Only necessary information visible at each step -- Clear visual hierarchy directing attention -- Purposeful use of color and emphasis -- No decorative clutter competing for attention -- Focused, uncluttered layouts - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Overwhelming; everything competes for attention equally | -| 1 | Cluttered; too much noise, hard to find what matters | -| 2 | Some clutter; main content clear, periphery noisy | -| 3 | Mostly clean; focused design, minor visual noise | -| 4 | Perfectly minimal; every element earns its pixel | - -### 9. Help Users Recognize, Diagnose, and Recover from Errors - -Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution. - -**Check for**: -- Plain language error messages (no error codes for users) -- Specific problem identification ("Email is missing @" not "Invalid input") -- Actionable recovery suggestions -- Errors displayed near the source of the problem -- Non-blocking error handling (don't wipe the form) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Cryptic errors; codes, jargon, or no message at all | -| 1 | Vague errors; "Something went wrong" with no guidance | -| 2 | Clear but unhelpful; names the problem but not the fix | -| 3 | Clear with suggestions; identifies problem and offers next steps | -| 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work | - -### 10. Help and Documentation - -Even if the system is usable without docs, help should be easy to find, task-focused, and concise. - -**Check for**: -- Searchable help or documentation -- Contextual help (tooltips, inline hints, guided tours) -- Task-focused organization (not feature-organized) -- Concise, scannable content -- Easy access without leaving current context - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | No help available anywhere | -| 1 | Help exists but hard to find or irrelevant | -| 2 | Basic help; FAQ or docs exist, not contextual | -| 3 | Good documentation; searchable, mostly task-focused | -| 4 | Excellent contextual help; right info at the right moment | - ---- - -## Score Summary - -**Total possible**: 40 points (10 heuristics × 4 max) - -| Score Range | Rating | What It Means | -|-------------|--------|---------------| -| 36–40 | Excellent | Minor polish only; ship it | -| 28–35 | Good | Address weak areas, solid foundation | -| 20–27 | Acceptable | Significant improvements needed before users are happy | -| 12–19 | Poor | Major UX overhaul required; core experience broken | -| 0–11 | Critical | Redesign needed; unusable in current state | - ---- - -## Issue Severity (P0–P3) - -Tag each individual issue found during scoring with a priority level: - -| Priority | Name | Description | Action | -|----------|------|-------------|--------| -| **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper | -| **P1** | Major | Causes significant difficulty or confusion | Fix before release | -| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass | -| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits | - -**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1. diff --git a/.kiro/skills/impeccable/reference/teach.md b/.kiro/skills/impeccable/reference/init.md similarity index 58% rename from .kiro/skills/impeccable/reference/teach.md rename to .kiro/skills/impeccable/reference/init.md index 81949a89e..9f23a5d20 100644 --- a/.kiro/skills/impeccable/reference/teach.md +++ b/.kiro/skills/impeccable/reference/init.md @@ -1,21 +1,16 @@ -# Teach Flow +# Init Flow -Gathers design context for a project and writes two complementary files at the project root: +The setup command for a project. One codebase crawl feeds everything it writes: - **PRODUCT.md** (strategic): root project file for register, target users, product purpose, brand personality, anti-references, strategic design principles. Answers "who/what/why". - **DESIGN.md** (visual): root project file for visual theme, color palette, typography, components, layout. Follows the [Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/). Answers "how it looks". +- **`.impeccable/live/config.json`** (live mode): pre-configured so `/impeccable live` boots straight into variant mode with no first-time detour. -Every other impeccable command reads these files before doing any work. +It closes by pointing the user at the best command to run next. Every other impeccable command reads PRODUCT.md and DESIGN.md before doing any work. ## Step 1: Load current state -Run the shared loader first so you know what already exists: - -```bash -node .kiro/skills/impeccable/scripts/load-context.mjs -``` - -The output tells you whether PRODUCT.md and/or DESIGN.md already exist. If `migrated: true`, legacy `.impeccable.md` was auto-renamed to `PRODUCT.md`. Mention this once to the user. +Check what already exists. PRODUCT.md and DESIGN.md live at the project root, or under `.agents/context/` or `docs/` (case-insensitive). Read whichever are present with your native file tool. Also note whether `.impeccable/live/config.json` already exists (Step 6 leaves it untouched if so). Decision tree: - **Neither file exists (empty project or no context yet)**: do Steps 2-4 (write PRODUCT.md), then decide on DESIGN.md based on whether there's code to analyze. @@ -26,14 +21,14 @@ Decision tree: Never silently overwrite an existing file. Always confirm first. -If teach was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete teach, re-run the loader, then resume the original command with the freshly loaded context. For craft, resume into shape next; teach creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief. +If init was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete init, then resume the original command. Your own writes are the freshest source; no reload needed. For craft, resume into shape next; init creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief. ## Step 2: Explore the codebase -Before asking questions, thoroughly scan the project to discover what you can: +Before asking questions, thoroughly scan the project to discover what you can. This single crawl feeds PRODUCT.md, DESIGN.md, **and** the live-mode framework detection in Step 6, so be thorough once rather than re-scanning later: - **README and docs**: Project purpose, target audience, any stated goals -- **Package.json / config files**: Tech stack, dependencies, existing design libraries +- **Package.json / config files**: Tech stack, dependencies, existing design libraries, **and the framework** (Vite/SPA, Next.js, Nuxt, SvelteKit, Astro, multi-page static) plus the HTML entry the browser actually loads - **Existing components**: Current design patterns, spacing, typography in use - **Brand assets**: Logos, favicons, color values already defined - **Design tokens / CSS variables**: Existing color palettes, font stacks, spacing scales @@ -46,7 +41,7 @@ Also form a **register hypothesis** from what you find: Register is a hypothesis at this point, not a decision; Step 3 confirms it. -Note what you've learned and what remains unclear. This exploration feeds both PRODUCT.md and DESIGN.md. +Note what you've learned and what remains unclear. Also note any rough edges worth a follow-up command (thin hierarchy, flat or gray palette, missing error/empty states, dull copy); Step 7 turns these into concrete recommendations without re-analyzing. ## Step 3: Ask strategic questions (for PRODUCT.md) @@ -84,8 +79,7 @@ If the signal is genuinely split (e.g. a product with a big marketing landing), ### Brand & Personality - How would you describe the brand personality in 3 words? - Reference sites or apps that capture the right feel? What specifically about them? - - For brand, push for real-world references in the right lane (tech-minimal, editorial-magazine, consumer-warm, brutalist-grid, etc.), not generic "modern" adjectives. - - For product, push for category best-tool references (Linear, Figma, Notion, Raycast, Stripe). + - Push for specific named references with the *specific* thing about them that fits this brand, not generic "modern" adjectives or category-bucket lanes. - What should this explicitly NOT look like? Any anti-references? ### Accessibility & Inclusion @@ -141,16 +135,38 @@ If the user agrees, delegate to `/impeccable document` (it auto-detects scan vs If the user prefers to skip, mention they can run `/impeccable document` any time later. -## Step 6: Confirm and wrap up +## Step 6: Configure live mode (when code exists) -Summarize: +If the project has code with HTML entries and a dev server (the same "code exists" condition that puts `/impeccable document` in scan mode), pre-configure live mode now. You already identified the framework and the served HTML entry in Step 2, so this is nearly free, and it spares the user the first-time setup detour when they later run `/impeccable live`. + +**Skip this step for empty / pre-implementation projects** (nothing to inject into yet). Tell the user live mode will configure itself the first time they run it once there's code. + +**If `.impeccable/live/config.json` already exists, leave it untouched** and note that live mode is already configured. + +Otherwise: + +1. Write `.impeccable/live/config.json`. Choose `files` (the HTML entries the browser actually loads), `insertBefore`, and `commentSyntax` from the framework table in [live.md](live.md)'s **First-time setup** section, using the framework you found in Step 2. That table is canonical; do not restate it here. For multi-page static sites, prefer a glob (`["public/**/*.html"]`) over a literal list. +2. Run `node .kiro/skills/impeccable/scripts/detect-csp.mjs`. If it reports a patchable shape (`append-arrays` / `append-string`), use the **consent prompt template** from live.md before editing any source file. On decline, skip the patch. For `middleware` / `meta-tag` shapes, surface the detected files and ask the user to add `http://localhost:8400` to `script-src` and `connect-src` manually. For `null`, there's nothing to do. +3. Set `cspChecked: true` in the config once CSP is handled (patched, declined, manual, or not needed). The schema and per-shape patch details live in live.md's First-time setup; follow it rather than duplicating. + +Writing the config file is harmless and needs no consent; only the CSP **source-file patch** requires a yes. + +## Step 7: Recommend starting points, then wrap up + +Summarize tersely: - Register captured (brand / product) -- What was written (PRODUCT.md, DESIGN.md, or both) +- What was written (PRODUCT.md, DESIGN.md, live config, or a subset) - The 3-5 strategic principles from PRODUCT.md that will guide future work -- If DESIGN.md is pending, remind the user how to generate it later +- If DESIGN.md or live config is pending, one line on how to set it up later -**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node .kiro/skills/impeccable/scripts/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. +Then recommend the **best commands to run next**, drawn from what your Step 2 crawl already surfaced. Do not run a fresh analysis here; surface observations you already have. Tailor to register and to what you saw, offer the 2-4 most relevant (not a menu dump), and give the exact command to type. Group by intent: -If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context. +- **Build something new**: `/impeccable craft ` (shape, then build end-to-end) or `/impeccable shape ` (plan first). Lead with this for empty or early-stage projects. +- **Improve what's there**: name the specific surface. `/impeccable critique ` for a scored UX review; `/impeccable audit ` for a11y / perf / responsive checks; `/impeccable polish ` for a pre-ship pass. When the crawl flagged a specific weakness, point the matching command at it: thin hierarchy or spacing → `layout`, flat or gray palette → `colorize`, missing error / empty states → `harden` or `onboard`, dull or unclear copy → `clarify`. +- **Iterate visually**: `/impeccable live` (configured in Step 6) to pick elements in the browser and generate variants in place. + +The full command menu is one bare `/impeccable` away; keep this list short and pointed. + +If init was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now. Your own writes are the freshest source; no reload needed. Optionally ask the user directly to clarify what you cannot infer. Ask whether they'd like a brief summary of PRODUCT.md appended to .kiro/settings.json for easier agent reference. If yes, append a short **Design Context** pointer section there. diff --git a/.kiro/skills/impeccable/reference/interaction-design.md b/.kiro/skills/impeccable/reference/interaction-design.md index 15aed5b29..c42c9d810 100644 --- a/.kiro/skills/impeccable/reference/interaction-design.md +++ b/.kiro/skills/impeccable/reference/interaction-design.md @@ -150,12 +150,6 @@ For browsers without anchor positioning support, `position: fixed` with manual c Check viewport boundaries before rendering. If the dropdown would overflow the bottom edge, flip it above the trigger. If it would overflow the right edge, align it to the trigger's right side instead. -### Anti-Patterns - -- **`position: absolute` inside `overflow: hidden`** - The dropdown will be clipped. Use `position: fixed` or the top layer instead. -- **Arbitrary z-index values** like `z-index: 9999` - Use a semantic z-index scale: `dropdown (100) -> sticky (200) -> modal-backdrop (300) -> modal (400) -> toast (500) -> tooltip (600)`. -- **Rendering dropdown markup inline** without an escape hatch from the parent's stacking context. Either use `popover` (top layer), a portal, or `position: fixed`. - ## Destructive Actions: Undo > Confirm **Undo is better than confirmation dialogs.** Users click through confirmations mindlessly. Remove from UI immediately, show undo toast, actually delete after toast expires. Use confirmation only for truly irreversible actions (account deletion), high-cost actions, or batch operations. diff --git a/.kiro/skills/impeccable/reference/layout.md b/.kiro/skills/impeccable/reference/layout.md index cad4c62a1..8765978b0 100644 --- a/.kiro/skills/impeccable/reference/layout.md +++ b/.kiro/skills/impeccable/reference/layout.md @@ -1,4 +1,4 @@ -Space is the most underused design tool. Find the layout's actual problem (monotone spacing, weak hierarchy, identical card grids, the centered-stack default) and fix the structure, not the surface. +Space is the most underused design tool. Find the layout's actual problem (monotone spacing, weak hierarchy, identical card grids) and fix the structure, not the surface. --- @@ -27,7 +27,6 @@ Analyze what's weak about the current spatial design: 3. **Grid & structure**: - Is there a clear underlying structure, or does the layout feel random? - Are identical card grids used everywhere? (Icon + heading + text, repeated endlessly) - - Is everything centered? (Left-aligned with asymmetric layouts feels more designed, but not a hard and fast rule) 4. **Rhythm & variety**: - Does the layout have visual rhythm? (Alternating tight/generous spacing) @@ -43,8 +42,6 @@ Analyze what's weak about the current spatial design: ## Plan Layout Improvements -Consult the [spatial design reference](spatial-design.md) for detailed guidance on grids, rhythm, and container queries. - Create a systematic plan: - **Spacing system**: Use a consistent scale (a framework's built-in scale like Tailwind's, rem-based tokens, or a custom system). The specific values matter less than consistency. @@ -57,6 +54,7 @@ Create a systematic plan: ### Establish a Spacing System - Use a consistent spacing scale (framework scales like Tailwind, rem-based tokens, or a custom scale all work). What matters is that values come from a defined set, not arbitrary numbers. +- Prefer a 4pt base scale (4, 8, 12, 16, 24, 32, 48, 64, 96px) over 8pt; 8pt is too coarse and you'll frequently need 12px between 8 and 16. - Name tokens semantically if using custom properties: `--space-xs` through `--space-xl`, not `--spacing-8` - Use `gap` for sibling spacing instead of margins; eliminates margin collapse hacks - Apply `clamp()` for fluid spacing that breathes on larger screens @@ -66,15 +64,22 @@ Create a systematic plan: - **Tight grouping** for related elements (8-12px between siblings) - **Generous separation** between distinct sections (48-96px) - **Varied spacing** within sections (not every row needs the same gap) -- **Asymmetric compositions**: break the predictable centered-content pattern when it makes sense +- **Asymmetric compositions**: a deliberate choice when the content invites it (not a default to chase). ### Choose the Right Layout Tool -- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. Flex is simpler and more appropriate for the majority of layout tasks. +- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. - **Use Grid for 2D layouts**: Page-level structure, dashboards, data-dense interfaces, anything where rows AND columns need coordinated control. -- **Don't default to Grid** when Flexbox with `flex-wrap` would be simpler and more flexible. -- Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. - Use named grid areas (`grid-template-areas`) for complex page layouts; redefine at breakpoints. +- Use **container queries** for components, viewport queries for page layouts. A card in a narrow sidebar can stay compact while the same card in a main content area expands automatically: + +```css +.card-container { container-type: inline-size; } +.card { display: grid; gap: var(--space-md); } +@container (min-width: 400px) { + .card { grid-template-columns: 120px 1fr; } +} +``` ### Break Card Grid Monotony @@ -85,18 +90,36 @@ Create a systematic plan: ### Strengthen Visual Hierarchy - Use the fewest dimensions needed for clear hierarchy. Space alone can be enough; generous whitespace around an element draws the eye. Some of the most polished designs achieve rhythm with just space and weight. Add color or size contrast only when simpler means aren't sufficient. +- The best hierarchy combines 2–3 dimensions at once. A heading that's larger, bolder, AND has more space above it reads as primary without trying: + +| Tool | Strong Hierarchy | Weak Hierarchy | +|------|------------------|----------------| +| **Size** | 3:1 ratio or more | <2:1 ratio | +| **Weight** | Bold vs Regular | Medium vs Regular | +| **Color** | High contrast | Similar tones | +| **Position** | Top/left (primary) | Bottom/right | +| **Space** | Surrounded by white space | Crowded | + - Be aware of reading flow: in LTR languages, the eye naturally scans top-left to bottom-right, but primary action placement depends on context (e.g., bottom-right in dialogs, top in navigation). - Create clear content groupings through proximity and separation. ### Manage Depth & Elevation -- Create a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip) - Build a consistent shadow scale (sm → md → lg → xl); shadows should be subtle - Use elevation to reinforce hierarchy, not as decoration ### Optical Adjustments - If an icon looks visually off-center despite being geometrically centered, nudge it. But only if you're confident it actually looks wrong. Don't adjust speculatively. +- Text at `margin-left: 0` looks slightly indented because of letterform whitespace; a negative margin (`-0.05em`) optically aligns it. Geometrically centered glyphs often look off-center (play icons need to shift right, arrows shift toward their direction). +- Touch targets must be 44×44px minimum even when the visual element is smaller. Expand the hit area with padding or a pseudo-element: + +```css +.icon-button { width: 24px; height: 24px; position: relative; } +.icon-button::before { + content: ''; position: absolute; inset: -10px; +} +``` **NEVER**: - Use arbitrary spacing values outside your scale @@ -104,10 +127,7 @@ Create a systematic plan: - Wrap everything in cards (not everything needs a container) - Nest cards inside cards (use spacing and dividers for hierarchy within) - Use identical card grids everywhere (icon + heading + text, repeated) -- Center everything (left-aligned with asymmetry feels more designed) - Default to the hero metric layout (big number, small label, stats, gradient) as a template. If showing real user data, a prominent metric can work, but it should display actual data, not decorative numbers. -- Default to CSS Grid when Flexbox would be simpler; use the simplest tool for the job -- Use arbitrary z-index values (999, 9999); build a semantic scale ## Verify Layout Improvements diff --git a/.kiro/skills/impeccable/reference/live.md b/.kiro/skills/impeccable/reference/live.md index 06c5bd9a8..2ec257c44 100644 --- a/.kiro/skills/impeccable/reference/live.md +++ b/.kiro/skills/impeccable/reference/live.md @@ -9,18 +9,21 @@ A running dev server with hot module replacement (Vite, Next.js, Bun, etc.), OR Execute in order. No step skipped, no step reordered. 1. `live.mjs`: boot. -2. Navigate to the URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). If you can't infer it confidently, tell the user once to open their dev/preview URL. Never use `serverPort` as that URL; it's the helper, not the app. +2. Open the app URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). Never use `serverPort`; it's the helper, not the app. **Cursor:** `browser_navigate` to that URL before polling; do not skip. **Other harnesses:** use the available browser tool; if the URL is uncertain, ask the user once. 3. Poll loop with the default long timeout (600000 ms). After every event or `--reply`, run `live-poll.mjs` again immediately. Never pass a short `--timeout=`. + +The global bar **Impeccable mark** dims and shows a pulsing amber dot when no agent is long-polling `/poll`. Hover the mark for the hint; restart `live-poll.mjs` to reconnect. 4. On `generate`: read screenshot if present; load the action's reference; plan three distinct directions; write all variants in one edit; `--reply done`; poll again. -5. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts remain recoverable until you finish cleanup, run `live-complete.mjs --id EVENT_ID`, and only then poll again. -6. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart. -7. On `exit`: run the cleanup at the bottom. +5. On `steer`: read the message and `pageUrl`; do the work (page edits, navigation help, or a short reply in the `--reply` message); `--reply steer_done`; poll again. No pickup ack. The Steer bar unlocks when `steer_done` arrives over SSE. +6. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts remain recoverable until you finish cleanup, run `live-complete.mjs --id EVENT_ID`, and only then poll again. +7. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart. +8. On `exit`: run the cleanup at the bottom. Harness policy: - **Claude Code**: run the poll as a **background task** (no short timeout). The harness notifies you when it completes, so the main conversation stays free. Do not block the shell. -- **Cursor**: run the poll in the **foreground** (blocking shell; not a background terminal, not a subagent). Cursor background terminals and subagents do not reliably resume the chat with poll stdout. +- **Cursor**: run **one-shot** poll in a **background terminal** with notify on `"type":"(steer|generate|accept|discard|exit)"`. After each event the poll exits; handle it, `--reply`, then start `live-poll.mjs` again. Do **not** use `--stream` on Cursor: incremental stdout notify is slower in practice than exit-based notify (~5s vs sub-second in testing). - **Codex**: run the poll in the **foreground** (blocking shell; not a background task, not a subagent). Codex background exec sessions do not reliably surface poll stdout back into the conversation at the moment events arrive, so a "fire-and-forget" background poll will stall live mode. -- **Other harnesses**: foreground unless you know stdout reliably returns to this session. +- **Other harnesses**: one-shot foreground unless you know stdout reliably returns to this session when a shell exits. Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodies. Spend tokens on tools and edits; on failure, one or two short sentences. @@ -30,7 +33,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi node .kiro/skills/impeccable/scripts/live.mjs ``` -Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation; **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md. +Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation; **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. `serverPort` and `serverToken` belong to the small **Impeccable live helper** HTTP server (serves `/live.js`, SSE, and `/poll`). That port is **not** your dev server and is usually not the URL you open to view the app. The browser page is whatever origin serves one of the `pageFiles` entries (Vite / Next / Bun / tunnel / LAN hostname). @@ -38,12 +41,15 @@ If output is `{ ok: false, error: "config_missing" | "config_invalid", path }`, ## Poll loop +**Default (portable, all harnesses):** + ``` LOOP: node .kiro/skills/impeccable/scripts/live-poll.mjs # default long timeout; no --timeout= Read JSON; dispatch on "type" "generate" → Handle Generate; reply done; LOOP + "steer" → Handle Steer; reply steer_done; LOOP "accept" → Handle Accept; complete carbonize cleanup if required; LOOP "discard" → Handle Discard; LOOP "prefetch" → Handle Prefetch; LOOP @@ -51,6 +57,16 @@ LOOP: "exit" → break → Cleanup ``` +**Stream mode (experimental, not for Cursor):** + +``` +node .kiro/skills/impeccable/scripts/live-poll.mjs --stream # stays running; one JSON line per event + Handle event; run --reply in a separate command + Repeat until "exit" line → Cleanup +``` + +Stream keeps one process alive and waits for `--reply` ack before polling again. Useful only when the harness reads incremental stdout reliably and quickly. **Cursor is not one of those:** background pattern notify on a long-running shell was ~5s to pick up events vs sub-second for one-shot exit notify. Default to one-shot everywhere unless you have measured otherwise. + ## Recovery commands The live helper persists an append-only journal under `.impeccable/live/sessions/`. Browser checkpoints are advisory but durable; the journal is canonical. This is local durable recovery state, not project source. @@ -71,9 +87,32 @@ Server restart rule: start `live-server.mjs` again, then poll. Startup requeues ## Handle `generate` -Event: `{id, action, freeformPrompt?, count, pageUrl, element, screenshotPath?, comments?, strokes?}`. +**Replace mode** (default): `{id, action, freeformPrompt?, count, pageUrl, element, screenshotPath?, comments?, strokes?}`. -Speed matters; the user is watching a spinner. Minimize tool calls by using the `wrap` helper and writing all variants in a single edit. +**Insert mode** (`event.mode === "insert"`): `{id, mode: "insert", count, pageUrl, insert: { position, anchor }, placeholder: { width, height }, freeformPrompt?, screenshotPath?, comments?, strokes?}`. No `action`. Requires a non-empty `freeformPrompt` **or** annotations. Screenshot is sent only when annotations exist (same rule as replace). Use `placeholder` dimensions as a soft size hint for net-new content. + +Speed matters; the user is watching a spinner. Minimize tool calls by using the wrap/insert helper and writing all variants in a single edit. + +### Insert mode branch + +When `event.mode === "insert"`: + +1. Read the screenshot if `event.screenshotPath` is present (annotations only). +2. Run the insert helper instead of wrap: + +```bash +node .kiro/skills/impeccable/scripts/live-insert.mjs --id EVENT_ID --count EVENT_COUNT --position after \ + --element-id "ANCHOR_ID" --classes "class1,class2" --tag "section" --text "ANCHOR_TEXT" +``` + +- `--position` ← `event.insert.position` (`before` | `after`) +- Anchor flags ← `event.insert.anchor` (same mapping as wrap: id, classes, tag, text) + +The scaffold has **no** `data-impeccable-variant="original"`. Variants are net-new HTML+CSS inserted at `insertLine`. Load `brand.md` or `product.md` (freeform only, no action sub-command). Write all variants in one edit, then `--reply done`. + +On accept/discard, `live-accept.mjs` removes the wrapper block; the anchor element is untouched. + +### Replace mode (default) ### 1. Read the screenshot (if present) @@ -424,6 +463,28 @@ A background agent may be used for the rewrite, but the current thread is respon Event: `{id, _acceptResult, _completionAck}`. The poll script already restored the original, removed all variant markers, and acknowledged `discarded` durable completion. Nothing to do unless `_completionAck.ok !== true`; in that case run `live-complete.mjs --id EVENT_ID --discarded`, then poll again. +## Handle `steer` + +Event: `{id, message, pageUrl}`. The user typed or spoke into the global bar **Steer** control: page-level direction without picking an element or launching variant generation. + +The mic button uses the browser **Web Speech API** (MVP): click to start, speak, stop automatically when the utterance ends, then the transcript submits as a steer event. Click again while listening to cancel without submitting. + +This is lighter than `generate`: no screenshot, no element context, no variant cycling. Read `message` and inspect the live page or project files as needed, then either make edits or answer in prose. + +When finished: + +```bash +node .kiro/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID steer_done ["Optional short note for a browser toast"] +``` + +On failure: + +```bash +node .kiro/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID error "Short reason" +``` + +Then poll again immediately. Do not send a separate "picked up" reply. The Steer bar stays locked until `steer_done` or `error` arrives over SSE. + ## Handle `prefetch` Event: `{pageUrl}`. The browser fires this the first time the user selects an element on a given route, as a latency shortcut; it signals the user is likely about to Go on a page you haven't read yet. diff --git a/.kiro/skills/impeccable/reference/motion-design.md b/.kiro/skills/impeccable/reference/motion-design.md deleted file mode 100644 index 78b6fd260..000000000 --- a/.kiro/skills/impeccable/reference/motion-design.md +++ /dev/null @@ -1,109 +0,0 @@ -# Motion Design - -## Duration: The 100/300/500 Rule - -Timing matters more than easing. These durations feel right for most UI: - -| Duration | Use Case | Examples | -|----------|----------|----------| -| **100-150ms** | Instant feedback | Button press, toggle, color change | -| **200-300ms** | State changes | Menu open, tooltip, hover states | -| **300-500ms** | Layout changes | Accordion, modal, drawer | -| **500-800ms** | Entrance animations | Page load, hero reveals | - -**Exit animations are faster than entrances.** Use ~75% of enter duration. - -## Easing: Pick the Right Curve - -**Don't use `ease`.** It's a compromise that's rarely optimal. Instead: - -| Curve | Use For | CSS | -|-------|---------|-----| -| **ease-out** | Elements entering | `cubic-bezier(0.16, 1, 0.3, 1)` | -| **ease-in** | Elements leaving | `cubic-bezier(0.7, 0, 0.84, 0)` | -| **ease-in-out** | State toggles (there → back) | `cubic-bezier(0.65, 0, 0.35, 1)` | - -**For micro-interactions, use exponential curves.** They feel natural because they mimic real physics (friction, deceleration): - -```css -/* Quart out - smooth, refined (recommended default) */ ---ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1); - -/* Quint out - slightly more dramatic */ ---ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1); - -/* Expo out - snappy, confident */ ---ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); -``` - -**Avoid bounce and elastic curves.** They were trendy in 2015 but now feel tacky and amateurish. Real objects don't bounce when they stop; they decelerate smoothly. Overshoot effects draw attention to the animation itself rather than the content. - -## Premium Motion Materials - -Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties: blur reveals, backdrop-filter panels, saturation or brightness shifts, shadow bloom, SVG filters, masks, clip paths, gradient-position movement, and variable font or shader-driven effects. - -Use the right material for the effect: - -- **Transform / opacity**: movement, press feedback, simple reveals, list choreography. -- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances, atmospheric transitions. -- **Clip path / masks**: wipes, reveals, editorial cropping, product-like transitions. -- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state. -- **Grid-template rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly. - -The hard rule is not "transform and opacity only." The hard rule is: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify in-browser that the result is smooth on the target viewports. If blur/filter makes the interaction feel significantly more premium and remains smooth, use it. - -## Staggered Animations - -Use CSS custom properties for cleaner stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"` on each item. **Cap total stagger time**: 10 items at 50ms = 500ms total. For many items, reduce per-item delay or cap staggered count. - -## Reduced Motion - -This is not optional. Vestibular disorders affect ~35% of adults over 40. - -```css -/* Define animations normally */ -.card { - animation: slide-up 500ms ease-out; -} - -/* Provide alternative for reduced motion */ -@media (prefers-reduced-motion: reduce) { - .card { - animation: fade-in 200ms ease-out; /* Crossfade instead of motion */ - } -} - -/* Or disable entirely */ -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - transition-duration: 0.01ms !important; - } -} -``` - -**What to preserve**: Functional animations like progress bars, loading spinners (slowed down), and focus indicators should still work, just without spatial movement. - -## Perceived Performance - -**Nobody cares how fast your site is, just how fast it feels.** Perception can be as effective as actual performance. - -**The 80ms threshold**: Our brains buffer sensory input for ~80ms to synchronize perception. Anything under 80ms feels instant and simultaneous. This is your target for micro-interactions. - -**Active vs passive time**: Passive waiting (staring at a spinner) feels longer than active engagement. Strategies to shift the balance: - -- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening. -- **Early completion**: Show content progressively, don't wait for everything. Video buffering, progressive images, streaming HTML. -- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Instagram likes work offline; the UI updates instantly, syncs later. Use for low-stakes actions; avoid for payments or destructive operations. - -**Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances, but ease-in toward a task's end compresses perceived time. - -**Caution**: Too-fast responses can decrease perceived value. Users may distrust instant results for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening. - -## Performance - -Don't use `will-change` preemptively, only when animation is imminent (`:hover`, `.animating`). For scroll-triggered animations, use Intersection Observer instead of scroll events; unobserve after animating once. Create motion tokens for consistency (durations, easings, common transitions). - ---- - -**Avoid**: Animating everything (animation fatigue is real). Using >500ms for UI feedback. Ignoring `prefers-reduced-motion`. Using animation to hide slow loading. diff --git a/.kiro/skills/impeccable/reference/personas.md b/.kiro/skills/impeccable/reference/personas.md deleted file mode 100644 index ac061fb39..000000000 --- a/.kiro/skills/impeccable/reference/personas.md +++ /dev/null @@ -1,179 +0,0 @@ -# Persona-Based Design Testing - -Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss. - -**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns. - ---- - -## 1. Impatient Power User: "Alex" - - -**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave. - -**Behaviors**: -- Skips all onboarding and instructions -- Looks for keyboard shortcuts immediately -- Tries to bulk-select, batch-edit, and automate -- Gets frustrated by required steps that feel unnecessary -- Abandons if anything feels slow or patronizing - -**Test Questions**: -- Can Alex complete the core task in under 60 seconds? -- Are there keyboard shortcuts for common actions? -- Can onboarding be skipped entirely? -- Do modals have keyboard dismiss (Esc)? -- Is there a "power user" path (shortcuts, bulk actions)? - -**Red Flags** (report these specifically): -- Forced tutorials or unskippable onboarding -- No keyboard navigation for primary actions -- Slow animations that can't be skipped -- One-item-at-a-time workflows where batch would be natural -- Redundant confirmation steps for low-risk actions - ---- - -## 2. Confused First-Timer: "Jordan" - -**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out. - -**Behaviors**: -- Reads all instructions carefully -- Hesitates before clicking anything unfamiliar -- Looks for help or support constantly -- Misunderstands jargon and abbreviations -- Takes the most literal interpretation of any label - -**Test Questions**: -- Is the first action obviously clear within 5 seconds? -- Are all icons labeled with text? -- Is there contextual help at decision points? -- Does terminology assume prior knowledge? -- Is there a clear "back" or "undo" at every step? - -**Red Flags** (report these specifically): -- Icon-only navigation with no labels -- Technical jargon without explanation -- No visible help option or guidance -- Ambiguous next steps after completing an action -- No confirmation that an action succeeded - ---- - -## 3. Accessibility-Dependent User: "Sam" - -**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences. - -**Behaviors**: -- Tabs through the interface linearly -- Relies on ARIA labels and heading structure -- Cannot see hover states or visual-only indicators -- Needs adequate color contrast (4.5:1 minimum) -- May use browser zoom up to 200% - -**Test Questions**: -- Can the entire primary flow be completed keyboard-only? -- Are all interactive elements focusable with visible focus indicators? -- Do images have meaningful alt text? -- Is color contrast WCAG AA compliant (4.5:1 for text)? -- Does the screen reader announce state changes (loading, success, errors)? - -**Red Flags** (report these specifically): -- Click-only interactions with no keyboard alternative -- Missing or invisible focus indicators -- Meaning conveyed by color alone (red = error, green = success) -- Unlabeled form fields or buttons -- Time-limited actions without extension option -- Custom components that break screen reader flow - ---- - -## 4. Deliberate Stress Tester: "Riley" - -**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience. - -**Behaviors**: -- Tests edge cases intentionally (empty states, long strings, special characters) -- Submits forms with unexpected data (emoji, RTL text, very long values) -- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs -- Looks for inconsistencies between what the UI promises and what actually happens -- Documents problems methodically - -**Test Questions**: -- What happens at the edges (0 items, 1000 items, very long text)? -- Do error states recover gracefully or leave the UI in a broken state? -- What happens on refresh mid-workflow? Is state preserved? -- Are there features that appear to work but produce broken results? -- How does the UI handle unexpected input (emoji, special chars, paste from Excel)? - -**Red Flags** (report these specifically): -- Features that appear to work but silently fail or produce wrong results -- Error handling that exposes technical details or leaves UI in a broken state -- Empty states that show nothing useful ("No results" with no guidance) -- Workflows that lose user data on refresh or navigation -- Inconsistent behavior between similar interactions in different parts of the UI - ---- - -## 5. Distracted Mobile User: "Casey" - -**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection. - -**Behaviors**: -- Uses thumb only; prefers bottom-of-screen actions -- Gets interrupted mid-flow and returns later -- Switches between apps frequently -- Has limited attention span and low patience -- Types as little as possible, prefers taps and selections - -**Test Questions**: -- Are primary actions in the thumb zone (bottom half of screen)? -- Is state preserved if the user leaves and returns? -- Does it work on slow connections (3G)? -- Can forms use autocomplete and smart defaults? -- Are touch targets at least 44×44pt? - -**Red Flags** (report these specifically): -- Important actions positioned at the top of the screen (unreachable by thumb) -- No state persistence; progress lost on tab switch or interruption -- Large text inputs required where selection would work -- Heavy assets loading on every page (no lazy loading) -- Tiny tap targets or targets too close together - ---- - -## Selecting Personas - -Choose personas based on the interface type: - -| Interface Type | Primary Personas | Why | -|---------------|-----------------|-----| -| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile | -| Dashboard / admin | Alex, Sam | Power users, accessibility | -| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity | -| Onboarding flow | Jordan, Casey | Confusion, interruption | -| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav | -| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile | - ---- - -## Project-Specific Personas - -If `.kiro/settings.json` contains a `## Design Context` section (generated by `impeccable teach`), derive 1–2 additional personas from the audience and brand information: - -1. Read the target audience description -2. Identify the primary user archetype not covered by the 5 predefined personas -3. Create a persona following this template: - -``` -### [Role]: "[Name]" - -**Profile**: [2-3 key characteristics derived from Design Context] - -**Behaviors**: [3-4 specific behaviors based on the described audience] - -**Red Flags**: [3-4 things that would alienate this specific user type] -``` - -Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists. diff --git a/.kiro/skills/impeccable/reference/polish.md b/.kiro/skills/impeccable/reference/polish.md index e20e1467c..5ac13d316 100644 --- a/.kiro/skills/impeccable/reference/polish.md +++ b/.kiro/skills/impeccable/reference/polish.md @@ -91,7 +91,6 @@ Visual polish on a misshapen flow is wasted work. Match the *shape* of the exper - **Theme consistency**: Works in all theme variants - **Color meaning**: Same colors mean same things throughout - **Accessible focus**: Focus indicators visible with sufficient contrast -- **Tinted neutrals**: No pure gray or pure black; add subtle color tint (0.01 chroma) - **Gray on color**: Never put gray text on colored backgrounds; use a shade of that color or transparency ### Interaction States diff --git a/.kiro/skills/impeccable/reference/product.md b/.kiro/skills/impeccable/reference/product.md index 64b3b0169..2ca462350 100644 --- a/.kiro/skills/impeccable/reference/product.md +++ b/.kiro/skills/impeccable/reference/product.md @@ -10,7 +10,6 @@ Product UI's failure mode isn't flatness, it's strangeness without purpose: over ## Typography -- **System fonts are legitimate.** `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif` gives you native feel on every platform. Inter is the common cross-platform default for a reason. - **One family is often right.** Product UIs don't need display/body pairing. A well-tuned sans carries headings, buttons, labels, body, data. - **Fixed rem scale, not fluid.** Clamp-sized headings don't serve product UI. Users view at consistent DPI, and a fluid h1 that shrinks in a sidebar looks worse, not better. - **Tighter scale ratio.** 1.125–1.2 between steps is typical. More type elements here than on brand surfaces; exaggerated contrast creates noise. @@ -26,8 +25,6 @@ Product defaults to Restrained. A single surface can earn Committed (a dashboard ## Layout -- Predictable grids. Consistency IS an affordance; users navigate faster when the structure is expected. -- Familiar patterns are features. Standard navigation (top bar, side nav), breadcrumbs, tabs, and form layouts have established user expectations. Don't reinvent for flavor. - Responsive behavior is structural (collapse sidebar, responsive table, breakpoint-driven columns), not fluid typography. ## Components @@ -51,6 +48,7 @@ Every interactive component has: default, hover, focus, active, disabled, loadin - Display fonts in UI labels, buttons, data. - Reinventing standard affordances for flavor (custom scrollbars, weird form controls, non-standard modals). - Heavy color or full-saturation accents on inactive states. +- Modal as first thought. Modals are usually laziness. Exhaust inline / progressive alternatives first. ## Product permissions diff --git a/.kiro/skills/impeccable/reference/responsive-design.md b/.kiro/skills/impeccable/reference/responsive-design.md deleted file mode 100644 index f079e5e5d..000000000 --- a/.kiro/skills/impeccable/reference/responsive-design.md +++ /dev/null @@ -1,114 +0,0 @@ -# Responsive Design - -## Mobile-First: Write It Right - -Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first. - -## Breakpoints: Content-Driven - -Don't chase device sizes; let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints. - -## Detect Input Method, Not Just Screen Size - -**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard. Use pointer and hover queries: - -```css -/* Fine pointer (mouse, trackpad) */ -@media (pointer: fine) { - .button { padding: 8px 16px; } -} - -/* Coarse pointer (touch, stylus) */ -@media (pointer: coarse) { - .button { padding: 12px 20px; } /* Larger touch target */ -} - -/* Device supports hover */ -@media (hover: hover) { - .card:hover { transform: translateY(-2px); } -} - -/* Device doesn't support hover (touch) */ -@media (hover: none) { - .card { /* No hover state - use active instead */ } -} -``` - -**Critical**: Don't rely on hover for functionality. Touch users can't hover. - -## Safe Areas: Handle the Notch - -Modern phones have notches, rounded corners, and home indicators. Use `env()`: - -```css -body { - padding-top: env(safe-area-inset-top); - padding-bottom: env(safe-area-inset-bottom); - padding-left: env(safe-area-inset-left); - padding-right: env(safe-area-inset-right); -} - -/* With fallback */ -.footer { - padding-bottom: max(1rem, env(safe-area-inset-bottom)); -} -``` - -**Enable viewport-fit** in your meta tag: -```html - -``` - -## Responsive Images: Get It Right - -### srcset with Width Descriptors - -```html -Hero image -``` - -**How it works**: -- `srcset` lists available images with their actual widths (`w` descriptors) -- `sizes` tells the browser how wide the image will display -- Browser picks the best file based on viewport width AND device pixel ratio - -### Picture Element for Art Direction - -When you need different crops/compositions (not just resolutions): - -```html - - - - ... - -``` - -## Layout Adaptation Patterns - -**Navigation**: Three stages: hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `
/` for content that can collapse on mobile. - -## Testing: Don't Trust DevTools Alone - -DevTools device emulation is useful for layout but misses: - -- Actual touch interactions -- Real CPU/memory constraints -- Network latency patterns -- Font rendering differences -- Browser chrome/keyboard appearances - -**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators. - ---- - -**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful. diff --git a/.kiro/skills/impeccable/reference/shape.md b/.kiro/skills/impeccable/reference/shape.md index f7c4cd2f1..592def8a7 100644 --- a/.kiro/skills/impeccable/reference/shape.md +++ b/.kiro/skills/impeccable/reference/shape.md @@ -151,7 +151,7 @@ How users interact with this feature. What happens on click, hover, scroll? What 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., spatial-design.md for complex layouts, motion-design.md for animated features, interaction-design.md for form-heavy features). +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. diff --git a/.kiro/skills/impeccable/reference/spatial-design.md b/.kiro/skills/impeccable/reference/spatial-design.md deleted file mode 100644 index 4d4b83afc..000000000 --- a/.kiro/skills/impeccable/reference/spatial-design.md +++ /dev/null @@ -1,100 +0,0 @@ -# Spatial Design - -## Spacing Systems - -### Use 4pt Base, Not 8pt - -8pt systems are too coarse; you'll frequently need 12px (between 8 and 16). Use 4pt for granularity: 4, 8, 12, 16, 24, 32, 48, 64, 96px. - -### Name Tokens Semantically - -Name by relationship (`--space-sm`, `--space-lg`), not value (`--spacing-8`). Use `gap` instead of margins for sibling spacing; it eliminates margin collapse and cleanup hacks. - -## Grid Systems - -### The Self-Adjusting Grid - -Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. Columns are at least 280px, as many as fit per row, leftovers stretch. For complex layouts, use named grid areas (`grid-template-areas`) and redefine them at breakpoints. - -## Visual Hierarchy - -### The Squint Test - -Blur your eyes (or screenshot and blur). Can you still identify: -- The most important element? -- The second most important? -- Clear groupings? - -If everything looks the same weight blurred, you have a hierarchy problem. - -### Hierarchy Through Multiple Dimensions - -Don't rely on size alone. Combine: - -| Tool | Strong Hierarchy | Weak Hierarchy | -|------|------------------|----------------| -| **Size** | 3:1 ratio or more | <2:1 ratio | -| **Weight** | Bold vs Regular | Medium vs Regular | -| **Color** | High contrast | Similar tones | -| **Position** | Top/left (primary) | Bottom/right | -| **Space** | Surrounded by white space | Crowded | - -**The best hierarchy uses 2-3 dimensions at once**: A heading that's larger, bolder, AND has more space above it. - -### Cards Are Not Required - -Cards are overused. Spacing and alignment create visual grouping naturally. Use cards only when content is truly distinct and actionable, items need visual comparison in a grid, or content needs clear interaction boundaries. **Never nest cards inside cards.** Use spacing, typography, and subtle dividers for hierarchy within a card. - -## Container Queries - -Viewport queries are for page layouts. **Container queries are for components**: - -```css -.card-container { - container-type: inline-size; -} - -.card { - display: grid; - gap: var(--space-md); -} - -/* Card layout changes based on its container, not viewport */ -@container (min-width: 400px) { - .card { - grid-template-columns: 120px 1fr; - } -} -``` - -**Why this matters**: A card in a narrow sidebar stays compact, while the same card in a main content area expands automatically, without viewport hacks. - -## Optical Adjustments - -Text at `margin-left: 0` looks indented due to letterform whitespace; use negative margin (`-0.05em`) to optically align. Geometrically centered icons often look off-center; play icons need to shift right, arrows shift toward their direction. - -### Touch Targets vs Visual Size - -Buttons can look small but need large touch targets (44px minimum). Use padding or pseudo-elements: - -```css -.icon-button { - width: 24px; /* Visual size */ - height: 24px; - position: relative; -} - -.icon-button::before { - content: ''; - position: absolute; - inset: -10px; /* Expand tap target to 44px */ -} -``` - -## Depth & Elevation - -Create semantic z-index scales (dropdown → sticky → modal-backdrop → modal → toast → tooltip) instead of arbitrary numbers. For shadows, create a consistent elevation scale (sm → md → lg → xl). **Key insight**: Shadows should be subtle. If you can clearly see it, it's probably too strong. - ---- - -**Avoid**: Arbitrary spacing values outside your scale. Making all spacing equal (variety creates hierarchy). Creating hierarchy through size alone - combine size, weight, color, and space. diff --git a/.kiro/skills/impeccable/reference/typeset.md b/.kiro/skills/impeccable/reference/typeset.md index dd3e67ea3..5f5e0c2c0 100644 --- a/.kiro/skills/impeccable/reference/typeset.md +++ b/.kiro/skills/impeccable/reference/typeset.md @@ -4,7 +4,7 @@ Typography carries most of the information on the page. Replace generic defaults ## Register -Brand: run the font selection procedure in [brand.md](brand.md). Pairing follows the brand's lane (display serif + sans body for editorial/luxury, one committed sans for tech, etc.). Fluid `clamp()` scale, ≥1.25 ratio between steps. +Brand: run the font selection procedure in [brand.md](brand.md). Fluid `clamp()` scale, ≥1.25 ratio between steps. Product: system fonts and familiar sans stacks are legitimate here. One well-tuned family typically carries the whole UI. Fixed `rem` scale, 1.125–1.2 ratio between more closely-spaced steps. @@ -43,7 +43,7 @@ Analyze what's weak or generic about the current type: ## Plan Typography Improvements -Consult the [typography reference](typography.md) for detailed guidance on scales, pairing, and loading strategies. +Consult the [Reference Material](#reference-material) section below for detailed guidance on scales, pairing, and loading strategies. Create a systematic plan: @@ -122,3 +122,158 @@ Each variant MUST declare a `scale` param controlling the hierarchy ratio. Expre Where the variant riffs on a specific pairing, expose the pairing choice as a `steps` param (e.g. "serif display + sans body" vs. "mono display + sans body" vs. "all-sans"). Each branch routes through `:scope[data-p-pairing="X"]` selectors in scoped CSS. See `reference/live.md` for the full params contract. + +--- + +## Reference Material + +The sections below were previously `typography.md` and live inline now so the typeset flow has its deep typography reference in one place. `bolder.md` also references this section. + +### Typography + +#### Classic Typography Principles + +##### Vertical Rhythm + +Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony; text and space share a mathematical foundation. + +##### Modular Scale & Hierarchy + +The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy. + +**Use fewer sizes with more contrast.** A 5-size system covers most needs: + +| Role | Typical Ratio | Use Case | +|------|---------------|----------| +| xs | 0.75rem | Captions, legal | +| sm | 0.875rem | Secondary UI, metadata | +| base | 1rem | Body text | +| lg | 1.25-1.5rem | Subheadings, lead text | +| xl+ | 2-4rem | Headlines, hero text | + +Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit. + +##### Readability & Measure + +Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length: narrow columns need tighter leading, wide columns need more. + +**Non-obvious**: Light text on dark backgrounds needs compensation on three axes, not just one. Bump line-height by 0.05–0.1, add a touch of letter-spacing (0.01–0.02em), and optionally step the body weight up one notch (regular → medium). The perceived weight drops across all three; fix all three. + +**Paragraph rhythm**: Pick either space between paragraphs OR first-line indentation. Never both. Digital usually wants space; editorial/long-form can justify indent-only. + +#### Font Selection & Pairing + +The tactical selection procedure and the reflex-reject list live in [reference/brand.md](brand.md) under **Font selection procedure** and **Reflex-reject list** (loaded for brand-register tasks). The rest of this section covers the adjacent knowledge: anti-reflex corrections, system font use, and pairing rules. + +##### Anti-reflexes worth defending against + +- A technical/utilitarian brief does NOT need a serif "for warmth." Most tech tools should look like tech tools. +- An editorial/premium brief does NOT need the same expressive serif everyone is using right now. Premium can be Swiss-modern, can be neo-grotesque, can be a literal monospace, can be a quiet humanist sans. +- A children's product does NOT need a rounded display font. Kids' books use real type. +- A "modern" brief does NOT need a geometric sans. The most modern thing you can do is not use the font everyone else is using. + +**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality. + +##### Pairing Principles + +**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif). + +When pairing, contrast on multiple axes: +- Serif + Sans (structure contrast) +- Geometric + Humanist (personality contrast) +- Condensed display + Wide body (proportion contrast) + +##### Web Font Loading + +The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix: + +```css +/* 1. Use font-display: swap for visibility */ +@font-face { + font-family: 'CustomFont'; + src: url('font.woff2') format('woff2'); + font-display: swap; +} + +/* 2. Match fallback metrics to minimize shift */ +@font-face { + font-family: 'CustomFont-Fallback'; + src: local('Arial'); + size-adjust: 105%; /* Scale to match x-height */ + ascent-override: 90%; /* Match ascender height */ + descent-override: 20%; /* Match descender depth */ + line-gap-override: 10%; /* Match line spacing */ +} + +body { + font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; +} +``` + +Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically. + +**`swap` vs `optional`**: `swap` shows fallback text immediately and FOUT-swaps when the web font arrives. `optional` uses the fallback if the web font misses a small load budget (~100ms) and avoids the shift entirely. Pick `optional` when zero layout shift matters more than seeing the branded font on slow networks. + +**Preload the critical weight only**: typically the regular-weight body font used above the fold. Preloading every weight costs more bandwidth than it saves. + +**Variable fonts for 3+ weights or styles**: a single variable font file is usually smaller than three static weight files, gives fractional weight control, and pairs well with `font-optical-sizing: auto`. For 1–2 weights, static is fine. + +#### Modern Web Typography + +##### Fluid Type + +Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate (higher vw = faster scaling). Add a rem offset so it doesn't collapse to 0 on small screens. + +**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes. + +**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI; fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it. + +**Bound your clamp()**: keep `max-size ≤ ~2.5 × min-size`. Wider ratios break the browser's zoom and reflow behaviour and make large viewports feel like the page is shouting. + +**Scale container width and font-size together** so effective character measure stays in the 45–75ch band at every viewport. A heading that widens faster than its container drifts out of the comfortable measure at the top end. + +##### OpenType Features + +Most developers don't know these exist. Use them for polish: + +```css +/* Proper fractions */ +.recipe-amount { font-variant-numeric: diagonal-fractions; } + +/* Small caps for abbreviations */ +abbr { font-variant-caps: all-small-caps; } + +/* Disable ligatures in code */ +code { font-variant-ligatures: none; } + +/* Enable kerning (usually on by default, but be explicit) */ +body { font-kerning: normal; } +``` + +Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/). + +##### Rendering polish + +```css +/* Variable fonts: pick the right optical-size master automatically */ +body { font-optical-sizing: auto; } +``` + +**ALL-CAPS tracking**: capitals sit too close at default spacing. Add 5–12% letter-spacing (`letter-spacing: 0.05em` to `0.12em`) to short all-caps labels, eyebrows, and small headings. Real small caps (via `font-variant-caps`) need the same treatment, slightly gentler. + +#### Typography System Architecture + +Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system. + +#### Accessibility Considerations + +Beyond contrast ratios (which are well-documented), consider: + +- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout. +- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text. +- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile. +- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets. + +--- + +**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text. diff --git a/.kiro/skills/impeccable/reference/typography.md b/.kiro/skills/impeccable/reference/typography.md deleted file mode 100644 index ed5ca272a..000000000 --- a/.kiro/skills/impeccable/reference/typography.md +++ /dev/null @@ -1,159 +0,0 @@ -# Typography - -## Classic Typography Principles - -### Vertical Rhythm - -Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony; text and space share a mathematical foundation. - -### Modular Scale & Hierarchy - -The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy. - -**Use fewer sizes with more contrast.** A 5-size system covers most needs: - -| Role | Typical Ratio | Use Case | -|------|---------------|----------| -| xs | 0.75rem | Captions, legal | -| sm | 0.875rem | Secondary UI, metadata | -| base | 1rem | Body text | -| lg | 1.25-1.5rem | Subheadings, lead text | -| xl+ | 2-4rem | Headlines, hero text | - -Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit. - -### Readability & Measure - -Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length: narrow columns need tighter leading, wide columns need more. - -**Non-obvious**: Light text on dark backgrounds needs compensation on three axes, not just one. Bump line-height by 0.05–0.1, add a touch of letter-spacing (0.01–0.02em), and optionally step the body weight up one notch (regular → medium). The perceived weight drops across all three; fix all three. - -**Paragraph rhythm**: Pick either space between paragraphs OR first-line indentation. Never both. Digital usually wants space; editorial/long-form can justify indent-only. - -## Font Selection & Pairing - -The tactical selection procedure and the reflex-reject list live in [reference/brand.md](brand.md) under **Font selection procedure** and **Reflex-reject list** (loaded for brand-register tasks). The rest of this section covers the adjacent knowledge: anti-reflex corrections, system font use, and pairing rules. - -### Anti-reflexes worth defending against - -- A technical/utilitarian brief does NOT need a serif "for warmth." Most tech tools should look like tech tools. -- An editorial/premium brief does NOT need the same expressive serif everyone is using right now. Premium can be Swiss-modern, can be neo-grotesque, can be a literal monospace, can be a quiet humanist sans. -- A children's product does NOT need a rounded display font. Kids' books use real type. -- A "modern" brief does NOT need a geometric sans. The most modern thing you can do is not use the font everyone else is using. - -**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality. - -### Pairing Principles - -**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif). - -When pairing, contrast on multiple axes: -- Serif + Sans (structure contrast) -- Geometric + Humanist (personality contrast) -- Condensed display + Wide body (proportion contrast) - -**Never pair fonts that are similar but not identical** (e.g., two geometric sans-serifs). They create visual tension without clear hierarchy. - -### Web Font Loading - -The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix: - -```css -/* 1. Use font-display: swap for visibility */ -@font-face { - font-family: 'CustomFont'; - src: url('font.woff2') format('woff2'); - font-display: swap; -} - -/* 2. Match fallback metrics to minimize shift */ -@font-face { - font-family: 'CustomFont-Fallback'; - src: local('Arial'); - size-adjust: 105%; /* Scale to match x-height */ - ascent-override: 90%; /* Match ascender height */ - descent-override: 20%; /* Match descender depth */ - line-gap-override: 10%; /* Match line spacing */ -} - -body { - font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; -} -``` - -Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically. - -**`swap` vs `optional`**: `swap` shows fallback text immediately and FOUT-swaps when the web font arrives. `optional` uses the fallback if the web font misses a small load budget (~100ms) and avoids the shift entirely. Pick `optional` when zero layout shift matters more than seeing the branded font on slow networks. - -**Preload the critical weight only**: typically the regular-weight body font used above the fold. Preloading every weight costs more bandwidth than it saves. - -**Variable fonts for 3+ weights or styles**: a single variable font file is usually smaller than three static weight files, gives fractional weight control, and pairs well with `font-optical-sizing: auto`. For 1–2 weights, static is fine. - -## Modern Web Typography - -### Fluid Type - -Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate (higher vw = faster scaling). Add a rem offset so it doesn't collapse to 0 on small screens. - -**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes. - -**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI; fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it. - -**Bound your clamp()**: keep `max-size ≤ ~2.5 × min-size`. Wider ratios break the browser's zoom and reflow behaviour and make large viewports feel like the page is shouting. - -**Scale container width and font-size together** so effective character measure stays in the 45–75ch band at every viewport. A heading that widens faster than its container drifts out of the comfortable measure at the top end. - -### OpenType Features - -Most developers don't know these exist. Use them for polish: - -```css -/* Tabular numbers for data alignment */ -.data-table { font-variant-numeric: tabular-nums; } - -/* Proper fractions */ -.recipe-amount { font-variant-numeric: diagonal-fractions; } - -/* Small caps for abbreviations */ -abbr { font-variant-caps: all-small-caps; } - -/* Disable ligatures in code */ -code { font-variant-ligatures: none; } - -/* Enable kerning (usually on by default, but be explicit) */ -body { font-kerning: normal; } -``` - -Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/). - -### Rendering polish - -```css -/* Even out heading line lengths (browser picks better break points) */ -h1, h2, h3 { text-wrap: balance; } - -/* Reduce orphans and ragged endings in long prose */ -article p { text-wrap: pretty; } - -/* Variable fonts: pick the right optical-size master automatically */ -body { font-optical-sizing: auto; } -``` - -**ALL-CAPS tracking**: capitals sit too close at default spacing. Add 5–12% letter-spacing (`letter-spacing: 0.05em` to `0.12em`) to short all-caps labels, eyebrows, and small headings. Real small caps (via `font-variant-caps`) need the same treatment, slightly gentler. - -## Typography System Architecture - -Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system. - -## Accessibility Considerations - -Beyond contrast ratios (which are well-documented), consider: - -- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout. -- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text. -- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile. -- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets. - ---- - -**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text. diff --git a/.kiro/skills/impeccable/reference/ux-writing.md b/.kiro/skills/impeccable/reference/ux-writing.md deleted file mode 100644 index 417b5f56c..000000000 --- a/.kiro/skills/impeccable/reference/ux-writing.md +++ /dev/null @@ -1,107 +0,0 @@ -# UX Writing - -## The Button Label Problem - -**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns: - -| Bad | Good | Why | -|-----|------|-----| -| OK | Save changes | Says what will happen | -| Submit | Create account | Outcome-focused | -| Yes | Delete message | Confirms the action | -| Cancel | Keep editing | Clarifies what "cancel" means | -| Click here | Download PDF | Describes the destination | - -**For destructive actions**, name the destruction: -- "Delete" not "Remove" (delete is permanent, remove implies recoverable) -- "Delete 5 items" not "Delete selected" (show the count) - -## Error Messages: The Formula - -Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input". - -### Error Message Templates - -| Situation | Template | -|-----------|----------| -| **Format error** | "[Field] needs to be [format]. Example: [example]" | -| **Missing required** | "Please enter [what's missing]" | -| **Permission denied** | "You don't have access to [thing]. [What to do instead]" | -| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." | -| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" | - -### Don't Blame the User - -Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date". - -## Empty States Are Opportunities - -Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items". - -## Voice vs Tone - -**Voice** is your brand's personality, consistent everywhere. -**Tone** adapts to the moment. - -| Moment | Tone Shift | -|--------|------------| -| Success | Celebratory, brief: "Done! Your changes are live." | -| Error | Empathetic, helpful: "That didn't work. Here's what to try..." | -| Loading | Reassuring: "Saving your work..." | -| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | - -**Never use humor for errors.** Users are already frustrated. Be helpful, not cute. - -## Writing for Accessibility - -**Link text** must have standalone meaning: "View pricing plans" not "Click here". **Alt text** describes information, not the image: "Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context. - -## Writing for Translation - -### Plan for Expansion - -German text is ~30% longer than English. Allocate space: - -| Language | Expansion | -|----------|-----------| -| German | +30% | -| French | +20% | -| Finnish | +30-40% | -| Chinese | -30% (fewer chars, but same width) | - -### Translation-Friendly Patterns - -Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear. - -## Consistency: The Terminology Problem - -Pick one term and stick with it: - -| Inconsistent | Consistent | -|--------------|------------| -| Delete / Remove / Trash | Delete | -| Settings / Preferences / Options | Settings | -| Sign in / Log in / Enter | Sign in | -| Create / Add / New | Create | - -Build a terminology glossary and enforce it. Variety creates confusion. - -## Avoid Redundant Copy - -If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well. - -## Loading States - -Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress. - -## Confirmation Dialogs: Use Sparingly - -Most confirmation dialogs are design failures; consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No"). - -## Form Instructions - -Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking. - ---- - -**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors. diff --git a/.kiro/skills/impeccable/scripts/cleanup-deprecated.mjs b/.kiro/skills/impeccable/scripts/cleanup-deprecated.mjs index 6aee4710a..bc6400e90 100644 --- a/.kiro/skills/impeccable/scripts/cleanup-deprecated.mjs +++ b/.kiro/skills/impeccable/scripts/cleanup-deprecated.mjs @@ -25,7 +25,7 @@ import { join, resolve } from 'node:path'; const DEPRECATED_NAMES = [ // v2.0 renames 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable teach + 'teach-impeccable', // folded into /impeccable init // v2.1 merges 'arrange', // renamed to layout 'normalize', // merged into polish diff --git a/.kiro/skills/impeccable/scripts/command-metadata.json b/.kiro/skills/impeccable/scripts/command-metadata.json index 1488bd5f1..83796d54b 100644 --- a/.kiro/skills/impeccable/scripts/command-metadata.json +++ b/.kiro/skills/impeccable/scripts/command-metadata.json @@ -3,8 +3,8 @@ "description": "Full confirmed-brief-then-build flow. Runs multi-round shape discovery first, resolves visual probe and north-star mock gates when available, then builds and visually iterates. Use when building a new feature end-to-end.", "argumentHint": "[feature description]" }, - "teach": { - "description": "Gathers design context for a project. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.", + "init": { + "description": "Sets up a project for impeccable. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles); offers DESIGN.md (visual: colors, typography, components) when code exists; pre-configures live mode; then recommends the best commands to run next. Every other command reads these files before doing work. Use once per project.", "argumentHint": "" }, "document": { diff --git a/.kiro/skills/impeccable/scripts/context.mjs b/.kiro/skills/impeccable/scripts/context.mjs new file mode 100644 index 000000000..52263c883 --- /dev/null +++ b/.kiro/skills/impeccable/scripts/context.mjs @@ -0,0 +1,253 @@ +/** + * Context loader: prints PRODUCT.md (and DESIGN.md if present) as one + * markdown block on stdout, or exits with empty stdout when no PRODUCT.md + * is found anywhere. The skill keys off "empty stdout" to branch into the + * init flow. + * + * Path resolution (first match wins): + * 1. cwd, if PRODUCT.md or DESIGN.md is there + * 2. .agents/context/ then docs/ + * 3. $IMPECCABLE_CONTEXT_DIR (absolute or cwd-relative) — power-user + * escape hatch, only consulted when defaults are empty + * 4. cwd as a "nothing found" default + * + * `resolveContextDir()` and `loadContext()` are also exported for the + * server-side scripts (live.mjs, live-server.mjs) that need the structured + * shape rather than the markdown block. + */ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; +const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; +const FALLBACK_DIRS = ['.agents/context', 'docs']; + +// ─── Update check ────────────────────────────────────────────────────────── +// Piggyback a lightweight skill-version check on the once-per-session boot. +// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent +// can offer `npx impeccable skills update`. Everything here is best-effort and +// silent on failure: a network problem, sandbox, or missing cache must never +// block context output or print an error. + +const UPDATE_HOST = (process.env.IMPECCABLE_UPDATE_HOST || 'https://impeccable.style').replace(/\/$/, ''); +const UPDATE_CACHE_PATH = + process.env.IMPECCABLE_UPDATE_CACHE || path.join(os.homedir(), '.impeccable', 'update-check.json'); +const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // throttle the network poll to once a day +const RENOTIFY_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000; // don't re-surface the same version for a week +const FETCH_TIMEOUT_MS = 1200; + +export function resolveContextDir(cwd = process.cwd()) { + if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { + return cwd; + } + for (const rel of FALLBACK_DIRS) { + const candidate = path.resolve(cwd, rel); + if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { + return candidate; + } + } + const envDir = process.env.IMPECCABLE_CONTEXT_DIR; + if (envDir && envDir.trim()) { + const trimmed = envDir.trim(); + return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); + } + return cwd; +} + +export function loadContext(cwd = process.cwd()) { + const contextDir = resolveContextDir(cwd); + const productPath = firstExisting(contextDir, PRODUCT_NAMES); + const designPath = firstExisting(contextDir, DESIGN_NAMES); + const product = productPath ? safeRead(productPath) : null; + const design = designPath ? safeRead(designPath) : null; + return { + hasProduct: !!product, + product, + productPath: productPath ? path.relative(cwd, productPath) : null, + hasDesign: !!design, + design, + designPath: designPath ? path.relative(cwd, designPath) : null, + contextDir, + }; +} + +function firstExisting(dir, names) { + for (const name of names) { + const abs = path.join(dir, name); + if (fs.existsSync(abs)) return abs; + } + return null; +} + +function safeRead(p) { + try { + return fs.readFileSync(p, 'utf-8'); + } catch { + return null; + } +} + +/** + * Pull the register (`brand` or `product`) out of PRODUCT.md by looking + * for a `## Register` section and reading the first non-empty line that + * follows it. Returns null when the file is legacy / register-less. + */ +function extractRegister(product) { + if (!product) return null; + const lines = product.split('\n'); + for (let i = 0; i < lines.length; i++) { + if (/^##\s+Register\b/i.test(lines[i].trim())) { + for (let j = i + 1; j < lines.length; j++) { + const next = lines[j].trim(); + if (!next) continue; + const word = next.toLowerCase(); + if (word === 'brand' || word === 'product') return word; + return null; + } + } + } + return null; +} + +/** + * Read the installed skill's own version from the sibling SKILL.md frontmatter + * (this file lives at `/scripts/context.mjs`). Returns null when the + * frontmatter is missing or unreadable. + */ +function readLocalSkillVersion() { + try { + const here = path.dirname(fileURLToPath(import.meta.url)); + const skillMd = path.join(here, '..', 'SKILL.md'); + const content = fs.readFileSync(skillMd, 'utf-8'); + const match = content.match(/^version:\s*(.+)$/m); + return match ? match[1].trim().replace(/^["']|["']$/g, '') : null; + } catch { + return null; + } +} + +function readUpdateCache() { + try { + return JSON.parse(fs.readFileSync(UPDATE_CACHE_PATH, 'utf-8')); + } catch { + return {}; + } +} + +function writeUpdateCache(cache) { + try { + fs.mkdirSync(path.dirname(UPDATE_CACHE_PATH), { recursive: true }); + fs.writeFileSync(UPDATE_CACHE_PATH, JSON.stringify(cache)); + } catch { + // Best-effort: a read-only home dir just means we re-poll next session. + } +} + +/** Compare dotted numeric versions. Returns >0 when a is newer than b. */ +function compareSemver(a, b) { + const pa = String(a).split('.').map(n => parseInt(n, 10) || 0); + const pb = String(b).split('.').map(n => parseInt(n, 10) || 0); + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + const diff = (pa[i] || 0) - (pb[i] || 0); + if (diff !== 0) return diff; + } + return 0; +} + +async function fetchLatestSkillVersion() { + try { + const res = await fetch(`${UPDATE_HOST}/api/version`, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }); + if (!res.ok) return null; + const data = await res.json(); + return typeof data?.skills === 'string' ? data.skills : null; + } catch { + return null; // offline, sandboxed, timed out, or bad JSON: all non-fatal + } +} + +function buildUpdateDirective(localVersion, latestVersion) { + return ( + `UPDATE_AVAILABLE: A newer Impeccable skill is available ` + + `(installed v${localVersion}, latest v${latestVersion}). ` + + `Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` + + `Update now? It runs \`npx impeccable skills update\`." ` + + `If they agree, run \`npx impeccable skills update\` (the update applies to the next session, not this one). ` + + `Either way, continue the current task without waiting, and do not raise this again.` + ); +} + +/** + * Best-effort update directive for the boot output. Returns a string to append + * or null. Polls the version endpoint at most once per day (cached globally in + * the user's home dir) and re-surfaces a given version at most once per week so + * the agent never nags. Opt out entirely with IMPECCABLE_NO_UPDATE_CHECK=1. + */ +async function computeUpdateDirective(now = Date.now()) { + try { + if (process.env.IMPECCABLE_NO_UPDATE_CHECK) return null; + const localVersion = readLocalSkillVersion(); + if (!localVersion) return null; + + const cache = readUpdateCache(); + + // Poll the network only when the throttle window has elapsed. Stamp + // lastCheck even on failure so an offline machine doesn't poll every boot. + if (!cache.lastCheck || now - cache.lastCheck > CHECK_INTERVAL_MS) { + const latest = await fetchLatestSkillVersion(); + cache.lastCheck = now; + if (latest) cache.latestVersion = latest; + writeUpdateCache(cache); + } + + const latest = cache.latestVersion; + if (!latest || compareSemver(latest, localVersion) <= 0) return null; + + // Anti-nag: surface a given version at most once per RENOTIFY window. + if (cache.notifiedVersion === latest && cache.notifiedAt && now - cache.notifiedAt < RENOTIFY_INTERVAL_MS) { + return null; + } + cache.notifiedVersion = latest; + cache.notifiedAt = now; + writeUpdateCache(cache); + + return buildUpdateDirective(localVersion, latest); + } catch { + return null; + } +} + +async function cli() { + const ctx = loadContext(process.cwd()); + const updateDirective = await computeUpdateDirective(); + + if (!ctx.hasProduct) { + // Direct stdout message instead of relying on empty output as a signal + // — cheap models miss the empty case more often than the explicit one. + const parts = [ + 'NO_PRODUCT_MD: This project has no PRODUCT.md yet. ' + + 'Stop the current task, load reference/init.md, and follow its ' + + 'instructions to write PRODUCT.md before resuming.', + ]; + if (updateDirective) parts.push(updateDirective); + process.stdout.write(parts.join('\n\n---\n\n') + '\n'); + process.exit(0); + } + const parts = [`# PRODUCT.md\n\n${ctx.product.trim()}`]; + if (ctx.hasDesign) { + parts.push(`# DESIGN.md\n\n${ctx.design.trim()}`); + } + const register = extractRegister(ctx.product); + const next = register + ? `NEXT STEP: This project's register is \`${register}\`. You MUST now read \`reference/${register}.md\` before producing any design output.` + : `NEXT STEP: You MUST now read the matching register reference (\`reference/brand.md\` or \`reference/product.md\`) before producing any design output. Pick based on PRODUCT.md above.`; + parts.push(next); + if (updateDirective) parts.push(updateDirective); + process.stdout.write(parts.join('\n\n---\n\n') + '\n'); +} + +const _running = process.argv[1]; +if (_running?.endsWith('context.mjs') || _running?.endsWith('context.mjs/')) { + cli(); +} diff --git a/.kiro/skills/impeccable/scripts/design-parser.mjs b/.kiro/skills/impeccable/scripts/design-parser.mjs index b68756640..82a5cd7dc 100644 --- a/.kiro/skills/impeccable/scripts/design-parser.mjs +++ b/.kiro/skills/impeccable/scripts/design-parser.mjs @@ -63,7 +63,7 @@ function parseYamlSubset(yaml) { } const key = content.slice(0, colonIdx).trim(); - const rest = content.slice(colonIdx + 1).trim(); + const rest = stripInlineYamlComment(content.slice(colonIdx + 1).trim()); const parent = stack[stack.length - 1].obj; if (rest === '') { @@ -93,6 +93,21 @@ function findTopLevelColon(s) { return -1; } +function stripInlineYamlComment(s) { + let inQuote = null; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (inQuote) { + if (ch === inQuote && s[i - 1] !== '\\') inQuote = null; + } else if (ch === '"' || ch === "'") { + inQuote = ch; + } else if (ch === '#' && i > 0 && /\s/.test(s[i - 1])) { + return s.slice(0, i).trimEnd(); + } + } + return s; +} + function parseScalar(raw) { const s = raw.trim(); if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { diff --git a/.kiro/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.kiro/skills/impeccable/scripts/detector/browser/injected/index.mjs index c03a69caf..ceff16737 100644 --- a/.kiro/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/.kiro/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -9,8 +9,21 @@ if (IS_BROWSER) { const EXTENSION_MODE = (_myScript && _myScript.dataset.impeccableExtension === 'true') || document.documentElement.dataset.impeccableExtension === 'true'; - const BRAND_COLOR = 'oklch(55% 0.25 350)'; - const BRAND_COLOR_HOVER = 'oklch(45% 0.25 350)'; + // Kinpaku gold — pinned to the site's brand token (see + // site/styles/kinpaku-tokens.css --ks-kinpaku). Keep this in sync with + // the picker's C.brand in skill/scripts/live-browser.js and the kit's + // picker section in site/styles/kinpaku-kit.css. + // + // One color across both light and dark host pages. The outline is a + // 2px gesture pointing at an element + a labeled tag — it's a marker, + // not body text, so it doesn't need WCAG AA against the page. The + // label text inside the gold tag is dark (LABEL_INK) which has ~16:1 + // against the leaf gold, so reading the rule name is solid in both + // modes. Hover deepens the gold (preserves chroma — never drops it, + // dropping chroma washes the gold into a sand/olive tone). + const BRAND_COLOR = 'oklch(84% 0.19 80.46)'; + const BRAND_COLOR_HOVER = 'oklch(74% 0.18 80)'; + const LABEL_INK = 'oklch(4% 0.004 95)'; const LABEL_BG = BRAND_COLOR; const OUTLINE_COLOR = BRAND_COLOR; @@ -278,7 +291,7 @@ if (IS_BROWSER) { display: 'flex', alignItems: 'center', whiteSpace: 'nowrap', fontSize: '11px', fontWeight: '600', letterSpacing: '0.02em', - color: 'white', lineHeight: '14px', + color: LABEL_INK, lineHeight: '14px', background: LABEL_BG, fontFamily: 'system-ui, sans-serif', borderRadius: '4px 4px 0 0', @@ -398,7 +411,7 @@ if (IS_BROWSER) { banner.className = 'impeccable-overlay impeccable-banner'; Object.assign(banner.style, { position: 'fixed', top: '0', left: '0', right: '0', zIndex: '100000', - background: LABEL_BG, color: 'white', + background: LABEL_BG, color: LABEL_INK, fontFamily: 'system-ui, sans-serif', fontSize: '13px', display: 'flex', alignItems: 'center', pointerEvents: 'auto', height: '36px', overflow: 'hidden', maxWidth: '100vw', @@ -1223,12 +1236,12 @@ if (IS_BROWSER) { } console.group( `%c[impeccable] ${allFindings.length} anti-pattern${allFindings.length === 1 ? '' : 's'} found`, - 'color: oklch(60% 0.25 350); font-weight: bold' + 'color: oklch(84% 0.19 80.46); font-weight: bold' ); for (const { el, findings } of allFindings) { for (const f of findings) { console.log(`%c${f.type || f.id}%c ${f.detail || f.snippet}`, - 'color: oklch(55% 0.25 350); font-weight: bold', 'color: inherit', el); + 'color: oklch(84% 0.19 80.46); font-weight: bold', 'color: inherit', el); } } console.groupEnd(); @@ -1249,6 +1262,10 @@ if (IS_BROWSER) { const groupMap = new Map(); const _disabled = EXTENSION_MODE ? (window.__IMPECCABLE_CONFIG__?.disabledRules || []) : []; const _ruleOk = (id) => !_disabled.length || !_disabled.includes(id); + // Note: provider-gated rules (--gpt / --gemini) are NOT filtered here. In a + // real browser env (detector page, live overlay, extension) running every + // check is free, so we always surface them; the gating is purely a CLI + // output concern, applied in the Node engines' detect* return paths. for (const el of document.querySelectorAll('*')) { // Skip impeccable's own elements and any descendants (overlays, labels, banner, nav buttons) @@ -1270,11 +1287,23 @@ if (IS_BROWSER) { ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), - ...checkElementHeroEyebrowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementOversizedH1DOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementClippedOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementGptBorderShadowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementTextOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ].filter(f => _ruleOk(f.type)); addBrowserFindings(groupMap, el, findings); + + // Hero eyebrow: the offending element is the eyebrow above the heading, + // not the heading itself — highlight the previous sibling instead. + const eyebrowFindings = checkElementHeroEyebrowDOM(el) + .map(f => ({ type: f.id, detail: f.snippet })) + .filter(f => _ruleOk(f.type)); + if (eyebrowFindings.length > 0 && el.previousElementSibling) { + addBrowserFindings(groupMap, el.previousElementSibling, eyebrowFindings); + } } const pageLevelFindings = []; @@ -1306,6 +1335,14 @@ if (IS_BROWSER) { addBrowserFindings(groupMap, document.body, qualityFindings); } + const creamFindings = checkCreamPalette(document) + .map(f => ({ type: f.id, detail: f.snippet })) + .filter(f => _ruleOk(f.type)); + if (creamFindings.length > 0) { + pageLevelFindings.push(...creamFindings); + addBrowserFindings(groupMap, document.body, creamFindings); + } + // Regex-on-HTML checks (shared with Node) // Clone the document and strip impeccable-live overlay nodes before the // regex scan, so the inspector's own inline styles (transitions on top/ diff --git a/.kiro/skills/impeccable/scripts/detector/cli/main.mjs b/.kiro/skills/impeccable/scripts/detector/cli/main.mjs index 76da09f0c..94283e01b 100644 --- a/.kiro/skills/impeccable/scripts/detector/cli/main.mjs +++ b/.kiro/skills/impeccable/scripts/detector/cli/main.mjs @@ -41,7 +41,7 @@ function formatFindings(findings, jsonMode) { // Stdin handling // --------------------------------------------------------------------------- -async function handleStdin() { +async function handleStdin(options = {}) { const chunks = []; for await (const chunk of process.stdin) chunks.push(chunk); const input = Buffer.concat(chunks).toString('utf-8'); @@ -50,10 +50,10 @@ async function handleStdin() { const fp = parsed?.tool_input?.file_path; if (fp && fs.existsSync(fp)) { return HTML_EXTENSIONS.has(path.extname(fp).toLowerCase()) - ? detectHtml(fp) : detectText(fs.readFileSync(fp, 'utf-8'), fp); + ? detectHtml(fp, options) : detectText(fs.readFileSync(fp, 'utf-8'), fp, options); } } catch { /* not JSON */ } - return detectText(input, ''); + return detectText(input, '', options); } @@ -81,6 +81,8 @@ Scan files or URLs for UI anti-patterns and design quality issues. Options: --fast Regex-only mode (skip static HTML/CSS analysis, faster but misses linked stylesheets) --json Output results as JSON + --gpt Also report GPT-specific provider tells (off by default) + --gemini Also report Gemini-specific provider tells (off by default) --help Show this help message Detection modes: @@ -106,6 +108,10 @@ async function detectCli() { const jsonMode = args.includes('--json'); const helpMode = args.includes('--help'); const fastMode = args.includes('--fast'); + const providers = []; + if (args.includes('--gpt')) providers.push('gpt'); + if (args.includes('--gemini')) providers.push('gemini'); + const scanOptions = { providers }; const targets = args.filter(a => !a.startsWith('--')); if (helpMode) { printUsage(); process.exit(0); } @@ -113,7 +119,7 @@ async function detectCli() { let allFindings = []; if (!process.stdin.isTTY && targets.length === 0) { - allFindings = await handleStdin(); + allFindings = await handleStdin(scanOptions); } else { const paths = targets.length > 0 ? targets : [process.cwd()]; const urlTargetCount = paths.filter(target => /^https?:\/\//i.test(target)).length; @@ -124,8 +130,8 @@ async function detectCli() { if (/^https?:\/\//i.test(target)) { try { const scanner = browserDetector - ? (url) => browserDetector.detectUrl(url) - : (url) => detectUrl(url); + ? (url) => browserDetector.detectUrl(url, scanOptions) + : (url) => detectUrl(url, scanOptions); allFindings.push(...await scanner(target)); } catch (e) { process.stderr.write(`Error: ${e.message}\n`); } continue; @@ -192,9 +198,9 @@ async function detectCli() { const ext = path.extname(file).toLowerCase(); let fileFindings; if (!fastMode && HTML_EXTENSIONS.has(ext)) { - fileFindings = await detectHtml(file); + fileFindings = await detectHtml(file, scanOptions); } else { - fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file); + fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file, scanOptions); } // Annotate findings with import context const importers = importedByMap.get(file); @@ -209,9 +215,9 @@ async function detectCli() { } else if (stat.isFile()) { const ext = path.extname(resolved).toLowerCase(); if (!fastMode && HTML_EXTENSIONS.has(ext)) { - allFindings.push(...await detectHtml(resolved)); + allFindings.push(...await detectHtml(resolved, scanOptions)); } else { - allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved)); + allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved, scanOptions)); } } } diff --git a/.kiro/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.kiro/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 1afbe3f12..88439a5c3 100644 --- a/.kiro/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.kiro/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -38,7 +38,7 @@ const OVERUSED_FONTS = new Set([ // Older monoculture (still ubiquitous): 'inter', 'roboto', 'open sans', 'lato', 'montserrat', 'arial', 'helvetica', // Newer monoculture (the Anthropic-skill / Vercel / GitHub default wave): - 'fraunces', 'instrument sans', + 'fraunces', 'instrument sans', 'instrument serif', 'geist', 'geist sans', 'geist mono', 'mona sans', 'plus jakarta sans', 'space grotesk', 'recoleta', @@ -165,6 +165,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'AI color palette', }, + { + id: 'cream-palette', + category: 'slop', + name: 'Cream / beige palette', + description: + 'A warm cream or beige page background has become the default "tasteful" AI surface, reached for by reflex. Choose a background that comes from a deliberate palette, not the safe warm off-white.', + skillSection: 'Color & Contrast', + skillGuideline: 'cream and beige as the default surface', + }, { id: 'nested-cards', category: 'slop', @@ -183,15 +192,6 @@ const ANTIPATTERNS = [ skillSection: 'Layout & Space', skillGuideline: 'same spacing everywhere', }, - { - id: 'everything-centered', - category: 'slop', - name: 'Everything centered', - description: - 'Every text element is center-aligned. Left-aligned text with asymmetric layouts feels more designed. Center only hero sections and CTAs.', - skillSection: 'Layout & Space', - skillGuideline: 'Center everything', - }, { id: 'bounce-easing', category: 'slop', @@ -247,17 +247,72 @@ const ANTIPATTERNS = [ skillSection: 'Typography', skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding', }, + { + id: 'numbered-section-markers', + category: 'slop', + severity: 'advisory', + name: 'Numbered section markers (01 / 02 / 03)', + description: + 'Numbered display markers as section labels (01, 02, 03) are the AI editorial scaffold one tier deeper than tracked eyebrow chips. If you find yourself reaching for them, choose a different section cadence.', + skillSection: 'Layout & Space', + skillGuideline: 'numbered section markers', + }, + { + id: 'em-dash-overuse', + category: 'slop', + name: 'Em-dash overuse', + description: + 'More than two em-dashes (— or --) in body copy is an AI cadence tell. Use commas, colons, periods, or parentheses instead.', + skillSection: 'Copy', + skillGuideline: 'no em dashes', + }, + { + id: 'marketing-buzzword', + category: 'slop', + name: 'Marketing buzzword', + description: + 'Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does.', + skillSection: 'Copy', + skillGuideline: 'marketing buzzwords', + }, + { + id: 'aphoristic-cadence', + category: 'slop', + name: 'Aphoristic-cadence copy', + description: + 'Three or more sections landing on a short rebuttal sentence ("X. No Y." / "X. Just Y.") or a manufactured-contrast aphorism ("Not a feature. A platform.") reads as AI cadence, not voice. Once is fine; the pattern is the tell.', + skillSection: 'Copy', + skillGuideline: 'aphoristic cadence', + }, + { + id: 'oversized-h1', + category: 'slop', + name: 'Oversized hero headline', + description: + 'A full-sentence headline set at display size ends up dominating the viewport, leaving no room for anything else above the fold. A punchy one- or two-word headline at that size is fine — the problem is a long headline blown up too large. Set long headlines smaller, or tighten the copy.', + skillSection: 'Typography', + skillGuideline: 'long headline set at display size', + }, + { + id: 'extreme-negative-tracking', + category: 'slop', + name: 'Crushed letter spacing', + description: + 'Letter-spacing pulled tighter than the point where characters keep their own shapes costs legibility. Tighten display type optically, not destructively.', + skillSection: 'Typography', + skillGuideline: 'letter spacing crushed past legibility', + }, + { + id: 'broken-image', + category: 'quality', + name: 'Broken or placeholder image', + description: + ' tags with empty src, missing src, or placeholder values ship as broken-image boxes. Use real images, generated assets, or remove the tag.', + skillSection: 'Imagery', + skillGuideline: 'broken image references', + }, // ── Quality: general design and accessibility issues ── - { - id: 'pure-black-white', - category: 'quality', - name: 'Pure black background', - description: - 'Pure #000000 as a background color looks harsh and unnatural. Tint it slightly toward your brand hue (e.g., oklch(12% 0.01 250)) for a more refined feel.', - skillSection: 'Color & Contrast', - skillGuideline: 'pure black (#000)', - }, { id: 'gray-on-color', category: 'quality', @@ -297,7 +352,9 @@ const ANTIPATTERNS = [ category: 'quality', name: 'Cramped padding', description: - 'Text is too close to the edge of its container. Add at least 8px (ideally 12-16px) of padding inside bordered or colored containers.', + 'Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.', + skillSection: 'Layout & Space', + skillGuideline: 'inside bordered or colored containers', }, { id: 'body-text-viewport-edge', @@ -350,6 +407,70 @@ const ANTIPATTERNS = [ description: 'Letter spacing above 0.05em on body text disrupts natural character groupings and slows reading. Reserve wide tracking for short uppercase labels only.', }, + { + id: 'text-overflow', + category: 'quality', + name: 'Content overflowing its container', + description: + 'Content renders wider than its container, spilling out or forcing a horizontal scrollbar. Let text wrap, constrain widths, or give the region a deliberate scroll affordance.', + skillSection: 'Layout & Space', + skillGuideline: 'content wider than its container', + }, + { + id: 'clipped-overflow-container', + category: 'quality', + name: 'Positioned child clipped by overflow container', + description: + 'A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.', + skillSection: 'Layout & Space', + skillGuideline: 'overflow container clipping positioned children', + }, + + // ── Provider tells: opt-in via --gpt / --gemini (gated off by default) ── + { + id: 'gpt-thin-border-wide-shadow', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Hairline border with wide shadow', + description: + 'A hairline border paired with a wide, diffuse shadow is a recurring generated-UI signature. Commit to one — a defined edge or a soft elevation — rather than both at once.', + skillSection: 'Visual Details', + skillGuideline: 'hairline border plus wide diffuse shadow', + }, + { + id: 'repeating-stripes-gradient', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Repeating-gradient stripes', + description: + 'Repeating-gradient stripes used as surface decoration are a recurring generated-UI signature. Reach for a deliberate texture or leave the surface plain.', + skillSection: 'Visual Details', + skillGuideline: 'repeating-gradient decorative stripes', + }, + { + id: 'theater-slop-phrase', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Theater framing copy', + description: + 'Dismissing something as "theater" is a recurring generated-copy tic. Say plainly what the thing does or does not do.', + skillSection: 'Copy', + skillGuideline: 'theater framing copy', + }, + { + id: 'image-hover-transform', + category: 'slop', + severity: 'advisory', + gated: 'gemini', + name: 'Image hover transform', + description: + 'Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction.', + skillSection: 'Motion', + skillGuideline: 'image scale or rotate on hover', + }, ]; // --- cli/engine/shared/color.mjs --- @@ -527,11 +648,6 @@ function checkColors(opts) { } const findings = []; - // Pure black background (only solid or near-solid, not semi-transparent overlays) - if (bgColor && bgColor.a >= 0.9 && bgColor.r === 0 && bgColor.g === 0 && bgColor.b === 0) { - findings.push({ id: 'pure-black-white', snippet: '#000000 background' }); - } - if (hasDirectText && textColor && !isEmojiOnly) { // Run background-dependent checks against either a solid bg or, if the // ancestor is a gradient, against every gradient stop (use the worst case). @@ -587,9 +703,6 @@ function checkColors(opts) { // Tailwind class checks if (classList) { const classStr = typeof classList === 'string' ? classList : Array.from(classList).join(' '); - if (/\bbg-black\b(?!\/)/.test(classStr)) { - findings.push({ id: 'pure-black-white', snippet: 'bg-black' }); - } const grayMatch = classStr.match(/\btext-(?:gray|slate|zinc|neutral|stone)-\d+\b/); const colorBgMatch = classStr.match(/\bbg-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+\b/); @@ -905,12 +1018,6 @@ function checkHtmlPatterns(html) { // --- Color --- - // Pure black background - const pureBlackBgRe = /background(?:-color)?\s*:\s*(?:#000000|#000|rgb\(\s*0,\s*0,\s*0\s*\))\b/gi; - if (pureBlackBgRe.test(html)) { - findings.push({ id: 'pure-black-white', snippet: 'Pure #000 background' }); - } - // AI color palette: purple/violet const purpleHexRe = /#(?:7c3aed|8b5cf6|a855f7|9333ea|7e22ce|6d28d9|6366f1|764ba2|667eea)\b/gi; if (purpleHexRe.test(html)) { @@ -1027,6 +1134,39 @@ function checkHtmlPatterns(html) { } } + // --- Provider tells (gated): repeating-gradient stripes (GPT) --- + if (/repeating-(?:linear|radial|conic)-gradient\s*\(/i.test(html)) { + findings.push({ id: 'repeating-stripes-gradient', snippet: 'repeating-gradient decorative stripes' }); + } + + // --- Provider tells (gated): "X theater" framing copy (GPT) --- + // Lives here (regex-on-HTML) rather than in the text-content analyzers so it + // runs in the bundled browser path too, not just the CLI/static path. + { + const bodyText = html + .replace(/]*>[\s\S]*?<\/script>/gi, ' ') + .replace(/]*>[\s\S]*?<\/style>/gi, ' ') + .replace(/<[^>]+>/g, ' '); + const tm = /\b(\w+)\s+theater\b/i.exec(bodyText); + if (tm) findings.push({ id: 'theater-slop-phrase', snippet: `"${tm[0].trim()}"` }); + } + + // --- Provider tells (gated): image hover transform (Gemini) --- + // A CSS `img...:hover { transform: ... }` rule, or a Tailwind hover:scale / + // hover:rotate / hover:translate utility on an . Each distinct + // mechanism is its own finding. + const imgHoverCss = /\bimg\b[^,{}]*:hover\b[^{}]*\{[^}]*\btransform\s*:\s*(?:scale|rotate|translate|matrix|skew)/i; + if (imgHoverCss.test(html)) { + findings.push({ id: 'image-hover-transform', snippet: 'img:hover { transform } rule' }); + } + const imgTagRe = /]*\bclass\s*=\s*"([^"]*)"/gi; + let im; + while ((im = imgTagRe.exec(html)) !== null) { + if (/\bhover:(?:scale|rotate|translate|skew)-/.test(im[1])) { + findings.push({ id: 'image-hover-transform', snippet: 'Tailwind hover transform on ' }); + } + } + return findings; } @@ -1673,7 +1813,7 @@ function resolveLengthPx(value, fontSizePx) { // Both adapters resolve font-size, line-height and letter-spacing to pixels // before calling this so the pure function only deals with numbers. function checkQuality(opts) { - const { el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax = 80, viewportWidth = 0 } = opts; + const { el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax = 80, viewportWidth = 0, win = null } = opts; const findings = []; // Skip browser extension injected elements const elId = el.id || ''; @@ -1724,6 +1864,155 @@ function checkQuality(opts) { } } + // --- Flush against a visible boundary --- + // Fires when a container has a visible boundary (border, outline, OR a + // non-transparent background) AND near-zero padding on the bounded + // side(s) AND text-bearing children land flush against the boundary. + // + // Distinct from cramped-padding: that rule needs the element itself to + // have direct text (hasDirectText). This rule targets the OPPOSITE + // shape — a container with NO direct text, only children — which is + // exactly what cramped-padding misses (a section wrapping a label + + // list lands a free pass). + // + // The classic shape: agent writes `padding: 28px 0 0` shorthand on a + // section that also has a border, zeroing horizontal padding so the + // text-bearing children touch the side borders. Background and + // outline count too: a colored card with zero padding has the same + // visual failure mode. + { + const FLUSH_SKIP_TAGS = new Set(['HTML', 'BODY', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ARTICLE', 'ASIDE', 'BUTTON', 'A', 'LABEL', 'SUMMARY', 'CODE', 'PRE', 'INPUT', 'TEXTAREA', 'SELECT', 'FORM', 'FIGURE', 'TABLE', 'TBODY', 'THEAD', 'TR', 'TD', 'TH']); + const upperTag = tag ? tag.toUpperCase() : ''; + const elPosition = style.position || ''; + if ( + !FLUSH_SKIP_TAGS.has(upperTag) && + !hasDirectText && + !['fixed', 'absolute'].includes(elPosition) && + el.children && el.children.length > 0 + ) { + const isTransparent = (c) => + !c || c === 'transparent' || c === 'rgba(0, 0, 0, 0)' || + /^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*0(?:\.0+)?\s*\)$/.test(c); + + const borderW = { + top: parseFloat(style.borderTopWidth) || 0, + right: parseFloat(style.borderRightWidth) || 0, + bottom: parseFloat(style.borderBottomWidth) || 0, + left: parseFloat(style.borderLeftWidth) || 0, + }; + const borderVisible = { + top: borderW.top > 0 && !isTransparent(style.borderTopColor), + right: borderW.right > 0 && !isTransparent(style.borderRightColor), + bottom: borderW.bottom > 0 && !isTransparent(style.borderBottomColor), + left: borderW.left > 0 && !isTransparent(style.borderLeftColor), + }; + // Outline detection. jsdom decomposes `border` shorthand into + // border{Top,…}Width/Color but does NOT decompose `outline` — + // the longhands come back empty when the value was set via the + // shorthand. Fall back to parsing `style.outline` ourselves. + let outlineW = parseFloat(style.outlineWidth) || 0; + let outlineStyleVal = style.outlineStyle || ''; + let outlineColorVal = style.outlineColor || ''; + if (!outlineW && style.outline) { + const wMatch = style.outline.match(/(\d+(?:\.\d+)?)\s*px/); + if (wMatch) outlineW = parseFloat(wMatch[1]) || 0; + if (!outlineStyleVal) { + outlineStyleVal = /\b(solid|dashed|dotted|double|groove|ridge|inset|outset)\b/.test(style.outline) ? 'solid' : ''; + } + if (!outlineColorVal) { + const cMatch = style.outline.match(/(rgba?\([^)]+\)|#[0-9a-fA-F]{3,8}|[a-zA-Z]+)\s*$/); + if (cMatch) outlineColorVal = cMatch[1]; + } + } + const outlineVisible = outlineW > 0 && !isTransparent(outlineColorVal) && outlineStyleVal && outlineStyleVal !== 'none'; + const bgVisible = !isTransparent(style.backgroundColor); + + const anyVisible = borderVisible.top || borderVisible.right || borderVisible.bottom || borderVisible.left || outlineVisible || bgVisible; + if (anyVisible) { + // Resolve padding to px (jsdom returns raw "1.5rem" etc., not the + // computed px value; parseFloat would strip the unit and treat + // 1.5rem as 1.5px, false-flagging legitimate insets). + const pad = { + top: resolveLengthPx(style.paddingTop, fontSize) ?? 0, + right: resolveLengthPx(style.paddingRight, fontSize) ?? 0, + bottom: resolveLengthPx(style.paddingBottom, fontSize) ?? 0, + left: resolveLengthPx(style.paddingLeft, fontSize) ?? 0, + }; + const PAD_THRESHOLD = 2; + // Children-insulate-this-side: a side is insulated if ANY direct + // child has its own padding ≥ 4px on that side. Rationale: in + // typical flow, only the first/last (or leftmost/rightmost) + // children actually sit at the parent's edges. If even one of + // them has its own padding, the visual flush is broken on that + // side. Classic example: a column-flow card frame where the + // top child (header) has padding-top:12 and the bottom child + // (footer) has padding-bottom:8 — the parent's padding:0 doesn't + // matter; nothing is actually flush. The `any-child-insulates` + // heuristic accepts some false negatives (a card with one heavily + // padded middle child won't flag) for far fewer false positives. + const CHILD_INSULATE_THRESHOLD = 4; + const childrenInsulate = { top: false, right: false, bottom: false, left: false }; + for (const child of el.children) { + let childStyle = null; + if (win && typeof win.getComputedStyle === 'function') { + try { childStyle = win.getComputedStyle(child); } catch {} + } + if (!childStyle && typeof getComputedStyle === 'function') { + try { childStyle = getComputedStyle(child); } catch {} + } + if (!childStyle) continue; + const childPad = { + top: resolveLengthPx(childStyle.paddingTop, fontSize) ?? 0, + right: resolveLengthPx(childStyle.paddingRight, fontSize) ?? 0, + bottom: resolveLengthPx(childStyle.paddingBottom, fontSize) ?? 0, + left: resolveLengthPx(childStyle.paddingLeft, fontSize) ?? 0, + }; + for (const s of ['top', 'right', 'bottom', 'left']) { + if (childPad[s] >= CHILD_INSULATE_THRESHOLD) childrenInsulate[s] = true; + } + } + + const flushSides = []; + for (const side of ['top', 'right', 'bottom', 'left']) { + const sideBounded = borderVisible[side] || outlineVisible || bgVisible; + if (sideBounded && pad[side] <= PAD_THRESHOLD && !childrenInsulate[side]) { + flushSides.push(side); + } + } + + if (flushSides.length > 0) { + // Confirm at least one direct child has substantial text content + // (> 4 chars). Without this, the flush is harmless: e.g. an + // image-only card. + let hasTextChild = false; + for (const child of el.children) { + const childText = (child.textContent || '').trim(); + if (childText.length > 4) { hasTextChild = true; break; } + } + if (hasTextChild) { + const cls = (typeof el.className === 'string' && el.className.trim()) + ? el.className.trim().split(/\s+/)[0] + : ''; + const boundaryParts = []; + const borderSidesVisible = ['top', 'right', 'bottom', 'left'].filter(s => borderVisible[s]); + if (borderSidesVisible.length === 4) boundaryParts.push('border'); + else if (borderSidesVisible.length > 0) boundaryParts.push(`border-${borderSidesVisible.join('/')}`); + if (outlineVisible) boundaryParts.push('outline'); + if (bgVisible) boundaryParts.push('bg'); + const sidesLabel = flushSides.length === 4 ? 'all sides' : flushSides.join('/'); + const ident = cls + ? `<${tag.toLowerCase()}> "${cls}"` + : `<${tag.toLowerCase()}>`; + findings.push({ + id: 'cramped-padding', + snippet: `${ident}: children flush against ${boundaryParts.join('+')} on ${sidesLabel} (no inset)`, + }); + } + } + } + } + } + // --- Body text touching viewport edge --- (browser-only: needs rect) // Catches the failure mode where the agent ships body paragraphs // with NO container providing horizontal padding — text bleeds @@ -1804,6 +2093,20 @@ function checkQuality(opts) { } } + // --- Crushed letter spacing (mirror of wide-tracking) --- + // Tracking pulled tighter than ~-0.05em crushes characters into each other. + // Optical tightening that display type legitimately wants (around -0.02em) + // stays well above this floor. + if (hasDirectText && textLen > 20 && fontSize > 0) { + if (letterSpacingPx != null && letterSpacingPx < 0) { + const trackingEm = letterSpacingPx / fontSize; + if (trackingEm <= -0.05) { + const excerpt = (el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 40); + findings.push({ id: 'extreme-negative-tracking', snippet: `letter-spacing: ${trackingEm.toFixed(2)}em — "${excerpt}"` }); + } + } + } + return findings; } @@ -1820,7 +2123,7 @@ function checkElementQualityDOM(el) { const rect = el.getBoundingClientRect(); const lineMax = (typeof window !== 'undefined' && window.__IMPECCABLE_CONFIG__?.lineLengthMax) || 80; const viewportWidth = (typeof window !== 'undefined' ? window.innerWidth : 0) || 0; - return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax, viewportWidth }); + return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax, viewportWidth, win: typeof window !== 'undefined' ? window : null }); } // Pure page-level skipped-heading walk. Takes a Document so it works in both @@ -1862,7 +2165,7 @@ function checkElementQuality(el, style, tag, window) { const fontSize = resolveFontSizePx(el, window); const lineHeightPx = resolveLengthPx(style.lineHeight, fontSize); const letterSpacingPx = resolveLengthPx(style.letterSpacing, fontSize); - return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect: null }); + return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect: null, win: window }); } function checkElementBorders(tag, style, overrides, resolvedRadius) { @@ -2303,40 +2606,213 @@ function checkPageLayout(doc, win) { } } - // Everything centered - const textEls = doc.querySelectorAll('h1, h2, h3, h4, h5, h6, p, li, div, button'); - let centeredCount = 0; - let totalText = 0; - for (const el of textEls) { - const hasDirectText = [...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim().length >= 3); - if (!hasDirectText) continue; - totalText++; - - let cur = el; - let isCentered = false; - while (cur && cur.nodeType === 1) { - const rawStyle = cur.getAttribute?.('style') || ''; - const cls = cur.getAttribute?.('class') || ''; - if (/text-align\s*:\s*center/i.test(rawStyle) || /\btext-center\b/.test(cls)) { - isCentered = true; - break; - } - if (cur.tagName === 'BODY') break; - cur = cur.parentElement; - } - if (isCentered) centeredCount++; - } - - if (totalText >= 5 && centeredCount / totalText > 0.7) { - findings.push({ - id: 'everything-centered', - snippet: `${centeredCount}/${totalText} text elements centered (${Math.round(centeredCount / totalText * 100)}%)`, - }); - } - return findings; } +// ─── Cream / beige palette (the default "tasteful" AI surface) ──────────────── +// A warm, lightly-tinted off-white page background — light, with R≥G≥B and a +// small warm tint (not white, not a strong color). The current reflex surface. +function isCreamColor(rgb) { + if (!rgb) return false; + const { r, g, b } = rgb; + if (Math.min(r, g, b) < 209) return false; // must be light + if (!(r >= g && g >= b)) return false; // warm ordering + const warmth = r - b; + return warmth >= 6 && warmth <= 48; // tinted, not white, not strong +} + +// Tailwind background utilities that render as a warm off-white surface. The +// static engine doesn't fetch Tailwind's CSS, so a `bg-amber-50` on +// resolves to nothing in computed style — catch it from the class list +// instead. Candidate tokens map to their actual Tailwind hex and are still +// filtered through isCreamColor, so neutral grays (stone) and over-saturated +// shades drop out on their own. +const TAILWIND_BG_HEX = { + 'bg-amber-50': '#fffbeb', 'bg-amber-100': '#fef3c7', + 'bg-orange-50': '#fff7ed', 'bg-orange-100': '#ffedd5', + 'bg-yellow-50': '#fefce8', + 'bg-stone-50': '#fafaf9', 'bg-stone-100': '#f5f5f4', 'bg-stone-200': '#e7e5e4', +}; + +function creamFromClassList(cls) { + if (!cls) return null; + // Arbitrary value: bg-[#f5f0e6] / bg-[rgb(245_240_230)] (underscores = spaces). + const arb = cls.match(/\bbg-\[([^\]]+)\]/); + if (arb && isCreamColor(parseAnyColor(arb[1].replace(/_/g, ' ')))) return `bg-[${arb[1]}]`; + // Named warm-light utilities. + for (const [tok, hex] of Object.entries(TAILWIND_BG_HEX)) { + if (new RegExp(`(^|\\s)${tok}($|\\s)`).test(cls) && isCreamColor(parseAnyColor(hex))) return tok; + } + return null; +} + +function checkCreamPalette(doc, win) { + const findings = []; + const body = doc.body || (doc.querySelector ? doc.querySelector('body') : null); + if (!body) return findings; + const html = doc.documentElement; + const getCS = (el) => (win ? win.getComputedStyle(el) : getComputedStyle(el)); + + // 1. Computed background — covers inline / close.', 'Prefix every preview selector with the matching [data-impeccable-variant="N"] selector.', 'Keep selectors anchored to the generated variant wrapper; do not rely on component CSS scoping for preview rules.', ], forbidden: [ 'Do not use @scope for this styleMode.', + 'Do not wrap style content in a JSX/TSX template literal ({` ... `}); that syntax is for .tsx/.jsx only.', + 'Do not put { immediately after the style opening tag; Astro parses { as expression syntax.', ], }; } @@ -629,4 +632,15 @@ if (_running?.endsWith('live-wrap.mjs') || _running?.endsWith('live-wrap.mjs/')) } // Test exports (used by tests/live-wrap.test.mjs) -export { buildSearchQueries, findElement, findClosingLine, detectCommentSyntax }; +export { + buildSearchQueries, + findElement, + findClosingLine, + detectCommentSyntax, + findAllElements, + filterByText, + findFileWithQuery, + detectStyleMode, + buildCssAuthoring, + buildCssSelectorPrefixExamples, +}; diff --git a/.kiro/skills/impeccable/scripts/live.mjs b/.kiro/skills/impeccable/scripts/live.mjs index cafb0eca9..8acd300ed 100644 --- a/.kiro/skills/impeccable/scripts/live.mjs +++ b/.kiro/skills/impeccable/scripts/live.mjs @@ -21,7 +21,7 @@ import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { loadContext } from './load-context.mjs'; +import { loadContext } from './context.mjs'; import { resolveFiles } from './live-inject.mjs'; import { readLiveServerInfo } from './impeccable-paths.mjs'; @@ -80,7 +80,7 @@ The agent should then: process.exit(1); } - // 4. Load PRODUCT.md + DESIGN.md context (auto-migrates legacy .impeccable.md) + // 4. Load PRODUCT.md + DESIGN.md context. const ctx = loadContext(process.cwd()); // 5. Compute drift-heal: compare resolved inject targets against the @@ -102,7 +102,6 @@ The agent should then: hasDesign: ctx.hasDesign, design: ctx.design, designPath: ctx.designPath, - migrated: ctx.migrated, }, null, 2)); } diff --git a/.kiro/skills/impeccable/scripts/load-context.mjs b/.kiro/skills/impeccable/scripts/load-context.mjs deleted file mode 100644 index dc340bf16..000000000 --- a/.kiro/skills/impeccable/scripts/load-context.mjs +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Shared context loader for every impeccable command that needs to know - * "who is this for" and "what does this look like". - * - * Input: project root (process.cwd()). - * - * Output (JSON to stdout): - * { - * hasProduct: boolean, // PRODUCT.md found (or auto-migrated) - * product: string | null, // PRODUCT.md contents - * productPath: string | null, // relative path - * hasDesign: boolean, // DESIGN.md found - * design: string | null, // DESIGN.md contents - * designPath: string | null, - * migrated: boolean, // true if we auto-renamed .impeccable.md -> PRODUCT.md - * contextDir: string, // absolute path of the directory the files were found in - * } - * - * Filename matching is case-insensitive for PRODUCT.md and DESIGN.md. The - * Google DESIGN.md convention is uppercase at repo root; Kiro-style and - * lowercase variants are also matched so users don't get punished for case. - * - * Lookup directory resolution (first match wins): - * 1. process.env.IMPECCABLE_CONTEXT_DIR (absolute or relative to cwd) - * 2. cwd, if PRODUCT.md / DESIGN.md / .impeccable.md is there (back-compat) - * 3. Auto-fallback subdirectories of cwd: .agents/context/, then docs/ - * 4. cwd as a default "no context found" location - * - * Legacy `.impeccable.md` -> PRODUCT.md migration only fires at cwd root; - * fallback directories are read-only as far as auto-rename is concerned. - */ - -import fs from 'node:fs'; -import path from 'node:path'; - -const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; -const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; -const LEGACY_NAMES = ['.impeccable.md']; -const FALLBACK_DIRS = ['.agents/context', 'docs']; - -/** - * Resolve the directory that holds PRODUCT.md / DESIGN.md for - * this project. Exported so other scripts (e.g. live-server.mjs) can read the - * design files from the same location the loader uses. - */ -export function resolveContextDir(cwd = process.cwd()) { - // 1. Explicit override - const envDir = process.env.IMPECCABLE_CONTEXT_DIR; - if (envDir && envDir.trim()) { - const trimmed = envDir.trim(); - return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); - } - - // 2. cwd wins if any canonical or legacy file is there. We check legacy too - // so the auto-migration path in loadContext stays predictable. - if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES, ...LEGACY_NAMES])) { - return cwd; - } - - // 3. Auto-fallback subdirs. Match if PRODUCT.md or DESIGN.md is present; - // legacy `.impeccable.md` does not pull the lookup into a fallback dir. - for (const rel of FALLBACK_DIRS) { - const candidate = path.resolve(cwd, rel); - if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { - return candidate; - } - } - - // 4. Nothing found — keep the historical "default to cwd" behaviour so the - // caller's `hasProduct === false` branch still fires the same way. - return cwd; -} - -export function loadContext(cwd = process.cwd()) { - let migrated = false; - const contextDir = resolveContextDir(cwd); - - // 1. Look for PRODUCT.md (case-insensitive) in the resolved dir - let productPath = firstExisting(contextDir, PRODUCT_NAMES); - - // 2. Legacy: if no PRODUCT.md but .impeccable.md exists at cwd root, rename - // it in place. We only migrate at the root — fallback dirs are read-only - // so we don't surprise users by mutating files under docs/ or .agents/. - if (!productPath && contextDir === cwd) { - const legacyPath = firstExisting(cwd, LEGACY_NAMES); - if (legacyPath) { - const newPath = path.join(cwd, 'PRODUCT.md'); - try { - fs.renameSync(legacyPath, newPath); - productPath = newPath; - migrated = true; - } catch { - // Rename failed (permissions, etc.) — fall back to reading legacy in place - productPath = legacyPath; - } - } - } - - // 3. DESIGN.md (case-insensitive) - const designPath = firstExisting(contextDir, DESIGN_NAMES); - - const product = productPath ? safeRead(productPath) : null; - const design = designPath ? safeRead(designPath) : null; - - return { - hasProduct: !!product, - product, - productPath: productPath ? path.relative(cwd, productPath) : null, - hasDesign: !!design, - design, - designPath: designPath ? path.relative(cwd, designPath) : null, - migrated, - contextDir, - }; -} - -function firstExisting(dir, names) { - for (const name of names) { - const abs = path.join(dir, name); - if (fs.existsSync(abs)) return abs; - } - return null; -} - -function safeRead(p) { - try { return fs.readFileSync(p, 'utf-8'); } catch { return null; } -} - -// --------------------------------------------------------------------------- -// CLI mode — print the context as JSON -// --------------------------------------------------------------------------- - -function cli() { - const result = loadContext(process.cwd()); - console.log(JSON.stringify(result, null, 2)); -} - -const _running = process.argv[1]; -if (_running?.endsWith('load-context.mjs') || _running?.endsWith('load-context.mjs/')) { - cli(); -} diff --git a/.kiro/skills/impeccable/scripts/palette.mjs b/.kiro/skills/impeccable/scripts/palette.mjs new file mode 100644 index 000000000..4c3f1751a --- /dev/null +++ b/.kiro/skills/impeccable/scripts/palette.mjs @@ -0,0 +1,633 @@ +#!/usr/bin/env node +/** + * Brand-seed picker. Returns one OKLCH seed color + the mood it most + * naturally evokes, and teaches the model how to compose a full palette + * around it. + * + * The seed is the brand's anchor color. The 5-role palette (bg, surface, + * ink, accent, muted) is composed by the caller at runtime using their + * judgment + the brief (PRODUCT.md / DESIGN.md / user prompt), NOT picked + * from a frozen 4-color preset. + * + * Why: 4-color frozen palettes drift toward safe defaults (warm-cream bg, + * complementary accent on near-white) regardless of brief. A single seed + + * the model's own composition lets the same seed produce a dark-mode jazz + * club or a light-mode hospitality brand depending on what the brief calls + * for. Tested empirically against curated 4-color palettes; seed approach + * wins on mood-fit in 3 of 5 cases and ties on the rest. + * + * Usage: + * node scripts/palette.mjs # pick at random + * node scripts/palette.mjs --id seed-021 # pick a specific seed + * node scripts/palette.mjs --from # hash to a seed (deterministic) + * + * Env vars: + * IMPECCABLE_PALETTE_SEED — same as --from; useful for the eval harness + * to make runs reproducible. + */ + +import crypto from 'node:crypto'; + +// Seeds are inlined (129 entries, hand-curated via a tinder review of +// ~400 candidates from ColorHunt + synthesis + Radix/brand/Pantone anchors). +// Each carries a mood + strategy the judging model produced — surfaced as +// hints, not commands; the brief still drives composition. +const SEEDS = [ + { id: "seed-200", oklch: [0.360, 0.137, 0.0], + mood: "Aesop apothecary shelf — oxblood bottle glass against linen, considered and unhurried", + strategy: "Seed is a deep desaturated red-brown that reads as brand ink itself; I push primary darker toward bottle-glass oxblood, pair with a pure white surface so the red does the work, and use a clear pale-blush accent that can carry dark text in pills." }, + { id: "seed-000", oklch: [0.400, 0.130, 0.0], + mood: "oxblood leather banquette in a 1940s steakhouse — low lamplight on dark wood and burgundy", + strategy: "Near-black bg with the faintest red undertone lets the oxblood primary glow like lamplit leather; warm cream ink and a brass accent complete the chophouse register." }, + { id: "seed-002", oklch: [0.450, 0.150, 0.0], + mood: "darkroom red light — analog photography, blood-warm safelight glow on chemical trays", + strategy: "Near-black surface with a deep oxblood primary lets the seed function like a safelight in a darkroom — the bg disappears so the red becomes the only emotional signal." }, + { id: "seed-003", oklch: [0.500, 0.194, 0.0], + mood: "darkroom safelight — the deep oxblood glow of analog photography, chemical and contemplative", + strategy: "Anchored the seed as primary against pure near-black so the red reads like a single illuminated bulb in a developing room, with cool desaturated ink to evoke silver gelatin print tones." }, + { id: "seed-004", oklch: [0.546, 0.204, 3.4], + mood: "midnight boudoir — velvet rose under low lamplight, perfumed and intimate", + strategy: "Near-black surface lets the rose seed glow like silk in shadow; a warm champagne accent provides the candle-flame counterpoint without breaking the hush." }, + { id: "seed-005", oklch: [0.550, 0.180, 0.0], + mood: "smoldering vermillion at dusk — the last red ember in a blacksmith's forge, iron-rich and quietly violent", + strategy: "Near-black gallery surround lets the seed read as glowing forged metal; ink stays warm-off-white, accent shifts to a hotter ember orange so the primary feels like cooling steel against a fresh strike." }, + { id: "seed-201", oklch: [0.647, 0.262, 0.3], + mood: "Figma plugin marketplace red — confident product-brand crimson, the kind a modern dev tool uses for a 'live' indicator or a primary CTA on a pristine docs page", + strategy: "Pure white surface lets a high-chroma crimson primary do all the brand work, paired with a hue-shifted warm coral accent for hierarchy without competing saturation" }, + { id: "seed-006", oklch: [0.650, 0.160, 0.0], + mood: "1960s Italian cinema — Technicolor lipstick red against a darkened theater", + strategy: "Pure near-black surface lets a saturated cinematic red and its warm peach accent perform like film light projected in a dark room — the brand colors carry the drama, the bg disappears." }, + { id: "seed-008", oklch: [0.520, 0.200, 10.4], + mood: "Negroni hour at a Milanese bar — bittersweet crimson, vermouth and amaro under low tungsten", + strategy: "Seed is a saturated red-crimson with cinematic weight, so I sit it on near-black to let the primary glow like backlit liquor, with a warmer amber accent acting as the citrus twist against the bitter red." }, + { id: "seed-010", oklch: [0.563, 0.223, 11.0], + mood: "Negroni hour on a Milan rooftop — bittersweet crimson, aperitivo light, polished restraint", + strategy: "Seed is a vivid carmine-red with strong chroma, so the surface gets out of the way (pure white) and lets the primary do the aperitivo work, with a cooled garnet accent for tension." }, + { id: "seed-202", oklch: [0.643, 0.247, 7.0], + mood: "Glossier brand pink — modern beauty editorial, confident and current", + strategy: "Pure white bg lets a saturated rose-red primary do all the brand work, paired with a deeper crimson accent for hierarchy — the Stripe/Glossier move where the color carries the mood." }, + { id: "seed-013", oklch: [0.400, 0.130, 20.0], + mood: "Tuscan cellar at dusk — aged terracotta, oxidized iron, the deep red of decanted Sangiovese", + strategy: "Black surface lets the oxblood seed and copper accent glow like firelight on cellar stone; brand colors carry all the warmth while the room recedes." }, + { id: "seed-014", oklch: [0.450, 0.150, 20.0], + mood: "smoldering tannery — oxblood leather, cured under low workshop light", + strategy: "Anchor the deep oxblood seed as primary against a near-black architectural ground, then lift with a single warm ember accent so the leather reads burnished rather than bloody." }, + { id: "seed-016", oklch: [0.550, 0.180, 20.0], + mood: "Negroni hour on a Roman terrace — bitter campari red, vermouth, late golden light spilling on white linen", + strategy: "Pure white surface lets the campari-red primary do all the emotional work, paired with a deeper oxblood accent for bittersweet depth — Italian aperitivo restraint, not warmth-washed." }, + { id: "seed-205", oklch: [0.634, 0.254, 17.6], + mood: "Aesop apothecary bottle — considered red-coral on a clinical white surface, the kind of brand restraint where one saturated object does all the work", + strategy: "Default A pure white surface lets a single coral-red primary carry the entire brand voice; accent shifts to a deeper oxblood for hierarchy without competing chroma." }, + { id: "seed-011", oklch: [0.639, 0.207, 13.5], + mood: "Aperitivo hour in Milan — Campari glow on a white marble bar, crisp and effervescent", + strategy: "Pure white gallery backdrop lets the Campari-red primary ring like a single bitter note; ink is near-black with a whisper of warmth, accent shifts to a deeper oxblood for hierarchy without competing hues." }, + { id: "seed-015", oklch: [0.527, 0.202, 22.7], + mood: "Negroni hour on a Milanese terrace — bittersweet vermillion, aperitivo glassware catching low sun", + strategy: "Seed becomes a saturated aperitivo-red primary against pure white so the color carries the bittersweet warmth alone, paired with a deep oxblood accent for typographic gravitas." }, + { id: "seed-023", oklch: [0.427, 0.175, 29.2], + mood: "blacksmith's forge at dusk — iron heated to ember red, the deep glow of oxidized metal and quenching oil", + strategy: "Pure black bg lets the seed's ember-red glow radiate like hot iron in a dark forge; accent shifts to a copper-amber to suggest scaling metal and sparks, while ink stays near-white for tool-precise legibility." }, + { id: "seed-206", oklch: [0.614, 0.234, 28.2], + mood: "Aesop apothecary bottle — considered red-orange on lab-white, calm utility with a single confident pigment", + strategy: "Pure white surface lets a saturated vermilion primary do all the brand work, paired with a deep oxblood accent for hierarchy without introducing a second hue family" }, + { id: "seed-029", oklch: [0.665, 0.222, 25.7], + mood: "Negroni hour at a Milanese bar — bittersweet orange-red liqueur catching late afternoon light on polished marble", + strategy: "Pure white surface lets the seed's vermilion read like Campari in a glass; a deeper oxblood accent provides the bitter depth, with neutral graphite ink keeping the editorial restraint of Italian design." }, + { id: "seed-022", oklch: [0.418, 0.155, 27.2], + mood: "Pompeiian red fresco — oxidized cinnabar on a museum wall, archaeological gravity", + strategy: "Pure black gallery surface lets the seed's iron-oxide red read as a lit artifact; accent shifts to an aged terracotta amber, so primary and accent form a fired-clay duet against neutral void." }, + { id: "seed-024", oklch: [0.464, 0.169, 26.9], + mood: "Mid-century darkroom under the safelight — developer trays, oxblood leather, the quiet patience of a print emerging", + strategy: "Seed becomes a deep oxblood primary; surface stays pure black so the red glows like a safelight, with a warmer ember accent for hierarchy" }, + { id: "seed-026", oklch: [0.489, 0.190, 28.3], + mood: "smoldering ember in a blacksmith's forge — iron-hot rust, soot, and controlled fire", + strategy: "Near-black soot background lets the seed's red-orange glow like heated metal; ink is bone-white, accent is a cooler tempered-steel orange that creates internal heat gradient with the primary." }, + { id: "seed-027", oklch: [0.568, 0.208, 27.1], + mood: "Sicilian blood orange at golden hour — citrus rind, terracotta, sun on stucco", + strategy: "Seed reads as vivid blood-orange — picked pure white surface so the citrus-red primary and a deep oxblood accent do all the emotional work, like a Loro Piana editorial spread." }, + { id: "seed-028", oklch: [0.591, 0.172, 24.0], + mood: "Sienna-fired ceramic studio at dusk — terracotta cooling on a wheel, hands still dusted with slip", + strategy: "Pure black stage lets the fired-clay primary glow like a kiln ember, with a deeper oxblood accent providing tonal weight rather than hue contrast — a monochrome warm-axis play." }, + { id: "seed-033", oklch: [0.544, 0.169, 31.3], + mood: "1960s Italian terracotta workshop — fired clay, espresso, late-afternoon Mediterranean dust", + strategy: "Pure black ground lets the seed's burnt-sienna primary glow like a lit kiln, with a deeper oxblood accent for restrained warmth tension — the brand carries the heat, the surface stays out." }, + { id: "seed-207", oklch: [0.564, 0.231, 29.1], + mood: "Aesop apothecary bottle — considered red oxide, the calm authority of a well-made object on a white shelf", + strategy: "Seed becomes the singular brand voice against pure white, with a deeper oxblood accent for hierarchy — the surface disappears so the red does all the speaking." }, + { id: "seed-035", oklch: [0.663, 0.153, 32.1], + mood: "Aesop apothecary bottle — clay-fired warmth, considered retail", + strategy: "Pure white surface lets the terracotta primary do the brand work, paired with a deep umber ink and a cooler clay accent for editorial tension." }, + { id: "seed-037", oklch: [0.590, 0.188, 35.8], + mood: "Aesop apothecary bottle — considered terracotta, herbalist restraint, the warmth comes from the glass not the room", + strategy: "Seed becomes a muted terracotta primary against pure white so the brand's warmth carries entirely through the color itself; accent shifts to a deeper umber for quiet hierarchy." }, + { id: "seed-038", oklch: [0.652, 0.229, 34.8], + mood: "blown-glass furnace at dusk — molten orange iron pulled from the kiln, a craftsman's signature heat", + strategy: "Pure black stage so the seed reads as live ember; primary holds the seed's heat, accent shifts to a brass-amber a hue-step away for a 1.7+ contrast pairing without leaving the fire." }, + { id: "seed-039", oklch: [0.653, 0.185, 33.5], + mood: "Aesop apothecary bottle — considered terracotta, quiet retail craft", + strategy: "Seed becomes a grounded clay primary against pure white, paired with a deeper umber accent so the warmth lives entirely in the brand marks, not the surface." }, + { id: "seed-167", oklch: [0.495, 0.134, 36.0], + mood: "Aesop apothecary shelf — burnished terracotta on clinical white, considered craft pharmacy", + strategy: "Treat the seed as a brand-carrying burnt-sienna against a pure paper-white surface so the warmth lives entirely in the primary, with a deep umber accent pulled along the same warm axis for typographic gravity." }, + { id: "seed-147", oklch: [0.500, 0.151, 40.0], + mood: "Aesop apothecary shelf — considered terracotta, pharmacy restraint, the brand color does the work against clinical white", + strategy: "Anchor the seed's burnt-sienna primary against a pure white surface so the rust speaks alone, with a deep umber ink and a cooler clay accent to give the palette product-brand discipline rather than environmental warmth." }, + { id: "seed-040", oklch: [0.660, 0.201, 40.0], + mood: "Aesop apothecary bottle — amber glass on a clean dispensary shelf, considered and clinical-warm", + strategy: "Seed becomes a burnt-amber primary against pure white so the bottle-glass color does the emotional work; accent shifts to a deep olive-bronze for the apothecary-label pairing." }, + { id: "seed-041", oklch: [0.673, 0.217, 38.6], + mood: "Aesop apothecary shelf — considered orange glass, clinical retail restraint", + strategy: "Pure white surface lets the burnt-orange primary do all the brand work, with a deep ink-brown for editorial gravity and a muted clay accent that reads as a sibling, not a contrast." }, + { id: "seed-042", oklch: [0.688, 0.133, 35.8], + mood: "Aesop apothecary shelf — terracotta glass, considered retail", + strategy: "Seed becomes a warm clay primary against pure white so the bottle-on-marble retail feel comes from the brand color alone; a deeper umber accent gives the label-print contrast." }, + { id: "seed-043", oklch: [0.781, 0.119, 38.1], + mood: "Aesop apothecary catalogue — considered terracotta, dermatological restraint, the warm color doing all the work against clinical white", + strategy: "Pure white surface lets the seed's warm clay tone read as the entire brand voice, paired with a deeper umber accent for hierarchy without competing with the primary's warmth." }, + { id: "seed-168", oklch: [0.400, 0.103, 50.0], + mood: "Aesop apothecary bottle — amber glass on a clinical white shelf, considered and pharmaceutical", + strategy: "Pure white surface lets the deep amber primary act like tinted glass against a clean shelf; accent is a muted clay that complements without competing, keeping the brand quiet and product-led." }, + { id: "seed-044", oklch: [0.568, 0.149, 45.9], + mood: "1970s desert highway at golden hour — sun-faded terracotta, denim dust, the warmth of a Polaroid pulled from a glovebox", + strategy: "Seed becomes a burnt-sienna primary against pure white so the terracotta does all the emotional work; a deep indigo accent acts as the denim shadow opposing the sun, creating the era's signature warm/cool tension without tinting the page." }, + { id: "seed-045", oklch: [0.607, 0.163, 47.7], + mood: "Aesop apothecary shelf — considered amber glass, clinical restraint, craft pharmacy", + strategy: "Pure white bg lets the burnt-amber primary do the apothecary work alone, paired with a deeper umber accent and graphite ink for editorial calm." }, + { id: "seed-046", oklch: [0.653, 0.175, 45.0], + mood: "Aesop apothecary shelf — considered amber glass, quiet luxury, restrained craft", + strategy: "Pure black backdrop lets the warm amber primary glow like backlit apothecary glass, with a deeper rust accent providing tonal depth in the same hue family — monochromatic warm against neutral void." }, + { id: "seed-047", oklch: [0.695, 0.205, 43.2], + mood: "Aesop apothecary label — sun-warmed amber glass on a clinical countertop, restrained botanical pharmacy", + strategy: "Pure white surface lets the burnt-amber primary and a deeper sienna accent do all the brand work, like an apothecary bottle photographed under daylight." }, + { id: "seed-051", oklch: [0.704, 0.189, 49.0], + mood: "blacksmith's forge at dusk — glowing iron, hammered copper, ember light against cooling steel", + strategy: "Pure near-black surface lets the seed's molten orange burn like heated metal; accent shifts to a deeper amber-red to suggest the cooling end of the same iron, while ink stays a clean off-white so type reads like chalk on slate." }, + { id: "seed-171", oklch: [0.550, 0.124, 60.0], + mood: "Klim Type Foundry specimen page — considered ochre on paper, design-school-honest", + strategy: "Seed becomes a muted ochre primary on pure white; accent is a deep ink-navy pulled across the wheel for editorial contrast without warmth-pooling in the bg" }, + { id: "seed-148", oklch: [0.650, 0.146, 60.0], + mood: "Klim-style editorial gold — late-afternoon paper light on a serif specimen sheet, considered and dry", + strategy: "Hold the seed's amber as primary on a pure white page so the gold reads as ink rather than atmosphere, and pair with a deep aubergine accent for typographic contrast." }, + { id: "seed-052", oklch: [0.700, 0.130, 60.0], + mood: "late-afternoon terracotta studio — sun-warmed clay, hands-on craft, the hour before dusk", + strategy: "Seed is a saturated amber-ochre with strong environmental association (ceramics, adobe, sunlit plaster), so I lean into Exception (a) with a faintly warm bone surface that reads as lime-washed wall, then deepen the seed slightly for primary and pair it with a fired-clay rust accent for hand-thrown warmth." }, + { id: "seed-053", oklch: [0.773, 0.157, 56.6], + mood: "late-summer apricot orchard at golden hour — sun-warmed fruit, considered Californian craft", + strategy: "Seed is a juicy mid-warm orange at daylight luminance — leaning optimistic/editorial, so pure white surface lets the apricot primary glow without muddying it; a deep wine accent provides the bite." }, + { id: "seed-149", oklch: [0.600, 0.124, 70.0], + mood: "1970s desert highway — late-afternoon amber light on chrome and asphalt", + strategy: "Anchor the amber seed as primary against pure black so the warm hue reads as headlight glow against night; a cooler dusk-mauve accent provides the complementary tension of horizon vs. sun." }, + { id: "seed-054", oklch: [0.740, 0.162, 68.1], + mood: "late-afternoon honey on terracotta — Mediterranean stucco at golden hour, sun-baked amber", + strategy: "Seed is a saturated honey-amber at high lightness; pairing it with pure black lets the warmth read as luminous gold against gravity, like lamplight in a dark room." }, + { id: "seed-055", oklch: [0.774, 0.174, 65.1], + mood: "late-summer honey hour — amber light slanting through a west-facing window, optimistic and golden", + strategy: "Anchor a saturated honey-amber primary on pure white so the warmth radiates from the brand itself, then pair with a deep teak accent for grounded contrast rather than tinting the canvas." }, + { id: "seed-056", oklch: [0.691, 0.146, 74.6], + mood: "Klim-style modern publishing house — late-afternoon paper warmth, considered editorial gold", + strategy: "Pure white surface so the amber seed becomes the brand voice; ink stays near-black neutral and accent shifts to a deep ink-blue to give the gold something structural to lean on." }, + { id: "seed-150", oklch: [0.750, 0.148, 80.0], + mood: "Klim Type Foundry specimen page — late-summer editorial gold, considered and grown-up", + strategy: "Pure white surface lets a single restrained ochre primary do all the brand work, paired with a deep ink-blue accent for typographic contrast in the Klim/Commercial Type tradition." }, + { id: "seed-058", oklch: [0.764, 0.120, 77.1], + mood: "Klim Type Foundry specimen page — late-afternoon ochre, considered editorial typography", + strategy: "Pure white surface lets the ochre primary do the brand work, paired with a deep ink-blue accent for editorial contrast — the type-foundry move where one warm hue carries the whole feeling against neutral paper." }, + { id: "seed-059", oklch: [0.784, 0.144, 79.8], + mood: "late afternoon in a Tuscan limonaia — sun-cured amber on whitewashed plaster", + strategy: "Pure white surface lets the saffron-amber primary and a deep olive accent carry the Mediterranean warmth, with split-complementary tension between gold and a quiet evergreen." }, + { id: "seed-061", oklch: [0.817, 0.161, 75.1], + mood: "late-afternoon honey on Tuscan limestone — golden hour, slow and luminous", + strategy: "Pure white surface lets the amber primary glow like sunlight on a wall, paired with a deep terracotta accent for warm tonal contrast within the same hue family." }, + { id: "seed-063", oklch: [0.842, 0.165, 91.3], + mood: "late-afternoon Tuscan sun on limestone — golden hour, considered, optimistic", + strategy: "Pure white surface lets the amber-gold primary radiate as the mood-carrier, with a deep aubergine accent providing the long shadow that golden light needs to feel three-dimensional." }, + { id: "seed-174", oklch: [0.350, 0.075, 110.0], + mood: "olive grove at late afternoon — sun-cured leaves, dust, and quiet Mediterranean weight", + strategy: "Pure white surface lets a deep, sun-cured olive primary do the emotional work, with a burnt-terracotta accent providing the warm-earth counterpoint olive groves are known for." }, + { id: "seed-117", oklch: [0.650, 0.100, 110.0], + mood: "Klim-style editorial sage — late-summer foundry catalogue, considered olive-yellow on paper", + strategy: "Seed sits at olive-chartreuse; treating it as a quiet typographic primary on pure paper, with a deeper bronze-olive accent for hierarchy — the color does the work, the page disappears." }, + { id: "seed-118", oklch: [0.750, 0.090, 110.0], + mood: "Klim Type Foundry specimen page — late-summer olive light on a working specimen, the honesty of a type designer showing their work", + strategy: "Pure white bg lets a desaturated olive-yellow primary do the editorial work, with a deeper olive-bronze accent providing typographic emphasis the way a specimen uses one heavy weight against the body roman." }, + { id: "seed-065", oklch: [0.797, 0.166, 113.1], + mood: "late-summer olive grove at noon — sun-bleached leaves, dry stone, Mediterranean glare", + strategy: "Hold the seed as a luminous chartreuse-olive primary against pure white so the color reads as sunlit foliage, pairing it with a deep umber accent for the dry-stone contrast." }, + { id: "seed-176", oklch: [0.300, 0.071, 120.0], + mood: "moss-darkened apothecary jar — herbal, shadowed, mid-19th-century botanical study", + strategy: "Seed is a deep desaturated olive-green that reads as preserved botanical pigment; I anchor it on pure white so the dim moss-green primary feels like ink on a herbarium page, with a warm ochre accent supplying the aged-paper counterpoint." }, + { id: "seed-155", oklch: [0.550, 0.142, 130.0], + mood: "moss-bed forest floor at noon — chlorophyll, lichen, sunlit fern", + strategy: "Seed is a confident mid-olive green with strong chroma; mood is daylight botanical, so I let the brand greens do the work on a pure paper-white bg and pair with a warm umber accent for fern-against-bark contrast." }, + { id: "seed-119", oklch: [0.600, 0.154, 130.0], + mood: "moss garden at Saihō-ji — damp stone, filtered green light through old cedar", + strategy: "Pure near-black bg lets the seed's mossy green glow like wet lichen under low light; accent shifts to a pale ochre-gold like sun catching through canopy." }, + { id: "seed-179", oklch: [0.300, 0.096, 140.0], + mood: "moss on wet stone — forest floor at dusk, deep botanical hush", + strategy: "Kept the seed's deep moss green as primary against a near-black surface so the green reads as living shadow, with a pale lichen accent providing the single point of light." }, + { id: "seed-180", oklch: [0.350, 0.110, 140.0], + mood: "moss-darkened apothecary — herbal tinctures in amber glass, pressed botanicals, the deep green of a conservatory at dusk", + strategy: "Near-black bg with a whisper of green undertone lets the seed's deep moss read as luminous foliage; a warm parchment accent provides the apothecary-label counterpoint without breaking the herbal register." }, + { id: "seed-120", oklch: [0.650, 0.100, 140.0], + mood: "moss on weathered stone — quiet botanical garden conservatory at midday", + strategy: "Pure white bg lets the muted sage-green primary read as a considered botanical mark, with a deeper terracotta accent providing earthen counterpoint without breaking the gallery-like restraint." }, + { id: "seed-121", oklch: [0.750, 0.090, 140.0], + mood: "moss garden at Saihō-ji — diffuse green light filtered through wet stone and lichen", + strategy: "Pure near-black bg lets the muted sage-green primary glow like lichen under low light; a warm pale-bone accent acts as the single ray of sun cutting through canopy." }, + { id: "seed-182", oklch: [0.400, 0.106, 150.0], + mood: "moss garden at Saiho-ji — deep cultivated green under wet stone shadow, contemplative and damp", + strategy: "Near-black bg with the faintest cool-green undertone evokes shaded stone; primary holds the seed's moss tone while accent shifts to a lichen-yellow for organic counterpoint without breaking the hush." }, + { id: "seed-157", oklch: [0.550, 0.145, 150.0], + mood: "moss garden at Saiho-ji — damp stone, filtered green light through cedar canopy", + strategy: "Near-black bg with a faint green undertone evokes deep forest shadow; primary holds the seed's verdant register while accent shifts to a pale lichen-cream to mimic light catching moss." }, + { id: "seed-122", oklch: [0.600, 0.158, 150.0], + mood: "forest floor at first light — moss, lichen, and clean morning air", + strategy: "Seed reads as a living, daylight green; surface stays pure white so the green carries the freshness, with a cool teal accent pulling it toward dew rather than earth." }, + { id: "seed-195", oklch: [0.650, 0.150, 145.0], + mood: "Considered horticulture brand — botanical research lab, the green of a healthy stem photographed in clean daylight", + strategy: "Pure white surface lets the seed's vegetal green carry the entire brand voice, paired with a deep forest ink and a warm clay accent for editorial contrast." }, + { id: "seed-183", oklch: [0.350, 0.077, 160.0], + mood: "moss-stained apothecary — deep forest glass, herbal tinctures shelved in low candlelight", + strategy: "Anchored the seed as primary and built a near-black dark surface with whisper-tinted green to evoke aged apothecary glass, letting the green glow rather than shout." }, + { id: "seed-184", oklch: [0.400, 0.087, 160.0], + mood: "deep forest apothecary — moss, bottle glass, and herbal tincture under afternoon light", + strategy: "Seed becomes a botanical-bottle-green primary on pure white, paired with a warm clove-amber accent to evoke herbal pharmacy contrast without tinting the surface." }, + { id: "seed-158", oklch: [0.550, 0.119, 160.0], + mood: "moss on wet stone — forest floor after rain, mineral and quiet", + strategy: "Pure white surface lets the deep mossy green carry the entire mood; accent shifts to a damp slate-teal to sit beside primary like lichen on stone without competing." }, + { id: "seed-159", oklch: [0.600, 0.130, 160.0], + mood: "moss-covered forest apothecary — herbal tinctures in amber glass, eucalyptus shadow", + strategy: "Anchored the green seed in a near-black backdrop so it reads like botanical glassware lit from within, with a warm amber accent pulled across the wheel to evoke tincture bottles against dark wood." }, + { id: "seed-185", oklch: [0.450, 0.086, 170.0], + mood: "weathered copper patina on a Pacific Northwest greenhouse — oxidized teal, glass light, botanical hush", + strategy: "Seed sits as a deep oxidized-teal primary against pure white so the patina reads as pigment, not atmosphere; a rust-copper accent completes the verdigris/oxidation story across the warm-cool axis." }, + { id: "seed-124", oklch: [0.750, 0.080, 170.0], + mood: "sea-glass on a foggy Pacific shoreline — weathered, mineral, quietly oxidized", + strategy: "Seed is a soft desaturated teal-green; pairing it on pure white lets the mineral primary read as patinated copper-glass, with a deeper kelp-toned primary and a rusted coral accent to spark the muted teal against its complement." }, + { id: "seed-160", oklch: [0.550, 0.095, 180.0], + mood: "weathered copper patina on a museum bronze — oxidized teal, conservatorial quiet", + strategy: "Pure near-black gallery surround lets the patina-teal primary glow like a lit artifact, with a warm verdigris-adjacent accent providing the oxidation contrast against the cool seed." }, + { id: "seed-161", oklch: [0.720, 0.100, 188.0], + mood: "climate-tech dashboard — calm verdigris on plain paper, the quiet confidence of an instrument that just works", + strategy: "Seed teal carries the entire mood as a single considered brand color on pure white, with a desaturated copper accent providing warm signal against the cool primary without competing for attention." }, + { id: "seed-186", oklch: [0.450, 0.074, 200.0], + mood: "deep hydrothermal vent — mineral teal under pressure, the cold blue-green of oxidized copper in submerged light", + strategy: "Near-black surface lets the mineral teal glow as if lit from within; accent shifts toward verdigris-copper to suggest patina on submerged metal, while ink stays cool-neutral to keep the register austere rather than aquatic-cute." }, + { id: "seed-125", oklch: [0.650, 0.100, 200.0], + mood: "climate-tech dashboard — calm operational teal, the color of clean water data and atmospheric sensors", + strategy: "Pure white surface lets a single muted-teal primary do all the brand work, with a deeper marine accent providing hierarchy without competing chroma." }, + { id: "seed-126", oklch: [0.750, 0.080, 200.0], + mood: "climate-tech product brand — quiet competence, dashboards for hard infrastructure problems", + strategy: "Hold the seed's muted teal as primary, pair with a sharper cyan-leaning accent for interactive lift, and let a pure white surface do the disappearing act so the brand reads as a tool, not an atmosphere." }, + { id: "seed-162", oklch: [0.550, 0.091, 210.0], + mood: "weathered nautical instrument — patinated brass on oxidized steel, the cool blue-grey of a ship's chronometer at dawn", + strategy: "Pure white surface lets the muted teal-steel primary read as a precise instrument mark, with a warm brass accent providing the single point of patina against clinical white." }, + { id: "seed-163", oklch: [0.450, 0.086, 230.0], + mood: "deep harbor at dusk — weathered nautical instruments, brass dials on oxidized steel", + strategy: "Near-black background with subtle cool tint evokes the marine dusk; primary holds the seed's teal-blue while a warm brass accent creates the instrument-on-steel tension." }, + { id: "seed-164", oklch: [0.550, 0.105, 230.0], + mood: "deep harbor at dawn — cold steel water, fog-muted light, the quiet before the boats leave", + strategy: "Pure near-black bg lets the seed's cold marine blue read as a luminous beacon, while a pale frost-cyan accent evokes diffused dawn light cutting through fog." }, + { id: "seed-127", oklch: [0.650, 0.100, 230.0], + mood: "climate-tech dashboard — atmospheric sensor blue, calm operational clarity", + strategy: "Anchor the seed as a confident mid-blue primary on pure white so the brand color carries all the atmospheric feeling, with a deep navy accent for hierarchy and a soft slate muted for body text." }, + { id: "seed-128", oklch: [0.750, 0.080, 230.0], + mood: "climate-tech dashboard — calm atmospheric data, considered sky-blue", + strategy: "Pure white surface lets the muted sky-blue primary carry the meteorological calm, with a deep-navy accent providing readable weight against the soft primary." }, + { id: "seed-187", oklch: [0.350, 0.078, 240.0], + mood: "deep harbor at blue hour — wet stone, cold steel, the quiet before night fully lands", + strategy: "Near-black architectural bg with a hint of marine chroma lets the seed read as ambient atmosphere rather than UI chrome; a cooler steel accent sits opposite the warmer-shifted primary for navigational clarity." }, + { id: "seed-077", oklch: [0.578, 0.130, 241.7], + mood: "pre-dawn signal tower — cold blue solitude, instruments glowing against the dark", + strategy: "Pure near-black bg lets the seed's cold tower-light blue glow as the sole emotional source, with a frost-cyan accent acting as a secondary indicator light." }, + { id: "seed-188", oklch: [0.400, 0.110, 250.0], + mood: "Linear's considered indigo — the calm authority of a well-built developer tool, blueprint ink on a clean page", + strategy: "Held the seed as a deep indigo primary against pure white so the brand color carries all the gravity; accent shifts to a cooler, brighter cyan-blue to create a crisp hierarchy pair without warming the surface." }, + { id: "seed-165", oklch: [0.450, 0.123, 250.0], + mood: "blueprint room at dusk — drafting table, graphite, civic-engineering blue", + strategy: "Seed is a mid-deep architectural blue with real chroma and no environmental cue, so I stay out of the way with a pure white surface and let the primary do all the talking, pairing it with a burnt-ochre accent for drafting-pencil contrast." }, + { id: "seed-079", oklch: [0.478, 0.136, 251.8], + mood: "twilight cartography — the blue of deep dusk over open water, precise and navigational", + strategy: "Pure white surface lets the seed's oceanic blue act as a single navigational anchor, with a warm amber accent struck across it like a lighthouse beam at dusk." }, + { id: "seed-080", oklch: [0.541, 0.122, 248.2], + mood: "Linear-style considered tool blue — the calm, exact register of a modern engineering app where every pixel is intentional", + strategy: "Pure white surface lets the considered indigo-blue primary carry the entire brand; a deeper navy accent provides hierarchy without warmth, keeping the palette in a single cool family for that focused-software feel" }, + { id: "seed-166", oklch: [0.550, 0.149, 250.0], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and quietly intense", + strategy: "Near-black bg with the faintest cool tint reads like a darkened cockpit; the seed becomes a luminous instrument-blue primary, paired with a warm amber accent that mimics avionics readouts for unmistakable signal contrast." }, + { id: "seed-081", oklch: [0.650, 0.160, 250.0], + mood: "deep-sea research vessel at dawn — instrument glow against cold steel light", + strategy: "Pure near-white bg keeps the palette technical and instrument-like; the seed blue holds as primary while a desaturated steel-cyan accent reads like signal readouts on glass." }, + { id: "seed-082", oklch: [0.742, 0.140, 247.4], + mood: "high-altitude flight deck at dawn — cold cabin instruments glowing against a sky still holding night", + strategy: "Near-black cockpit ground with a faint blue cast lets the seed read as an illuminated instrument; primary holds the seed, accent shifts to cyan for signal/indicator contrast." }, + { id: "seed-210", oklch: [0.360, 0.140, 260.0], + mood: "Linear-style considered tool indigo — late-night focused work, the deep blue of a code editor at 2am where everything else falls away", + strategy: "Pure black bg lets the indigo primary carry all the cognitive-focus weight, with a slightly brighter periwinkle accent for interactive lift — the surface disappears so the tool feels weightless." }, + { id: "seed-189", oklch: [0.400, 0.130, 260.0], + mood: "pre-dawn observatory — cold instrument blue, star-chart precision", + strategy: "Seed becomes the primary on pure black so the deep instrument-blue glows like a calibration light, with a faint cyan accent reading as starlight against the void." }, + { id: "seed-211", oklch: [0.420, 0.161, 260.0], + mood: "Linear's considered indigo — the tool-for-thought blue of focused product work, calm authority without coldness", + strategy: "Hold the seed as a deep indigo primary against pure white, then pair with a slightly warmer, lighter periwinkle accent to create gentle hue separation without breaking the disciplined tool-brand register." }, + { id: "seed-129", oklch: [0.450, 0.150, 260.0], + mood: "pre-dawn observatory — deep cobalt sky just before astronomical twilight, instruments cool to the touch", + strategy: "Near-black surface lets the cobalt seed read as luminous starlight; a single warm amber accent acts as the calibration lamp against the cold blue field." }, + { id: "seed-084", oklch: [0.476, 0.207, 261.2], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and awake", + strategy: "Default B black bg lets the cobalt primary read as a luminous instrument signal, with a cyan accent striking the analogous 'cockpit display' relationship." }, + { id: "seed-085", oklch: [0.681, 0.132, 258.4], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky", + strategy: "Anchored the seed as a luminous primary against a near-black architectural ground, with a warm amber accent acting as the single instrument light cutting through cold blue." }, + { id: "seed-086", oklch: [0.767, 0.106, 255.9], + mood: "Scandinavian winter morning — quiet light through frost, pale sky over snow", + strategy: "Anchored a pure white editorial stage so the seed's cool sky-blue reads as crisp polar light, with a deeper navy primary providing the only saturated weight — like a single dark pine against snow." }, + { id: "seed-083", oklch: [0.340, 0.159, 262.4], + mood: "deep cobalt twilight — the moment after sunset when the sky goes electric blue and city windows start to glow", + strategy: "Pure black stage lets the cobalt seed act as a luminous neon-window glow, with a warm amber accent across the wheel for the lit-window contrast." }, + { id: "seed-212", oklch: [0.360, 0.219, 270.0], + mood: "Linear-grade tooling indigo — considered software for people who care about craft", + strategy: "Anchored the deep indigo seed as primary on a pure white surface so the brand color carries all the weight, with a slightly cooler violet-blue accent for hierarchy without competing chroma." }, + { id: "seed-130", oklch: [0.400, 0.150, 270.0], + mood: "Linear-grade indigo — considered productivity tool, ink on paper, no theatrics", + strategy: "Pure white surface lets a deep cool indigo carry all the brand weight, paired with a slightly warmer violet-blue accent for hierarchy without acid." }, + { id: "seed-213", oklch: [0.411, 0.241, 267.9], + mood: "Linear-style indigo — considered tool surface, the kind of blue-violet that sits behind a developer's keyboard at 11pm without shouting", + strategy: "Pure black canvas lets a saturated indigo primary do all the brand work, with a cooler cyan-violet accent providing UI signal without competing." }, + { id: "seed-131", oklch: [0.450, 0.180, 270.0], + mood: "monastic indigo dusk — vespers light through stained glass, contemplative and severe", + strategy: "Seed becomes a deep indigo primary against pure near-black so the violet reads as luminous stained-glass against architectural shadow, with a cooler iris accent for tonal lift." }, + { id: "seed-088", oklch: [0.476, 0.158, 268.5], + mood: "pre-dawn astronomer's notebook — deep indigo sky just before the stars fade, ink and graphite", + strategy: "Near-black bg with the faintest cool tint to evoke night sky without theatrics; primary holds the seed's indigo, accent shifts to a paler periwinkle for stellar contrast, keeping the palette monochromatic-cool and observational." }, + { id: "seed-196", oklch: [0.530, 0.130, 268.0], + mood: "Linear-style considered tool indigo — the deep-focus blue-violet of a thoughtfully built productivity surface, the color of a well-typeset keyboard shortcut", + strategy: "Pure white bg lets the indigo seed do all the brand work as primary, with a slightly darker, more saturated violet-shifted accent for hierarchy and interactive states — the surface disappears so the brand color reads as the entire identity." }, + { id: "seed-132", oklch: [0.700, 0.120, 270.0], + mood: "Linear-style considered tool indigo — the quiet violet of a focused product workspace, late-afternoon thinking", + strategy: "Pure white surface lets a muted indigo-violet primary and a slightly cooler accent do all the brand work, keeping the register calm and software-like rather than theatrical." }, + { id: "seed-090", oklch: [0.445, 0.206, 279.1], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, not a nightclub", + strategy: "Anchor the seed as a confident product primary on pure white, with a cooler indigo-shift accent that reads as a sibling tool color, so the brand violet does all the emotional work." }, + { id: "seed-133", oklch: [0.500, 0.160, 280.0], + mood: "Linear-adjacent indigo — considered productivity tool, the violet of a thinking workspace", + strategy: "Seed becomes a measured indigo primary on pure white; accent shifts to a cooler blue-violet to create hierarchy without nightclub saturation, letting the brand color do all the emotional work." }, + { id: "seed-094", oklch: [0.533, 0.125, 294.3], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, calm authority for a creative workspace", + strategy: "Pure white canvas lets the indigo-violet primary carry the entire brand voice; accent shifts hue slightly toward blue for a cool, tool-like duotone rather than warm decorative pairing." }, + { id: "seed-137", oklch: [0.700, 0.120, 290.0], + mood: "Linear-adjacent indigo — the considered tool, late-evening focus mode, software made for people who care about craft", + strategy: "Pure black surface lets a single restrained indigo-violet carry the brand, with a cooler periwinkle accent providing UI hierarchy without competing — Vercel/Linear dark-mode discipline." }, + { id: "seed-100", oklch: [0.450, 0.150, 330.0], + mood: "velvet boudoir at last call — bruised orchid and lipstick traces under low lamplight", + strategy: "Pure near-black surface lets a deep magenta-rose primary smolder while a warm peach accent acts like skin-lit lamplight — drama lives in the brand pair, not the room." }, + { id: "seed-103", oklch: [0.650, 0.160, 330.0], + mood: "1980s Memphis boudoir — powder-pink neon humming against lacquered black, lipstick and lacquer", + strategy: "Near-black gallery surface lets the magenta-pink seed read as lit neon; accent shifts to warm coral to create cinematic dichromatic tension without competing chroma." }, + { id: "seed-228", oklch: [0.360, 0.147, 340.0], + mood: "Figma-era creative tool plum — considered productivity software for designers, the inky violet of a serif wordmark on a marketing site", + strategy: "Held the seed as a deep plum primary against pure white so the brand color does the emotional work; paired with a muted rose accent for warmth without breaking the productivity-tool restraint." }, + { id: "seed-107", oklch: [0.500, 0.200, 340.0], + mood: "Figma plum — creative-tool confidence, considered magenta for a modern design product", + strategy: "Pure white surface lets a saturated magenta-plum primary carry all the brand voice, paired with a cooler violet-leaning accent for hierarchy without competing." }, + { id: "seed-198", oklch: [0.600, 0.210, 340.0], + mood: "Figma-era creative tool plum — confident, considered, made for makers", + strategy: "Anchor a saturated plum primary against pure white so the brand color does all the emotional work, with a deeper magenta-rose accent for hierarchy." }, + { id: "seed-112", oklch: [0.754, 0.193, 343.4], + mood: "Figma-era creative tool — confident pink primary doing the brand work on a clean canvas, the way Linear uses indigo or Stripe uses violet", + strategy: "Anchor the seed pink as a saturated brand primary on pure white so the color carries all the personality; pair with a cooler plum accent to give the pink something to push against without competing." }, + { id: "seed-229", oklch: [0.420, 0.163, 350.0], + mood: "considered fintech rose — the deep magenta of a modern product brand (think Stripe-adjacent, but rotated toward berry), confident and current", + strategy: "pure white surface lets a single deep berry-rose primary do all the brand work, paired with a cooler indigo accent for the contrast move you see in modern product marketing" }, + { id: "seed-113", oklch: [0.470, 0.173, 354.8], + mood: "1960s velvet rope nightclub — crushed magenta, low light, cigarette smoke catching a spotlight", + strategy: "Pure black stage so the seed's smoky magenta reads as a single hot spotlight, paired with a cooler violet accent for the second light cue." }, + { id: "seed-114", oklch: [0.570, 0.158, 353.3], + mood: "fin-de-siècle Parisian rose — velvet curtain, theatre program, lipstick blotted on linen", + strategy: "Drop bg to true black so the dusty-rose primary reads as stage-lit silk; accent shifts to a warmer coral-mauve at higher lightness to create gentle hue rotation without breaking the romance." }, + { id: "seed-199", oklch: [0.650, 0.180, 350.0], + mood: "modern fintech rose — the considered pink of a Series B brand mark, confident and current without nostalgia", + strategy: "Pure white surface lets a saturated rose primary do the brand work, paired with a deep plum accent for hierarchy — the Stripe move applied to a pink hue." }, + { id: "seed-115", oklch: [0.636, 0.218, 355.3], + mood: "backstage at a cabaret — velvet rope, lipstick mark on a champagne glass", + strategy: "Seed reads as a saturated stage-light magenta-red; I push it into pure black so the primary glows like a neon sign and the accent (a cold pearl-pink) acts as the spotlight rim — the room is dark, the color does the singing." }, + { id: "seed-230", oklch: [0.650, 0.249, 354.5], + mood: "Modern fintech rose — the considered pink of a contemporary payments brand: confident, alive, and clear-headed", + strategy: "Pure white bg lets a saturated rose-magenta primary carry all the brand energy, paired with a cooler indigo accent for trustworthy contrast — the Stripe move applied to a pink hue." }, + { id: "seed-231", oklch: [0.682, 0.241, 353.2], + mood: "Figma-era creative tool — a confident pink-magenta product brand, the kind a modern design platform uses to feel alive without shouting", + strategy: "Default A pure white bg lets the saturated pink-magenta primary do all the brand work, with a near-complementary cool teal accent for tool-like clarity and a neutral ink for editorial calm" }, + { id: "seed-116", oklch: [0.734, 0.183, 356.8], + mood: "modern beauty brand DTC — Glossier-adjacent pink, confident and current without being saccharine", + 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." }, +]; + +function parseArgs(argv) { + const args = { id: null, from: null }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === '--id' && argv[i + 1]) { args.id = argv[++i]; } + else if (a === '--from' && argv[i + 1]) { args.from = argv[++i]; } + } + return args; +} + +// Hash a key into a stable float in [0, 1) for deterministic weighted picks. +function hashUnit(key) { + const h = crypto.createHash('sha256').update(key).digest(); + return h.readUInt32BE(0) / 0x100000000; +} + +// The curated library is hue-skewed (more reds/oranges than teals/magentas) +// because that's where the source material + taste landed. Left uniform, a +// random pick would land on red ~1/3 of the time. Inverse-frequency weighting +// gives each seed a weight of 1/(count in its 30° hue bucket), so each hue +// ZONE is roughly equally likely to be chosen regardless of how many seeds it +// holds — fair rainbow exposure across runs without pruning the library. +function buildWeights(seeds) { + const bucketCount = {}; + const bucketOf = (s) => Math.floor(((s.oklch[2] % 360) + 360) % 360 / 30); + for (const s of seeds) { const b = bucketOf(s); bucketCount[b] = (bucketCount[b] || 0) + 1; } + const weights = seeds.map((s) => 1 / bucketCount[bucketOf(s)]); + const total = weights.reduce((a, b) => a + b, 0); + return { weights, total }; +} + +function weightedPick(seeds, unit) { + const { weights, total } = buildWeights(seeds); + let target = unit * total; + for (let i = 0; i < seeds.length; i++) { + target -= weights[i]; + if (target < 0) return seeds[i]; + } + return seeds[seeds.length - 1]; +} + +function pickSeed(seeds, { id, from }) { + if (id) { + const found = seeds.find(s => s.id === id); + if (!found) { console.error(`no seed with id "${id}"`); process.exit(2); } + return found; + } + const envFrom = process.env.IMPECCABLE_PALETTE_SEED; + const key = from || envFrom; + const unit = key ? hashUnit(key) : Math.random(); + return weightedPick(seeds, unit); +} + +function fmtOklch([L, C, H]) { + return `oklch(${L.toFixed(3)} ${C.toFixed(3)} ${H.toFixed(1)})`; +} + +function hueWord(H) { + if (H < 15 || H >= 345) return 'pure red'; + if (H < 35) return 'warm red / crimson'; + if (H < 55) return 'warm coral / burnt orange'; + if (H < 80) return 'orange / honey'; + if (H < 105) return 'warm amber / honey-gold'; + if (H < 135) return 'yellow-green / olive'; + if (H < 170) return 'green'; + if (H < 200) return 'teal'; + if (H < 230) return 'sky blue'; + if (H < 265) return 'cobalt / indigo'; + if (H < 295) return 'violet / purple'; + if (H < 330) return 'magenta / pink'; + return 'deep pink / rose'; +} + +// --------------------------------------------------------------- + +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}` : ''; + +// --------------------------------------------------------------- +// Fat tool-exit response — what the model sees on stdout. +// --------------------------------------------------------------- + +process.stdout.write(`BRAND SEED · ${seed.id} + +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. + +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. + +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é. + + 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 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. + + 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. + +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): + + - 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. + +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. + +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. +`); diff --git a/.kiro/skills/impeccable/scripts/pin.mjs b/.kiro/skills/impeccable/scripts/pin.mjs index ba02783fb..320d98ce1 100644 --- a/.kiro/skills/impeccable/scripts/pin.mjs +++ b/.kiro/skills/impeccable/scripts/pin.mjs @@ -27,7 +27,7 @@ const HARNESS_DIRS = [ // Valid sub-command names const VALID_COMMANDS = [ - 'craft', 'teach', 'extract', 'document', 'shape', + 'craft', 'init', 'extract', 'document', 'shape', 'critique', 'audit', 'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live', 'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive', diff --git a/.opencode/skills/impeccable/SKILL.md b/.opencode/skills/impeccable/SKILL.md index 8e6122791..4445b6790 100644 --- a/.opencode/skills/impeccable/SKILL.md +++ b/.opencode/skills/impeccable/SKILL.md @@ -1,10 +1,10 @@ --- name: impeccable description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks. -version: 3.1.1 +version: 3.5.0 user-invocable: true -argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · teach|document|extract|live] [target]" -license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution. +argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]" +license: Apache 2.0 allowed-tools: - Bash(npx impeccable *) --- @@ -13,85 +13,80 @@ Designs and iterates production-grade frontend interfaces. Real working code, co ## Setup -Before any design work or file edits: +You MUST do these steps before proceeding: -1. Load context (PRODUCT.md / DESIGN.md) via the loader script. -2. Identify the register and load the matching register reference (brand.md or product.md). -3. **If the user invoked a sub-command (e.g. `craft`, `shape`, `audit`), load its reference file too.** This is non-negotiable: `craft` without `craft.md` loaded means you'll skip the shape-and-confirm step the user expects. +1. Run `node .opencode/skills/impeccable/scripts/context.mjs` once per session. If you've already seen its output in this conversation, do not re-run it. The script either prints the project's PRODUCT.md (and DESIGN.md when present) as a markdown block, or tells you it's missing. Follow whatever it prints. **If it reports `NO_PRODUCT_MD`, stop and follow `reference/init.md` before doing anything else.** If the output ends with an `UPDATE_AVAILABLE` directive, follow it (ask the user once about updating, then continue). It never blocks the current task. +2. If the user invoked a sub-command (`craft`, `shape`, `audit`, `polish`, ...), you MUST read `reference/.md` next. Non-optional. The reference defines the command's flow; without it you will skip steps the user expects. +3. Familiarize yourself with any existing design system, conventions, and components in the code. Read at least one project file (CSS / tokens / theme / a representative component or page). **Required even when you've loaded a sub-command reference in step 2.** Don't reinvent the wheel; use what's there when it works, branch out when the UX wins. +4. Read the matching register reference. **This is non-optional; skipping it produces generic output.** If the project is marketing, a landing page, a campaign, long-form content, or a portfolio (design IS the product), read `reference/brand.md`. If it is app UI, admin, a dashboard, or a tool (design SERVES the product), read `reference/product.md`. Pick by first match: (1) task cue ("landing page" vs "dashboard"); (2) surface in focus (the page, file, or route being worked on); (3) `register` field in PRODUCT.md. +5. **If the project is brand-new (no existing CSS tokens / theme / committed brand colors found in step 3)**, run `node .opencode/skills/impeccable/scripts/palette.mjs` to receive a brand seed color and composition guidance. This is the anchor for your primary brand color. Compose the rest of the palette (bg, surface, ink, accent, muted) around it per the script's instructions. Use OKLCH throughout. **Skip this step only if step 3 found committed brand colors in existing tokens; in that case identity-preservation wins.** -Skipping these produces generic output that ignores the project. +## Design guidance -### 1. Context gathering +Produce ready-to-ship, production-grade code, not prototypes or starting points. Take no shortcuts unless the user asks for them (when in doubt, ask). Don't stop until arriving at a complete implementation (beautiful, responsive, fast, precise, bug-free, on brand). You take attention to detail seriously: every page, section or component crafted is battle tested using the tools available to you (browser screenshotting, computer use, etc). Claude is capable of extraordinary work. Don't hold back. -Two files, case-insensitive. The loader looks at the project root by default and falls back to `.agents/context/` and `docs/` if the root is clean. Override with `IMPECCABLE_CONTEXT_DIR=path/to/dir` (absolute or relative to cwd). +### General rules -- **PRODUCT.md**: required. Users, brand, tone, anti-references, strategic principles. -- **DESIGN.md**: optional, strongly recommended. Colors, typography, elevation, components. +#### Color -Load both in one call: +- **Verify contrast.** Body text must hit ≥4.5:1 against its background; large text (≥18px or bold ≥14px) needs ≥3:1. Placeholder text needs the same 4.5:1, not the muted-gray default. The most common failure: muted gray body text on a tinted near-white. If the contrast is even close, bump the body color toward the ink end of the ramp; light gray "for elegance" is the single biggest reason AI designs feel hard to read. +- Gray text on a colored background looks washed out. Use a darker shade of the background's own hue, or a transparency of the text color. -```bash -node .opencode/skills/impeccable/scripts/load-context.mjs -``` +#### Typography -Consume the full JSON output. Never pipe through `head`, `tail`, `grep`, or `jq`. The output's `contextDir` field tells you where the files were resolved from. +- Cap body line length at 65–75ch. +- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales. +- Cap font-family count at 3 (display + body + optional mono). More than 3 reads as indecision, not richness. One well-tuned family with weight contrast usually beats three competing typefaces. +- Don't pair fonts that are similar but not identical (two geometric sans-serifs, two humanist sans-serifs). Pair on a contrast axis (serif + sans, geometric + humanist) or use one family in multiple weights. +- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. +- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. +- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". +- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. -If the output is already in this session's conversation history, don't re-run. Exceptions requiring a fresh load: you just ran `/impeccable teach` or `/impeccable document` (they rewrite the files), or the user manually edited one. +#### Layout -`/impeccable live` already warms context via `live.mjs`. If you've run `live.mjs`, don't also run `load-context.mjs` this session. +- Vary spacing for rhythm. +- Cards are the lazy answer. Use them only when they're truly the best affordance. Nested cards are always wrong. +- Flexbox for 1D, Grid for 2D. Don't default to Grid when `flex-wrap` would be simpler. +- For responsive grids without breakpoints: `repeat(auto-fit, minmax(280px, 1fr))`. +- Build a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip). Never arbitrary values like 999 or 9999. -If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `/impeccable teach`, then resume the user's original task with the fresh context. If the original task was `/impeccable craft`, resume into `/impeccable shape` before any implementation work. +#### Motion +- Motion should be intentional, and not be an afterthought. consider it as part of the build. +- Don't animate CSS layout properties unless truly needed. +- Ease out with exponential curves (ease-out-quart / quint / expo). No bounce, no elastic. +- Use libraries for more advanced motion needs (e.g. motion, gsap, anime.js, lenis etc) +- Reduced motion is not optional. Every animation needs a `@media (prefers-reduced-motion: reduce)` alternative: typically a crossfade or instant transition. +- Staggering the items within one list is legitimate. The tell is the uniform reflex (one identical entrance applied to every section), not motion itself; each reveal should fit what it reveals. Suppressing the reflex is never a reason to ship a page with no motion at all. +- Reveal animations must enhance an already-visible default. Don't gate content visibility on a class-triggered transition; transitions pause on hidden tabs and headless renderers, so the reveal never fires and the section ships blank. +- Premium motion materials are not just transform/opacity. Blur, backdrop-filter, clip-path, mask, and shadow/glow are part of the palette when they materially improve the effect and stay smooth. -If DESIGN.md is missing: nudge once per session (*"Run `/impeccable document` for more on-brand output"*), then proceed. +#### Interaction -### 2. Register +- Dropdowns rendered with `position: absolute` inside an `overflow: hidden` or `overflow: auto` container will be clipped. Use the native `` / popover API, `position: fixed`, or a portal to escape the stacking context. -Every design task is **brand** (marketing, landing, campaign, long-form content, portfolio: design IS the product) or **product** (app UI, admin, dashboard, tool: design SERVES the product). +### Copy -Identify before designing. Priority: (1) cue in the task itself ("landing page" vs "dashboard"); (2) the surface in focus (the page, file, or route being worked on); (3) `register` field in PRODUCT.md. First match wins. +- Every word earns its place. No restated headings, no intros that repeat the title. +- **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`. +- **No aphoristic-cadence body copy as a default voice.** Don't fall into the rhythm of "serious statement, then punchy short negation" as the page's recurring voice. If three or more section copy blocks on the page land on a short rebuttal-shaped sentence, rewrite. Specific, not aphoristic. +- **No marketing buzzwords.** The streamline / empower / supercharge / leverage / unleash / transform / seamless / world-class / enterprise-grade / next-generation / cutting-edge / game-changer / mission-critical family of phrases. Pick a specific noun and a verb that describes what the product literally does. +- Button labels: verb + object. "Save changes" beats "OK"; "Delete project" beats "Yes". The label should say what will happen. +- Link text needs standalone meaning. "View pricing plans" beats "Click here"; screen readers announce links out of context. -If PRODUCT.md lacks the `register` field (legacy), infer it once from its "Users" and "Product Purpose" sections, then cache the inferred value for the session. Suggest the user run `/impeccable teach` to add the field explicitly. +### New projects only (when no prior work exists) -Load the matching reference: [reference/brand.md](reference/brand.md) or [reference/product.md](reference/product.md). The shared design laws below apply to both. +#### Color & Theme -## Shared design laws - -Apply to every design, both registers. Match implementation complexity to the aesthetic vision: maximalism needs elaborate code, minimalism needs precision. Interpret creatively. Vary across projects; never converge on the same choices. Claude is capable of extraordinary work. Don't hold back. - -### Color - -- Use OKLCH. Reduce chroma as lightness approaches 0 or 100; high chroma at extremes looks garish. -- Never use `#000` or `#fff`. Tint every neutral toward the brand hue (chroma 0.005–0.01 is enough). +- Use OKLCH. +- **The cream / sand / beige body bg is the saturated AI default of 2026.** The whole warm-neutral band (OKLCH L 0.84-0.97, C < 0.06, hue 40-100) reads as cream/sand/paper/parchment regardless of what you call it. Token names like `--paper`, `--cream`, `--sand`, `--bone`, `--flour`, `--linen`, `--parchment`, `--wheat`, `--biscuit`, `--ivory` are tells in themselves. If the brief is "warm, traditional, family-coastal-Italian" or "magazine-warm" or "editorial-restraint", DO NOT translate that into a near-white warm-tinted bg; that's the AI move. Pick: (a) a saturated brand color as the body (terracotta, oxblood, deep ochre, near-black), (b) a true off-white at chroma 0 (or chroma toward the brand's own hue, not toward warmth-by-default), or (c) a darker mid-tone tinted neutral that's clearly the brand's own. "Warmth" in the brand is carried by accent + typography + imagery, not by body bg. +- Tinted neutrals: add 0.005–0.015 chroma toward the brand's hue. Don't default-tint toward warm or cool "because the brand feels that way"; that's the cross-project monoculture move. +- When picking a theme: Dark vs. light is never a default. Not dark "because tools look cool dark." Not light "to be safe.".Before choosing, write one sentence of physical scene: who uses this, where, under what ambient light, in what mood. If the sentence doesn't force the answer, it's not concrete enough. Add detail until it does. - Pick a **color strategy** before picking colors. Four steps on the commitment axis: - **Restrained**: tinted neutrals + one accent ≤10%. Product default; brand minimalism. - **Committed**: one saturated color carries 30–60% of the surface. Brand default for identity-driven pages. - **Full palette**: 3–4 named roles, each used deliberately. Brand campaigns; product data viz. - **Drenched**: the surface IS the color. Brand heroes, campaign pages. -- The "one accent ≤10%" rule is Restrained only. Committed / Full palette / Drenched exceed it on purpose. Don't collapse every design to Restrained by reflex. - -### Theme - -Dark vs. light is never a default. Not dark "because tools look cool dark." Not light "to be safe." - -Before choosing, write one sentence of physical scene: who uses this, where, under what ambient light, in what mood. If the sentence doesn't force the answer, it's not concrete enough. Add detail until it does. - -"Observability dashboard" does not force an answer. "SRE glancing at incident severity on a 27-inch monitor at 2am in a dim room" does. Run the sentence, not the category. - -### Typography - -- Cap body line length at 65–75ch. -- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales. - -### Layout - -- Vary spacing for rhythm. Same padding everywhere is monotony. -- Cards are the lazy answer. Use them only when they're truly the best affordance. Nested cards are always wrong. -- Don't wrap everything in a container. Most things don't need one. - -### Motion - -- Don't animate CSS layout properties. -- Ease out with exponential curves (ease-out-quart / quint / expo). No bounce, no elastic. ### Absolute bans @@ -102,12 +97,9 @@ Match-and-refuse. If you're about to write any of these, rewrite the element wit - **Glassmorphism as default.** Blurs and glass cards used decoratively. Rare and purposeful, or nothing. - **The hero-metric template.** Big number, small label, supporting stats, gradient accent. SaaS cliché. - **Identical card grids.** Same-sized cards with icon + heading + text, repeated endlessly. -- **Modal as first thought.** Modals are usually laziness. Exhaust inline / progressive alternatives first. - -### Copy - -- Every word earns its place. No restated headings, no intros that repeat the title. -- **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`. +- **Tiny uppercase tracked eyebrow above every section.** The 2023-era kicker (small all-caps text with wide tracking, "ABOUT" "PROCESS" "PRICING" above each heading) is now the saturated AI scaffold; it appears on 55-95% of generations regardless of brief, which is the definition of a tell. One named kicker as a deliberate brand system is voice; an eyebrow on every section is AI grammar. Choose a different cadence. +- **Numbered section markers as default scaffolding (01 / 02 / 03).** Putting `01 · About / 02 · Process / 03 · Pricing` above every section is the eyebrow trope one tier deeper: reach for it because "landing pages do this" and you're scaffolding by reflex. Numbers earn their place when the section actually IS a sequence (a real 3-step process, an ordered flow, a typed timeline) and the order carries information the reader needs. One deliberate numbered sequence on one page is voice; numbered eyebrows on every section across the site is AI grammar. +- **Text that overflows its container.** Long heading words plus large clamp scales plus narrow grids cause headline overflow on tablet/mobile. Test the heading copy at every breakpoint; if it overflows, reduce the clamp max or rewrite the copy. The viewport is part of the design. ### The AI slop test @@ -115,7 +107,7 @@ If someone could look at this interface and say "AI made that" without doubt, it **Category-reflex check.** Run at two altitudes; the second one catches what the first one misses. -- **First-order:** if someone could guess the theme + palette from the category alone ("observability → dark blue", "healthcare → white + teal", "finance → navy + gold", "crypto → neon on black"), it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain. +- **First-order:** if someone could guess the theme + palette from the category alone, it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain. - **Second-order:** if someone could guess the aesthetic family from category-plus-anti-references ("AI workflow tool that's not SaaS-cream → editorial-typographic", "fintech that's not navy-and-gold → terminal-native dark mode"), it's the trap one tier deeper. The first reflex was avoided; the second wasn't. Rework until both answers are not obvious. The brand register's [reflex-reject aesthetic lanes](reference/brand.md) list catches the currently-saturated families. ## Commands @@ -124,7 +116,7 @@ If someone could look at this interface and say "AI made that" without doubt, it |---|---|---|---| | `craft [feature]` | Build | Shape, then build a feature end-to-end | [reference/craft.md](reference/craft.md) | | `shape [feature]` | Build | Plan UX/UI before writing code | [reference/shape.md](reference/shape.md) | -| `teach` | Build | Set up PRODUCT.md and DESIGN.md context | [reference/teach.md](reference/teach.md) | +| `init` | Build | Set up project context: PRODUCT.md, DESIGN.md, live config, next steps | [reference/init.md](reference/init.md) | | `document` | Build | Generate DESIGN.md from existing project code | [reference/document.md](reference/document.md) | | `extract [target]` | Build | Pull reusable tokens and components into design system | [reference/extract.md](reference/extract.md) | | `critique [target]` | Evaluate | UX design review with heuristic scoring | [reference/critique.md](reference/critique.md) | @@ -152,11 +144,14 @@ Plus two management commands: `pin ` and `unpin `, detailed be 1. **No argument**: render the table above as the user-facing command menu, grouped by category. Ask what they'd like to do. 2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. -3. **First word doesn't match**: general design invocation. Apply the setup steps, shared design laws, and the loaded register reference, using the full argument as context. +3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. +4. **No clear command match**: general design invocation. Apply the setup steps, the General rules, and the loaded register reference, using the full argument as context. Setup (context gathering, register) is already loaded by then; sub-commands don't re-invoke `/impeccable`. -If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `teach` as a blocker, finish teach, refresh context, then resume the original command and target. +If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `init` as a blocker, finish init, refresh context, then resume the original command and target. + +`teach` is a deprecated alias for `init`: if the user types it, load [reference/init.md](reference/init.md) and proceed as if they ran `init`. ## Pin / Unpin diff --git a/.opencode/skills/impeccable/reference/adapt.md b/.opencode/skills/impeccable/reference/adapt.md index 9d0673ea2..5af7606cb 100644 --- a/.opencode/skills/impeccable/reference/adapt.md +++ b/.opencode/skills/impeccable/reference/adapt.md @@ -188,3 +188,124 @@ Test thoroughly across contexts: - **Slow connections**: Test on throttled network When the adaptation feels native to each context, hand off to `/impeccable polish` for the final pass. + +--- + +## Reference Material + +The sections below were previously `responsive-design.md` and live inline now so the adapt flow has its deep responsive reference in one place. + +### Responsive Design + +#### Mobile-First: Write It Right + +Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first. + +#### Breakpoints: Content-Driven + +Don't chase device sizes; let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints. + +#### Detect Input Method, Not Just Screen Size + +**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard. Use pointer and hover queries: + +```css +/* Fine pointer (mouse, trackpad) */ +@media (pointer: fine) { + .button { padding: 8px 16px; } +} + +/* Coarse pointer (touch, stylus) */ +@media (pointer: coarse) { + .button { padding: 12px 20px; } /* Larger touch target */ +} + +/* Device supports hover */ +@media (hover: hover) { + .card:hover { transform: translateY(-2px); } +} + +/* Device doesn't support hover (touch) */ +@media (hover: none) { + .card { /* No hover state - use active instead */ } +} +``` + +**Critical**: Don't rely on hover for functionality. Touch users can't hover. + +#### Safe Areas: Handle the Notch + +Modern phones have notches, rounded corners, and home indicators. Use `env()`: + +```css +body { + padding-top: env(safe-area-inset-top); + padding-bottom: env(safe-area-inset-bottom); + padding-left: env(safe-area-inset-left); + padding-right: env(safe-area-inset-right); +} + +/* With fallback */ +.footer { + padding-bottom: max(1rem, env(safe-area-inset-bottom)); +} +``` + +**Enable viewport-fit** in your meta tag: +```html + +``` + +#### Responsive Images: Get It Right + +##### srcset with Width Descriptors + +```html +Hero image +``` + +**How it works**: +- `srcset` lists available images with their actual widths (`w` descriptors) +- `sizes` tells the browser how wide the image will display +- Browser picks the best file based on viewport width AND device pixel ratio + +##### Picture Element for Art Direction + +When you need different crops/compositions (not just resolutions): + +```html + + + + ... + +``` + +#### Layout Adaptation Patterns + +**Navigation**: Three stages: hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `
/` for content that can collapse on mobile. + +#### Testing: Don't Trust DevTools Alone + +DevTools device emulation is useful for layout but misses: + +- Actual touch interactions +- Real CPU/memory constraints +- Network latency patterns +- Font rendering differences +- Browser chrome/keyboard appearances + +**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators. + +--- + +**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful. diff --git a/.opencode/skills/impeccable/reference/animate.md b/.opencode/skills/impeccable/reference/animate.md index 9e274c3c7..836615fe7 100644 --- a/.opencode/skills/impeccable/reference/animate.md +++ b/.opencode/skills/impeccable/reference/animate.md @@ -6,7 +6,7 @@ Add motion that conveys state, gives feedback, and clarifies hierarchy. Cut moti ## Register -Brand: orchestrated page-load sequences, staggered reveals, scroll-driven animation. Motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions. +Brand: motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions. The saturated AI default is fade-and-rise reveals on every scrolled section; that's a tell, not a choreography. Reserve scroll-triggered motion for moments that earn it. Product: 150–250 ms on most transitions. Motion conveys state: feedback, reveal, loading, transitions between views. No page-load choreography; users are in a task and won't wait for it. @@ -49,10 +49,11 @@ Create a purposeful animation plan: Add motion systematically across these categories: ### Entrance Animations -- **Page load choreography**: Stagger element reveals (100-150ms delays), fade + slide combinations - **Hero section**: Dramatic entrance for primary content (scale, parallax, or creative effects) -- **Content reveals**: Scroll-triggered animations using intersection observer - **Modal/drawer entry**: Smooth slide + fade, backdrop fade, focus management +- **List rhythm**: Sibling stagger is legitimate for cards-in-a-grid or list-items-appearing. Whole-section fade-on-scroll is not a list and is not legitimate. Cap total stagger time: 10 items at 50ms each = 500ms total. For more items, reduce per-item delay or cap the staggered count. + + Use CSS custom properties for clean stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"`, `style="--i: 1"`, etc. on each item. ### Micro-interactions - **Button feedback**: @@ -97,11 +98,14 @@ Use appropriate techniques for each animation: ### Timing & Easing -**Durations by purpose:** -- **100-150ms**: Instant feedback (button press, toggle) -- **200-300ms**: State changes (hover, menu open) -- **300-500ms**: Layout changes (accordion, modal) -- **500-800ms**: Entrance animations (page load) +**Duration: the 100/300/500 rule.** Timing matters more than easing for "feels right": + +| Duration | Use Case | Examples | +|----------|----------|----------| +| **100–150ms** | Instant feedback | Button press, toggle, color change | +| **200–300ms** | State changes | Menu open, tooltip, hover state | +| **300–500ms** | Layout changes | Accordion, modal, drawer | +| **500–800ms** | Entrance animations | Page load, hero reveal | **Easing curves (use these, not CSS defaults):** ```css @@ -134,13 +138,35 @@ Use appropriate techniques for each animation: - GSAP for complex sequences ``` +### Motion Materials + +Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties. Match material to effect: + +- **Transform / opacity**: movement, press feedback, simple reveals, list choreography +- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances +- **Clip-path / masks**: wipes, reveals, editorial cropping, product-like transitions +- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state +- **Grid-template-rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly + +The hard rule isn't "transform and opacity only." It's: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify smoothness in-browser on target viewports. + ### Performance -- **Motion materials**: Use transform/opacity for reliable movement, but use blur, filters, masks, shadows, and color shifts when they materially improve the effect - **Layout safety**: Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins) -- **will-change**: Add sparingly for known expensive animations +- **will-change**: Add sparingly for known expensive animations only (e.g. on `:hover` or an `.animating` class), never preemptively across the whole page +- **Scroll triggers**: Use Intersection Observer instead of scroll event listeners; unobserve after the animation fires once - **Bound expensive effects**: Keep blur/filter/shadow areas small or isolated, use `contain` where appropriate - **Monitor FPS**: Ensure 60fps on target devices +### Perceived Performance + +Nobody cares how fast your site *is*, only how fast it feels. The 80ms threshold: anything under ~80ms feels instant because our brains buffer sensory input for that long to synchronize perception. Target this for micro-interactions. + +- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening. +- **Early completion**: Show content progressively, don't wait for everything (progressive images, streaming HTML, skeleton fade-ins). +- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Use for low-stakes actions (likes, follows). Avoid for payments or destructive operations. +- **Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances. +- **Caution**: Too-fast responses can decrease perceived value for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening. + ### Accessibility ```css @media (prefers-reduced-motion: reduce) { diff --git a/.opencode/skills/impeccable/reference/bolder.md b/.opencode/skills/impeccable/reference/bolder.md index 71998f37e..c5527f3cd 100644 --- a/.opencode/skills/impeccable/reference/bolder.md +++ b/.opencode/skills/impeccable/reference/bolder.md @@ -50,7 +50,7 @@ Create a strategy to increase impact while maintaining coherence: Systematically increase impact across these dimensions: ### Typography Amplification -- **Replace generic fonts**: Swap system fonts for distinctive choices (see the parent skill's typography guidelines and [typography.md](typography.md) for inspiration) +- **Replace generic fonts**: Swap system fonts for distinctive choices (see the parent skill's typography guidelines and the [Reference Material section of typeset.md](typeset.md#reference-material) for inspiration) - **Extreme scale**: Create dramatic size jumps (3x-5x differences, not 1.5x) - **Weight contrast**: Pair 900 weights with 200 weights, not 600 with 400 - **Unexpected choices**: Variable fonts, display fonts for headlines, condensed/extended widths, monospace as intentional accent (not as lazy "dev tool" default) @@ -78,10 +78,10 @@ Systematically increase impact across these dimensions: - **Custom elements**: Illustrative elements, custom icons, decorative details that reinforce brand ### Motion & Animation -- **Entrance choreography**: Staggered, dramatic page load animations with 50-100ms delays -- **Scroll effects**: Parallax, reveal animations, scroll-triggered sequences -- **Micro-interactions**: Satisfying hover effects, click feedback, state changes -- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic, which cheapen the effect) +- **Hero moment**: One signature entrance, once. Not on every visit and not on every section. +- **Micro-interactions**: Satisfying hover effects, click feedback, state changes. +- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic, which cheapen the effect). +- **Bolder ≠ scroll-fade-rise on every section.** That's the saturated AI default, the opposite of bold. ### Composition Boldness - **Hero moments**: Create clear focal points with dramatic treatment diff --git a/.opencode/skills/impeccable/reference/brand.md b/.opencode/skills/impeccable/reference/brand.md index 3d83a1cdc..194514e95 100644 --- a/.opencode/skills/impeccable/reference/brand.md +++ b/.opencode/skills/impeccable/reference/brand.md @@ -43,17 +43,10 @@ The reflex-reject lists apply to **new design choices**. When the existing brand ### Pairing and voice -Distinctive + refined is the goal. The specific shape depends on the brand: - -- **Editorial / long-form / luxury**: display serif + sans body (a magazine shape). -- **Tech / dev tools / fintech**: one committed sans, usually; custom-tight tracking, strong weight contrast inside a single family. -- **Consumer / food / travel**: warmer pairings, often a humanist sans plus a script or display serif. -- **Creative studios / agencies**: rule-breaking welcome. Mono-only, or display-only, or custom-drawn type as voice. +Distinctive + refined is the goal. The specific shape depends on the brand, not on the brand's category. A category ("restaurant", "dev tool", "magazine", "fintech") is not a recipe; treating it as one is the first-order reflex SKILL.md warns against. Two families minimum is the rule *only* when the voice needs it. A single well-chosen family with committed weight/size contrast is stronger than a timid display+body pair. -Vary across projects. If the last brief was a serif-display landing page, this one isn't. - ### Scale Modular scale, fluid `clamp()` for headings, ≥1.25 ratio between steps. Flat scales (1.1× apart) read as uncommitted. @@ -74,8 +67,7 @@ Brand surfaces have permission for Committed, Full palette, and Drenched strateg - Asymmetric compositions are one option. Break the grid intentionally for emphasis. - Fluid spacing with `clamp()` that breathes on larger viewports. Vary for rhythm: generous separations, tight groupings. -- Alternative: a strict, visible grid as the voice (brutalist / Swiss / tech-spec aesthetics). Either asymmetric or rigorously-gridded can be "designed"; the failure mode is splitting the difference into a generic centered stack. -- Don't default to centering everything. Left-aligned with asymmetric layouts feels more designed; a strict grid reads as confident structure. A centered-stack hero with icon-title-subtitle cards reads as template. +- For image-led briefs (hotels, restaurants, magazines, photography), full-bleed hero imagery with overlaid menu and centered headline is a canonical move; let the photograph be the design. - When cards ARE the right affordance, use `grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` for breakpoint-free responsiveness. ## Imagery @@ -93,8 +85,7 @@ Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landin ## Motion -- One well-orchestrated page-load with staggered reveals beats scattered micro-interactions, when the brand invites it. Tech-minimal brands often skip entrance motion entirely; the restraint is the voice. -- For collapsing/expanding sections, transition `grid-template-rows` rather than `height`. +- One well-orchestrated page-load beats scattered micro-interactions, when the brand invites it. Some brands skip entrance motion entirely; the restraint is the voice. ## Brand bans (on top of the shared absolute bans) @@ -111,8 +102,7 @@ Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landin Brand can afford things product can't. Take them. -- Ambitious first-load motion. Reveals, scroll-triggered transitions, typographic choreography. +- Ambitious first-load motion. Reveals and typographic choreography that earn their place; not fade-on-scroll for every section. - Single-purpose viewports. One dominant idea per fold, long scroll, deliberate pacing. -- Typographic risk. Enormous display type, unexpected italic cuts, mixed cases, hand-drawn headlines, a single oversize word as a hero. - Unexpected color strategies. Palette IS voice; a calm brand and a restless brand should not share palette mechanics. - Art direction per section. Different sections can have different visual worlds if the narrative demands it. Consistency of voice beats consistency of treatment. diff --git a/.opencode/skills/impeccable/reference/clarify.md b/.opencode/skills/impeccable/reference/clarify.md index 5f7d420f4..00ee978c3 100644 --- a/.opencode/skills/impeccable/reference/clarify.md +++ b/.opencode/skills/impeccable/reference/clarify.md @@ -172,3 +172,117 @@ Test that copy improvements work: - **Tone**: Is it appropriate for the situation? When the copy reads cleanly, hand off to `/impeccable polish` for the final pass. + +--- + +## Reference Material + +The sections below were previously `ux-writing.md` and live inline now so the clarify flow has its deep UX-writing reference in one place. + +### UX Writing + +#### The Button Label Problem + +**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns: + +| Bad | Good | Why | +|-----|------|-----| +| OK | Save changes | Says what will happen | +| Submit | Create account | Outcome-focused | +| Yes | Delete message | Confirms the action | +| Cancel | Keep editing | Clarifies what "cancel" means | +| Click here | Download PDF | Describes the destination | + +**For destructive actions**, name the destruction: +- "Delete" not "Remove" (delete is permanent, remove implies recoverable) +- "Delete 5 items" not "Delete selected" (show the count) + +#### Error Messages: The Formula + +Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input". + +##### Error Message Templates + +| Situation | Template | +|-----------|----------| +| **Format error** | "[Field] needs to be [format]. Example: [example]" | +| **Missing required** | "Please enter [what's missing]" | +| **Permission denied** | "You don't have access to [thing]. [What to do instead]" | +| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." | +| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" | + +##### Don't Blame the User + +Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date". + +#### Empty States Are Opportunities + +Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items". + +#### Voice vs Tone + +**Voice** is your brand's personality, consistent everywhere. +**Tone** adapts to the moment. + +| Moment | Tone Shift | +|--------|------------| +| Success | Celebratory, brief: "Done! Your changes are live." | +| Error | Empathetic, helpful: "That didn't work. Here's what to try..." | +| Loading | Reassuring: "Saving your work..." | +| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | + +**Never use humor for errors.** Users are already frustrated. Be helpful, not cute. + +#### Writing for Accessibility + +**Link text** must have standalone meaning: "View pricing plans" not "Click here". **Alt text** describes information, not the image: "Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context. + +#### Writing for Translation + +##### Plan for Expansion + +German text is ~30% longer than English. Allocate space: + +| Language | Expansion | +|----------|-----------| +| German | +30% | +| French | +20% | +| Finnish | +30-40% | +| Chinese | -30% (fewer chars, but same width) | + +##### Translation-Friendly Patterns + +Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear. + +#### Consistency: The Terminology Problem + +Pick one term and stick with it: + +| Inconsistent | Consistent | +|--------------|------------| +| Delete / Remove / Trash | Delete | +| Settings / Preferences / Options | Settings | +| Sign in / Log in / Enter | Sign in | +| Create / Add / New | Create | + +Build a terminology glossary and enforce it. Variety creates confusion. + +#### Avoid Redundant Copy + +If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well. + +#### Loading States + +Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress. + +#### Confirmation Dialogs: Use Sparingly + +Most confirmation dialogs are design failures; consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No"). + +#### Form Instructions + +Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking. + +--- + +**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors. diff --git a/.opencode/skills/impeccable/reference/codex.md b/.opencode/skills/impeccable/reference/codex.md index f40ec746f..d563af95b 100644 --- a/.opencode/skills/impeccable/reference/codex.md +++ b/.opencode/skills/impeccable/reference/codex.md @@ -23,8 +23,8 @@ Before generating anything, run a brief direction conversation grounded in the s Ask **2-3 targeted questions** about visual lane, color strategy, atmosphere, and named anchor references. Don't enumerate generic menus; tie each question to the shape brief's answers. Example shape-grounded questions: -- "Brief says 'editorial restraint, Klim-adjacent.' Are we closer to a quiet specimen page or a magazine-spread feel with hero imagery?" -- "Palette strategy from shape was 'Committed.' Want it warm-grounded (deep oxblood + cream) or cool-grounded (slate + paper white)?" +- "Brief says 'specimen-page restraint.' Are we closer to a quiet typographic page or a wider editorial spread with hero imagery?" +- "Palette strategy from shape was 'Committed.' Which one color carries the surface (a brand-driven pick rather than a default warm-or-cool framing)? (And no, the answer isn't a cream/sand body bg; that's the saturated AI default.)" **STOP and wait for answers.** These pin the palette before any pixel gets generated. Do not proceed to Step B until the user has responded. diff --git a/.opencode/skills/impeccable/reference/cognitive-load.md b/.opencode/skills/impeccable/reference/cognitive-load.md deleted file mode 100644 index 48f8ad58b..000000000 --- a/.opencode/skills/impeccable/reference/cognitive-load.md +++ /dev/null @@ -1,106 +0,0 @@ -# Cognitive Load Assessment - -Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload. - ---- - -## Three Types of Cognitive Load - -### Intrinsic Load: The Task Itself -Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it. - -**Manage it by**: -- Breaking complex tasks into discrete steps -- Providing scaffolding (templates, defaults, examples) -- Progressive disclosure: show what's needed now, hide the rest -- Grouping related decisions together - -### Extraneous Load: Bad Design -Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste. - -**Common sources**: -- Confusing navigation that requires mental mapping -- Unclear labels that force users to guess meaning -- Visual clutter competing for attention -- Inconsistent patterns that prevent learning -- Unnecessary steps between user intent and result - -### Germane Load: Learning Effort -Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery. - -**Support it by**: -- Progressive disclosure that reveals complexity gradually -- Consistent patterns that reward learning -- Feedback that confirms correct understanding -- Onboarding that teaches through action, not walls of text - ---- - -## Cognitive Load Checklist - -Evaluate the interface against these 8 items: - -- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements? -- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)? -- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)? -- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen? -- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next? -- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)? -- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one? -- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it? - -**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed). - ---- - -## The Working Memory Rule - -**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001). - -At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider: -- **≤4 items**: Within working memory limits, manageable -- **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure -- **8+ items**: Overloaded; users will skip, misclick, or abandon - -**Practical applications**: -- Navigation menus: ≤5 top-level items (group the rest under clear categories) -- Form sections: ≤4 fields visible per group before a visual break -- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu -- Dashboard widgets: ≤4 key metrics visible without scrolling -- Pricing tiers: ≤3 options (more causes analysis paralysis) - ---- - -## Common Cognitive Load Violations - -### 1. The Wall of Options -**Problem**: Presenting 10+ choices at once with no hierarchy. -**Fix**: Group into categories, highlight recommended, use progressive disclosure. - -### 2. The Memory Bridge -**Problem**: User must remember info from step 1 to complete step 3. -**Fix**: Keep relevant context visible, or repeat it where it's needed. - -### 3. The Hidden Navigation -**Problem**: User must build a mental map of where things are. -**Fix**: Always show current location (breadcrumbs, active states, progress indicators). - -### 4. The Jargon Barrier -**Problem**: Technical or domain language forces translation effort. -**Fix**: Use plain language. If domain terms are unavoidable, define them inline. - -### 5. The Visual Noise Floor -**Problem**: Every element has the same visual weight; nothing stands out. -**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted. - -### 6. The Inconsistent Pattern -**Problem**: Similar actions work differently in different places. -**Fix**: Standardize interaction patterns. Same type of action = same type of UI. - -### 7. The Multi-Task Demand -**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating). -**Fix**: Sequence the steps. Let the user do one thing at a time. - -### 8. The Context Switch -**Problem**: User must jump between screens/tabs/modals to gather info for a single decision. -**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth. diff --git a/.opencode/skills/impeccable/reference/color-and-contrast.md b/.opencode/skills/impeccable/reference/color-and-contrast.md deleted file mode 100644 index 110c2ee47..000000000 --- a/.opencode/skills/impeccable/reference/color-and-contrast.md +++ /dev/null @@ -1,105 +0,0 @@ -# Color & Contrast - -## Color Spaces: Use OKLCH - -**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal, unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark. - -The OKLCH function takes three components: `oklch(lightness chroma hue)` where lightness is 0-100%, chroma is roughly 0-0.4, and hue is 0-360. To build a primary color and its lighter / darker variants, hold the chroma+hue roughly constant and vary the lightness, but **reduce chroma as you approach white or black**, because high chroma at extreme lightness looks garish. - -The hue you pick is a brand decision and should not come from a default. Do not reach for blue (hue 250) or warm orange (hue 60) by reflex; those are the dominant AI-design defaults, not the right answer for any specific brand. - -## Building Functional Palettes - -### Tinted Neutrals - -**Pure gray is dead.** A neutral with zero chroma feels lifeless next to a colored brand. Add a tiny chroma value (0.005-0.015) to all your neutrals, hued toward whatever your brand color is. The chroma is small enough not to read as "tinted" consciously, but it creates subconscious cohesion between brand color and UI surfaces. - -The hue you tint toward should come from THIS project's brand, not from a "warm = friendly, cool = tech" formula. If your brand color is teal, your neutrals lean toward teal. If your brand color is amber, they lean toward amber. The point is cohesion with the SPECIFIC brand, not a stock palette. - -**Avoid** the trap of always tinting toward warm orange or always tinting toward cool blue. Those are the two laziest defaults and they create their own monoculture across projects. - -### Palette Structure - -A complete system needs: - -| Role | Purpose | Example | -|------|---------|---------| -| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades | -| **Neutral** | Text, backgrounds, borders | 9-11 shade scale | -| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each | -| **Surface** | Cards, modals, overlays | 2-3 elevation levels | - -**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise. - -### The 60-30-10 Rule (Applied Correctly) - -This rule is about **visual weight**, not pixel count: - -- **60%**: Neutral backgrounds, white space, base surfaces -- **30%**: Secondary colors: text, borders, inactive states -- **10%**: Accent: CTAs, highlights, focus states - -The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power. - -## Contrast & Accessibility - -### WCAG Requirements - -| Content Type | AA Minimum | AAA Target | -|--------------|------------|------------| -| Body text | 4.5:1 | 7:1 | -| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | -| UI components, icons | 3:1 | 4.5:1 | -| Non-essential decorations | None | None | - -**The gotcha**: Placeholder text still needs 4.5:1. That light gray placeholder you see everywhere? Usually fails WCAG. - -### Dangerous Color Combinations - -These commonly fail contrast or cause readability issues: - -- Light gray text on white (the #1 accessibility fail) -- **Gray text on any colored background**: gray looks washed out and dead on color. Use a darker shade of the background color, or transparency -- Red text on green background (or vice versa): 8% of men can't distinguish these -- Blue text on red background (vibrates visually) -- Yellow text on white (almost always fails) -- Thin light text on images (unpredictable contrast) - -### Never Use Pure Gray or Pure Black - -Pure gray (`oklch(50% 0 0)`) and pure black (`#000`) don't exist in nature; real shadows and surfaces always have a color cast. Even a chroma of 0.005-0.01 is enough to feel natural without being obviously tinted. (See tinted neutrals example above.) - -### Testing - -Don't trust your eyes. Use tools: - -- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) -- Browser DevTools → Rendering → Emulate vision deficiencies -- [Polypane](https://polypane.app/) for real-time testing - -## Theming: Light & Dark Mode - -### Dark Mode Is Not Inverted Light Mode - -You can't just swap colors. Dark mode requires different design decisions: - -| Light Mode | Dark Mode | -|------------|-----------| -| Shadows for depth | Lighter surfaces for depth (no shadows) | -| Dark text on light | Light text on dark (reduce font weight) | -| Vibrant accents | Desaturate accents slightly | -| White backgrounds | Never pure black; use dark gray (oklch 12-18%) | - -In dark mode, depth comes from surface lightness, not shadow. Build a 3-step surface scale where higher elevations are lighter (e.g. 15% / 20% / 25% lightness). Use the SAME hue and chroma as your brand color (whatever it is for THIS project; do not reach for blue) and only vary the lightness. Reduce body text weight slightly (e.g. 350 instead of 400) because light text on dark reads as heavier than dark text on light. - -### Token Hierarchy - -Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer; primitives stay the same. - -## Alpha Is A Design Smell - -Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed. - ---- - -**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Using pure black (#000) for large areas. Skipping color blindness testing (8% of men affected). diff --git a/.opencode/skills/impeccable/reference/colorize.md b/.opencode/skills/impeccable/reference/colorize.md index e464925b7..9ce453823 100644 --- a/.opencode/skills/impeccable/reference/colorize.md +++ b/.opencode/skills/impeccable/reference/colorize.md @@ -68,10 +68,10 @@ Add color systematically across these dimensions: - **Hover states**: Introduce color on interaction ### Background & Surfaces -- **Tinted backgrounds**: Replace pure gray (`#f5f5f5`) with warm neutrals (`oklch(97% 0.01 60)`) or cool tints (`oklch(97% 0.01 250)`) +- **Tinted backgrounds**: If you replace pure gray, tint toward the brand hue, not toward a generic-warm-or-cool pair. The default-warm-tint (`oklch(97% 0.01 60)` and its neighbors) is now the AI cream/sand giveaway. Be specific to the brand or stay neutral. - **Colored sections**: Use subtle background colors to separate areas - **Gradient backgrounds**: Add depth with subtle, intentional gradients (not generic purple-blue) -- **Cards & surfaces**: Tint cards or surfaces slightly for warmth +- **Cards & surfaces**: Tint cards or surfaces toward the brand, not "for warmth" by reflex **Use OKLCH for color**: It's perceptually uniform, meaning equal steps in lightness *look* equal. Great for generating harmonious scales. @@ -124,8 +124,6 @@ Ensure color addition improves rather than overwhelms: - Use every color in the rainbow (choose 2-4 colors beyond neutrals) - Apply color randomly without semantic meaning - Put gray text on colored backgrounds. It looks washed out; use a darker shade of the background color or transparency instead -- Use pure gray for neutrals. Add subtle color tint (warm or cool) for depth -- Use pure black (`#000`) or pure white (`#fff`) for large areas - Violate WCAG contrast requirements - Use color as the only indicator (accessibility issue) - Make everything colorful (defeats the purpose) @@ -152,3 +150,108 @@ When invoked from live mode, each variant MUST declare a `color-amount` param so ``` Layer 1-2 variant-specific params on top: palette selection (`steps` with named options), temperature warmth, or tint vs. true color. See `reference/live.md` for the full params contract. + +--- + +## Reference Material + +The sections below were previously `color-and-contrast.md` and live inline now so the colorize flow has its deep color reference in one place. + +### Color & Contrast + +#### Color Spaces: Use OKLCH + +**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal, unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark. + +The OKLCH function takes three components: `oklch(lightness chroma hue)` where lightness is 0-100%, chroma is roughly 0-0.4, and hue is 0-360. To build a primary color and its lighter / darker variants, hold the chroma+hue roughly constant and vary the lightness, but **reduce chroma as you approach white or black**, because high chroma at extreme lightness looks garish. + +The hue you pick is a brand decision and should not come from a default. Do not reach for blue (hue 250) or warm orange (hue 60) by reflex; those are the dominant AI-design defaults, not the right answer for any specific brand. + +#### Building Functional Palettes + +##### Tinted Neutrals + +**Pure gray is dead.** A neutral with zero chroma feels lifeless next to a colored brand. Add a tiny chroma value (0.005-0.015) to all your neutrals, hued toward whatever your brand color is. The chroma is small enough not to read as "tinted" consciously, but it creates subconscious cohesion between brand color and UI surfaces. + +The hue you tint toward should come from THIS project's brand, not from a "warm = friendly, cool = tech" formula. If your brand color is teal, your neutrals lean toward teal. If your brand color is amber, they lean toward amber. The point is cohesion with the SPECIFIC brand, not a stock palette. + +**Avoid** the trap of always tinting toward warm orange or always tinting toward cool blue. Those are the two laziest defaults and they create their own monoculture across projects. + +##### Palette Structure + +A complete system needs: + +| Role | Purpose | Example | +|------|---------|---------| +| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades | +| **Neutral** | Text, backgrounds, borders | 9-11 shade scale | +| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each | +| **Surface** | Cards, modals, overlays | 2-3 elevation levels | + +**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise. + +##### The 60-30-10 Rule (Applied Correctly) + +This rule is about **visual weight**, not pixel count: + +- **60%**: Neutral backgrounds, white space, base surfaces +- **30%**: Secondary colors: text, borders, inactive states +- **10%**: Accent: CTAs, highlights, focus states + +The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power. + +#### Contrast & Accessibility + +##### WCAG Requirements + +| Content Type | AA Minimum | AAA Target | +|--------------|------------|------------| +| Body text | 4.5:1 | 7:1 | +| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | +| UI components, icons | 3:1 | 4.5:1 | +| Non-essential decorations | None | None | + +##### Dangerous Color Combinations + +These commonly fail contrast or cause readability issues: + +- Light gray text on white (the #1 accessibility fail) +- Red text on green background (or vice versa): 8% of men can't distinguish these +- Blue text on red background (vibrates visually) +- Yellow text on white (almost always fails) +- Thin light text on images (unpredictable contrast) + +##### Testing + +Don't trust your eyes. Use tools: + +- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) +- Browser DevTools → Rendering → Emulate vision deficiencies +- [Polypane](https://polypane.app/) for real-time testing + +#### Theming: Light & Dark Mode + +##### Dark Mode Is Not Inverted Light Mode + +You can't just swap colors. Dark mode requires different design decisions: + +| Light Mode | Dark Mode | +|------------|-----------| +| Shadows for depth | Lighter surfaces for depth (no shadows) | +| Dark text on light | Light text on dark (reduce font weight) | +| Vibrant accents | Desaturate accents slightly | +| White backgrounds | Either pure black or a deep surface that fits the brand (a brand-tinted near-black at oklch 12-18% works too) | + +In dark mode, depth comes from surface lightness, not shadow. Build a 3-step surface scale where higher elevations are lighter (e.g. 15% / 20% / 25% lightness). Use the SAME hue and chroma as your brand color (whatever it is for THIS project; do not reach for blue) and only vary the lightness. Reduce body text weight slightly (e.g. 350 instead of 400) because light text on dark reads as heavier than dark text on light. + +##### Token Hierarchy + +Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer; primitives stay the same. + +#### Alpha Is A Design Smell + +Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed. + +--- + +**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Skipping color blindness testing (8% of men affected). diff --git a/.opencode/skills/impeccable/reference/craft.md b/.opencode/skills/impeccable/reference/craft.md index 06094b1dd..2c7bd99b1 100644 --- a/.opencode/skills/impeccable/reference/craft.md +++ b/.opencode/skills/impeccable/reference/craft.md @@ -56,15 +56,15 @@ If the harness has native image generation (Codex), a compact shape's "confirm o Based on the design brief's "Recommended References" section, consult the relevant impeccable reference files. At minimum, always consult: -- [spatial-design.md](spatial-design.md) for layout and spacing -- [typography.md](typography.md) for type hierarchy +- [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 [motion-design.md](motion-design.md) -- Color-heavy or themed? Consult [color-and-contrast.md](color-and-contrast.md) -- Responsive requirements? Consult [responsive-design.md](responsive-design.md) -- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.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) diff --git a/.opencode/skills/impeccable/reference/critique.md b/.opencode/skills/impeccable/reference/critique.md index 2e017b6cf..aaadcd1fa 100644 --- a/.opencode/skills/impeccable/reference/critique.md +++ b/.opencode/skills/impeccable/reference/critique.md @@ -38,9 +38,9 @@ Read relevant source files and visually inspect the live page when browser autom Evaluate: - **AI slop**: Would someone believe "AI made this" immediately? Check all DON'T guidance from the parent Impeccable skill. - **Holistic design**: hierarchy, IA, emotional fit, discoverability, composition, typography, color, accessibility, states, copy, and edge cases. -- **Cognitive load**: consult [cognitive-load](cognitive-load.md); report checklist failures and decision points with >4 visible options. +- **Cognitive load**: consult the [Cognitive Load Assessment](#cognitive-load-assessment) section below; report checklist failures and decision points with >4 visible options. - **Emotional journey**: peak-end rule, emotional valleys, reassurance at high-stakes moments. -- **Nielsen heuristics**: consult [heuristics-scoring](heuristics-scoring.md); score all 10 heuristics 0-4. +- **Nielsen heuristics**: consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below; score all 10 heuristics 0-4. Return: AI slop verdict, heuristic scores, cognitive load, emotional journey, 2-3 strengths, 3-5 priority issues, persona red flags, minor observations, and provocative questions. @@ -80,7 +80,7 @@ The chat response is the primary user-facing deliverable. Present the full struc Structure your feedback as a design director would: #### Design Health Score -> *Consult [heuristics-scoring](heuristics-scoring.md)* +> *Consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below.* Present the Nielsen's 10 heuristics scores as a table: @@ -119,16 +119,16 @@ Highlight 2-3 things done well. Be specific about why they work. #### Priority Issues The 3-5 most impactful design problems, ordered by importance. -For each issue, tag with **P0-P3 severity** (consult [heuristics-scoring](heuristics-scoring.md) for severity definitions): +For each issue, tag with **P0-P3 severity** (see [Issue Severity below](#issue-severity-p0p3) for definitions): - **[P?] What**: Name the problem clearly - **Why it matters**: How this hurts users or undermines goals - **Fix**: What to do about it (be concrete) - **Suggested command**: Which command could address this (from: /impeccable adapt, /impeccable animate, /impeccable audit, /impeccable bolder, /impeccable clarify, /impeccable colorize, /impeccable critique, /impeccable delight, /impeccable distill, /impeccable document, /impeccable harden, /impeccable layout, /impeccable onboard, /impeccable optimize, /impeccable overdrive, /impeccable polish, /impeccable quieter, /impeccable shape, /impeccable typeset) #### Persona Red Flags -> *Consult [personas](personas.md)* +> *Consult the [Personas reference](#persona-based-design-testing) below.* -Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `AGENTS.md` contains a `## Design Context` section from `impeccable teach`, also generate 1-2 project-specific personas from the audience/brand info. +Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `AGENTS.md` contains a `## Design Context` section from `impeccable init`, also generate 1-2 project-specific personas from the audience/brand info. For each selected persona, walk through the primary user action and list specific red flags found: @@ -234,3 +234,534 @@ After presenting the summary, tell the user: > You can ask me to run these one at a time, all at once, or in any order you prefer. > > Re-run `/impeccable critique` after fixes to see your score improve. + +--- + +## Reference Material + +The sections below were previously separate reference files (`cognitive-load.md`, `heuristics-scoring.md`, `personas.md`). They live inline now so the critique flow has all its deep context in one place. + +### Cognitive Load Assessment + +Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload. + +--- + +#### Three Types of Cognitive Load + +##### Intrinsic Load: The Task Itself +Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it. + +**Manage it by**: +- Breaking complex tasks into discrete steps +- Providing scaffolding (templates, defaults, examples) +- Progressive disclosure: show what's needed now, hide the rest +- Grouping related decisions together + +##### Extraneous Load: Bad Design +Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste. + +**Common sources**: +- Confusing navigation that requires mental mapping +- Unclear labels that force users to guess meaning +- Visual clutter competing for attention +- Inconsistent patterns that prevent learning +- Unnecessary steps between user intent and result + +##### Germane Load: Learning Effort +Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery. + +**Support it by**: +- Progressive disclosure that reveals complexity gradually +- Consistent patterns that reward learning +- Feedback that confirms correct understanding +- Onboarding that teaches through action, not walls of text + +--- + +#### Cognitive Load Checklist + +Evaluate the interface against these 8 items: + +- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements? +- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)? +- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)? +- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen? +- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next? +- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)? +- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one? +- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it? + +**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed). + +--- + +#### The Working Memory Rule + +**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001). + +At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider: +- **≤4 items**: Within working memory limits, manageable +- **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure +- **8+ items**: Overloaded; users will skip, misclick, or abandon + +**Practical applications**: +- Navigation menus: ≤5 top-level items (group the rest under clear categories) +- Form sections: ≤4 fields visible per group before a visual break +- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu +- Dashboard widgets: ≤4 key metrics visible without scrolling +- Pricing tiers: ≤3 options (more causes analysis paralysis) + +--- + +#### Common Cognitive Load Violations + +##### 1. The Wall of Options +**Problem**: Presenting 10+ choices at once with no hierarchy. +**Fix**: Group into categories, highlight recommended, use progressive disclosure. + +##### 2. The Memory Bridge +**Problem**: User must remember info from step 1 to complete step 3. +**Fix**: Keep relevant context visible, or repeat it where it's needed. + +##### 3. The Hidden Navigation +**Problem**: User must build a mental map of where things are. +**Fix**: Always show current location (breadcrumbs, active states, progress indicators). + +##### 4. The Jargon Barrier +**Problem**: Technical or domain language forces translation effort. +**Fix**: Use plain language. If domain terms are unavoidable, define them inline. + +##### 5. The Visual Noise Floor +**Problem**: Every element has the same visual weight; nothing stands out. +**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted. + +##### 6. The Inconsistent Pattern +**Problem**: Similar actions work differently in different places. +**Fix**: Standardize interaction patterns. Same type of action = same type of UI. + +##### 7. The Multi-Task Demand +**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating). +**Fix**: Sequence the steps. Let the user do one thing at a time. + +##### 8. The Context Switch +**Problem**: User must jump between screens/tabs/modals to gather info for a single decision. +**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth. + +--- + +### Heuristics Scoring Guide + +Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough." + +#### Nielsen's 10 Heuristics + +##### 1. Visibility of System Status + +Keep users informed about what's happening through timely, appropriate feedback. + +**Check for**: +- Loading indicators during async operations +- Confirmation of user actions (save, submit, delete) +- Progress indicators for multi-step processes +- Current location in navigation (breadcrumbs, active states) +- Form validation feedback (inline, not just on submit) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | No feedback; user is guessing what happened | +| 1 | Rare feedback; most actions produce no visible response | +| 2 | Partial; some states communicated, major gaps remain | +| 3 | Good; most operations give clear feedback, minor gaps | +| 4 | Excellent; every action confirms, progress is always visible | + +##### 2. Match Between System and Real World + +Speak the user's language. Follow real-world conventions. Information appears in natural, logical order. + +**Check for**: +- Familiar terminology (no unexplained jargon) +- Logical information order matching user expectations +- Recognizable icons and metaphors +- Domain-appropriate language for the target audience +- Natural reading flow (left-to-right, top-to-bottom priority) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Pure tech jargon, alien to users | +| 1 | Mostly confusing; requires domain expertise to navigate | +| 2 | Mixed; some plain language, some jargon leaks through | +| 3 | Mostly natural; occasional term needs context | +| 4 | Speaks the user's language fluently throughout | + +##### 3. User Control and Freedom + +Users need a clear "emergency exit" from unwanted states without extended dialogue. + +**Check for**: +- Undo/redo functionality +- Cancel buttons on forms and modals +- Clear navigation back to safety (home, previous) +- Easy way to clear filters, search, selections +- Escape from long or multi-step processes + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Users get trapped; no way out without refreshing | +| 1 | Difficult exits; must find obscure paths to escape | +| 2 | Some exits; main flows have escape, edge cases don't | +| 3 | Good control; users can exit and undo most actions | +| 4 | Full control; undo, cancel, back, and escape everywhere | + +##### 4. Consistency and Standards + +Users shouldn't wonder whether different words, situations, or actions mean the same thing. + +**Check for**: +- Consistent terminology throughout the interface +- Same actions produce same results everywhere +- Platform conventions followed (standard UI patterns) +- Visual consistency (colors, typography, spacing, components) +- Consistent interaction patterns (same gesture = same behavior) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Inconsistent everywhere; feels like different products stitched together | +| 1 | Many inconsistencies; similar things look/behave differently | +| 2 | Partially consistent; main flows match, details diverge | +| 3 | Mostly consistent; occasional deviation, nothing confusing | +| 4 | Fully consistent; cohesive system, predictable behavior | + +##### 5. Error Prevention + +Better than good error messages is a design that prevents problems in the first place. + +**Check for**: +- Confirmation before destructive actions (delete, overwrite) +- Constraints preventing invalid input (date pickers, dropdowns) +- Smart defaults that reduce errors +- Clear labels that prevent misunderstanding +- Autosave and draft recovery + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Errors easy to make; no guardrails anywhere | +| 1 | Few safeguards; some inputs validated, most aren't | +| 2 | Partial prevention; common errors caught, edge cases slip | +| 3 | Good prevention; most error paths blocked proactively | +| 4 | Excellent; errors nearly impossible through smart constraints | + +##### 6. Recognition Rather Than Recall + +Minimize memory load. Make objects, actions, and options visible or easily retrievable. + +**Check for**: +- Visible options (not buried in hidden menus) +- Contextual help when needed (tooltips, inline hints) +- Recent items and history +- Autocomplete and suggestions +- Labels on icons (not icon-only navigation) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Heavy memorization; users must remember paths and commands | +| 1 | Mostly recall; many hidden features, few visible cues | +| 2 | Some aids; main actions visible, secondary features hidden | +| 3 | Good recognition; most things discoverable, few memory demands | +| 4 | Everything discoverable; users never need to memorize | + +##### 7. Flexibility and Efficiency of Use + +Accelerators, invisible to novices, speed up expert interaction. + +**Check for**: +- Keyboard shortcuts for common actions +- Customizable interface elements +- Recent items and favorites +- Bulk/batch actions +- Power user features that don't complicate the basics + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | One rigid path; no shortcuts or alternatives | +| 1 | Limited flexibility; few alternatives to the main path | +| 2 | Some shortcuts; basic keyboard support, limited bulk actions | +| 3 | Good accelerators; keyboard nav, some customization | +| 4 | Highly flexible; multiple paths, power features, customizable | + +##### 8. Aesthetic and Minimalist Design + +Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose. + +**Check for**: +- Only necessary information visible at each step +- Clear visual hierarchy directing attention +- Purposeful use of color and emphasis +- No decorative clutter competing for attention +- Focused, uncluttered layouts + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Overwhelming; everything competes for attention equally | +| 1 | Cluttered; too much noise, hard to find what matters | +| 2 | Some clutter; main content clear, periphery noisy | +| 3 | Mostly clean; focused design, minor visual noise | +| 4 | Perfectly minimal; every element earns its pixel | + +##### 9. Help Users Recognize, Diagnose, and Recover from Errors + +Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution. + +**Check for**: +- Plain language error messages (no error codes for users) +- Specific problem identification ("Email is missing @" not "Invalid input") +- Actionable recovery suggestions +- Errors displayed near the source of the problem +- Non-blocking error handling (don't wipe the form) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Cryptic errors; codes, jargon, or no message at all | +| 1 | Vague errors; "Something went wrong" with no guidance | +| 2 | Clear but unhelpful; names the problem but not the fix | +| 3 | Clear with suggestions; identifies problem and offers next steps | +| 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work | + +##### 10. Help and Documentation + +Even if the system is usable without docs, help should be easy to find, task-focused, and concise. + +**Check for**: +- Searchable help or documentation +- Contextual help (tooltips, inline hints, guided tours) +- Task-focused organization (not feature-organized) +- Concise, scannable content +- Easy access without leaving current context + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | No help available anywhere | +| 1 | Help exists but hard to find or irrelevant | +| 2 | Basic help; FAQ or docs exist, not contextual | +| 3 | Good documentation; searchable, mostly task-focused | +| 4 | Excellent contextual help; right info at the right moment | + +--- + +#### Score Summary + +**Total possible**: 40 points (10 heuristics × 4 max) + +| Score Range | Rating | What It Means | +|-------------|--------|---------------| +| 36–40 | Excellent | Minor polish only; ship it | +| 28–35 | Good | Address weak areas, solid foundation | +| 20–27 | Acceptable | Significant improvements needed before users are happy | +| 12–19 | Poor | Major UX overhaul required; core experience broken | +| 0–11 | Critical | Redesign needed; unusable in current state | + +--- + +#### Issue Severity (P0–P3) + +Tag each individual issue found during scoring with a priority level: + +| Priority | Name | Description | Action | +|----------|------|-------------|--------| +| **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper | +| **P1** | Major | Causes significant difficulty or confusion | Fix before release | +| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass | +| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits | + +**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1. + +--- + +### Persona-Based Design Testing + +Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss. + +**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns. + +--- + +#### 1. Impatient Power User: "Alex" + +**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave. + +**Behaviors**: +- Skips all onboarding and instructions +- Looks for keyboard shortcuts immediately +- Tries to bulk-select, batch-edit, and automate +- Gets frustrated by required steps that feel unnecessary +- Abandons if anything feels slow or patronizing + +**Test Questions**: +- Can Alex complete the core task in under 60 seconds? +- Are there keyboard shortcuts for common actions? +- Can onboarding be skipped entirely? +- Do modals have keyboard dismiss (Esc)? +- Is there a "power user" path (shortcuts, bulk actions)? + +**Red Flags** (report these specifically): +- Forced tutorials or unskippable onboarding +- No keyboard navigation for primary actions +- Slow animations that can't be skipped +- One-item-at-a-time workflows where batch would be natural +- Redundant confirmation steps for low-risk actions + +--- + +#### 2. Confused First-Timer: "Jordan" + +**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out. + +**Behaviors**: +- Reads all instructions carefully +- Hesitates before clicking anything unfamiliar +- Looks for help or support constantly +- Misunderstands jargon and abbreviations +- Takes the most literal interpretation of any label + +**Test Questions**: +- Is the first action obviously clear within 5 seconds? +- Are all icons labeled with text? +- Is there contextual help at decision points? +- Does terminology assume prior knowledge? +- Is there a clear "back" or "undo" at every step? + +**Red Flags** (report these specifically): +- Icon-only navigation with no labels +- Technical jargon without explanation +- No visible help option or guidance +- Ambiguous next steps after completing an action +- No confirmation that an action succeeded + +--- + +#### 3. Accessibility-Dependent User: "Sam" + +**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences. + +**Behaviors**: +- Tabs through the interface linearly +- Relies on ARIA labels and heading structure +- Cannot see hover states or visual-only indicators +- Needs adequate color contrast (4.5:1 minimum) +- May use browser zoom up to 200% + +**Test Questions**: +- Can the entire primary flow be completed keyboard-only? +- Are all interactive elements focusable with visible focus indicators? +- Do images have meaningful alt text? +- Is color contrast WCAG AA compliant (4.5:1 for text)? +- Does the screen reader announce state changes (loading, success, errors)? + +**Red Flags** (report these specifically): +- Click-only interactions with no keyboard alternative +- Missing or invisible focus indicators +- Meaning conveyed by color alone (red = error, green = success) +- Unlabeled form fields or buttons +- Time-limited actions without extension option +- Custom components that break screen reader flow + +--- + +#### 4. Deliberate Stress Tester: "Riley" + +**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience. + +**Behaviors**: +- Tests edge cases intentionally (empty states, long strings, special characters) +- Submits forms with unexpected data (emoji, RTL text, very long values) +- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs +- Looks for inconsistencies between what the UI promises and what actually happens +- Documents problems methodically + +**Test Questions**: +- What happens at the edges (0 items, 1000 items, very long text)? +- Do error states recover gracefully or leave the UI in a broken state? +- What happens on refresh mid-workflow? Is state preserved? +- Are there features that appear to work but produce broken results? +- How does the UI handle unexpected input (emoji, special chars, paste from Excel)? + +**Red Flags** (report these specifically): +- Features that appear to work but silently fail or produce wrong results +- Error handling that exposes technical details or leaves UI in a broken state +- Empty states that show nothing useful ("No results" with no guidance) +- Workflows that lose user data on refresh or navigation +- Inconsistent behavior between similar interactions in different parts of the UI + +--- + +#### 5. Distracted Mobile User: "Casey" + +**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection. + +**Behaviors**: +- Uses thumb only; prefers bottom-of-screen actions +- Gets interrupted mid-flow and returns later +- Switches between apps frequently +- Has limited attention span and low patience +- Types as little as possible, prefers taps and selections + +**Test Questions**: +- Are primary actions in the thumb zone (bottom half of screen)? +- Is state preserved if the user leaves and returns? +- Does it work on slow connections (3G)? +- Can forms use autocomplete and smart defaults? +- Are touch targets at least 44×44pt? + +**Red Flags** (report these specifically): +- Important actions positioned at the top of the screen (unreachable by thumb) +- No state persistence; progress lost on tab switch or interruption +- Large text inputs required where selection would work +- Heavy assets loading on every page (no lazy loading) +- Tiny tap targets or targets too close together + +--- + +#### Selecting Personas + +Choose personas based on the interface type: + +| Interface Type | Primary Personas | Why | +|---------------|-----------------|-----| +| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile | +| Dashboard / admin | Alex, Sam | Power users, accessibility | +| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity | +| Onboarding flow | Jordan, Casey | Confusion, interruption | +| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav | +| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile | + +--- + +#### Project-Specific Personas + +If `AGENTS.md` contains a `## Design Context` section (generated by `impeccable init`), derive 1–2 additional personas from the audience and brand information: + +1. Read the target audience description +2. Identify the primary user archetype not covered by the 5 predefined personas +3. Create a persona following this template: + +``` +##### [Role]: "[Name]" + +**Profile**: [2-3 key characteristics derived from Design Context] + +**Behaviors**: [3-4 specific behaviors based on the described audience] + +**Red Flags**: [3-4 things that would alienate this specific user type] +``` + +Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists. diff --git a/.opencode/skills/impeccable/reference/document.md b/.opencode/skills/impeccable/reference/document.md index ad09f87cb..0186c7d81 100644 --- a/.opencode/skills/impeccable/reference/document.md +++ b/.opencode/skills/impeccable/reference/document.md @@ -45,7 +45,7 @@ Rules that matter: - **Token refs** use `{path.to.token}` (e.g. `{colors.primary}`, `{rounded.md}`). Components may reference primitives; primitives may not reference each other. - **Stitch validates colors as hex sRGB only** (`#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`); OKLCH/HSL/P3 trigger a linter warning, not a hard error. YAML accepts the string either way and our own parser is format-agnostic. Choose based on project posture: (a) if the project has an "OKLCH-only" doctrine or uses Display-P3 values that don't round-trip through sRGB, put OKLCH directly in the frontmatter and accept the Stitch linter warning; (b) if the project wants strict Stitch compliance or plans to use their Tailwind/DTCG export pipeline, put hex in the frontmatter and keep OKLCH in prose as the canonical reference. Never split the source of truth without explicit reason. - **Component sub-tokens** are limited to 8 props: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, `width`. Shadows, motion, focus rings, backdrop-filter: none of those fit. Carry them in the sidecar (Step 4b). -- **Scale keys are open-ended.** Use whatever names the project already uses (`warm-ash-cream`, `surface-container-low`). Don't rename to Material defaults. +- **Scale keys are open-ended.** Use whatever names the project already uses (`oxblood-deep`, `surface-container-low`). Don't rename to Material defaults. - **Variants are naming convention, not schema.** `button-primary` / `button-primary-hover` / `button-primary-active` as sibling keys. ## The markdown body: six sections (exact order) @@ -61,7 +61,7 @@ Optional evocative subtitles are allowed in the form `## 2. Colors: The [Name] P ## When to run -- The user just ran `/impeccable teach` and needs the visual side documented. +- The user just ran `/impeccable init` and needs the visual side documented. - The skill noticed no `DESIGN.md` exists and nudged the user to create one. - An existing `DESIGN.md` is stale (the design has drifted). - Before a large redesign, to capture the current state as a reference. @@ -71,7 +71,7 @@ If a `DESIGN.md` already exists, **do not silently overwrite it**. Show the user ## Two paths - **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 teach, nothing built yet). Interview for five high-level answers, write a minimal DESIGN.md marked ``. Re-run in scan mode once there's code. +- **Seed mode**: the project is pre-implementation (fresh init, nothing built yet). Interview for five high-level answers, 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` forces seed mode regardless of code presence. @@ -103,7 +103,7 @@ Build a structured draft from the discovered tokens. For each token class: From the auto-extracted tokens, draft the YAML frontmatter now (you'll write it at the top of DESIGN.md in Step 4). This is the machine-readable layer: what the live panel and Stitch's linter consume. -- **Colors**: one entry per extracted color. Key = descriptive slug (`warm-ash-cream`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. +- **Colors**: one entry per extracted color. Key = descriptive slug (`oxblood-deep`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. - **Typography**: one entry per role (`display`, `headline`, `title`, `body`, `label`). Typography is an object; include only the props that are real for the project (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`). - **Rounded / Spacing**: whatever scale steps the project actually uses, keyed by whatever scale name the project uses (`sm` / `md` / `lg`, or `surface-sm`, or numeric steps). - **Components**: one entry per variant (`button-primary`, `button-primary-hover`, `button-ghost`). Reference primitives via `{colors.X}`, `{rounded.Y}`. If a variant needs a property Stitch's 8-prop set doesn't cover (shadow, focus ring, backdrop-filter), carry the full snippet in the sidecar instead. @@ -253,7 +253,7 @@ Regenerate the sidecar whenever you regenerate root `DESIGN.md`. If the user onl "extensions": { "colorMeta": { "primary": { "role": "primary", "displayName": "Editorial Magenta", "canonical": "oklch(60% 0.25 350)", "tonalRamp": ["...", "...", "..."] }, - "warm-ash-cream": { "role": "neutral", "displayName": "Warm Ash Cream", "canonical": "oklch(96% 0.005 350)", "tonalRamp": ["...", "...", "..."] } + "cool-paper": { "role": "neutral", "displayName": "Cool Paper", "canonical": "oklch(96% 0.005 230)", "tonalRamp": ["...", "...", "..."] } }, "typographyMeta": { "display": { "displayName": "Display", "purpose": "Hero headlines only." } @@ -328,12 +328,13 @@ Pull directly from the DESIGN.md you just wrote: Do not reword. The panel shows these as secondary collapsible context; the same voice that's in the Markdown carries through. -### Step 5: Confirm, refine, and refresh session cache +### Step 5: Confirm and refine 1. Show the user the full DESIGN.md you wrote. Briefly highlight the non-obvious creative choices (descriptive color names, atmosphere language, named rules). 2. Mention that `.impeccable/design.json` was also written alongside; the live panel will now render this project's actual button/input/nav primitives instead of generic approximations. 3. Offer to refine any section: "Want me to revise a section, add component patterns I missed, or adjust the atmosphere language?" -4. **Refresh the session cache.** Run `node .opencode/skills/impeccable/scripts/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. + +Your own write is the freshest source; subsequent commands in this session don't need a reload. ## Seed mode @@ -394,11 +395,12 @@ Per-section guidance in seed mode: 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. -### Step 4: Confirm and refresh session cache +### Step 4: Confirm 1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment). 2. Tell the user: "Re-run `/impeccable document` once you have some code. That pass will extract real tokens and generate the sidecar." -3. Run `node .opencode/skills/impeccable/scripts/load-context.mjs` once so the seed lands in conversation for the rest of the session. + +Your own write is the freshest source; no reload needed. ## Style guidelines diff --git a/.opencode/skills/impeccable/reference/heuristics-scoring.md b/.opencode/skills/impeccable/reference/heuristics-scoring.md deleted file mode 100644 index edbe5028d..000000000 --- a/.opencode/skills/impeccable/reference/heuristics-scoring.md +++ /dev/null @@ -1,234 +0,0 @@ -# Heuristics Scoring Guide - -Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough." - -## Nielsen's 10 Heuristics - -### 1. Visibility of System Status - -Keep users informed about what's happening through timely, appropriate feedback. - -**Check for**: -- Loading indicators during async operations -- Confirmation of user actions (save, submit, delete) -- Progress indicators for multi-step processes -- Current location in navigation (breadcrumbs, active states) -- Form validation feedback (inline, not just on submit) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | No feedback; user is guessing what happened | -| 1 | Rare feedback; most actions produce no visible response | -| 2 | Partial; some states communicated, major gaps remain | -| 3 | Good; most operations give clear feedback, minor gaps | -| 4 | Excellent; every action confirms, progress is always visible | - -### 2. Match Between System and Real World - -Speak the user's language. Follow real-world conventions. Information appears in natural, logical order. - -**Check for**: -- Familiar terminology (no unexplained jargon) -- Logical information order matching user expectations -- Recognizable icons and metaphors -- Domain-appropriate language for the target audience -- Natural reading flow (left-to-right, top-to-bottom priority) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Pure tech jargon, alien to users | -| 1 | Mostly confusing; requires domain expertise to navigate | -| 2 | Mixed; some plain language, some jargon leaks through | -| 3 | Mostly natural; occasional term needs context | -| 4 | Speaks the user's language fluently throughout | - -### 3. User Control and Freedom - -Users need a clear "emergency exit" from unwanted states without extended dialogue. - -**Check for**: -- Undo/redo functionality -- Cancel buttons on forms and modals -- Clear navigation back to safety (home, previous) -- Easy way to clear filters, search, selections -- Escape from long or multi-step processes - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Users get trapped; no way out without refreshing | -| 1 | Difficult exits; must find obscure paths to escape | -| 2 | Some exits; main flows have escape, edge cases don't | -| 3 | Good control; users can exit and undo most actions | -| 4 | Full control; undo, cancel, back, and escape everywhere | - -### 4. Consistency and Standards - -Users shouldn't wonder whether different words, situations, or actions mean the same thing. - -**Check for**: -- Consistent terminology throughout the interface -- Same actions produce same results everywhere -- Platform conventions followed (standard UI patterns) -- Visual consistency (colors, typography, spacing, components) -- Consistent interaction patterns (same gesture = same behavior) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Inconsistent everywhere; feels like different products stitched together | -| 1 | Many inconsistencies; similar things look/behave differently | -| 2 | Partially consistent; main flows match, details diverge | -| 3 | Mostly consistent; occasional deviation, nothing confusing | -| 4 | Fully consistent; cohesive system, predictable behavior | - -### 5. Error Prevention - -Better than good error messages is a design that prevents problems in the first place. - -**Check for**: -- Confirmation before destructive actions (delete, overwrite) -- Constraints preventing invalid input (date pickers, dropdowns) -- Smart defaults that reduce errors -- Clear labels that prevent misunderstanding -- Autosave and draft recovery - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Errors easy to make; no guardrails anywhere | -| 1 | Few safeguards; some inputs validated, most aren't | -| 2 | Partial prevention; common errors caught, edge cases slip | -| 3 | Good prevention; most error paths blocked proactively | -| 4 | Excellent; errors nearly impossible through smart constraints | - -### 6. Recognition Rather Than Recall - -Minimize memory load. Make objects, actions, and options visible or easily retrievable. - -**Check for**: -- Visible options (not buried in hidden menus) -- Contextual help when needed (tooltips, inline hints) -- Recent items and history -- Autocomplete and suggestions -- Labels on icons (not icon-only navigation) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Heavy memorization; users must remember paths and commands | -| 1 | Mostly recall; many hidden features, few visible cues | -| 2 | Some aids; main actions visible, secondary features hidden | -| 3 | Good recognition; most things discoverable, few memory demands | -| 4 | Everything discoverable; users never need to memorize | - -### 7. Flexibility and Efficiency of Use - -Accelerators, invisible to novices, speed up expert interaction. - -**Check for**: -- Keyboard shortcuts for common actions -- Customizable interface elements -- Recent items and favorites -- Bulk/batch actions -- Power user features that don't complicate the basics - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | One rigid path; no shortcuts or alternatives | -| 1 | Limited flexibility; few alternatives to the main path | -| 2 | Some shortcuts; basic keyboard support, limited bulk actions | -| 3 | Good accelerators; keyboard nav, some customization | -| 4 | Highly flexible; multiple paths, power features, customizable | - -### 8. Aesthetic and Minimalist Design - -Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose. - -**Check for**: -- Only necessary information visible at each step -- Clear visual hierarchy directing attention -- Purposeful use of color and emphasis -- No decorative clutter competing for attention -- Focused, uncluttered layouts - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Overwhelming; everything competes for attention equally | -| 1 | Cluttered; too much noise, hard to find what matters | -| 2 | Some clutter; main content clear, periphery noisy | -| 3 | Mostly clean; focused design, minor visual noise | -| 4 | Perfectly minimal; every element earns its pixel | - -### 9. Help Users Recognize, Diagnose, and Recover from Errors - -Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution. - -**Check for**: -- Plain language error messages (no error codes for users) -- Specific problem identification ("Email is missing @" not "Invalid input") -- Actionable recovery suggestions -- Errors displayed near the source of the problem -- Non-blocking error handling (don't wipe the form) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Cryptic errors; codes, jargon, or no message at all | -| 1 | Vague errors; "Something went wrong" with no guidance | -| 2 | Clear but unhelpful; names the problem but not the fix | -| 3 | Clear with suggestions; identifies problem and offers next steps | -| 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work | - -### 10. Help and Documentation - -Even if the system is usable without docs, help should be easy to find, task-focused, and concise. - -**Check for**: -- Searchable help or documentation -- Contextual help (tooltips, inline hints, guided tours) -- Task-focused organization (not feature-organized) -- Concise, scannable content -- Easy access without leaving current context - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | No help available anywhere | -| 1 | Help exists but hard to find or irrelevant | -| 2 | Basic help; FAQ or docs exist, not contextual | -| 3 | Good documentation; searchable, mostly task-focused | -| 4 | Excellent contextual help; right info at the right moment | - ---- - -## Score Summary - -**Total possible**: 40 points (10 heuristics × 4 max) - -| Score Range | Rating | What It Means | -|-------------|--------|---------------| -| 36–40 | Excellent | Minor polish only; ship it | -| 28–35 | Good | Address weak areas, solid foundation | -| 20–27 | Acceptable | Significant improvements needed before users are happy | -| 12–19 | Poor | Major UX overhaul required; core experience broken | -| 0–11 | Critical | Redesign needed; unusable in current state | - ---- - -## Issue Severity (P0–P3) - -Tag each individual issue found during scoring with a priority level: - -| Priority | Name | Description | Action | -|----------|------|-------------|--------| -| **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper | -| **P1** | Major | Causes significant difficulty or confusion | Fix before release | -| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass | -| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits | - -**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1. diff --git a/.opencode/skills/impeccable/reference/teach.md b/.opencode/skills/impeccable/reference/init.md similarity index 58% rename from .opencode/skills/impeccable/reference/teach.md rename to .opencode/skills/impeccable/reference/init.md index 93c2a74f0..9e2ea6d06 100644 --- a/.opencode/skills/impeccable/reference/teach.md +++ b/.opencode/skills/impeccable/reference/init.md @@ -1,21 +1,16 @@ -# Teach Flow +# Init Flow -Gathers design context for a project and writes two complementary files at the project root: +The setup command for a project. One codebase crawl feeds everything it writes: - **PRODUCT.md** (strategic): root project file for register, target users, product purpose, brand personality, anti-references, strategic design principles. Answers "who/what/why". - **DESIGN.md** (visual): root project file for visual theme, color palette, typography, components, layout. Follows the [Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/). Answers "how it looks". +- **`.impeccable/live/config.json`** (live mode): pre-configured so `/impeccable live` boots straight into variant mode with no first-time detour. -Every other impeccable command reads these files before doing any work. +It closes by pointing the user at the best command to run next. Every other impeccable command reads PRODUCT.md and DESIGN.md before doing any work. ## Step 1: Load current state -Run the shared loader first so you know what already exists: - -```bash -node .opencode/skills/impeccable/scripts/load-context.mjs -``` - -The output tells you whether PRODUCT.md and/or DESIGN.md already exist. If `migrated: true`, legacy `.impeccable.md` was auto-renamed to `PRODUCT.md`. Mention this once to the user. +Check what already exists. PRODUCT.md and DESIGN.md live at the project root, or under `.agents/context/` or `docs/` (case-insensitive). Read whichever are present with your native file tool. Also note whether `.impeccable/live/config.json` already exists (Step 6 leaves it untouched if so). Decision tree: - **Neither file exists (empty project or no context yet)**: do Steps 2-4 (write PRODUCT.md), then decide on DESIGN.md based on whether there's code to analyze. @@ -26,14 +21,14 @@ Decision tree: Never silently overwrite an existing file. Always confirm first. -If teach was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete teach, re-run the loader, then resume the original command with the freshly loaded context. For craft, resume into shape next; teach creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief. +If init was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete init, then resume the original command. Your own writes are the freshest source; no reload needed. For craft, resume into shape next; init creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief. ## Step 2: Explore the codebase -Before asking questions, thoroughly scan the project to discover what you can: +Before asking questions, thoroughly scan the project to discover what you can. This single crawl feeds PRODUCT.md, DESIGN.md, **and** the live-mode framework detection in Step 6, so be thorough once rather than re-scanning later: - **README and docs**: Project purpose, target audience, any stated goals -- **Package.json / config files**: Tech stack, dependencies, existing design libraries +- **Package.json / config files**: Tech stack, dependencies, existing design libraries, **and the framework** (Vite/SPA, Next.js, Nuxt, SvelteKit, Astro, multi-page static) plus the HTML entry the browser actually loads - **Existing components**: Current design patterns, spacing, typography in use - **Brand assets**: Logos, favicons, color values already defined - **Design tokens / CSS variables**: Existing color palettes, font stacks, spacing scales @@ -46,7 +41,7 @@ Also form a **register hypothesis** from what you find: Register is a hypothesis at this point, not a decision; Step 3 confirms it. -Note what you've learned and what remains unclear. This exploration feeds both PRODUCT.md and DESIGN.md. +Note what you've learned and what remains unclear. Also note any rough edges worth a follow-up command (thin hierarchy, flat or gray palette, missing error/empty states, dull copy); Step 7 turns these into concrete recommendations without re-analyzing. ## Step 3: Ask strategic questions (for PRODUCT.md) @@ -84,8 +79,7 @@ If the signal is genuinely split (e.g. a product with a big marketing landing), ### Brand & Personality - How would you describe the brand personality in 3 words? - Reference sites or apps that capture the right feel? What specifically about them? - - For brand, push for real-world references in the right lane (tech-minimal, editorial-magazine, consumer-warm, brutalist-grid, etc.), not generic "modern" adjectives. - - For product, push for category best-tool references (Linear, Figma, Notion, Raycast, Stripe). + - Push for specific named references with the *specific* thing about them that fits this brand, not generic "modern" adjectives or category-bucket lanes. - What should this explicitly NOT look like? Any anti-references? ### Accessibility & Inclusion @@ -141,16 +135,38 @@ If the user agrees, delegate to `/impeccable document` (it auto-detects scan vs If the user prefers to skip, mention they can run `/impeccable document` any time later. -## Step 6: Confirm and wrap up +## Step 6: Configure live mode (when code exists) -Summarize: +If the project has code with HTML entries and a dev server (the same "code exists" condition that puts `/impeccable document` in scan mode), pre-configure live mode now. You already identified the framework and the served HTML entry in Step 2, so this is nearly free, and it spares the user the first-time setup detour when they later run `/impeccable live`. + +**Skip this step for empty / pre-implementation projects** (nothing to inject into yet). Tell the user live mode will configure itself the first time they run it once there's code. + +**If `.impeccable/live/config.json` already exists, leave it untouched** and note that live mode is already configured. + +Otherwise: + +1. Write `.impeccable/live/config.json`. Choose `files` (the HTML entries the browser actually loads), `insertBefore`, and `commentSyntax` from the framework table in [live.md](live.md)'s **First-time setup** section, using the framework you found in Step 2. That table is canonical; do not restate it here. For multi-page static sites, prefer a glob (`["public/**/*.html"]`) over a literal list. +2. Run `node .opencode/skills/impeccable/scripts/detect-csp.mjs`. If it reports a patchable shape (`append-arrays` / `append-string`), use the **consent prompt template** from live.md before editing any source file. On decline, skip the patch. For `middleware` / `meta-tag` shapes, surface the detected files and ask the user to add `http://localhost:8400` to `script-src` and `connect-src` manually. For `null`, there's nothing to do. +3. Set `cspChecked: true` in the config once CSP is handled (patched, declined, manual, or not needed). The schema and per-shape patch details live in live.md's First-time setup; follow it rather than duplicating. + +Writing the config file is harmless and needs no consent; only the CSP **source-file patch** requires a yes. + +## Step 7: Recommend starting points, then wrap up + +Summarize tersely: - Register captured (brand / product) -- What was written (PRODUCT.md, DESIGN.md, or both) +- What was written (PRODUCT.md, DESIGN.md, live config, or a subset) - The 3-5 strategic principles from PRODUCT.md that will guide future work -- If DESIGN.md is pending, remind the user how to generate it later +- If DESIGN.md or live config is pending, one line on how to set it up later -**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node .opencode/skills/impeccable/scripts/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. +Then recommend the **best commands to run next**, drawn from what your Step 2 crawl already surfaced. Do not run a fresh analysis here; surface observations you already have. Tailor to register and to what you saw, offer the 2-4 most relevant (not a menu dump), and give the exact command to type. Group by intent: -If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context. +- **Build something new**: `/impeccable craft ` (shape, then build end-to-end) or `/impeccable shape ` (plan first). Lead with this for empty or early-stage projects. +- **Improve what's there**: name the specific surface. `/impeccable critique ` for a scored UX review; `/impeccable audit ` for a11y / perf / responsive checks; `/impeccable polish ` for a pre-ship pass. When the crawl flagged a specific weakness, point the matching command at it: thin hierarchy or spacing → `layout`, flat or gray palette → `colorize`, missing error / empty states → `harden` or `onboard`, dull or unclear copy → `clarify`. +- **Iterate visually**: `/impeccable live` (configured in Step 6) to pick elements in the browser and generate variants in place. + +The full command menu is one bare `/impeccable` away; keep this list short and pointed. + +If init was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now. Your own writes are the freshest source; no reload needed. Optionally STOP and call the `question` tool to clarify. Ask whether they'd like a brief summary of PRODUCT.md appended to AGENTS.md for easier agent reference. If yes, append a short **Design Context** pointer section there. diff --git a/.opencode/skills/impeccable/reference/interaction-design.md b/.opencode/skills/impeccable/reference/interaction-design.md index 15aed5b29..c42c9d810 100644 --- a/.opencode/skills/impeccable/reference/interaction-design.md +++ b/.opencode/skills/impeccable/reference/interaction-design.md @@ -150,12 +150,6 @@ For browsers without anchor positioning support, `position: fixed` with manual c Check viewport boundaries before rendering. If the dropdown would overflow the bottom edge, flip it above the trigger. If it would overflow the right edge, align it to the trigger's right side instead. -### Anti-Patterns - -- **`position: absolute` inside `overflow: hidden`** - The dropdown will be clipped. Use `position: fixed` or the top layer instead. -- **Arbitrary z-index values** like `z-index: 9999` - Use a semantic z-index scale: `dropdown (100) -> sticky (200) -> modal-backdrop (300) -> modal (400) -> toast (500) -> tooltip (600)`. -- **Rendering dropdown markup inline** without an escape hatch from the parent's stacking context. Either use `popover` (top layer), a portal, or `position: fixed`. - ## Destructive Actions: Undo > Confirm **Undo is better than confirmation dialogs.** Users click through confirmations mindlessly. Remove from UI immediately, show undo toast, actually delete after toast expires. Use confirmation only for truly irreversible actions (account deletion), high-cost actions, or batch operations. diff --git a/.opencode/skills/impeccable/reference/layout.md b/.opencode/skills/impeccable/reference/layout.md index cad4c62a1..8765978b0 100644 --- a/.opencode/skills/impeccable/reference/layout.md +++ b/.opencode/skills/impeccable/reference/layout.md @@ -1,4 +1,4 @@ -Space is the most underused design tool. Find the layout's actual problem (monotone spacing, weak hierarchy, identical card grids, the centered-stack default) and fix the structure, not the surface. +Space is the most underused design tool. Find the layout's actual problem (monotone spacing, weak hierarchy, identical card grids) and fix the structure, not the surface. --- @@ -27,7 +27,6 @@ Analyze what's weak about the current spatial design: 3. **Grid & structure**: - Is there a clear underlying structure, or does the layout feel random? - Are identical card grids used everywhere? (Icon + heading + text, repeated endlessly) - - Is everything centered? (Left-aligned with asymmetric layouts feels more designed, but not a hard and fast rule) 4. **Rhythm & variety**: - Does the layout have visual rhythm? (Alternating tight/generous spacing) @@ -43,8 +42,6 @@ Analyze what's weak about the current spatial design: ## Plan Layout Improvements -Consult the [spatial design reference](spatial-design.md) for detailed guidance on grids, rhythm, and container queries. - Create a systematic plan: - **Spacing system**: Use a consistent scale (a framework's built-in scale like Tailwind's, rem-based tokens, or a custom system). The specific values matter less than consistency. @@ -57,6 +54,7 @@ Create a systematic plan: ### Establish a Spacing System - Use a consistent spacing scale (framework scales like Tailwind, rem-based tokens, or a custom scale all work). What matters is that values come from a defined set, not arbitrary numbers. +- Prefer a 4pt base scale (4, 8, 12, 16, 24, 32, 48, 64, 96px) over 8pt; 8pt is too coarse and you'll frequently need 12px between 8 and 16. - Name tokens semantically if using custom properties: `--space-xs` through `--space-xl`, not `--spacing-8` - Use `gap` for sibling spacing instead of margins; eliminates margin collapse hacks - Apply `clamp()` for fluid spacing that breathes on larger screens @@ -66,15 +64,22 @@ Create a systematic plan: - **Tight grouping** for related elements (8-12px between siblings) - **Generous separation** between distinct sections (48-96px) - **Varied spacing** within sections (not every row needs the same gap) -- **Asymmetric compositions**: break the predictable centered-content pattern when it makes sense +- **Asymmetric compositions**: a deliberate choice when the content invites it (not a default to chase). ### Choose the Right Layout Tool -- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. Flex is simpler and more appropriate for the majority of layout tasks. +- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. - **Use Grid for 2D layouts**: Page-level structure, dashboards, data-dense interfaces, anything where rows AND columns need coordinated control. -- **Don't default to Grid** when Flexbox with `flex-wrap` would be simpler and more flexible. -- Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. - Use named grid areas (`grid-template-areas`) for complex page layouts; redefine at breakpoints. +- Use **container queries** for components, viewport queries for page layouts. A card in a narrow sidebar can stay compact while the same card in a main content area expands automatically: + +```css +.card-container { container-type: inline-size; } +.card { display: grid; gap: var(--space-md); } +@container (min-width: 400px) { + .card { grid-template-columns: 120px 1fr; } +} +``` ### Break Card Grid Monotony @@ -85,18 +90,36 @@ Create a systematic plan: ### Strengthen Visual Hierarchy - Use the fewest dimensions needed for clear hierarchy. Space alone can be enough; generous whitespace around an element draws the eye. Some of the most polished designs achieve rhythm with just space and weight. Add color or size contrast only when simpler means aren't sufficient. +- The best hierarchy combines 2–3 dimensions at once. A heading that's larger, bolder, AND has more space above it reads as primary without trying: + +| Tool | Strong Hierarchy | Weak Hierarchy | +|------|------------------|----------------| +| **Size** | 3:1 ratio or more | <2:1 ratio | +| **Weight** | Bold vs Regular | Medium vs Regular | +| **Color** | High contrast | Similar tones | +| **Position** | Top/left (primary) | Bottom/right | +| **Space** | Surrounded by white space | Crowded | + - Be aware of reading flow: in LTR languages, the eye naturally scans top-left to bottom-right, but primary action placement depends on context (e.g., bottom-right in dialogs, top in navigation). - Create clear content groupings through proximity and separation. ### Manage Depth & Elevation -- Create a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip) - Build a consistent shadow scale (sm → md → lg → xl); shadows should be subtle - Use elevation to reinforce hierarchy, not as decoration ### Optical Adjustments - If an icon looks visually off-center despite being geometrically centered, nudge it. But only if you're confident it actually looks wrong. Don't adjust speculatively. +- Text at `margin-left: 0` looks slightly indented because of letterform whitespace; a negative margin (`-0.05em`) optically aligns it. Geometrically centered glyphs often look off-center (play icons need to shift right, arrows shift toward their direction). +- Touch targets must be 44×44px minimum even when the visual element is smaller. Expand the hit area with padding or a pseudo-element: + +```css +.icon-button { width: 24px; height: 24px; position: relative; } +.icon-button::before { + content: ''; position: absolute; inset: -10px; +} +``` **NEVER**: - Use arbitrary spacing values outside your scale @@ -104,10 +127,7 @@ Create a systematic plan: - Wrap everything in cards (not everything needs a container) - Nest cards inside cards (use spacing and dividers for hierarchy within) - Use identical card grids everywhere (icon + heading + text, repeated) -- Center everything (left-aligned with asymmetry feels more designed) - Default to the hero metric layout (big number, small label, stats, gradient) as a template. If showing real user data, a prominent metric can work, but it should display actual data, not decorative numbers. -- Default to CSS Grid when Flexbox would be simpler; use the simplest tool for the job -- Use arbitrary z-index values (999, 9999); build a semantic scale ## Verify Layout Improvements diff --git a/.opencode/skills/impeccable/reference/live.md b/.opencode/skills/impeccable/reference/live.md index 2447e03fc..baac1a877 100644 --- a/.opencode/skills/impeccable/reference/live.md +++ b/.opencode/skills/impeccable/reference/live.md @@ -9,18 +9,21 @@ A running dev server with hot module replacement (Vite, Next.js, Bun, etc.), OR Execute in order. No step skipped, no step reordered. 1. `live.mjs`: boot. -2. Navigate to the URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). If you can't infer it confidently, tell the user once to open their dev/preview URL. Never use `serverPort` as that URL; it's the helper, not the app. +2. Open the app URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). Never use `serverPort`; it's the helper, not the app. **Cursor:** `browser_navigate` to that URL before polling; do not skip. **Other harnesses:** use the available browser tool; if the URL is uncertain, ask the user once. 3. Poll loop with the default long timeout (600000 ms). After every event or `--reply`, run `live-poll.mjs` again immediately. Never pass a short `--timeout=`. + +The global bar **Impeccable mark** dims and shows a pulsing amber dot when no agent is long-polling `/poll`. Hover the mark for the hint; restart `live-poll.mjs` to reconnect. 4. On `generate`: read screenshot if present; load the action's reference; plan three distinct directions; write all variants in one edit; `--reply done`; poll again. -5. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts remain recoverable until you finish cleanup, run `live-complete.mjs --id EVENT_ID`, and only then poll again. -6. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart. -7. On `exit`: run the cleanup at the bottom. +5. On `steer`: read the message and `pageUrl`; do the work (page edits, navigation help, or a short reply in the `--reply` message); `--reply steer_done`; poll again. No pickup ack. The Steer bar unlocks when `steer_done` arrives over SSE. +6. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts remain recoverable until you finish cleanup, run `live-complete.mjs --id EVENT_ID`, and only then poll again. +7. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart. +8. On `exit`: run the cleanup at the bottom. Harness policy: - **Claude Code**: run the poll as a **background task** (no short timeout). The harness notifies you when it completes, so the main conversation stays free. Do not block the shell. -- **Cursor**: run the poll in the **foreground** (blocking shell; not a background terminal, not a subagent). Cursor background terminals and subagents do not reliably resume the chat with poll stdout. +- **Cursor**: run **one-shot** poll in a **background terminal** with notify on `"type":"(steer|generate|accept|discard|exit)"`. After each event the poll exits; handle it, `--reply`, then start `live-poll.mjs` again. Do **not** use `--stream` on Cursor: incremental stdout notify is slower in practice than exit-based notify (~5s vs sub-second in testing). - **Codex**: run the poll in the **foreground** (blocking shell; not a background task, not a subagent). Codex background exec sessions do not reliably surface poll stdout back into the conversation at the moment events arrive, so a "fire-and-forget" background poll will stall live mode. -- **Other harnesses**: foreground unless you know stdout reliably returns to this session. +- **Other harnesses**: one-shot foreground unless you know stdout reliably returns to this session when a shell exits. Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodies. Spend tokens on tools and edits; on failure, one or two short sentences. @@ -30,7 +33,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi node .opencode/skills/impeccable/scripts/live.mjs ``` -Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation; **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md. +Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation; **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. `serverPort` and `serverToken` belong to the small **Impeccable live helper** HTTP server (serves `/live.js`, SSE, and `/poll`). That port is **not** your dev server and is usually not the URL you open to view the app. The browser page is whatever origin serves one of the `pageFiles` entries (Vite / Next / Bun / tunnel / LAN hostname). @@ -38,12 +41,15 @@ If output is `{ ok: false, error: "config_missing" | "config_invalid", path }`, ## Poll loop +**Default (portable, all harnesses):** + ``` LOOP: node .opencode/skills/impeccable/scripts/live-poll.mjs # default long timeout; no --timeout= Read JSON; dispatch on "type" "generate" → Handle Generate; reply done; LOOP + "steer" → Handle Steer; reply steer_done; LOOP "accept" → Handle Accept; complete carbonize cleanup if required; LOOP "discard" → Handle Discard; LOOP "prefetch" → Handle Prefetch; LOOP @@ -51,6 +57,16 @@ LOOP: "exit" → break → Cleanup ``` +**Stream mode (experimental, not for Cursor):** + +``` +node .opencode/skills/impeccable/scripts/live-poll.mjs --stream # stays running; one JSON line per event + Handle event; run --reply in a separate command + Repeat until "exit" line → Cleanup +``` + +Stream keeps one process alive and waits for `--reply` ack before polling again. Useful only when the harness reads incremental stdout reliably and quickly. **Cursor is not one of those:** background pattern notify on a long-running shell was ~5s to pick up events vs sub-second for one-shot exit notify. Default to one-shot everywhere unless you have measured otherwise. + ## Recovery commands The live helper persists an append-only journal under `.impeccable/live/sessions/`. Browser checkpoints are advisory but durable; the journal is canonical. This is local durable recovery state, not project source. @@ -71,9 +87,32 @@ Server restart rule: start `live-server.mjs` again, then poll. Startup requeues ## Handle `generate` -Event: `{id, action, freeformPrompt?, count, pageUrl, element, screenshotPath?, comments?, strokes?}`. +**Replace mode** (default): `{id, action, freeformPrompt?, count, pageUrl, element, screenshotPath?, comments?, strokes?}`. -Speed matters; the user is watching a spinner. Minimize tool calls by using the `wrap` helper and writing all variants in a single edit. +**Insert mode** (`event.mode === "insert"`): `{id, mode: "insert", count, pageUrl, insert: { position, anchor }, placeholder: { width, height }, freeformPrompt?, screenshotPath?, comments?, strokes?}`. No `action`. Requires a non-empty `freeformPrompt` **or** annotations. Screenshot is sent only when annotations exist (same rule as replace). Use `placeholder` dimensions as a soft size hint for net-new content. + +Speed matters; the user is watching a spinner. Minimize tool calls by using the wrap/insert helper and writing all variants in a single edit. + +### Insert mode branch + +When `event.mode === "insert"`: + +1. Read the screenshot if `event.screenshotPath` is present (annotations only). +2. Run the insert helper instead of wrap: + +```bash +node .opencode/skills/impeccable/scripts/live-insert.mjs --id EVENT_ID --count EVENT_COUNT --position after \ + --element-id "ANCHOR_ID" --classes "class1,class2" --tag "section" --text "ANCHOR_TEXT" +``` + +- `--position` ← `event.insert.position` (`before` | `after`) +- Anchor flags ← `event.insert.anchor` (same mapping as wrap: id, classes, tag, text) + +The scaffold has **no** `data-impeccable-variant="original"`. Variants are net-new HTML+CSS inserted at `insertLine`. Load `brand.md` or `product.md` (freeform only, no action sub-command). Write all variants in one edit, then `--reply done`. + +On accept/discard, `live-accept.mjs` removes the wrapper block; the anchor element is untouched. + +### Replace mode (default) ### 1. Read the screenshot (if present) @@ -424,6 +463,28 @@ A background agent may be used for the rewrite, but the current thread is respon Event: `{id, _acceptResult, _completionAck}`. The poll script already restored the original, removed all variant markers, and acknowledged `discarded` durable completion. Nothing to do unless `_completionAck.ok !== true`; in that case run `live-complete.mjs --id EVENT_ID --discarded`, then poll again. +## Handle `steer` + +Event: `{id, message, pageUrl}`. The user typed or spoke into the global bar **Steer** control: page-level direction without picking an element or launching variant generation. + +The mic button uses the browser **Web Speech API** (MVP): click to start, speak, stop automatically when the utterance ends, then the transcript submits as a steer event. Click again while listening to cancel without submitting. + +This is lighter than `generate`: no screenshot, no element context, no variant cycling. Read `message` and inspect the live page or project files as needed, then either make edits or answer in prose. + +When finished: + +```bash +node .opencode/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID steer_done ["Optional short note for a browser toast"] +``` + +On failure: + +```bash +node .opencode/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID error "Short reason" +``` + +Then poll again immediately. Do not send a separate "picked up" reply. The Steer bar stays locked until `steer_done` or `error` arrives over SSE. + ## Handle `prefetch` Event: `{pageUrl}`. The browser fires this the first time the user selects an element on a given route, as a latency shortcut; it signals the user is likely about to Go on a page you haven't read yet. diff --git a/.opencode/skills/impeccable/reference/motion-design.md b/.opencode/skills/impeccable/reference/motion-design.md deleted file mode 100644 index 78b6fd260..000000000 --- a/.opencode/skills/impeccable/reference/motion-design.md +++ /dev/null @@ -1,109 +0,0 @@ -# Motion Design - -## Duration: The 100/300/500 Rule - -Timing matters more than easing. These durations feel right for most UI: - -| Duration | Use Case | Examples | -|----------|----------|----------| -| **100-150ms** | Instant feedback | Button press, toggle, color change | -| **200-300ms** | State changes | Menu open, tooltip, hover states | -| **300-500ms** | Layout changes | Accordion, modal, drawer | -| **500-800ms** | Entrance animations | Page load, hero reveals | - -**Exit animations are faster than entrances.** Use ~75% of enter duration. - -## Easing: Pick the Right Curve - -**Don't use `ease`.** It's a compromise that's rarely optimal. Instead: - -| Curve | Use For | CSS | -|-------|---------|-----| -| **ease-out** | Elements entering | `cubic-bezier(0.16, 1, 0.3, 1)` | -| **ease-in** | Elements leaving | `cubic-bezier(0.7, 0, 0.84, 0)` | -| **ease-in-out** | State toggles (there → back) | `cubic-bezier(0.65, 0, 0.35, 1)` | - -**For micro-interactions, use exponential curves.** They feel natural because they mimic real physics (friction, deceleration): - -```css -/* Quart out - smooth, refined (recommended default) */ ---ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1); - -/* Quint out - slightly more dramatic */ ---ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1); - -/* Expo out - snappy, confident */ ---ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); -``` - -**Avoid bounce and elastic curves.** They were trendy in 2015 but now feel tacky and amateurish. Real objects don't bounce when they stop; they decelerate smoothly. Overshoot effects draw attention to the animation itself rather than the content. - -## Premium Motion Materials - -Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties: blur reveals, backdrop-filter panels, saturation or brightness shifts, shadow bloom, SVG filters, masks, clip paths, gradient-position movement, and variable font or shader-driven effects. - -Use the right material for the effect: - -- **Transform / opacity**: movement, press feedback, simple reveals, list choreography. -- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances, atmospheric transitions. -- **Clip path / masks**: wipes, reveals, editorial cropping, product-like transitions. -- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state. -- **Grid-template rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly. - -The hard rule is not "transform and opacity only." The hard rule is: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify in-browser that the result is smooth on the target viewports. If blur/filter makes the interaction feel significantly more premium and remains smooth, use it. - -## Staggered Animations - -Use CSS custom properties for cleaner stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"` on each item. **Cap total stagger time**: 10 items at 50ms = 500ms total. For many items, reduce per-item delay or cap staggered count. - -## Reduced Motion - -This is not optional. Vestibular disorders affect ~35% of adults over 40. - -```css -/* Define animations normally */ -.card { - animation: slide-up 500ms ease-out; -} - -/* Provide alternative for reduced motion */ -@media (prefers-reduced-motion: reduce) { - .card { - animation: fade-in 200ms ease-out; /* Crossfade instead of motion */ - } -} - -/* Or disable entirely */ -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - transition-duration: 0.01ms !important; - } -} -``` - -**What to preserve**: Functional animations like progress bars, loading spinners (slowed down), and focus indicators should still work, just without spatial movement. - -## Perceived Performance - -**Nobody cares how fast your site is, just how fast it feels.** Perception can be as effective as actual performance. - -**The 80ms threshold**: Our brains buffer sensory input for ~80ms to synchronize perception. Anything under 80ms feels instant and simultaneous. This is your target for micro-interactions. - -**Active vs passive time**: Passive waiting (staring at a spinner) feels longer than active engagement. Strategies to shift the balance: - -- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening. -- **Early completion**: Show content progressively, don't wait for everything. Video buffering, progressive images, streaming HTML. -- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Instagram likes work offline; the UI updates instantly, syncs later. Use for low-stakes actions; avoid for payments or destructive operations. - -**Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances, but ease-in toward a task's end compresses perceived time. - -**Caution**: Too-fast responses can decrease perceived value. Users may distrust instant results for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening. - -## Performance - -Don't use `will-change` preemptively, only when animation is imminent (`:hover`, `.animating`). For scroll-triggered animations, use Intersection Observer instead of scroll events; unobserve after animating once. Create motion tokens for consistency (durations, easings, common transitions). - ---- - -**Avoid**: Animating everything (animation fatigue is real). Using >500ms for UI feedback. Ignoring `prefers-reduced-motion`. Using animation to hide slow loading. diff --git a/.opencode/skills/impeccable/reference/personas.md b/.opencode/skills/impeccable/reference/personas.md deleted file mode 100644 index 43ae8eb2f..000000000 --- a/.opencode/skills/impeccable/reference/personas.md +++ /dev/null @@ -1,179 +0,0 @@ -# Persona-Based Design Testing - -Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss. - -**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns. - ---- - -## 1. Impatient Power User: "Alex" - - -**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave. - -**Behaviors**: -- Skips all onboarding and instructions -- Looks for keyboard shortcuts immediately -- Tries to bulk-select, batch-edit, and automate -- Gets frustrated by required steps that feel unnecessary -- Abandons if anything feels slow or patronizing - -**Test Questions**: -- Can Alex complete the core task in under 60 seconds? -- Are there keyboard shortcuts for common actions? -- Can onboarding be skipped entirely? -- Do modals have keyboard dismiss (Esc)? -- Is there a "power user" path (shortcuts, bulk actions)? - -**Red Flags** (report these specifically): -- Forced tutorials or unskippable onboarding -- No keyboard navigation for primary actions -- Slow animations that can't be skipped -- One-item-at-a-time workflows where batch would be natural -- Redundant confirmation steps for low-risk actions - ---- - -## 2. Confused First-Timer: "Jordan" - -**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out. - -**Behaviors**: -- Reads all instructions carefully -- Hesitates before clicking anything unfamiliar -- Looks for help or support constantly -- Misunderstands jargon and abbreviations -- Takes the most literal interpretation of any label - -**Test Questions**: -- Is the first action obviously clear within 5 seconds? -- Are all icons labeled with text? -- Is there contextual help at decision points? -- Does terminology assume prior knowledge? -- Is there a clear "back" or "undo" at every step? - -**Red Flags** (report these specifically): -- Icon-only navigation with no labels -- Technical jargon without explanation -- No visible help option or guidance -- Ambiguous next steps after completing an action -- No confirmation that an action succeeded - ---- - -## 3. Accessibility-Dependent User: "Sam" - -**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences. - -**Behaviors**: -- Tabs through the interface linearly -- Relies on ARIA labels and heading structure -- Cannot see hover states or visual-only indicators -- Needs adequate color contrast (4.5:1 minimum) -- May use browser zoom up to 200% - -**Test Questions**: -- Can the entire primary flow be completed keyboard-only? -- Are all interactive elements focusable with visible focus indicators? -- Do images have meaningful alt text? -- Is color contrast WCAG AA compliant (4.5:1 for text)? -- Does the screen reader announce state changes (loading, success, errors)? - -**Red Flags** (report these specifically): -- Click-only interactions with no keyboard alternative -- Missing or invisible focus indicators -- Meaning conveyed by color alone (red = error, green = success) -- Unlabeled form fields or buttons -- Time-limited actions without extension option -- Custom components that break screen reader flow - ---- - -## 4. Deliberate Stress Tester: "Riley" - -**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience. - -**Behaviors**: -- Tests edge cases intentionally (empty states, long strings, special characters) -- Submits forms with unexpected data (emoji, RTL text, very long values) -- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs -- Looks for inconsistencies between what the UI promises and what actually happens -- Documents problems methodically - -**Test Questions**: -- What happens at the edges (0 items, 1000 items, very long text)? -- Do error states recover gracefully or leave the UI in a broken state? -- What happens on refresh mid-workflow? Is state preserved? -- Are there features that appear to work but produce broken results? -- How does the UI handle unexpected input (emoji, special chars, paste from Excel)? - -**Red Flags** (report these specifically): -- Features that appear to work but silently fail or produce wrong results -- Error handling that exposes technical details or leaves UI in a broken state -- Empty states that show nothing useful ("No results" with no guidance) -- Workflows that lose user data on refresh or navigation -- Inconsistent behavior between similar interactions in different parts of the UI - ---- - -## 5. Distracted Mobile User: "Casey" - -**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection. - -**Behaviors**: -- Uses thumb only; prefers bottom-of-screen actions -- Gets interrupted mid-flow and returns later -- Switches between apps frequently -- Has limited attention span and low patience -- Types as little as possible, prefers taps and selections - -**Test Questions**: -- Are primary actions in the thumb zone (bottom half of screen)? -- Is state preserved if the user leaves and returns? -- Does it work on slow connections (3G)? -- Can forms use autocomplete and smart defaults? -- Are touch targets at least 44×44pt? - -**Red Flags** (report these specifically): -- Important actions positioned at the top of the screen (unreachable by thumb) -- No state persistence; progress lost on tab switch or interruption -- Large text inputs required where selection would work -- Heavy assets loading on every page (no lazy loading) -- Tiny tap targets or targets too close together - ---- - -## Selecting Personas - -Choose personas based on the interface type: - -| Interface Type | Primary Personas | Why | -|---------------|-----------------|-----| -| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile | -| Dashboard / admin | Alex, Sam | Power users, accessibility | -| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity | -| Onboarding flow | Jordan, Casey | Confusion, interruption | -| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav | -| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile | - ---- - -## Project-Specific Personas - -If `AGENTS.md` contains a `## Design Context` section (generated by `impeccable teach`), derive 1–2 additional personas from the audience and brand information: - -1. Read the target audience description -2. Identify the primary user archetype not covered by the 5 predefined personas -3. Create a persona following this template: - -``` -### [Role]: "[Name]" - -**Profile**: [2-3 key characteristics derived from Design Context] - -**Behaviors**: [3-4 specific behaviors based on the described audience] - -**Red Flags**: [3-4 things that would alienate this specific user type] -``` - -Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists. diff --git a/.opencode/skills/impeccable/reference/polish.md b/.opencode/skills/impeccable/reference/polish.md index 1d940a30f..8905ff24d 100644 --- a/.opencode/skills/impeccable/reference/polish.md +++ b/.opencode/skills/impeccable/reference/polish.md @@ -91,7 +91,6 @@ Visual polish on a misshapen flow is wasted work. Match the *shape* of the exper - **Theme consistency**: Works in all theme variants - **Color meaning**: Same colors mean same things throughout - **Accessible focus**: Focus indicators visible with sufficient contrast -- **Tinted neutrals**: No pure gray or pure black; add subtle color tint (0.01 chroma) - **Gray on color**: Never put gray text on colored backgrounds; use a shade of that color or transparency ### Interaction States diff --git a/.opencode/skills/impeccable/reference/product.md b/.opencode/skills/impeccable/reference/product.md index 64b3b0169..2ca462350 100644 --- a/.opencode/skills/impeccable/reference/product.md +++ b/.opencode/skills/impeccable/reference/product.md @@ -10,7 +10,6 @@ Product UI's failure mode isn't flatness, it's strangeness without purpose: over ## Typography -- **System fonts are legitimate.** `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif` gives you native feel on every platform. Inter is the common cross-platform default for a reason. - **One family is often right.** Product UIs don't need display/body pairing. A well-tuned sans carries headings, buttons, labels, body, data. - **Fixed rem scale, not fluid.** Clamp-sized headings don't serve product UI. Users view at consistent DPI, and a fluid h1 that shrinks in a sidebar looks worse, not better. - **Tighter scale ratio.** 1.125–1.2 between steps is typical. More type elements here than on brand surfaces; exaggerated contrast creates noise. @@ -26,8 +25,6 @@ Product defaults to Restrained. A single surface can earn Committed (a dashboard ## Layout -- Predictable grids. Consistency IS an affordance; users navigate faster when the structure is expected. -- Familiar patterns are features. Standard navigation (top bar, side nav), breadcrumbs, tabs, and form layouts have established user expectations. Don't reinvent for flavor. - Responsive behavior is structural (collapse sidebar, responsive table, breakpoint-driven columns), not fluid typography. ## Components @@ -51,6 +48,7 @@ Every interactive component has: default, hover, focus, active, disabled, loadin - Display fonts in UI labels, buttons, data. - Reinventing standard affordances for flavor (custom scrollbars, weird form controls, non-standard modals). - Heavy color or full-saturation accents on inactive states. +- Modal as first thought. Modals are usually laziness. Exhaust inline / progressive alternatives first. ## Product permissions diff --git a/.opencode/skills/impeccable/reference/responsive-design.md b/.opencode/skills/impeccable/reference/responsive-design.md deleted file mode 100644 index f079e5e5d..000000000 --- a/.opencode/skills/impeccable/reference/responsive-design.md +++ /dev/null @@ -1,114 +0,0 @@ -# Responsive Design - -## Mobile-First: Write It Right - -Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first. - -## Breakpoints: Content-Driven - -Don't chase device sizes; let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints. - -## Detect Input Method, Not Just Screen Size - -**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard. Use pointer and hover queries: - -```css -/* Fine pointer (mouse, trackpad) */ -@media (pointer: fine) { - .button { padding: 8px 16px; } -} - -/* Coarse pointer (touch, stylus) */ -@media (pointer: coarse) { - .button { padding: 12px 20px; } /* Larger touch target */ -} - -/* Device supports hover */ -@media (hover: hover) { - .card:hover { transform: translateY(-2px); } -} - -/* Device doesn't support hover (touch) */ -@media (hover: none) { - .card { /* No hover state - use active instead */ } -} -``` - -**Critical**: Don't rely on hover for functionality. Touch users can't hover. - -## Safe Areas: Handle the Notch - -Modern phones have notches, rounded corners, and home indicators. Use `env()`: - -```css -body { - padding-top: env(safe-area-inset-top); - padding-bottom: env(safe-area-inset-bottom); - padding-left: env(safe-area-inset-left); - padding-right: env(safe-area-inset-right); -} - -/* With fallback */ -.footer { - padding-bottom: max(1rem, env(safe-area-inset-bottom)); -} -``` - -**Enable viewport-fit** in your meta tag: -```html - -``` - -## Responsive Images: Get It Right - -### srcset with Width Descriptors - -```html -Hero image -``` - -**How it works**: -- `srcset` lists available images with their actual widths (`w` descriptors) -- `sizes` tells the browser how wide the image will display -- Browser picks the best file based on viewport width AND device pixel ratio - -### Picture Element for Art Direction - -When you need different crops/compositions (not just resolutions): - -```html - - - - ... - -``` - -## Layout Adaptation Patterns - -**Navigation**: Three stages: hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `
/` for content that can collapse on mobile. - -## Testing: Don't Trust DevTools Alone - -DevTools device emulation is useful for layout but misses: - -- Actual touch interactions -- Real CPU/memory constraints -- Network latency patterns -- Font rendering differences -- Browser chrome/keyboard appearances - -**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators. - ---- - -**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful. diff --git a/.opencode/skills/impeccable/reference/shape.md b/.opencode/skills/impeccable/reference/shape.md index ad79137ac..5c3519269 100644 --- a/.opencode/skills/impeccable/reference/shape.md +++ b/.opencode/skills/impeccable/reference/shape.md @@ -151,7 +151,7 @@ How users interact with this feature. What happens on click, hover, scroll? What 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., spatial-design.md for complex layouts, motion-design.md for animated features, interaction-design.md for form-heavy features). +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. diff --git a/.opencode/skills/impeccable/reference/spatial-design.md b/.opencode/skills/impeccable/reference/spatial-design.md deleted file mode 100644 index 4d4b83afc..000000000 --- a/.opencode/skills/impeccable/reference/spatial-design.md +++ /dev/null @@ -1,100 +0,0 @@ -# Spatial Design - -## Spacing Systems - -### Use 4pt Base, Not 8pt - -8pt systems are too coarse; you'll frequently need 12px (between 8 and 16). Use 4pt for granularity: 4, 8, 12, 16, 24, 32, 48, 64, 96px. - -### Name Tokens Semantically - -Name by relationship (`--space-sm`, `--space-lg`), not value (`--spacing-8`). Use `gap` instead of margins for sibling spacing; it eliminates margin collapse and cleanup hacks. - -## Grid Systems - -### The Self-Adjusting Grid - -Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. Columns are at least 280px, as many as fit per row, leftovers stretch. For complex layouts, use named grid areas (`grid-template-areas`) and redefine them at breakpoints. - -## Visual Hierarchy - -### The Squint Test - -Blur your eyes (or screenshot and blur). Can you still identify: -- The most important element? -- The second most important? -- Clear groupings? - -If everything looks the same weight blurred, you have a hierarchy problem. - -### Hierarchy Through Multiple Dimensions - -Don't rely on size alone. Combine: - -| Tool | Strong Hierarchy | Weak Hierarchy | -|------|------------------|----------------| -| **Size** | 3:1 ratio or more | <2:1 ratio | -| **Weight** | Bold vs Regular | Medium vs Regular | -| **Color** | High contrast | Similar tones | -| **Position** | Top/left (primary) | Bottom/right | -| **Space** | Surrounded by white space | Crowded | - -**The best hierarchy uses 2-3 dimensions at once**: A heading that's larger, bolder, AND has more space above it. - -### Cards Are Not Required - -Cards are overused. Spacing and alignment create visual grouping naturally. Use cards only when content is truly distinct and actionable, items need visual comparison in a grid, or content needs clear interaction boundaries. **Never nest cards inside cards.** Use spacing, typography, and subtle dividers for hierarchy within a card. - -## Container Queries - -Viewport queries are for page layouts. **Container queries are for components**: - -```css -.card-container { - container-type: inline-size; -} - -.card { - display: grid; - gap: var(--space-md); -} - -/* Card layout changes based on its container, not viewport */ -@container (min-width: 400px) { - .card { - grid-template-columns: 120px 1fr; - } -} -``` - -**Why this matters**: A card in a narrow sidebar stays compact, while the same card in a main content area expands automatically, without viewport hacks. - -## Optical Adjustments - -Text at `margin-left: 0` looks indented due to letterform whitespace; use negative margin (`-0.05em`) to optically align. Geometrically centered icons often look off-center; play icons need to shift right, arrows shift toward their direction. - -### Touch Targets vs Visual Size - -Buttons can look small but need large touch targets (44px minimum). Use padding or pseudo-elements: - -```css -.icon-button { - width: 24px; /* Visual size */ - height: 24px; - position: relative; -} - -.icon-button::before { - content: ''; - position: absolute; - inset: -10px; /* Expand tap target to 44px */ -} -``` - -## Depth & Elevation - -Create semantic z-index scales (dropdown → sticky → modal-backdrop → modal → toast → tooltip) instead of arbitrary numbers. For shadows, create a consistent elevation scale (sm → md → lg → xl). **Key insight**: Shadows should be subtle. If you can clearly see it, it's probably too strong. - ---- - -**Avoid**: Arbitrary spacing values outside your scale. Making all spacing equal (variety creates hierarchy). Creating hierarchy through size alone - combine size, weight, color, and space. diff --git a/.opencode/skills/impeccable/reference/typeset.md b/.opencode/skills/impeccable/reference/typeset.md index dd3e67ea3..5f5e0c2c0 100644 --- a/.opencode/skills/impeccable/reference/typeset.md +++ b/.opencode/skills/impeccable/reference/typeset.md @@ -4,7 +4,7 @@ Typography carries most of the information on the page. Replace generic defaults ## Register -Brand: run the font selection procedure in [brand.md](brand.md). Pairing follows the brand's lane (display serif + sans body for editorial/luxury, one committed sans for tech, etc.). Fluid `clamp()` scale, ≥1.25 ratio between steps. +Brand: run the font selection procedure in [brand.md](brand.md). Fluid `clamp()` scale, ≥1.25 ratio between steps. Product: system fonts and familiar sans stacks are legitimate here. One well-tuned family typically carries the whole UI. Fixed `rem` scale, 1.125–1.2 ratio between more closely-spaced steps. @@ -43,7 +43,7 @@ Analyze what's weak or generic about the current type: ## Plan Typography Improvements -Consult the [typography reference](typography.md) for detailed guidance on scales, pairing, and loading strategies. +Consult the [Reference Material](#reference-material) section below for detailed guidance on scales, pairing, and loading strategies. Create a systematic plan: @@ -122,3 +122,158 @@ Each variant MUST declare a `scale` param controlling the hierarchy ratio. Expre Where the variant riffs on a specific pairing, expose the pairing choice as a `steps` param (e.g. "serif display + sans body" vs. "mono display + sans body" vs. "all-sans"). Each branch routes through `:scope[data-p-pairing="X"]` selectors in scoped CSS. See `reference/live.md` for the full params contract. + +--- + +## Reference Material + +The sections below were previously `typography.md` and live inline now so the typeset flow has its deep typography reference in one place. `bolder.md` also references this section. + +### Typography + +#### Classic Typography Principles + +##### Vertical Rhythm + +Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony; text and space share a mathematical foundation. + +##### Modular Scale & Hierarchy + +The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy. + +**Use fewer sizes with more contrast.** A 5-size system covers most needs: + +| Role | Typical Ratio | Use Case | +|------|---------------|----------| +| xs | 0.75rem | Captions, legal | +| sm | 0.875rem | Secondary UI, metadata | +| base | 1rem | Body text | +| lg | 1.25-1.5rem | Subheadings, lead text | +| xl+ | 2-4rem | Headlines, hero text | + +Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit. + +##### Readability & Measure + +Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length: narrow columns need tighter leading, wide columns need more. + +**Non-obvious**: Light text on dark backgrounds needs compensation on three axes, not just one. Bump line-height by 0.05–0.1, add a touch of letter-spacing (0.01–0.02em), and optionally step the body weight up one notch (regular → medium). The perceived weight drops across all three; fix all three. + +**Paragraph rhythm**: Pick either space between paragraphs OR first-line indentation. Never both. Digital usually wants space; editorial/long-form can justify indent-only. + +#### Font Selection & Pairing + +The tactical selection procedure and the reflex-reject list live in [reference/brand.md](brand.md) under **Font selection procedure** and **Reflex-reject list** (loaded for brand-register tasks). The rest of this section covers the adjacent knowledge: anti-reflex corrections, system font use, and pairing rules. + +##### Anti-reflexes worth defending against + +- A technical/utilitarian brief does NOT need a serif "for warmth." Most tech tools should look like tech tools. +- An editorial/premium brief does NOT need the same expressive serif everyone is using right now. Premium can be Swiss-modern, can be neo-grotesque, can be a literal monospace, can be a quiet humanist sans. +- A children's product does NOT need a rounded display font. Kids' books use real type. +- A "modern" brief does NOT need a geometric sans. The most modern thing you can do is not use the font everyone else is using. + +**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality. + +##### Pairing Principles + +**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif). + +When pairing, contrast on multiple axes: +- Serif + Sans (structure contrast) +- Geometric + Humanist (personality contrast) +- Condensed display + Wide body (proportion contrast) + +##### Web Font Loading + +The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix: + +```css +/* 1. Use font-display: swap for visibility */ +@font-face { + font-family: 'CustomFont'; + src: url('font.woff2') format('woff2'); + font-display: swap; +} + +/* 2. Match fallback metrics to minimize shift */ +@font-face { + font-family: 'CustomFont-Fallback'; + src: local('Arial'); + size-adjust: 105%; /* Scale to match x-height */ + ascent-override: 90%; /* Match ascender height */ + descent-override: 20%; /* Match descender depth */ + line-gap-override: 10%; /* Match line spacing */ +} + +body { + font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; +} +``` + +Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically. + +**`swap` vs `optional`**: `swap` shows fallback text immediately and FOUT-swaps when the web font arrives. `optional` uses the fallback if the web font misses a small load budget (~100ms) and avoids the shift entirely. Pick `optional` when zero layout shift matters more than seeing the branded font on slow networks. + +**Preload the critical weight only**: typically the regular-weight body font used above the fold. Preloading every weight costs more bandwidth than it saves. + +**Variable fonts for 3+ weights or styles**: a single variable font file is usually smaller than three static weight files, gives fractional weight control, and pairs well with `font-optical-sizing: auto`. For 1–2 weights, static is fine. + +#### Modern Web Typography + +##### Fluid Type + +Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate (higher vw = faster scaling). Add a rem offset so it doesn't collapse to 0 on small screens. + +**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes. + +**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI; fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it. + +**Bound your clamp()**: keep `max-size ≤ ~2.5 × min-size`. Wider ratios break the browser's zoom and reflow behaviour and make large viewports feel like the page is shouting. + +**Scale container width and font-size together** so effective character measure stays in the 45–75ch band at every viewport. A heading that widens faster than its container drifts out of the comfortable measure at the top end. + +##### OpenType Features + +Most developers don't know these exist. Use them for polish: + +```css +/* Proper fractions */ +.recipe-amount { font-variant-numeric: diagonal-fractions; } + +/* Small caps for abbreviations */ +abbr { font-variant-caps: all-small-caps; } + +/* Disable ligatures in code */ +code { font-variant-ligatures: none; } + +/* Enable kerning (usually on by default, but be explicit) */ +body { font-kerning: normal; } +``` + +Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/). + +##### Rendering polish + +```css +/* Variable fonts: pick the right optical-size master automatically */ +body { font-optical-sizing: auto; } +``` + +**ALL-CAPS tracking**: capitals sit too close at default spacing. Add 5–12% letter-spacing (`letter-spacing: 0.05em` to `0.12em`) to short all-caps labels, eyebrows, and small headings. Real small caps (via `font-variant-caps`) need the same treatment, slightly gentler. + +#### Typography System Architecture + +Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system. + +#### Accessibility Considerations + +Beyond contrast ratios (which are well-documented), consider: + +- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout. +- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text. +- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile. +- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets. + +--- + +**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text. diff --git a/.opencode/skills/impeccable/reference/typography.md b/.opencode/skills/impeccable/reference/typography.md deleted file mode 100644 index ed5ca272a..000000000 --- a/.opencode/skills/impeccable/reference/typography.md +++ /dev/null @@ -1,159 +0,0 @@ -# Typography - -## Classic Typography Principles - -### Vertical Rhythm - -Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony; text and space share a mathematical foundation. - -### Modular Scale & Hierarchy - -The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy. - -**Use fewer sizes with more contrast.** A 5-size system covers most needs: - -| Role | Typical Ratio | Use Case | -|------|---------------|----------| -| xs | 0.75rem | Captions, legal | -| sm | 0.875rem | Secondary UI, metadata | -| base | 1rem | Body text | -| lg | 1.25-1.5rem | Subheadings, lead text | -| xl+ | 2-4rem | Headlines, hero text | - -Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit. - -### Readability & Measure - -Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length: narrow columns need tighter leading, wide columns need more. - -**Non-obvious**: Light text on dark backgrounds needs compensation on three axes, not just one. Bump line-height by 0.05–0.1, add a touch of letter-spacing (0.01–0.02em), and optionally step the body weight up one notch (regular → medium). The perceived weight drops across all three; fix all three. - -**Paragraph rhythm**: Pick either space between paragraphs OR first-line indentation. Never both. Digital usually wants space; editorial/long-form can justify indent-only. - -## Font Selection & Pairing - -The tactical selection procedure and the reflex-reject list live in [reference/brand.md](brand.md) under **Font selection procedure** and **Reflex-reject list** (loaded for brand-register tasks). The rest of this section covers the adjacent knowledge: anti-reflex corrections, system font use, and pairing rules. - -### Anti-reflexes worth defending against - -- A technical/utilitarian brief does NOT need a serif "for warmth." Most tech tools should look like tech tools. -- An editorial/premium brief does NOT need the same expressive serif everyone is using right now. Premium can be Swiss-modern, can be neo-grotesque, can be a literal monospace, can be a quiet humanist sans. -- A children's product does NOT need a rounded display font. Kids' books use real type. -- A "modern" brief does NOT need a geometric sans. The most modern thing you can do is not use the font everyone else is using. - -**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality. - -### Pairing Principles - -**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif). - -When pairing, contrast on multiple axes: -- Serif + Sans (structure contrast) -- Geometric + Humanist (personality contrast) -- Condensed display + Wide body (proportion contrast) - -**Never pair fonts that are similar but not identical** (e.g., two geometric sans-serifs). They create visual tension without clear hierarchy. - -### Web Font Loading - -The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix: - -```css -/* 1. Use font-display: swap for visibility */ -@font-face { - font-family: 'CustomFont'; - src: url('font.woff2') format('woff2'); - font-display: swap; -} - -/* 2. Match fallback metrics to minimize shift */ -@font-face { - font-family: 'CustomFont-Fallback'; - src: local('Arial'); - size-adjust: 105%; /* Scale to match x-height */ - ascent-override: 90%; /* Match ascender height */ - descent-override: 20%; /* Match descender depth */ - line-gap-override: 10%; /* Match line spacing */ -} - -body { - font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; -} -``` - -Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically. - -**`swap` vs `optional`**: `swap` shows fallback text immediately and FOUT-swaps when the web font arrives. `optional` uses the fallback if the web font misses a small load budget (~100ms) and avoids the shift entirely. Pick `optional` when zero layout shift matters more than seeing the branded font on slow networks. - -**Preload the critical weight only**: typically the regular-weight body font used above the fold. Preloading every weight costs more bandwidth than it saves. - -**Variable fonts for 3+ weights or styles**: a single variable font file is usually smaller than three static weight files, gives fractional weight control, and pairs well with `font-optical-sizing: auto`. For 1–2 weights, static is fine. - -## Modern Web Typography - -### Fluid Type - -Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate (higher vw = faster scaling). Add a rem offset so it doesn't collapse to 0 on small screens. - -**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes. - -**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI; fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it. - -**Bound your clamp()**: keep `max-size ≤ ~2.5 × min-size`. Wider ratios break the browser's zoom and reflow behaviour and make large viewports feel like the page is shouting. - -**Scale container width and font-size together** so effective character measure stays in the 45–75ch band at every viewport. A heading that widens faster than its container drifts out of the comfortable measure at the top end. - -### OpenType Features - -Most developers don't know these exist. Use them for polish: - -```css -/* Tabular numbers for data alignment */ -.data-table { font-variant-numeric: tabular-nums; } - -/* Proper fractions */ -.recipe-amount { font-variant-numeric: diagonal-fractions; } - -/* Small caps for abbreviations */ -abbr { font-variant-caps: all-small-caps; } - -/* Disable ligatures in code */ -code { font-variant-ligatures: none; } - -/* Enable kerning (usually on by default, but be explicit) */ -body { font-kerning: normal; } -``` - -Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/). - -### Rendering polish - -```css -/* Even out heading line lengths (browser picks better break points) */ -h1, h2, h3 { text-wrap: balance; } - -/* Reduce orphans and ragged endings in long prose */ -article p { text-wrap: pretty; } - -/* Variable fonts: pick the right optical-size master automatically */ -body { font-optical-sizing: auto; } -``` - -**ALL-CAPS tracking**: capitals sit too close at default spacing. Add 5–12% letter-spacing (`letter-spacing: 0.05em` to `0.12em`) to short all-caps labels, eyebrows, and small headings. Real small caps (via `font-variant-caps`) need the same treatment, slightly gentler. - -## Typography System Architecture - -Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system. - -## Accessibility Considerations - -Beyond contrast ratios (which are well-documented), consider: - -- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout. -- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text. -- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile. -- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets. - ---- - -**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text. diff --git a/.opencode/skills/impeccable/reference/ux-writing.md b/.opencode/skills/impeccable/reference/ux-writing.md deleted file mode 100644 index 417b5f56c..000000000 --- a/.opencode/skills/impeccable/reference/ux-writing.md +++ /dev/null @@ -1,107 +0,0 @@ -# UX Writing - -## The Button Label Problem - -**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns: - -| Bad | Good | Why | -|-----|------|-----| -| OK | Save changes | Says what will happen | -| Submit | Create account | Outcome-focused | -| Yes | Delete message | Confirms the action | -| Cancel | Keep editing | Clarifies what "cancel" means | -| Click here | Download PDF | Describes the destination | - -**For destructive actions**, name the destruction: -- "Delete" not "Remove" (delete is permanent, remove implies recoverable) -- "Delete 5 items" not "Delete selected" (show the count) - -## Error Messages: The Formula - -Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input". - -### Error Message Templates - -| Situation | Template | -|-----------|----------| -| **Format error** | "[Field] needs to be [format]. Example: [example]" | -| **Missing required** | "Please enter [what's missing]" | -| **Permission denied** | "You don't have access to [thing]. [What to do instead]" | -| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." | -| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" | - -### Don't Blame the User - -Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date". - -## Empty States Are Opportunities - -Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items". - -## Voice vs Tone - -**Voice** is your brand's personality, consistent everywhere. -**Tone** adapts to the moment. - -| Moment | Tone Shift | -|--------|------------| -| Success | Celebratory, brief: "Done! Your changes are live." | -| Error | Empathetic, helpful: "That didn't work. Here's what to try..." | -| Loading | Reassuring: "Saving your work..." | -| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | - -**Never use humor for errors.** Users are already frustrated. Be helpful, not cute. - -## Writing for Accessibility - -**Link text** must have standalone meaning: "View pricing plans" not "Click here". **Alt text** describes information, not the image: "Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context. - -## Writing for Translation - -### Plan for Expansion - -German text is ~30% longer than English. Allocate space: - -| Language | Expansion | -|----------|-----------| -| German | +30% | -| French | +20% | -| Finnish | +30-40% | -| Chinese | -30% (fewer chars, but same width) | - -### Translation-Friendly Patterns - -Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear. - -## Consistency: The Terminology Problem - -Pick one term and stick with it: - -| Inconsistent | Consistent | -|--------------|------------| -| Delete / Remove / Trash | Delete | -| Settings / Preferences / Options | Settings | -| Sign in / Log in / Enter | Sign in | -| Create / Add / New | Create | - -Build a terminology glossary and enforce it. Variety creates confusion. - -## Avoid Redundant Copy - -If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well. - -## Loading States - -Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress. - -## Confirmation Dialogs: Use Sparingly - -Most confirmation dialogs are design failures; consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No"). - -## Form Instructions - -Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking. - ---- - -**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors. diff --git a/.opencode/skills/impeccable/scripts/cleanup-deprecated.mjs b/.opencode/skills/impeccable/scripts/cleanup-deprecated.mjs index 6aee4710a..bc6400e90 100644 --- a/.opencode/skills/impeccable/scripts/cleanup-deprecated.mjs +++ b/.opencode/skills/impeccable/scripts/cleanup-deprecated.mjs @@ -25,7 +25,7 @@ import { join, resolve } from 'node:path'; const DEPRECATED_NAMES = [ // v2.0 renames 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable teach + 'teach-impeccable', // folded into /impeccable init // v2.1 merges 'arrange', // renamed to layout 'normalize', // merged into polish diff --git a/.opencode/skills/impeccable/scripts/command-metadata.json b/.opencode/skills/impeccable/scripts/command-metadata.json index 1488bd5f1..83796d54b 100644 --- a/.opencode/skills/impeccable/scripts/command-metadata.json +++ b/.opencode/skills/impeccable/scripts/command-metadata.json @@ -3,8 +3,8 @@ "description": "Full confirmed-brief-then-build flow. Runs multi-round shape discovery first, resolves visual probe and north-star mock gates when available, then builds and visually iterates. Use when building a new feature end-to-end.", "argumentHint": "[feature description]" }, - "teach": { - "description": "Gathers design context for a project. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.", + "init": { + "description": "Sets up a project for impeccable. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles); offers DESIGN.md (visual: colors, typography, components) when code exists; pre-configures live mode; then recommends the best commands to run next. Every other command reads these files before doing work. Use once per project.", "argumentHint": "" }, "document": { diff --git a/.opencode/skills/impeccable/scripts/context.mjs b/.opencode/skills/impeccable/scripts/context.mjs new file mode 100644 index 000000000..52263c883 --- /dev/null +++ b/.opencode/skills/impeccable/scripts/context.mjs @@ -0,0 +1,253 @@ +/** + * Context loader: prints PRODUCT.md (and DESIGN.md if present) as one + * markdown block on stdout, or exits with empty stdout when no PRODUCT.md + * is found anywhere. The skill keys off "empty stdout" to branch into the + * init flow. + * + * Path resolution (first match wins): + * 1. cwd, if PRODUCT.md or DESIGN.md is there + * 2. .agents/context/ then docs/ + * 3. $IMPECCABLE_CONTEXT_DIR (absolute or cwd-relative) — power-user + * escape hatch, only consulted when defaults are empty + * 4. cwd as a "nothing found" default + * + * `resolveContextDir()` and `loadContext()` are also exported for the + * server-side scripts (live.mjs, live-server.mjs) that need the structured + * shape rather than the markdown block. + */ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; +const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; +const FALLBACK_DIRS = ['.agents/context', 'docs']; + +// ─── Update check ────────────────────────────────────────────────────────── +// Piggyback a lightweight skill-version check on the once-per-session boot. +// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent +// can offer `npx impeccable skills update`. Everything here is best-effort and +// silent on failure: a network problem, sandbox, or missing cache must never +// block context output or print an error. + +const UPDATE_HOST = (process.env.IMPECCABLE_UPDATE_HOST || 'https://impeccable.style').replace(/\/$/, ''); +const UPDATE_CACHE_PATH = + process.env.IMPECCABLE_UPDATE_CACHE || path.join(os.homedir(), '.impeccable', 'update-check.json'); +const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // throttle the network poll to once a day +const RENOTIFY_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000; // don't re-surface the same version for a week +const FETCH_TIMEOUT_MS = 1200; + +export function resolveContextDir(cwd = process.cwd()) { + if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { + return cwd; + } + for (const rel of FALLBACK_DIRS) { + const candidate = path.resolve(cwd, rel); + if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { + return candidate; + } + } + const envDir = process.env.IMPECCABLE_CONTEXT_DIR; + if (envDir && envDir.trim()) { + const trimmed = envDir.trim(); + return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); + } + return cwd; +} + +export function loadContext(cwd = process.cwd()) { + const contextDir = resolveContextDir(cwd); + const productPath = firstExisting(contextDir, PRODUCT_NAMES); + const designPath = firstExisting(contextDir, DESIGN_NAMES); + const product = productPath ? safeRead(productPath) : null; + const design = designPath ? safeRead(designPath) : null; + return { + hasProduct: !!product, + product, + productPath: productPath ? path.relative(cwd, productPath) : null, + hasDesign: !!design, + design, + designPath: designPath ? path.relative(cwd, designPath) : null, + contextDir, + }; +} + +function firstExisting(dir, names) { + for (const name of names) { + const abs = path.join(dir, name); + if (fs.existsSync(abs)) return abs; + } + return null; +} + +function safeRead(p) { + try { + return fs.readFileSync(p, 'utf-8'); + } catch { + return null; + } +} + +/** + * Pull the register (`brand` or `product`) out of PRODUCT.md by looking + * for a `## Register` section and reading the first non-empty line that + * follows it. Returns null when the file is legacy / register-less. + */ +function extractRegister(product) { + if (!product) return null; + const lines = product.split('\n'); + for (let i = 0; i < lines.length; i++) { + if (/^##\s+Register\b/i.test(lines[i].trim())) { + for (let j = i + 1; j < lines.length; j++) { + const next = lines[j].trim(); + if (!next) continue; + const word = next.toLowerCase(); + if (word === 'brand' || word === 'product') return word; + return null; + } + } + } + return null; +} + +/** + * Read the installed skill's own version from the sibling SKILL.md frontmatter + * (this file lives at `/scripts/context.mjs`). Returns null when the + * frontmatter is missing or unreadable. + */ +function readLocalSkillVersion() { + try { + const here = path.dirname(fileURLToPath(import.meta.url)); + const skillMd = path.join(here, '..', 'SKILL.md'); + const content = fs.readFileSync(skillMd, 'utf-8'); + const match = content.match(/^version:\s*(.+)$/m); + return match ? match[1].trim().replace(/^["']|["']$/g, '') : null; + } catch { + return null; + } +} + +function readUpdateCache() { + try { + return JSON.parse(fs.readFileSync(UPDATE_CACHE_PATH, 'utf-8')); + } catch { + return {}; + } +} + +function writeUpdateCache(cache) { + try { + fs.mkdirSync(path.dirname(UPDATE_CACHE_PATH), { recursive: true }); + fs.writeFileSync(UPDATE_CACHE_PATH, JSON.stringify(cache)); + } catch { + // Best-effort: a read-only home dir just means we re-poll next session. + } +} + +/** Compare dotted numeric versions. Returns >0 when a is newer than b. */ +function compareSemver(a, b) { + const pa = String(a).split('.').map(n => parseInt(n, 10) || 0); + const pb = String(b).split('.').map(n => parseInt(n, 10) || 0); + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + const diff = (pa[i] || 0) - (pb[i] || 0); + if (diff !== 0) return diff; + } + return 0; +} + +async function fetchLatestSkillVersion() { + try { + const res = await fetch(`${UPDATE_HOST}/api/version`, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }); + if (!res.ok) return null; + const data = await res.json(); + return typeof data?.skills === 'string' ? data.skills : null; + } catch { + return null; // offline, sandboxed, timed out, or bad JSON: all non-fatal + } +} + +function buildUpdateDirective(localVersion, latestVersion) { + return ( + `UPDATE_AVAILABLE: A newer Impeccable skill is available ` + + `(installed v${localVersion}, latest v${latestVersion}). ` + + `Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` + + `Update now? It runs \`npx impeccable skills update\`." ` + + `If they agree, run \`npx impeccable skills update\` (the update applies to the next session, not this one). ` + + `Either way, continue the current task without waiting, and do not raise this again.` + ); +} + +/** + * Best-effort update directive for the boot output. Returns a string to append + * or null. Polls the version endpoint at most once per day (cached globally in + * the user's home dir) and re-surfaces a given version at most once per week so + * the agent never nags. Opt out entirely with IMPECCABLE_NO_UPDATE_CHECK=1. + */ +async function computeUpdateDirective(now = Date.now()) { + try { + if (process.env.IMPECCABLE_NO_UPDATE_CHECK) return null; + const localVersion = readLocalSkillVersion(); + if (!localVersion) return null; + + const cache = readUpdateCache(); + + // Poll the network only when the throttle window has elapsed. Stamp + // lastCheck even on failure so an offline machine doesn't poll every boot. + if (!cache.lastCheck || now - cache.lastCheck > CHECK_INTERVAL_MS) { + const latest = await fetchLatestSkillVersion(); + cache.lastCheck = now; + if (latest) cache.latestVersion = latest; + writeUpdateCache(cache); + } + + const latest = cache.latestVersion; + if (!latest || compareSemver(latest, localVersion) <= 0) return null; + + // Anti-nag: surface a given version at most once per RENOTIFY window. + if (cache.notifiedVersion === latest && cache.notifiedAt && now - cache.notifiedAt < RENOTIFY_INTERVAL_MS) { + return null; + } + cache.notifiedVersion = latest; + cache.notifiedAt = now; + writeUpdateCache(cache); + + return buildUpdateDirective(localVersion, latest); + } catch { + return null; + } +} + +async function cli() { + const ctx = loadContext(process.cwd()); + const updateDirective = await computeUpdateDirective(); + + if (!ctx.hasProduct) { + // Direct stdout message instead of relying on empty output as a signal + // — cheap models miss the empty case more often than the explicit one. + const parts = [ + 'NO_PRODUCT_MD: This project has no PRODUCT.md yet. ' + + 'Stop the current task, load reference/init.md, and follow its ' + + 'instructions to write PRODUCT.md before resuming.', + ]; + if (updateDirective) parts.push(updateDirective); + process.stdout.write(parts.join('\n\n---\n\n') + '\n'); + process.exit(0); + } + const parts = [`# PRODUCT.md\n\n${ctx.product.trim()}`]; + if (ctx.hasDesign) { + parts.push(`# DESIGN.md\n\n${ctx.design.trim()}`); + } + const register = extractRegister(ctx.product); + const next = register + ? `NEXT STEP: This project's register is \`${register}\`. You MUST now read \`reference/${register}.md\` before producing any design output.` + : `NEXT STEP: You MUST now read the matching register reference (\`reference/brand.md\` or \`reference/product.md\`) before producing any design output. Pick based on PRODUCT.md above.`; + parts.push(next); + if (updateDirective) parts.push(updateDirective); + process.stdout.write(parts.join('\n\n---\n\n') + '\n'); +} + +const _running = process.argv[1]; +if (_running?.endsWith('context.mjs') || _running?.endsWith('context.mjs/')) { + cli(); +} diff --git a/.opencode/skills/impeccable/scripts/design-parser.mjs b/.opencode/skills/impeccable/scripts/design-parser.mjs index b68756640..82a5cd7dc 100644 --- a/.opencode/skills/impeccable/scripts/design-parser.mjs +++ b/.opencode/skills/impeccable/scripts/design-parser.mjs @@ -63,7 +63,7 @@ function parseYamlSubset(yaml) { } const key = content.slice(0, colonIdx).trim(); - const rest = content.slice(colonIdx + 1).trim(); + const rest = stripInlineYamlComment(content.slice(colonIdx + 1).trim()); const parent = stack[stack.length - 1].obj; if (rest === '') { @@ -93,6 +93,21 @@ function findTopLevelColon(s) { return -1; } +function stripInlineYamlComment(s) { + let inQuote = null; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (inQuote) { + if (ch === inQuote && s[i - 1] !== '\\') inQuote = null; + } else if (ch === '"' || ch === "'") { + inQuote = ch; + } else if (ch === '#' && i > 0 && /\s/.test(s[i - 1])) { + return s.slice(0, i).trimEnd(); + } + } + return s; +} + function parseScalar(raw) { const s = raw.trim(); if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { diff --git a/.opencode/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.opencode/skills/impeccable/scripts/detector/browser/injected/index.mjs index c03a69caf..ceff16737 100644 --- a/.opencode/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/.opencode/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -9,8 +9,21 @@ if (IS_BROWSER) { const EXTENSION_MODE = (_myScript && _myScript.dataset.impeccableExtension === 'true') || document.documentElement.dataset.impeccableExtension === 'true'; - const BRAND_COLOR = 'oklch(55% 0.25 350)'; - const BRAND_COLOR_HOVER = 'oklch(45% 0.25 350)'; + // Kinpaku gold — pinned to the site's brand token (see + // site/styles/kinpaku-tokens.css --ks-kinpaku). Keep this in sync with + // the picker's C.brand in skill/scripts/live-browser.js and the kit's + // picker section in site/styles/kinpaku-kit.css. + // + // One color across both light and dark host pages. The outline is a + // 2px gesture pointing at an element + a labeled tag — it's a marker, + // not body text, so it doesn't need WCAG AA against the page. The + // label text inside the gold tag is dark (LABEL_INK) which has ~16:1 + // against the leaf gold, so reading the rule name is solid in both + // modes. Hover deepens the gold (preserves chroma — never drops it, + // dropping chroma washes the gold into a sand/olive tone). + const BRAND_COLOR = 'oklch(84% 0.19 80.46)'; + const BRAND_COLOR_HOVER = 'oklch(74% 0.18 80)'; + const LABEL_INK = 'oklch(4% 0.004 95)'; const LABEL_BG = BRAND_COLOR; const OUTLINE_COLOR = BRAND_COLOR; @@ -278,7 +291,7 @@ if (IS_BROWSER) { display: 'flex', alignItems: 'center', whiteSpace: 'nowrap', fontSize: '11px', fontWeight: '600', letterSpacing: '0.02em', - color: 'white', lineHeight: '14px', + color: LABEL_INK, lineHeight: '14px', background: LABEL_BG, fontFamily: 'system-ui, sans-serif', borderRadius: '4px 4px 0 0', @@ -398,7 +411,7 @@ if (IS_BROWSER) { banner.className = 'impeccable-overlay impeccable-banner'; Object.assign(banner.style, { position: 'fixed', top: '0', left: '0', right: '0', zIndex: '100000', - background: LABEL_BG, color: 'white', + background: LABEL_BG, color: LABEL_INK, fontFamily: 'system-ui, sans-serif', fontSize: '13px', display: 'flex', alignItems: 'center', pointerEvents: 'auto', height: '36px', overflow: 'hidden', maxWidth: '100vw', @@ -1223,12 +1236,12 @@ if (IS_BROWSER) { } console.group( `%c[impeccable] ${allFindings.length} anti-pattern${allFindings.length === 1 ? '' : 's'} found`, - 'color: oklch(60% 0.25 350); font-weight: bold' + 'color: oklch(84% 0.19 80.46); font-weight: bold' ); for (const { el, findings } of allFindings) { for (const f of findings) { console.log(`%c${f.type || f.id}%c ${f.detail || f.snippet}`, - 'color: oklch(55% 0.25 350); font-weight: bold', 'color: inherit', el); + 'color: oklch(84% 0.19 80.46); font-weight: bold', 'color: inherit', el); } } console.groupEnd(); @@ -1249,6 +1262,10 @@ if (IS_BROWSER) { const groupMap = new Map(); const _disabled = EXTENSION_MODE ? (window.__IMPECCABLE_CONFIG__?.disabledRules || []) : []; const _ruleOk = (id) => !_disabled.length || !_disabled.includes(id); + // Note: provider-gated rules (--gpt / --gemini) are NOT filtered here. In a + // real browser env (detector page, live overlay, extension) running every + // check is free, so we always surface them; the gating is purely a CLI + // output concern, applied in the Node engines' detect* return paths. for (const el of document.querySelectorAll('*')) { // Skip impeccable's own elements and any descendants (overlays, labels, banner, nav buttons) @@ -1270,11 +1287,23 @@ if (IS_BROWSER) { ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), - ...checkElementHeroEyebrowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementOversizedH1DOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementClippedOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementGptBorderShadowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementTextOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ].filter(f => _ruleOk(f.type)); addBrowserFindings(groupMap, el, findings); + + // Hero eyebrow: the offending element is the eyebrow above the heading, + // not the heading itself — highlight the previous sibling instead. + const eyebrowFindings = checkElementHeroEyebrowDOM(el) + .map(f => ({ type: f.id, detail: f.snippet })) + .filter(f => _ruleOk(f.type)); + if (eyebrowFindings.length > 0 && el.previousElementSibling) { + addBrowserFindings(groupMap, el.previousElementSibling, eyebrowFindings); + } } const pageLevelFindings = []; @@ -1306,6 +1335,14 @@ if (IS_BROWSER) { addBrowserFindings(groupMap, document.body, qualityFindings); } + const creamFindings = checkCreamPalette(document) + .map(f => ({ type: f.id, detail: f.snippet })) + .filter(f => _ruleOk(f.type)); + if (creamFindings.length > 0) { + pageLevelFindings.push(...creamFindings); + addBrowserFindings(groupMap, document.body, creamFindings); + } + // Regex-on-HTML checks (shared with Node) // Clone the document and strip impeccable-live overlay nodes before the // regex scan, so the inspector's own inline styles (transitions on top/ diff --git a/.opencode/skills/impeccable/scripts/detector/cli/main.mjs b/.opencode/skills/impeccable/scripts/detector/cli/main.mjs index 76da09f0c..94283e01b 100644 --- a/.opencode/skills/impeccable/scripts/detector/cli/main.mjs +++ b/.opencode/skills/impeccable/scripts/detector/cli/main.mjs @@ -41,7 +41,7 @@ function formatFindings(findings, jsonMode) { // Stdin handling // --------------------------------------------------------------------------- -async function handleStdin() { +async function handleStdin(options = {}) { const chunks = []; for await (const chunk of process.stdin) chunks.push(chunk); const input = Buffer.concat(chunks).toString('utf-8'); @@ -50,10 +50,10 @@ async function handleStdin() { const fp = parsed?.tool_input?.file_path; if (fp && fs.existsSync(fp)) { return HTML_EXTENSIONS.has(path.extname(fp).toLowerCase()) - ? detectHtml(fp) : detectText(fs.readFileSync(fp, 'utf-8'), fp); + ? detectHtml(fp, options) : detectText(fs.readFileSync(fp, 'utf-8'), fp, options); } } catch { /* not JSON */ } - return detectText(input, ''); + return detectText(input, '', options); } @@ -81,6 +81,8 @@ Scan files or URLs for UI anti-patterns and design quality issues. Options: --fast Regex-only mode (skip static HTML/CSS analysis, faster but misses linked stylesheets) --json Output results as JSON + --gpt Also report GPT-specific provider tells (off by default) + --gemini Also report Gemini-specific provider tells (off by default) --help Show this help message Detection modes: @@ -106,6 +108,10 @@ async function detectCli() { const jsonMode = args.includes('--json'); const helpMode = args.includes('--help'); const fastMode = args.includes('--fast'); + const providers = []; + if (args.includes('--gpt')) providers.push('gpt'); + if (args.includes('--gemini')) providers.push('gemini'); + const scanOptions = { providers }; const targets = args.filter(a => !a.startsWith('--')); if (helpMode) { printUsage(); process.exit(0); } @@ -113,7 +119,7 @@ async function detectCli() { let allFindings = []; if (!process.stdin.isTTY && targets.length === 0) { - allFindings = await handleStdin(); + allFindings = await handleStdin(scanOptions); } else { const paths = targets.length > 0 ? targets : [process.cwd()]; const urlTargetCount = paths.filter(target => /^https?:\/\//i.test(target)).length; @@ -124,8 +130,8 @@ async function detectCli() { if (/^https?:\/\//i.test(target)) { try { const scanner = browserDetector - ? (url) => browserDetector.detectUrl(url) - : (url) => detectUrl(url); + ? (url) => browserDetector.detectUrl(url, scanOptions) + : (url) => detectUrl(url, scanOptions); allFindings.push(...await scanner(target)); } catch (e) { process.stderr.write(`Error: ${e.message}\n`); } continue; @@ -192,9 +198,9 @@ async function detectCli() { const ext = path.extname(file).toLowerCase(); let fileFindings; if (!fastMode && HTML_EXTENSIONS.has(ext)) { - fileFindings = await detectHtml(file); + fileFindings = await detectHtml(file, scanOptions); } else { - fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file); + fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file, scanOptions); } // Annotate findings with import context const importers = importedByMap.get(file); @@ -209,9 +215,9 @@ async function detectCli() { } else if (stat.isFile()) { const ext = path.extname(resolved).toLowerCase(); if (!fastMode && HTML_EXTENSIONS.has(ext)) { - allFindings.push(...await detectHtml(resolved)); + allFindings.push(...await detectHtml(resolved, scanOptions)); } else { - allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved)); + allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved, scanOptions)); } } } diff --git a/.opencode/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.opencode/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 1afbe3f12..88439a5c3 100644 --- a/.opencode/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.opencode/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -38,7 +38,7 @@ const OVERUSED_FONTS = new Set([ // Older monoculture (still ubiquitous): 'inter', 'roboto', 'open sans', 'lato', 'montserrat', 'arial', 'helvetica', // Newer monoculture (the Anthropic-skill / Vercel / GitHub default wave): - 'fraunces', 'instrument sans', + 'fraunces', 'instrument sans', 'instrument serif', 'geist', 'geist sans', 'geist mono', 'mona sans', 'plus jakarta sans', 'space grotesk', 'recoleta', @@ -165,6 +165,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'AI color palette', }, + { + id: 'cream-palette', + category: 'slop', + name: 'Cream / beige palette', + description: + 'A warm cream or beige page background has become the default "tasteful" AI surface, reached for by reflex. Choose a background that comes from a deliberate palette, not the safe warm off-white.', + skillSection: 'Color & Contrast', + skillGuideline: 'cream and beige as the default surface', + }, { id: 'nested-cards', category: 'slop', @@ -183,15 +192,6 @@ const ANTIPATTERNS = [ skillSection: 'Layout & Space', skillGuideline: 'same spacing everywhere', }, - { - id: 'everything-centered', - category: 'slop', - name: 'Everything centered', - description: - 'Every text element is center-aligned. Left-aligned text with asymmetric layouts feels more designed. Center only hero sections and CTAs.', - skillSection: 'Layout & Space', - skillGuideline: 'Center everything', - }, { id: 'bounce-easing', category: 'slop', @@ -247,17 +247,72 @@ const ANTIPATTERNS = [ skillSection: 'Typography', skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding', }, + { + id: 'numbered-section-markers', + category: 'slop', + severity: 'advisory', + name: 'Numbered section markers (01 / 02 / 03)', + description: + 'Numbered display markers as section labels (01, 02, 03) are the AI editorial scaffold one tier deeper than tracked eyebrow chips. If you find yourself reaching for them, choose a different section cadence.', + skillSection: 'Layout & Space', + skillGuideline: 'numbered section markers', + }, + { + id: 'em-dash-overuse', + category: 'slop', + name: 'Em-dash overuse', + description: + 'More than two em-dashes (— or --) in body copy is an AI cadence tell. Use commas, colons, periods, or parentheses instead.', + skillSection: 'Copy', + skillGuideline: 'no em dashes', + }, + { + id: 'marketing-buzzword', + category: 'slop', + name: 'Marketing buzzword', + description: + 'Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does.', + skillSection: 'Copy', + skillGuideline: 'marketing buzzwords', + }, + { + id: 'aphoristic-cadence', + category: 'slop', + name: 'Aphoristic-cadence copy', + description: + 'Three or more sections landing on a short rebuttal sentence ("X. No Y." / "X. Just Y.") or a manufactured-contrast aphorism ("Not a feature. A platform.") reads as AI cadence, not voice. Once is fine; the pattern is the tell.', + skillSection: 'Copy', + skillGuideline: 'aphoristic cadence', + }, + { + id: 'oversized-h1', + category: 'slop', + name: 'Oversized hero headline', + description: + 'A full-sentence headline set at display size ends up dominating the viewport, leaving no room for anything else above the fold. A punchy one- or two-word headline at that size is fine — the problem is a long headline blown up too large. Set long headlines smaller, or tighten the copy.', + skillSection: 'Typography', + skillGuideline: 'long headline set at display size', + }, + { + id: 'extreme-negative-tracking', + category: 'slop', + name: 'Crushed letter spacing', + description: + 'Letter-spacing pulled tighter than the point where characters keep their own shapes costs legibility. Tighten display type optically, not destructively.', + skillSection: 'Typography', + skillGuideline: 'letter spacing crushed past legibility', + }, + { + id: 'broken-image', + category: 'quality', + name: 'Broken or placeholder image', + description: + ' tags with empty src, missing src, or placeholder values ship as broken-image boxes. Use real images, generated assets, or remove the tag.', + skillSection: 'Imagery', + skillGuideline: 'broken image references', + }, // ── Quality: general design and accessibility issues ── - { - id: 'pure-black-white', - category: 'quality', - name: 'Pure black background', - description: - 'Pure #000000 as a background color looks harsh and unnatural. Tint it slightly toward your brand hue (e.g., oklch(12% 0.01 250)) for a more refined feel.', - skillSection: 'Color & Contrast', - skillGuideline: 'pure black (#000)', - }, { id: 'gray-on-color', category: 'quality', @@ -297,7 +352,9 @@ const ANTIPATTERNS = [ category: 'quality', name: 'Cramped padding', description: - 'Text is too close to the edge of its container. Add at least 8px (ideally 12-16px) of padding inside bordered or colored containers.', + 'Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.', + skillSection: 'Layout & Space', + skillGuideline: 'inside bordered or colored containers', }, { id: 'body-text-viewport-edge', @@ -350,6 +407,70 @@ const ANTIPATTERNS = [ description: 'Letter spacing above 0.05em on body text disrupts natural character groupings and slows reading. Reserve wide tracking for short uppercase labels only.', }, + { + id: 'text-overflow', + category: 'quality', + name: 'Content overflowing its container', + description: + 'Content renders wider than its container, spilling out or forcing a horizontal scrollbar. Let text wrap, constrain widths, or give the region a deliberate scroll affordance.', + skillSection: 'Layout & Space', + skillGuideline: 'content wider than its container', + }, + { + id: 'clipped-overflow-container', + category: 'quality', + name: 'Positioned child clipped by overflow container', + description: + 'A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.', + skillSection: 'Layout & Space', + skillGuideline: 'overflow container clipping positioned children', + }, + + // ── Provider tells: opt-in via --gpt / --gemini (gated off by default) ── + { + id: 'gpt-thin-border-wide-shadow', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Hairline border with wide shadow', + description: + 'A hairline border paired with a wide, diffuse shadow is a recurring generated-UI signature. Commit to one — a defined edge or a soft elevation — rather than both at once.', + skillSection: 'Visual Details', + skillGuideline: 'hairline border plus wide diffuse shadow', + }, + { + id: 'repeating-stripes-gradient', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Repeating-gradient stripes', + description: + 'Repeating-gradient stripes used as surface decoration are a recurring generated-UI signature. Reach for a deliberate texture or leave the surface plain.', + skillSection: 'Visual Details', + skillGuideline: 'repeating-gradient decorative stripes', + }, + { + id: 'theater-slop-phrase', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Theater framing copy', + description: + 'Dismissing something as "theater" is a recurring generated-copy tic. Say plainly what the thing does or does not do.', + skillSection: 'Copy', + skillGuideline: 'theater framing copy', + }, + { + id: 'image-hover-transform', + category: 'slop', + severity: 'advisory', + gated: 'gemini', + name: 'Image hover transform', + description: + 'Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction.', + skillSection: 'Motion', + skillGuideline: 'image scale or rotate on hover', + }, ]; // --- cli/engine/shared/color.mjs --- @@ -527,11 +648,6 @@ function checkColors(opts) { } const findings = []; - // Pure black background (only solid or near-solid, not semi-transparent overlays) - if (bgColor && bgColor.a >= 0.9 && bgColor.r === 0 && bgColor.g === 0 && bgColor.b === 0) { - findings.push({ id: 'pure-black-white', snippet: '#000000 background' }); - } - if (hasDirectText && textColor && !isEmojiOnly) { // Run background-dependent checks against either a solid bg or, if the // ancestor is a gradient, against every gradient stop (use the worst case). @@ -587,9 +703,6 @@ function checkColors(opts) { // Tailwind class checks if (classList) { const classStr = typeof classList === 'string' ? classList : Array.from(classList).join(' '); - if (/\bbg-black\b(?!\/)/.test(classStr)) { - findings.push({ id: 'pure-black-white', snippet: 'bg-black' }); - } const grayMatch = classStr.match(/\btext-(?:gray|slate|zinc|neutral|stone)-\d+\b/); const colorBgMatch = classStr.match(/\bbg-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+\b/); @@ -905,12 +1018,6 @@ function checkHtmlPatterns(html) { // --- Color --- - // Pure black background - const pureBlackBgRe = /background(?:-color)?\s*:\s*(?:#000000|#000|rgb\(\s*0,\s*0,\s*0\s*\))\b/gi; - if (pureBlackBgRe.test(html)) { - findings.push({ id: 'pure-black-white', snippet: 'Pure #000 background' }); - } - // AI color palette: purple/violet const purpleHexRe = /#(?:7c3aed|8b5cf6|a855f7|9333ea|7e22ce|6d28d9|6366f1|764ba2|667eea)\b/gi; if (purpleHexRe.test(html)) { @@ -1027,6 +1134,39 @@ function checkHtmlPatterns(html) { } } + // --- Provider tells (gated): repeating-gradient stripes (GPT) --- + if (/repeating-(?:linear|radial|conic)-gradient\s*\(/i.test(html)) { + findings.push({ id: 'repeating-stripes-gradient', snippet: 'repeating-gradient decorative stripes' }); + } + + // --- Provider tells (gated): "X theater" framing copy (GPT) --- + // Lives here (regex-on-HTML) rather than in the text-content analyzers so it + // runs in the bundled browser path too, not just the CLI/static path. + { + const bodyText = html + .replace(/]*>[\s\S]*?<\/script>/gi, ' ') + .replace(/]*>[\s\S]*?<\/style>/gi, ' ') + .replace(/<[^>]+>/g, ' '); + const tm = /\b(\w+)\s+theater\b/i.exec(bodyText); + if (tm) findings.push({ id: 'theater-slop-phrase', snippet: `"${tm[0].trim()}"` }); + } + + // --- Provider tells (gated): image hover transform (Gemini) --- + // A CSS `img...:hover { transform: ... }` rule, or a Tailwind hover:scale / + // hover:rotate / hover:translate utility on an . Each distinct + // mechanism is its own finding. + const imgHoverCss = /\bimg\b[^,{}]*:hover\b[^{}]*\{[^}]*\btransform\s*:\s*(?:scale|rotate|translate|matrix|skew)/i; + if (imgHoverCss.test(html)) { + findings.push({ id: 'image-hover-transform', snippet: 'img:hover { transform } rule' }); + } + const imgTagRe = /]*\bclass\s*=\s*"([^"]*)"/gi; + let im; + while ((im = imgTagRe.exec(html)) !== null) { + if (/\bhover:(?:scale|rotate|translate|skew)-/.test(im[1])) { + findings.push({ id: 'image-hover-transform', snippet: 'Tailwind hover transform on ' }); + } + } + return findings; } @@ -1673,7 +1813,7 @@ function resolveLengthPx(value, fontSizePx) { // Both adapters resolve font-size, line-height and letter-spacing to pixels // before calling this so the pure function only deals with numbers. function checkQuality(opts) { - const { el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax = 80, viewportWidth = 0 } = opts; + const { el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax = 80, viewportWidth = 0, win = null } = opts; const findings = []; // Skip browser extension injected elements const elId = el.id || ''; @@ -1724,6 +1864,155 @@ function checkQuality(opts) { } } + // --- Flush against a visible boundary --- + // Fires when a container has a visible boundary (border, outline, OR a + // non-transparent background) AND near-zero padding on the bounded + // side(s) AND text-bearing children land flush against the boundary. + // + // Distinct from cramped-padding: that rule needs the element itself to + // have direct text (hasDirectText). This rule targets the OPPOSITE + // shape — a container with NO direct text, only children — which is + // exactly what cramped-padding misses (a section wrapping a label + + // list lands a free pass). + // + // The classic shape: agent writes `padding: 28px 0 0` shorthand on a + // section that also has a border, zeroing horizontal padding so the + // text-bearing children touch the side borders. Background and + // outline count too: a colored card with zero padding has the same + // visual failure mode. + { + const FLUSH_SKIP_TAGS = new Set(['HTML', 'BODY', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ARTICLE', 'ASIDE', 'BUTTON', 'A', 'LABEL', 'SUMMARY', 'CODE', 'PRE', 'INPUT', 'TEXTAREA', 'SELECT', 'FORM', 'FIGURE', 'TABLE', 'TBODY', 'THEAD', 'TR', 'TD', 'TH']); + const upperTag = tag ? tag.toUpperCase() : ''; + const elPosition = style.position || ''; + if ( + !FLUSH_SKIP_TAGS.has(upperTag) && + !hasDirectText && + !['fixed', 'absolute'].includes(elPosition) && + el.children && el.children.length > 0 + ) { + const isTransparent = (c) => + !c || c === 'transparent' || c === 'rgba(0, 0, 0, 0)' || + /^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*0(?:\.0+)?\s*\)$/.test(c); + + const borderW = { + top: parseFloat(style.borderTopWidth) || 0, + right: parseFloat(style.borderRightWidth) || 0, + bottom: parseFloat(style.borderBottomWidth) || 0, + left: parseFloat(style.borderLeftWidth) || 0, + }; + const borderVisible = { + top: borderW.top > 0 && !isTransparent(style.borderTopColor), + right: borderW.right > 0 && !isTransparent(style.borderRightColor), + bottom: borderW.bottom > 0 && !isTransparent(style.borderBottomColor), + left: borderW.left > 0 && !isTransparent(style.borderLeftColor), + }; + // Outline detection. jsdom decomposes `border` shorthand into + // border{Top,…}Width/Color but does NOT decompose `outline` — + // the longhands come back empty when the value was set via the + // shorthand. Fall back to parsing `style.outline` ourselves. + let outlineW = parseFloat(style.outlineWidth) || 0; + let outlineStyleVal = style.outlineStyle || ''; + let outlineColorVal = style.outlineColor || ''; + if (!outlineW && style.outline) { + const wMatch = style.outline.match(/(\d+(?:\.\d+)?)\s*px/); + if (wMatch) outlineW = parseFloat(wMatch[1]) || 0; + if (!outlineStyleVal) { + outlineStyleVal = /\b(solid|dashed|dotted|double|groove|ridge|inset|outset)\b/.test(style.outline) ? 'solid' : ''; + } + if (!outlineColorVal) { + const cMatch = style.outline.match(/(rgba?\([^)]+\)|#[0-9a-fA-F]{3,8}|[a-zA-Z]+)\s*$/); + if (cMatch) outlineColorVal = cMatch[1]; + } + } + const outlineVisible = outlineW > 0 && !isTransparent(outlineColorVal) && outlineStyleVal && outlineStyleVal !== 'none'; + const bgVisible = !isTransparent(style.backgroundColor); + + const anyVisible = borderVisible.top || borderVisible.right || borderVisible.bottom || borderVisible.left || outlineVisible || bgVisible; + if (anyVisible) { + // Resolve padding to px (jsdom returns raw "1.5rem" etc., not the + // computed px value; parseFloat would strip the unit and treat + // 1.5rem as 1.5px, false-flagging legitimate insets). + const pad = { + top: resolveLengthPx(style.paddingTop, fontSize) ?? 0, + right: resolveLengthPx(style.paddingRight, fontSize) ?? 0, + bottom: resolveLengthPx(style.paddingBottom, fontSize) ?? 0, + left: resolveLengthPx(style.paddingLeft, fontSize) ?? 0, + }; + const PAD_THRESHOLD = 2; + // Children-insulate-this-side: a side is insulated if ANY direct + // child has its own padding ≥ 4px on that side. Rationale: in + // typical flow, only the first/last (or leftmost/rightmost) + // children actually sit at the parent's edges. If even one of + // them has its own padding, the visual flush is broken on that + // side. Classic example: a column-flow card frame where the + // top child (header) has padding-top:12 and the bottom child + // (footer) has padding-bottom:8 — the parent's padding:0 doesn't + // matter; nothing is actually flush. The `any-child-insulates` + // heuristic accepts some false negatives (a card with one heavily + // padded middle child won't flag) for far fewer false positives. + const CHILD_INSULATE_THRESHOLD = 4; + const childrenInsulate = { top: false, right: false, bottom: false, left: false }; + for (const child of el.children) { + let childStyle = null; + if (win && typeof win.getComputedStyle === 'function') { + try { childStyle = win.getComputedStyle(child); } catch {} + } + if (!childStyle && typeof getComputedStyle === 'function') { + try { childStyle = getComputedStyle(child); } catch {} + } + if (!childStyle) continue; + const childPad = { + top: resolveLengthPx(childStyle.paddingTop, fontSize) ?? 0, + right: resolveLengthPx(childStyle.paddingRight, fontSize) ?? 0, + bottom: resolveLengthPx(childStyle.paddingBottom, fontSize) ?? 0, + left: resolveLengthPx(childStyle.paddingLeft, fontSize) ?? 0, + }; + for (const s of ['top', 'right', 'bottom', 'left']) { + if (childPad[s] >= CHILD_INSULATE_THRESHOLD) childrenInsulate[s] = true; + } + } + + const flushSides = []; + for (const side of ['top', 'right', 'bottom', 'left']) { + const sideBounded = borderVisible[side] || outlineVisible || bgVisible; + if (sideBounded && pad[side] <= PAD_THRESHOLD && !childrenInsulate[side]) { + flushSides.push(side); + } + } + + if (flushSides.length > 0) { + // Confirm at least one direct child has substantial text content + // (> 4 chars). Without this, the flush is harmless: e.g. an + // image-only card. + let hasTextChild = false; + for (const child of el.children) { + const childText = (child.textContent || '').trim(); + if (childText.length > 4) { hasTextChild = true; break; } + } + if (hasTextChild) { + const cls = (typeof el.className === 'string' && el.className.trim()) + ? el.className.trim().split(/\s+/)[0] + : ''; + const boundaryParts = []; + const borderSidesVisible = ['top', 'right', 'bottom', 'left'].filter(s => borderVisible[s]); + if (borderSidesVisible.length === 4) boundaryParts.push('border'); + else if (borderSidesVisible.length > 0) boundaryParts.push(`border-${borderSidesVisible.join('/')}`); + if (outlineVisible) boundaryParts.push('outline'); + if (bgVisible) boundaryParts.push('bg'); + const sidesLabel = flushSides.length === 4 ? 'all sides' : flushSides.join('/'); + const ident = cls + ? `<${tag.toLowerCase()}> "${cls}"` + : `<${tag.toLowerCase()}>`; + findings.push({ + id: 'cramped-padding', + snippet: `${ident}: children flush against ${boundaryParts.join('+')} on ${sidesLabel} (no inset)`, + }); + } + } + } + } + } + // --- Body text touching viewport edge --- (browser-only: needs rect) // Catches the failure mode where the agent ships body paragraphs // with NO container providing horizontal padding — text bleeds @@ -1804,6 +2093,20 @@ function checkQuality(opts) { } } + // --- Crushed letter spacing (mirror of wide-tracking) --- + // Tracking pulled tighter than ~-0.05em crushes characters into each other. + // Optical tightening that display type legitimately wants (around -0.02em) + // stays well above this floor. + if (hasDirectText && textLen > 20 && fontSize > 0) { + if (letterSpacingPx != null && letterSpacingPx < 0) { + const trackingEm = letterSpacingPx / fontSize; + if (trackingEm <= -0.05) { + const excerpt = (el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 40); + findings.push({ id: 'extreme-negative-tracking', snippet: `letter-spacing: ${trackingEm.toFixed(2)}em — "${excerpt}"` }); + } + } + } + return findings; } @@ -1820,7 +2123,7 @@ function checkElementQualityDOM(el) { const rect = el.getBoundingClientRect(); const lineMax = (typeof window !== 'undefined' && window.__IMPECCABLE_CONFIG__?.lineLengthMax) || 80; const viewportWidth = (typeof window !== 'undefined' ? window.innerWidth : 0) || 0; - return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax, viewportWidth }); + return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax, viewportWidth, win: typeof window !== 'undefined' ? window : null }); } // Pure page-level skipped-heading walk. Takes a Document so it works in both @@ -1862,7 +2165,7 @@ function checkElementQuality(el, style, tag, window) { const fontSize = resolveFontSizePx(el, window); const lineHeightPx = resolveLengthPx(style.lineHeight, fontSize); const letterSpacingPx = resolveLengthPx(style.letterSpacing, fontSize); - return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect: null }); + return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect: null, win: window }); } function checkElementBorders(tag, style, overrides, resolvedRadius) { @@ -2303,40 +2606,213 @@ function checkPageLayout(doc, win) { } } - // Everything centered - const textEls = doc.querySelectorAll('h1, h2, h3, h4, h5, h6, p, li, div, button'); - let centeredCount = 0; - let totalText = 0; - for (const el of textEls) { - const hasDirectText = [...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim().length >= 3); - if (!hasDirectText) continue; - totalText++; - - let cur = el; - let isCentered = false; - while (cur && cur.nodeType === 1) { - const rawStyle = cur.getAttribute?.('style') || ''; - const cls = cur.getAttribute?.('class') || ''; - if (/text-align\s*:\s*center/i.test(rawStyle) || /\btext-center\b/.test(cls)) { - isCentered = true; - break; - } - if (cur.tagName === 'BODY') break; - cur = cur.parentElement; - } - if (isCentered) centeredCount++; - } - - if (totalText >= 5 && centeredCount / totalText > 0.7) { - findings.push({ - id: 'everything-centered', - snippet: `${centeredCount}/${totalText} text elements centered (${Math.round(centeredCount / totalText * 100)}%)`, - }); - } - return findings; } +// ─── Cream / beige palette (the default "tasteful" AI surface) ──────────────── +// A warm, lightly-tinted off-white page background — light, with R≥G≥B and a +// small warm tint (not white, not a strong color). The current reflex surface. +function isCreamColor(rgb) { + if (!rgb) return false; + const { r, g, b } = rgb; + if (Math.min(r, g, b) < 209) return false; // must be light + if (!(r >= g && g >= b)) return false; // warm ordering + const warmth = r - b; + return warmth >= 6 && warmth <= 48; // tinted, not white, not strong +} + +// Tailwind background utilities that render as a warm off-white surface. The +// static engine doesn't fetch Tailwind's CSS, so a `bg-amber-50` on +// resolves to nothing in computed style — catch it from the class list +// instead. Candidate tokens map to their actual Tailwind hex and are still +// filtered through isCreamColor, so neutral grays (stone) and over-saturated +// shades drop out on their own. +const TAILWIND_BG_HEX = { + 'bg-amber-50': '#fffbeb', 'bg-amber-100': '#fef3c7', + 'bg-orange-50': '#fff7ed', 'bg-orange-100': '#ffedd5', + 'bg-yellow-50': '#fefce8', + 'bg-stone-50': '#fafaf9', 'bg-stone-100': '#f5f5f4', 'bg-stone-200': '#e7e5e4', +}; + +function creamFromClassList(cls) { + if (!cls) return null; + // Arbitrary value: bg-[#f5f0e6] / bg-[rgb(245_240_230)] (underscores = spaces). + const arb = cls.match(/\bbg-\[([^\]]+)\]/); + if (arb && isCreamColor(parseAnyColor(arb[1].replace(/_/g, ' ')))) return `bg-[${arb[1]}]`; + // Named warm-light utilities. + for (const [tok, hex] of Object.entries(TAILWIND_BG_HEX)) { + if (new RegExp(`(^|\\s)${tok}($|\\s)`).test(cls) && isCreamColor(parseAnyColor(hex))) return tok; + } + return null; +} + +function checkCreamPalette(doc, win) { + const findings = []; + const body = doc.body || (doc.querySelector ? doc.querySelector('body') : null); + if (!body) return findings; + const html = doc.documentElement; + const getCS = (el) => (win ? win.getComputedStyle(el) : getComputedStyle(el)); + + // 1. Computed background — covers inline / close.', 'Prefix every preview selector with the matching [data-impeccable-variant="N"] selector.', 'Keep selectors anchored to the generated variant wrapper; do not rely on component CSS scoping for preview rules.', ], forbidden: [ 'Do not use @scope for this styleMode.', + 'Do not wrap style content in a JSX/TSX template literal ({` ... `}); that syntax is for .tsx/.jsx only.', + 'Do not put { immediately after the style opening tag; Astro parses { as expression syntax.', ], }; } @@ -629,4 +632,15 @@ if (_running?.endsWith('live-wrap.mjs') || _running?.endsWith('live-wrap.mjs/')) } // Test exports (used by tests/live-wrap.test.mjs) -export { buildSearchQueries, findElement, findClosingLine, detectCommentSyntax }; +export { + buildSearchQueries, + findElement, + findClosingLine, + detectCommentSyntax, + findAllElements, + filterByText, + findFileWithQuery, + detectStyleMode, + buildCssAuthoring, + buildCssSelectorPrefixExamples, +}; diff --git a/.opencode/skills/impeccable/scripts/live.mjs b/.opencode/skills/impeccable/scripts/live.mjs index cafb0eca9..8acd300ed 100644 --- a/.opencode/skills/impeccable/scripts/live.mjs +++ b/.opencode/skills/impeccable/scripts/live.mjs @@ -21,7 +21,7 @@ import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { loadContext } from './load-context.mjs'; +import { loadContext } from './context.mjs'; import { resolveFiles } from './live-inject.mjs'; import { readLiveServerInfo } from './impeccable-paths.mjs'; @@ -80,7 +80,7 @@ The agent should then: process.exit(1); } - // 4. Load PRODUCT.md + DESIGN.md context (auto-migrates legacy .impeccable.md) + // 4. Load PRODUCT.md + DESIGN.md context. const ctx = loadContext(process.cwd()); // 5. Compute drift-heal: compare resolved inject targets against the @@ -102,7 +102,6 @@ The agent should then: hasDesign: ctx.hasDesign, design: ctx.design, designPath: ctx.designPath, - migrated: ctx.migrated, }, null, 2)); } diff --git a/.opencode/skills/impeccable/scripts/load-context.mjs b/.opencode/skills/impeccable/scripts/load-context.mjs deleted file mode 100644 index dc340bf16..000000000 --- a/.opencode/skills/impeccable/scripts/load-context.mjs +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Shared context loader for every impeccable command that needs to know - * "who is this for" and "what does this look like". - * - * Input: project root (process.cwd()). - * - * Output (JSON to stdout): - * { - * hasProduct: boolean, // PRODUCT.md found (or auto-migrated) - * product: string | null, // PRODUCT.md contents - * productPath: string | null, // relative path - * hasDesign: boolean, // DESIGN.md found - * design: string | null, // DESIGN.md contents - * designPath: string | null, - * migrated: boolean, // true if we auto-renamed .impeccable.md -> PRODUCT.md - * contextDir: string, // absolute path of the directory the files were found in - * } - * - * Filename matching is case-insensitive for PRODUCT.md and DESIGN.md. The - * Google DESIGN.md convention is uppercase at repo root; Kiro-style and - * lowercase variants are also matched so users don't get punished for case. - * - * Lookup directory resolution (first match wins): - * 1. process.env.IMPECCABLE_CONTEXT_DIR (absolute or relative to cwd) - * 2. cwd, if PRODUCT.md / DESIGN.md / .impeccable.md is there (back-compat) - * 3. Auto-fallback subdirectories of cwd: .agents/context/, then docs/ - * 4. cwd as a default "no context found" location - * - * Legacy `.impeccable.md` -> PRODUCT.md migration only fires at cwd root; - * fallback directories are read-only as far as auto-rename is concerned. - */ - -import fs from 'node:fs'; -import path from 'node:path'; - -const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; -const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; -const LEGACY_NAMES = ['.impeccable.md']; -const FALLBACK_DIRS = ['.agents/context', 'docs']; - -/** - * Resolve the directory that holds PRODUCT.md / DESIGN.md for - * this project. Exported so other scripts (e.g. live-server.mjs) can read the - * design files from the same location the loader uses. - */ -export function resolveContextDir(cwd = process.cwd()) { - // 1. Explicit override - const envDir = process.env.IMPECCABLE_CONTEXT_DIR; - if (envDir && envDir.trim()) { - const trimmed = envDir.trim(); - return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); - } - - // 2. cwd wins if any canonical or legacy file is there. We check legacy too - // so the auto-migration path in loadContext stays predictable. - if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES, ...LEGACY_NAMES])) { - return cwd; - } - - // 3. Auto-fallback subdirs. Match if PRODUCT.md or DESIGN.md is present; - // legacy `.impeccable.md` does not pull the lookup into a fallback dir. - for (const rel of FALLBACK_DIRS) { - const candidate = path.resolve(cwd, rel); - if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { - return candidate; - } - } - - // 4. Nothing found — keep the historical "default to cwd" behaviour so the - // caller's `hasProduct === false` branch still fires the same way. - return cwd; -} - -export function loadContext(cwd = process.cwd()) { - let migrated = false; - const contextDir = resolveContextDir(cwd); - - // 1. Look for PRODUCT.md (case-insensitive) in the resolved dir - let productPath = firstExisting(contextDir, PRODUCT_NAMES); - - // 2. Legacy: if no PRODUCT.md but .impeccable.md exists at cwd root, rename - // it in place. We only migrate at the root — fallback dirs are read-only - // so we don't surprise users by mutating files under docs/ or .agents/. - if (!productPath && contextDir === cwd) { - const legacyPath = firstExisting(cwd, LEGACY_NAMES); - if (legacyPath) { - const newPath = path.join(cwd, 'PRODUCT.md'); - try { - fs.renameSync(legacyPath, newPath); - productPath = newPath; - migrated = true; - } catch { - // Rename failed (permissions, etc.) — fall back to reading legacy in place - productPath = legacyPath; - } - } - } - - // 3. DESIGN.md (case-insensitive) - const designPath = firstExisting(contextDir, DESIGN_NAMES); - - const product = productPath ? safeRead(productPath) : null; - const design = designPath ? safeRead(designPath) : null; - - return { - hasProduct: !!product, - product, - productPath: productPath ? path.relative(cwd, productPath) : null, - hasDesign: !!design, - design, - designPath: designPath ? path.relative(cwd, designPath) : null, - migrated, - contextDir, - }; -} - -function firstExisting(dir, names) { - for (const name of names) { - const abs = path.join(dir, name); - if (fs.existsSync(abs)) return abs; - } - return null; -} - -function safeRead(p) { - try { return fs.readFileSync(p, 'utf-8'); } catch { return null; } -} - -// --------------------------------------------------------------------------- -// CLI mode — print the context as JSON -// --------------------------------------------------------------------------- - -function cli() { - const result = loadContext(process.cwd()); - console.log(JSON.stringify(result, null, 2)); -} - -const _running = process.argv[1]; -if (_running?.endsWith('load-context.mjs') || _running?.endsWith('load-context.mjs/')) { - cli(); -} diff --git a/.opencode/skills/impeccable/scripts/palette.mjs b/.opencode/skills/impeccable/scripts/palette.mjs new file mode 100644 index 000000000..4c3f1751a --- /dev/null +++ b/.opencode/skills/impeccable/scripts/palette.mjs @@ -0,0 +1,633 @@ +#!/usr/bin/env node +/** + * Brand-seed picker. Returns one OKLCH seed color + the mood it most + * naturally evokes, and teaches the model how to compose a full palette + * around it. + * + * The seed is the brand's anchor color. The 5-role palette (bg, surface, + * ink, accent, muted) is composed by the caller at runtime using their + * judgment + the brief (PRODUCT.md / DESIGN.md / user prompt), NOT picked + * from a frozen 4-color preset. + * + * Why: 4-color frozen palettes drift toward safe defaults (warm-cream bg, + * complementary accent on near-white) regardless of brief. A single seed + + * the model's own composition lets the same seed produce a dark-mode jazz + * club or a light-mode hospitality brand depending on what the brief calls + * for. Tested empirically against curated 4-color palettes; seed approach + * wins on mood-fit in 3 of 5 cases and ties on the rest. + * + * Usage: + * node scripts/palette.mjs # pick at random + * node scripts/palette.mjs --id seed-021 # pick a specific seed + * node scripts/palette.mjs --from # hash to a seed (deterministic) + * + * Env vars: + * IMPECCABLE_PALETTE_SEED — same as --from; useful for the eval harness + * to make runs reproducible. + */ + +import crypto from 'node:crypto'; + +// Seeds are inlined (129 entries, hand-curated via a tinder review of +// ~400 candidates from ColorHunt + synthesis + Radix/brand/Pantone anchors). +// Each carries a mood + strategy the judging model produced — surfaced as +// hints, not commands; the brief still drives composition. +const SEEDS = [ + { id: "seed-200", oklch: [0.360, 0.137, 0.0], + mood: "Aesop apothecary shelf — oxblood bottle glass against linen, considered and unhurried", + strategy: "Seed is a deep desaturated red-brown that reads as brand ink itself; I push primary darker toward bottle-glass oxblood, pair with a pure white surface so the red does the work, and use a clear pale-blush accent that can carry dark text in pills." }, + { id: "seed-000", oklch: [0.400, 0.130, 0.0], + mood: "oxblood leather banquette in a 1940s steakhouse — low lamplight on dark wood and burgundy", + strategy: "Near-black bg with the faintest red undertone lets the oxblood primary glow like lamplit leather; warm cream ink and a brass accent complete the chophouse register." }, + { id: "seed-002", oklch: [0.450, 0.150, 0.0], + mood: "darkroom red light — analog photography, blood-warm safelight glow on chemical trays", + strategy: "Near-black surface with a deep oxblood primary lets the seed function like a safelight in a darkroom — the bg disappears so the red becomes the only emotional signal." }, + { id: "seed-003", oklch: [0.500, 0.194, 0.0], + mood: "darkroom safelight — the deep oxblood glow of analog photography, chemical and contemplative", + strategy: "Anchored the seed as primary against pure near-black so the red reads like a single illuminated bulb in a developing room, with cool desaturated ink to evoke silver gelatin print tones." }, + { id: "seed-004", oklch: [0.546, 0.204, 3.4], + mood: "midnight boudoir — velvet rose under low lamplight, perfumed and intimate", + strategy: "Near-black surface lets the rose seed glow like silk in shadow; a warm champagne accent provides the candle-flame counterpoint without breaking the hush." }, + { id: "seed-005", oklch: [0.550, 0.180, 0.0], + mood: "smoldering vermillion at dusk — the last red ember in a blacksmith's forge, iron-rich and quietly violent", + strategy: "Near-black gallery surround lets the seed read as glowing forged metal; ink stays warm-off-white, accent shifts to a hotter ember orange so the primary feels like cooling steel against a fresh strike." }, + { id: "seed-201", oklch: [0.647, 0.262, 0.3], + mood: "Figma plugin marketplace red — confident product-brand crimson, the kind a modern dev tool uses for a 'live' indicator or a primary CTA on a pristine docs page", + strategy: "Pure white surface lets a high-chroma crimson primary do all the brand work, paired with a hue-shifted warm coral accent for hierarchy without competing saturation" }, + { id: "seed-006", oklch: [0.650, 0.160, 0.0], + mood: "1960s Italian cinema — Technicolor lipstick red against a darkened theater", + strategy: "Pure near-black surface lets a saturated cinematic red and its warm peach accent perform like film light projected in a dark room — the brand colors carry the drama, the bg disappears." }, + { id: "seed-008", oklch: [0.520, 0.200, 10.4], + mood: "Negroni hour at a Milanese bar — bittersweet crimson, vermouth and amaro under low tungsten", + strategy: "Seed is a saturated red-crimson with cinematic weight, so I sit it on near-black to let the primary glow like backlit liquor, with a warmer amber accent acting as the citrus twist against the bitter red." }, + { id: "seed-010", oklch: [0.563, 0.223, 11.0], + mood: "Negroni hour on a Milan rooftop — bittersweet crimson, aperitivo light, polished restraint", + strategy: "Seed is a vivid carmine-red with strong chroma, so the surface gets out of the way (pure white) and lets the primary do the aperitivo work, with a cooled garnet accent for tension." }, + { id: "seed-202", oklch: [0.643, 0.247, 7.0], + mood: "Glossier brand pink — modern beauty editorial, confident and current", + strategy: "Pure white bg lets a saturated rose-red primary do all the brand work, paired with a deeper crimson accent for hierarchy — the Stripe/Glossier move where the color carries the mood." }, + { id: "seed-013", oklch: [0.400, 0.130, 20.0], + mood: "Tuscan cellar at dusk — aged terracotta, oxidized iron, the deep red of decanted Sangiovese", + strategy: "Black surface lets the oxblood seed and copper accent glow like firelight on cellar stone; brand colors carry all the warmth while the room recedes." }, + { id: "seed-014", oklch: [0.450, 0.150, 20.0], + mood: "smoldering tannery — oxblood leather, cured under low workshop light", + strategy: "Anchor the deep oxblood seed as primary against a near-black architectural ground, then lift with a single warm ember accent so the leather reads burnished rather than bloody." }, + { id: "seed-016", oklch: [0.550, 0.180, 20.0], + mood: "Negroni hour on a Roman terrace — bitter campari red, vermouth, late golden light spilling on white linen", + strategy: "Pure white surface lets the campari-red primary do all the emotional work, paired with a deeper oxblood accent for bittersweet depth — Italian aperitivo restraint, not warmth-washed." }, + { id: "seed-205", oklch: [0.634, 0.254, 17.6], + mood: "Aesop apothecary bottle — considered red-coral on a clinical white surface, the kind of brand restraint where one saturated object does all the work", + strategy: "Default A pure white surface lets a single coral-red primary carry the entire brand voice; accent shifts to a deeper oxblood for hierarchy without competing chroma." }, + { id: "seed-011", oklch: [0.639, 0.207, 13.5], + mood: "Aperitivo hour in Milan — Campari glow on a white marble bar, crisp and effervescent", + strategy: "Pure white gallery backdrop lets the Campari-red primary ring like a single bitter note; ink is near-black with a whisper of warmth, accent shifts to a deeper oxblood for hierarchy without competing hues." }, + { id: "seed-015", oklch: [0.527, 0.202, 22.7], + mood: "Negroni hour on a Milanese terrace — bittersweet vermillion, aperitivo glassware catching low sun", + strategy: "Seed becomes a saturated aperitivo-red primary against pure white so the color carries the bittersweet warmth alone, paired with a deep oxblood accent for typographic gravitas." }, + { id: "seed-023", oklch: [0.427, 0.175, 29.2], + mood: "blacksmith's forge at dusk — iron heated to ember red, the deep glow of oxidized metal and quenching oil", + strategy: "Pure black bg lets the seed's ember-red glow radiate like hot iron in a dark forge; accent shifts to a copper-amber to suggest scaling metal and sparks, while ink stays near-white for tool-precise legibility." }, + { id: "seed-206", oklch: [0.614, 0.234, 28.2], + mood: "Aesop apothecary bottle — considered red-orange on lab-white, calm utility with a single confident pigment", + strategy: "Pure white surface lets a saturated vermilion primary do all the brand work, paired with a deep oxblood accent for hierarchy without introducing a second hue family" }, + { id: "seed-029", oklch: [0.665, 0.222, 25.7], + mood: "Negroni hour at a Milanese bar — bittersweet orange-red liqueur catching late afternoon light on polished marble", + strategy: "Pure white surface lets the seed's vermilion read like Campari in a glass; a deeper oxblood accent provides the bitter depth, with neutral graphite ink keeping the editorial restraint of Italian design." }, + { id: "seed-022", oklch: [0.418, 0.155, 27.2], + mood: "Pompeiian red fresco — oxidized cinnabar on a museum wall, archaeological gravity", + strategy: "Pure black gallery surface lets the seed's iron-oxide red read as a lit artifact; accent shifts to an aged terracotta amber, so primary and accent form a fired-clay duet against neutral void." }, + { id: "seed-024", oklch: [0.464, 0.169, 26.9], + mood: "Mid-century darkroom under the safelight — developer trays, oxblood leather, the quiet patience of a print emerging", + strategy: "Seed becomes a deep oxblood primary; surface stays pure black so the red glows like a safelight, with a warmer ember accent for hierarchy" }, + { id: "seed-026", oklch: [0.489, 0.190, 28.3], + mood: "smoldering ember in a blacksmith's forge — iron-hot rust, soot, and controlled fire", + strategy: "Near-black soot background lets the seed's red-orange glow like heated metal; ink is bone-white, accent is a cooler tempered-steel orange that creates internal heat gradient with the primary." }, + { id: "seed-027", oklch: [0.568, 0.208, 27.1], + mood: "Sicilian blood orange at golden hour — citrus rind, terracotta, sun on stucco", + strategy: "Seed reads as vivid blood-orange — picked pure white surface so the citrus-red primary and a deep oxblood accent do all the emotional work, like a Loro Piana editorial spread." }, + { id: "seed-028", oklch: [0.591, 0.172, 24.0], + mood: "Sienna-fired ceramic studio at dusk — terracotta cooling on a wheel, hands still dusted with slip", + strategy: "Pure black stage lets the fired-clay primary glow like a kiln ember, with a deeper oxblood accent providing tonal weight rather than hue contrast — a monochrome warm-axis play." }, + { id: "seed-033", oklch: [0.544, 0.169, 31.3], + mood: "1960s Italian terracotta workshop — fired clay, espresso, late-afternoon Mediterranean dust", + strategy: "Pure black ground lets the seed's burnt-sienna primary glow like a lit kiln, with a deeper oxblood accent for restrained warmth tension — the brand carries the heat, the surface stays out." }, + { id: "seed-207", oklch: [0.564, 0.231, 29.1], + mood: "Aesop apothecary bottle — considered red oxide, the calm authority of a well-made object on a white shelf", + strategy: "Seed becomes the singular brand voice against pure white, with a deeper oxblood accent for hierarchy — the surface disappears so the red does all the speaking." }, + { id: "seed-035", oklch: [0.663, 0.153, 32.1], + mood: "Aesop apothecary bottle — clay-fired warmth, considered retail", + strategy: "Pure white surface lets the terracotta primary do the brand work, paired with a deep umber ink and a cooler clay accent for editorial tension." }, + { id: "seed-037", oklch: [0.590, 0.188, 35.8], + mood: "Aesop apothecary bottle — considered terracotta, herbalist restraint, the warmth comes from the glass not the room", + strategy: "Seed becomes a muted terracotta primary against pure white so the brand's warmth carries entirely through the color itself; accent shifts to a deeper umber for quiet hierarchy." }, + { id: "seed-038", oklch: [0.652, 0.229, 34.8], + mood: "blown-glass furnace at dusk — molten orange iron pulled from the kiln, a craftsman's signature heat", + strategy: "Pure black stage so the seed reads as live ember; primary holds the seed's heat, accent shifts to a brass-amber a hue-step away for a 1.7+ contrast pairing without leaving the fire." }, + { id: "seed-039", oklch: [0.653, 0.185, 33.5], + mood: "Aesop apothecary bottle — considered terracotta, quiet retail craft", + strategy: "Seed becomes a grounded clay primary against pure white, paired with a deeper umber accent so the warmth lives entirely in the brand marks, not the surface." }, + { id: "seed-167", oklch: [0.495, 0.134, 36.0], + mood: "Aesop apothecary shelf — burnished terracotta on clinical white, considered craft pharmacy", + strategy: "Treat the seed as a brand-carrying burnt-sienna against a pure paper-white surface so the warmth lives entirely in the primary, with a deep umber accent pulled along the same warm axis for typographic gravity." }, + { id: "seed-147", oklch: [0.500, 0.151, 40.0], + mood: "Aesop apothecary shelf — considered terracotta, pharmacy restraint, the brand color does the work against clinical white", + strategy: "Anchor the seed's burnt-sienna primary against a pure white surface so the rust speaks alone, with a deep umber ink and a cooler clay accent to give the palette product-brand discipline rather than environmental warmth." }, + { id: "seed-040", oklch: [0.660, 0.201, 40.0], + mood: "Aesop apothecary bottle — amber glass on a clean dispensary shelf, considered and clinical-warm", + strategy: "Seed becomes a burnt-amber primary against pure white so the bottle-glass color does the emotional work; accent shifts to a deep olive-bronze for the apothecary-label pairing." }, + { id: "seed-041", oklch: [0.673, 0.217, 38.6], + mood: "Aesop apothecary shelf — considered orange glass, clinical retail restraint", + strategy: "Pure white surface lets the burnt-orange primary do all the brand work, with a deep ink-brown for editorial gravity and a muted clay accent that reads as a sibling, not a contrast." }, + { id: "seed-042", oklch: [0.688, 0.133, 35.8], + mood: "Aesop apothecary shelf — terracotta glass, considered retail", + strategy: "Seed becomes a warm clay primary against pure white so the bottle-on-marble retail feel comes from the brand color alone; a deeper umber accent gives the label-print contrast." }, + { id: "seed-043", oklch: [0.781, 0.119, 38.1], + mood: "Aesop apothecary catalogue — considered terracotta, dermatological restraint, the warm color doing all the work against clinical white", + strategy: "Pure white surface lets the seed's warm clay tone read as the entire brand voice, paired with a deeper umber accent for hierarchy without competing with the primary's warmth." }, + { id: "seed-168", oklch: [0.400, 0.103, 50.0], + mood: "Aesop apothecary bottle — amber glass on a clinical white shelf, considered and pharmaceutical", + strategy: "Pure white surface lets the deep amber primary act like tinted glass against a clean shelf; accent is a muted clay that complements without competing, keeping the brand quiet and product-led." }, + { id: "seed-044", oklch: [0.568, 0.149, 45.9], + mood: "1970s desert highway at golden hour — sun-faded terracotta, denim dust, the warmth of a Polaroid pulled from a glovebox", + strategy: "Seed becomes a burnt-sienna primary against pure white so the terracotta does all the emotional work; a deep indigo accent acts as the denim shadow opposing the sun, creating the era's signature warm/cool tension without tinting the page." }, + { id: "seed-045", oklch: [0.607, 0.163, 47.7], + mood: "Aesop apothecary shelf — considered amber glass, clinical restraint, craft pharmacy", + strategy: "Pure white bg lets the burnt-amber primary do the apothecary work alone, paired with a deeper umber accent and graphite ink for editorial calm." }, + { id: "seed-046", oklch: [0.653, 0.175, 45.0], + mood: "Aesop apothecary shelf — considered amber glass, quiet luxury, restrained craft", + strategy: "Pure black backdrop lets the warm amber primary glow like backlit apothecary glass, with a deeper rust accent providing tonal depth in the same hue family — monochromatic warm against neutral void." }, + { id: "seed-047", oklch: [0.695, 0.205, 43.2], + mood: "Aesop apothecary label — sun-warmed amber glass on a clinical countertop, restrained botanical pharmacy", + strategy: "Pure white surface lets the burnt-amber primary and a deeper sienna accent do all the brand work, like an apothecary bottle photographed under daylight." }, + { id: "seed-051", oklch: [0.704, 0.189, 49.0], + mood: "blacksmith's forge at dusk — glowing iron, hammered copper, ember light against cooling steel", + strategy: "Pure near-black surface lets the seed's molten orange burn like heated metal; accent shifts to a deeper amber-red to suggest the cooling end of the same iron, while ink stays a clean off-white so type reads like chalk on slate." }, + { id: "seed-171", oklch: [0.550, 0.124, 60.0], + mood: "Klim Type Foundry specimen page — considered ochre on paper, design-school-honest", + strategy: "Seed becomes a muted ochre primary on pure white; accent is a deep ink-navy pulled across the wheel for editorial contrast without warmth-pooling in the bg" }, + { id: "seed-148", oklch: [0.650, 0.146, 60.0], + mood: "Klim-style editorial gold — late-afternoon paper light on a serif specimen sheet, considered and dry", + strategy: "Hold the seed's amber as primary on a pure white page so the gold reads as ink rather than atmosphere, and pair with a deep aubergine accent for typographic contrast." }, + { id: "seed-052", oklch: [0.700, 0.130, 60.0], + mood: "late-afternoon terracotta studio — sun-warmed clay, hands-on craft, the hour before dusk", + strategy: "Seed is a saturated amber-ochre with strong environmental association (ceramics, adobe, sunlit plaster), so I lean into Exception (a) with a faintly warm bone surface that reads as lime-washed wall, then deepen the seed slightly for primary and pair it with a fired-clay rust accent for hand-thrown warmth." }, + { id: "seed-053", oklch: [0.773, 0.157, 56.6], + mood: "late-summer apricot orchard at golden hour — sun-warmed fruit, considered Californian craft", + strategy: "Seed is a juicy mid-warm orange at daylight luminance — leaning optimistic/editorial, so pure white surface lets the apricot primary glow without muddying it; a deep wine accent provides the bite." }, + { id: "seed-149", oklch: [0.600, 0.124, 70.0], + mood: "1970s desert highway — late-afternoon amber light on chrome and asphalt", + strategy: "Anchor the amber seed as primary against pure black so the warm hue reads as headlight glow against night; a cooler dusk-mauve accent provides the complementary tension of horizon vs. sun." }, + { id: "seed-054", oklch: [0.740, 0.162, 68.1], + mood: "late-afternoon honey on terracotta — Mediterranean stucco at golden hour, sun-baked amber", + strategy: "Seed is a saturated honey-amber at high lightness; pairing it with pure black lets the warmth read as luminous gold against gravity, like lamplight in a dark room." }, + { id: "seed-055", oklch: [0.774, 0.174, 65.1], + mood: "late-summer honey hour — amber light slanting through a west-facing window, optimistic and golden", + strategy: "Anchor a saturated honey-amber primary on pure white so the warmth radiates from the brand itself, then pair with a deep teak accent for grounded contrast rather than tinting the canvas." }, + { id: "seed-056", oklch: [0.691, 0.146, 74.6], + mood: "Klim-style modern publishing house — late-afternoon paper warmth, considered editorial gold", + strategy: "Pure white surface so the amber seed becomes the brand voice; ink stays near-black neutral and accent shifts to a deep ink-blue to give the gold something structural to lean on." }, + { id: "seed-150", oklch: [0.750, 0.148, 80.0], + mood: "Klim Type Foundry specimen page — late-summer editorial gold, considered and grown-up", + strategy: "Pure white surface lets a single restrained ochre primary do all the brand work, paired with a deep ink-blue accent for typographic contrast in the Klim/Commercial Type tradition." }, + { id: "seed-058", oklch: [0.764, 0.120, 77.1], + mood: "Klim Type Foundry specimen page — late-afternoon ochre, considered editorial typography", + strategy: "Pure white surface lets the ochre primary do the brand work, paired with a deep ink-blue accent for editorial contrast — the type-foundry move where one warm hue carries the whole feeling against neutral paper." }, + { id: "seed-059", oklch: [0.784, 0.144, 79.8], + mood: "late afternoon in a Tuscan limonaia — sun-cured amber on whitewashed plaster", + strategy: "Pure white surface lets the saffron-amber primary and a deep olive accent carry the Mediterranean warmth, with split-complementary tension between gold and a quiet evergreen." }, + { id: "seed-061", oklch: [0.817, 0.161, 75.1], + mood: "late-afternoon honey on Tuscan limestone — golden hour, slow and luminous", + strategy: "Pure white surface lets the amber primary glow like sunlight on a wall, paired with a deep terracotta accent for warm tonal contrast within the same hue family." }, + { id: "seed-063", oklch: [0.842, 0.165, 91.3], + mood: "late-afternoon Tuscan sun on limestone — golden hour, considered, optimistic", + strategy: "Pure white surface lets the amber-gold primary radiate as the mood-carrier, with a deep aubergine accent providing the long shadow that golden light needs to feel three-dimensional." }, + { id: "seed-174", oklch: [0.350, 0.075, 110.0], + mood: "olive grove at late afternoon — sun-cured leaves, dust, and quiet Mediterranean weight", + strategy: "Pure white surface lets a deep, sun-cured olive primary do the emotional work, with a burnt-terracotta accent providing the warm-earth counterpoint olive groves are known for." }, + { id: "seed-117", oklch: [0.650, 0.100, 110.0], + mood: "Klim-style editorial sage — late-summer foundry catalogue, considered olive-yellow on paper", + strategy: "Seed sits at olive-chartreuse; treating it as a quiet typographic primary on pure paper, with a deeper bronze-olive accent for hierarchy — the color does the work, the page disappears." }, + { id: "seed-118", oklch: [0.750, 0.090, 110.0], + mood: "Klim Type Foundry specimen page — late-summer olive light on a working specimen, the honesty of a type designer showing their work", + strategy: "Pure white bg lets a desaturated olive-yellow primary do the editorial work, with a deeper olive-bronze accent providing typographic emphasis the way a specimen uses one heavy weight against the body roman." }, + { id: "seed-065", oklch: [0.797, 0.166, 113.1], + mood: "late-summer olive grove at noon — sun-bleached leaves, dry stone, Mediterranean glare", + strategy: "Hold the seed as a luminous chartreuse-olive primary against pure white so the color reads as sunlit foliage, pairing it with a deep umber accent for the dry-stone contrast." }, + { id: "seed-176", oklch: [0.300, 0.071, 120.0], + mood: "moss-darkened apothecary jar — herbal, shadowed, mid-19th-century botanical study", + strategy: "Seed is a deep desaturated olive-green that reads as preserved botanical pigment; I anchor it on pure white so the dim moss-green primary feels like ink on a herbarium page, with a warm ochre accent supplying the aged-paper counterpoint." }, + { id: "seed-155", oklch: [0.550, 0.142, 130.0], + mood: "moss-bed forest floor at noon — chlorophyll, lichen, sunlit fern", + strategy: "Seed is a confident mid-olive green with strong chroma; mood is daylight botanical, so I let the brand greens do the work on a pure paper-white bg and pair with a warm umber accent for fern-against-bark contrast." }, + { id: "seed-119", oklch: [0.600, 0.154, 130.0], + mood: "moss garden at Saihō-ji — damp stone, filtered green light through old cedar", + strategy: "Pure near-black bg lets the seed's mossy green glow like wet lichen under low light; accent shifts to a pale ochre-gold like sun catching through canopy." }, + { id: "seed-179", oklch: [0.300, 0.096, 140.0], + mood: "moss on wet stone — forest floor at dusk, deep botanical hush", + strategy: "Kept the seed's deep moss green as primary against a near-black surface so the green reads as living shadow, with a pale lichen accent providing the single point of light." }, + { id: "seed-180", oklch: [0.350, 0.110, 140.0], + mood: "moss-darkened apothecary — herbal tinctures in amber glass, pressed botanicals, the deep green of a conservatory at dusk", + strategy: "Near-black bg with a whisper of green undertone lets the seed's deep moss read as luminous foliage; a warm parchment accent provides the apothecary-label counterpoint without breaking the herbal register." }, + { id: "seed-120", oklch: [0.650, 0.100, 140.0], + mood: "moss on weathered stone — quiet botanical garden conservatory at midday", + strategy: "Pure white bg lets the muted sage-green primary read as a considered botanical mark, with a deeper terracotta accent providing earthen counterpoint without breaking the gallery-like restraint." }, + { id: "seed-121", oklch: [0.750, 0.090, 140.0], + mood: "moss garden at Saihō-ji — diffuse green light filtered through wet stone and lichen", + strategy: "Pure near-black bg lets the muted sage-green primary glow like lichen under low light; a warm pale-bone accent acts as the single ray of sun cutting through canopy." }, + { id: "seed-182", oklch: [0.400, 0.106, 150.0], + mood: "moss garden at Saiho-ji — deep cultivated green under wet stone shadow, contemplative and damp", + strategy: "Near-black bg with the faintest cool-green undertone evokes shaded stone; primary holds the seed's moss tone while accent shifts to a lichen-yellow for organic counterpoint without breaking the hush." }, + { id: "seed-157", oklch: [0.550, 0.145, 150.0], + mood: "moss garden at Saiho-ji — damp stone, filtered green light through cedar canopy", + strategy: "Near-black bg with a faint green undertone evokes deep forest shadow; primary holds the seed's verdant register while accent shifts to a pale lichen-cream to mimic light catching moss." }, + { id: "seed-122", oklch: [0.600, 0.158, 150.0], + mood: "forest floor at first light — moss, lichen, and clean morning air", + strategy: "Seed reads as a living, daylight green; surface stays pure white so the green carries the freshness, with a cool teal accent pulling it toward dew rather than earth." }, + { id: "seed-195", oklch: [0.650, 0.150, 145.0], + mood: "Considered horticulture brand — botanical research lab, the green of a healthy stem photographed in clean daylight", + strategy: "Pure white surface lets the seed's vegetal green carry the entire brand voice, paired with a deep forest ink and a warm clay accent for editorial contrast." }, + { id: "seed-183", oklch: [0.350, 0.077, 160.0], + mood: "moss-stained apothecary — deep forest glass, herbal tinctures shelved in low candlelight", + strategy: "Anchored the seed as primary and built a near-black dark surface with whisper-tinted green to evoke aged apothecary glass, letting the green glow rather than shout." }, + { id: "seed-184", oklch: [0.400, 0.087, 160.0], + mood: "deep forest apothecary — moss, bottle glass, and herbal tincture under afternoon light", + strategy: "Seed becomes a botanical-bottle-green primary on pure white, paired with a warm clove-amber accent to evoke herbal pharmacy contrast without tinting the surface." }, + { id: "seed-158", oklch: [0.550, 0.119, 160.0], + mood: "moss on wet stone — forest floor after rain, mineral and quiet", + strategy: "Pure white surface lets the deep mossy green carry the entire mood; accent shifts to a damp slate-teal to sit beside primary like lichen on stone without competing." }, + { id: "seed-159", oklch: [0.600, 0.130, 160.0], + mood: "moss-covered forest apothecary — herbal tinctures in amber glass, eucalyptus shadow", + strategy: "Anchored the green seed in a near-black backdrop so it reads like botanical glassware lit from within, with a warm amber accent pulled across the wheel to evoke tincture bottles against dark wood." }, + { id: "seed-185", oklch: [0.450, 0.086, 170.0], + mood: "weathered copper patina on a Pacific Northwest greenhouse — oxidized teal, glass light, botanical hush", + strategy: "Seed sits as a deep oxidized-teal primary against pure white so the patina reads as pigment, not atmosphere; a rust-copper accent completes the verdigris/oxidation story across the warm-cool axis." }, + { id: "seed-124", oklch: [0.750, 0.080, 170.0], + mood: "sea-glass on a foggy Pacific shoreline — weathered, mineral, quietly oxidized", + strategy: "Seed is a soft desaturated teal-green; pairing it on pure white lets the mineral primary read as patinated copper-glass, with a deeper kelp-toned primary and a rusted coral accent to spark the muted teal against its complement." }, + { id: "seed-160", oklch: [0.550, 0.095, 180.0], + mood: "weathered copper patina on a museum bronze — oxidized teal, conservatorial quiet", + strategy: "Pure near-black gallery surround lets the patina-teal primary glow like a lit artifact, with a warm verdigris-adjacent accent providing the oxidation contrast against the cool seed." }, + { id: "seed-161", oklch: [0.720, 0.100, 188.0], + mood: "climate-tech dashboard — calm verdigris on plain paper, the quiet confidence of an instrument that just works", + strategy: "Seed teal carries the entire mood as a single considered brand color on pure white, with a desaturated copper accent providing warm signal against the cool primary without competing for attention." }, + { id: "seed-186", oklch: [0.450, 0.074, 200.0], + mood: "deep hydrothermal vent — mineral teal under pressure, the cold blue-green of oxidized copper in submerged light", + strategy: "Near-black surface lets the mineral teal glow as if lit from within; accent shifts toward verdigris-copper to suggest patina on submerged metal, while ink stays cool-neutral to keep the register austere rather than aquatic-cute." }, + { id: "seed-125", oklch: [0.650, 0.100, 200.0], + mood: "climate-tech dashboard — calm operational teal, the color of clean water data and atmospheric sensors", + strategy: "Pure white surface lets a single muted-teal primary do all the brand work, with a deeper marine accent providing hierarchy without competing chroma." }, + { id: "seed-126", oklch: [0.750, 0.080, 200.0], + mood: "climate-tech product brand — quiet competence, dashboards for hard infrastructure problems", + strategy: "Hold the seed's muted teal as primary, pair with a sharper cyan-leaning accent for interactive lift, and let a pure white surface do the disappearing act so the brand reads as a tool, not an atmosphere." }, + { id: "seed-162", oklch: [0.550, 0.091, 210.0], + mood: "weathered nautical instrument — patinated brass on oxidized steel, the cool blue-grey of a ship's chronometer at dawn", + strategy: "Pure white surface lets the muted teal-steel primary read as a precise instrument mark, with a warm brass accent providing the single point of patina against clinical white." }, + { id: "seed-163", oklch: [0.450, 0.086, 230.0], + mood: "deep harbor at dusk — weathered nautical instruments, brass dials on oxidized steel", + strategy: "Near-black background with subtle cool tint evokes the marine dusk; primary holds the seed's teal-blue while a warm brass accent creates the instrument-on-steel tension." }, + { id: "seed-164", oklch: [0.550, 0.105, 230.0], + mood: "deep harbor at dawn — cold steel water, fog-muted light, the quiet before the boats leave", + strategy: "Pure near-black bg lets the seed's cold marine blue read as a luminous beacon, while a pale frost-cyan accent evokes diffused dawn light cutting through fog." }, + { id: "seed-127", oklch: [0.650, 0.100, 230.0], + mood: "climate-tech dashboard — atmospheric sensor blue, calm operational clarity", + strategy: "Anchor the seed as a confident mid-blue primary on pure white so the brand color carries all the atmospheric feeling, with a deep navy accent for hierarchy and a soft slate muted for body text." }, + { id: "seed-128", oklch: [0.750, 0.080, 230.0], + mood: "climate-tech dashboard — calm atmospheric data, considered sky-blue", + strategy: "Pure white surface lets the muted sky-blue primary carry the meteorological calm, with a deep-navy accent providing readable weight against the soft primary." }, + { id: "seed-187", oklch: [0.350, 0.078, 240.0], + mood: "deep harbor at blue hour — wet stone, cold steel, the quiet before night fully lands", + strategy: "Near-black architectural bg with a hint of marine chroma lets the seed read as ambient atmosphere rather than UI chrome; a cooler steel accent sits opposite the warmer-shifted primary for navigational clarity." }, + { id: "seed-077", oklch: [0.578, 0.130, 241.7], + mood: "pre-dawn signal tower — cold blue solitude, instruments glowing against the dark", + strategy: "Pure near-black bg lets the seed's cold tower-light blue glow as the sole emotional source, with a frost-cyan accent acting as a secondary indicator light." }, + { id: "seed-188", oklch: [0.400, 0.110, 250.0], + mood: "Linear's considered indigo — the calm authority of a well-built developer tool, blueprint ink on a clean page", + strategy: "Held the seed as a deep indigo primary against pure white so the brand color carries all the gravity; accent shifts to a cooler, brighter cyan-blue to create a crisp hierarchy pair without warming the surface." }, + { id: "seed-165", oklch: [0.450, 0.123, 250.0], + mood: "blueprint room at dusk — drafting table, graphite, civic-engineering blue", + strategy: "Seed is a mid-deep architectural blue with real chroma and no environmental cue, so I stay out of the way with a pure white surface and let the primary do all the talking, pairing it with a burnt-ochre accent for drafting-pencil contrast." }, + { id: "seed-079", oklch: [0.478, 0.136, 251.8], + mood: "twilight cartography — the blue of deep dusk over open water, precise and navigational", + strategy: "Pure white surface lets the seed's oceanic blue act as a single navigational anchor, with a warm amber accent struck across it like a lighthouse beam at dusk." }, + { id: "seed-080", oklch: [0.541, 0.122, 248.2], + mood: "Linear-style considered tool blue — the calm, exact register of a modern engineering app where every pixel is intentional", + strategy: "Pure white surface lets the considered indigo-blue primary carry the entire brand; a deeper navy accent provides hierarchy without warmth, keeping the palette in a single cool family for that focused-software feel" }, + { id: "seed-166", oklch: [0.550, 0.149, 250.0], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and quietly intense", + strategy: "Near-black bg with the faintest cool tint reads like a darkened cockpit; the seed becomes a luminous instrument-blue primary, paired with a warm amber accent that mimics avionics readouts for unmistakable signal contrast." }, + { id: "seed-081", oklch: [0.650, 0.160, 250.0], + mood: "deep-sea research vessel at dawn — instrument glow against cold steel light", + strategy: "Pure near-white bg keeps the palette technical and instrument-like; the seed blue holds as primary while a desaturated steel-cyan accent reads like signal readouts on glass." }, + { id: "seed-082", oklch: [0.742, 0.140, 247.4], + mood: "high-altitude flight deck at dawn — cold cabin instruments glowing against a sky still holding night", + strategy: "Near-black cockpit ground with a faint blue cast lets the seed read as an illuminated instrument; primary holds the seed, accent shifts to cyan for signal/indicator contrast." }, + { id: "seed-210", oklch: [0.360, 0.140, 260.0], + mood: "Linear-style considered tool indigo — late-night focused work, the deep blue of a code editor at 2am where everything else falls away", + strategy: "Pure black bg lets the indigo primary carry all the cognitive-focus weight, with a slightly brighter periwinkle accent for interactive lift — the surface disappears so the tool feels weightless." }, + { id: "seed-189", oklch: [0.400, 0.130, 260.0], + mood: "pre-dawn observatory — cold instrument blue, star-chart precision", + strategy: "Seed becomes the primary on pure black so the deep instrument-blue glows like a calibration light, with a faint cyan accent reading as starlight against the void." }, + { id: "seed-211", oklch: [0.420, 0.161, 260.0], + mood: "Linear's considered indigo — the tool-for-thought blue of focused product work, calm authority without coldness", + strategy: "Hold the seed as a deep indigo primary against pure white, then pair with a slightly warmer, lighter periwinkle accent to create gentle hue separation without breaking the disciplined tool-brand register." }, + { id: "seed-129", oklch: [0.450, 0.150, 260.0], + mood: "pre-dawn observatory — deep cobalt sky just before astronomical twilight, instruments cool to the touch", + strategy: "Near-black surface lets the cobalt seed read as luminous starlight; a single warm amber accent acts as the calibration lamp against the cold blue field." }, + { id: "seed-084", oklch: [0.476, 0.207, 261.2], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and awake", + strategy: "Default B black bg lets the cobalt primary read as a luminous instrument signal, with a cyan accent striking the analogous 'cockpit display' relationship." }, + { id: "seed-085", oklch: [0.681, 0.132, 258.4], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky", + strategy: "Anchored the seed as a luminous primary against a near-black architectural ground, with a warm amber accent acting as the single instrument light cutting through cold blue." }, + { id: "seed-086", oklch: [0.767, 0.106, 255.9], + mood: "Scandinavian winter morning — quiet light through frost, pale sky over snow", + strategy: "Anchored a pure white editorial stage so the seed's cool sky-blue reads as crisp polar light, with a deeper navy primary providing the only saturated weight — like a single dark pine against snow." }, + { id: "seed-083", oklch: [0.340, 0.159, 262.4], + mood: "deep cobalt twilight — the moment after sunset when the sky goes electric blue and city windows start to glow", + strategy: "Pure black stage lets the cobalt seed act as a luminous neon-window glow, with a warm amber accent across the wheel for the lit-window contrast." }, + { id: "seed-212", oklch: [0.360, 0.219, 270.0], + mood: "Linear-grade tooling indigo — considered software for people who care about craft", + strategy: "Anchored the deep indigo seed as primary on a pure white surface so the brand color carries all the weight, with a slightly cooler violet-blue accent for hierarchy without competing chroma." }, + { id: "seed-130", oklch: [0.400, 0.150, 270.0], + mood: "Linear-grade indigo — considered productivity tool, ink on paper, no theatrics", + strategy: "Pure white surface lets a deep cool indigo carry all the brand weight, paired with a slightly warmer violet-blue accent for hierarchy without acid." }, + { id: "seed-213", oklch: [0.411, 0.241, 267.9], + mood: "Linear-style indigo — considered tool surface, the kind of blue-violet that sits behind a developer's keyboard at 11pm without shouting", + strategy: "Pure black canvas lets a saturated indigo primary do all the brand work, with a cooler cyan-violet accent providing UI signal without competing." }, + { id: "seed-131", oklch: [0.450, 0.180, 270.0], + mood: "monastic indigo dusk — vespers light through stained glass, contemplative and severe", + strategy: "Seed becomes a deep indigo primary against pure near-black so the violet reads as luminous stained-glass against architectural shadow, with a cooler iris accent for tonal lift." }, + { id: "seed-088", oklch: [0.476, 0.158, 268.5], + mood: "pre-dawn astronomer's notebook — deep indigo sky just before the stars fade, ink and graphite", + strategy: "Near-black bg with the faintest cool tint to evoke night sky without theatrics; primary holds the seed's indigo, accent shifts to a paler periwinkle for stellar contrast, keeping the palette monochromatic-cool and observational." }, + { id: "seed-196", oklch: [0.530, 0.130, 268.0], + mood: "Linear-style considered tool indigo — the deep-focus blue-violet of a thoughtfully built productivity surface, the color of a well-typeset keyboard shortcut", + strategy: "Pure white bg lets the indigo seed do all the brand work as primary, with a slightly darker, more saturated violet-shifted accent for hierarchy and interactive states — the surface disappears so the brand color reads as the entire identity." }, + { id: "seed-132", oklch: [0.700, 0.120, 270.0], + mood: "Linear-style considered tool indigo — the quiet violet of a focused product workspace, late-afternoon thinking", + strategy: "Pure white surface lets a muted indigo-violet primary and a slightly cooler accent do all the brand work, keeping the register calm and software-like rather than theatrical." }, + { id: "seed-090", oklch: [0.445, 0.206, 279.1], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, not a nightclub", + strategy: "Anchor the seed as a confident product primary on pure white, with a cooler indigo-shift accent that reads as a sibling tool color, so the brand violet does all the emotional work." }, + { id: "seed-133", oklch: [0.500, 0.160, 280.0], + mood: "Linear-adjacent indigo — considered productivity tool, the violet of a thinking workspace", + strategy: "Seed becomes a measured indigo primary on pure white; accent shifts to a cooler blue-violet to create hierarchy without nightclub saturation, letting the brand color do all the emotional work." }, + { id: "seed-094", oklch: [0.533, 0.125, 294.3], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, calm authority for a creative workspace", + strategy: "Pure white canvas lets the indigo-violet primary carry the entire brand voice; accent shifts hue slightly toward blue for a cool, tool-like duotone rather than warm decorative pairing." }, + { id: "seed-137", oklch: [0.700, 0.120, 290.0], + mood: "Linear-adjacent indigo — the considered tool, late-evening focus mode, software made for people who care about craft", + strategy: "Pure black surface lets a single restrained indigo-violet carry the brand, with a cooler periwinkle accent providing UI hierarchy without competing — Vercel/Linear dark-mode discipline." }, + { id: "seed-100", oklch: [0.450, 0.150, 330.0], + mood: "velvet boudoir at last call — bruised orchid and lipstick traces under low lamplight", + strategy: "Pure near-black surface lets a deep magenta-rose primary smolder while a warm peach accent acts like skin-lit lamplight — drama lives in the brand pair, not the room." }, + { id: "seed-103", oklch: [0.650, 0.160, 330.0], + mood: "1980s Memphis boudoir — powder-pink neon humming against lacquered black, lipstick and lacquer", + strategy: "Near-black gallery surface lets the magenta-pink seed read as lit neon; accent shifts to warm coral to create cinematic dichromatic tension without competing chroma." }, + { id: "seed-228", oklch: [0.360, 0.147, 340.0], + mood: "Figma-era creative tool plum — considered productivity software for designers, the inky violet of a serif wordmark on a marketing site", + strategy: "Held the seed as a deep plum primary against pure white so the brand color does the emotional work; paired with a muted rose accent for warmth without breaking the productivity-tool restraint." }, + { id: "seed-107", oklch: [0.500, 0.200, 340.0], + mood: "Figma plum — creative-tool confidence, considered magenta for a modern design product", + strategy: "Pure white surface lets a saturated magenta-plum primary carry all the brand voice, paired with a cooler violet-leaning accent for hierarchy without competing." }, + { id: "seed-198", oklch: [0.600, 0.210, 340.0], + mood: "Figma-era creative tool plum — confident, considered, made for makers", + strategy: "Anchor a saturated plum primary against pure white so the brand color does all the emotional work, with a deeper magenta-rose accent for hierarchy." }, + { id: "seed-112", oklch: [0.754, 0.193, 343.4], + mood: "Figma-era creative tool — confident pink primary doing the brand work on a clean canvas, the way Linear uses indigo or Stripe uses violet", + strategy: "Anchor the seed pink as a saturated brand primary on pure white so the color carries all the personality; pair with a cooler plum accent to give the pink something to push against without competing." }, + { id: "seed-229", oklch: [0.420, 0.163, 350.0], + mood: "considered fintech rose — the deep magenta of a modern product brand (think Stripe-adjacent, but rotated toward berry), confident and current", + strategy: "pure white surface lets a single deep berry-rose primary do all the brand work, paired with a cooler indigo accent for the contrast move you see in modern product marketing" }, + { id: "seed-113", oklch: [0.470, 0.173, 354.8], + mood: "1960s velvet rope nightclub — crushed magenta, low light, cigarette smoke catching a spotlight", + strategy: "Pure black stage so the seed's smoky magenta reads as a single hot spotlight, paired with a cooler violet accent for the second light cue." }, + { id: "seed-114", oklch: [0.570, 0.158, 353.3], + mood: "fin-de-siècle Parisian rose — velvet curtain, theatre program, lipstick blotted on linen", + strategy: "Drop bg to true black so the dusty-rose primary reads as stage-lit silk; accent shifts to a warmer coral-mauve at higher lightness to create gentle hue rotation without breaking the romance." }, + { id: "seed-199", oklch: [0.650, 0.180, 350.0], + mood: "modern fintech rose — the considered pink of a Series B brand mark, confident and current without nostalgia", + strategy: "Pure white surface lets a saturated rose primary do the brand work, paired with a deep plum accent for hierarchy — the Stripe move applied to a pink hue." }, + { id: "seed-115", oklch: [0.636, 0.218, 355.3], + mood: "backstage at a cabaret — velvet rope, lipstick mark on a champagne glass", + strategy: "Seed reads as a saturated stage-light magenta-red; I push it into pure black so the primary glows like a neon sign and the accent (a cold pearl-pink) acts as the spotlight rim — the room is dark, the color does the singing." }, + { id: "seed-230", oklch: [0.650, 0.249, 354.5], + mood: "Modern fintech rose — the considered pink of a contemporary payments brand: confident, alive, and clear-headed", + strategy: "Pure white bg lets a saturated rose-magenta primary carry all the brand energy, paired with a cooler indigo accent for trustworthy contrast — the Stripe move applied to a pink hue." }, + { id: "seed-231", oklch: [0.682, 0.241, 353.2], + mood: "Figma-era creative tool — a confident pink-magenta product brand, the kind a modern design platform uses to feel alive without shouting", + strategy: "Default A pure white bg lets the saturated pink-magenta primary do all the brand work, with a near-complementary cool teal accent for tool-like clarity and a neutral ink for editorial calm" }, + { id: "seed-116", oklch: [0.734, 0.183, 356.8], + mood: "modern beauty brand DTC — Glossier-adjacent pink, confident and current without being saccharine", + 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." }, +]; + +function parseArgs(argv) { + const args = { id: null, from: null }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === '--id' && argv[i + 1]) { args.id = argv[++i]; } + else if (a === '--from' && argv[i + 1]) { args.from = argv[++i]; } + } + return args; +} + +// Hash a key into a stable float in [0, 1) for deterministic weighted picks. +function hashUnit(key) { + const h = crypto.createHash('sha256').update(key).digest(); + return h.readUInt32BE(0) / 0x100000000; +} + +// The curated library is hue-skewed (more reds/oranges than teals/magentas) +// because that's where the source material + taste landed. Left uniform, a +// random pick would land on red ~1/3 of the time. Inverse-frequency weighting +// gives each seed a weight of 1/(count in its 30° hue bucket), so each hue +// ZONE is roughly equally likely to be chosen regardless of how many seeds it +// holds — fair rainbow exposure across runs without pruning the library. +function buildWeights(seeds) { + const bucketCount = {}; + const bucketOf = (s) => Math.floor(((s.oklch[2] % 360) + 360) % 360 / 30); + for (const s of seeds) { const b = bucketOf(s); bucketCount[b] = (bucketCount[b] || 0) + 1; } + const weights = seeds.map((s) => 1 / bucketCount[bucketOf(s)]); + const total = weights.reduce((a, b) => a + b, 0); + return { weights, total }; +} + +function weightedPick(seeds, unit) { + const { weights, total } = buildWeights(seeds); + let target = unit * total; + for (let i = 0; i < seeds.length; i++) { + target -= weights[i]; + if (target < 0) return seeds[i]; + } + return seeds[seeds.length - 1]; +} + +function pickSeed(seeds, { id, from }) { + if (id) { + const found = seeds.find(s => s.id === id); + if (!found) { console.error(`no seed with id "${id}"`); process.exit(2); } + return found; + } + const envFrom = process.env.IMPECCABLE_PALETTE_SEED; + const key = from || envFrom; + const unit = key ? hashUnit(key) : Math.random(); + return weightedPick(seeds, unit); +} + +function fmtOklch([L, C, H]) { + return `oklch(${L.toFixed(3)} ${C.toFixed(3)} ${H.toFixed(1)})`; +} + +function hueWord(H) { + if (H < 15 || H >= 345) return 'pure red'; + if (H < 35) return 'warm red / crimson'; + if (H < 55) return 'warm coral / burnt orange'; + if (H < 80) return 'orange / honey'; + if (H < 105) return 'warm amber / honey-gold'; + if (H < 135) return 'yellow-green / olive'; + if (H < 170) return 'green'; + if (H < 200) return 'teal'; + if (H < 230) return 'sky blue'; + if (H < 265) return 'cobalt / indigo'; + if (H < 295) return 'violet / purple'; + if (H < 330) return 'magenta / pink'; + return 'deep pink / rose'; +} + +// --------------------------------------------------------------- + +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}` : ''; + +// --------------------------------------------------------------- +// Fat tool-exit response — what the model sees on stdout. +// --------------------------------------------------------------- + +process.stdout.write(`BRAND SEED · ${seed.id} + +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. + +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. + +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é. + + 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 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. + + 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. + +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): + + - 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. + +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. + +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. +`); diff --git a/.opencode/skills/impeccable/scripts/pin.mjs b/.opencode/skills/impeccable/scripts/pin.mjs index ba02783fb..320d98ce1 100644 --- a/.opencode/skills/impeccable/scripts/pin.mjs +++ b/.opencode/skills/impeccable/scripts/pin.mjs @@ -27,7 +27,7 @@ const HARNESS_DIRS = [ // Valid sub-command names const VALID_COMMANDS = [ - 'craft', 'teach', 'extract', 'document', 'shape', + 'craft', 'init', 'extract', 'document', 'shape', 'critique', 'audit', 'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live', 'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive', diff --git a/.pi/skills/impeccable/SKILL.md b/.pi/skills/impeccable/SKILL.md index 3a074d6df..b41e0c61a 100644 --- a/.pi/skills/impeccable/SKILL.md +++ b/.pi/skills/impeccable/SKILL.md @@ -1,8 +1,8 @@ --- name: impeccable description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks. -version: 3.1.1 -license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution. +version: 3.5.0 +license: Apache 2.0 allowed-tools: - Bash(npx impeccable *) --- @@ -11,85 +11,80 @@ Designs and iterates production-grade frontend interfaces. Real working code, co ## Setup -Before any design work or file edits: +You MUST do these steps before proceeding: -1. Load context (PRODUCT.md / DESIGN.md) via the loader script. -2. Identify the register and load the matching register reference (brand.md or product.md). -3. **If the user invoked a sub-command (e.g. `craft`, `shape`, `audit`), load its reference file too.** This is non-negotiable: `craft` without `craft.md` loaded means you'll skip the shape-and-confirm step the user expects. +1. Run `node .pi/skills/impeccable/scripts/context.mjs` once per session. If you've already seen its output in this conversation, do not re-run it. The script either prints the project's PRODUCT.md (and DESIGN.md when present) as a markdown block, or tells you it's missing. Follow whatever it prints. **If it reports `NO_PRODUCT_MD`, stop and follow `reference/init.md` before doing anything else.** If the output ends with an `UPDATE_AVAILABLE` directive, follow it (ask the user once about updating, then continue). It never blocks the current task. +2. If the user invoked a sub-command (`craft`, `shape`, `audit`, `polish`, ...), you MUST read `reference/.md` next. Non-optional. The reference defines the command's flow; without it you will skip steps the user expects. +3. Familiarize yourself with any existing design system, conventions, and components in the code. Read at least one project file (CSS / tokens / theme / a representative component or page). **Required even when you've loaded a sub-command reference in step 2.** Don't reinvent the wheel; use what's there when it works, branch out when the UX wins. +4. Read the matching register reference. **This is non-optional; skipping it produces generic output.** If the project is marketing, a landing page, a campaign, long-form content, or a portfolio (design IS the product), read `reference/brand.md`. If it is app UI, admin, a dashboard, or a tool (design SERVES the product), read `reference/product.md`. Pick by first match: (1) task cue ("landing page" vs "dashboard"); (2) surface in focus (the page, file, or route being worked on); (3) `register` field in PRODUCT.md. +5. **If the project is brand-new (no existing CSS tokens / theme / committed brand colors found in step 3)**, run `node .pi/skills/impeccable/scripts/palette.mjs` to receive a brand seed color and composition guidance. This is the anchor for your primary brand color. Compose the rest of the palette (bg, surface, ink, accent, muted) around it per the script's instructions. Use OKLCH throughout. **Skip this step only if step 3 found committed brand colors in existing tokens; in that case identity-preservation wins.** -Skipping these produces generic output that ignores the project. +## Design guidance -### 1. Context gathering +Produce ready-to-ship, production-grade code, not prototypes or starting points. Take no shortcuts unless the user asks for them (when in doubt, ask). Don't stop until arriving at a complete implementation (beautiful, responsive, fast, precise, bug-free, on brand). You take attention to detail seriously: every page, section or component crafted is battle tested using the tools available to you (browser screenshotting, computer use, etc). the model is capable of extraordinary work. Don't hold back. -Two files, case-insensitive. The loader looks at the project root by default and falls back to `.agents/context/` and `docs/` if the root is clean. Override with `IMPECCABLE_CONTEXT_DIR=path/to/dir` (absolute or relative to cwd). +### General rules -- **PRODUCT.md**: required. Users, brand, tone, anti-references, strategic principles. -- **DESIGN.md**: optional, strongly recommended. Colors, typography, elevation, components. +#### Color -Load both in one call: +- **Verify contrast.** Body text must hit ≥4.5:1 against its background; large text (≥18px or bold ≥14px) needs ≥3:1. Placeholder text needs the same 4.5:1, not the muted-gray default. The most common failure: muted gray body text on a tinted near-white. If the contrast is even close, bump the body color toward the ink end of the ramp; light gray "for elegance" is the single biggest reason AI designs feel hard to read. +- Gray text on a colored background looks washed out. Use a darker shade of the background's own hue, or a transparency of the text color. -```bash -node .pi/skills/impeccable/scripts/load-context.mjs -``` +#### Typography -Consume the full JSON output. Never pipe through `head`, `tail`, `grep`, or `jq`. The output's `contextDir` field tells you where the files were resolved from. +- Cap body line length at 65–75ch. +- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales. +- Cap font-family count at 3 (display + body + optional mono). More than 3 reads as indecision, not richness. One well-tuned family with weight contrast usually beats three competing typefaces. +- Don't pair fonts that are similar but not identical (two geometric sans-serifs, two humanist sans-serifs). Pair on a contrast axis (serif + sans, geometric + humanist) or use one family in multiple weights. +- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. +- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. +- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". +- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. -If the output is already in this session's conversation history, don't re-run. Exceptions requiring a fresh load: you just ran `/impeccable teach` or `/impeccable document` (they rewrite the files), or the user manually edited one. +#### Layout -`/impeccable live` already warms context via `live.mjs`. If you've run `live.mjs`, don't also run `load-context.mjs` this session. +- Vary spacing for rhythm. +- Cards are the lazy answer. Use them only when they're truly the best affordance. Nested cards are always wrong. +- Flexbox for 1D, Grid for 2D. Don't default to Grid when `flex-wrap` would be simpler. +- For responsive grids without breakpoints: `repeat(auto-fit, minmax(280px, 1fr))`. +- Build a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip). Never arbitrary values like 999 or 9999. -If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `/impeccable teach`, then resume the user's original task with the fresh context. If the original task was `/impeccable craft`, resume into `/impeccable shape` before any implementation work. +#### Motion +- Motion should be intentional, and not be an afterthought. consider it as part of the build. +- Don't animate CSS layout properties unless truly needed. +- Ease out with exponential curves (ease-out-quart / quint / expo). No bounce, no elastic. +- Use libraries for more advanced motion needs (e.g. motion, gsap, anime.js, lenis etc) +- Reduced motion is not optional. Every animation needs a `@media (prefers-reduced-motion: reduce)` alternative: typically a crossfade or instant transition. +- Staggering the items within one list is legitimate. The tell is the uniform reflex (one identical entrance applied to every section), not motion itself; each reveal should fit what it reveals. Suppressing the reflex is never a reason to ship a page with no motion at all. +- Reveal animations must enhance an already-visible default. Don't gate content visibility on a class-triggered transition; transitions pause on hidden tabs and headless renderers, so the reveal never fires and the section ships blank. +- Premium motion materials are not just transform/opacity. Blur, backdrop-filter, clip-path, mask, and shadow/glow are part of the palette when they materially improve the effect and stay smooth. -If DESIGN.md is missing: nudge once per session (*"Run `/impeccable document` for more on-brand output"*), then proceed. +#### Interaction -### 2. Register +- Dropdowns rendered with `position: absolute` inside an `overflow: hidden` or `overflow: auto` container will be clipped. Use the native `` / popover API, `position: fixed`, or a portal to escape the stacking context. -Every design task is **brand** (marketing, landing, campaign, long-form content, portfolio: design IS the product) or **product** (app UI, admin, dashboard, tool: design SERVES the product). +### Copy -Identify before designing. Priority: (1) cue in the task itself ("landing page" vs "dashboard"); (2) the surface in focus (the page, file, or route being worked on); (3) `register` field in PRODUCT.md. First match wins. +- Every word earns its place. No restated headings, no intros that repeat the title. +- **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`. +- **No aphoristic-cadence body copy as a default voice.** Don't fall into the rhythm of "serious statement, then punchy short negation" as the page's recurring voice. If three or more section copy blocks on the page land on a short rebuttal-shaped sentence, rewrite. Specific, not aphoristic. +- **No marketing buzzwords.** The streamline / empower / supercharge / leverage / unleash / transform / seamless / world-class / enterprise-grade / next-generation / cutting-edge / game-changer / mission-critical family of phrases. Pick a specific noun and a verb that describes what the product literally does. +- Button labels: verb + object. "Save changes" beats "OK"; "Delete project" beats "Yes". The label should say what will happen. +- Link text needs standalone meaning. "View pricing plans" beats "Click here"; screen readers announce links out of context. -If PRODUCT.md lacks the `register` field (legacy), infer it once from its "Users" and "Product Purpose" sections, then cache the inferred value for the session. Suggest the user run `/impeccable teach` to add the field explicitly. +### New projects only (when no prior work exists) -Load the matching reference: [reference/brand.md](reference/brand.md) or [reference/product.md](reference/product.md). The shared design laws below apply to both. +#### Color & Theme -## Shared design laws - -Apply to every design, both registers. Match implementation complexity to the aesthetic vision: maximalism needs elaborate code, minimalism needs precision. Interpret creatively. Vary across projects; never converge on the same choices. the model is capable of extraordinary work. Don't hold back. - -### Color - -- Use OKLCH. Reduce chroma as lightness approaches 0 or 100; high chroma at extremes looks garish. -- Never use `#000` or `#fff`. Tint every neutral toward the brand hue (chroma 0.005–0.01 is enough). +- Use OKLCH. +- **The cream / sand / beige body bg is the saturated AI default of 2026.** The whole warm-neutral band (OKLCH L 0.84-0.97, C < 0.06, hue 40-100) reads as cream/sand/paper/parchment regardless of what you call it. Token names like `--paper`, `--cream`, `--sand`, `--bone`, `--flour`, `--linen`, `--parchment`, `--wheat`, `--biscuit`, `--ivory` are tells in themselves. If the brief is "warm, traditional, family-coastal-Italian" or "magazine-warm" or "editorial-restraint", DO NOT translate that into a near-white warm-tinted bg; that's the AI move. Pick: (a) a saturated brand color as the body (terracotta, oxblood, deep ochre, near-black), (b) a true off-white at chroma 0 (or chroma toward the brand's own hue, not toward warmth-by-default), or (c) a darker mid-tone tinted neutral that's clearly the brand's own. "Warmth" in the brand is carried by accent + typography + imagery, not by body bg. +- Tinted neutrals: add 0.005–0.015 chroma toward the brand's hue. Don't default-tint toward warm or cool "because the brand feels that way"; that's the cross-project monoculture move. +- When picking a theme: Dark vs. light is never a default. Not dark "because tools look cool dark." Not light "to be safe.".Before choosing, write one sentence of physical scene: who uses this, where, under what ambient light, in what mood. If the sentence doesn't force the answer, it's not concrete enough. Add detail until it does. - Pick a **color strategy** before picking colors. Four steps on the commitment axis: - **Restrained**: tinted neutrals + one accent ≤10%. Product default; brand minimalism. - **Committed**: one saturated color carries 30–60% of the surface. Brand default for identity-driven pages. - **Full palette**: 3–4 named roles, each used deliberately. Brand campaigns; product data viz. - **Drenched**: the surface IS the color. Brand heroes, campaign pages. -- The "one accent ≤10%" rule is Restrained only. Committed / Full palette / Drenched exceed it on purpose. Don't collapse every design to Restrained by reflex. - -### Theme - -Dark vs. light is never a default. Not dark "because tools look cool dark." Not light "to be safe." - -Before choosing, write one sentence of physical scene: who uses this, where, under what ambient light, in what mood. If the sentence doesn't force the answer, it's not concrete enough. Add detail until it does. - -"Observability dashboard" does not force an answer. "SRE glancing at incident severity on a 27-inch monitor at 2am in a dim room" does. Run the sentence, not the category. - -### Typography - -- Cap body line length at 65–75ch. -- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales. - -### Layout - -- Vary spacing for rhythm. Same padding everywhere is monotony. -- Cards are the lazy answer. Use them only when they're truly the best affordance. Nested cards are always wrong. -- Don't wrap everything in a container. Most things don't need one. - -### Motion - -- Don't animate CSS layout properties. -- Ease out with exponential curves (ease-out-quart / quint / expo). No bounce, no elastic. ### Absolute bans @@ -100,12 +95,9 @@ Match-and-refuse. If you're about to write any of these, rewrite the element wit - **Glassmorphism as default.** Blurs and glass cards used decoratively. Rare and purposeful, or nothing. - **The hero-metric template.** Big number, small label, supporting stats, gradient accent. SaaS cliché. - **Identical card grids.** Same-sized cards with icon + heading + text, repeated endlessly. -- **Modal as first thought.** Modals are usually laziness. Exhaust inline / progressive alternatives first. - -### Copy - -- Every word earns its place. No restated headings, no intros that repeat the title. -- **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`. +- **Tiny uppercase tracked eyebrow above every section.** The 2023-era kicker (small all-caps text with wide tracking, "ABOUT" "PROCESS" "PRICING" above each heading) is now the saturated AI scaffold; it appears on 55-95% of generations regardless of brief, which is the definition of a tell. One named kicker as a deliberate brand system is voice; an eyebrow on every section is AI grammar. Choose a different cadence. +- **Numbered section markers as default scaffolding (01 / 02 / 03).** Putting `01 · About / 02 · Process / 03 · Pricing` above every section is the eyebrow trope one tier deeper: reach for it because "landing pages do this" and you're scaffolding by reflex. Numbers earn their place when the section actually IS a sequence (a real 3-step process, an ordered flow, a typed timeline) and the order carries information the reader needs. One deliberate numbered sequence on one page is voice; numbered eyebrows on every section across the site is AI grammar. +- **Text that overflows its container.** Long heading words plus large clamp scales plus narrow grids cause headline overflow on tablet/mobile. Test the heading copy at every breakpoint; if it overflows, reduce the clamp max or rewrite the copy. The viewport is part of the design. ### The AI slop test @@ -113,7 +105,7 @@ If someone could look at this interface and say "AI made that" without doubt, it **Category-reflex check.** Run at two altitudes; the second one catches what the first one misses. -- **First-order:** if someone could guess the theme + palette from the category alone ("observability → dark blue", "healthcare → white + teal", "finance → navy + gold", "crypto → neon on black"), it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain. +- **First-order:** if someone could guess the theme + palette from the category alone, it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain. - **Second-order:** if someone could guess the aesthetic family from category-plus-anti-references ("AI workflow tool that's not SaaS-cream → editorial-typographic", "fintech that's not navy-and-gold → terminal-native dark mode"), it's the trap one tier deeper. The first reflex was avoided; the second wasn't. Rework until both answers are not obvious. The brand register's [reflex-reject aesthetic lanes](reference/brand.md) list catches the currently-saturated families. ## Commands @@ -122,7 +114,7 @@ If someone could look at this interface and say "AI made that" without doubt, it |---|---|---|---| | `craft [feature]` | Build | Shape, then build a feature end-to-end | [reference/craft.md](reference/craft.md) | | `shape [feature]` | Build | Plan UX/UI before writing code | [reference/shape.md](reference/shape.md) | -| `teach` | Build | Set up PRODUCT.md and DESIGN.md context | [reference/teach.md](reference/teach.md) | +| `init` | Build | Set up project context: PRODUCT.md, DESIGN.md, live config, next steps | [reference/init.md](reference/init.md) | | `document` | Build | Generate DESIGN.md from existing project code | [reference/document.md](reference/document.md) | | `extract [target]` | Build | Pull reusable tokens and components into design system | [reference/extract.md](reference/extract.md) | | `critique [target]` | Evaluate | UX design review with heuristic scoring | [reference/critique.md](reference/critique.md) | @@ -150,11 +142,14 @@ Plus two management commands: `pin ` and `unpin `, detailed be 1. **No argument**: render the table above as the user-facing command menu, grouped by category. Ask what they'd like to do. 2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. -3. **First word doesn't match**: general design invocation. Apply the setup steps, shared design laws, and the loaded register reference, using the full argument as context. +3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. +4. **No clear command match**: general design invocation. Apply the setup steps, the General rules, and the loaded register reference, using the full argument as context. Setup (context gathering, register) is already loaded by then; sub-commands don't re-invoke `/impeccable`. -If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `teach` as a blocker, finish teach, refresh context, then resume the original command and target. +If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `init` as a blocker, finish init, refresh context, then resume the original command and target. + +`teach` is a deprecated alias for `init`: if the user types it, load [reference/init.md](reference/init.md) and proceed as if they ran `init`. ## Pin / Unpin diff --git a/.pi/skills/impeccable/reference/adapt.md b/.pi/skills/impeccable/reference/adapt.md index 9d0673ea2..5af7606cb 100644 --- a/.pi/skills/impeccable/reference/adapt.md +++ b/.pi/skills/impeccable/reference/adapt.md @@ -188,3 +188,124 @@ Test thoroughly across contexts: - **Slow connections**: Test on throttled network When the adaptation feels native to each context, hand off to `/impeccable polish` for the final pass. + +--- + +## Reference Material + +The sections below were previously `responsive-design.md` and live inline now so the adapt flow has its deep responsive reference in one place. + +### Responsive Design + +#### Mobile-First: Write It Right + +Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first. + +#### Breakpoints: Content-Driven + +Don't chase device sizes; let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints. + +#### Detect Input Method, Not Just Screen Size + +**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard. Use pointer and hover queries: + +```css +/* Fine pointer (mouse, trackpad) */ +@media (pointer: fine) { + .button { padding: 8px 16px; } +} + +/* Coarse pointer (touch, stylus) */ +@media (pointer: coarse) { + .button { padding: 12px 20px; } /* Larger touch target */ +} + +/* Device supports hover */ +@media (hover: hover) { + .card:hover { transform: translateY(-2px); } +} + +/* Device doesn't support hover (touch) */ +@media (hover: none) { + .card { /* No hover state - use active instead */ } +} +``` + +**Critical**: Don't rely on hover for functionality. Touch users can't hover. + +#### Safe Areas: Handle the Notch + +Modern phones have notches, rounded corners, and home indicators. Use `env()`: + +```css +body { + padding-top: env(safe-area-inset-top); + padding-bottom: env(safe-area-inset-bottom); + padding-left: env(safe-area-inset-left); + padding-right: env(safe-area-inset-right); +} + +/* With fallback */ +.footer { + padding-bottom: max(1rem, env(safe-area-inset-bottom)); +} +``` + +**Enable viewport-fit** in your meta tag: +```html + +``` + +#### Responsive Images: Get It Right + +##### srcset with Width Descriptors + +```html +Hero image +``` + +**How it works**: +- `srcset` lists available images with their actual widths (`w` descriptors) +- `sizes` tells the browser how wide the image will display +- Browser picks the best file based on viewport width AND device pixel ratio + +##### Picture Element for Art Direction + +When you need different crops/compositions (not just resolutions): + +```html + + + + ... + +``` + +#### Layout Adaptation Patterns + +**Navigation**: Three stages: hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `
/` for content that can collapse on mobile. + +#### Testing: Don't Trust DevTools Alone + +DevTools device emulation is useful for layout but misses: + +- Actual touch interactions +- Real CPU/memory constraints +- Network latency patterns +- Font rendering differences +- Browser chrome/keyboard appearances + +**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators. + +--- + +**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful. diff --git a/.pi/skills/impeccable/reference/animate.md b/.pi/skills/impeccable/reference/animate.md index 20aeea659..15d9f9080 100644 --- a/.pi/skills/impeccable/reference/animate.md +++ b/.pi/skills/impeccable/reference/animate.md @@ -6,7 +6,7 @@ Add motion that conveys state, gives feedback, and clarifies hierarchy. Cut moti ## Register -Brand: orchestrated page-load sequences, staggered reveals, scroll-driven animation. Motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions. +Brand: motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions. The saturated AI default is fade-and-rise reveals on every scrolled section; that's a tell, not a choreography. Reserve scroll-triggered motion for moments that earn it. Product: 150–250 ms on most transitions. Motion conveys state: feedback, reveal, loading, transitions between views. No page-load choreography; users are in a task and won't wait for it. @@ -49,10 +49,11 @@ Create a purposeful animation plan: Add motion systematically across these categories: ### Entrance Animations -- **Page load choreography**: Stagger element reveals (100-150ms delays), fade + slide combinations - **Hero section**: Dramatic entrance for primary content (scale, parallax, or creative effects) -- **Content reveals**: Scroll-triggered animations using intersection observer - **Modal/drawer entry**: Smooth slide + fade, backdrop fade, focus management +- **List rhythm**: Sibling stagger is legitimate for cards-in-a-grid or list-items-appearing. Whole-section fade-on-scroll is not a list and is not legitimate. Cap total stagger time: 10 items at 50ms each = 500ms total. For more items, reduce per-item delay or cap the staggered count. + + Use CSS custom properties for clean stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"`, `style="--i: 1"`, etc. on each item. ### Micro-interactions - **Button feedback**: @@ -97,11 +98,14 @@ Use appropriate techniques for each animation: ### Timing & Easing -**Durations by purpose:** -- **100-150ms**: Instant feedback (button press, toggle) -- **200-300ms**: State changes (hover, menu open) -- **300-500ms**: Layout changes (accordion, modal) -- **500-800ms**: Entrance animations (page load) +**Duration: the 100/300/500 rule.** Timing matters more than easing for "feels right": + +| Duration | Use Case | Examples | +|----------|----------|----------| +| **100–150ms** | Instant feedback | Button press, toggle, color change | +| **200–300ms** | State changes | Menu open, tooltip, hover state | +| **300–500ms** | Layout changes | Accordion, modal, drawer | +| **500–800ms** | Entrance animations | Page load, hero reveal | **Easing curves (use these, not CSS defaults):** ```css @@ -134,13 +138,35 @@ Use appropriate techniques for each animation: - GSAP for complex sequences ``` +### Motion Materials + +Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties. Match material to effect: + +- **Transform / opacity**: movement, press feedback, simple reveals, list choreography +- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances +- **Clip-path / masks**: wipes, reveals, editorial cropping, product-like transitions +- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state +- **Grid-template-rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly + +The hard rule isn't "transform and opacity only." It's: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify smoothness in-browser on target viewports. + ### Performance -- **Motion materials**: Use transform/opacity for reliable movement, but use blur, filters, masks, shadows, and color shifts when they materially improve the effect - **Layout safety**: Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins) -- **will-change**: Add sparingly for known expensive animations +- **will-change**: Add sparingly for known expensive animations only (e.g. on `:hover` or an `.animating` class), never preemptively across the whole page +- **Scroll triggers**: Use Intersection Observer instead of scroll event listeners; unobserve after the animation fires once - **Bound expensive effects**: Keep blur/filter/shadow areas small or isolated, use `contain` where appropriate - **Monitor FPS**: Ensure 60fps on target devices +### Perceived Performance + +Nobody cares how fast your site *is*, only how fast it feels. The 80ms threshold: anything under ~80ms feels instant because our brains buffer sensory input for that long to synchronize perception. Target this for micro-interactions. + +- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening. +- **Early completion**: Show content progressively, don't wait for everything (progressive images, streaming HTML, skeleton fade-ins). +- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Use for low-stakes actions (likes, follows). Avoid for payments or destructive operations. +- **Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances. +- **Caution**: Too-fast responses can decrease perceived value for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening. + ### Accessibility ```css @media (prefers-reduced-motion: reduce) { diff --git a/.pi/skills/impeccable/reference/bolder.md b/.pi/skills/impeccable/reference/bolder.md index ff0ef2f14..30d4e852c 100644 --- a/.pi/skills/impeccable/reference/bolder.md +++ b/.pi/skills/impeccable/reference/bolder.md @@ -50,7 +50,7 @@ Create a strategy to increase impact while maintaining coherence: Systematically increase impact across these dimensions: ### Typography Amplification -- **Replace generic fonts**: Swap system fonts for distinctive choices (see the parent skill's typography guidelines and [typography.md](typography.md) for inspiration) +- **Replace generic fonts**: Swap system fonts for distinctive choices (see the parent skill's typography guidelines and the [Reference Material section of typeset.md](typeset.md#reference-material) for inspiration) - **Extreme scale**: Create dramatic size jumps (3x-5x differences, not 1.5x) - **Weight contrast**: Pair 900 weights with 200 weights, not 600 with 400 - **Unexpected choices**: Variable fonts, display fonts for headlines, condensed/extended widths, monospace as intentional accent (not as lazy "dev tool" default) @@ -78,10 +78,10 @@ Systematically increase impact across these dimensions: - **Custom elements**: Illustrative elements, custom icons, decorative details that reinforce brand ### Motion & Animation -- **Entrance choreography**: Staggered, dramatic page load animations with 50-100ms delays -- **Scroll effects**: Parallax, reveal animations, scroll-triggered sequences -- **Micro-interactions**: Satisfying hover effects, click feedback, state changes -- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic, which cheapen the effect) +- **Hero moment**: One signature entrance, once. Not on every visit and not on every section. +- **Micro-interactions**: Satisfying hover effects, click feedback, state changes. +- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic, which cheapen the effect). +- **Bolder ≠ scroll-fade-rise on every section.** That's the saturated AI default, the opposite of bold. ### Composition Boldness - **Hero moments**: Create clear focal points with dramatic treatment diff --git a/.pi/skills/impeccable/reference/brand.md b/.pi/skills/impeccable/reference/brand.md index 3d83a1cdc..194514e95 100644 --- a/.pi/skills/impeccable/reference/brand.md +++ b/.pi/skills/impeccable/reference/brand.md @@ -43,17 +43,10 @@ The reflex-reject lists apply to **new design choices**. When the existing brand ### Pairing and voice -Distinctive + refined is the goal. The specific shape depends on the brand: - -- **Editorial / long-form / luxury**: display serif + sans body (a magazine shape). -- **Tech / dev tools / fintech**: one committed sans, usually; custom-tight tracking, strong weight contrast inside a single family. -- **Consumer / food / travel**: warmer pairings, often a humanist sans plus a script or display serif. -- **Creative studios / agencies**: rule-breaking welcome. Mono-only, or display-only, or custom-drawn type as voice. +Distinctive + refined is the goal. The specific shape depends on the brand, not on the brand's category. A category ("restaurant", "dev tool", "magazine", "fintech") is not a recipe; treating it as one is the first-order reflex SKILL.md warns against. Two families minimum is the rule *only* when the voice needs it. A single well-chosen family with committed weight/size contrast is stronger than a timid display+body pair. -Vary across projects. If the last brief was a serif-display landing page, this one isn't. - ### Scale Modular scale, fluid `clamp()` for headings, ≥1.25 ratio between steps. Flat scales (1.1× apart) read as uncommitted. @@ -74,8 +67,7 @@ Brand surfaces have permission for Committed, Full palette, and Drenched strateg - Asymmetric compositions are one option. Break the grid intentionally for emphasis. - Fluid spacing with `clamp()` that breathes on larger viewports. Vary for rhythm: generous separations, tight groupings. -- Alternative: a strict, visible grid as the voice (brutalist / Swiss / tech-spec aesthetics). Either asymmetric or rigorously-gridded can be "designed"; the failure mode is splitting the difference into a generic centered stack. -- Don't default to centering everything. Left-aligned with asymmetric layouts feels more designed; a strict grid reads as confident structure. A centered-stack hero with icon-title-subtitle cards reads as template. +- For image-led briefs (hotels, restaurants, magazines, photography), full-bleed hero imagery with overlaid menu and centered headline is a canonical move; let the photograph be the design. - When cards ARE the right affordance, use `grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` for breakpoint-free responsiveness. ## Imagery @@ -93,8 +85,7 @@ Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landin ## Motion -- One well-orchestrated page-load with staggered reveals beats scattered micro-interactions, when the brand invites it. Tech-minimal brands often skip entrance motion entirely; the restraint is the voice. -- For collapsing/expanding sections, transition `grid-template-rows` rather than `height`. +- One well-orchestrated page-load beats scattered micro-interactions, when the brand invites it. Some brands skip entrance motion entirely; the restraint is the voice. ## Brand bans (on top of the shared absolute bans) @@ -111,8 +102,7 @@ Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landin Brand can afford things product can't. Take them. -- Ambitious first-load motion. Reveals, scroll-triggered transitions, typographic choreography. +- Ambitious first-load motion. Reveals and typographic choreography that earn their place; not fade-on-scroll for every section. - Single-purpose viewports. One dominant idea per fold, long scroll, deliberate pacing. -- Typographic risk. Enormous display type, unexpected italic cuts, mixed cases, hand-drawn headlines, a single oversize word as a hero. - Unexpected color strategies. Palette IS voice; a calm brand and a restless brand should not share palette mechanics. - Art direction per section. Different sections can have different visual worlds if the narrative demands it. Consistency of voice beats consistency of treatment. diff --git a/.pi/skills/impeccable/reference/clarify.md b/.pi/skills/impeccable/reference/clarify.md index 5f7d420f4..00ee978c3 100644 --- a/.pi/skills/impeccable/reference/clarify.md +++ b/.pi/skills/impeccable/reference/clarify.md @@ -172,3 +172,117 @@ Test that copy improvements work: - **Tone**: Is it appropriate for the situation? When the copy reads cleanly, hand off to `/impeccable polish` for the final pass. + +--- + +## Reference Material + +The sections below were previously `ux-writing.md` and live inline now so the clarify flow has its deep UX-writing reference in one place. + +### UX Writing + +#### The Button Label Problem + +**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns: + +| Bad | Good | Why | +|-----|------|-----| +| OK | Save changes | Says what will happen | +| Submit | Create account | Outcome-focused | +| Yes | Delete message | Confirms the action | +| Cancel | Keep editing | Clarifies what "cancel" means | +| Click here | Download PDF | Describes the destination | + +**For destructive actions**, name the destruction: +- "Delete" not "Remove" (delete is permanent, remove implies recoverable) +- "Delete 5 items" not "Delete selected" (show the count) + +#### Error Messages: The Formula + +Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input". + +##### Error Message Templates + +| Situation | Template | +|-----------|----------| +| **Format error** | "[Field] needs to be [format]. Example: [example]" | +| **Missing required** | "Please enter [what's missing]" | +| **Permission denied** | "You don't have access to [thing]. [What to do instead]" | +| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." | +| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" | + +##### Don't Blame the User + +Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date". + +#### Empty States Are Opportunities + +Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items". + +#### Voice vs Tone + +**Voice** is your brand's personality, consistent everywhere. +**Tone** adapts to the moment. + +| Moment | Tone Shift | +|--------|------------| +| Success | Celebratory, brief: "Done! Your changes are live." | +| Error | Empathetic, helpful: "That didn't work. Here's what to try..." | +| Loading | Reassuring: "Saving your work..." | +| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | + +**Never use humor for errors.** Users are already frustrated. Be helpful, not cute. + +#### Writing for Accessibility + +**Link text** must have standalone meaning: "View pricing plans" not "Click here". **Alt text** describes information, not the image: "Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context. + +#### Writing for Translation + +##### Plan for Expansion + +German text is ~30% longer than English. Allocate space: + +| Language | Expansion | +|----------|-----------| +| German | +30% | +| French | +20% | +| Finnish | +30-40% | +| Chinese | -30% (fewer chars, but same width) | + +##### Translation-Friendly Patterns + +Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear. + +#### Consistency: The Terminology Problem + +Pick one term and stick with it: + +| Inconsistent | Consistent | +|--------------|------------| +| Delete / Remove / Trash | Delete | +| Settings / Preferences / Options | Settings | +| Sign in / Log in / Enter | Sign in | +| Create / Add / New | Create | + +Build a terminology glossary and enforce it. Variety creates confusion. + +#### Avoid Redundant Copy + +If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well. + +#### Loading States + +Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress. + +#### Confirmation Dialogs: Use Sparingly + +Most confirmation dialogs are design failures; consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No"). + +#### Form Instructions + +Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking. + +--- + +**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors. diff --git a/.pi/skills/impeccable/reference/codex.md b/.pi/skills/impeccable/reference/codex.md index f40ec746f..d563af95b 100644 --- a/.pi/skills/impeccable/reference/codex.md +++ b/.pi/skills/impeccable/reference/codex.md @@ -23,8 +23,8 @@ Before generating anything, run a brief direction conversation grounded in the s Ask **2-3 targeted questions** about visual lane, color strategy, atmosphere, and named anchor references. Don't enumerate generic menus; tie each question to the shape brief's answers. Example shape-grounded questions: -- "Brief says 'editorial restraint, Klim-adjacent.' Are we closer to a quiet specimen page or a magazine-spread feel with hero imagery?" -- "Palette strategy from shape was 'Committed.' Want it warm-grounded (deep oxblood + cream) or cool-grounded (slate + paper white)?" +- "Brief says 'specimen-page restraint.' Are we closer to a quiet typographic page or a wider editorial spread with hero imagery?" +- "Palette strategy from shape was 'Committed.' Which one color carries the surface (a brand-driven pick rather than a default warm-or-cool framing)? (And no, the answer isn't a cream/sand body bg; that's the saturated AI default.)" **STOP and wait for answers.** These pin the palette before any pixel gets generated. Do not proceed to Step B until the user has responded. diff --git a/.pi/skills/impeccable/reference/cognitive-load.md b/.pi/skills/impeccable/reference/cognitive-load.md deleted file mode 100644 index 48f8ad58b..000000000 --- a/.pi/skills/impeccable/reference/cognitive-load.md +++ /dev/null @@ -1,106 +0,0 @@ -# Cognitive Load Assessment - -Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload. - ---- - -## Three Types of Cognitive Load - -### Intrinsic Load: The Task Itself -Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it. - -**Manage it by**: -- Breaking complex tasks into discrete steps -- Providing scaffolding (templates, defaults, examples) -- Progressive disclosure: show what's needed now, hide the rest -- Grouping related decisions together - -### Extraneous Load: Bad Design -Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste. - -**Common sources**: -- Confusing navigation that requires mental mapping -- Unclear labels that force users to guess meaning -- Visual clutter competing for attention -- Inconsistent patterns that prevent learning -- Unnecessary steps between user intent and result - -### Germane Load: Learning Effort -Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery. - -**Support it by**: -- Progressive disclosure that reveals complexity gradually -- Consistent patterns that reward learning -- Feedback that confirms correct understanding -- Onboarding that teaches through action, not walls of text - ---- - -## Cognitive Load Checklist - -Evaluate the interface against these 8 items: - -- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements? -- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)? -- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)? -- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen? -- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next? -- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)? -- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one? -- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it? - -**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed). - ---- - -## The Working Memory Rule - -**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001). - -At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider: -- **≤4 items**: Within working memory limits, manageable -- **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure -- **8+ items**: Overloaded; users will skip, misclick, or abandon - -**Practical applications**: -- Navigation menus: ≤5 top-level items (group the rest under clear categories) -- Form sections: ≤4 fields visible per group before a visual break -- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu -- Dashboard widgets: ≤4 key metrics visible without scrolling -- Pricing tiers: ≤3 options (more causes analysis paralysis) - ---- - -## Common Cognitive Load Violations - -### 1. The Wall of Options -**Problem**: Presenting 10+ choices at once with no hierarchy. -**Fix**: Group into categories, highlight recommended, use progressive disclosure. - -### 2. The Memory Bridge -**Problem**: User must remember info from step 1 to complete step 3. -**Fix**: Keep relevant context visible, or repeat it where it's needed. - -### 3. The Hidden Navigation -**Problem**: User must build a mental map of where things are. -**Fix**: Always show current location (breadcrumbs, active states, progress indicators). - -### 4. The Jargon Barrier -**Problem**: Technical or domain language forces translation effort. -**Fix**: Use plain language. If domain terms are unavoidable, define them inline. - -### 5. The Visual Noise Floor -**Problem**: Every element has the same visual weight; nothing stands out. -**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted. - -### 6. The Inconsistent Pattern -**Problem**: Similar actions work differently in different places. -**Fix**: Standardize interaction patterns. Same type of action = same type of UI. - -### 7. The Multi-Task Demand -**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating). -**Fix**: Sequence the steps. Let the user do one thing at a time. - -### 8. The Context Switch -**Problem**: User must jump between screens/tabs/modals to gather info for a single decision. -**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth. diff --git a/.pi/skills/impeccable/reference/color-and-contrast.md b/.pi/skills/impeccable/reference/color-and-contrast.md deleted file mode 100644 index 110c2ee47..000000000 --- a/.pi/skills/impeccable/reference/color-and-contrast.md +++ /dev/null @@ -1,105 +0,0 @@ -# Color & Contrast - -## Color Spaces: Use OKLCH - -**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal, unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark. - -The OKLCH function takes three components: `oklch(lightness chroma hue)` where lightness is 0-100%, chroma is roughly 0-0.4, and hue is 0-360. To build a primary color and its lighter / darker variants, hold the chroma+hue roughly constant and vary the lightness, but **reduce chroma as you approach white or black**, because high chroma at extreme lightness looks garish. - -The hue you pick is a brand decision and should not come from a default. Do not reach for blue (hue 250) or warm orange (hue 60) by reflex; those are the dominant AI-design defaults, not the right answer for any specific brand. - -## Building Functional Palettes - -### Tinted Neutrals - -**Pure gray is dead.** A neutral with zero chroma feels lifeless next to a colored brand. Add a tiny chroma value (0.005-0.015) to all your neutrals, hued toward whatever your brand color is. The chroma is small enough not to read as "tinted" consciously, but it creates subconscious cohesion between brand color and UI surfaces. - -The hue you tint toward should come from THIS project's brand, not from a "warm = friendly, cool = tech" formula. If your brand color is teal, your neutrals lean toward teal. If your brand color is amber, they lean toward amber. The point is cohesion with the SPECIFIC brand, not a stock palette. - -**Avoid** the trap of always tinting toward warm orange or always tinting toward cool blue. Those are the two laziest defaults and they create their own monoculture across projects. - -### Palette Structure - -A complete system needs: - -| Role | Purpose | Example | -|------|---------|---------| -| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades | -| **Neutral** | Text, backgrounds, borders | 9-11 shade scale | -| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each | -| **Surface** | Cards, modals, overlays | 2-3 elevation levels | - -**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise. - -### The 60-30-10 Rule (Applied Correctly) - -This rule is about **visual weight**, not pixel count: - -- **60%**: Neutral backgrounds, white space, base surfaces -- **30%**: Secondary colors: text, borders, inactive states -- **10%**: Accent: CTAs, highlights, focus states - -The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power. - -## Contrast & Accessibility - -### WCAG Requirements - -| Content Type | AA Minimum | AAA Target | -|--------------|------------|------------| -| Body text | 4.5:1 | 7:1 | -| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | -| UI components, icons | 3:1 | 4.5:1 | -| Non-essential decorations | None | None | - -**The gotcha**: Placeholder text still needs 4.5:1. That light gray placeholder you see everywhere? Usually fails WCAG. - -### Dangerous Color Combinations - -These commonly fail contrast or cause readability issues: - -- Light gray text on white (the #1 accessibility fail) -- **Gray text on any colored background**: gray looks washed out and dead on color. Use a darker shade of the background color, or transparency -- Red text on green background (or vice versa): 8% of men can't distinguish these -- Blue text on red background (vibrates visually) -- Yellow text on white (almost always fails) -- Thin light text on images (unpredictable contrast) - -### Never Use Pure Gray or Pure Black - -Pure gray (`oklch(50% 0 0)`) and pure black (`#000`) don't exist in nature; real shadows and surfaces always have a color cast. Even a chroma of 0.005-0.01 is enough to feel natural without being obviously tinted. (See tinted neutrals example above.) - -### Testing - -Don't trust your eyes. Use tools: - -- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) -- Browser DevTools → Rendering → Emulate vision deficiencies -- [Polypane](https://polypane.app/) for real-time testing - -## Theming: Light & Dark Mode - -### Dark Mode Is Not Inverted Light Mode - -You can't just swap colors. Dark mode requires different design decisions: - -| Light Mode | Dark Mode | -|------------|-----------| -| Shadows for depth | Lighter surfaces for depth (no shadows) | -| Dark text on light | Light text on dark (reduce font weight) | -| Vibrant accents | Desaturate accents slightly | -| White backgrounds | Never pure black; use dark gray (oklch 12-18%) | - -In dark mode, depth comes from surface lightness, not shadow. Build a 3-step surface scale where higher elevations are lighter (e.g. 15% / 20% / 25% lightness). Use the SAME hue and chroma as your brand color (whatever it is for THIS project; do not reach for blue) and only vary the lightness. Reduce body text weight slightly (e.g. 350 instead of 400) because light text on dark reads as heavier than dark text on light. - -### Token Hierarchy - -Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer; primitives stay the same. - -## Alpha Is A Design Smell - -Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed. - ---- - -**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Using pure black (#000) for large areas. Skipping color blindness testing (8% of men affected). diff --git a/.pi/skills/impeccable/reference/colorize.md b/.pi/skills/impeccable/reference/colorize.md index 9e96eef59..2457f608c 100644 --- a/.pi/skills/impeccable/reference/colorize.md +++ b/.pi/skills/impeccable/reference/colorize.md @@ -68,10 +68,10 @@ Add color systematically across these dimensions: - **Hover states**: Introduce color on interaction ### Background & Surfaces -- **Tinted backgrounds**: Replace pure gray (`#f5f5f5`) with warm neutrals (`oklch(97% 0.01 60)`) or cool tints (`oklch(97% 0.01 250)`) +- **Tinted backgrounds**: If you replace pure gray, tint toward the brand hue, not toward a generic-warm-or-cool pair. The default-warm-tint (`oklch(97% 0.01 60)` and its neighbors) is now the AI cream/sand giveaway. Be specific to the brand or stay neutral. - **Colored sections**: Use subtle background colors to separate areas - **Gradient backgrounds**: Add depth with subtle, intentional gradients (not generic purple-blue) -- **Cards & surfaces**: Tint cards or surfaces slightly for warmth +- **Cards & surfaces**: Tint cards or surfaces toward the brand, not "for warmth" by reflex **Use OKLCH for color**: It's perceptually uniform, meaning equal steps in lightness *look* equal. Great for generating harmonious scales. @@ -124,8 +124,6 @@ Ensure color addition improves rather than overwhelms: - Use every color in the rainbow (choose 2-4 colors beyond neutrals) - Apply color randomly without semantic meaning - Put gray text on colored backgrounds. It looks washed out; use a darker shade of the background color or transparency instead -- Use pure gray for neutrals. Add subtle color tint (warm or cool) for depth -- Use pure black (`#000`) or pure white (`#fff`) for large areas - Violate WCAG contrast requirements - Use color as the only indicator (accessibility issue) - Make everything colorful (defeats the purpose) @@ -152,3 +150,108 @@ When invoked from live mode, each variant MUST declare a `color-amount` param so ``` Layer 1-2 variant-specific params on top: palette selection (`steps` with named options), temperature warmth, or tint vs. true color. See `reference/live.md` for the full params contract. + +--- + +## Reference Material + +The sections below were previously `color-and-contrast.md` and live inline now so the colorize flow has its deep color reference in one place. + +### Color & Contrast + +#### Color Spaces: Use OKLCH + +**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal, unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark. + +The OKLCH function takes three components: `oklch(lightness chroma hue)` where lightness is 0-100%, chroma is roughly 0-0.4, and hue is 0-360. To build a primary color and its lighter / darker variants, hold the chroma+hue roughly constant and vary the lightness, but **reduce chroma as you approach white or black**, because high chroma at extreme lightness looks garish. + +The hue you pick is a brand decision and should not come from a default. Do not reach for blue (hue 250) or warm orange (hue 60) by reflex; those are the dominant AI-design defaults, not the right answer for any specific brand. + +#### Building Functional Palettes + +##### Tinted Neutrals + +**Pure gray is dead.** A neutral with zero chroma feels lifeless next to a colored brand. Add a tiny chroma value (0.005-0.015) to all your neutrals, hued toward whatever your brand color is. The chroma is small enough not to read as "tinted" consciously, but it creates subconscious cohesion between brand color and UI surfaces. + +The hue you tint toward should come from THIS project's brand, not from a "warm = friendly, cool = tech" formula. If your brand color is teal, your neutrals lean toward teal. If your brand color is amber, they lean toward amber. The point is cohesion with the SPECIFIC brand, not a stock palette. + +**Avoid** the trap of always tinting toward warm orange or always tinting toward cool blue. Those are the two laziest defaults and they create their own monoculture across projects. + +##### Palette Structure + +A complete system needs: + +| Role | Purpose | Example | +|------|---------|---------| +| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades | +| **Neutral** | Text, backgrounds, borders | 9-11 shade scale | +| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each | +| **Surface** | Cards, modals, overlays | 2-3 elevation levels | + +**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise. + +##### The 60-30-10 Rule (Applied Correctly) + +This rule is about **visual weight**, not pixel count: + +- **60%**: Neutral backgrounds, white space, base surfaces +- **30%**: Secondary colors: text, borders, inactive states +- **10%**: Accent: CTAs, highlights, focus states + +The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power. + +#### Contrast & Accessibility + +##### WCAG Requirements + +| Content Type | AA Minimum | AAA Target | +|--------------|------------|------------| +| Body text | 4.5:1 | 7:1 | +| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | +| UI components, icons | 3:1 | 4.5:1 | +| Non-essential decorations | None | None | + +##### Dangerous Color Combinations + +These commonly fail contrast or cause readability issues: + +- Light gray text on white (the #1 accessibility fail) +- Red text on green background (or vice versa): 8% of men can't distinguish these +- Blue text on red background (vibrates visually) +- Yellow text on white (almost always fails) +- Thin light text on images (unpredictable contrast) + +##### Testing + +Don't trust your eyes. Use tools: + +- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) +- Browser DevTools → Rendering → Emulate vision deficiencies +- [Polypane](https://polypane.app/) for real-time testing + +#### Theming: Light & Dark Mode + +##### Dark Mode Is Not Inverted Light Mode + +You can't just swap colors. Dark mode requires different design decisions: + +| Light Mode | Dark Mode | +|------------|-----------| +| Shadows for depth | Lighter surfaces for depth (no shadows) | +| Dark text on light | Light text on dark (reduce font weight) | +| Vibrant accents | Desaturate accents slightly | +| White backgrounds | Either pure black or a deep surface that fits the brand (a brand-tinted near-black at oklch 12-18% works too) | + +In dark mode, depth comes from surface lightness, not shadow. Build a 3-step surface scale where higher elevations are lighter (e.g. 15% / 20% / 25% lightness). Use the SAME hue and chroma as your brand color (whatever it is for THIS project; do not reach for blue) and only vary the lightness. Reduce body text weight slightly (e.g. 350 instead of 400) because light text on dark reads as heavier than dark text on light. + +##### Token Hierarchy + +Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer; primitives stay the same. + +#### Alpha Is A Design Smell + +Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed. + +--- + +**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Skipping color blindness testing (8% of men affected). diff --git a/.pi/skills/impeccable/reference/craft.md b/.pi/skills/impeccable/reference/craft.md index 06094b1dd..2c7bd99b1 100644 --- a/.pi/skills/impeccable/reference/craft.md +++ b/.pi/skills/impeccable/reference/craft.md @@ -56,15 +56,15 @@ If the harness has native image generation (Codex), a compact shape's "confirm o Based on the design brief's "Recommended References" section, consult the relevant impeccable reference files. At minimum, always consult: -- [spatial-design.md](spatial-design.md) for layout and spacing -- [typography.md](typography.md) for type hierarchy +- [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 [motion-design.md](motion-design.md) -- Color-heavy or themed? Consult [color-and-contrast.md](color-and-contrast.md) -- Responsive requirements? Consult [responsive-design.md](responsive-design.md) -- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.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) diff --git a/.pi/skills/impeccable/reference/critique.md b/.pi/skills/impeccable/reference/critique.md index 8d02e36f5..17cf50f84 100644 --- a/.pi/skills/impeccable/reference/critique.md +++ b/.pi/skills/impeccable/reference/critique.md @@ -38,9 +38,9 @@ Read relevant source files and visually inspect the live page when browser autom Evaluate: - **AI slop**: Would someone believe "AI made this" immediately? Check all DON'T guidance from the parent Impeccable skill. - **Holistic design**: hierarchy, IA, emotional fit, discoverability, composition, typography, color, accessibility, states, copy, and edge cases. -- **Cognitive load**: consult [cognitive-load](cognitive-load.md); report checklist failures and decision points with >4 visible options. +- **Cognitive load**: consult the [Cognitive Load Assessment](#cognitive-load-assessment) section below; report checklist failures and decision points with >4 visible options. - **Emotional journey**: peak-end rule, emotional valleys, reassurance at high-stakes moments. -- **Nielsen heuristics**: consult [heuristics-scoring](heuristics-scoring.md); score all 10 heuristics 0-4. +- **Nielsen heuristics**: consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below; score all 10 heuristics 0-4. Return: AI slop verdict, heuristic scores, cognitive load, emotional journey, 2-3 strengths, 3-5 priority issues, persona red flags, minor observations, and provocative questions. @@ -80,7 +80,7 @@ The chat response is the primary user-facing deliverable. Present the full struc Structure your feedback as a design director would: #### Design Health Score -> *Consult [heuristics-scoring](heuristics-scoring.md)* +> *Consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below.* Present the Nielsen's 10 heuristics scores as a table: @@ -119,16 +119,16 @@ Highlight 2-3 things done well. Be specific about why they work. #### Priority Issues The 3-5 most impactful design problems, ordered by importance. -For each issue, tag with **P0-P3 severity** (consult [heuristics-scoring](heuristics-scoring.md) for severity definitions): +For each issue, tag with **P0-P3 severity** (see [Issue Severity below](#issue-severity-p0p3) for definitions): - **[P?] What**: Name the problem clearly - **Why it matters**: How this hurts users or undermines goals - **Fix**: What to do about it (be concrete) - **Suggested command**: Which command could address this (from: /impeccable adapt, /impeccable animate, /impeccable audit, /impeccable bolder, /impeccable clarify, /impeccable colorize, /impeccable critique, /impeccable delight, /impeccable distill, /impeccable document, /impeccable harden, /impeccable layout, /impeccable onboard, /impeccable optimize, /impeccable overdrive, /impeccable polish, /impeccable quieter, /impeccable shape, /impeccable typeset) #### Persona Red Flags -> *Consult [personas](personas.md)* +> *Consult the [Personas reference](#persona-based-design-testing) below.* -Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `AGENTS.md` contains a `## Design Context` section from `impeccable teach`, also generate 1-2 project-specific personas from the audience/brand info. +Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `AGENTS.md` contains a `## Design Context` section from `impeccable init`, also generate 1-2 project-specific personas from the audience/brand info. For each selected persona, walk through the primary user action and list specific red flags found: @@ -234,3 +234,534 @@ After presenting the summary, tell the user: > You can ask me to run these one at a time, all at once, or in any order you prefer. > > Re-run `/impeccable critique` after fixes to see your score improve. + +--- + +## Reference Material + +The sections below were previously separate reference files (`cognitive-load.md`, `heuristics-scoring.md`, `personas.md`). They live inline now so the critique flow has all its deep context in one place. + +### Cognitive Load Assessment + +Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload. + +--- + +#### Three Types of Cognitive Load + +##### Intrinsic Load: The Task Itself +Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it. + +**Manage it by**: +- Breaking complex tasks into discrete steps +- Providing scaffolding (templates, defaults, examples) +- Progressive disclosure: show what's needed now, hide the rest +- Grouping related decisions together + +##### Extraneous Load: Bad Design +Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste. + +**Common sources**: +- Confusing navigation that requires mental mapping +- Unclear labels that force users to guess meaning +- Visual clutter competing for attention +- Inconsistent patterns that prevent learning +- Unnecessary steps between user intent and result + +##### Germane Load: Learning Effort +Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery. + +**Support it by**: +- Progressive disclosure that reveals complexity gradually +- Consistent patterns that reward learning +- Feedback that confirms correct understanding +- Onboarding that teaches through action, not walls of text + +--- + +#### Cognitive Load Checklist + +Evaluate the interface against these 8 items: + +- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements? +- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)? +- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)? +- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen? +- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next? +- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)? +- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one? +- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it? + +**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed). + +--- + +#### The Working Memory Rule + +**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001). + +At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider: +- **≤4 items**: Within working memory limits, manageable +- **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure +- **8+ items**: Overloaded; users will skip, misclick, or abandon + +**Practical applications**: +- Navigation menus: ≤5 top-level items (group the rest under clear categories) +- Form sections: ≤4 fields visible per group before a visual break +- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu +- Dashboard widgets: ≤4 key metrics visible without scrolling +- Pricing tiers: ≤3 options (more causes analysis paralysis) + +--- + +#### Common Cognitive Load Violations + +##### 1. The Wall of Options +**Problem**: Presenting 10+ choices at once with no hierarchy. +**Fix**: Group into categories, highlight recommended, use progressive disclosure. + +##### 2. The Memory Bridge +**Problem**: User must remember info from step 1 to complete step 3. +**Fix**: Keep relevant context visible, or repeat it where it's needed. + +##### 3. The Hidden Navigation +**Problem**: User must build a mental map of where things are. +**Fix**: Always show current location (breadcrumbs, active states, progress indicators). + +##### 4. The Jargon Barrier +**Problem**: Technical or domain language forces translation effort. +**Fix**: Use plain language. If domain terms are unavoidable, define them inline. + +##### 5. The Visual Noise Floor +**Problem**: Every element has the same visual weight; nothing stands out. +**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted. + +##### 6. The Inconsistent Pattern +**Problem**: Similar actions work differently in different places. +**Fix**: Standardize interaction patterns. Same type of action = same type of UI. + +##### 7. The Multi-Task Demand +**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating). +**Fix**: Sequence the steps. Let the user do one thing at a time. + +##### 8. The Context Switch +**Problem**: User must jump between screens/tabs/modals to gather info for a single decision. +**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth. + +--- + +### Heuristics Scoring Guide + +Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough." + +#### Nielsen's 10 Heuristics + +##### 1. Visibility of System Status + +Keep users informed about what's happening through timely, appropriate feedback. + +**Check for**: +- Loading indicators during async operations +- Confirmation of user actions (save, submit, delete) +- Progress indicators for multi-step processes +- Current location in navigation (breadcrumbs, active states) +- Form validation feedback (inline, not just on submit) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | No feedback; user is guessing what happened | +| 1 | Rare feedback; most actions produce no visible response | +| 2 | Partial; some states communicated, major gaps remain | +| 3 | Good; most operations give clear feedback, minor gaps | +| 4 | Excellent; every action confirms, progress is always visible | + +##### 2. Match Between System and Real World + +Speak the user's language. Follow real-world conventions. Information appears in natural, logical order. + +**Check for**: +- Familiar terminology (no unexplained jargon) +- Logical information order matching user expectations +- Recognizable icons and metaphors +- Domain-appropriate language for the target audience +- Natural reading flow (left-to-right, top-to-bottom priority) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Pure tech jargon, alien to users | +| 1 | Mostly confusing; requires domain expertise to navigate | +| 2 | Mixed; some plain language, some jargon leaks through | +| 3 | Mostly natural; occasional term needs context | +| 4 | Speaks the user's language fluently throughout | + +##### 3. User Control and Freedom + +Users need a clear "emergency exit" from unwanted states without extended dialogue. + +**Check for**: +- Undo/redo functionality +- Cancel buttons on forms and modals +- Clear navigation back to safety (home, previous) +- Easy way to clear filters, search, selections +- Escape from long or multi-step processes + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Users get trapped; no way out without refreshing | +| 1 | Difficult exits; must find obscure paths to escape | +| 2 | Some exits; main flows have escape, edge cases don't | +| 3 | Good control; users can exit and undo most actions | +| 4 | Full control; undo, cancel, back, and escape everywhere | + +##### 4. Consistency and Standards + +Users shouldn't wonder whether different words, situations, or actions mean the same thing. + +**Check for**: +- Consistent terminology throughout the interface +- Same actions produce same results everywhere +- Platform conventions followed (standard UI patterns) +- Visual consistency (colors, typography, spacing, components) +- Consistent interaction patterns (same gesture = same behavior) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Inconsistent everywhere; feels like different products stitched together | +| 1 | Many inconsistencies; similar things look/behave differently | +| 2 | Partially consistent; main flows match, details diverge | +| 3 | Mostly consistent; occasional deviation, nothing confusing | +| 4 | Fully consistent; cohesive system, predictable behavior | + +##### 5. Error Prevention + +Better than good error messages is a design that prevents problems in the first place. + +**Check for**: +- Confirmation before destructive actions (delete, overwrite) +- Constraints preventing invalid input (date pickers, dropdowns) +- Smart defaults that reduce errors +- Clear labels that prevent misunderstanding +- Autosave and draft recovery + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Errors easy to make; no guardrails anywhere | +| 1 | Few safeguards; some inputs validated, most aren't | +| 2 | Partial prevention; common errors caught, edge cases slip | +| 3 | Good prevention; most error paths blocked proactively | +| 4 | Excellent; errors nearly impossible through smart constraints | + +##### 6. Recognition Rather Than Recall + +Minimize memory load. Make objects, actions, and options visible or easily retrievable. + +**Check for**: +- Visible options (not buried in hidden menus) +- Contextual help when needed (tooltips, inline hints) +- Recent items and history +- Autocomplete and suggestions +- Labels on icons (not icon-only navigation) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Heavy memorization; users must remember paths and commands | +| 1 | Mostly recall; many hidden features, few visible cues | +| 2 | Some aids; main actions visible, secondary features hidden | +| 3 | Good recognition; most things discoverable, few memory demands | +| 4 | Everything discoverable; users never need to memorize | + +##### 7. Flexibility and Efficiency of Use + +Accelerators, invisible to novices, speed up expert interaction. + +**Check for**: +- Keyboard shortcuts for common actions +- Customizable interface elements +- Recent items and favorites +- Bulk/batch actions +- Power user features that don't complicate the basics + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | One rigid path; no shortcuts or alternatives | +| 1 | Limited flexibility; few alternatives to the main path | +| 2 | Some shortcuts; basic keyboard support, limited bulk actions | +| 3 | Good accelerators; keyboard nav, some customization | +| 4 | Highly flexible; multiple paths, power features, customizable | + +##### 8. Aesthetic and Minimalist Design + +Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose. + +**Check for**: +- Only necessary information visible at each step +- Clear visual hierarchy directing attention +- Purposeful use of color and emphasis +- No decorative clutter competing for attention +- Focused, uncluttered layouts + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Overwhelming; everything competes for attention equally | +| 1 | Cluttered; too much noise, hard to find what matters | +| 2 | Some clutter; main content clear, periphery noisy | +| 3 | Mostly clean; focused design, minor visual noise | +| 4 | Perfectly minimal; every element earns its pixel | + +##### 9. Help Users Recognize, Diagnose, and Recover from Errors + +Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution. + +**Check for**: +- Plain language error messages (no error codes for users) +- Specific problem identification ("Email is missing @" not "Invalid input") +- Actionable recovery suggestions +- Errors displayed near the source of the problem +- Non-blocking error handling (don't wipe the form) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Cryptic errors; codes, jargon, or no message at all | +| 1 | Vague errors; "Something went wrong" with no guidance | +| 2 | Clear but unhelpful; names the problem but not the fix | +| 3 | Clear with suggestions; identifies problem and offers next steps | +| 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work | + +##### 10. Help and Documentation + +Even if the system is usable without docs, help should be easy to find, task-focused, and concise. + +**Check for**: +- Searchable help or documentation +- Contextual help (tooltips, inline hints, guided tours) +- Task-focused organization (not feature-organized) +- Concise, scannable content +- Easy access without leaving current context + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | No help available anywhere | +| 1 | Help exists but hard to find or irrelevant | +| 2 | Basic help; FAQ or docs exist, not contextual | +| 3 | Good documentation; searchable, mostly task-focused | +| 4 | Excellent contextual help; right info at the right moment | + +--- + +#### Score Summary + +**Total possible**: 40 points (10 heuristics × 4 max) + +| Score Range | Rating | What It Means | +|-------------|--------|---------------| +| 36–40 | Excellent | Minor polish only; ship it | +| 28–35 | Good | Address weak areas, solid foundation | +| 20–27 | Acceptable | Significant improvements needed before users are happy | +| 12–19 | Poor | Major UX overhaul required; core experience broken | +| 0–11 | Critical | Redesign needed; unusable in current state | + +--- + +#### Issue Severity (P0–P3) + +Tag each individual issue found during scoring with a priority level: + +| Priority | Name | Description | Action | +|----------|------|-------------|--------| +| **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper | +| **P1** | Major | Causes significant difficulty or confusion | Fix before release | +| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass | +| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits | + +**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1. + +--- + +### Persona-Based Design Testing + +Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss. + +**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns. + +--- + +#### 1. Impatient Power User: "Alex" + +**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave. + +**Behaviors**: +- Skips all onboarding and instructions +- Looks for keyboard shortcuts immediately +- Tries to bulk-select, batch-edit, and automate +- Gets frustrated by required steps that feel unnecessary +- Abandons if anything feels slow or patronizing + +**Test Questions**: +- Can Alex complete the core task in under 60 seconds? +- Are there keyboard shortcuts for common actions? +- Can onboarding be skipped entirely? +- Do modals have keyboard dismiss (Esc)? +- Is there a "power user" path (shortcuts, bulk actions)? + +**Red Flags** (report these specifically): +- Forced tutorials or unskippable onboarding +- No keyboard navigation for primary actions +- Slow animations that can't be skipped +- One-item-at-a-time workflows where batch would be natural +- Redundant confirmation steps for low-risk actions + +--- + +#### 2. Confused First-Timer: "Jordan" + +**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out. + +**Behaviors**: +- Reads all instructions carefully +- Hesitates before clicking anything unfamiliar +- Looks for help or support constantly +- Misunderstands jargon and abbreviations +- Takes the most literal interpretation of any label + +**Test Questions**: +- Is the first action obviously clear within 5 seconds? +- Are all icons labeled with text? +- Is there contextual help at decision points? +- Does terminology assume prior knowledge? +- Is there a clear "back" or "undo" at every step? + +**Red Flags** (report these specifically): +- Icon-only navigation with no labels +- Technical jargon without explanation +- No visible help option or guidance +- Ambiguous next steps after completing an action +- No confirmation that an action succeeded + +--- + +#### 3. Accessibility-Dependent User: "Sam" + +**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences. + +**Behaviors**: +- Tabs through the interface linearly +- Relies on ARIA labels and heading structure +- Cannot see hover states or visual-only indicators +- Needs adequate color contrast (4.5:1 minimum) +- May use browser zoom up to 200% + +**Test Questions**: +- Can the entire primary flow be completed keyboard-only? +- Are all interactive elements focusable with visible focus indicators? +- Do images have meaningful alt text? +- Is color contrast WCAG AA compliant (4.5:1 for text)? +- Does the screen reader announce state changes (loading, success, errors)? + +**Red Flags** (report these specifically): +- Click-only interactions with no keyboard alternative +- Missing or invisible focus indicators +- Meaning conveyed by color alone (red = error, green = success) +- Unlabeled form fields or buttons +- Time-limited actions without extension option +- Custom components that break screen reader flow + +--- + +#### 4. Deliberate Stress Tester: "Riley" + +**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience. + +**Behaviors**: +- Tests edge cases intentionally (empty states, long strings, special characters) +- Submits forms with unexpected data (emoji, RTL text, very long values) +- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs +- Looks for inconsistencies between what the UI promises and what actually happens +- Documents problems methodically + +**Test Questions**: +- What happens at the edges (0 items, 1000 items, very long text)? +- Do error states recover gracefully or leave the UI in a broken state? +- What happens on refresh mid-workflow? Is state preserved? +- Are there features that appear to work but produce broken results? +- How does the UI handle unexpected input (emoji, special chars, paste from Excel)? + +**Red Flags** (report these specifically): +- Features that appear to work but silently fail or produce wrong results +- Error handling that exposes technical details or leaves UI in a broken state +- Empty states that show nothing useful ("No results" with no guidance) +- Workflows that lose user data on refresh or navigation +- Inconsistent behavior between similar interactions in different parts of the UI + +--- + +#### 5. Distracted Mobile User: "Casey" + +**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection. + +**Behaviors**: +- Uses thumb only; prefers bottom-of-screen actions +- Gets interrupted mid-flow and returns later +- Switches between apps frequently +- Has limited attention span and low patience +- Types as little as possible, prefers taps and selections + +**Test Questions**: +- Are primary actions in the thumb zone (bottom half of screen)? +- Is state preserved if the user leaves and returns? +- Does it work on slow connections (3G)? +- Can forms use autocomplete and smart defaults? +- Are touch targets at least 44×44pt? + +**Red Flags** (report these specifically): +- Important actions positioned at the top of the screen (unreachable by thumb) +- No state persistence; progress lost on tab switch or interruption +- Large text inputs required where selection would work +- Heavy assets loading on every page (no lazy loading) +- Tiny tap targets or targets too close together + +--- + +#### Selecting Personas + +Choose personas based on the interface type: + +| Interface Type | Primary Personas | Why | +|---------------|-----------------|-----| +| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile | +| Dashboard / admin | Alex, Sam | Power users, accessibility | +| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity | +| Onboarding flow | Jordan, Casey | Confusion, interruption | +| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav | +| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile | + +--- + +#### Project-Specific Personas + +If `AGENTS.md` contains a `## Design Context` section (generated by `impeccable init`), derive 1–2 additional personas from the audience and brand information: + +1. Read the target audience description +2. Identify the primary user archetype not covered by the 5 predefined personas +3. Create a persona following this template: + +``` +##### [Role]: "[Name]" + +**Profile**: [2-3 key characteristics derived from Design Context] + +**Behaviors**: [3-4 specific behaviors based on the described audience] + +**Red Flags**: [3-4 things that would alienate this specific user type] +``` + +Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists. diff --git a/.pi/skills/impeccable/reference/document.md b/.pi/skills/impeccable/reference/document.md index e5488c074..60075688a 100644 --- a/.pi/skills/impeccable/reference/document.md +++ b/.pi/skills/impeccable/reference/document.md @@ -45,7 +45,7 @@ Rules that matter: - **Token refs** use `{path.to.token}` (e.g. `{colors.primary}`, `{rounded.md}`). Components may reference primitives; primitives may not reference each other. - **Stitch validates colors as hex sRGB only** (`#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`); OKLCH/HSL/P3 trigger a linter warning, not a hard error. YAML accepts the string either way and our own parser is format-agnostic. Choose based on project posture: (a) if the project has an "OKLCH-only" doctrine or uses Display-P3 values that don't round-trip through sRGB, put OKLCH directly in the frontmatter and accept the Stitch linter warning; (b) if the project wants strict Stitch compliance or plans to use their Tailwind/DTCG export pipeline, put hex in the frontmatter and keep OKLCH in prose as the canonical reference. Never split the source of truth without explicit reason. - **Component sub-tokens** are limited to 8 props: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, `width`. Shadows, motion, focus rings, backdrop-filter: none of those fit. Carry them in the sidecar (Step 4b). -- **Scale keys are open-ended.** Use whatever names the project already uses (`warm-ash-cream`, `surface-container-low`). Don't rename to Material defaults. +- **Scale keys are open-ended.** Use whatever names the project already uses (`oxblood-deep`, `surface-container-low`). Don't rename to Material defaults. - **Variants are naming convention, not schema.** `button-primary` / `button-primary-hover` / `button-primary-active` as sibling keys. ## The markdown body: six sections (exact order) @@ -61,7 +61,7 @@ Optional evocative subtitles are allowed in the form `## 2. Colors: The [Name] P ## When to run -- The user just ran `/impeccable teach` and needs the visual side documented. +- The user just ran `/impeccable init` and needs the visual side documented. - The skill noticed no `DESIGN.md` exists and nudged the user to create one. - An existing `DESIGN.md` is stale (the design has drifted). - Before a large redesign, to capture the current state as a reference. @@ -71,7 +71,7 @@ If a `DESIGN.md` already exists, **do not silently overwrite it**. Show the user ## Two paths - **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 teach, nothing built yet). Interview for five high-level answers, write a minimal DESIGN.md marked ``. Re-run in scan mode once there's code. +- **Seed mode**: the project is pre-implementation (fresh init, nothing built yet). Interview for five high-level answers, 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` forces seed mode regardless of code presence. @@ -103,7 +103,7 @@ Build a structured draft from the discovered tokens. For each token class: From the auto-extracted tokens, draft the YAML frontmatter now (you'll write it at the top of DESIGN.md in Step 4). This is the machine-readable layer: what the live panel and Stitch's linter consume. -- **Colors**: one entry per extracted color. Key = descriptive slug (`warm-ash-cream`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. +- **Colors**: one entry per extracted color. Key = descriptive slug (`oxblood-deep`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. - **Typography**: one entry per role (`display`, `headline`, `title`, `body`, `label`). Typography is an object; include only the props that are real for the project (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`). - **Rounded / Spacing**: whatever scale steps the project actually uses, keyed by whatever scale name the project uses (`sm` / `md` / `lg`, or `surface-sm`, or numeric steps). - **Components**: one entry per variant (`button-primary`, `button-primary-hover`, `button-ghost`). Reference primitives via `{colors.X}`, `{rounded.Y}`. If a variant needs a property Stitch's 8-prop set doesn't cover (shadow, focus ring, backdrop-filter), carry the full snippet in the sidecar instead. @@ -253,7 +253,7 @@ Regenerate the sidecar whenever you regenerate root `DESIGN.md`. If the user onl "extensions": { "colorMeta": { "primary": { "role": "primary", "displayName": "Editorial Magenta", "canonical": "oklch(60% 0.25 350)", "tonalRamp": ["...", "...", "..."] }, - "warm-ash-cream": { "role": "neutral", "displayName": "Warm Ash Cream", "canonical": "oklch(96% 0.005 350)", "tonalRamp": ["...", "...", "..."] } + "cool-paper": { "role": "neutral", "displayName": "Cool Paper", "canonical": "oklch(96% 0.005 230)", "tonalRamp": ["...", "...", "..."] } }, "typographyMeta": { "display": { "displayName": "Display", "purpose": "Hero headlines only." } @@ -328,12 +328,13 @@ Pull directly from the DESIGN.md you just wrote: Do not reword. The panel shows these as secondary collapsible context; the same voice that's in the Markdown carries through. -### Step 5: Confirm, refine, and refresh session cache +### Step 5: Confirm and refine 1. Show the user the full DESIGN.md you wrote. Briefly highlight the non-obvious creative choices (descriptive color names, atmosphere language, named rules). 2. Mention that `.impeccable/design.json` was also written alongside; the live panel will now render this project's actual button/input/nav primitives instead of generic approximations. 3. Offer to refine any section: "Want me to revise a section, add component patterns I missed, or adjust the atmosphere language?" -4. **Refresh the session cache.** Run `node .pi/skills/impeccable/scripts/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. + +Your own write is the freshest source; subsequent commands in this session don't need a reload. ## Seed mode @@ -394,11 +395,12 @@ Per-section guidance in seed mode: 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. -### Step 4: Confirm and refresh session cache +### Step 4: Confirm 1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment). 2. Tell the user: "Re-run `/impeccable document` once you have some code. That pass will extract real tokens and generate the sidecar." -3. Run `node .pi/skills/impeccable/scripts/load-context.mjs` once so the seed lands in conversation for the rest of the session. + +Your own write is the freshest source; no reload needed. ## Style guidelines diff --git a/.pi/skills/impeccable/reference/heuristics-scoring.md b/.pi/skills/impeccable/reference/heuristics-scoring.md deleted file mode 100644 index edbe5028d..000000000 --- a/.pi/skills/impeccable/reference/heuristics-scoring.md +++ /dev/null @@ -1,234 +0,0 @@ -# Heuristics Scoring Guide - -Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough." - -## Nielsen's 10 Heuristics - -### 1. Visibility of System Status - -Keep users informed about what's happening through timely, appropriate feedback. - -**Check for**: -- Loading indicators during async operations -- Confirmation of user actions (save, submit, delete) -- Progress indicators for multi-step processes -- Current location in navigation (breadcrumbs, active states) -- Form validation feedback (inline, not just on submit) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | No feedback; user is guessing what happened | -| 1 | Rare feedback; most actions produce no visible response | -| 2 | Partial; some states communicated, major gaps remain | -| 3 | Good; most operations give clear feedback, minor gaps | -| 4 | Excellent; every action confirms, progress is always visible | - -### 2. Match Between System and Real World - -Speak the user's language. Follow real-world conventions. Information appears in natural, logical order. - -**Check for**: -- Familiar terminology (no unexplained jargon) -- Logical information order matching user expectations -- Recognizable icons and metaphors -- Domain-appropriate language for the target audience -- Natural reading flow (left-to-right, top-to-bottom priority) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Pure tech jargon, alien to users | -| 1 | Mostly confusing; requires domain expertise to navigate | -| 2 | Mixed; some plain language, some jargon leaks through | -| 3 | Mostly natural; occasional term needs context | -| 4 | Speaks the user's language fluently throughout | - -### 3. User Control and Freedom - -Users need a clear "emergency exit" from unwanted states without extended dialogue. - -**Check for**: -- Undo/redo functionality -- Cancel buttons on forms and modals -- Clear navigation back to safety (home, previous) -- Easy way to clear filters, search, selections -- Escape from long or multi-step processes - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Users get trapped; no way out without refreshing | -| 1 | Difficult exits; must find obscure paths to escape | -| 2 | Some exits; main flows have escape, edge cases don't | -| 3 | Good control; users can exit and undo most actions | -| 4 | Full control; undo, cancel, back, and escape everywhere | - -### 4. Consistency and Standards - -Users shouldn't wonder whether different words, situations, or actions mean the same thing. - -**Check for**: -- Consistent terminology throughout the interface -- Same actions produce same results everywhere -- Platform conventions followed (standard UI patterns) -- Visual consistency (colors, typography, spacing, components) -- Consistent interaction patterns (same gesture = same behavior) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Inconsistent everywhere; feels like different products stitched together | -| 1 | Many inconsistencies; similar things look/behave differently | -| 2 | Partially consistent; main flows match, details diverge | -| 3 | Mostly consistent; occasional deviation, nothing confusing | -| 4 | Fully consistent; cohesive system, predictable behavior | - -### 5. Error Prevention - -Better than good error messages is a design that prevents problems in the first place. - -**Check for**: -- Confirmation before destructive actions (delete, overwrite) -- Constraints preventing invalid input (date pickers, dropdowns) -- Smart defaults that reduce errors -- Clear labels that prevent misunderstanding -- Autosave and draft recovery - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Errors easy to make; no guardrails anywhere | -| 1 | Few safeguards; some inputs validated, most aren't | -| 2 | Partial prevention; common errors caught, edge cases slip | -| 3 | Good prevention; most error paths blocked proactively | -| 4 | Excellent; errors nearly impossible through smart constraints | - -### 6. Recognition Rather Than Recall - -Minimize memory load. Make objects, actions, and options visible or easily retrievable. - -**Check for**: -- Visible options (not buried in hidden menus) -- Contextual help when needed (tooltips, inline hints) -- Recent items and history -- Autocomplete and suggestions -- Labels on icons (not icon-only navigation) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Heavy memorization; users must remember paths and commands | -| 1 | Mostly recall; many hidden features, few visible cues | -| 2 | Some aids; main actions visible, secondary features hidden | -| 3 | Good recognition; most things discoverable, few memory demands | -| 4 | Everything discoverable; users never need to memorize | - -### 7. Flexibility and Efficiency of Use - -Accelerators, invisible to novices, speed up expert interaction. - -**Check for**: -- Keyboard shortcuts for common actions -- Customizable interface elements -- Recent items and favorites -- Bulk/batch actions -- Power user features that don't complicate the basics - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | One rigid path; no shortcuts or alternatives | -| 1 | Limited flexibility; few alternatives to the main path | -| 2 | Some shortcuts; basic keyboard support, limited bulk actions | -| 3 | Good accelerators; keyboard nav, some customization | -| 4 | Highly flexible; multiple paths, power features, customizable | - -### 8. Aesthetic and Minimalist Design - -Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose. - -**Check for**: -- Only necessary information visible at each step -- Clear visual hierarchy directing attention -- Purposeful use of color and emphasis -- No decorative clutter competing for attention -- Focused, uncluttered layouts - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Overwhelming; everything competes for attention equally | -| 1 | Cluttered; too much noise, hard to find what matters | -| 2 | Some clutter; main content clear, periphery noisy | -| 3 | Mostly clean; focused design, minor visual noise | -| 4 | Perfectly minimal; every element earns its pixel | - -### 9. Help Users Recognize, Diagnose, and Recover from Errors - -Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution. - -**Check for**: -- Plain language error messages (no error codes for users) -- Specific problem identification ("Email is missing @" not "Invalid input") -- Actionable recovery suggestions -- Errors displayed near the source of the problem -- Non-blocking error handling (don't wipe the form) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Cryptic errors; codes, jargon, or no message at all | -| 1 | Vague errors; "Something went wrong" with no guidance | -| 2 | Clear but unhelpful; names the problem but not the fix | -| 3 | Clear with suggestions; identifies problem and offers next steps | -| 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work | - -### 10. Help and Documentation - -Even if the system is usable without docs, help should be easy to find, task-focused, and concise. - -**Check for**: -- Searchable help or documentation -- Contextual help (tooltips, inline hints, guided tours) -- Task-focused organization (not feature-organized) -- Concise, scannable content -- Easy access without leaving current context - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | No help available anywhere | -| 1 | Help exists but hard to find or irrelevant | -| 2 | Basic help; FAQ or docs exist, not contextual | -| 3 | Good documentation; searchable, mostly task-focused | -| 4 | Excellent contextual help; right info at the right moment | - ---- - -## Score Summary - -**Total possible**: 40 points (10 heuristics × 4 max) - -| Score Range | Rating | What It Means | -|-------------|--------|---------------| -| 36–40 | Excellent | Minor polish only; ship it | -| 28–35 | Good | Address weak areas, solid foundation | -| 20–27 | Acceptable | Significant improvements needed before users are happy | -| 12–19 | Poor | Major UX overhaul required; core experience broken | -| 0–11 | Critical | Redesign needed; unusable in current state | - ---- - -## Issue Severity (P0–P3) - -Tag each individual issue found during scoring with a priority level: - -| Priority | Name | Description | Action | -|----------|------|-------------|--------| -| **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper | -| **P1** | Major | Causes significant difficulty or confusion | Fix before release | -| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass | -| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits | - -**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1. diff --git a/.pi/skills/impeccable/reference/teach.md b/.pi/skills/impeccable/reference/init.md similarity index 58% rename from .pi/skills/impeccable/reference/teach.md rename to .pi/skills/impeccable/reference/init.md index 9da9cffd6..0af9dda3e 100644 --- a/.pi/skills/impeccable/reference/teach.md +++ b/.pi/skills/impeccable/reference/init.md @@ -1,21 +1,16 @@ -# Teach Flow +# Init Flow -Gathers design context for a project and writes two complementary files at the project root: +The setup command for a project. One codebase crawl feeds everything it writes: - **PRODUCT.md** (strategic): root project file for register, target users, product purpose, brand personality, anti-references, strategic design principles. Answers "who/what/why". - **DESIGN.md** (visual): root project file for visual theme, color palette, typography, components, layout. Follows the [Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/). Answers "how it looks". +- **`.impeccable/live/config.json`** (live mode): pre-configured so `/impeccable live` boots straight into variant mode with no first-time detour. -Every other impeccable command reads these files before doing any work. +It closes by pointing the user at the best command to run next. Every other impeccable command reads PRODUCT.md and DESIGN.md before doing any work. ## Step 1: Load current state -Run the shared loader first so you know what already exists: - -```bash -node .pi/skills/impeccable/scripts/load-context.mjs -``` - -The output tells you whether PRODUCT.md and/or DESIGN.md already exist. If `migrated: true`, legacy `.impeccable.md` was auto-renamed to `PRODUCT.md`. Mention this once to the user. +Check what already exists. PRODUCT.md and DESIGN.md live at the project root, or under `.agents/context/` or `docs/` (case-insensitive). Read whichever are present with your native file tool. Also note whether `.impeccable/live/config.json` already exists (Step 6 leaves it untouched if so). Decision tree: - **Neither file exists (empty project or no context yet)**: do Steps 2-4 (write PRODUCT.md), then decide on DESIGN.md based on whether there's code to analyze. @@ -26,14 +21,14 @@ Decision tree: Never silently overwrite an existing file. Always confirm first. -If teach was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete teach, re-run the loader, then resume the original command with the freshly loaded context. For craft, resume into shape next; teach creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief. +If init was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete init, then resume the original command. Your own writes are the freshest source; no reload needed. For craft, resume into shape next; init creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief. ## Step 2: Explore the codebase -Before asking questions, thoroughly scan the project to discover what you can: +Before asking questions, thoroughly scan the project to discover what you can. This single crawl feeds PRODUCT.md, DESIGN.md, **and** the live-mode framework detection in Step 6, so be thorough once rather than re-scanning later: - **README and docs**: Project purpose, target audience, any stated goals -- **Package.json / config files**: Tech stack, dependencies, existing design libraries +- **Package.json / config files**: Tech stack, dependencies, existing design libraries, **and the framework** (Vite/SPA, Next.js, Nuxt, SvelteKit, Astro, multi-page static) plus the HTML entry the browser actually loads - **Existing components**: Current design patterns, spacing, typography in use - **Brand assets**: Logos, favicons, color values already defined - **Design tokens / CSS variables**: Existing color palettes, font stacks, spacing scales @@ -46,7 +41,7 @@ Also form a **register hypothesis** from what you find: Register is a hypothesis at this point, not a decision; Step 3 confirms it. -Note what you've learned and what remains unclear. This exploration feeds both PRODUCT.md and DESIGN.md. +Note what you've learned and what remains unclear. Also note any rough edges worth a follow-up command (thin hierarchy, flat or gray palette, missing error/empty states, dull copy); Step 7 turns these into concrete recommendations without re-analyzing. ## Step 3: Ask strategic questions (for PRODUCT.md) @@ -84,8 +79,7 @@ If the signal is genuinely split (e.g. a product with a big marketing landing), ### Brand & Personality - How would you describe the brand personality in 3 words? - Reference sites or apps that capture the right feel? What specifically about them? - - For brand, push for real-world references in the right lane (tech-minimal, editorial-magazine, consumer-warm, brutalist-grid, etc.), not generic "modern" adjectives. - - For product, push for category best-tool references (Linear, Figma, Notion, Raycast, Stripe). + - Push for specific named references with the *specific* thing about them that fits this brand, not generic "modern" adjectives or category-bucket lanes. - What should this explicitly NOT look like? Any anti-references? ### Accessibility & Inclusion @@ -141,16 +135,38 @@ If the user agrees, delegate to `/impeccable document` (it auto-detects scan vs If the user prefers to skip, mention they can run `/impeccable document` any time later. -## Step 6: Confirm and wrap up +## Step 6: Configure live mode (when code exists) -Summarize: +If the project has code with HTML entries and a dev server (the same "code exists" condition that puts `/impeccable document` in scan mode), pre-configure live mode now. You already identified the framework and the served HTML entry in Step 2, so this is nearly free, and it spares the user the first-time setup detour when they later run `/impeccable live`. + +**Skip this step for empty / pre-implementation projects** (nothing to inject into yet). Tell the user live mode will configure itself the first time they run it once there's code. + +**If `.impeccable/live/config.json` already exists, leave it untouched** and note that live mode is already configured. + +Otherwise: + +1. Write `.impeccable/live/config.json`. Choose `files` (the HTML entries the browser actually loads), `insertBefore`, and `commentSyntax` from the framework table in [live.md](live.md)'s **First-time setup** section, using the framework you found in Step 2. That table is canonical; do not restate it here. For multi-page static sites, prefer a glob (`["public/**/*.html"]`) over a literal list. +2. Run `node .pi/skills/impeccable/scripts/detect-csp.mjs`. If it reports a patchable shape (`append-arrays` / `append-string`), use the **consent prompt template** from live.md before editing any source file. On decline, skip the patch. For `middleware` / `meta-tag` shapes, surface the detected files and ask the user to add `http://localhost:8400` to `script-src` and `connect-src` manually. For `null`, there's nothing to do. +3. Set `cspChecked: true` in the config once CSP is handled (patched, declined, manual, or not needed). The schema and per-shape patch details live in live.md's First-time setup; follow it rather than duplicating. + +Writing the config file is harmless and needs no consent; only the CSP **source-file patch** requires a yes. + +## Step 7: Recommend starting points, then wrap up + +Summarize tersely: - Register captured (brand / product) -- What was written (PRODUCT.md, DESIGN.md, or both) +- What was written (PRODUCT.md, DESIGN.md, live config, or a subset) - The 3-5 strategic principles from PRODUCT.md that will guide future work -- If DESIGN.md is pending, remind the user how to generate it later +- If DESIGN.md or live config is pending, one line on how to set it up later -**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node .pi/skills/impeccable/scripts/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. +Then recommend the **best commands to run next**, drawn from what your Step 2 crawl already surfaced. Do not run a fresh analysis here; surface observations you already have. Tailor to register and to what you saw, offer the 2-4 most relevant (not a menu dump), and give the exact command to type. Group by intent: -If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context. +- **Build something new**: `/impeccable craft ` (shape, then build end-to-end) or `/impeccable shape ` (plan first). Lead with this for empty or early-stage projects. +- **Improve what's there**: name the specific surface. `/impeccable critique ` for a scored UX review; `/impeccable audit ` for a11y / perf / responsive checks; `/impeccable polish ` for a pre-ship pass. When the crawl flagged a specific weakness, point the matching command at it: thin hierarchy or spacing → `layout`, flat or gray palette → `colorize`, missing error / empty states → `harden` or `onboard`, dull or unclear copy → `clarify`. +- **Iterate visually**: `/impeccable live` (configured in Step 6) to pick elements in the browser and generate variants in place. + +The full command menu is one bare `/impeccable` away; keep this list short and pointed. + +If init was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now. Your own writes are the freshest source; no reload needed. Optionally ask the user directly to clarify what you cannot infer. Ask whether they'd like a brief summary of PRODUCT.md appended to AGENTS.md for easier agent reference. If yes, append a short **Design Context** pointer section there. diff --git a/.pi/skills/impeccable/reference/interaction-design.md b/.pi/skills/impeccable/reference/interaction-design.md index 15aed5b29..c42c9d810 100644 --- a/.pi/skills/impeccable/reference/interaction-design.md +++ b/.pi/skills/impeccable/reference/interaction-design.md @@ -150,12 +150,6 @@ For browsers without anchor positioning support, `position: fixed` with manual c Check viewport boundaries before rendering. If the dropdown would overflow the bottom edge, flip it above the trigger. If it would overflow the right edge, align it to the trigger's right side instead. -### Anti-Patterns - -- **`position: absolute` inside `overflow: hidden`** - The dropdown will be clipped. Use `position: fixed` or the top layer instead. -- **Arbitrary z-index values** like `z-index: 9999` - Use a semantic z-index scale: `dropdown (100) -> sticky (200) -> modal-backdrop (300) -> modal (400) -> toast (500) -> tooltip (600)`. -- **Rendering dropdown markup inline** without an escape hatch from the parent's stacking context. Either use `popover` (top layer), a portal, or `position: fixed`. - ## Destructive Actions: Undo > Confirm **Undo is better than confirmation dialogs.** Users click through confirmations mindlessly. Remove from UI immediately, show undo toast, actually delete after toast expires. Use confirmation only for truly irreversible actions (account deletion), high-cost actions, or batch operations. diff --git a/.pi/skills/impeccable/reference/layout.md b/.pi/skills/impeccable/reference/layout.md index cad4c62a1..8765978b0 100644 --- a/.pi/skills/impeccable/reference/layout.md +++ b/.pi/skills/impeccable/reference/layout.md @@ -1,4 +1,4 @@ -Space is the most underused design tool. Find the layout's actual problem (monotone spacing, weak hierarchy, identical card grids, the centered-stack default) and fix the structure, not the surface. +Space is the most underused design tool. Find the layout's actual problem (monotone spacing, weak hierarchy, identical card grids) and fix the structure, not the surface. --- @@ -27,7 +27,6 @@ Analyze what's weak about the current spatial design: 3. **Grid & structure**: - Is there a clear underlying structure, or does the layout feel random? - Are identical card grids used everywhere? (Icon + heading + text, repeated endlessly) - - Is everything centered? (Left-aligned with asymmetric layouts feels more designed, but not a hard and fast rule) 4. **Rhythm & variety**: - Does the layout have visual rhythm? (Alternating tight/generous spacing) @@ -43,8 +42,6 @@ Analyze what's weak about the current spatial design: ## Plan Layout Improvements -Consult the [spatial design reference](spatial-design.md) for detailed guidance on grids, rhythm, and container queries. - Create a systematic plan: - **Spacing system**: Use a consistent scale (a framework's built-in scale like Tailwind's, rem-based tokens, or a custom system). The specific values matter less than consistency. @@ -57,6 +54,7 @@ Create a systematic plan: ### Establish a Spacing System - Use a consistent spacing scale (framework scales like Tailwind, rem-based tokens, or a custom scale all work). What matters is that values come from a defined set, not arbitrary numbers. +- Prefer a 4pt base scale (4, 8, 12, 16, 24, 32, 48, 64, 96px) over 8pt; 8pt is too coarse and you'll frequently need 12px between 8 and 16. - Name tokens semantically if using custom properties: `--space-xs` through `--space-xl`, not `--spacing-8` - Use `gap` for sibling spacing instead of margins; eliminates margin collapse hacks - Apply `clamp()` for fluid spacing that breathes on larger screens @@ -66,15 +64,22 @@ Create a systematic plan: - **Tight grouping** for related elements (8-12px between siblings) - **Generous separation** between distinct sections (48-96px) - **Varied spacing** within sections (not every row needs the same gap) -- **Asymmetric compositions**: break the predictable centered-content pattern when it makes sense +- **Asymmetric compositions**: a deliberate choice when the content invites it (not a default to chase). ### Choose the Right Layout Tool -- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. Flex is simpler and more appropriate for the majority of layout tasks. +- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. - **Use Grid for 2D layouts**: Page-level structure, dashboards, data-dense interfaces, anything where rows AND columns need coordinated control. -- **Don't default to Grid** when Flexbox with `flex-wrap` would be simpler and more flexible. -- Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. - Use named grid areas (`grid-template-areas`) for complex page layouts; redefine at breakpoints. +- Use **container queries** for components, viewport queries for page layouts. A card in a narrow sidebar can stay compact while the same card in a main content area expands automatically: + +```css +.card-container { container-type: inline-size; } +.card { display: grid; gap: var(--space-md); } +@container (min-width: 400px) { + .card { grid-template-columns: 120px 1fr; } +} +``` ### Break Card Grid Monotony @@ -85,18 +90,36 @@ Create a systematic plan: ### Strengthen Visual Hierarchy - Use the fewest dimensions needed for clear hierarchy. Space alone can be enough; generous whitespace around an element draws the eye. Some of the most polished designs achieve rhythm with just space and weight. Add color or size contrast only when simpler means aren't sufficient. +- The best hierarchy combines 2–3 dimensions at once. A heading that's larger, bolder, AND has more space above it reads as primary without trying: + +| Tool | Strong Hierarchy | Weak Hierarchy | +|------|------------------|----------------| +| **Size** | 3:1 ratio or more | <2:1 ratio | +| **Weight** | Bold vs Regular | Medium vs Regular | +| **Color** | High contrast | Similar tones | +| **Position** | Top/left (primary) | Bottom/right | +| **Space** | Surrounded by white space | Crowded | + - Be aware of reading flow: in LTR languages, the eye naturally scans top-left to bottom-right, but primary action placement depends on context (e.g., bottom-right in dialogs, top in navigation). - Create clear content groupings through proximity and separation. ### Manage Depth & Elevation -- Create a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip) - Build a consistent shadow scale (sm → md → lg → xl); shadows should be subtle - Use elevation to reinforce hierarchy, not as decoration ### Optical Adjustments - If an icon looks visually off-center despite being geometrically centered, nudge it. But only if you're confident it actually looks wrong. Don't adjust speculatively. +- Text at `margin-left: 0` looks slightly indented because of letterform whitespace; a negative margin (`-0.05em`) optically aligns it. Geometrically centered glyphs often look off-center (play icons need to shift right, arrows shift toward their direction). +- Touch targets must be 44×44px minimum even when the visual element is smaller. Expand the hit area with padding or a pseudo-element: + +```css +.icon-button { width: 24px; height: 24px; position: relative; } +.icon-button::before { + content: ''; position: absolute; inset: -10px; +} +``` **NEVER**: - Use arbitrary spacing values outside your scale @@ -104,10 +127,7 @@ Create a systematic plan: - Wrap everything in cards (not everything needs a container) - Nest cards inside cards (use spacing and dividers for hierarchy within) - Use identical card grids everywhere (icon + heading + text, repeated) -- Center everything (left-aligned with asymmetry feels more designed) - Default to the hero metric layout (big number, small label, stats, gradient) as a template. If showing real user data, a prominent metric can work, but it should display actual data, not decorative numbers. -- Default to CSS Grid when Flexbox would be simpler; use the simplest tool for the job -- Use arbitrary z-index values (999, 9999); build a semantic scale ## Verify Layout Improvements diff --git a/.pi/skills/impeccable/reference/live.md b/.pi/skills/impeccable/reference/live.md index 50ce9e2bd..e8cd861f3 100644 --- a/.pi/skills/impeccable/reference/live.md +++ b/.pi/skills/impeccable/reference/live.md @@ -9,18 +9,21 @@ A running dev server with hot module replacement (Vite, Next.js, Bun, etc.), OR Execute in order. No step skipped, no step reordered. 1. `live.mjs`: boot. -2. Navigate to the URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). If you can't infer it confidently, tell the user once to open their dev/preview URL. Never use `serverPort` as that URL; it's the helper, not the app. +2. Open the app URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). Never use `serverPort`; it's the helper, not the app. **Cursor:** `browser_navigate` to that URL before polling; do not skip. **Other harnesses:** use the available browser tool; if the URL is uncertain, ask the user once. 3. Poll loop with the default long timeout (600000 ms). After every event or `--reply`, run `live-poll.mjs` again immediately. Never pass a short `--timeout=`. + +The global bar **Impeccable mark** dims and shows a pulsing amber dot when no agent is long-polling `/poll`. Hover the mark for the hint; restart `live-poll.mjs` to reconnect. 4. On `generate`: read screenshot if present; load the action's reference; plan three distinct directions; write all variants in one edit; `--reply done`; poll again. -5. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts remain recoverable until you finish cleanup, run `live-complete.mjs --id EVENT_ID`, and only then poll again. -6. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart. -7. On `exit`: run the cleanup at the bottom. +5. On `steer`: read the message and `pageUrl`; do the work (page edits, navigation help, or a short reply in the `--reply` message); `--reply steer_done`; poll again. No pickup ack. The Steer bar unlocks when `steer_done` arrives over SSE. +6. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts remain recoverable until you finish cleanup, run `live-complete.mjs --id EVENT_ID`, and only then poll again. +7. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart. +8. On `exit`: run the cleanup at the bottom. Harness policy: - **Claude Code**: run the poll as a **background task** (no short timeout). The harness notifies you when it completes, so the main conversation stays free. Do not block the shell. -- **Cursor**: run the poll in the **foreground** (blocking shell; not a background terminal, not a subagent). Cursor background terminals and subagents do not reliably resume the chat with poll stdout. +- **Cursor**: run **one-shot** poll in a **background terminal** with notify on `"type":"(steer|generate|accept|discard|exit)"`. After each event the poll exits; handle it, `--reply`, then start `live-poll.mjs` again. Do **not** use `--stream` on Cursor: incremental stdout notify is slower in practice than exit-based notify (~5s vs sub-second in testing). - **Codex**: run the poll in the **foreground** (blocking shell; not a background task, not a subagent). Codex background exec sessions do not reliably surface poll stdout back into the conversation at the moment events arrive, so a "fire-and-forget" background poll will stall live mode. -- **Other harnesses**: foreground unless you know stdout reliably returns to this session. +- **Other harnesses**: one-shot foreground unless you know stdout reliably returns to this session when a shell exits. Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodies. Spend tokens on tools and edits; on failure, one or two short sentences. @@ -30,7 +33,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi node .pi/skills/impeccable/scripts/live.mjs ``` -Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation; **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md. +Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation; **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. `serverPort` and `serverToken` belong to the small **Impeccable live helper** HTTP server (serves `/live.js`, SSE, and `/poll`). That port is **not** your dev server and is usually not the URL you open to view the app. The browser page is whatever origin serves one of the `pageFiles` entries (Vite / Next / Bun / tunnel / LAN hostname). @@ -38,12 +41,15 @@ If output is `{ ok: false, error: "config_missing" | "config_invalid", path }`, ## Poll loop +**Default (portable, all harnesses):** + ``` LOOP: node .pi/skills/impeccable/scripts/live-poll.mjs # default long timeout; no --timeout= Read JSON; dispatch on "type" "generate" → Handle Generate; reply done; LOOP + "steer" → Handle Steer; reply steer_done; LOOP "accept" → Handle Accept; complete carbonize cleanup if required; LOOP "discard" → Handle Discard; LOOP "prefetch" → Handle Prefetch; LOOP @@ -51,6 +57,16 @@ LOOP: "exit" → break → Cleanup ``` +**Stream mode (experimental, not for Cursor):** + +``` +node .pi/skills/impeccable/scripts/live-poll.mjs --stream # stays running; one JSON line per event + Handle event; run --reply in a separate command + Repeat until "exit" line → Cleanup +``` + +Stream keeps one process alive and waits for `--reply` ack before polling again. Useful only when the harness reads incremental stdout reliably and quickly. **Cursor is not one of those:** background pattern notify on a long-running shell was ~5s to pick up events vs sub-second for one-shot exit notify. Default to one-shot everywhere unless you have measured otherwise. + ## Recovery commands The live helper persists an append-only journal under `.impeccable/live/sessions/`. Browser checkpoints are advisory but durable; the journal is canonical. This is local durable recovery state, not project source. @@ -71,9 +87,32 @@ Server restart rule: start `live-server.mjs` again, then poll. Startup requeues ## Handle `generate` -Event: `{id, action, freeformPrompt?, count, pageUrl, element, screenshotPath?, comments?, strokes?}`. +**Replace mode** (default): `{id, action, freeformPrompt?, count, pageUrl, element, screenshotPath?, comments?, strokes?}`. -Speed matters; the user is watching a spinner. Minimize tool calls by using the `wrap` helper and writing all variants in a single edit. +**Insert mode** (`event.mode === "insert"`): `{id, mode: "insert", count, pageUrl, insert: { position, anchor }, placeholder: { width, height }, freeformPrompt?, screenshotPath?, comments?, strokes?}`. No `action`. Requires a non-empty `freeformPrompt` **or** annotations. Screenshot is sent only when annotations exist (same rule as replace). Use `placeholder` dimensions as a soft size hint for net-new content. + +Speed matters; the user is watching a spinner. Minimize tool calls by using the wrap/insert helper and writing all variants in a single edit. + +### Insert mode branch + +When `event.mode === "insert"`: + +1. Read the screenshot if `event.screenshotPath` is present (annotations only). +2. Run the insert helper instead of wrap: + +```bash +node .pi/skills/impeccable/scripts/live-insert.mjs --id EVENT_ID --count EVENT_COUNT --position after \ + --element-id "ANCHOR_ID" --classes "class1,class2" --tag "section" --text "ANCHOR_TEXT" +``` + +- `--position` ← `event.insert.position` (`before` | `after`) +- Anchor flags ← `event.insert.anchor` (same mapping as wrap: id, classes, tag, text) + +The scaffold has **no** `data-impeccable-variant="original"`. Variants are net-new HTML+CSS inserted at `insertLine`. Load `brand.md` or `product.md` (freeform only, no action sub-command). Write all variants in one edit, then `--reply done`. + +On accept/discard, `live-accept.mjs` removes the wrapper block; the anchor element is untouched. + +### Replace mode (default) ### 1. Read the screenshot (if present) @@ -424,6 +463,28 @@ A background agent may be used for the rewrite, but the current thread is respon Event: `{id, _acceptResult, _completionAck}`. The poll script already restored the original, removed all variant markers, and acknowledged `discarded` durable completion. Nothing to do unless `_completionAck.ok !== true`; in that case run `live-complete.mjs --id EVENT_ID --discarded`, then poll again. +## Handle `steer` + +Event: `{id, message, pageUrl}`. The user typed or spoke into the global bar **Steer** control: page-level direction without picking an element or launching variant generation. + +The mic button uses the browser **Web Speech API** (MVP): click to start, speak, stop automatically when the utterance ends, then the transcript submits as a steer event. Click again while listening to cancel without submitting. + +This is lighter than `generate`: no screenshot, no element context, no variant cycling. Read `message` and inspect the live page or project files as needed, then either make edits or answer in prose. + +When finished: + +```bash +node .pi/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID steer_done ["Optional short note for a browser toast"] +``` + +On failure: + +```bash +node .pi/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID error "Short reason" +``` + +Then poll again immediately. Do not send a separate "picked up" reply. The Steer bar stays locked until `steer_done` or `error` arrives over SSE. + ## Handle `prefetch` Event: `{pageUrl}`. The browser fires this the first time the user selects an element on a given route, as a latency shortcut; it signals the user is likely about to Go on a page you haven't read yet. diff --git a/.pi/skills/impeccable/reference/motion-design.md b/.pi/skills/impeccable/reference/motion-design.md deleted file mode 100644 index 78b6fd260..000000000 --- a/.pi/skills/impeccable/reference/motion-design.md +++ /dev/null @@ -1,109 +0,0 @@ -# Motion Design - -## Duration: The 100/300/500 Rule - -Timing matters more than easing. These durations feel right for most UI: - -| Duration | Use Case | Examples | -|----------|----------|----------| -| **100-150ms** | Instant feedback | Button press, toggle, color change | -| **200-300ms** | State changes | Menu open, tooltip, hover states | -| **300-500ms** | Layout changes | Accordion, modal, drawer | -| **500-800ms** | Entrance animations | Page load, hero reveals | - -**Exit animations are faster than entrances.** Use ~75% of enter duration. - -## Easing: Pick the Right Curve - -**Don't use `ease`.** It's a compromise that's rarely optimal. Instead: - -| Curve | Use For | CSS | -|-------|---------|-----| -| **ease-out** | Elements entering | `cubic-bezier(0.16, 1, 0.3, 1)` | -| **ease-in** | Elements leaving | `cubic-bezier(0.7, 0, 0.84, 0)` | -| **ease-in-out** | State toggles (there → back) | `cubic-bezier(0.65, 0, 0.35, 1)` | - -**For micro-interactions, use exponential curves.** They feel natural because they mimic real physics (friction, deceleration): - -```css -/* Quart out - smooth, refined (recommended default) */ ---ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1); - -/* Quint out - slightly more dramatic */ ---ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1); - -/* Expo out - snappy, confident */ ---ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); -``` - -**Avoid bounce and elastic curves.** They were trendy in 2015 but now feel tacky and amateurish. Real objects don't bounce when they stop; they decelerate smoothly. Overshoot effects draw attention to the animation itself rather than the content. - -## Premium Motion Materials - -Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties: blur reveals, backdrop-filter panels, saturation or brightness shifts, shadow bloom, SVG filters, masks, clip paths, gradient-position movement, and variable font or shader-driven effects. - -Use the right material for the effect: - -- **Transform / opacity**: movement, press feedback, simple reveals, list choreography. -- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances, atmospheric transitions. -- **Clip path / masks**: wipes, reveals, editorial cropping, product-like transitions. -- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state. -- **Grid-template rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly. - -The hard rule is not "transform and opacity only." The hard rule is: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify in-browser that the result is smooth on the target viewports. If blur/filter makes the interaction feel significantly more premium and remains smooth, use it. - -## Staggered Animations - -Use CSS custom properties for cleaner stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"` on each item. **Cap total stagger time**: 10 items at 50ms = 500ms total. For many items, reduce per-item delay or cap staggered count. - -## Reduced Motion - -This is not optional. Vestibular disorders affect ~35% of adults over 40. - -```css -/* Define animations normally */ -.card { - animation: slide-up 500ms ease-out; -} - -/* Provide alternative for reduced motion */ -@media (prefers-reduced-motion: reduce) { - .card { - animation: fade-in 200ms ease-out; /* Crossfade instead of motion */ - } -} - -/* Or disable entirely */ -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - transition-duration: 0.01ms !important; - } -} -``` - -**What to preserve**: Functional animations like progress bars, loading spinners (slowed down), and focus indicators should still work, just without spatial movement. - -## Perceived Performance - -**Nobody cares how fast your site is, just how fast it feels.** Perception can be as effective as actual performance. - -**The 80ms threshold**: Our brains buffer sensory input for ~80ms to synchronize perception. Anything under 80ms feels instant and simultaneous. This is your target for micro-interactions. - -**Active vs passive time**: Passive waiting (staring at a spinner) feels longer than active engagement. Strategies to shift the balance: - -- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening. -- **Early completion**: Show content progressively, don't wait for everything. Video buffering, progressive images, streaming HTML. -- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Instagram likes work offline; the UI updates instantly, syncs later. Use for low-stakes actions; avoid for payments or destructive operations. - -**Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances, but ease-in toward a task's end compresses perceived time. - -**Caution**: Too-fast responses can decrease perceived value. Users may distrust instant results for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening. - -## Performance - -Don't use `will-change` preemptively, only when animation is imminent (`:hover`, `.animating`). For scroll-triggered animations, use Intersection Observer instead of scroll events; unobserve after animating once. Create motion tokens for consistency (durations, easings, common transitions). - ---- - -**Avoid**: Animating everything (animation fatigue is real). Using >500ms for UI feedback. Ignoring `prefers-reduced-motion`. Using animation to hide slow loading. diff --git a/.pi/skills/impeccable/reference/personas.md b/.pi/skills/impeccable/reference/personas.md deleted file mode 100644 index 43ae8eb2f..000000000 --- a/.pi/skills/impeccable/reference/personas.md +++ /dev/null @@ -1,179 +0,0 @@ -# Persona-Based Design Testing - -Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss. - -**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns. - ---- - -## 1. Impatient Power User: "Alex" - - -**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave. - -**Behaviors**: -- Skips all onboarding and instructions -- Looks for keyboard shortcuts immediately -- Tries to bulk-select, batch-edit, and automate -- Gets frustrated by required steps that feel unnecessary -- Abandons if anything feels slow or patronizing - -**Test Questions**: -- Can Alex complete the core task in under 60 seconds? -- Are there keyboard shortcuts for common actions? -- Can onboarding be skipped entirely? -- Do modals have keyboard dismiss (Esc)? -- Is there a "power user" path (shortcuts, bulk actions)? - -**Red Flags** (report these specifically): -- Forced tutorials or unskippable onboarding -- No keyboard navigation for primary actions -- Slow animations that can't be skipped -- One-item-at-a-time workflows where batch would be natural -- Redundant confirmation steps for low-risk actions - ---- - -## 2. Confused First-Timer: "Jordan" - -**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out. - -**Behaviors**: -- Reads all instructions carefully -- Hesitates before clicking anything unfamiliar -- Looks for help or support constantly -- Misunderstands jargon and abbreviations -- Takes the most literal interpretation of any label - -**Test Questions**: -- Is the first action obviously clear within 5 seconds? -- Are all icons labeled with text? -- Is there contextual help at decision points? -- Does terminology assume prior knowledge? -- Is there a clear "back" or "undo" at every step? - -**Red Flags** (report these specifically): -- Icon-only navigation with no labels -- Technical jargon without explanation -- No visible help option or guidance -- Ambiguous next steps after completing an action -- No confirmation that an action succeeded - ---- - -## 3. Accessibility-Dependent User: "Sam" - -**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences. - -**Behaviors**: -- Tabs through the interface linearly -- Relies on ARIA labels and heading structure -- Cannot see hover states or visual-only indicators -- Needs adequate color contrast (4.5:1 minimum) -- May use browser zoom up to 200% - -**Test Questions**: -- Can the entire primary flow be completed keyboard-only? -- Are all interactive elements focusable with visible focus indicators? -- Do images have meaningful alt text? -- Is color contrast WCAG AA compliant (4.5:1 for text)? -- Does the screen reader announce state changes (loading, success, errors)? - -**Red Flags** (report these specifically): -- Click-only interactions with no keyboard alternative -- Missing or invisible focus indicators -- Meaning conveyed by color alone (red = error, green = success) -- Unlabeled form fields or buttons -- Time-limited actions without extension option -- Custom components that break screen reader flow - ---- - -## 4. Deliberate Stress Tester: "Riley" - -**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience. - -**Behaviors**: -- Tests edge cases intentionally (empty states, long strings, special characters) -- Submits forms with unexpected data (emoji, RTL text, very long values) -- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs -- Looks for inconsistencies between what the UI promises and what actually happens -- Documents problems methodically - -**Test Questions**: -- What happens at the edges (0 items, 1000 items, very long text)? -- Do error states recover gracefully or leave the UI in a broken state? -- What happens on refresh mid-workflow? Is state preserved? -- Are there features that appear to work but produce broken results? -- How does the UI handle unexpected input (emoji, special chars, paste from Excel)? - -**Red Flags** (report these specifically): -- Features that appear to work but silently fail or produce wrong results -- Error handling that exposes technical details or leaves UI in a broken state -- Empty states that show nothing useful ("No results" with no guidance) -- Workflows that lose user data on refresh or navigation -- Inconsistent behavior between similar interactions in different parts of the UI - ---- - -## 5. Distracted Mobile User: "Casey" - -**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection. - -**Behaviors**: -- Uses thumb only; prefers bottom-of-screen actions -- Gets interrupted mid-flow and returns later -- Switches between apps frequently -- Has limited attention span and low patience -- Types as little as possible, prefers taps and selections - -**Test Questions**: -- Are primary actions in the thumb zone (bottom half of screen)? -- Is state preserved if the user leaves and returns? -- Does it work on slow connections (3G)? -- Can forms use autocomplete and smart defaults? -- Are touch targets at least 44×44pt? - -**Red Flags** (report these specifically): -- Important actions positioned at the top of the screen (unreachable by thumb) -- No state persistence; progress lost on tab switch or interruption -- Large text inputs required where selection would work -- Heavy assets loading on every page (no lazy loading) -- Tiny tap targets or targets too close together - ---- - -## Selecting Personas - -Choose personas based on the interface type: - -| Interface Type | Primary Personas | Why | -|---------------|-----------------|-----| -| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile | -| Dashboard / admin | Alex, Sam | Power users, accessibility | -| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity | -| Onboarding flow | Jordan, Casey | Confusion, interruption | -| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav | -| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile | - ---- - -## Project-Specific Personas - -If `AGENTS.md` contains a `## Design Context` section (generated by `impeccable teach`), derive 1–2 additional personas from the audience and brand information: - -1. Read the target audience description -2. Identify the primary user archetype not covered by the 5 predefined personas -3. Create a persona following this template: - -``` -### [Role]: "[Name]" - -**Profile**: [2-3 key characteristics derived from Design Context] - -**Behaviors**: [3-4 specific behaviors based on the described audience] - -**Red Flags**: [3-4 things that would alienate this specific user type] -``` - -Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists. diff --git a/.pi/skills/impeccable/reference/polish.md b/.pi/skills/impeccable/reference/polish.md index 63a0f4bb0..11b5ad91e 100644 --- a/.pi/skills/impeccable/reference/polish.md +++ b/.pi/skills/impeccable/reference/polish.md @@ -91,7 +91,6 @@ Visual polish on a misshapen flow is wasted work. Match the *shape* of the exper - **Theme consistency**: Works in all theme variants - **Color meaning**: Same colors mean same things throughout - **Accessible focus**: Focus indicators visible with sufficient contrast -- **Tinted neutrals**: No pure gray or pure black; add subtle color tint (0.01 chroma) - **Gray on color**: Never put gray text on colored backgrounds; use a shade of that color or transparency ### Interaction States diff --git a/.pi/skills/impeccable/reference/product.md b/.pi/skills/impeccable/reference/product.md index 64b3b0169..2ca462350 100644 --- a/.pi/skills/impeccable/reference/product.md +++ b/.pi/skills/impeccable/reference/product.md @@ -10,7 +10,6 @@ Product UI's failure mode isn't flatness, it's strangeness without purpose: over ## Typography -- **System fonts are legitimate.** `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif` gives you native feel on every platform. Inter is the common cross-platform default for a reason. - **One family is often right.** Product UIs don't need display/body pairing. A well-tuned sans carries headings, buttons, labels, body, data. - **Fixed rem scale, not fluid.** Clamp-sized headings don't serve product UI. Users view at consistent DPI, and a fluid h1 that shrinks in a sidebar looks worse, not better. - **Tighter scale ratio.** 1.125–1.2 between steps is typical. More type elements here than on brand surfaces; exaggerated contrast creates noise. @@ -26,8 +25,6 @@ Product defaults to Restrained. A single surface can earn Committed (a dashboard ## Layout -- Predictable grids. Consistency IS an affordance; users navigate faster when the structure is expected. -- Familiar patterns are features. Standard navigation (top bar, side nav), breadcrumbs, tabs, and form layouts have established user expectations. Don't reinvent for flavor. - Responsive behavior is structural (collapse sidebar, responsive table, breakpoint-driven columns), not fluid typography. ## Components @@ -51,6 +48,7 @@ Every interactive component has: default, hover, focus, active, disabled, loadin - Display fonts in UI labels, buttons, data. - Reinventing standard affordances for flavor (custom scrollbars, weird form controls, non-standard modals). - Heavy color or full-saturation accents on inactive states. +- Modal as first thought. Modals are usually laziness. Exhaust inline / progressive alternatives first. ## Product permissions diff --git a/.pi/skills/impeccable/reference/responsive-design.md b/.pi/skills/impeccable/reference/responsive-design.md deleted file mode 100644 index f079e5e5d..000000000 --- a/.pi/skills/impeccable/reference/responsive-design.md +++ /dev/null @@ -1,114 +0,0 @@ -# Responsive Design - -## Mobile-First: Write It Right - -Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first. - -## Breakpoints: Content-Driven - -Don't chase device sizes; let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints. - -## Detect Input Method, Not Just Screen Size - -**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard. Use pointer and hover queries: - -```css -/* Fine pointer (mouse, trackpad) */ -@media (pointer: fine) { - .button { padding: 8px 16px; } -} - -/* Coarse pointer (touch, stylus) */ -@media (pointer: coarse) { - .button { padding: 12px 20px; } /* Larger touch target */ -} - -/* Device supports hover */ -@media (hover: hover) { - .card:hover { transform: translateY(-2px); } -} - -/* Device doesn't support hover (touch) */ -@media (hover: none) { - .card { /* No hover state - use active instead */ } -} -``` - -**Critical**: Don't rely on hover for functionality. Touch users can't hover. - -## Safe Areas: Handle the Notch - -Modern phones have notches, rounded corners, and home indicators. Use `env()`: - -```css -body { - padding-top: env(safe-area-inset-top); - padding-bottom: env(safe-area-inset-bottom); - padding-left: env(safe-area-inset-left); - padding-right: env(safe-area-inset-right); -} - -/* With fallback */ -.footer { - padding-bottom: max(1rem, env(safe-area-inset-bottom)); -} -``` - -**Enable viewport-fit** in your meta tag: -```html - -``` - -## Responsive Images: Get It Right - -### srcset with Width Descriptors - -```html -Hero image -``` - -**How it works**: -- `srcset` lists available images with their actual widths (`w` descriptors) -- `sizes` tells the browser how wide the image will display -- Browser picks the best file based on viewport width AND device pixel ratio - -### Picture Element for Art Direction - -When you need different crops/compositions (not just resolutions): - -```html - - - - ... - -``` - -## Layout Adaptation Patterns - -**Navigation**: Three stages: hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `
/` for content that can collapse on mobile. - -## Testing: Don't Trust DevTools Alone - -DevTools device emulation is useful for layout but misses: - -- Actual touch interactions -- Real CPU/memory constraints -- Network latency patterns -- Font rendering differences -- Browser chrome/keyboard appearances - -**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators. - ---- - -**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful. diff --git a/.pi/skills/impeccable/reference/shape.md b/.pi/skills/impeccable/reference/shape.md index f7c4cd2f1..592def8a7 100644 --- a/.pi/skills/impeccable/reference/shape.md +++ b/.pi/skills/impeccable/reference/shape.md @@ -151,7 +151,7 @@ How users interact with this feature. What happens on click, hover, scroll? What 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., spatial-design.md for complex layouts, motion-design.md for animated features, interaction-design.md for form-heavy features). +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. diff --git a/.pi/skills/impeccable/reference/spatial-design.md b/.pi/skills/impeccable/reference/spatial-design.md deleted file mode 100644 index 4d4b83afc..000000000 --- a/.pi/skills/impeccable/reference/spatial-design.md +++ /dev/null @@ -1,100 +0,0 @@ -# Spatial Design - -## Spacing Systems - -### Use 4pt Base, Not 8pt - -8pt systems are too coarse; you'll frequently need 12px (between 8 and 16). Use 4pt for granularity: 4, 8, 12, 16, 24, 32, 48, 64, 96px. - -### Name Tokens Semantically - -Name by relationship (`--space-sm`, `--space-lg`), not value (`--spacing-8`). Use `gap` instead of margins for sibling spacing; it eliminates margin collapse and cleanup hacks. - -## Grid Systems - -### The Self-Adjusting Grid - -Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. Columns are at least 280px, as many as fit per row, leftovers stretch. For complex layouts, use named grid areas (`grid-template-areas`) and redefine them at breakpoints. - -## Visual Hierarchy - -### The Squint Test - -Blur your eyes (or screenshot and blur). Can you still identify: -- The most important element? -- The second most important? -- Clear groupings? - -If everything looks the same weight blurred, you have a hierarchy problem. - -### Hierarchy Through Multiple Dimensions - -Don't rely on size alone. Combine: - -| Tool | Strong Hierarchy | Weak Hierarchy | -|------|------------------|----------------| -| **Size** | 3:1 ratio or more | <2:1 ratio | -| **Weight** | Bold vs Regular | Medium vs Regular | -| **Color** | High contrast | Similar tones | -| **Position** | Top/left (primary) | Bottom/right | -| **Space** | Surrounded by white space | Crowded | - -**The best hierarchy uses 2-3 dimensions at once**: A heading that's larger, bolder, AND has more space above it. - -### Cards Are Not Required - -Cards are overused. Spacing and alignment create visual grouping naturally. Use cards only when content is truly distinct and actionable, items need visual comparison in a grid, or content needs clear interaction boundaries. **Never nest cards inside cards.** Use spacing, typography, and subtle dividers for hierarchy within a card. - -## Container Queries - -Viewport queries are for page layouts. **Container queries are for components**: - -```css -.card-container { - container-type: inline-size; -} - -.card { - display: grid; - gap: var(--space-md); -} - -/* Card layout changes based on its container, not viewport */ -@container (min-width: 400px) { - .card { - grid-template-columns: 120px 1fr; - } -} -``` - -**Why this matters**: A card in a narrow sidebar stays compact, while the same card in a main content area expands automatically, without viewport hacks. - -## Optical Adjustments - -Text at `margin-left: 0` looks indented due to letterform whitespace; use negative margin (`-0.05em`) to optically align. Geometrically centered icons often look off-center; play icons need to shift right, arrows shift toward their direction. - -### Touch Targets vs Visual Size - -Buttons can look small but need large touch targets (44px minimum). Use padding or pseudo-elements: - -```css -.icon-button { - width: 24px; /* Visual size */ - height: 24px; - position: relative; -} - -.icon-button::before { - content: ''; - position: absolute; - inset: -10px; /* Expand tap target to 44px */ -} -``` - -## Depth & Elevation - -Create semantic z-index scales (dropdown → sticky → modal-backdrop → modal → toast → tooltip) instead of arbitrary numbers. For shadows, create a consistent elevation scale (sm → md → lg → xl). **Key insight**: Shadows should be subtle. If you can clearly see it, it's probably too strong. - ---- - -**Avoid**: Arbitrary spacing values outside your scale. Making all spacing equal (variety creates hierarchy). Creating hierarchy through size alone - combine size, weight, color, and space. diff --git a/.pi/skills/impeccable/reference/typeset.md b/.pi/skills/impeccable/reference/typeset.md index dd3e67ea3..5f5e0c2c0 100644 --- a/.pi/skills/impeccable/reference/typeset.md +++ b/.pi/skills/impeccable/reference/typeset.md @@ -4,7 +4,7 @@ Typography carries most of the information on the page. Replace generic defaults ## Register -Brand: run the font selection procedure in [brand.md](brand.md). Pairing follows the brand's lane (display serif + sans body for editorial/luxury, one committed sans for tech, etc.). Fluid `clamp()` scale, ≥1.25 ratio between steps. +Brand: run the font selection procedure in [brand.md](brand.md). Fluid `clamp()` scale, ≥1.25 ratio between steps. Product: system fonts and familiar sans stacks are legitimate here. One well-tuned family typically carries the whole UI. Fixed `rem` scale, 1.125–1.2 ratio between more closely-spaced steps. @@ -43,7 +43,7 @@ Analyze what's weak or generic about the current type: ## Plan Typography Improvements -Consult the [typography reference](typography.md) for detailed guidance on scales, pairing, and loading strategies. +Consult the [Reference Material](#reference-material) section below for detailed guidance on scales, pairing, and loading strategies. Create a systematic plan: @@ -122,3 +122,158 @@ Each variant MUST declare a `scale` param controlling the hierarchy ratio. Expre Where the variant riffs on a specific pairing, expose the pairing choice as a `steps` param (e.g. "serif display + sans body" vs. "mono display + sans body" vs. "all-sans"). Each branch routes through `:scope[data-p-pairing="X"]` selectors in scoped CSS. See `reference/live.md` for the full params contract. + +--- + +## Reference Material + +The sections below were previously `typography.md` and live inline now so the typeset flow has its deep typography reference in one place. `bolder.md` also references this section. + +### Typography + +#### Classic Typography Principles + +##### Vertical Rhythm + +Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony; text and space share a mathematical foundation. + +##### Modular Scale & Hierarchy + +The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy. + +**Use fewer sizes with more contrast.** A 5-size system covers most needs: + +| Role | Typical Ratio | Use Case | +|------|---------------|----------| +| xs | 0.75rem | Captions, legal | +| sm | 0.875rem | Secondary UI, metadata | +| base | 1rem | Body text | +| lg | 1.25-1.5rem | Subheadings, lead text | +| xl+ | 2-4rem | Headlines, hero text | + +Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit. + +##### Readability & Measure + +Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length: narrow columns need tighter leading, wide columns need more. + +**Non-obvious**: Light text on dark backgrounds needs compensation on three axes, not just one. Bump line-height by 0.05–0.1, add a touch of letter-spacing (0.01–0.02em), and optionally step the body weight up one notch (regular → medium). The perceived weight drops across all three; fix all three. + +**Paragraph rhythm**: Pick either space between paragraphs OR first-line indentation. Never both. Digital usually wants space; editorial/long-form can justify indent-only. + +#### Font Selection & Pairing + +The tactical selection procedure and the reflex-reject list live in [reference/brand.md](brand.md) under **Font selection procedure** and **Reflex-reject list** (loaded for brand-register tasks). The rest of this section covers the adjacent knowledge: anti-reflex corrections, system font use, and pairing rules. + +##### Anti-reflexes worth defending against + +- A technical/utilitarian brief does NOT need a serif "for warmth." Most tech tools should look like tech tools. +- An editorial/premium brief does NOT need the same expressive serif everyone is using right now. Premium can be Swiss-modern, can be neo-grotesque, can be a literal monospace, can be a quiet humanist sans. +- A children's product does NOT need a rounded display font. Kids' books use real type. +- A "modern" brief does NOT need a geometric sans. The most modern thing you can do is not use the font everyone else is using. + +**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality. + +##### Pairing Principles + +**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif). + +When pairing, contrast on multiple axes: +- Serif + Sans (structure contrast) +- Geometric + Humanist (personality contrast) +- Condensed display + Wide body (proportion contrast) + +##### Web Font Loading + +The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix: + +```css +/* 1. Use font-display: swap for visibility */ +@font-face { + font-family: 'CustomFont'; + src: url('font.woff2') format('woff2'); + font-display: swap; +} + +/* 2. Match fallback metrics to minimize shift */ +@font-face { + font-family: 'CustomFont-Fallback'; + src: local('Arial'); + size-adjust: 105%; /* Scale to match x-height */ + ascent-override: 90%; /* Match ascender height */ + descent-override: 20%; /* Match descender depth */ + line-gap-override: 10%; /* Match line spacing */ +} + +body { + font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; +} +``` + +Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically. + +**`swap` vs `optional`**: `swap` shows fallback text immediately and FOUT-swaps when the web font arrives. `optional` uses the fallback if the web font misses a small load budget (~100ms) and avoids the shift entirely. Pick `optional` when zero layout shift matters more than seeing the branded font on slow networks. + +**Preload the critical weight only**: typically the regular-weight body font used above the fold. Preloading every weight costs more bandwidth than it saves. + +**Variable fonts for 3+ weights or styles**: a single variable font file is usually smaller than three static weight files, gives fractional weight control, and pairs well with `font-optical-sizing: auto`. For 1–2 weights, static is fine. + +#### Modern Web Typography + +##### Fluid Type + +Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate (higher vw = faster scaling). Add a rem offset so it doesn't collapse to 0 on small screens. + +**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes. + +**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI; fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it. + +**Bound your clamp()**: keep `max-size ≤ ~2.5 × min-size`. Wider ratios break the browser's zoom and reflow behaviour and make large viewports feel like the page is shouting. + +**Scale container width and font-size together** so effective character measure stays in the 45–75ch band at every viewport. A heading that widens faster than its container drifts out of the comfortable measure at the top end. + +##### OpenType Features + +Most developers don't know these exist. Use them for polish: + +```css +/* Proper fractions */ +.recipe-amount { font-variant-numeric: diagonal-fractions; } + +/* Small caps for abbreviations */ +abbr { font-variant-caps: all-small-caps; } + +/* Disable ligatures in code */ +code { font-variant-ligatures: none; } + +/* Enable kerning (usually on by default, but be explicit) */ +body { font-kerning: normal; } +``` + +Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/). + +##### Rendering polish + +```css +/* Variable fonts: pick the right optical-size master automatically */ +body { font-optical-sizing: auto; } +``` + +**ALL-CAPS tracking**: capitals sit too close at default spacing. Add 5–12% letter-spacing (`letter-spacing: 0.05em` to `0.12em`) to short all-caps labels, eyebrows, and small headings. Real small caps (via `font-variant-caps`) need the same treatment, slightly gentler. + +#### Typography System Architecture + +Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system. + +#### Accessibility Considerations + +Beyond contrast ratios (which are well-documented), consider: + +- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout. +- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text. +- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile. +- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets. + +--- + +**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text. diff --git a/.pi/skills/impeccable/reference/typography.md b/.pi/skills/impeccable/reference/typography.md deleted file mode 100644 index ed5ca272a..000000000 --- a/.pi/skills/impeccable/reference/typography.md +++ /dev/null @@ -1,159 +0,0 @@ -# Typography - -## Classic Typography Principles - -### Vertical Rhythm - -Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony; text and space share a mathematical foundation. - -### Modular Scale & Hierarchy - -The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy. - -**Use fewer sizes with more contrast.** A 5-size system covers most needs: - -| Role | Typical Ratio | Use Case | -|------|---------------|----------| -| xs | 0.75rem | Captions, legal | -| sm | 0.875rem | Secondary UI, metadata | -| base | 1rem | Body text | -| lg | 1.25-1.5rem | Subheadings, lead text | -| xl+ | 2-4rem | Headlines, hero text | - -Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit. - -### Readability & Measure - -Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length: narrow columns need tighter leading, wide columns need more. - -**Non-obvious**: Light text on dark backgrounds needs compensation on three axes, not just one. Bump line-height by 0.05–0.1, add a touch of letter-spacing (0.01–0.02em), and optionally step the body weight up one notch (regular → medium). The perceived weight drops across all three; fix all three. - -**Paragraph rhythm**: Pick either space between paragraphs OR first-line indentation. Never both. Digital usually wants space; editorial/long-form can justify indent-only. - -## Font Selection & Pairing - -The tactical selection procedure and the reflex-reject list live in [reference/brand.md](brand.md) under **Font selection procedure** and **Reflex-reject list** (loaded for brand-register tasks). The rest of this section covers the adjacent knowledge: anti-reflex corrections, system font use, and pairing rules. - -### Anti-reflexes worth defending against - -- A technical/utilitarian brief does NOT need a serif "for warmth." Most tech tools should look like tech tools. -- An editorial/premium brief does NOT need the same expressive serif everyone is using right now. Premium can be Swiss-modern, can be neo-grotesque, can be a literal monospace, can be a quiet humanist sans. -- A children's product does NOT need a rounded display font. Kids' books use real type. -- A "modern" brief does NOT need a geometric sans. The most modern thing you can do is not use the font everyone else is using. - -**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality. - -### Pairing Principles - -**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif). - -When pairing, contrast on multiple axes: -- Serif + Sans (structure contrast) -- Geometric + Humanist (personality contrast) -- Condensed display + Wide body (proportion contrast) - -**Never pair fonts that are similar but not identical** (e.g., two geometric sans-serifs). They create visual tension without clear hierarchy. - -### Web Font Loading - -The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix: - -```css -/* 1. Use font-display: swap for visibility */ -@font-face { - font-family: 'CustomFont'; - src: url('font.woff2') format('woff2'); - font-display: swap; -} - -/* 2. Match fallback metrics to minimize shift */ -@font-face { - font-family: 'CustomFont-Fallback'; - src: local('Arial'); - size-adjust: 105%; /* Scale to match x-height */ - ascent-override: 90%; /* Match ascender height */ - descent-override: 20%; /* Match descender depth */ - line-gap-override: 10%; /* Match line spacing */ -} - -body { - font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; -} -``` - -Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically. - -**`swap` vs `optional`**: `swap` shows fallback text immediately and FOUT-swaps when the web font arrives. `optional` uses the fallback if the web font misses a small load budget (~100ms) and avoids the shift entirely. Pick `optional` when zero layout shift matters more than seeing the branded font on slow networks. - -**Preload the critical weight only**: typically the regular-weight body font used above the fold. Preloading every weight costs more bandwidth than it saves. - -**Variable fonts for 3+ weights or styles**: a single variable font file is usually smaller than three static weight files, gives fractional weight control, and pairs well with `font-optical-sizing: auto`. For 1–2 weights, static is fine. - -## Modern Web Typography - -### Fluid Type - -Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate (higher vw = faster scaling). Add a rem offset so it doesn't collapse to 0 on small screens. - -**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes. - -**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI; fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it. - -**Bound your clamp()**: keep `max-size ≤ ~2.5 × min-size`. Wider ratios break the browser's zoom and reflow behaviour and make large viewports feel like the page is shouting. - -**Scale container width and font-size together** so effective character measure stays in the 45–75ch band at every viewport. A heading that widens faster than its container drifts out of the comfortable measure at the top end. - -### OpenType Features - -Most developers don't know these exist. Use them for polish: - -```css -/* Tabular numbers for data alignment */ -.data-table { font-variant-numeric: tabular-nums; } - -/* Proper fractions */ -.recipe-amount { font-variant-numeric: diagonal-fractions; } - -/* Small caps for abbreviations */ -abbr { font-variant-caps: all-small-caps; } - -/* Disable ligatures in code */ -code { font-variant-ligatures: none; } - -/* Enable kerning (usually on by default, but be explicit) */ -body { font-kerning: normal; } -``` - -Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/). - -### Rendering polish - -```css -/* Even out heading line lengths (browser picks better break points) */ -h1, h2, h3 { text-wrap: balance; } - -/* Reduce orphans and ragged endings in long prose */ -article p { text-wrap: pretty; } - -/* Variable fonts: pick the right optical-size master automatically */ -body { font-optical-sizing: auto; } -``` - -**ALL-CAPS tracking**: capitals sit too close at default spacing. Add 5–12% letter-spacing (`letter-spacing: 0.05em` to `0.12em`) to short all-caps labels, eyebrows, and small headings. Real small caps (via `font-variant-caps`) need the same treatment, slightly gentler. - -## Typography System Architecture - -Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system. - -## Accessibility Considerations - -Beyond contrast ratios (which are well-documented), consider: - -- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout. -- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text. -- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile. -- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets. - ---- - -**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text. diff --git a/.pi/skills/impeccable/reference/ux-writing.md b/.pi/skills/impeccable/reference/ux-writing.md deleted file mode 100644 index 417b5f56c..000000000 --- a/.pi/skills/impeccable/reference/ux-writing.md +++ /dev/null @@ -1,107 +0,0 @@ -# UX Writing - -## The Button Label Problem - -**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns: - -| Bad | Good | Why | -|-----|------|-----| -| OK | Save changes | Says what will happen | -| Submit | Create account | Outcome-focused | -| Yes | Delete message | Confirms the action | -| Cancel | Keep editing | Clarifies what "cancel" means | -| Click here | Download PDF | Describes the destination | - -**For destructive actions**, name the destruction: -- "Delete" not "Remove" (delete is permanent, remove implies recoverable) -- "Delete 5 items" not "Delete selected" (show the count) - -## Error Messages: The Formula - -Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input". - -### Error Message Templates - -| Situation | Template | -|-----------|----------| -| **Format error** | "[Field] needs to be [format]. Example: [example]" | -| **Missing required** | "Please enter [what's missing]" | -| **Permission denied** | "You don't have access to [thing]. [What to do instead]" | -| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." | -| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" | - -### Don't Blame the User - -Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date". - -## Empty States Are Opportunities - -Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items". - -## Voice vs Tone - -**Voice** is your brand's personality, consistent everywhere. -**Tone** adapts to the moment. - -| Moment | Tone Shift | -|--------|------------| -| Success | Celebratory, brief: "Done! Your changes are live." | -| Error | Empathetic, helpful: "That didn't work. Here's what to try..." | -| Loading | Reassuring: "Saving your work..." | -| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | - -**Never use humor for errors.** Users are already frustrated. Be helpful, not cute. - -## Writing for Accessibility - -**Link text** must have standalone meaning: "View pricing plans" not "Click here". **Alt text** describes information, not the image: "Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context. - -## Writing for Translation - -### Plan for Expansion - -German text is ~30% longer than English. Allocate space: - -| Language | Expansion | -|----------|-----------| -| German | +30% | -| French | +20% | -| Finnish | +30-40% | -| Chinese | -30% (fewer chars, but same width) | - -### Translation-Friendly Patterns - -Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear. - -## Consistency: The Terminology Problem - -Pick one term and stick with it: - -| Inconsistent | Consistent | -|--------------|------------| -| Delete / Remove / Trash | Delete | -| Settings / Preferences / Options | Settings | -| Sign in / Log in / Enter | Sign in | -| Create / Add / New | Create | - -Build a terminology glossary and enforce it. Variety creates confusion. - -## Avoid Redundant Copy - -If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well. - -## Loading States - -Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress. - -## Confirmation Dialogs: Use Sparingly - -Most confirmation dialogs are design failures; consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No"). - -## Form Instructions - -Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking. - ---- - -**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors. diff --git a/.pi/skills/impeccable/scripts/cleanup-deprecated.mjs b/.pi/skills/impeccable/scripts/cleanup-deprecated.mjs index 6aee4710a..bc6400e90 100644 --- a/.pi/skills/impeccable/scripts/cleanup-deprecated.mjs +++ b/.pi/skills/impeccable/scripts/cleanup-deprecated.mjs @@ -25,7 +25,7 @@ import { join, resolve } from 'node:path'; const DEPRECATED_NAMES = [ // v2.0 renames 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable teach + 'teach-impeccable', // folded into /impeccable init // v2.1 merges 'arrange', // renamed to layout 'normalize', // merged into polish diff --git a/.pi/skills/impeccable/scripts/command-metadata.json b/.pi/skills/impeccable/scripts/command-metadata.json index 1488bd5f1..83796d54b 100644 --- a/.pi/skills/impeccable/scripts/command-metadata.json +++ b/.pi/skills/impeccable/scripts/command-metadata.json @@ -3,8 +3,8 @@ "description": "Full confirmed-brief-then-build flow. Runs multi-round shape discovery first, resolves visual probe and north-star mock gates when available, then builds and visually iterates. Use when building a new feature end-to-end.", "argumentHint": "[feature description]" }, - "teach": { - "description": "Gathers design context for a project. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.", + "init": { + "description": "Sets up a project for impeccable. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles); offers DESIGN.md (visual: colors, typography, components) when code exists; pre-configures live mode; then recommends the best commands to run next. Every other command reads these files before doing work. Use once per project.", "argumentHint": "" }, "document": { diff --git a/.pi/skills/impeccable/scripts/context.mjs b/.pi/skills/impeccable/scripts/context.mjs new file mode 100644 index 000000000..52263c883 --- /dev/null +++ b/.pi/skills/impeccable/scripts/context.mjs @@ -0,0 +1,253 @@ +/** + * Context loader: prints PRODUCT.md (and DESIGN.md if present) as one + * markdown block on stdout, or exits with empty stdout when no PRODUCT.md + * is found anywhere. The skill keys off "empty stdout" to branch into the + * init flow. + * + * Path resolution (first match wins): + * 1. cwd, if PRODUCT.md or DESIGN.md is there + * 2. .agents/context/ then docs/ + * 3. $IMPECCABLE_CONTEXT_DIR (absolute or cwd-relative) — power-user + * escape hatch, only consulted when defaults are empty + * 4. cwd as a "nothing found" default + * + * `resolveContextDir()` and `loadContext()` are also exported for the + * server-side scripts (live.mjs, live-server.mjs) that need the structured + * shape rather than the markdown block. + */ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; +const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; +const FALLBACK_DIRS = ['.agents/context', 'docs']; + +// ─── Update check ────────────────────────────────────────────────────────── +// Piggyback a lightweight skill-version check on the once-per-session boot. +// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent +// can offer `npx impeccable skills update`. Everything here is best-effort and +// silent on failure: a network problem, sandbox, or missing cache must never +// block context output or print an error. + +const UPDATE_HOST = (process.env.IMPECCABLE_UPDATE_HOST || 'https://impeccable.style').replace(/\/$/, ''); +const UPDATE_CACHE_PATH = + process.env.IMPECCABLE_UPDATE_CACHE || path.join(os.homedir(), '.impeccable', 'update-check.json'); +const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // throttle the network poll to once a day +const RENOTIFY_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000; // don't re-surface the same version for a week +const FETCH_TIMEOUT_MS = 1200; + +export function resolveContextDir(cwd = process.cwd()) { + if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { + return cwd; + } + for (const rel of FALLBACK_DIRS) { + const candidate = path.resolve(cwd, rel); + if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { + return candidate; + } + } + const envDir = process.env.IMPECCABLE_CONTEXT_DIR; + if (envDir && envDir.trim()) { + const trimmed = envDir.trim(); + return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); + } + return cwd; +} + +export function loadContext(cwd = process.cwd()) { + const contextDir = resolveContextDir(cwd); + const productPath = firstExisting(contextDir, PRODUCT_NAMES); + const designPath = firstExisting(contextDir, DESIGN_NAMES); + const product = productPath ? safeRead(productPath) : null; + const design = designPath ? safeRead(designPath) : null; + return { + hasProduct: !!product, + product, + productPath: productPath ? path.relative(cwd, productPath) : null, + hasDesign: !!design, + design, + designPath: designPath ? path.relative(cwd, designPath) : null, + contextDir, + }; +} + +function firstExisting(dir, names) { + for (const name of names) { + const abs = path.join(dir, name); + if (fs.existsSync(abs)) return abs; + } + return null; +} + +function safeRead(p) { + try { + return fs.readFileSync(p, 'utf-8'); + } catch { + return null; + } +} + +/** + * Pull the register (`brand` or `product`) out of PRODUCT.md by looking + * for a `## Register` section and reading the first non-empty line that + * follows it. Returns null when the file is legacy / register-less. + */ +function extractRegister(product) { + if (!product) return null; + const lines = product.split('\n'); + for (let i = 0; i < lines.length; i++) { + if (/^##\s+Register\b/i.test(lines[i].trim())) { + for (let j = i + 1; j < lines.length; j++) { + const next = lines[j].trim(); + if (!next) continue; + const word = next.toLowerCase(); + if (word === 'brand' || word === 'product') return word; + return null; + } + } + } + return null; +} + +/** + * Read the installed skill's own version from the sibling SKILL.md frontmatter + * (this file lives at `/scripts/context.mjs`). Returns null when the + * frontmatter is missing or unreadable. + */ +function readLocalSkillVersion() { + try { + const here = path.dirname(fileURLToPath(import.meta.url)); + const skillMd = path.join(here, '..', 'SKILL.md'); + const content = fs.readFileSync(skillMd, 'utf-8'); + const match = content.match(/^version:\s*(.+)$/m); + return match ? match[1].trim().replace(/^["']|["']$/g, '') : null; + } catch { + return null; + } +} + +function readUpdateCache() { + try { + return JSON.parse(fs.readFileSync(UPDATE_CACHE_PATH, 'utf-8')); + } catch { + return {}; + } +} + +function writeUpdateCache(cache) { + try { + fs.mkdirSync(path.dirname(UPDATE_CACHE_PATH), { recursive: true }); + fs.writeFileSync(UPDATE_CACHE_PATH, JSON.stringify(cache)); + } catch { + // Best-effort: a read-only home dir just means we re-poll next session. + } +} + +/** Compare dotted numeric versions. Returns >0 when a is newer than b. */ +function compareSemver(a, b) { + const pa = String(a).split('.').map(n => parseInt(n, 10) || 0); + const pb = String(b).split('.').map(n => parseInt(n, 10) || 0); + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + const diff = (pa[i] || 0) - (pb[i] || 0); + if (diff !== 0) return diff; + } + return 0; +} + +async function fetchLatestSkillVersion() { + try { + const res = await fetch(`${UPDATE_HOST}/api/version`, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }); + if (!res.ok) return null; + const data = await res.json(); + return typeof data?.skills === 'string' ? data.skills : null; + } catch { + return null; // offline, sandboxed, timed out, or bad JSON: all non-fatal + } +} + +function buildUpdateDirective(localVersion, latestVersion) { + return ( + `UPDATE_AVAILABLE: A newer Impeccable skill is available ` + + `(installed v${localVersion}, latest v${latestVersion}). ` + + `Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` + + `Update now? It runs \`npx impeccable skills update\`." ` + + `If they agree, run \`npx impeccable skills update\` (the update applies to the next session, not this one). ` + + `Either way, continue the current task without waiting, and do not raise this again.` + ); +} + +/** + * Best-effort update directive for the boot output. Returns a string to append + * or null. Polls the version endpoint at most once per day (cached globally in + * the user's home dir) and re-surfaces a given version at most once per week so + * the agent never nags. Opt out entirely with IMPECCABLE_NO_UPDATE_CHECK=1. + */ +async function computeUpdateDirective(now = Date.now()) { + try { + if (process.env.IMPECCABLE_NO_UPDATE_CHECK) return null; + const localVersion = readLocalSkillVersion(); + if (!localVersion) return null; + + const cache = readUpdateCache(); + + // Poll the network only when the throttle window has elapsed. Stamp + // lastCheck even on failure so an offline machine doesn't poll every boot. + if (!cache.lastCheck || now - cache.lastCheck > CHECK_INTERVAL_MS) { + const latest = await fetchLatestSkillVersion(); + cache.lastCheck = now; + if (latest) cache.latestVersion = latest; + writeUpdateCache(cache); + } + + const latest = cache.latestVersion; + if (!latest || compareSemver(latest, localVersion) <= 0) return null; + + // Anti-nag: surface a given version at most once per RENOTIFY window. + if (cache.notifiedVersion === latest && cache.notifiedAt && now - cache.notifiedAt < RENOTIFY_INTERVAL_MS) { + return null; + } + cache.notifiedVersion = latest; + cache.notifiedAt = now; + writeUpdateCache(cache); + + return buildUpdateDirective(localVersion, latest); + } catch { + return null; + } +} + +async function cli() { + const ctx = loadContext(process.cwd()); + const updateDirective = await computeUpdateDirective(); + + if (!ctx.hasProduct) { + // Direct stdout message instead of relying on empty output as a signal + // — cheap models miss the empty case more often than the explicit one. + const parts = [ + 'NO_PRODUCT_MD: This project has no PRODUCT.md yet. ' + + 'Stop the current task, load reference/init.md, and follow its ' + + 'instructions to write PRODUCT.md before resuming.', + ]; + if (updateDirective) parts.push(updateDirective); + process.stdout.write(parts.join('\n\n---\n\n') + '\n'); + process.exit(0); + } + const parts = [`# PRODUCT.md\n\n${ctx.product.trim()}`]; + if (ctx.hasDesign) { + parts.push(`# DESIGN.md\n\n${ctx.design.trim()}`); + } + const register = extractRegister(ctx.product); + const next = register + ? `NEXT STEP: This project's register is \`${register}\`. You MUST now read \`reference/${register}.md\` before producing any design output.` + : `NEXT STEP: You MUST now read the matching register reference (\`reference/brand.md\` or \`reference/product.md\`) before producing any design output. Pick based on PRODUCT.md above.`; + parts.push(next); + if (updateDirective) parts.push(updateDirective); + process.stdout.write(parts.join('\n\n---\n\n') + '\n'); +} + +const _running = process.argv[1]; +if (_running?.endsWith('context.mjs') || _running?.endsWith('context.mjs/')) { + cli(); +} diff --git a/.pi/skills/impeccable/scripts/design-parser.mjs b/.pi/skills/impeccable/scripts/design-parser.mjs index b68756640..82a5cd7dc 100644 --- a/.pi/skills/impeccable/scripts/design-parser.mjs +++ b/.pi/skills/impeccable/scripts/design-parser.mjs @@ -63,7 +63,7 @@ function parseYamlSubset(yaml) { } const key = content.slice(0, colonIdx).trim(); - const rest = content.slice(colonIdx + 1).trim(); + const rest = stripInlineYamlComment(content.slice(colonIdx + 1).trim()); const parent = stack[stack.length - 1].obj; if (rest === '') { @@ -93,6 +93,21 @@ function findTopLevelColon(s) { return -1; } +function stripInlineYamlComment(s) { + let inQuote = null; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (inQuote) { + if (ch === inQuote && s[i - 1] !== '\\') inQuote = null; + } else if (ch === '"' || ch === "'") { + inQuote = ch; + } else if (ch === '#' && i > 0 && /\s/.test(s[i - 1])) { + return s.slice(0, i).trimEnd(); + } + } + return s; +} + function parseScalar(raw) { const s = raw.trim(); if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { diff --git a/.pi/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.pi/skills/impeccable/scripts/detector/browser/injected/index.mjs index c03a69caf..ceff16737 100644 --- a/.pi/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/.pi/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -9,8 +9,21 @@ if (IS_BROWSER) { const EXTENSION_MODE = (_myScript && _myScript.dataset.impeccableExtension === 'true') || document.documentElement.dataset.impeccableExtension === 'true'; - const BRAND_COLOR = 'oklch(55% 0.25 350)'; - const BRAND_COLOR_HOVER = 'oklch(45% 0.25 350)'; + // Kinpaku gold — pinned to the site's brand token (see + // site/styles/kinpaku-tokens.css --ks-kinpaku). Keep this in sync with + // the picker's C.brand in skill/scripts/live-browser.js and the kit's + // picker section in site/styles/kinpaku-kit.css. + // + // One color across both light and dark host pages. The outline is a + // 2px gesture pointing at an element + a labeled tag — it's a marker, + // not body text, so it doesn't need WCAG AA against the page. The + // label text inside the gold tag is dark (LABEL_INK) which has ~16:1 + // against the leaf gold, so reading the rule name is solid in both + // modes. Hover deepens the gold (preserves chroma — never drops it, + // dropping chroma washes the gold into a sand/olive tone). + const BRAND_COLOR = 'oklch(84% 0.19 80.46)'; + const BRAND_COLOR_HOVER = 'oklch(74% 0.18 80)'; + const LABEL_INK = 'oklch(4% 0.004 95)'; const LABEL_BG = BRAND_COLOR; const OUTLINE_COLOR = BRAND_COLOR; @@ -278,7 +291,7 @@ if (IS_BROWSER) { display: 'flex', alignItems: 'center', whiteSpace: 'nowrap', fontSize: '11px', fontWeight: '600', letterSpacing: '0.02em', - color: 'white', lineHeight: '14px', + color: LABEL_INK, lineHeight: '14px', background: LABEL_BG, fontFamily: 'system-ui, sans-serif', borderRadius: '4px 4px 0 0', @@ -398,7 +411,7 @@ if (IS_BROWSER) { banner.className = 'impeccable-overlay impeccable-banner'; Object.assign(banner.style, { position: 'fixed', top: '0', left: '0', right: '0', zIndex: '100000', - background: LABEL_BG, color: 'white', + background: LABEL_BG, color: LABEL_INK, fontFamily: 'system-ui, sans-serif', fontSize: '13px', display: 'flex', alignItems: 'center', pointerEvents: 'auto', height: '36px', overflow: 'hidden', maxWidth: '100vw', @@ -1223,12 +1236,12 @@ if (IS_BROWSER) { } console.group( `%c[impeccable] ${allFindings.length} anti-pattern${allFindings.length === 1 ? '' : 's'} found`, - 'color: oklch(60% 0.25 350); font-weight: bold' + 'color: oklch(84% 0.19 80.46); font-weight: bold' ); for (const { el, findings } of allFindings) { for (const f of findings) { console.log(`%c${f.type || f.id}%c ${f.detail || f.snippet}`, - 'color: oklch(55% 0.25 350); font-weight: bold', 'color: inherit', el); + 'color: oklch(84% 0.19 80.46); font-weight: bold', 'color: inherit', el); } } console.groupEnd(); @@ -1249,6 +1262,10 @@ if (IS_BROWSER) { const groupMap = new Map(); const _disabled = EXTENSION_MODE ? (window.__IMPECCABLE_CONFIG__?.disabledRules || []) : []; const _ruleOk = (id) => !_disabled.length || !_disabled.includes(id); + // Note: provider-gated rules (--gpt / --gemini) are NOT filtered here. In a + // real browser env (detector page, live overlay, extension) running every + // check is free, so we always surface them; the gating is purely a CLI + // output concern, applied in the Node engines' detect* return paths. for (const el of document.querySelectorAll('*')) { // Skip impeccable's own elements and any descendants (overlays, labels, banner, nav buttons) @@ -1270,11 +1287,23 @@ if (IS_BROWSER) { ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), - ...checkElementHeroEyebrowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementOversizedH1DOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementClippedOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementGptBorderShadowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementTextOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ].filter(f => _ruleOk(f.type)); addBrowserFindings(groupMap, el, findings); + + // Hero eyebrow: the offending element is the eyebrow above the heading, + // not the heading itself — highlight the previous sibling instead. + const eyebrowFindings = checkElementHeroEyebrowDOM(el) + .map(f => ({ type: f.id, detail: f.snippet })) + .filter(f => _ruleOk(f.type)); + if (eyebrowFindings.length > 0 && el.previousElementSibling) { + addBrowserFindings(groupMap, el.previousElementSibling, eyebrowFindings); + } } const pageLevelFindings = []; @@ -1306,6 +1335,14 @@ if (IS_BROWSER) { addBrowserFindings(groupMap, document.body, qualityFindings); } + const creamFindings = checkCreamPalette(document) + .map(f => ({ type: f.id, detail: f.snippet })) + .filter(f => _ruleOk(f.type)); + if (creamFindings.length > 0) { + pageLevelFindings.push(...creamFindings); + addBrowserFindings(groupMap, document.body, creamFindings); + } + // Regex-on-HTML checks (shared with Node) // Clone the document and strip impeccable-live overlay nodes before the // regex scan, so the inspector's own inline styles (transitions on top/ diff --git a/.pi/skills/impeccable/scripts/detector/cli/main.mjs b/.pi/skills/impeccable/scripts/detector/cli/main.mjs index 76da09f0c..94283e01b 100644 --- a/.pi/skills/impeccable/scripts/detector/cli/main.mjs +++ b/.pi/skills/impeccable/scripts/detector/cli/main.mjs @@ -41,7 +41,7 @@ function formatFindings(findings, jsonMode) { // Stdin handling // --------------------------------------------------------------------------- -async function handleStdin() { +async function handleStdin(options = {}) { const chunks = []; for await (const chunk of process.stdin) chunks.push(chunk); const input = Buffer.concat(chunks).toString('utf-8'); @@ -50,10 +50,10 @@ async function handleStdin() { const fp = parsed?.tool_input?.file_path; if (fp && fs.existsSync(fp)) { return HTML_EXTENSIONS.has(path.extname(fp).toLowerCase()) - ? detectHtml(fp) : detectText(fs.readFileSync(fp, 'utf-8'), fp); + ? detectHtml(fp, options) : detectText(fs.readFileSync(fp, 'utf-8'), fp, options); } } catch { /* not JSON */ } - return detectText(input, ''); + return detectText(input, '', options); } @@ -81,6 +81,8 @@ Scan files or URLs for UI anti-patterns and design quality issues. Options: --fast Regex-only mode (skip static HTML/CSS analysis, faster but misses linked stylesheets) --json Output results as JSON + --gpt Also report GPT-specific provider tells (off by default) + --gemini Also report Gemini-specific provider tells (off by default) --help Show this help message Detection modes: @@ -106,6 +108,10 @@ async function detectCli() { const jsonMode = args.includes('--json'); const helpMode = args.includes('--help'); const fastMode = args.includes('--fast'); + const providers = []; + if (args.includes('--gpt')) providers.push('gpt'); + if (args.includes('--gemini')) providers.push('gemini'); + const scanOptions = { providers }; const targets = args.filter(a => !a.startsWith('--')); if (helpMode) { printUsage(); process.exit(0); } @@ -113,7 +119,7 @@ async function detectCli() { let allFindings = []; if (!process.stdin.isTTY && targets.length === 0) { - allFindings = await handleStdin(); + allFindings = await handleStdin(scanOptions); } else { const paths = targets.length > 0 ? targets : [process.cwd()]; const urlTargetCount = paths.filter(target => /^https?:\/\//i.test(target)).length; @@ -124,8 +130,8 @@ async function detectCli() { if (/^https?:\/\//i.test(target)) { try { const scanner = browserDetector - ? (url) => browserDetector.detectUrl(url) - : (url) => detectUrl(url); + ? (url) => browserDetector.detectUrl(url, scanOptions) + : (url) => detectUrl(url, scanOptions); allFindings.push(...await scanner(target)); } catch (e) { process.stderr.write(`Error: ${e.message}\n`); } continue; @@ -192,9 +198,9 @@ async function detectCli() { const ext = path.extname(file).toLowerCase(); let fileFindings; if (!fastMode && HTML_EXTENSIONS.has(ext)) { - fileFindings = await detectHtml(file); + fileFindings = await detectHtml(file, scanOptions); } else { - fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file); + fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file, scanOptions); } // Annotate findings with import context const importers = importedByMap.get(file); @@ -209,9 +215,9 @@ async function detectCli() { } else if (stat.isFile()) { const ext = path.extname(resolved).toLowerCase(); if (!fastMode && HTML_EXTENSIONS.has(ext)) { - allFindings.push(...await detectHtml(resolved)); + allFindings.push(...await detectHtml(resolved, scanOptions)); } else { - allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved)); + allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved, scanOptions)); } } } diff --git a/.pi/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.pi/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 1afbe3f12..88439a5c3 100644 --- a/.pi/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.pi/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -38,7 +38,7 @@ const OVERUSED_FONTS = new Set([ // Older monoculture (still ubiquitous): 'inter', 'roboto', 'open sans', 'lato', 'montserrat', 'arial', 'helvetica', // Newer monoculture (the Anthropic-skill / Vercel / GitHub default wave): - 'fraunces', 'instrument sans', + 'fraunces', 'instrument sans', 'instrument serif', 'geist', 'geist sans', 'geist mono', 'mona sans', 'plus jakarta sans', 'space grotesk', 'recoleta', @@ -165,6 +165,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'AI color palette', }, + { + id: 'cream-palette', + category: 'slop', + name: 'Cream / beige palette', + description: + 'A warm cream or beige page background has become the default "tasteful" AI surface, reached for by reflex. Choose a background that comes from a deliberate palette, not the safe warm off-white.', + skillSection: 'Color & Contrast', + skillGuideline: 'cream and beige as the default surface', + }, { id: 'nested-cards', category: 'slop', @@ -183,15 +192,6 @@ const ANTIPATTERNS = [ skillSection: 'Layout & Space', skillGuideline: 'same spacing everywhere', }, - { - id: 'everything-centered', - category: 'slop', - name: 'Everything centered', - description: - 'Every text element is center-aligned. Left-aligned text with asymmetric layouts feels more designed. Center only hero sections and CTAs.', - skillSection: 'Layout & Space', - skillGuideline: 'Center everything', - }, { id: 'bounce-easing', category: 'slop', @@ -247,17 +247,72 @@ const ANTIPATTERNS = [ skillSection: 'Typography', skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding', }, + { + id: 'numbered-section-markers', + category: 'slop', + severity: 'advisory', + name: 'Numbered section markers (01 / 02 / 03)', + description: + 'Numbered display markers as section labels (01, 02, 03) are the AI editorial scaffold one tier deeper than tracked eyebrow chips. If you find yourself reaching for them, choose a different section cadence.', + skillSection: 'Layout & Space', + skillGuideline: 'numbered section markers', + }, + { + id: 'em-dash-overuse', + category: 'slop', + name: 'Em-dash overuse', + description: + 'More than two em-dashes (— or --) in body copy is an AI cadence tell. Use commas, colons, periods, or parentheses instead.', + skillSection: 'Copy', + skillGuideline: 'no em dashes', + }, + { + id: 'marketing-buzzword', + category: 'slop', + name: 'Marketing buzzword', + description: + 'Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does.', + skillSection: 'Copy', + skillGuideline: 'marketing buzzwords', + }, + { + id: 'aphoristic-cadence', + category: 'slop', + name: 'Aphoristic-cadence copy', + description: + 'Three or more sections landing on a short rebuttal sentence ("X. No Y." / "X. Just Y.") or a manufactured-contrast aphorism ("Not a feature. A platform.") reads as AI cadence, not voice. Once is fine; the pattern is the tell.', + skillSection: 'Copy', + skillGuideline: 'aphoristic cadence', + }, + { + id: 'oversized-h1', + category: 'slop', + name: 'Oversized hero headline', + description: + 'A full-sentence headline set at display size ends up dominating the viewport, leaving no room for anything else above the fold. A punchy one- or two-word headline at that size is fine — the problem is a long headline blown up too large. Set long headlines smaller, or tighten the copy.', + skillSection: 'Typography', + skillGuideline: 'long headline set at display size', + }, + { + id: 'extreme-negative-tracking', + category: 'slop', + name: 'Crushed letter spacing', + description: + 'Letter-spacing pulled tighter than the point where characters keep their own shapes costs legibility. Tighten display type optically, not destructively.', + skillSection: 'Typography', + skillGuideline: 'letter spacing crushed past legibility', + }, + { + id: 'broken-image', + category: 'quality', + name: 'Broken or placeholder image', + description: + ' tags with empty src, missing src, or placeholder values ship as broken-image boxes. Use real images, generated assets, or remove the tag.', + skillSection: 'Imagery', + skillGuideline: 'broken image references', + }, // ── Quality: general design and accessibility issues ── - { - id: 'pure-black-white', - category: 'quality', - name: 'Pure black background', - description: - 'Pure #000000 as a background color looks harsh and unnatural. Tint it slightly toward your brand hue (e.g., oklch(12% 0.01 250)) for a more refined feel.', - skillSection: 'Color & Contrast', - skillGuideline: 'pure black (#000)', - }, { id: 'gray-on-color', category: 'quality', @@ -297,7 +352,9 @@ const ANTIPATTERNS = [ category: 'quality', name: 'Cramped padding', description: - 'Text is too close to the edge of its container. Add at least 8px (ideally 12-16px) of padding inside bordered or colored containers.', + 'Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.', + skillSection: 'Layout & Space', + skillGuideline: 'inside bordered or colored containers', }, { id: 'body-text-viewport-edge', @@ -350,6 +407,70 @@ const ANTIPATTERNS = [ description: 'Letter spacing above 0.05em on body text disrupts natural character groupings and slows reading. Reserve wide tracking for short uppercase labels only.', }, + { + id: 'text-overflow', + category: 'quality', + name: 'Content overflowing its container', + description: + 'Content renders wider than its container, spilling out or forcing a horizontal scrollbar. Let text wrap, constrain widths, or give the region a deliberate scroll affordance.', + skillSection: 'Layout & Space', + skillGuideline: 'content wider than its container', + }, + { + id: 'clipped-overflow-container', + category: 'quality', + name: 'Positioned child clipped by overflow container', + description: + 'A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.', + skillSection: 'Layout & Space', + skillGuideline: 'overflow container clipping positioned children', + }, + + // ── Provider tells: opt-in via --gpt / --gemini (gated off by default) ── + { + id: 'gpt-thin-border-wide-shadow', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Hairline border with wide shadow', + description: + 'A hairline border paired with a wide, diffuse shadow is a recurring generated-UI signature. Commit to one — a defined edge or a soft elevation — rather than both at once.', + skillSection: 'Visual Details', + skillGuideline: 'hairline border plus wide diffuse shadow', + }, + { + id: 'repeating-stripes-gradient', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Repeating-gradient stripes', + description: + 'Repeating-gradient stripes used as surface decoration are a recurring generated-UI signature. Reach for a deliberate texture or leave the surface plain.', + skillSection: 'Visual Details', + skillGuideline: 'repeating-gradient decorative stripes', + }, + { + id: 'theater-slop-phrase', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Theater framing copy', + description: + 'Dismissing something as "theater" is a recurring generated-copy tic. Say plainly what the thing does or does not do.', + skillSection: 'Copy', + skillGuideline: 'theater framing copy', + }, + { + id: 'image-hover-transform', + category: 'slop', + severity: 'advisory', + gated: 'gemini', + name: 'Image hover transform', + description: + 'Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction.', + skillSection: 'Motion', + skillGuideline: 'image scale or rotate on hover', + }, ]; // --- cli/engine/shared/color.mjs --- @@ -527,11 +648,6 @@ function checkColors(opts) { } const findings = []; - // Pure black background (only solid or near-solid, not semi-transparent overlays) - if (bgColor && bgColor.a >= 0.9 && bgColor.r === 0 && bgColor.g === 0 && bgColor.b === 0) { - findings.push({ id: 'pure-black-white', snippet: '#000000 background' }); - } - if (hasDirectText && textColor && !isEmojiOnly) { // Run background-dependent checks against either a solid bg or, if the // ancestor is a gradient, against every gradient stop (use the worst case). @@ -587,9 +703,6 @@ function checkColors(opts) { // Tailwind class checks if (classList) { const classStr = typeof classList === 'string' ? classList : Array.from(classList).join(' '); - if (/\bbg-black\b(?!\/)/.test(classStr)) { - findings.push({ id: 'pure-black-white', snippet: 'bg-black' }); - } const grayMatch = classStr.match(/\btext-(?:gray|slate|zinc|neutral|stone)-\d+\b/); const colorBgMatch = classStr.match(/\bbg-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+\b/); @@ -905,12 +1018,6 @@ function checkHtmlPatterns(html) { // --- Color --- - // Pure black background - const pureBlackBgRe = /background(?:-color)?\s*:\s*(?:#000000|#000|rgb\(\s*0,\s*0,\s*0\s*\))\b/gi; - if (pureBlackBgRe.test(html)) { - findings.push({ id: 'pure-black-white', snippet: 'Pure #000 background' }); - } - // AI color palette: purple/violet const purpleHexRe = /#(?:7c3aed|8b5cf6|a855f7|9333ea|7e22ce|6d28d9|6366f1|764ba2|667eea)\b/gi; if (purpleHexRe.test(html)) { @@ -1027,6 +1134,39 @@ function checkHtmlPatterns(html) { } } + // --- Provider tells (gated): repeating-gradient stripes (GPT) --- + if (/repeating-(?:linear|radial|conic)-gradient\s*\(/i.test(html)) { + findings.push({ id: 'repeating-stripes-gradient', snippet: 'repeating-gradient decorative stripes' }); + } + + // --- Provider tells (gated): "X theater" framing copy (GPT) --- + // Lives here (regex-on-HTML) rather than in the text-content analyzers so it + // runs in the bundled browser path too, not just the CLI/static path. + { + const bodyText = html + .replace(/]*>[\s\S]*?<\/script>/gi, ' ') + .replace(/]*>[\s\S]*?<\/style>/gi, ' ') + .replace(/<[^>]+>/g, ' '); + const tm = /\b(\w+)\s+theater\b/i.exec(bodyText); + if (tm) findings.push({ id: 'theater-slop-phrase', snippet: `"${tm[0].trim()}"` }); + } + + // --- Provider tells (gated): image hover transform (Gemini) --- + // A CSS `img...:hover { transform: ... }` rule, or a Tailwind hover:scale / + // hover:rotate / hover:translate utility on an . Each distinct + // mechanism is its own finding. + const imgHoverCss = /\bimg\b[^,{}]*:hover\b[^{}]*\{[^}]*\btransform\s*:\s*(?:scale|rotate|translate|matrix|skew)/i; + if (imgHoverCss.test(html)) { + findings.push({ id: 'image-hover-transform', snippet: 'img:hover { transform } rule' }); + } + const imgTagRe = /]*\bclass\s*=\s*"([^"]*)"/gi; + let im; + while ((im = imgTagRe.exec(html)) !== null) { + if (/\bhover:(?:scale|rotate|translate|skew)-/.test(im[1])) { + findings.push({ id: 'image-hover-transform', snippet: 'Tailwind hover transform on ' }); + } + } + return findings; } @@ -1673,7 +1813,7 @@ function resolveLengthPx(value, fontSizePx) { // Both adapters resolve font-size, line-height and letter-spacing to pixels // before calling this so the pure function only deals with numbers. function checkQuality(opts) { - const { el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax = 80, viewportWidth = 0 } = opts; + const { el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax = 80, viewportWidth = 0, win = null } = opts; const findings = []; // Skip browser extension injected elements const elId = el.id || ''; @@ -1724,6 +1864,155 @@ function checkQuality(opts) { } } + // --- Flush against a visible boundary --- + // Fires when a container has a visible boundary (border, outline, OR a + // non-transparent background) AND near-zero padding on the bounded + // side(s) AND text-bearing children land flush against the boundary. + // + // Distinct from cramped-padding: that rule needs the element itself to + // have direct text (hasDirectText). This rule targets the OPPOSITE + // shape — a container with NO direct text, only children — which is + // exactly what cramped-padding misses (a section wrapping a label + + // list lands a free pass). + // + // The classic shape: agent writes `padding: 28px 0 0` shorthand on a + // section that also has a border, zeroing horizontal padding so the + // text-bearing children touch the side borders. Background and + // outline count too: a colored card with zero padding has the same + // visual failure mode. + { + const FLUSH_SKIP_TAGS = new Set(['HTML', 'BODY', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ARTICLE', 'ASIDE', 'BUTTON', 'A', 'LABEL', 'SUMMARY', 'CODE', 'PRE', 'INPUT', 'TEXTAREA', 'SELECT', 'FORM', 'FIGURE', 'TABLE', 'TBODY', 'THEAD', 'TR', 'TD', 'TH']); + const upperTag = tag ? tag.toUpperCase() : ''; + const elPosition = style.position || ''; + if ( + !FLUSH_SKIP_TAGS.has(upperTag) && + !hasDirectText && + !['fixed', 'absolute'].includes(elPosition) && + el.children && el.children.length > 0 + ) { + const isTransparent = (c) => + !c || c === 'transparent' || c === 'rgba(0, 0, 0, 0)' || + /^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*0(?:\.0+)?\s*\)$/.test(c); + + const borderW = { + top: parseFloat(style.borderTopWidth) || 0, + right: parseFloat(style.borderRightWidth) || 0, + bottom: parseFloat(style.borderBottomWidth) || 0, + left: parseFloat(style.borderLeftWidth) || 0, + }; + const borderVisible = { + top: borderW.top > 0 && !isTransparent(style.borderTopColor), + right: borderW.right > 0 && !isTransparent(style.borderRightColor), + bottom: borderW.bottom > 0 && !isTransparent(style.borderBottomColor), + left: borderW.left > 0 && !isTransparent(style.borderLeftColor), + }; + // Outline detection. jsdom decomposes `border` shorthand into + // border{Top,…}Width/Color but does NOT decompose `outline` — + // the longhands come back empty when the value was set via the + // shorthand. Fall back to parsing `style.outline` ourselves. + let outlineW = parseFloat(style.outlineWidth) || 0; + let outlineStyleVal = style.outlineStyle || ''; + let outlineColorVal = style.outlineColor || ''; + if (!outlineW && style.outline) { + const wMatch = style.outline.match(/(\d+(?:\.\d+)?)\s*px/); + if (wMatch) outlineW = parseFloat(wMatch[1]) || 0; + if (!outlineStyleVal) { + outlineStyleVal = /\b(solid|dashed|dotted|double|groove|ridge|inset|outset)\b/.test(style.outline) ? 'solid' : ''; + } + if (!outlineColorVal) { + const cMatch = style.outline.match(/(rgba?\([^)]+\)|#[0-9a-fA-F]{3,8}|[a-zA-Z]+)\s*$/); + if (cMatch) outlineColorVal = cMatch[1]; + } + } + const outlineVisible = outlineW > 0 && !isTransparent(outlineColorVal) && outlineStyleVal && outlineStyleVal !== 'none'; + const bgVisible = !isTransparent(style.backgroundColor); + + const anyVisible = borderVisible.top || borderVisible.right || borderVisible.bottom || borderVisible.left || outlineVisible || bgVisible; + if (anyVisible) { + // Resolve padding to px (jsdom returns raw "1.5rem" etc., not the + // computed px value; parseFloat would strip the unit and treat + // 1.5rem as 1.5px, false-flagging legitimate insets). + const pad = { + top: resolveLengthPx(style.paddingTop, fontSize) ?? 0, + right: resolveLengthPx(style.paddingRight, fontSize) ?? 0, + bottom: resolveLengthPx(style.paddingBottom, fontSize) ?? 0, + left: resolveLengthPx(style.paddingLeft, fontSize) ?? 0, + }; + const PAD_THRESHOLD = 2; + // Children-insulate-this-side: a side is insulated if ANY direct + // child has its own padding ≥ 4px on that side. Rationale: in + // typical flow, only the first/last (or leftmost/rightmost) + // children actually sit at the parent's edges. If even one of + // them has its own padding, the visual flush is broken on that + // side. Classic example: a column-flow card frame where the + // top child (header) has padding-top:12 and the bottom child + // (footer) has padding-bottom:8 — the parent's padding:0 doesn't + // matter; nothing is actually flush. The `any-child-insulates` + // heuristic accepts some false negatives (a card with one heavily + // padded middle child won't flag) for far fewer false positives. + const CHILD_INSULATE_THRESHOLD = 4; + const childrenInsulate = { top: false, right: false, bottom: false, left: false }; + for (const child of el.children) { + let childStyle = null; + if (win && typeof win.getComputedStyle === 'function') { + try { childStyle = win.getComputedStyle(child); } catch {} + } + if (!childStyle && typeof getComputedStyle === 'function') { + try { childStyle = getComputedStyle(child); } catch {} + } + if (!childStyle) continue; + const childPad = { + top: resolveLengthPx(childStyle.paddingTop, fontSize) ?? 0, + right: resolveLengthPx(childStyle.paddingRight, fontSize) ?? 0, + bottom: resolveLengthPx(childStyle.paddingBottom, fontSize) ?? 0, + left: resolveLengthPx(childStyle.paddingLeft, fontSize) ?? 0, + }; + for (const s of ['top', 'right', 'bottom', 'left']) { + if (childPad[s] >= CHILD_INSULATE_THRESHOLD) childrenInsulate[s] = true; + } + } + + const flushSides = []; + for (const side of ['top', 'right', 'bottom', 'left']) { + const sideBounded = borderVisible[side] || outlineVisible || bgVisible; + if (sideBounded && pad[side] <= PAD_THRESHOLD && !childrenInsulate[side]) { + flushSides.push(side); + } + } + + if (flushSides.length > 0) { + // Confirm at least one direct child has substantial text content + // (> 4 chars). Without this, the flush is harmless: e.g. an + // image-only card. + let hasTextChild = false; + for (const child of el.children) { + const childText = (child.textContent || '').trim(); + if (childText.length > 4) { hasTextChild = true; break; } + } + if (hasTextChild) { + const cls = (typeof el.className === 'string' && el.className.trim()) + ? el.className.trim().split(/\s+/)[0] + : ''; + const boundaryParts = []; + const borderSidesVisible = ['top', 'right', 'bottom', 'left'].filter(s => borderVisible[s]); + if (borderSidesVisible.length === 4) boundaryParts.push('border'); + else if (borderSidesVisible.length > 0) boundaryParts.push(`border-${borderSidesVisible.join('/')}`); + if (outlineVisible) boundaryParts.push('outline'); + if (bgVisible) boundaryParts.push('bg'); + const sidesLabel = flushSides.length === 4 ? 'all sides' : flushSides.join('/'); + const ident = cls + ? `<${tag.toLowerCase()}> "${cls}"` + : `<${tag.toLowerCase()}>`; + findings.push({ + id: 'cramped-padding', + snippet: `${ident}: children flush against ${boundaryParts.join('+')} on ${sidesLabel} (no inset)`, + }); + } + } + } + } + } + // --- Body text touching viewport edge --- (browser-only: needs rect) // Catches the failure mode where the agent ships body paragraphs // with NO container providing horizontal padding — text bleeds @@ -1804,6 +2093,20 @@ function checkQuality(opts) { } } + // --- Crushed letter spacing (mirror of wide-tracking) --- + // Tracking pulled tighter than ~-0.05em crushes characters into each other. + // Optical tightening that display type legitimately wants (around -0.02em) + // stays well above this floor. + if (hasDirectText && textLen > 20 && fontSize > 0) { + if (letterSpacingPx != null && letterSpacingPx < 0) { + const trackingEm = letterSpacingPx / fontSize; + if (trackingEm <= -0.05) { + const excerpt = (el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 40); + findings.push({ id: 'extreme-negative-tracking', snippet: `letter-spacing: ${trackingEm.toFixed(2)}em — "${excerpt}"` }); + } + } + } + return findings; } @@ -1820,7 +2123,7 @@ function checkElementQualityDOM(el) { const rect = el.getBoundingClientRect(); const lineMax = (typeof window !== 'undefined' && window.__IMPECCABLE_CONFIG__?.lineLengthMax) || 80; const viewportWidth = (typeof window !== 'undefined' ? window.innerWidth : 0) || 0; - return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax, viewportWidth }); + return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax, viewportWidth, win: typeof window !== 'undefined' ? window : null }); } // Pure page-level skipped-heading walk. Takes a Document so it works in both @@ -1862,7 +2165,7 @@ function checkElementQuality(el, style, tag, window) { const fontSize = resolveFontSizePx(el, window); const lineHeightPx = resolveLengthPx(style.lineHeight, fontSize); const letterSpacingPx = resolveLengthPx(style.letterSpacing, fontSize); - return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect: null }); + return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect: null, win: window }); } function checkElementBorders(tag, style, overrides, resolvedRadius) { @@ -2303,40 +2606,213 @@ function checkPageLayout(doc, win) { } } - // Everything centered - const textEls = doc.querySelectorAll('h1, h2, h3, h4, h5, h6, p, li, div, button'); - let centeredCount = 0; - let totalText = 0; - for (const el of textEls) { - const hasDirectText = [...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim().length >= 3); - if (!hasDirectText) continue; - totalText++; - - let cur = el; - let isCentered = false; - while (cur && cur.nodeType === 1) { - const rawStyle = cur.getAttribute?.('style') || ''; - const cls = cur.getAttribute?.('class') || ''; - if (/text-align\s*:\s*center/i.test(rawStyle) || /\btext-center\b/.test(cls)) { - isCentered = true; - break; - } - if (cur.tagName === 'BODY') break; - cur = cur.parentElement; - } - if (isCentered) centeredCount++; - } - - if (totalText >= 5 && centeredCount / totalText > 0.7) { - findings.push({ - id: 'everything-centered', - snippet: `${centeredCount}/${totalText} text elements centered (${Math.round(centeredCount / totalText * 100)}%)`, - }); - } - return findings; } +// ─── Cream / beige palette (the default "tasteful" AI surface) ──────────────── +// A warm, lightly-tinted off-white page background — light, with R≥G≥B and a +// small warm tint (not white, not a strong color). The current reflex surface. +function isCreamColor(rgb) { + if (!rgb) return false; + const { r, g, b } = rgb; + if (Math.min(r, g, b) < 209) return false; // must be light + if (!(r >= g && g >= b)) return false; // warm ordering + const warmth = r - b; + return warmth >= 6 && warmth <= 48; // tinted, not white, not strong +} + +// Tailwind background utilities that render as a warm off-white surface. The +// static engine doesn't fetch Tailwind's CSS, so a `bg-amber-50` on +// resolves to nothing in computed style — catch it from the class list +// instead. Candidate tokens map to their actual Tailwind hex and are still +// filtered through isCreamColor, so neutral grays (stone) and over-saturated +// shades drop out on their own. +const TAILWIND_BG_HEX = { + 'bg-amber-50': '#fffbeb', 'bg-amber-100': '#fef3c7', + 'bg-orange-50': '#fff7ed', 'bg-orange-100': '#ffedd5', + 'bg-yellow-50': '#fefce8', + 'bg-stone-50': '#fafaf9', 'bg-stone-100': '#f5f5f4', 'bg-stone-200': '#e7e5e4', +}; + +function creamFromClassList(cls) { + if (!cls) return null; + // Arbitrary value: bg-[#f5f0e6] / bg-[rgb(245_240_230)] (underscores = spaces). + const arb = cls.match(/\bbg-\[([^\]]+)\]/); + if (arb && isCreamColor(parseAnyColor(arb[1].replace(/_/g, ' ')))) return `bg-[${arb[1]}]`; + // Named warm-light utilities. + for (const [tok, hex] of Object.entries(TAILWIND_BG_HEX)) { + if (new RegExp(`(^|\\s)${tok}($|\\s)`).test(cls) && isCreamColor(parseAnyColor(hex))) return tok; + } + return null; +} + +function checkCreamPalette(doc, win) { + const findings = []; + const body = doc.body || (doc.querySelector ? doc.querySelector('body') : null); + if (!body) return findings; + const html = doc.documentElement; + const getCS = (el) => (win ? win.getComputedStyle(el) : getComputedStyle(el)); + + // 1. Computed background — covers inline / close.', 'Prefix every preview selector with the matching [data-impeccable-variant="N"] selector.', 'Keep selectors anchored to the generated variant wrapper; do not rely on component CSS scoping for preview rules.', ], forbidden: [ 'Do not use @scope for this styleMode.', + 'Do not wrap style content in a JSX/TSX template literal ({` ... `}); that syntax is for .tsx/.jsx only.', + 'Do not put { immediately after the style opening tag; Astro parses { as expression syntax.', ], }; } @@ -629,4 +632,15 @@ if (_running?.endsWith('live-wrap.mjs') || _running?.endsWith('live-wrap.mjs/')) } // Test exports (used by tests/live-wrap.test.mjs) -export { buildSearchQueries, findElement, findClosingLine, detectCommentSyntax }; +export { + buildSearchQueries, + findElement, + findClosingLine, + detectCommentSyntax, + findAllElements, + filterByText, + findFileWithQuery, + detectStyleMode, + buildCssAuthoring, + buildCssSelectorPrefixExamples, +}; diff --git a/.pi/skills/impeccable/scripts/live.mjs b/.pi/skills/impeccable/scripts/live.mjs index cafb0eca9..8acd300ed 100644 --- a/.pi/skills/impeccable/scripts/live.mjs +++ b/.pi/skills/impeccable/scripts/live.mjs @@ -21,7 +21,7 @@ import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { loadContext } from './load-context.mjs'; +import { loadContext } from './context.mjs'; import { resolveFiles } from './live-inject.mjs'; import { readLiveServerInfo } from './impeccable-paths.mjs'; @@ -80,7 +80,7 @@ The agent should then: process.exit(1); } - // 4. Load PRODUCT.md + DESIGN.md context (auto-migrates legacy .impeccable.md) + // 4. Load PRODUCT.md + DESIGN.md context. const ctx = loadContext(process.cwd()); // 5. Compute drift-heal: compare resolved inject targets against the @@ -102,7 +102,6 @@ The agent should then: hasDesign: ctx.hasDesign, design: ctx.design, designPath: ctx.designPath, - migrated: ctx.migrated, }, null, 2)); } diff --git a/.pi/skills/impeccable/scripts/load-context.mjs b/.pi/skills/impeccable/scripts/load-context.mjs deleted file mode 100644 index dc340bf16..000000000 --- a/.pi/skills/impeccable/scripts/load-context.mjs +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Shared context loader for every impeccable command that needs to know - * "who is this for" and "what does this look like". - * - * Input: project root (process.cwd()). - * - * Output (JSON to stdout): - * { - * hasProduct: boolean, // PRODUCT.md found (or auto-migrated) - * product: string | null, // PRODUCT.md contents - * productPath: string | null, // relative path - * hasDesign: boolean, // DESIGN.md found - * design: string | null, // DESIGN.md contents - * designPath: string | null, - * migrated: boolean, // true if we auto-renamed .impeccable.md -> PRODUCT.md - * contextDir: string, // absolute path of the directory the files were found in - * } - * - * Filename matching is case-insensitive for PRODUCT.md and DESIGN.md. The - * Google DESIGN.md convention is uppercase at repo root; Kiro-style and - * lowercase variants are also matched so users don't get punished for case. - * - * Lookup directory resolution (first match wins): - * 1. process.env.IMPECCABLE_CONTEXT_DIR (absolute or relative to cwd) - * 2. cwd, if PRODUCT.md / DESIGN.md / .impeccable.md is there (back-compat) - * 3. Auto-fallback subdirectories of cwd: .agents/context/, then docs/ - * 4. cwd as a default "no context found" location - * - * Legacy `.impeccable.md` -> PRODUCT.md migration only fires at cwd root; - * fallback directories are read-only as far as auto-rename is concerned. - */ - -import fs from 'node:fs'; -import path from 'node:path'; - -const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; -const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; -const LEGACY_NAMES = ['.impeccable.md']; -const FALLBACK_DIRS = ['.agents/context', 'docs']; - -/** - * Resolve the directory that holds PRODUCT.md / DESIGN.md for - * this project. Exported so other scripts (e.g. live-server.mjs) can read the - * design files from the same location the loader uses. - */ -export function resolveContextDir(cwd = process.cwd()) { - // 1. Explicit override - const envDir = process.env.IMPECCABLE_CONTEXT_DIR; - if (envDir && envDir.trim()) { - const trimmed = envDir.trim(); - return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); - } - - // 2. cwd wins if any canonical or legacy file is there. We check legacy too - // so the auto-migration path in loadContext stays predictable. - if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES, ...LEGACY_NAMES])) { - return cwd; - } - - // 3. Auto-fallback subdirs. Match if PRODUCT.md or DESIGN.md is present; - // legacy `.impeccable.md` does not pull the lookup into a fallback dir. - for (const rel of FALLBACK_DIRS) { - const candidate = path.resolve(cwd, rel); - if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { - return candidate; - } - } - - // 4. Nothing found — keep the historical "default to cwd" behaviour so the - // caller's `hasProduct === false` branch still fires the same way. - return cwd; -} - -export function loadContext(cwd = process.cwd()) { - let migrated = false; - const contextDir = resolveContextDir(cwd); - - // 1. Look for PRODUCT.md (case-insensitive) in the resolved dir - let productPath = firstExisting(contextDir, PRODUCT_NAMES); - - // 2. Legacy: if no PRODUCT.md but .impeccable.md exists at cwd root, rename - // it in place. We only migrate at the root — fallback dirs are read-only - // so we don't surprise users by mutating files under docs/ or .agents/. - if (!productPath && contextDir === cwd) { - const legacyPath = firstExisting(cwd, LEGACY_NAMES); - if (legacyPath) { - const newPath = path.join(cwd, 'PRODUCT.md'); - try { - fs.renameSync(legacyPath, newPath); - productPath = newPath; - migrated = true; - } catch { - // Rename failed (permissions, etc.) — fall back to reading legacy in place - productPath = legacyPath; - } - } - } - - // 3. DESIGN.md (case-insensitive) - const designPath = firstExisting(contextDir, DESIGN_NAMES); - - const product = productPath ? safeRead(productPath) : null; - const design = designPath ? safeRead(designPath) : null; - - return { - hasProduct: !!product, - product, - productPath: productPath ? path.relative(cwd, productPath) : null, - hasDesign: !!design, - design, - designPath: designPath ? path.relative(cwd, designPath) : null, - migrated, - contextDir, - }; -} - -function firstExisting(dir, names) { - for (const name of names) { - const abs = path.join(dir, name); - if (fs.existsSync(abs)) return abs; - } - return null; -} - -function safeRead(p) { - try { return fs.readFileSync(p, 'utf-8'); } catch { return null; } -} - -// --------------------------------------------------------------------------- -// CLI mode — print the context as JSON -// --------------------------------------------------------------------------- - -function cli() { - const result = loadContext(process.cwd()); - console.log(JSON.stringify(result, null, 2)); -} - -const _running = process.argv[1]; -if (_running?.endsWith('load-context.mjs') || _running?.endsWith('load-context.mjs/')) { - cli(); -} diff --git a/.pi/skills/impeccable/scripts/palette.mjs b/.pi/skills/impeccable/scripts/palette.mjs new file mode 100644 index 000000000..4c3f1751a --- /dev/null +++ b/.pi/skills/impeccable/scripts/palette.mjs @@ -0,0 +1,633 @@ +#!/usr/bin/env node +/** + * Brand-seed picker. Returns one OKLCH seed color + the mood it most + * naturally evokes, and teaches the model how to compose a full palette + * around it. + * + * The seed is the brand's anchor color. The 5-role palette (bg, surface, + * ink, accent, muted) is composed by the caller at runtime using their + * judgment + the brief (PRODUCT.md / DESIGN.md / user prompt), NOT picked + * from a frozen 4-color preset. + * + * Why: 4-color frozen palettes drift toward safe defaults (warm-cream bg, + * complementary accent on near-white) regardless of brief. A single seed + + * the model's own composition lets the same seed produce a dark-mode jazz + * club or a light-mode hospitality brand depending on what the brief calls + * for. Tested empirically against curated 4-color palettes; seed approach + * wins on mood-fit in 3 of 5 cases and ties on the rest. + * + * Usage: + * node scripts/palette.mjs # pick at random + * node scripts/palette.mjs --id seed-021 # pick a specific seed + * node scripts/palette.mjs --from # hash to a seed (deterministic) + * + * Env vars: + * IMPECCABLE_PALETTE_SEED — same as --from; useful for the eval harness + * to make runs reproducible. + */ + +import crypto from 'node:crypto'; + +// Seeds are inlined (129 entries, hand-curated via a tinder review of +// ~400 candidates from ColorHunt + synthesis + Radix/brand/Pantone anchors). +// Each carries a mood + strategy the judging model produced — surfaced as +// hints, not commands; the brief still drives composition. +const SEEDS = [ + { id: "seed-200", oklch: [0.360, 0.137, 0.0], + mood: "Aesop apothecary shelf — oxblood bottle glass against linen, considered and unhurried", + strategy: "Seed is a deep desaturated red-brown that reads as brand ink itself; I push primary darker toward bottle-glass oxblood, pair with a pure white surface so the red does the work, and use a clear pale-blush accent that can carry dark text in pills." }, + { id: "seed-000", oklch: [0.400, 0.130, 0.0], + mood: "oxblood leather banquette in a 1940s steakhouse — low lamplight on dark wood and burgundy", + strategy: "Near-black bg with the faintest red undertone lets the oxblood primary glow like lamplit leather; warm cream ink and a brass accent complete the chophouse register." }, + { id: "seed-002", oklch: [0.450, 0.150, 0.0], + mood: "darkroom red light — analog photography, blood-warm safelight glow on chemical trays", + strategy: "Near-black surface with a deep oxblood primary lets the seed function like a safelight in a darkroom — the bg disappears so the red becomes the only emotional signal." }, + { id: "seed-003", oklch: [0.500, 0.194, 0.0], + mood: "darkroom safelight — the deep oxblood glow of analog photography, chemical and contemplative", + strategy: "Anchored the seed as primary against pure near-black so the red reads like a single illuminated bulb in a developing room, with cool desaturated ink to evoke silver gelatin print tones." }, + { id: "seed-004", oklch: [0.546, 0.204, 3.4], + mood: "midnight boudoir — velvet rose under low lamplight, perfumed and intimate", + strategy: "Near-black surface lets the rose seed glow like silk in shadow; a warm champagne accent provides the candle-flame counterpoint without breaking the hush." }, + { id: "seed-005", oklch: [0.550, 0.180, 0.0], + mood: "smoldering vermillion at dusk — the last red ember in a blacksmith's forge, iron-rich and quietly violent", + strategy: "Near-black gallery surround lets the seed read as glowing forged metal; ink stays warm-off-white, accent shifts to a hotter ember orange so the primary feels like cooling steel against a fresh strike." }, + { id: "seed-201", oklch: [0.647, 0.262, 0.3], + mood: "Figma plugin marketplace red — confident product-brand crimson, the kind a modern dev tool uses for a 'live' indicator or a primary CTA on a pristine docs page", + strategy: "Pure white surface lets a high-chroma crimson primary do all the brand work, paired with a hue-shifted warm coral accent for hierarchy without competing saturation" }, + { id: "seed-006", oklch: [0.650, 0.160, 0.0], + mood: "1960s Italian cinema — Technicolor lipstick red against a darkened theater", + strategy: "Pure near-black surface lets a saturated cinematic red and its warm peach accent perform like film light projected in a dark room — the brand colors carry the drama, the bg disappears." }, + { id: "seed-008", oklch: [0.520, 0.200, 10.4], + mood: "Negroni hour at a Milanese bar — bittersweet crimson, vermouth and amaro under low tungsten", + strategy: "Seed is a saturated red-crimson with cinematic weight, so I sit it on near-black to let the primary glow like backlit liquor, with a warmer amber accent acting as the citrus twist against the bitter red." }, + { id: "seed-010", oklch: [0.563, 0.223, 11.0], + mood: "Negroni hour on a Milan rooftop — bittersweet crimson, aperitivo light, polished restraint", + strategy: "Seed is a vivid carmine-red with strong chroma, so the surface gets out of the way (pure white) and lets the primary do the aperitivo work, with a cooled garnet accent for tension." }, + { id: "seed-202", oklch: [0.643, 0.247, 7.0], + mood: "Glossier brand pink — modern beauty editorial, confident and current", + strategy: "Pure white bg lets a saturated rose-red primary do all the brand work, paired with a deeper crimson accent for hierarchy — the Stripe/Glossier move where the color carries the mood." }, + { id: "seed-013", oklch: [0.400, 0.130, 20.0], + mood: "Tuscan cellar at dusk — aged terracotta, oxidized iron, the deep red of decanted Sangiovese", + strategy: "Black surface lets the oxblood seed and copper accent glow like firelight on cellar stone; brand colors carry all the warmth while the room recedes." }, + { id: "seed-014", oklch: [0.450, 0.150, 20.0], + mood: "smoldering tannery — oxblood leather, cured under low workshop light", + strategy: "Anchor the deep oxblood seed as primary against a near-black architectural ground, then lift with a single warm ember accent so the leather reads burnished rather than bloody." }, + { id: "seed-016", oklch: [0.550, 0.180, 20.0], + mood: "Negroni hour on a Roman terrace — bitter campari red, vermouth, late golden light spilling on white linen", + strategy: "Pure white surface lets the campari-red primary do all the emotional work, paired with a deeper oxblood accent for bittersweet depth — Italian aperitivo restraint, not warmth-washed." }, + { id: "seed-205", oklch: [0.634, 0.254, 17.6], + mood: "Aesop apothecary bottle — considered red-coral on a clinical white surface, the kind of brand restraint where one saturated object does all the work", + strategy: "Default A pure white surface lets a single coral-red primary carry the entire brand voice; accent shifts to a deeper oxblood for hierarchy without competing chroma." }, + { id: "seed-011", oklch: [0.639, 0.207, 13.5], + mood: "Aperitivo hour in Milan — Campari glow on a white marble bar, crisp and effervescent", + strategy: "Pure white gallery backdrop lets the Campari-red primary ring like a single bitter note; ink is near-black with a whisper of warmth, accent shifts to a deeper oxblood for hierarchy without competing hues." }, + { id: "seed-015", oklch: [0.527, 0.202, 22.7], + mood: "Negroni hour on a Milanese terrace — bittersweet vermillion, aperitivo glassware catching low sun", + strategy: "Seed becomes a saturated aperitivo-red primary against pure white so the color carries the bittersweet warmth alone, paired with a deep oxblood accent for typographic gravitas." }, + { id: "seed-023", oklch: [0.427, 0.175, 29.2], + mood: "blacksmith's forge at dusk — iron heated to ember red, the deep glow of oxidized metal and quenching oil", + strategy: "Pure black bg lets the seed's ember-red glow radiate like hot iron in a dark forge; accent shifts to a copper-amber to suggest scaling metal and sparks, while ink stays near-white for tool-precise legibility." }, + { id: "seed-206", oklch: [0.614, 0.234, 28.2], + mood: "Aesop apothecary bottle — considered red-orange on lab-white, calm utility with a single confident pigment", + strategy: "Pure white surface lets a saturated vermilion primary do all the brand work, paired with a deep oxblood accent for hierarchy without introducing a second hue family" }, + { id: "seed-029", oklch: [0.665, 0.222, 25.7], + mood: "Negroni hour at a Milanese bar — bittersweet orange-red liqueur catching late afternoon light on polished marble", + strategy: "Pure white surface lets the seed's vermilion read like Campari in a glass; a deeper oxblood accent provides the bitter depth, with neutral graphite ink keeping the editorial restraint of Italian design." }, + { id: "seed-022", oklch: [0.418, 0.155, 27.2], + mood: "Pompeiian red fresco — oxidized cinnabar on a museum wall, archaeological gravity", + strategy: "Pure black gallery surface lets the seed's iron-oxide red read as a lit artifact; accent shifts to an aged terracotta amber, so primary and accent form a fired-clay duet against neutral void." }, + { id: "seed-024", oklch: [0.464, 0.169, 26.9], + mood: "Mid-century darkroom under the safelight — developer trays, oxblood leather, the quiet patience of a print emerging", + strategy: "Seed becomes a deep oxblood primary; surface stays pure black so the red glows like a safelight, with a warmer ember accent for hierarchy" }, + { id: "seed-026", oklch: [0.489, 0.190, 28.3], + mood: "smoldering ember in a blacksmith's forge — iron-hot rust, soot, and controlled fire", + strategy: "Near-black soot background lets the seed's red-orange glow like heated metal; ink is bone-white, accent is a cooler tempered-steel orange that creates internal heat gradient with the primary." }, + { id: "seed-027", oklch: [0.568, 0.208, 27.1], + mood: "Sicilian blood orange at golden hour — citrus rind, terracotta, sun on stucco", + strategy: "Seed reads as vivid blood-orange — picked pure white surface so the citrus-red primary and a deep oxblood accent do all the emotional work, like a Loro Piana editorial spread." }, + { id: "seed-028", oklch: [0.591, 0.172, 24.0], + mood: "Sienna-fired ceramic studio at dusk — terracotta cooling on a wheel, hands still dusted with slip", + strategy: "Pure black stage lets the fired-clay primary glow like a kiln ember, with a deeper oxblood accent providing tonal weight rather than hue contrast — a monochrome warm-axis play." }, + { id: "seed-033", oklch: [0.544, 0.169, 31.3], + mood: "1960s Italian terracotta workshop — fired clay, espresso, late-afternoon Mediterranean dust", + strategy: "Pure black ground lets the seed's burnt-sienna primary glow like a lit kiln, with a deeper oxblood accent for restrained warmth tension — the brand carries the heat, the surface stays out." }, + { id: "seed-207", oklch: [0.564, 0.231, 29.1], + mood: "Aesop apothecary bottle — considered red oxide, the calm authority of a well-made object on a white shelf", + strategy: "Seed becomes the singular brand voice against pure white, with a deeper oxblood accent for hierarchy — the surface disappears so the red does all the speaking." }, + { id: "seed-035", oklch: [0.663, 0.153, 32.1], + mood: "Aesop apothecary bottle — clay-fired warmth, considered retail", + strategy: "Pure white surface lets the terracotta primary do the brand work, paired with a deep umber ink and a cooler clay accent for editorial tension." }, + { id: "seed-037", oklch: [0.590, 0.188, 35.8], + mood: "Aesop apothecary bottle — considered terracotta, herbalist restraint, the warmth comes from the glass not the room", + strategy: "Seed becomes a muted terracotta primary against pure white so the brand's warmth carries entirely through the color itself; accent shifts to a deeper umber for quiet hierarchy." }, + { id: "seed-038", oklch: [0.652, 0.229, 34.8], + mood: "blown-glass furnace at dusk — molten orange iron pulled from the kiln, a craftsman's signature heat", + strategy: "Pure black stage so the seed reads as live ember; primary holds the seed's heat, accent shifts to a brass-amber a hue-step away for a 1.7+ contrast pairing without leaving the fire." }, + { id: "seed-039", oklch: [0.653, 0.185, 33.5], + mood: "Aesop apothecary bottle — considered terracotta, quiet retail craft", + strategy: "Seed becomes a grounded clay primary against pure white, paired with a deeper umber accent so the warmth lives entirely in the brand marks, not the surface." }, + { id: "seed-167", oklch: [0.495, 0.134, 36.0], + mood: "Aesop apothecary shelf — burnished terracotta on clinical white, considered craft pharmacy", + strategy: "Treat the seed as a brand-carrying burnt-sienna against a pure paper-white surface so the warmth lives entirely in the primary, with a deep umber accent pulled along the same warm axis for typographic gravity." }, + { id: "seed-147", oklch: [0.500, 0.151, 40.0], + mood: "Aesop apothecary shelf — considered terracotta, pharmacy restraint, the brand color does the work against clinical white", + strategy: "Anchor the seed's burnt-sienna primary against a pure white surface so the rust speaks alone, with a deep umber ink and a cooler clay accent to give the palette product-brand discipline rather than environmental warmth." }, + { id: "seed-040", oklch: [0.660, 0.201, 40.0], + mood: "Aesop apothecary bottle — amber glass on a clean dispensary shelf, considered and clinical-warm", + strategy: "Seed becomes a burnt-amber primary against pure white so the bottle-glass color does the emotional work; accent shifts to a deep olive-bronze for the apothecary-label pairing." }, + { id: "seed-041", oklch: [0.673, 0.217, 38.6], + mood: "Aesop apothecary shelf — considered orange glass, clinical retail restraint", + strategy: "Pure white surface lets the burnt-orange primary do all the brand work, with a deep ink-brown for editorial gravity and a muted clay accent that reads as a sibling, not a contrast." }, + { id: "seed-042", oklch: [0.688, 0.133, 35.8], + mood: "Aesop apothecary shelf — terracotta glass, considered retail", + strategy: "Seed becomes a warm clay primary against pure white so the bottle-on-marble retail feel comes from the brand color alone; a deeper umber accent gives the label-print contrast." }, + { id: "seed-043", oklch: [0.781, 0.119, 38.1], + mood: "Aesop apothecary catalogue — considered terracotta, dermatological restraint, the warm color doing all the work against clinical white", + strategy: "Pure white surface lets the seed's warm clay tone read as the entire brand voice, paired with a deeper umber accent for hierarchy without competing with the primary's warmth." }, + { id: "seed-168", oklch: [0.400, 0.103, 50.0], + mood: "Aesop apothecary bottle — amber glass on a clinical white shelf, considered and pharmaceutical", + strategy: "Pure white surface lets the deep amber primary act like tinted glass against a clean shelf; accent is a muted clay that complements without competing, keeping the brand quiet and product-led." }, + { id: "seed-044", oklch: [0.568, 0.149, 45.9], + mood: "1970s desert highway at golden hour — sun-faded terracotta, denim dust, the warmth of a Polaroid pulled from a glovebox", + strategy: "Seed becomes a burnt-sienna primary against pure white so the terracotta does all the emotional work; a deep indigo accent acts as the denim shadow opposing the sun, creating the era's signature warm/cool tension without tinting the page." }, + { id: "seed-045", oklch: [0.607, 0.163, 47.7], + mood: "Aesop apothecary shelf — considered amber glass, clinical restraint, craft pharmacy", + strategy: "Pure white bg lets the burnt-amber primary do the apothecary work alone, paired with a deeper umber accent and graphite ink for editorial calm." }, + { id: "seed-046", oklch: [0.653, 0.175, 45.0], + mood: "Aesop apothecary shelf — considered amber glass, quiet luxury, restrained craft", + strategy: "Pure black backdrop lets the warm amber primary glow like backlit apothecary glass, with a deeper rust accent providing tonal depth in the same hue family — monochromatic warm against neutral void." }, + { id: "seed-047", oklch: [0.695, 0.205, 43.2], + mood: "Aesop apothecary label — sun-warmed amber glass on a clinical countertop, restrained botanical pharmacy", + strategy: "Pure white surface lets the burnt-amber primary and a deeper sienna accent do all the brand work, like an apothecary bottle photographed under daylight." }, + { id: "seed-051", oklch: [0.704, 0.189, 49.0], + mood: "blacksmith's forge at dusk — glowing iron, hammered copper, ember light against cooling steel", + strategy: "Pure near-black surface lets the seed's molten orange burn like heated metal; accent shifts to a deeper amber-red to suggest the cooling end of the same iron, while ink stays a clean off-white so type reads like chalk on slate." }, + { id: "seed-171", oklch: [0.550, 0.124, 60.0], + mood: "Klim Type Foundry specimen page — considered ochre on paper, design-school-honest", + strategy: "Seed becomes a muted ochre primary on pure white; accent is a deep ink-navy pulled across the wheel for editorial contrast without warmth-pooling in the bg" }, + { id: "seed-148", oklch: [0.650, 0.146, 60.0], + mood: "Klim-style editorial gold — late-afternoon paper light on a serif specimen sheet, considered and dry", + strategy: "Hold the seed's amber as primary on a pure white page so the gold reads as ink rather than atmosphere, and pair with a deep aubergine accent for typographic contrast." }, + { id: "seed-052", oklch: [0.700, 0.130, 60.0], + mood: "late-afternoon terracotta studio — sun-warmed clay, hands-on craft, the hour before dusk", + strategy: "Seed is a saturated amber-ochre with strong environmental association (ceramics, adobe, sunlit plaster), so I lean into Exception (a) with a faintly warm bone surface that reads as lime-washed wall, then deepen the seed slightly for primary and pair it with a fired-clay rust accent for hand-thrown warmth." }, + { id: "seed-053", oklch: [0.773, 0.157, 56.6], + mood: "late-summer apricot orchard at golden hour — sun-warmed fruit, considered Californian craft", + strategy: "Seed is a juicy mid-warm orange at daylight luminance — leaning optimistic/editorial, so pure white surface lets the apricot primary glow without muddying it; a deep wine accent provides the bite." }, + { id: "seed-149", oklch: [0.600, 0.124, 70.0], + mood: "1970s desert highway — late-afternoon amber light on chrome and asphalt", + strategy: "Anchor the amber seed as primary against pure black so the warm hue reads as headlight glow against night; a cooler dusk-mauve accent provides the complementary tension of horizon vs. sun." }, + { id: "seed-054", oklch: [0.740, 0.162, 68.1], + mood: "late-afternoon honey on terracotta — Mediterranean stucco at golden hour, sun-baked amber", + strategy: "Seed is a saturated honey-amber at high lightness; pairing it with pure black lets the warmth read as luminous gold against gravity, like lamplight in a dark room." }, + { id: "seed-055", oklch: [0.774, 0.174, 65.1], + mood: "late-summer honey hour — amber light slanting through a west-facing window, optimistic and golden", + strategy: "Anchor a saturated honey-amber primary on pure white so the warmth radiates from the brand itself, then pair with a deep teak accent for grounded contrast rather than tinting the canvas." }, + { id: "seed-056", oklch: [0.691, 0.146, 74.6], + mood: "Klim-style modern publishing house — late-afternoon paper warmth, considered editorial gold", + strategy: "Pure white surface so the amber seed becomes the brand voice; ink stays near-black neutral and accent shifts to a deep ink-blue to give the gold something structural to lean on." }, + { id: "seed-150", oklch: [0.750, 0.148, 80.0], + mood: "Klim Type Foundry specimen page — late-summer editorial gold, considered and grown-up", + strategy: "Pure white surface lets a single restrained ochre primary do all the brand work, paired with a deep ink-blue accent for typographic contrast in the Klim/Commercial Type tradition." }, + { id: "seed-058", oklch: [0.764, 0.120, 77.1], + mood: "Klim Type Foundry specimen page — late-afternoon ochre, considered editorial typography", + strategy: "Pure white surface lets the ochre primary do the brand work, paired with a deep ink-blue accent for editorial contrast — the type-foundry move where one warm hue carries the whole feeling against neutral paper." }, + { id: "seed-059", oklch: [0.784, 0.144, 79.8], + mood: "late afternoon in a Tuscan limonaia — sun-cured amber on whitewashed plaster", + strategy: "Pure white surface lets the saffron-amber primary and a deep olive accent carry the Mediterranean warmth, with split-complementary tension between gold and a quiet evergreen." }, + { id: "seed-061", oklch: [0.817, 0.161, 75.1], + mood: "late-afternoon honey on Tuscan limestone — golden hour, slow and luminous", + strategy: "Pure white surface lets the amber primary glow like sunlight on a wall, paired with a deep terracotta accent for warm tonal contrast within the same hue family." }, + { id: "seed-063", oklch: [0.842, 0.165, 91.3], + mood: "late-afternoon Tuscan sun on limestone — golden hour, considered, optimistic", + strategy: "Pure white surface lets the amber-gold primary radiate as the mood-carrier, with a deep aubergine accent providing the long shadow that golden light needs to feel three-dimensional." }, + { id: "seed-174", oklch: [0.350, 0.075, 110.0], + mood: "olive grove at late afternoon — sun-cured leaves, dust, and quiet Mediterranean weight", + strategy: "Pure white surface lets a deep, sun-cured olive primary do the emotional work, with a burnt-terracotta accent providing the warm-earth counterpoint olive groves are known for." }, + { id: "seed-117", oklch: [0.650, 0.100, 110.0], + mood: "Klim-style editorial sage — late-summer foundry catalogue, considered olive-yellow on paper", + strategy: "Seed sits at olive-chartreuse; treating it as a quiet typographic primary on pure paper, with a deeper bronze-olive accent for hierarchy — the color does the work, the page disappears." }, + { id: "seed-118", oklch: [0.750, 0.090, 110.0], + mood: "Klim Type Foundry specimen page — late-summer olive light on a working specimen, the honesty of a type designer showing their work", + strategy: "Pure white bg lets a desaturated olive-yellow primary do the editorial work, with a deeper olive-bronze accent providing typographic emphasis the way a specimen uses one heavy weight against the body roman." }, + { id: "seed-065", oklch: [0.797, 0.166, 113.1], + mood: "late-summer olive grove at noon — sun-bleached leaves, dry stone, Mediterranean glare", + strategy: "Hold the seed as a luminous chartreuse-olive primary against pure white so the color reads as sunlit foliage, pairing it with a deep umber accent for the dry-stone contrast." }, + { id: "seed-176", oklch: [0.300, 0.071, 120.0], + mood: "moss-darkened apothecary jar — herbal, shadowed, mid-19th-century botanical study", + strategy: "Seed is a deep desaturated olive-green that reads as preserved botanical pigment; I anchor it on pure white so the dim moss-green primary feels like ink on a herbarium page, with a warm ochre accent supplying the aged-paper counterpoint." }, + { id: "seed-155", oklch: [0.550, 0.142, 130.0], + mood: "moss-bed forest floor at noon — chlorophyll, lichen, sunlit fern", + strategy: "Seed is a confident mid-olive green with strong chroma; mood is daylight botanical, so I let the brand greens do the work on a pure paper-white bg and pair with a warm umber accent for fern-against-bark contrast." }, + { id: "seed-119", oklch: [0.600, 0.154, 130.0], + mood: "moss garden at Saihō-ji — damp stone, filtered green light through old cedar", + strategy: "Pure near-black bg lets the seed's mossy green glow like wet lichen under low light; accent shifts to a pale ochre-gold like sun catching through canopy." }, + { id: "seed-179", oklch: [0.300, 0.096, 140.0], + mood: "moss on wet stone — forest floor at dusk, deep botanical hush", + strategy: "Kept the seed's deep moss green as primary against a near-black surface so the green reads as living shadow, with a pale lichen accent providing the single point of light." }, + { id: "seed-180", oklch: [0.350, 0.110, 140.0], + mood: "moss-darkened apothecary — herbal tinctures in amber glass, pressed botanicals, the deep green of a conservatory at dusk", + strategy: "Near-black bg with a whisper of green undertone lets the seed's deep moss read as luminous foliage; a warm parchment accent provides the apothecary-label counterpoint without breaking the herbal register." }, + { id: "seed-120", oklch: [0.650, 0.100, 140.0], + mood: "moss on weathered stone — quiet botanical garden conservatory at midday", + strategy: "Pure white bg lets the muted sage-green primary read as a considered botanical mark, with a deeper terracotta accent providing earthen counterpoint without breaking the gallery-like restraint." }, + { id: "seed-121", oklch: [0.750, 0.090, 140.0], + mood: "moss garden at Saihō-ji — diffuse green light filtered through wet stone and lichen", + strategy: "Pure near-black bg lets the muted sage-green primary glow like lichen under low light; a warm pale-bone accent acts as the single ray of sun cutting through canopy." }, + { id: "seed-182", oklch: [0.400, 0.106, 150.0], + mood: "moss garden at Saiho-ji — deep cultivated green under wet stone shadow, contemplative and damp", + strategy: "Near-black bg with the faintest cool-green undertone evokes shaded stone; primary holds the seed's moss tone while accent shifts to a lichen-yellow for organic counterpoint without breaking the hush." }, + { id: "seed-157", oklch: [0.550, 0.145, 150.0], + mood: "moss garden at Saiho-ji — damp stone, filtered green light through cedar canopy", + strategy: "Near-black bg with a faint green undertone evokes deep forest shadow; primary holds the seed's verdant register while accent shifts to a pale lichen-cream to mimic light catching moss." }, + { id: "seed-122", oklch: [0.600, 0.158, 150.0], + mood: "forest floor at first light — moss, lichen, and clean morning air", + strategy: "Seed reads as a living, daylight green; surface stays pure white so the green carries the freshness, with a cool teal accent pulling it toward dew rather than earth." }, + { id: "seed-195", oklch: [0.650, 0.150, 145.0], + mood: "Considered horticulture brand — botanical research lab, the green of a healthy stem photographed in clean daylight", + strategy: "Pure white surface lets the seed's vegetal green carry the entire brand voice, paired with a deep forest ink and a warm clay accent for editorial contrast." }, + { id: "seed-183", oklch: [0.350, 0.077, 160.0], + mood: "moss-stained apothecary — deep forest glass, herbal tinctures shelved in low candlelight", + strategy: "Anchored the seed as primary and built a near-black dark surface with whisper-tinted green to evoke aged apothecary glass, letting the green glow rather than shout." }, + { id: "seed-184", oklch: [0.400, 0.087, 160.0], + mood: "deep forest apothecary — moss, bottle glass, and herbal tincture under afternoon light", + strategy: "Seed becomes a botanical-bottle-green primary on pure white, paired with a warm clove-amber accent to evoke herbal pharmacy contrast without tinting the surface." }, + { id: "seed-158", oklch: [0.550, 0.119, 160.0], + mood: "moss on wet stone — forest floor after rain, mineral and quiet", + strategy: "Pure white surface lets the deep mossy green carry the entire mood; accent shifts to a damp slate-teal to sit beside primary like lichen on stone without competing." }, + { id: "seed-159", oklch: [0.600, 0.130, 160.0], + mood: "moss-covered forest apothecary — herbal tinctures in amber glass, eucalyptus shadow", + strategy: "Anchored the green seed in a near-black backdrop so it reads like botanical glassware lit from within, with a warm amber accent pulled across the wheel to evoke tincture bottles against dark wood." }, + { id: "seed-185", oklch: [0.450, 0.086, 170.0], + mood: "weathered copper patina on a Pacific Northwest greenhouse — oxidized teal, glass light, botanical hush", + strategy: "Seed sits as a deep oxidized-teal primary against pure white so the patina reads as pigment, not atmosphere; a rust-copper accent completes the verdigris/oxidation story across the warm-cool axis." }, + { id: "seed-124", oklch: [0.750, 0.080, 170.0], + mood: "sea-glass on a foggy Pacific shoreline — weathered, mineral, quietly oxidized", + strategy: "Seed is a soft desaturated teal-green; pairing it on pure white lets the mineral primary read as patinated copper-glass, with a deeper kelp-toned primary and a rusted coral accent to spark the muted teal against its complement." }, + { id: "seed-160", oklch: [0.550, 0.095, 180.0], + mood: "weathered copper patina on a museum bronze — oxidized teal, conservatorial quiet", + strategy: "Pure near-black gallery surround lets the patina-teal primary glow like a lit artifact, with a warm verdigris-adjacent accent providing the oxidation contrast against the cool seed." }, + { id: "seed-161", oklch: [0.720, 0.100, 188.0], + mood: "climate-tech dashboard — calm verdigris on plain paper, the quiet confidence of an instrument that just works", + strategy: "Seed teal carries the entire mood as a single considered brand color on pure white, with a desaturated copper accent providing warm signal against the cool primary without competing for attention." }, + { id: "seed-186", oklch: [0.450, 0.074, 200.0], + mood: "deep hydrothermal vent — mineral teal under pressure, the cold blue-green of oxidized copper in submerged light", + strategy: "Near-black surface lets the mineral teal glow as if lit from within; accent shifts toward verdigris-copper to suggest patina on submerged metal, while ink stays cool-neutral to keep the register austere rather than aquatic-cute." }, + { id: "seed-125", oklch: [0.650, 0.100, 200.0], + mood: "climate-tech dashboard — calm operational teal, the color of clean water data and atmospheric sensors", + strategy: "Pure white surface lets a single muted-teal primary do all the brand work, with a deeper marine accent providing hierarchy without competing chroma." }, + { id: "seed-126", oklch: [0.750, 0.080, 200.0], + mood: "climate-tech product brand — quiet competence, dashboards for hard infrastructure problems", + strategy: "Hold the seed's muted teal as primary, pair with a sharper cyan-leaning accent for interactive lift, and let a pure white surface do the disappearing act so the brand reads as a tool, not an atmosphere." }, + { id: "seed-162", oklch: [0.550, 0.091, 210.0], + mood: "weathered nautical instrument — patinated brass on oxidized steel, the cool blue-grey of a ship's chronometer at dawn", + strategy: "Pure white surface lets the muted teal-steel primary read as a precise instrument mark, with a warm brass accent providing the single point of patina against clinical white." }, + { id: "seed-163", oklch: [0.450, 0.086, 230.0], + mood: "deep harbor at dusk — weathered nautical instruments, brass dials on oxidized steel", + strategy: "Near-black background with subtle cool tint evokes the marine dusk; primary holds the seed's teal-blue while a warm brass accent creates the instrument-on-steel tension." }, + { id: "seed-164", oklch: [0.550, 0.105, 230.0], + mood: "deep harbor at dawn — cold steel water, fog-muted light, the quiet before the boats leave", + strategy: "Pure near-black bg lets the seed's cold marine blue read as a luminous beacon, while a pale frost-cyan accent evokes diffused dawn light cutting through fog." }, + { id: "seed-127", oklch: [0.650, 0.100, 230.0], + mood: "climate-tech dashboard — atmospheric sensor blue, calm operational clarity", + strategy: "Anchor the seed as a confident mid-blue primary on pure white so the brand color carries all the atmospheric feeling, with a deep navy accent for hierarchy and a soft slate muted for body text." }, + { id: "seed-128", oklch: [0.750, 0.080, 230.0], + mood: "climate-tech dashboard — calm atmospheric data, considered sky-blue", + strategy: "Pure white surface lets the muted sky-blue primary carry the meteorological calm, with a deep-navy accent providing readable weight against the soft primary." }, + { id: "seed-187", oklch: [0.350, 0.078, 240.0], + mood: "deep harbor at blue hour — wet stone, cold steel, the quiet before night fully lands", + strategy: "Near-black architectural bg with a hint of marine chroma lets the seed read as ambient atmosphere rather than UI chrome; a cooler steel accent sits opposite the warmer-shifted primary for navigational clarity." }, + { id: "seed-077", oklch: [0.578, 0.130, 241.7], + mood: "pre-dawn signal tower — cold blue solitude, instruments glowing against the dark", + strategy: "Pure near-black bg lets the seed's cold tower-light blue glow as the sole emotional source, with a frost-cyan accent acting as a secondary indicator light." }, + { id: "seed-188", oklch: [0.400, 0.110, 250.0], + mood: "Linear's considered indigo — the calm authority of a well-built developer tool, blueprint ink on a clean page", + strategy: "Held the seed as a deep indigo primary against pure white so the brand color carries all the gravity; accent shifts to a cooler, brighter cyan-blue to create a crisp hierarchy pair without warming the surface." }, + { id: "seed-165", oklch: [0.450, 0.123, 250.0], + mood: "blueprint room at dusk — drafting table, graphite, civic-engineering blue", + strategy: "Seed is a mid-deep architectural blue with real chroma and no environmental cue, so I stay out of the way with a pure white surface and let the primary do all the talking, pairing it with a burnt-ochre accent for drafting-pencil contrast." }, + { id: "seed-079", oklch: [0.478, 0.136, 251.8], + mood: "twilight cartography — the blue of deep dusk over open water, precise and navigational", + strategy: "Pure white surface lets the seed's oceanic blue act as a single navigational anchor, with a warm amber accent struck across it like a lighthouse beam at dusk." }, + { id: "seed-080", oklch: [0.541, 0.122, 248.2], + mood: "Linear-style considered tool blue — the calm, exact register of a modern engineering app where every pixel is intentional", + strategy: "Pure white surface lets the considered indigo-blue primary carry the entire brand; a deeper navy accent provides hierarchy without warmth, keeping the palette in a single cool family for that focused-software feel" }, + { id: "seed-166", oklch: [0.550, 0.149, 250.0], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and quietly intense", + strategy: "Near-black bg with the faintest cool tint reads like a darkened cockpit; the seed becomes a luminous instrument-blue primary, paired with a warm amber accent that mimics avionics readouts for unmistakable signal contrast." }, + { id: "seed-081", oklch: [0.650, 0.160, 250.0], + mood: "deep-sea research vessel at dawn — instrument glow against cold steel light", + strategy: "Pure near-white bg keeps the palette technical and instrument-like; the seed blue holds as primary while a desaturated steel-cyan accent reads like signal readouts on glass." }, + { id: "seed-082", oklch: [0.742, 0.140, 247.4], + mood: "high-altitude flight deck at dawn — cold cabin instruments glowing against a sky still holding night", + strategy: "Near-black cockpit ground with a faint blue cast lets the seed read as an illuminated instrument; primary holds the seed, accent shifts to cyan for signal/indicator contrast." }, + { id: "seed-210", oklch: [0.360, 0.140, 260.0], + mood: "Linear-style considered tool indigo — late-night focused work, the deep blue of a code editor at 2am where everything else falls away", + strategy: "Pure black bg lets the indigo primary carry all the cognitive-focus weight, with a slightly brighter periwinkle accent for interactive lift — the surface disappears so the tool feels weightless." }, + { id: "seed-189", oklch: [0.400, 0.130, 260.0], + mood: "pre-dawn observatory — cold instrument blue, star-chart precision", + strategy: "Seed becomes the primary on pure black so the deep instrument-blue glows like a calibration light, with a faint cyan accent reading as starlight against the void." }, + { id: "seed-211", oklch: [0.420, 0.161, 260.0], + mood: "Linear's considered indigo — the tool-for-thought blue of focused product work, calm authority without coldness", + strategy: "Hold the seed as a deep indigo primary against pure white, then pair with a slightly warmer, lighter periwinkle accent to create gentle hue separation without breaking the disciplined tool-brand register." }, + { id: "seed-129", oklch: [0.450, 0.150, 260.0], + mood: "pre-dawn observatory — deep cobalt sky just before astronomical twilight, instruments cool to the touch", + strategy: "Near-black surface lets the cobalt seed read as luminous starlight; a single warm amber accent acts as the calibration lamp against the cold blue field." }, + { id: "seed-084", oklch: [0.476, 0.207, 261.2], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and awake", + strategy: "Default B black bg lets the cobalt primary read as a luminous instrument signal, with a cyan accent striking the analogous 'cockpit display' relationship." }, + { id: "seed-085", oklch: [0.681, 0.132, 258.4], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky", + strategy: "Anchored the seed as a luminous primary against a near-black architectural ground, with a warm amber accent acting as the single instrument light cutting through cold blue." }, + { id: "seed-086", oklch: [0.767, 0.106, 255.9], + mood: "Scandinavian winter morning — quiet light through frost, pale sky over snow", + strategy: "Anchored a pure white editorial stage so the seed's cool sky-blue reads as crisp polar light, with a deeper navy primary providing the only saturated weight — like a single dark pine against snow." }, + { id: "seed-083", oklch: [0.340, 0.159, 262.4], + mood: "deep cobalt twilight — the moment after sunset when the sky goes electric blue and city windows start to glow", + strategy: "Pure black stage lets the cobalt seed act as a luminous neon-window glow, with a warm amber accent across the wheel for the lit-window contrast." }, + { id: "seed-212", oklch: [0.360, 0.219, 270.0], + mood: "Linear-grade tooling indigo — considered software for people who care about craft", + strategy: "Anchored the deep indigo seed as primary on a pure white surface so the brand color carries all the weight, with a slightly cooler violet-blue accent for hierarchy without competing chroma." }, + { id: "seed-130", oklch: [0.400, 0.150, 270.0], + mood: "Linear-grade indigo — considered productivity tool, ink on paper, no theatrics", + strategy: "Pure white surface lets a deep cool indigo carry all the brand weight, paired with a slightly warmer violet-blue accent for hierarchy without acid." }, + { id: "seed-213", oklch: [0.411, 0.241, 267.9], + mood: "Linear-style indigo — considered tool surface, the kind of blue-violet that sits behind a developer's keyboard at 11pm without shouting", + strategy: "Pure black canvas lets a saturated indigo primary do all the brand work, with a cooler cyan-violet accent providing UI signal without competing." }, + { id: "seed-131", oklch: [0.450, 0.180, 270.0], + mood: "monastic indigo dusk — vespers light through stained glass, contemplative and severe", + strategy: "Seed becomes a deep indigo primary against pure near-black so the violet reads as luminous stained-glass against architectural shadow, with a cooler iris accent for tonal lift." }, + { id: "seed-088", oklch: [0.476, 0.158, 268.5], + mood: "pre-dawn astronomer's notebook — deep indigo sky just before the stars fade, ink and graphite", + strategy: "Near-black bg with the faintest cool tint to evoke night sky without theatrics; primary holds the seed's indigo, accent shifts to a paler periwinkle for stellar contrast, keeping the palette monochromatic-cool and observational." }, + { id: "seed-196", oklch: [0.530, 0.130, 268.0], + mood: "Linear-style considered tool indigo — the deep-focus blue-violet of a thoughtfully built productivity surface, the color of a well-typeset keyboard shortcut", + strategy: "Pure white bg lets the indigo seed do all the brand work as primary, with a slightly darker, more saturated violet-shifted accent for hierarchy and interactive states — the surface disappears so the brand color reads as the entire identity." }, + { id: "seed-132", oklch: [0.700, 0.120, 270.0], + mood: "Linear-style considered tool indigo — the quiet violet of a focused product workspace, late-afternoon thinking", + strategy: "Pure white surface lets a muted indigo-violet primary and a slightly cooler accent do all the brand work, keeping the register calm and software-like rather than theatrical." }, + { id: "seed-090", oklch: [0.445, 0.206, 279.1], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, not a nightclub", + strategy: "Anchor the seed as a confident product primary on pure white, with a cooler indigo-shift accent that reads as a sibling tool color, so the brand violet does all the emotional work." }, + { id: "seed-133", oklch: [0.500, 0.160, 280.0], + mood: "Linear-adjacent indigo — considered productivity tool, the violet of a thinking workspace", + strategy: "Seed becomes a measured indigo primary on pure white; accent shifts to a cooler blue-violet to create hierarchy without nightclub saturation, letting the brand color do all the emotional work." }, + { id: "seed-094", oklch: [0.533, 0.125, 294.3], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, calm authority for a creative workspace", + strategy: "Pure white canvas lets the indigo-violet primary carry the entire brand voice; accent shifts hue slightly toward blue for a cool, tool-like duotone rather than warm decorative pairing." }, + { id: "seed-137", oklch: [0.700, 0.120, 290.0], + mood: "Linear-adjacent indigo — the considered tool, late-evening focus mode, software made for people who care about craft", + strategy: "Pure black surface lets a single restrained indigo-violet carry the brand, with a cooler periwinkle accent providing UI hierarchy without competing — Vercel/Linear dark-mode discipline." }, + { id: "seed-100", oklch: [0.450, 0.150, 330.0], + mood: "velvet boudoir at last call — bruised orchid and lipstick traces under low lamplight", + strategy: "Pure near-black surface lets a deep magenta-rose primary smolder while a warm peach accent acts like skin-lit lamplight — drama lives in the brand pair, not the room." }, + { id: "seed-103", oklch: [0.650, 0.160, 330.0], + mood: "1980s Memphis boudoir — powder-pink neon humming against lacquered black, lipstick and lacquer", + strategy: "Near-black gallery surface lets the magenta-pink seed read as lit neon; accent shifts to warm coral to create cinematic dichromatic tension without competing chroma." }, + { id: "seed-228", oklch: [0.360, 0.147, 340.0], + mood: "Figma-era creative tool plum — considered productivity software for designers, the inky violet of a serif wordmark on a marketing site", + strategy: "Held the seed as a deep plum primary against pure white so the brand color does the emotional work; paired with a muted rose accent for warmth without breaking the productivity-tool restraint." }, + { id: "seed-107", oklch: [0.500, 0.200, 340.0], + mood: "Figma plum — creative-tool confidence, considered magenta for a modern design product", + strategy: "Pure white surface lets a saturated magenta-plum primary carry all the brand voice, paired with a cooler violet-leaning accent for hierarchy without competing." }, + { id: "seed-198", oklch: [0.600, 0.210, 340.0], + mood: "Figma-era creative tool plum — confident, considered, made for makers", + strategy: "Anchor a saturated plum primary against pure white so the brand color does all the emotional work, with a deeper magenta-rose accent for hierarchy." }, + { id: "seed-112", oklch: [0.754, 0.193, 343.4], + mood: "Figma-era creative tool — confident pink primary doing the brand work on a clean canvas, the way Linear uses indigo or Stripe uses violet", + strategy: "Anchor the seed pink as a saturated brand primary on pure white so the color carries all the personality; pair with a cooler plum accent to give the pink something to push against without competing." }, + { id: "seed-229", oklch: [0.420, 0.163, 350.0], + mood: "considered fintech rose — the deep magenta of a modern product brand (think Stripe-adjacent, but rotated toward berry), confident and current", + strategy: "pure white surface lets a single deep berry-rose primary do all the brand work, paired with a cooler indigo accent for the contrast move you see in modern product marketing" }, + { id: "seed-113", oklch: [0.470, 0.173, 354.8], + mood: "1960s velvet rope nightclub — crushed magenta, low light, cigarette smoke catching a spotlight", + strategy: "Pure black stage so the seed's smoky magenta reads as a single hot spotlight, paired with a cooler violet accent for the second light cue." }, + { id: "seed-114", oklch: [0.570, 0.158, 353.3], + mood: "fin-de-siècle Parisian rose — velvet curtain, theatre program, lipstick blotted on linen", + strategy: "Drop bg to true black so the dusty-rose primary reads as stage-lit silk; accent shifts to a warmer coral-mauve at higher lightness to create gentle hue rotation without breaking the romance." }, + { id: "seed-199", oklch: [0.650, 0.180, 350.0], + mood: "modern fintech rose — the considered pink of a Series B brand mark, confident and current without nostalgia", + strategy: "Pure white surface lets a saturated rose primary do the brand work, paired with a deep plum accent for hierarchy — the Stripe move applied to a pink hue." }, + { id: "seed-115", oklch: [0.636, 0.218, 355.3], + mood: "backstage at a cabaret — velvet rope, lipstick mark on a champagne glass", + strategy: "Seed reads as a saturated stage-light magenta-red; I push it into pure black so the primary glows like a neon sign and the accent (a cold pearl-pink) acts as the spotlight rim — the room is dark, the color does the singing." }, + { id: "seed-230", oklch: [0.650, 0.249, 354.5], + mood: "Modern fintech rose — the considered pink of a contemporary payments brand: confident, alive, and clear-headed", + strategy: "Pure white bg lets a saturated rose-magenta primary carry all the brand energy, paired with a cooler indigo accent for trustworthy contrast — the Stripe move applied to a pink hue." }, + { id: "seed-231", oklch: [0.682, 0.241, 353.2], + mood: "Figma-era creative tool — a confident pink-magenta product brand, the kind a modern design platform uses to feel alive without shouting", + strategy: "Default A pure white bg lets the saturated pink-magenta primary do all the brand work, with a near-complementary cool teal accent for tool-like clarity and a neutral ink for editorial calm" }, + { id: "seed-116", oklch: [0.734, 0.183, 356.8], + mood: "modern beauty brand DTC — Glossier-adjacent pink, confident and current without being saccharine", + 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." }, +]; + +function parseArgs(argv) { + const args = { id: null, from: null }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === '--id' && argv[i + 1]) { args.id = argv[++i]; } + else if (a === '--from' && argv[i + 1]) { args.from = argv[++i]; } + } + return args; +} + +// Hash a key into a stable float in [0, 1) for deterministic weighted picks. +function hashUnit(key) { + const h = crypto.createHash('sha256').update(key).digest(); + return h.readUInt32BE(0) / 0x100000000; +} + +// The curated library is hue-skewed (more reds/oranges than teals/magentas) +// because that's where the source material + taste landed. Left uniform, a +// random pick would land on red ~1/3 of the time. Inverse-frequency weighting +// gives each seed a weight of 1/(count in its 30° hue bucket), so each hue +// ZONE is roughly equally likely to be chosen regardless of how many seeds it +// holds — fair rainbow exposure across runs without pruning the library. +function buildWeights(seeds) { + const bucketCount = {}; + const bucketOf = (s) => Math.floor(((s.oklch[2] % 360) + 360) % 360 / 30); + for (const s of seeds) { const b = bucketOf(s); bucketCount[b] = (bucketCount[b] || 0) + 1; } + const weights = seeds.map((s) => 1 / bucketCount[bucketOf(s)]); + const total = weights.reduce((a, b) => a + b, 0); + return { weights, total }; +} + +function weightedPick(seeds, unit) { + const { weights, total } = buildWeights(seeds); + let target = unit * total; + for (let i = 0; i < seeds.length; i++) { + target -= weights[i]; + if (target < 0) return seeds[i]; + } + return seeds[seeds.length - 1]; +} + +function pickSeed(seeds, { id, from }) { + if (id) { + const found = seeds.find(s => s.id === id); + if (!found) { console.error(`no seed with id "${id}"`); process.exit(2); } + return found; + } + const envFrom = process.env.IMPECCABLE_PALETTE_SEED; + const key = from || envFrom; + const unit = key ? hashUnit(key) : Math.random(); + return weightedPick(seeds, unit); +} + +function fmtOklch([L, C, H]) { + return `oklch(${L.toFixed(3)} ${C.toFixed(3)} ${H.toFixed(1)})`; +} + +function hueWord(H) { + if (H < 15 || H >= 345) return 'pure red'; + if (H < 35) return 'warm red / crimson'; + if (H < 55) return 'warm coral / burnt orange'; + if (H < 80) return 'orange / honey'; + if (H < 105) return 'warm amber / honey-gold'; + if (H < 135) return 'yellow-green / olive'; + if (H < 170) return 'green'; + if (H < 200) return 'teal'; + if (H < 230) return 'sky blue'; + if (H < 265) return 'cobalt / indigo'; + if (H < 295) return 'violet / purple'; + if (H < 330) return 'magenta / pink'; + return 'deep pink / rose'; +} + +// --------------------------------------------------------------- + +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}` : ''; + +// --------------------------------------------------------------- +// Fat tool-exit response — what the model sees on stdout. +// --------------------------------------------------------------- + +process.stdout.write(`BRAND SEED · ${seed.id} + +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. + +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. + +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é. + + 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 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. + + 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. + +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): + + - 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. + +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. + +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. +`); diff --git a/.pi/skills/impeccable/scripts/pin.mjs b/.pi/skills/impeccable/scripts/pin.mjs index ba02783fb..320d98ce1 100644 --- a/.pi/skills/impeccable/scripts/pin.mjs +++ b/.pi/skills/impeccable/scripts/pin.mjs @@ -27,7 +27,7 @@ const HARNESS_DIRS = [ // Valid sub-command names const VALID_COMMANDS = [ - 'craft', 'teach', 'extract', 'document', 'shape', + 'craft', 'init', 'extract', 'document', 'shape', 'critique', 'audit', 'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live', 'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive', diff --git a/.qoder/skills/impeccable/SKILL.md b/.qoder/skills/impeccable/SKILL.md index 5eb3d1be5..7f26e997b 100644 --- a/.qoder/skills/impeccable/SKILL.md +++ b/.qoder/skills/impeccable/SKILL.md @@ -1,10 +1,10 @@ --- name: impeccable description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks. -version: 3.1.1 +version: 3.5.0 user-invocable: true -argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · teach|document|extract|live] [target]" -license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution. +argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]" +license: Apache 2.0 allowed-tools: - Bash(npx impeccable *) --- @@ -13,85 +13,80 @@ Designs and iterates production-grade frontend interfaces. Real working code, co ## Setup -Before any design work or file edits: +You MUST do these steps before proceeding: -1. Load context (PRODUCT.md / DESIGN.md) via the loader script. -2. Identify the register and load the matching register reference (brand.md or product.md). -3. **If the user invoked a sub-command (e.g. `craft`, `shape`, `audit`), load its reference file too.** This is non-negotiable: `craft` without `craft.md` loaded means you'll skip the shape-and-confirm step the user expects. +1. Run `node .qoder/skills/impeccable/scripts/context.mjs` once per session. If you've already seen its output in this conversation, do not re-run it. The script either prints the project's PRODUCT.md (and DESIGN.md when present) as a markdown block, or tells you it's missing. Follow whatever it prints. **If it reports `NO_PRODUCT_MD`, stop and follow `reference/init.md` before doing anything else.** If the output ends with an `UPDATE_AVAILABLE` directive, follow it (ask the user once about updating, then continue). It never blocks the current task. +2. If the user invoked a sub-command (`craft`, `shape`, `audit`, `polish`, ...), you MUST read `reference/.md` next. Non-optional. The reference defines the command's flow; without it you will skip steps the user expects. +3. Familiarize yourself with any existing design system, conventions, and components in the code. Read at least one project file (CSS / tokens / theme / a representative component or page). **Required even when you've loaded a sub-command reference in step 2.** Don't reinvent the wheel; use what's there when it works, branch out when the UX wins. +4. Read the matching register reference. **This is non-optional; skipping it produces generic output.** If the project is marketing, a landing page, a campaign, long-form content, or a portfolio (design IS the product), read `reference/brand.md`. If it is app UI, admin, a dashboard, or a tool (design SERVES the product), read `reference/product.md`. Pick by first match: (1) task cue ("landing page" vs "dashboard"); (2) surface in focus (the page, file, or route being worked on); (3) `register` field in PRODUCT.md. +5. **If the project is brand-new (no existing CSS tokens / theme / committed brand colors found in step 3)**, run `node .qoder/skills/impeccable/scripts/palette.mjs` to receive a brand seed color and composition guidance. This is the anchor for your primary brand color. Compose the rest of the palette (bg, surface, ink, accent, muted) around it per the script's instructions. Use OKLCH throughout. **Skip this step only if step 3 found committed brand colors in existing tokens; in that case identity-preservation wins.** -Skipping these produces generic output that ignores the project. +## Design guidance -### 1. Context gathering +Produce ready-to-ship, production-grade code, not prototypes or starting points. Take no shortcuts unless the user asks for them (when in doubt, ask). Don't stop until arriving at a complete implementation (beautiful, responsive, fast, precise, bug-free, on brand). You take attention to detail seriously: every page, section or component crafted is battle tested using the tools available to you (browser screenshotting, computer use, etc). the model is capable of extraordinary work. Don't hold back. -Two files, case-insensitive. The loader looks at the project root by default and falls back to `.agents/context/` and `docs/` if the root is clean. Override with `IMPECCABLE_CONTEXT_DIR=path/to/dir` (absolute or relative to cwd). +### General rules -- **PRODUCT.md**: required. Users, brand, tone, anti-references, strategic principles. -- **DESIGN.md**: optional, strongly recommended. Colors, typography, elevation, components. +#### Color -Load both in one call: +- **Verify contrast.** Body text must hit ≥4.5:1 against its background; large text (≥18px or bold ≥14px) needs ≥3:1. Placeholder text needs the same 4.5:1, not the muted-gray default. The most common failure: muted gray body text on a tinted near-white. If the contrast is even close, bump the body color toward the ink end of the ramp; light gray "for elegance" is the single biggest reason AI designs feel hard to read. +- Gray text on a colored background looks washed out. Use a darker shade of the background's own hue, or a transparency of the text color. -```bash -node .qoder/skills/impeccable/scripts/load-context.mjs -``` +#### Typography -Consume the full JSON output. Never pipe through `head`, `tail`, `grep`, or `jq`. The output's `contextDir` field tells you where the files were resolved from. +- Cap body line length at 65–75ch. +- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales. +- Cap font-family count at 3 (display + body + optional mono). More than 3 reads as indecision, not richness. One well-tuned family with weight contrast usually beats three competing typefaces. +- Don't pair fonts that are similar but not identical (two geometric sans-serifs, two humanist sans-serifs). Pair on a contrast axis (serif + sans, geometric + humanist) or use one family in multiple weights. +- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. +- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. +- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". +- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. -If the output is already in this session's conversation history, don't re-run. Exceptions requiring a fresh load: you just ran `/impeccable teach` or `/impeccable document` (they rewrite the files), or the user manually edited one. +#### Layout -`/impeccable live` already warms context via `live.mjs`. If you've run `live.mjs`, don't also run `load-context.mjs` this session. +- Vary spacing for rhythm. +- Cards are the lazy answer. Use them only when they're truly the best affordance. Nested cards are always wrong. +- Flexbox for 1D, Grid for 2D. Don't default to Grid when `flex-wrap` would be simpler. +- For responsive grids without breakpoints: `repeat(auto-fit, minmax(280px, 1fr))`. +- Build a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip). Never arbitrary values like 999 or 9999. -If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `/impeccable teach`, then resume the user's original task with the fresh context. If the original task was `/impeccable craft`, resume into `/impeccable shape` before any implementation work. +#### Motion +- Motion should be intentional, and not be an afterthought. consider it as part of the build. +- Don't animate CSS layout properties unless truly needed. +- Ease out with exponential curves (ease-out-quart / quint / expo). No bounce, no elastic. +- Use libraries for more advanced motion needs (e.g. motion, gsap, anime.js, lenis etc) +- Reduced motion is not optional. Every animation needs a `@media (prefers-reduced-motion: reduce)` alternative: typically a crossfade or instant transition. +- Staggering the items within one list is legitimate. The tell is the uniform reflex (one identical entrance applied to every section), not motion itself; each reveal should fit what it reveals. Suppressing the reflex is never a reason to ship a page with no motion at all. +- Reveal animations must enhance an already-visible default. Don't gate content visibility on a class-triggered transition; transitions pause on hidden tabs and headless renderers, so the reveal never fires and the section ships blank. +- Premium motion materials are not just transform/opacity. Blur, backdrop-filter, clip-path, mask, and shadow/glow are part of the palette when they materially improve the effect and stay smooth. -If DESIGN.md is missing: nudge once per session (*"Run `/impeccable document` for more on-brand output"*), then proceed. +#### Interaction -### 2. Register +- Dropdowns rendered with `position: absolute` inside an `overflow: hidden` or `overflow: auto` container will be clipped. Use the native `` / popover API, `position: fixed`, or a portal to escape the stacking context. -Every design task is **brand** (marketing, landing, campaign, long-form content, portfolio: design IS the product) or **product** (app UI, admin, dashboard, tool: design SERVES the product). +### Copy -Identify before designing. Priority: (1) cue in the task itself ("landing page" vs "dashboard"); (2) the surface in focus (the page, file, or route being worked on); (3) `register` field in PRODUCT.md. First match wins. +- Every word earns its place. No restated headings, no intros that repeat the title. +- **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`. +- **No aphoristic-cadence body copy as a default voice.** Don't fall into the rhythm of "serious statement, then punchy short negation" as the page's recurring voice. If three or more section copy blocks on the page land on a short rebuttal-shaped sentence, rewrite. Specific, not aphoristic. +- **No marketing buzzwords.** The streamline / empower / supercharge / leverage / unleash / transform / seamless / world-class / enterprise-grade / next-generation / cutting-edge / game-changer / mission-critical family of phrases. Pick a specific noun and a verb that describes what the product literally does. +- Button labels: verb + object. "Save changes" beats "OK"; "Delete project" beats "Yes". The label should say what will happen. +- Link text needs standalone meaning. "View pricing plans" beats "Click here"; screen readers announce links out of context. -If PRODUCT.md lacks the `register` field (legacy), infer it once from its "Users" and "Product Purpose" sections, then cache the inferred value for the session. Suggest the user run `/impeccable teach` to add the field explicitly. +### New projects only (when no prior work exists) -Load the matching reference: [reference/brand.md](reference/brand.md) or [reference/product.md](reference/product.md). The shared design laws below apply to both. +#### Color & Theme -## Shared design laws - -Apply to every design, both registers. Match implementation complexity to the aesthetic vision: maximalism needs elaborate code, minimalism needs precision. Interpret creatively. Vary across projects; never converge on the same choices. the model is capable of extraordinary work. Don't hold back. - -### Color - -- Use OKLCH. Reduce chroma as lightness approaches 0 or 100; high chroma at extremes looks garish. -- Never use `#000` or `#fff`. Tint every neutral toward the brand hue (chroma 0.005–0.01 is enough). +- Use OKLCH. +- **The cream / sand / beige body bg is the saturated AI default of 2026.** The whole warm-neutral band (OKLCH L 0.84-0.97, C < 0.06, hue 40-100) reads as cream/sand/paper/parchment regardless of what you call it. Token names like `--paper`, `--cream`, `--sand`, `--bone`, `--flour`, `--linen`, `--parchment`, `--wheat`, `--biscuit`, `--ivory` are tells in themselves. If the brief is "warm, traditional, family-coastal-Italian" or "magazine-warm" or "editorial-restraint", DO NOT translate that into a near-white warm-tinted bg; that's the AI move. Pick: (a) a saturated brand color as the body (terracotta, oxblood, deep ochre, near-black), (b) a true off-white at chroma 0 (or chroma toward the brand's own hue, not toward warmth-by-default), or (c) a darker mid-tone tinted neutral that's clearly the brand's own. "Warmth" in the brand is carried by accent + typography + imagery, not by body bg. +- Tinted neutrals: add 0.005–0.015 chroma toward the brand's hue. Don't default-tint toward warm or cool "because the brand feels that way"; that's the cross-project monoculture move. +- When picking a theme: Dark vs. light is never a default. Not dark "because tools look cool dark." Not light "to be safe.".Before choosing, write one sentence of physical scene: who uses this, where, under what ambient light, in what mood. If the sentence doesn't force the answer, it's not concrete enough. Add detail until it does. - Pick a **color strategy** before picking colors. Four steps on the commitment axis: - **Restrained**: tinted neutrals + one accent ≤10%. Product default; brand minimalism. - **Committed**: one saturated color carries 30–60% of the surface. Brand default for identity-driven pages. - **Full palette**: 3–4 named roles, each used deliberately. Brand campaigns; product data viz. - **Drenched**: the surface IS the color. Brand heroes, campaign pages. -- The "one accent ≤10%" rule is Restrained only. Committed / Full palette / Drenched exceed it on purpose. Don't collapse every design to Restrained by reflex. - -### Theme - -Dark vs. light is never a default. Not dark "because tools look cool dark." Not light "to be safe." - -Before choosing, write one sentence of physical scene: who uses this, where, under what ambient light, in what mood. If the sentence doesn't force the answer, it's not concrete enough. Add detail until it does. - -"Observability dashboard" does not force an answer. "SRE glancing at incident severity on a 27-inch monitor at 2am in a dim room" does. Run the sentence, not the category. - -### Typography - -- Cap body line length at 65–75ch. -- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales. - -### Layout - -- Vary spacing for rhythm. Same padding everywhere is monotony. -- Cards are the lazy answer. Use them only when they're truly the best affordance. Nested cards are always wrong. -- Don't wrap everything in a container. Most things don't need one. - -### Motion - -- Don't animate CSS layout properties. -- Ease out with exponential curves (ease-out-quart / quint / expo). No bounce, no elastic. ### Absolute bans @@ -102,12 +97,9 @@ Match-and-refuse. If you're about to write any of these, rewrite the element wit - **Glassmorphism as default.** Blurs and glass cards used decoratively. Rare and purposeful, or nothing. - **The hero-metric template.** Big number, small label, supporting stats, gradient accent. SaaS cliché. - **Identical card grids.** Same-sized cards with icon + heading + text, repeated endlessly. -- **Modal as first thought.** Modals are usually laziness. Exhaust inline / progressive alternatives first. - -### Copy - -- Every word earns its place. No restated headings, no intros that repeat the title. -- **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`. +- **Tiny uppercase tracked eyebrow above every section.** The 2023-era kicker (small all-caps text with wide tracking, "ABOUT" "PROCESS" "PRICING" above each heading) is now the saturated AI scaffold; it appears on 55-95% of generations regardless of brief, which is the definition of a tell. One named kicker as a deliberate brand system is voice; an eyebrow on every section is AI grammar. Choose a different cadence. +- **Numbered section markers as default scaffolding (01 / 02 / 03).** Putting `01 · About / 02 · Process / 03 · Pricing` above every section is the eyebrow trope one tier deeper: reach for it because "landing pages do this" and you're scaffolding by reflex. Numbers earn their place when the section actually IS a sequence (a real 3-step process, an ordered flow, a typed timeline) and the order carries information the reader needs. One deliberate numbered sequence on one page is voice; numbered eyebrows on every section across the site is AI grammar. +- **Text that overflows its container.** Long heading words plus large clamp scales plus narrow grids cause headline overflow on tablet/mobile. Test the heading copy at every breakpoint; if it overflows, reduce the clamp max or rewrite the copy. The viewport is part of the design. ### The AI slop test @@ -115,7 +107,7 @@ If someone could look at this interface and say "AI made that" without doubt, it **Category-reflex check.** Run at two altitudes; the second one catches what the first one misses. -- **First-order:** if someone could guess the theme + palette from the category alone ("observability → dark blue", "healthcare → white + teal", "finance → navy + gold", "crypto → neon on black"), it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain. +- **First-order:** if someone could guess the theme + palette from the category alone, it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain. - **Second-order:** if someone could guess the aesthetic family from category-plus-anti-references ("AI workflow tool that's not SaaS-cream → editorial-typographic", "fintech that's not navy-and-gold → terminal-native dark mode"), it's the trap one tier deeper. The first reflex was avoided; the second wasn't. Rework until both answers are not obvious. The brand register's [reflex-reject aesthetic lanes](reference/brand.md) list catches the currently-saturated families. ## Commands @@ -124,7 +116,7 @@ If someone could look at this interface and say "AI made that" without doubt, it |---|---|---|---| | `craft [feature]` | Build | Shape, then build a feature end-to-end | [reference/craft.md](reference/craft.md) | | `shape [feature]` | Build | Plan UX/UI before writing code | [reference/shape.md](reference/shape.md) | -| `teach` | Build | Set up PRODUCT.md and DESIGN.md context | [reference/teach.md](reference/teach.md) | +| `init` | Build | Set up project context: PRODUCT.md, DESIGN.md, live config, next steps | [reference/init.md](reference/init.md) | | `document` | Build | Generate DESIGN.md from existing project code | [reference/document.md](reference/document.md) | | `extract [target]` | Build | Pull reusable tokens and components into design system | [reference/extract.md](reference/extract.md) | | `critique [target]` | Evaluate | UX design review with heuristic scoring | [reference/critique.md](reference/critique.md) | @@ -152,11 +144,14 @@ Plus two management commands: `pin ` and `unpin `, detailed be 1. **No argument**: render the table above as the user-facing command menu, grouped by category. Ask what they'd like to do. 2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. -3. **First word doesn't match**: general design invocation. Apply the setup steps, shared design laws, and the loaded register reference, using the full argument as context. +3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. +4. **No clear command match**: general design invocation. Apply the setup steps, the General rules, and the loaded register reference, using the full argument as context. Setup (context gathering, register) is already loaded by then; sub-commands don't re-invoke `/impeccable`. -If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `teach` as a blocker, finish teach, refresh context, then resume the original command and target. +If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `init` as a blocker, finish init, refresh context, then resume the original command and target. + +`teach` is a deprecated alias for `init`: if the user types it, load [reference/init.md](reference/init.md) and proceed as if they ran `init`. ## Pin / Unpin diff --git a/.qoder/skills/impeccable/reference/adapt.md b/.qoder/skills/impeccable/reference/adapt.md index 9d0673ea2..5af7606cb 100644 --- a/.qoder/skills/impeccable/reference/adapt.md +++ b/.qoder/skills/impeccable/reference/adapt.md @@ -188,3 +188,124 @@ Test thoroughly across contexts: - **Slow connections**: Test on throttled network When the adaptation feels native to each context, hand off to `/impeccable polish` for the final pass. + +--- + +## Reference Material + +The sections below were previously `responsive-design.md` and live inline now so the adapt flow has its deep responsive reference in one place. + +### Responsive Design + +#### Mobile-First: Write It Right + +Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first. + +#### Breakpoints: Content-Driven + +Don't chase device sizes; let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints. + +#### Detect Input Method, Not Just Screen Size + +**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard. Use pointer and hover queries: + +```css +/* Fine pointer (mouse, trackpad) */ +@media (pointer: fine) { + .button { padding: 8px 16px; } +} + +/* Coarse pointer (touch, stylus) */ +@media (pointer: coarse) { + .button { padding: 12px 20px; } /* Larger touch target */ +} + +/* Device supports hover */ +@media (hover: hover) { + .card:hover { transform: translateY(-2px); } +} + +/* Device doesn't support hover (touch) */ +@media (hover: none) { + .card { /* No hover state - use active instead */ } +} +``` + +**Critical**: Don't rely on hover for functionality. Touch users can't hover. + +#### Safe Areas: Handle the Notch + +Modern phones have notches, rounded corners, and home indicators. Use `env()`: + +```css +body { + padding-top: env(safe-area-inset-top); + padding-bottom: env(safe-area-inset-bottom); + padding-left: env(safe-area-inset-left); + padding-right: env(safe-area-inset-right); +} + +/* With fallback */ +.footer { + padding-bottom: max(1rem, env(safe-area-inset-bottom)); +} +``` + +**Enable viewport-fit** in your meta tag: +```html + +``` + +#### Responsive Images: Get It Right + +##### srcset with Width Descriptors + +```html +Hero image +``` + +**How it works**: +- `srcset` lists available images with their actual widths (`w` descriptors) +- `sizes` tells the browser how wide the image will display +- Browser picks the best file based on viewport width AND device pixel ratio + +##### Picture Element for Art Direction + +When you need different crops/compositions (not just resolutions): + +```html + + + + ... + +``` + +#### Layout Adaptation Patterns + +**Navigation**: Three stages: hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `
/` for content that can collapse on mobile. + +#### Testing: Don't Trust DevTools Alone + +DevTools device emulation is useful for layout but misses: + +- Actual touch interactions +- Real CPU/memory constraints +- Network latency patterns +- Font rendering differences +- Browser chrome/keyboard appearances + +**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators. + +--- + +**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful. diff --git a/.qoder/skills/impeccable/reference/animate.md b/.qoder/skills/impeccable/reference/animate.md index 20aeea659..15d9f9080 100644 --- a/.qoder/skills/impeccable/reference/animate.md +++ b/.qoder/skills/impeccable/reference/animate.md @@ -6,7 +6,7 @@ Add motion that conveys state, gives feedback, and clarifies hierarchy. Cut moti ## Register -Brand: orchestrated page-load sequences, staggered reveals, scroll-driven animation. Motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions. +Brand: motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions. The saturated AI default is fade-and-rise reveals on every scrolled section; that's a tell, not a choreography. Reserve scroll-triggered motion for moments that earn it. Product: 150–250 ms on most transitions. Motion conveys state: feedback, reveal, loading, transitions between views. No page-load choreography; users are in a task and won't wait for it. @@ -49,10 +49,11 @@ Create a purposeful animation plan: Add motion systematically across these categories: ### Entrance Animations -- **Page load choreography**: Stagger element reveals (100-150ms delays), fade + slide combinations - **Hero section**: Dramatic entrance for primary content (scale, parallax, or creative effects) -- **Content reveals**: Scroll-triggered animations using intersection observer - **Modal/drawer entry**: Smooth slide + fade, backdrop fade, focus management +- **List rhythm**: Sibling stagger is legitimate for cards-in-a-grid or list-items-appearing. Whole-section fade-on-scroll is not a list and is not legitimate. Cap total stagger time: 10 items at 50ms each = 500ms total. For more items, reduce per-item delay or cap the staggered count. + + Use CSS custom properties for clean stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"`, `style="--i: 1"`, etc. on each item. ### Micro-interactions - **Button feedback**: @@ -97,11 +98,14 @@ Use appropriate techniques for each animation: ### Timing & Easing -**Durations by purpose:** -- **100-150ms**: Instant feedback (button press, toggle) -- **200-300ms**: State changes (hover, menu open) -- **300-500ms**: Layout changes (accordion, modal) -- **500-800ms**: Entrance animations (page load) +**Duration: the 100/300/500 rule.** Timing matters more than easing for "feels right": + +| Duration | Use Case | Examples | +|----------|----------|----------| +| **100–150ms** | Instant feedback | Button press, toggle, color change | +| **200–300ms** | State changes | Menu open, tooltip, hover state | +| **300–500ms** | Layout changes | Accordion, modal, drawer | +| **500–800ms** | Entrance animations | Page load, hero reveal | **Easing curves (use these, not CSS defaults):** ```css @@ -134,13 +138,35 @@ Use appropriate techniques for each animation: - GSAP for complex sequences ``` +### Motion Materials + +Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties. Match material to effect: + +- **Transform / opacity**: movement, press feedback, simple reveals, list choreography +- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances +- **Clip-path / masks**: wipes, reveals, editorial cropping, product-like transitions +- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state +- **Grid-template-rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly + +The hard rule isn't "transform and opacity only." It's: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify smoothness in-browser on target viewports. + ### Performance -- **Motion materials**: Use transform/opacity for reliable movement, but use blur, filters, masks, shadows, and color shifts when they materially improve the effect - **Layout safety**: Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins) -- **will-change**: Add sparingly for known expensive animations +- **will-change**: Add sparingly for known expensive animations only (e.g. on `:hover` or an `.animating` class), never preemptively across the whole page +- **Scroll triggers**: Use Intersection Observer instead of scroll event listeners; unobserve after the animation fires once - **Bound expensive effects**: Keep blur/filter/shadow areas small or isolated, use `contain` where appropriate - **Monitor FPS**: Ensure 60fps on target devices +### Perceived Performance + +Nobody cares how fast your site *is*, only how fast it feels. The 80ms threshold: anything under ~80ms feels instant because our brains buffer sensory input for that long to synchronize perception. Target this for micro-interactions. + +- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening. +- **Early completion**: Show content progressively, don't wait for everything (progressive images, streaming HTML, skeleton fade-ins). +- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Use for low-stakes actions (likes, follows). Avoid for payments or destructive operations. +- **Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances. +- **Caution**: Too-fast responses can decrease perceived value for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening. + ### Accessibility ```css @media (prefers-reduced-motion: reduce) { diff --git a/.qoder/skills/impeccable/reference/bolder.md b/.qoder/skills/impeccable/reference/bolder.md index ff0ef2f14..30d4e852c 100644 --- a/.qoder/skills/impeccable/reference/bolder.md +++ b/.qoder/skills/impeccable/reference/bolder.md @@ -50,7 +50,7 @@ Create a strategy to increase impact while maintaining coherence: Systematically increase impact across these dimensions: ### Typography Amplification -- **Replace generic fonts**: Swap system fonts for distinctive choices (see the parent skill's typography guidelines and [typography.md](typography.md) for inspiration) +- **Replace generic fonts**: Swap system fonts for distinctive choices (see the parent skill's typography guidelines and the [Reference Material section of typeset.md](typeset.md#reference-material) for inspiration) - **Extreme scale**: Create dramatic size jumps (3x-5x differences, not 1.5x) - **Weight contrast**: Pair 900 weights with 200 weights, not 600 with 400 - **Unexpected choices**: Variable fonts, display fonts for headlines, condensed/extended widths, monospace as intentional accent (not as lazy "dev tool" default) @@ -78,10 +78,10 @@ Systematically increase impact across these dimensions: - **Custom elements**: Illustrative elements, custom icons, decorative details that reinforce brand ### Motion & Animation -- **Entrance choreography**: Staggered, dramatic page load animations with 50-100ms delays -- **Scroll effects**: Parallax, reveal animations, scroll-triggered sequences -- **Micro-interactions**: Satisfying hover effects, click feedback, state changes -- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic, which cheapen the effect) +- **Hero moment**: One signature entrance, once. Not on every visit and not on every section. +- **Micro-interactions**: Satisfying hover effects, click feedback, state changes. +- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic, which cheapen the effect). +- **Bolder ≠ scroll-fade-rise on every section.** That's the saturated AI default, the opposite of bold. ### Composition Boldness - **Hero moments**: Create clear focal points with dramatic treatment diff --git a/.qoder/skills/impeccable/reference/brand.md b/.qoder/skills/impeccable/reference/brand.md index 3d83a1cdc..194514e95 100644 --- a/.qoder/skills/impeccable/reference/brand.md +++ b/.qoder/skills/impeccable/reference/brand.md @@ -43,17 +43,10 @@ The reflex-reject lists apply to **new design choices**. When the existing brand ### Pairing and voice -Distinctive + refined is the goal. The specific shape depends on the brand: - -- **Editorial / long-form / luxury**: display serif + sans body (a magazine shape). -- **Tech / dev tools / fintech**: one committed sans, usually; custom-tight tracking, strong weight contrast inside a single family. -- **Consumer / food / travel**: warmer pairings, often a humanist sans plus a script or display serif. -- **Creative studios / agencies**: rule-breaking welcome. Mono-only, or display-only, or custom-drawn type as voice. +Distinctive + refined is the goal. The specific shape depends on the brand, not on the brand's category. A category ("restaurant", "dev tool", "magazine", "fintech") is not a recipe; treating it as one is the first-order reflex SKILL.md warns against. Two families minimum is the rule *only* when the voice needs it. A single well-chosen family with committed weight/size contrast is stronger than a timid display+body pair. -Vary across projects. If the last brief was a serif-display landing page, this one isn't. - ### Scale Modular scale, fluid `clamp()` for headings, ≥1.25 ratio between steps. Flat scales (1.1× apart) read as uncommitted. @@ -74,8 +67,7 @@ Brand surfaces have permission for Committed, Full palette, and Drenched strateg - Asymmetric compositions are one option. Break the grid intentionally for emphasis. - Fluid spacing with `clamp()` that breathes on larger viewports. Vary for rhythm: generous separations, tight groupings. -- Alternative: a strict, visible grid as the voice (brutalist / Swiss / tech-spec aesthetics). Either asymmetric or rigorously-gridded can be "designed"; the failure mode is splitting the difference into a generic centered stack. -- Don't default to centering everything. Left-aligned with asymmetric layouts feels more designed; a strict grid reads as confident structure. A centered-stack hero with icon-title-subtitle cards reads as template. +- For image-led briefs (hotels, restaurants, magazines, photography), full-bleed hero imagery with overlaid menu and centered headline is a canonical move; let the photograph be the design. - When cards ARE the right affordance, use `grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` for breakpoint-free responsiveness. ## Imagery @@ -93,8 +85,7 @@ Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landin ## Motion -- One well-orchestrated page-load with staggered reveals beats scattered micro-interactions, when the brand invites it. Tech-minimal brands often skip entrance motion entirely; the restraint is the voice. -- For collapsing/expanding sections, transition `grid-template-rows` rather than `height`. +- One well-orchestrated page-load beats scattered micro-interactions, when the brand invites it. Some brands skip entrance motion entirely; the restraint is the voice. ## Brand bans (on top of the shared absolute bans) @@ -111,8 +102,7 @@ Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landin Brand can afford things product can't. Take them. -- Ambitious first-load motion. Reveals, scroll-triggered transitions, typographic choreography. +- Ambitious first-load motion. Reveals and typographic choreography that earn their place; not fade-on-scroll for every section. - Single-purpose viewports. One dominant idea per fold, long scroll, deliberate pacing. -- Typographic risk. Enormous display type, unexpected italic cuts, mixed cases, hand-drawn headlines, a single oversize word as a hero. - Unexpected color strategies. Palette IS voice; a calm brand and a restless brand should not share palette mechanics. - Art direction per section. Different sections can have different visual worlds if the narrative demands it. Consistency of voice beats consistency of treatment. diff --git a/.qoder/skills/impeccable/reference/clarify.md b/.qoder/skills/impeccable/reference/clarify.md index 5f7d420f4..00ee978c3 100644 --- a/.qoder/skills/impeccable/reference/clarify.md +++ b/.qoder/skills/impeccable/reference/clarify.md @@ -172,3 +172,117 @@ Test that copy improvements work: - **Tone**: Is it appropriate for the situation? When the copy reads cleanly, hand off to `/impeccable polish` for the final pass. + +--- + +## Reference Material + +The sections below were previously `ux-writing.md` and live inline now so the clarify flow has its deep UX-writing reference in one place. + +### UX Writing + +#### The Button Label Problem + +**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns: + +| Bad | Good | Why | +|-----|------|-----| +| OK | Save changes | Says what will happen | +| Submit | Create account | Outcome-focused | +| Yes | Delete message | Confirms the action | +| Cancel | Keep editing | Clarifies what "cancel" means | +| Click here | Download PDF | Describes the destination | + +**For destructive actions**, name the destruction: +- "Delete" not "Remove" (delete is permanent, remove implies recoverable) +- "Delete 5 items" not "Delete selected" (show the count) + +#### Error Messages: The Formula + +Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input". + +##### Error Message Templates + +| Situation | Template | +|-----------|----------| +| **Format error** | "[Field] needs to be [format]. Example: [example]" | +| **Missing required** | "Please enter [what's missing]" | +| **Permission denied** | "You don't have access to [thing]. [What to do instead]" | +| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." | +| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" | + +##### Don't Blame the User + +Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date". + +#### Empty States Are Opportunities + +Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items". + +#### Voice vs Tone + +**Voice** is your brand's personality, consistent everywhere. +**Tone** adapts to the moment. + +| Moment | Tone Shift | +|--------|------------| +| Success | Celebratory, brief: "Done! Your changes are live." | +| Error | Empathetic, helpful: "That didn't work. Here's what to try..." | +| Loading | Reassuring: "Saving your work..." | +| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | + +**Never use humor for errors.** Users are already frustrated. Be helpful, not cute. + +#### Writing for Accessibility + +**Link text** must have standalone meaning: "View pricing plans" not "Click here". **Alt text** describes information, not the image: "Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context. + +#### Writing for Translation + +##### Plan for Expansion + +German text is ~30% longer than English. Allocate space: + +| Language | Expansion | +|----------|-----------| +| German | +30% | +| French | +20% | +| Finnish | +30-40% | +| Chinese | -30% (fewer chars, but same width) | + +##### Translation-Friendly Patterns + +Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear. + +#### Consistency: The Terminology Problem + +Pick one term and stick with it: + +| Inconsistent | Consistent | +|--------------|------------| +| Delete / Remove / Trash | Delete | +| Settings / Preferences / Options | Settings | +| Sign in / Log in / Enter | Sign in | +| Create / Add / New | Create | + +Build a terminology glossary and enforce it. Variety creates confusion. + +#### Avoid Redundant Copy + +If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well. + +#### Loading States + +Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress. + +#### Confirmation Dialogs: Use Sparingly + +Most confirmation dialogs are design failures; consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No"). + +#### Form Instructions + +Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking. + +--- + +**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors. diff --git a/.qoder/skills/impeccable/reference/codex.md b/.qoder/skills/impeccable/reference/codex.md index f40ec746f..d563af95b 100644 --- a/.qoder/skills/impeccable/reference/codex.md +++ b/.qoder/skills/impeccable/reference/codex.md @@ -23,8 +23,8 @@ Before generating anything, run a brief direction conversation grounded in the s Ask **2-3 targeted questions** about visual lane, color strategy, atmosphere, and named anchor references. Don't enumerate generic menus; tie each question to the shape brief's answers. Example shape-grounded questions: -- "Brief says 'editorial restraint, Klim-adjacent.' Are we closer to a quiet specimen page or a magazine-spread feel with hero imagery?" -- "Palette strategy from shape was 'Committed.' Want it warm-grounded (deep oxblood + cream) or cool-grounded (slate + paper white)?" +- "Brief says 'specimen-page restraint.' Are we closer to a quiet typographic page or a wider editorial spread with hero imagery?" +- "Palette strategy from shape was 'Committed.' Which one color carries the surface (a brand-driven pick rather than a default warm-or-cool framing)? (And no, the answer isn't a cream/sand body bg; that's the saturated AI default.)" **STOP and wait for answers.** These pin the palette before any pixel gets generated. Do not proceed to Step B until the user has responded. diff --git a/.qoder/skills/impeccable/reference/cognitive-load.md b/.qoder/skills/impeccable/reference/cognitive-load.md deleted file mode 100644 index 48f8ad58b..000000000 --- a/.qoder/skills/impeccable/reference/cognitive-load.md +++ /dev/null @@ -1,106 +0,0 @@ -# Cognitive Load Assessment - -Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload. - ---- - -## Three Types of Cognitive Load - -### Intrinsic Load: The Task Itself -Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it. - -**Manage it by**: -- Breaking complex tasks into discrete steps -- Providing scaffolding (templates, defaults, examples) -- Progressive disclosure: show what's needed now, hide the rest -- Grouping related decisions together - -### Extraneous Load: Bad Design -Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste. - -**Common sources**: -- Confusing navigation that requires mental mapping -- Unclear labels that force users to guess meaning -- Visual clutter competing for attention -- Inconsistent patterns that prevent learning -- Unnecessary steps between user intent and result - -### Germane Load: Learning Effort -Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery. - -**Support it by**: -- Progressive disclosure that reveals complexity gradually -- Consistent patterns that reward learning -- Feedback that confirms correct understanding -- Onboarding that teaches through action, not walls of text - ---- - -## Cognitive Load Checklist - -Evaluate the interface against these 8 items: - -- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements? -- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)? -- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)? -- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen? -- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next? -- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)? -- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one? -- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it? - -**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed). - ---- - -## The Working Memory Rule - -**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001). - -At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider: -- **≤4 items**: Within working memory limits, manageable -- **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure -- **8+ items**: Overloaded; users will skip, misclick, or abandon - -**Practical applications**: -- Navigation menus: ≤5 top-level items (group the rest under clear categories) -- Form sections: ≤4 fields visible per group before a visual break -- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu -- Dashboard widgets: ≤4 key metrics visible without scrolling -- Pricing tiers: ≤3 options (more causes analysis paralysis) - ---- - -## Common Cognitive Load Violations - -### 1. The Wall of Options -**Problem**: Presenting 10+ choices at once with no hierarchy. -**Fix**: Group into categories, highlight recommended, use progressive disclosure. - -### 2. The Memory Bridge -**Problem**: User must remember info from step 1 to complete step 3. -**Fix**: Keep relevant context visible, or repeat it where it's needed. - -### 3. The Hidden Navigation -**Problem**: User must build a mental map of where things are. -**Fix**: Always show current location (breadcrumbs, active states, progress indicators). - -### 4. The Jargon Barrier -**Problem**: Technical or domain language forces translation effort. -**Fix**: Use plain language. If domain terms are unavoidable, define them inline. - -### 5. The Visual Noise Floor -**Problem**: Every element has the same visual weight; nothing stands out. -**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted. - -### 6. The Inconsistent Pattern -**Problem**: Similar actions work differently in different places. -**Fix**: Standardize interaction patterns. Same type of action = same type of UI. - -### 7. The Multi-Task Demand -**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating). -**Fix**: Sequence the steps. Let the user do one thing at a time. - -### 8. The Context Switch -**Problem**: User must jump between screens/tabs/modals to gather info for a single decision. -**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth. diff --git a/.qoder/skills/impeccable/reference/color-and-contrast.md b/.qoder/skills/impeccable/reference/color-and-contrast.md deleted file mode 100644 index 110c2ee47..000000000 --- a/.qoder/skills/impeccable/reference/color-and-contrast.md +++ /dev/null @@ -1,105 +0,0 @@ -# Color & Contrast - -## Color Spaces: Use OKLCH - -**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal, unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark. - -The OKLCH function takes three components: `oklch(lightness chroma hue)` where lightness is 0-100%, chroma is roughly 0-0.4, and hue is 0-360. To build a primary color and its lighter / darker variants, hold the chroma+hue roughly constant and vary the lightness, but **reduce chroma as you approach white or black**, because high chroma at extreme lightness looks garish. - -The hue you pick is a brand decision and should not come from a default. Do not reach for blue (hue 250) or warm orange (hue 60) by reflex; those are the dominant AI-design defaults, not the right answer for any specific brand. - -## Building Functional Palettes - -### Tinted Neutrals - -**Pure gray is dead.** A neutral with zero chroma feels lifeless next to a colored brand. Add a tiny chroma value (0.005-0.015) to all your neutrals, hued toward whatever your brand color is. The chroma is small enough not to read as "tinted" consciously, but it creates subconscious cohesion between brand color and UI surfaces. - -The hue you tint toward should come from THIS project's brand, not from a "warm = friendly, cool = tech" formula. If your brand color is teal, your neutrals lean toward teal. If your brand color is amber, they lean toward amber. The point is cohesion with the SPECIFIC brand, not a stock palette. - -**Avoid** the trap of always tinting toward warm orange or always tinting toward cool blue. Those are the two laziest defaults and they create their own monoculture across projects. - -### Palette Structure - -A complete system needs: - -| Role | Purpose | Example | -|------|---------|---------| -| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades | -| **Neutral** | Text, backgrounds, borders | 9-11 shade scale | -| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each | -| **Surface** | Cards, modals, overlays | 2-3 elevation levels | - -**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise. - -### The 60-30-10 Rule (Applied Correctly) - -This rule is about **visual weight**, not pixel count: - -- **60%**: Neutral backgrounds, white space, base surfaces -- **30%**: Secondary colors: text, borders, inactive states -- **10%**: Accent: CTAs, highlights, focus states - -The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power. - -## Contrast & Accessibility - -### WCAG Requirements - -| Content Type | AA Minimum | AAA Target | -|--------------|------------|------------| -| Body text | 4.5:1 | 7:1 | -| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | -| UI components, icons | 3:1 | 4.5:1 | -| Non-essential decorations | None | None | - -**The gotcha**: Placeholder text still needs 4.5:1. That light gray placeholder you see everywhere? Usually fails WCAG. - -### Dangerous Color Combinations - -These commonly fail contrast or cause readability issues: - -- Light gray text on white (the #1 accessibility fail) -- **Gray text on any colored background**: gray looks washed out and dead on color. Use a darker shade of the background color, or transparency -- Red text on green background (or vice versa): 8% of men can't distinguish these -- Blue text on red background (vibrates visually) -- Yellow text on white (almost always fails) -- Thin light text on images (unpredictable contrast) - -### Never Use Pure Gray or Pure Black - -Pure gray (`oklch(50% 0 0)`) and pure black (`#000`) don't exist in nature; real shadows and surfaces always have a color cast. Even a chroma of 0.005-0.01 is enough to feel natural without being obviously tinted. (See tinted neutrals example above.) - -### Testing - -Don't trust your eyes. Use tools: - -- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) -- Browser DevTools → Rendering → Emulate vision deficiencies -- [Polypane](https://polypane.app/) for real-time testing - -## Theming: Light & Dark Mode - -### Dark Mode Is Not Inverted Light Mode - -You can't just swap colors. Dark mode requires different design decisions: - -| Light Mode | Dark Mode | -|------------|-----------| -| Shadows for depth | Lighter surfaces for depth (no shadows) | -| Dark text on light | Light text on dark (reduce font weight) | -| Vibrant accents | Desaturate accents slightly | -| White backgrounds | Never pure black; use dark gray (oklch 12-18%) | - -In dark mode, depth comes from surface lightness, not shadow. Build a 3-step surface scale where higher elevations are lighter (e.g. 15% / 20% / 25% lightness). Use the SAME hue and chroma as your brand color (whatever it is for THIS project; do not reach for blue) and only vary the lightness. Reduce body text weight slightly (e.g. 350 instead of 400) because light text on dark reads as heavier than dark text on light. - -### Token Hierarchy - -Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer; primitives stay the same. - -## Alpha Is A Design Smell - -Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed. - ---- - -**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Using pure black (#000) for large areas. Skipping color blindness testing (8% of men affected). diff --git a/.qoder/skills/impeccable/reference/colorize.md b/.qoder/skills/impeccable/reference/colorize.md index 9e96eef59..2457f608c 100644 --- a/.qoder/skills/impeccable/reference/colorize.md +++ b/.qoder/skills/impeccable/reference/colorize.md @@ -68,10 +68,10 @@ Add color systematically across these dimensions: - **Hover states**: Introduce color on interaction ### Background & Surfaces -- **Tinted backgrounds**: Replace pure gray (`#f5f5f5`) with warm neutrals (`oklch(97% 0.01 60)`) or cool tints (`oklch(97% 0.01 250)`) +- **Tinted backgrounds**: If you replace pure gray, tint toward the brand hue, not toward a generic-warm-or-cool pair. The default-warm-tint (`oklch(97% 0.01 60)` and its neighbors) is now the AI cream/sand giveaway. Be specific to the brand or stay neutral. - **Colored sections**: Use subtle background colors to separate areas - **Gradient backgrounds**: Add depth with subtle, intentional gradients (not generic purple-blue) -- **Cards & surfaces**: Tint cards or surfaces slightly for warmth +- **Cards & surfaces**: Tint cards or surfaces toward the brand, not "for warmth" by reflex **Use OKLCH for color**: It's perceptually uniform, meaning equal steps in lightness *look* equal. Great for generating harmonious scales. @@ -124,8 +124,6 @@ Ensure color addition improves rather than overwhelms: - Use every color in the rainbow (choose 2-4 colors beyond neutrals) - Apply color randomly without semantic meaning - Put gray text on colored backgrounds. It looks washed out; use a darker shade of the background color or transparency instead -- Use pure gray for neutrals. Add subtle color tint (warm or cool) for depth -- Use pure black (`#000`) or pure white (`#fff`) for large areas - Violate WCAG contrast requirements - Use color as the only indicator (accessibility issue) - Make everything colorful (defeats the purpose) @@ -152,3 +150,108 @@ When invoked from live mode, each variant MUST declare a `color-amount` param so ``` Layer 1-2 variant-specific params on top: palette selection (`steps` with named options), temperature warmth, or tint vs. true color. See `reference/live.md` for the full params contract. + +--- + +## Reference Material + +The sections below were previously `color-and-contrast.md` and live inline now so the colorize flow has its deep color reference in one place. + +### Color & Contrast + +#### Color Spaces: Use OKLCH + +**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal, unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark. + +The OKLCH function takes three components: `oklch(lightness chroma hue)` where lightness is 0-100%, chroma is roughly 0-0.4, and hue is 0-360. To build a primary color and its lighter / darker variants, hold the chroma+hue roughly constant and vary the lightness, but **reduce chroma as you approach white or black**, because high chroma at extreme lightness looks garish. + +The hue you pick is a brand decision and should not come from a default. Do not reach for blue (hue 250) or warm orange (hue 60) by reflex; those are the dominant AI-design defaults, not the right answer for any specific brand. + +#### Building Functional Palettes + +##### Tinted Neutrals + +**Pure gray is dead.** A neutral with zero chroma feels lifeless next to a colored brand. Add a tiny chroma value (0.005-0.015) to all your neutrals, hued toward whatever your brand color is. The chroma is small enough not to read as "tinted" consciously, but it creates subconscious cohesion between brand color and UI surfaces. + +The hue you tint toward should come from THIS project's brand, not from a "warm = friendly, cool = tech" formula. If your brand color is teal, your neutrals lean toward teal. If your brand color is amber, they lean toward amber. The point is cohesion with the SPECIFIC brand, not a stock palette. + +**Avoid** the trap of always tinting toward warm orange or always tinting toward cool blue. Those are the two laziest defaults and they create their own monoculture across projects. + +##### Palette Structure + +A complete system needs: + +| Role | Purpose | Example | +|------|---------|---------| +| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades | +| **Neutral** | Text, backgrounds, borders | 9-11 shade scale | +| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each | +| **Surface** | Cards, modals, overlays | 2-3 elevation levels | + +**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise. + +##### The 60-30-10 Rule (Applied Correctly) + +This rule is about **visual weight**, not pixel count: + +- **60%**: Neutral backgrounds, white space, base surfaces +- **30%**: Secondary colors: text, borders, inactive states +- **10%**: Accent: CTAs, highlights, focus states + +The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power. + +#### Contrast & Accessibility + +##### WCAG Requirements + +| Content Type | AA Minimum | AAA Target | +|--------------|------------|------------| +| Body text | 4.5:1 | 7:1 | +| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | +| UI components, icons | 3:1 | 4.5:1 | +| Non-essential decorations | None | None | + +##### Dangerous Color Combinations + +These commonly fail contrast or cause readability issues: + +- Light gray text on white (the #1 accessibility fail) +- Red text on green background (or vice versa): 8% of men can't distinguish these +- Blue text on red background (vibrates visually) +- Yellow text on white (almost always fails) +- Thin light text on images (unpredictable contrast) + +##### Testing + +Don't trust your eyes. Use tools: + +- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) +- Browser DevTools → Rendering → Emulate vision deficiencies +- [Polypane](https://polypane.app/) for real-time testing + +#### Theming: Light & Dark Mode + +##### Dark Mode Is Not Inverted Light Mode + +You can't just swap colors. Dark mode requires different design decisions: + +| Light Mode | Dark Mode | +|------------|-----------| +| Shadows for depth | Lighter surfaces for depth (no shadows) | +| Dark text on light | Light text on dark (reduce font weight) | +| Vibrant accents | Desaturate accents slightly | +| White backgrounds | Either pure black or a deep surface that fits the brand (a brand-tinted near-black at oklch 12-18% works too) | + +In dark mode, depth comes from surface lightness, not shadow. Build a 3-step surface scale where higher elevations are lighter (e.g. 15% / 20% / 25% lightness). Use the SAME hue and chroma as your brand color (whatever it is for THIS project; do not reach for blue) and only vary the lightness. Reduce body text weight slightly (e.g. 350 instead of 400) because light text on dark reads as heavier than dark text on light. + +##### Token Hierarchy + +Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer; primitives stay the same. + +#### Alpha Is A Design Smell + +Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed. + +--- + +**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Skipping color blindness testing (8% of men affected). diff --git a/.qoder/skills/impeccable/reference/craft.md b/.qoder/skills/impeccable/reference/craft.md index 06094b1dd..2c7bd99b1 100644 --- a/.qoder/skills/impeccable/reference/craft.md +++ b/.qoder/skills/impeccable/reference/craft.md @@ -56,15 +56,15 @@ If the harness has native image generation (Codex), a compact shape's "confirm o Based on the design brief's "Recommended References" section, consult the relevant impeccable reference files. At minimum, always consult: -- [spatial-design.md](spatial-design.md) for layout and spacing -- [typography.md](typography.md) for type hierarchy +- [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 [motion-design.md](motion-design.md) -- Color-heavy or themed? Consult [color-and-contrast.md](color-and-contrast.md) -- Responsive requirements? Consult [responsive-design.md](responsive-design.md) -- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.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) diff --git a/.qoder/skills/impeccable/reference/critique.md b/.qoder/skills/impeccable/reference/critique.md index 8cc69a5cb..d9453f40b 100644 --- a/.qoder/skills/impeccable/reference/critique.md +++ b/.qoder/skills/impeccable/reference/critique.md @@ -38,9 +38,9 @@ Read relevant source files and visually inspect the live page when browser autom Evaluate: - **AI slop**: Would someone believe "AI made this" immediately? Check all DON'T guidance from the parent Impeccable skill. - **Holistic design**: hierarchy, IA, emotional fit, discoverability, composition, typography, color, accessibility, states, copy, and edge cases. -- **Cognitive load**: consult [cognitive-load](cognitive-load.md); report checklist failures and decision points with >4 visible options. +- **Cognitive load**: consult the [Cognitive Load Assessment](#cognitive-load-assessment) section below; report checklist failures and decision points with >4 visible options. - **Emotional journey**: peak-end rule, emotional valleys, reassurance at high-stakes moments. -- **Nielsen heuristics**: consult [heuristics-scoring](heuristics-scoring.md); score all 10 heuristics 0-4. +- **Nielsen heuristics**: consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below; score all 10 heuristics 0-4. Return: AI slop verdict, heuristic scores, cognitive load, emotional journey, 2-3 strengths, 3-5 priority issues, persona red flags, minor observations, and provocative questions. @@ -80,7 +80,7 @@ The chat response is the primary user-facing deliverable. Present the full struc Structure your feedback as a design director would: #### Design Health Score -> *Consult [heuristics-scoring](heuristics-scoring.md)* +> *Consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below.* Present the Nielsen's 10 heuristics scores as a table: @@ -119,16 +119,16 @@ Highlight 2-3 things done well. Be specific about why they work. #### Priority Issues The 3-5 most impactful design problems, ordered by importance. -For each issue, tag with **P0-P3 severity** (consult [heuristics-scoring](heuristics-scoring.md) for severity definitions): +For each issue, tag with **P0-P3 severity** (see [Issue Severity below](#issue-severity-p0p3) for definitions): - **[P?] What**: Name the problem clearly - **Why it matters**: How this hurts users or undermines goals - **Fix**: What to do about it (be concrete) - **Suggested command**: Which command could address this (from: /impeccable adapt, /impeccable animate, /impeccable audit, /impeccable bolder, /impeccable clarify, /impeccable colorize, /impeccable critique, /impeccable delight, /impeccable distill, /impeccable document, /impeccable harden, /impeccable layout, /impeccable onboard, /impeccable optimize, /impeccable overdrive, /impeccable polish, /impeccable quieter, /impeccable shape, /impeccable typeset) #### Persona Red Flags -> *Consult [personas](personas.md)* +> *Consult the [Personas reference](#persona-based-design-testing) below.* -Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `AGENTS.md` contains a `## Design Context` section from `impeccable teach`, also generate 1-2 project-specific personas from the audience/brand info. +Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `AGENTS.md` contains a `## Design Context` section from `impeccable init`, also generate 1-2 project-specific personas from the audience/brand info. For each selected persona, walk through the primary user action and list specific red flags found: @@ -234,3 +234,534 @@ After presenting the summary, tell the user: > You can ask me to run these one at a time, all at once, or in any order you prefer. > > Re-run `/impeccable critique` after fixes to see your score improve. + +--- + +## Reference Material + +The sections below were previously separate reference files (`cognitive-load.md`, `heuristics-scoring.md`, `personas.md`). They live inline now so the critique flow has all its deep context in one place. + +### Cognitive Load Assessment + +Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload. + +--- + +#### Three Types of Cognitive Load + +##### Intrinsic Load: The Task Itself +Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it. + +**Manage it by**: +- Breaking complex tasks into discrete steps +- Providing scaffolding (templates, defaults, examples) +- Progressive disclosure: show what's needed now, hide the rest +- Grouping related decisions together + +##### Extraneous Load: Bad Design +Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste. + +**Common sources**: +- Confusing navigation that requires mental mapping +- Unclear labels that force users to guess meaning +- Visual clutter competing for attention +- Inconsistent patterns that prevent learning +- Unnecessary steps between user intent and result + +##### Germane Load: Learning Effort +Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery. + +**Support it by**: +- Progressive disclosure that reveals complexity gradually +- Consistent patterns that reward learning +- Feedback that confirms correct understanding +- Onboarding that teaches through action, not walls of text + +--- + +#### Cognitive Load Checklist + +Evaluate the interface against these 8 items: + +- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements? +- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)? +- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)? +- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen? +- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next? +- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)? +- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one? +- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it? + +**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed). + +--- + +#### The Working Memory Rule + +**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001). + +At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider: +- **≤4 items**: Within working memory limits, manageable +- **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure +- **8+ items**: Overloaded; users will skip, misclick, or abandon + +**Practical applications**: +- Navigation menus: ≤5 top-level items (group the rest under clear categories) +- Form sections: ≤4 fields visible per group before a visual break +- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu +- Dashboard widgets: ≤4 key metrics visible without scrolling +- Pricing tiers: ≤3 options (more causes analysis paralysis) + +--- + +#### Common Cognitive Load Violations + +##### 1. The Wall of Options +**Problem**: Presenting 10+ choices at once with no hierarchy. +**Fix**: Group into categories, highlight recommended, use progressive disclosure. + +##### 2. The Memory Bridge +**Problem**: User must remember info from step 1 to complete step 3. +**Fix**: Keep relevant context visible, or repeat it where it's needed. + +##### 3. The Hidden Navigation +**Problem**: User must build a mental map of where things are. +**Fix**: Always show current location (breadcrumbs, active states, progress indicators). + +##### 4. The Jargon Barrier +**Problem**: Technical or domain language forces translation effort. +**Fix**: Use plain language. If domain terms are unavoidable, define them inline. + +##### 5. The Visual Noise Floor +**Problem**: Every element has the same visual weight; nothing stands out. +**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted. + +##### 6. The Inconsistent Pattern +**Problem**: Similar actions work differently in different places. +**Fix**: Standardize interaction patterns. Same type of action = same type of UI. + +##### 7. The Multi-Task Demand +**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating). +**Fix**: Sequence the steps. Let the user do one thing at a time. + +##### 8. The Context Switch +**Problem**: User must jump between screens/tabs/modals to gather info for a single decision. +**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth. + +--- + +### Heuristics Scoring Guide + +Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough." + +#### Nielsen's 10 Heuristics + +##### 1. Visibility of System Status + +Keep users informed about what's happening through timely, appropriate feedback. + +**Check for**: +- Loading indicators during async operations +- Confirmation of user actions (save, submit, delete) +- Progress indicators for multi-step processes +- Current location in navigation (breadcrumbs, active states) +- Form validation feedback (inline, not just on submit) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | No feedback; user is guessing what happened | +| 1 | Rare feedback; most actions produce no visible response | +| 2 | Partial; some states communicated, major gaps remain | +| 3 | Good; most operations give clear feedback, minor gaps | +| 4 | Excellent; every action confirms, progress is always visible | + +##### 2. Match Between System and Real World + +Speak the user's language. Follow real-world conventions. Information appears in natural, logical order. + +**Check for**: +- Familiar terminology (no unexplained jargon) +- Logical information order matching user expectations +- Recognizable icons and metaphors +- Domain-appropriate language for the target audience +- Natural reading flow (left-to-right, top-to-bottom priority) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Pure tech jargon, alien to users | +| 1 | Mostly confusing; requires domain expertise to navigate | +| 2 | Mixed; some plain language, some jargon leaks through | +| 3 | Mostly natural; occasional term needs context | +| 4 | Speaks the user's language fluently throughout | + +##### 3. User Control and Freedom + +Users need a clear "emergency exit" from unwanted states without extended dialogue. + +**Check for**: +- Undo/redo functionality +- Cancel buttons on forms and modals +- Clear navigation back to safety (home, previous) +- Easy way to clear filters, search, selections +- Escape from long or multi-step processes + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Users get trapped; no way out without refreshing | +| 1 | Difficult exits; must find obscure paths to escape | +| 2 | Some exits; main flows have escape, edge cases don't | +| 3 | Good control; users can exit and undo most actions | +| 4 | Full control; undo, cancel, back, and escape everywhere | + +##### 4. Consistency and Standards + +Users shouldn't wonder whether different words, situations, or actions mean the same thing. + +**Check for**: +- Consistent terminology throughout the interface +- Same actions produce same results everywhere +- Platform conventions followed (standard UI patterns) +- Visual consistency (colors, typography, spacing, components) +- Consistent interaction patterns (same gesture = same behavior) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Inconsistent everywhere; feels like different products stitched together | +| 1 | Many inconsistencies; similar things look/behave differently | +| 2 | Partially consistent; main flows match, details diverge | +| 3 | Mostly consistent; occasional deviation, nothing confusing | +| 4 | Fully consistent; cohesive system, predictable behavior | + +##### 5. Error Prevention + +Better than good error messages is a design that prevents problems in the first place. + +**Check for**: +- Confirmation before destructive actions (delete, overwrite) +- Constraints preventing invalid input (date pickers, dropdowns) +- Smart defaults that reduce errors +- Clear labels that prevent misunderstanding +- Autosave and draft recovery + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Errors easy to make; no guardrails anywhere | +| 1 | Few safeguards; some inputs validated, most aren't | +| 2 | Partial prevention; common errors caught, edge cases slip | +| 3 | Good prevention; most error paths blocked proactively | +| 4 | Excellent; errors nearly impossible through smart constraints | + +##### 6. Recognition Rather Than Recall + +Minimize memory load. Make objects, actions, and options visible or easily retrievable. + +**Check for**: +- Visible options (not buried in hidden menus) +- Contextual help when needed (tooltips, inline hints) +- Recent items and history +- Autocomplete and suggestions +- Labels on icons (not icon-only navigation) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Heavy memorization; users must remember paths and commands | +| 1 | Mostly recall; many hidden features, few visible cues | +| 2 | Some aids; main actions visible, secondary features hidden | +| 3 | Good recognition; most things discoverable, few memory demands | +| 4 | Everything discoverable; users never need to memorize | + +##### 7. Flexibility and Efficiency of Use + +Accelerators, invisible to novices, speed up expert interaction. + +**Check for**: +- Keyboard shortcuts for common actions +- Customizable interface elements +- Recent items and favorites +- Bulk/batch actions +- Power user features that don't complicate the basics + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | One rigid path; no shortcuts or alternatives | +| 1 | Limited flexibility; few alternatives to the main path | +| 2 | Some shortcuts; basic keyboard support, limited bulk actions | +| 3 | Good accelerators; keyboard nav, some customization | +| 4 | Highly flexible; multiple paths, power features, customizable | + +##### 8. Aesthetic and Minimalist Design + +Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose. + +**Check for**: +- Only necessary information visible at each step +- Clear visual hierarchy directing attention +- Purposeful use of color and emphasis +- No decorative clutter competing for attention +- Focused, uncluttered layouts + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Overwhelming; everything competes for attention equally | +| 1 | Cluttered; too much noise, hard to find what matters | +| 2 | Some clutter; main content clear, periphery noisy | +| 3 | Mostly clean; focused design, minor visual noise | +| 4 | Perfectly minimal; every element earns its pixel | + +##### 9. Help Users Recognize, Diagnose, and Recover from Errors + +Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution. + +**Check for**: +- Plain language error messages (no error codes for users) +- Specific problem identification ("Email is missing @" not "Invalid input") +- Actionable recovery suggestions +- Errors displayed near the source of the problem +- Non-blocking error handling (don't wipe the form) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Cryptic errors; codes, jargon, or no message at all | +| 1 | Vague errors; "Something went wrong" with no guidance | +| 2 | Clear but unhelpful; names the problem but not the fix | +| 3 | Clear with suggestions; identifies problem and offers next steps | +| 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work | + +##### 10. Help and Documentation + +Even if the system is usable without docs, help should be easy to find, task-focused, and concise. + +**Check for**: +- Searchable help or documentation +- Contextual help (tooltips, inline hints, guided tours) +- Task-focused organization (not feature-organized) +- Concise, scannable content +- Easy access without leaving current context + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | No help available anywhere | +| 1 | Help exists but hard to find or irrelevant | +| 2 | Basic help; FAQ or docs exist, not contextual | +| 3 | Good documentation; searchable, mostly task-focused | +| 4 | Excellent contextual help; right info at the right moment | + +--- + +#### Score Summary + +**Total possible**: 40 points (10 heuristics × 4 max) + +| Score Range | Rating | What It Means | +|-------------|--------|---------------| +| 36–40 | Excellent | Minor polish only; ship it | +| 28–35 | Good | Address weak areas, solid foundation | +| 20–27 | Acceptable | Significant improvements needed before users are happy | +| 12–19 | Poor | Major UX overhaul required; core experience broken | +| 0–11 | Critical | Redesign needed; unusable in current state | + +--- + +#### Issue Severity (P0–P3) + +Tag each individual issue found during scoring with a priority level: + +| Priority | Name | Description | Action | +|----------|------|-------------|--------| +| **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper | +| **P1** | Major | Causes significant difficulty or confusion | Fix before release | +| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass | +| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits | + +**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1. + +--- + +### Persona-Based Design Testing + +Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss. + +**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns. + +--- + +#### 1. Impatient Power User: "Alex" + +**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave. + +**Behaviors**: +- Skips all onboarding and instructions +- Looks for keyboard shortcuts immediately +- Tries to bulk-select, batch-edit, and automate +- Gets frustrated by required steps that feel unnecessary +- Abandons if anything feels slow or patronizing + +**Test Questions**: +- Can Alex complete the core task in under 60 seconds? +- Are there keyboard shortcuts for common actions? +- Can onboarding be skipped entirely? +- Do modals have keyboard dismiss (Esc)? +- Is there a "power user" path (shortcuts, bulk actions)? + +**Red Flags** (report these specifically): +- Forced tutorials or unskippable onboarding +- No keyboard navigation for primary actions +- Slow animations that can't be skipped +- One-item-at-a-time workflows where batch would be natural +- Redundant confirmation steps for low-risk actions + +--- + +#### 2. Confused First-Timer: "Jordan" + +**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out. + +**Behaviors**: +- Reads all instructions carefully +- Hesitates before clicking anything unfamiliar +- Looks for help or support constantly +- Misunderstands jargon and abbreviations +- Takes the most literal interpretation of any label + +**Test Questions**: +- Is the first action obviously clear within 5 seconds? +- Are all icons labeled with text? +- Is there contextual help at decision points? +- Does terminology assume prior knowledge? +- Is there a clear "back" or "undo" at every step? + +**Red Flags** (report these specifically): +- Icon-only navigation with no labels +- Technical jargon without explanation +- No visible help option or guidance +- Ambiguous next steps after completing an action +- No confirmation that an action succeeded + +--- + +#### 3. Accessibility-Dependent User: "Sam" + +**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences. + +**Behaviors**: +- Tabs through the interface linearly +- Relies on ARIA labels and heading structure +- Cannot see hover states or visual-only indicators +- Needs adequate color contrast (4.5:1 minimum) +- May use browser zoom up to 200% + +**Test Questions**: +- Can the entire primary flow be completed keyboard-only? +- Are all interactive elements focusable with visible focus indicators? +- Do images have meaningful alt text? +- Is color contrast WCAG AA compliant (4.5:1 for text)? +- Does the screen reader announce state changes (loading, success, errors)? + +**Red Flags** (report these specifically): +- Click-only interactions with no keyboard alternative +- Missing or invisible focus indicators +- Meaning conveyed by color alone (red = error, green = success) +- Unlabeled form fields or buttons +- Time-limited actions without extension option +- Custom components that break screen reader flow + +--- + +#### 4. Deliberate Stress Tester: "Riley" + +**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience. + +**Behaviors**: +- Tests edge cases intentionally (empty states, long strings, special characters) +- Submits forms with unexpected data (emoji, RTL text, very long values) +- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs +- Looks for inconsistencies between what the UI promises and what actually happens +- Documents problems methodically + +**Test Questions**: +- What happens at the edges (0 items, 1000 items, very long text)? +- Do error states recover gracefully or leave the UI in a broken state? +- What happens on refresh mid-workflow? Is state preserved? +- Are there features that appear to work but produce broken results? +- How does the UI handle unexpected input (emoji, special chars, paste from Excel)? + +**Red Flags** (report these specifically): +- Features that appear to work but silently fail or produce wrong results +- Error handling that exposes technical details or leaves UI in a broken state +- Empty states that show nothing useful ("No results" with no guidance) +- Workflows that lose user data on refresh or navigation +- Inconsistent behavior between similar interactions in different parts of the UI + +--- + +#### 5. Distracted Mobile User: "Casey" + +**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection. + +**Behaviors**: +- Uses thumb only; prefers bottom-of-screen actions +- Gets interrupted mid-flow and returns later +- Switches between apps frequently +- Has limited attention span and low patience +- Types as little as possible, prefers taps and selections + +**Test Questions**: +- Are primary actions in the thumb zone (bottom half of screen)? +- Is state preserved if the user leaves and returns? +- Does it work on slow connections (3G)? +- Can forms use autocomplete and smart defaults? +- Are touch targets at least 44×44pt? + +**Red Flags** (report these specifically): +- Important actions positioned at the top of the screen (unreachable by thumb) +- No state persistence; progress lost on tab switch or interruption +- Large text inputs required where selection would work +- Heavy assets loading on every page (no lazy loading) +- Tiny tap targets or targets too close together + +--- + +#### Selecting Personas + +Choose personas based on the interface type: + +| Interface Type | Primary Personas | Why | +|---------------|-----------------|-----| +| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile | +| Dashboard / admin | Alex, Sam | Power users, accessibility | +| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity | +| Onboarding flow | Jordan, Casey | Confusion, interruption | +| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav | +| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile | + +--- + +#### Project-Specific Personas + +If `AGENTS.md` contains a `## Design Context` section (generated by `impeccable init`), derive 1–2 additional personas from the audience and brand information: + +1. Read the target audience description +2. Identify the primary user archetype not covered by the 5 predefined personas +3. Create a persona following this template: + +``` +##### [Role]: "[Name]" + +**Profile**: [2-3 key characteristics derived from Design Context] + +**Behaviors**: [3-4 specific behaviors based on the described audience] + +**Red Flags**: [3-4 things that would alienate this specific user type] +``` + +Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists. diff --git a/.qoder/skills/impeccable/reference/document.md b/.qoder/skills/impeccable/reference/document.md index 2722dc872..60075688a 100644 --- a/.qoder/skills/impeccable/reference/document.md +++ b/.qoder/skills/impeccable/reference/document.md @@ -45,7 +45,7 @@ Rules that matter: - **Token refs** use `{path.to.token}` (e.g. `{colors.primary}`, `{rounded.md}`). Components may reference primitives; primitives may not reference each other. - **Stitch validates colors as hex sRGB only** (`#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`); OKLCH/HSL/P3 trigger a linter warning, not a hard error. YAML accepts the string either way and our own parser is format-agnostic. Choose based on project posture: (a) if the project has an "OKLCH-only" doctrine or uses Display-P3 values that don't round-trip through sRGB, put OKLCH directly in the frontmatter and accept the Stitch linter warning; (b) if the project wants strict Stitch compliance or plans to use their Tailwind/DTCG export pipeline, put hex in the frontmatter and keep OKLCH in prose as the canonical reference. Never split the source of truth without explicit reason. - **Component sub-tokens** are limited to 8 props: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, `width`. Shadows, motion, focus rings, backdrop-filter: none of those fit. Carry them in the sidecar (Step 4b). -- **Scale keys are open-ended.** Use whatever names the project already uses (`warm-ash-cream`, `surface-container-low`). Don't rename to Material defaults. +- **Scale keys are open-ended.** Use whatever names the project already uses (`oxblood-deep`, `surface-container-low`). Don't rename to Material defaults. - **Variants are naming convention, not schema.** `button-primary` / `button-primary-hover` / `button-primary-active` as sibling keys. ## The markdown body: six sections (exact order) @@ -61,7 +61,7 @@ Optional evocative subtitles are allowed in the form `## 2. Colors: The [Name] P ## When to run -- The user just ran `/impeccable teach` and needs the visual side documented. +- The user just ran `/impeccable init` and needs the visual side documented. - The skill noticed no `DESIGN.md` exists and nudged the user to create one. - An existing `DESIGN.md` is stale (the design has drifted). - Before a large redesign, to capture the current state as a reference. @@ -71,7 +71,7 @@ If a `DESIGN.md` already exists, **do not silently overwrite it**. Show the user ## Two paths - **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 teach, nothing built yet). Interview for five high-level answers, write a minimal DESIGN.md marked ``. Re-run in scan mode once there's code. +- **Seed mode**: the project is pre-implementation (fresh init, nothing built yet). Interview for five high-level answers, 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` forces seed mode regardless of code presence. @@ -103,7 +103,7 @@ Build a structured draft from the discovered tokens. For each token class: From the auto-extracted tokens, draft the YAML frontmatter now (you'll write it at the top of DESIGN.md in Step 4). This is the machine-readable layer: what the live panel and Stitch's linter consume. -- **Colors**: one entry per extracted color. Key = descriptive slug (`warm-ash-cream`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. +- **Colors**: one entry per extracted color. Key = descriptive slug (`oxblood-deep`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. - **Typography**: one entry per role (`display`, `headline`, `title`, `body`, `label`). Typography is an object; include only the props that are real for the project (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`). - **Rounded / Spacing**: whatever scale steps the project actually uses, keyed by whatever scale name the project uses (`sm` / `md` / `lg`, or `surface-sm`, or numeric steps). - **Components**: one entry per variant (`button-primary`, `button-primary-hover`, `button-ghost`). Reference primitives via `{colors.X}`, `{rounded.Y}`. If a variant needs a property Stitch's 8-prop set doesn't cover (shadow, focus ring, backdrop-filter), carry the full snippet in the sidecar instead. @@ -253,7 +253,7 @@ Regenerate the sidecar whenever you regenerate root `DESIGN.md`. If the user onl "extensions": { "colorMeta": { "primary": { "role": "primary", "displayName": "Editorial Magenta", "canonical": "oklch(60% 0.25 350)", "tonalRamp": ["...", "...", "..."] }, - "warm-ash-cream": { "role": "neutral", "displayName": "Warm Ash Cream", "canonical": "oklch(96% 0.005 350)", "tonalRamp": ["...", "...", "..."] } + "cool-paper": { "role": "neutral", "displayName": "Cool Paper", "canonical": "oklch(96% 0.005 230)", "tonalRamp": ["...", "...", "..."] } }, "typographyMeta": { "display": { "displayName": "Display", "purpose": "Hero headlines only." } @@ -328,12 +328,13 @@ Pull directly from the DESIGN.md you just wrote: Do not reword. The panel shows these as secondary collapsible context; the same voice that's in the Markdown carries through. -### Step 5: Confirm, refine, and refresh session cache +### Step 5: Confirm and refine 1. Show the user the full DESIGN.md you wrote. Briefly highlight the non-obvious creative choices (descriptive color names, atmosphere language, named rules). 2. Mention that `.impeccable/design.json` was also written alongside; the live panel will now render this project's actual button/input/nav primitives instead of generic approximations. 3. Offer to refine any section: "Want me to revise a section, add component patterns I missed, or adjust the atmosphere language?" -4. **Refresh the session cache.** Run `node .qoder/skills/impeccable/scripts/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. + +Your own write is the freshest source; subsequent commands in this session don't need a reload. ## Seed mode @@ -394,11 +395,12 @@ Per-section guidance in seed mode: 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. -### Step 4: Confirm and refresh session cache +### Step 4: Confirm 1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment). 2. Tell the user: "Re-run `/impeccable document` once you have some code. That pass will extract real tokens and generate the sidecar." -3. Run `node .qoder/skills/impeccable/scripts/load-context.mjs` once so the seed lands in conversation for the rest of the session. + +Your own write is the freshest source; no reload needed. ## Style guidelines diff --git a/.qoder/skills/impeccable/reference/heuristics-scoring.md b/.qoder/skills/impeccable/reference/heuristics-scoring.md deleted file mode 100644 index edbe5028d..000000000 --- a/.qoder/skills/impeccable/reference/heuristics-scoring.md +++ /dev/null @@ -1,234 +0,0 @@ -# Heuristics Scoring Guide - -Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough." - -## Nielsen's 10 Heuristics - -### 1. Visibility of System Status - -Keep users informed about what's happening through timely, appropriate feedback. - -**Check for**: -- Loading indicators during async operations -- Confirmation of user actions (save, submit, delete) -- Progress indicators for multi-step processes -- Current location in navigation (breadcrumbs, active states) -- Form validation feedback (inline, not just on submit) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | No feedback; user is guessing what happened | -| 1 | Rare feedback; most actions produce no visible response | -| 2 | Partial; some states communicated, major gaps remain | -| 3 | Good; most operations give clear feedback, minor gaps | -| 4 | Excellent; every action confirms, progress is always visible | - -### 2. Match Between System and Real World - -Speak the user's language. Follow real-world conventions. Information appears in natural, logical order. - -**Check for**: -- Familiar terminology (no unexplained jargon) -- Logical information order matching user expectations -- Recognizable icons and metaphors -- Domain-appropriate language for the target audience -- Natural reading flow (left-to-right, top-to-bottom priority) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Pure tech jargon, alien to users | -| 1 | Mostly confusing; requires domain expertise to navigate | -| 2 | Mixed; some plain language, some jargon leaks through | -| 3 | Mostly natural; occasional term needs context | -| 4 | Speaks the user's language fluently throughout | - -### 3. User Control and Freedom - -Users need a clear "emergency exit" from unwanted states without extended dialogue. - -**Check for**: -- Undo/redo functionality -- Cancel buttons on forms and modals -- Clear navigation back to safety (home, previous) -- Easy way to clear filters, search, selections -- Escape from long or multi-step processes - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Users get trapped; no way out without refreshing | -| 1 | Difficult exits; must find obscure paths to escape | -| 2 | Some exits; main flows have escape, edge cases don't | -| 3 | Good control; users can exit and undo most actions | -| 4 | Full control; undo, cancel, back, and escape everywhere | - -### 4. Consistency and Standards - -Users shouldn't wonder whether different words, situations, or actions mean the same thing. - -**Check for**: -- Consistent terminology throughout the interface -- Same actions produce same results everywhere -- Platform conventions followed (standard UI patterns) -- Visual consistency (colors, typography, spacing, components) -- Consistent interaction patterns (same gesture = same behavior) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Inconsistent everywhere; feels like different products stitched together | -| 1 | Many inconsistencies; similar things look/behave differently | -| 2 | Partially consistent; main flows match, details diverge | -| 3 | Mostly consistent; occasional deviation, nothing confusing | -| 4 | Fully consistent; cohesive system, predictable behavior | - -### 5. Error Prevention - -Better than good error messages is a design that prevents problems in the first place. - -**Check for**: -- Confirmation before destructive actions (delete, overwrite) -- Constraints preventing invalid input (date pickers, dropdowns) -- Smart defaults that reduce errors -- Clear labels that prevent misunderstanding -- Autosave and draft recovery - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Errors easy to make; no guardrails anywhere | -| 1 | Few safeguards; some inputs validated, most aren't | -| 2 | Partial prevention; common errors caught, edge cases slip | -| 3 | Good prevention; most error paths blocked proactively | -| 4 | Excellent; errors nearly impossible through smart constraints | - -### 6. Recognition Rather Than Recall - -Minimize memory load. Make objects, actions, and options visible or easily retrievable. - -**Check for**: -- Visible options (not buried in hidden menus) -- Contextual help when needed (tooltips, inline hints) -- Recent items and history -- Autocomplete and suggestions -- Labels on icons (not icon-only navigation) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Heavy memorization; users must remember paths and commands | -| 1 | Mostly recall; many hidden features, few visible cues | -| 2 | Some aids; main actions visible, secondary features hidden | -| 3 | Good recognition; most things discoverable, few memory demands | -| 4 | Everything discoverable; users never need to memorize | - -### 7. Flexibility and Efficiency of Use - -Accelerators, invisible to novices, speed up expert interaction. - -**Check for**: -- Keyboard shortcuts for common actions -- Customizable interface elements -- Recent items and favorites -- Bulk/batch actions -- Power user features that don't complicate the basics - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | One rigid path; no shortcuts or alternatives | -| 1 | Limited flexibility; few alternatives to the main path | -| 2 | Some shortcuts; basic keyboard support, limited bulk actions | -| 3 | Good accelerators; keyboard nav, some customization | -| 4 | Highly flexible; multiple paths, power features, customizable | - -### 8. Aesthetic and Minimalist Design - -Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose. - -**Check for**: -- Only necessary information visible at each step -- Clear visual hierarchy directing attention -- Purposeful use of color and emphasis -- No decorative clutter competing for attention -- Focused, uncluttered layouts - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Overwhelming; everything competes for attention equally | -| 1 | Cluttered; too much noise, hard to find what matters | -| 2 | Some clutter; main content clear, periphery noisy | -| 3 | Mostly clean; focused design, minor visual noise | -| 4 | Perfectly minimal; every element earns its pixel | - -### 9. Help Users Recognize, Diagnose, and Recover from Errors - -Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution. - -**Check for**: -- Plain language error messages (no error codes for users) -- Specific problem identification ("Email is missing @" not "Invalid input") -- Actionable recovery suggestions -- Errors displayed near the source of the problem -- Non-blocking error handling (don't wipe the form) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Cryptic errors; codes, jargon, or no message at all | -| 1 | Vague errors; "Something went wrong" with no guidance | -| 2 | Clear but unhelpful; names the problem but not the fix | -| 3 | Clear with suggestions; identifies problem and offers next steps | -| 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work | - -### 10. Help and Documentation - -Even if the system is usable without docs, help should be easy to find, task-focused, and concise. - -**Check for**: -- Searchable help or documentation -- Contextual help (tooltips, inline hints, guided tours) -- Task-focused organization (not feature-organized) -- Concise, scannable content -- Easy access without leaving current context - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | No help available anywhere | -| 1 | Help exists but hard to find or irrelevant | -| 2 | Basic help; FAQ or docs exist, not contextual | -| 3 | Good documentation; searchable, mostly task-focused | -| 4 | Excellent contextual help; right info at the right moment | - ---- - -## Score Summary - -**Total possible**: 40 points (10 heuristics × 4 max) - -| Score Range | Rating | What It Means | -|-------------|--------|---------------| -| 36–40 | Excellent | Minor polish only; ship it | -| 28–35 | Good | Address weak areas, solid foundation | -| 20–27 | Acceptable | Significant improvements needed before users are happy | -| 12–19 | Poor | Major UX overhaul required; core experience broken | -| 0–11 | Critical | Redesign needed; unusable in current state | - ---- - -## Issue Severity (P0–P3) - -Tag each individual issue found during scoring with a priority level: - -| Priority | Name | Description | Action | -|----------|------|-------------|--------| -| **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper | -| **P1** | Major | Causes significant difficulty or confusion | Fix before release | -| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass | -| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits | - -**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1. diff --git a/.rovodev/skills/impeccable/reference/teach.md b/.qoder/skills/impeccable/reference/init.md similarity index 58% rename from .rovodev/skills/impeccable/reference/teach.md rename to .qoder/skills/impeccable/reference/init.md index 4b45f28d2..d5704f607 100644 --- a/.rovodev/skills/impeccable/reference/teach.md +++ b/.qoder/skills/impeccable/reference/init.md @@ -1,21 +1,16 @@ -# Teach Flow +# Init Flow -Gathers design context for a project and writes two complementary files at the project root: +The setup command for a project. One codebase crawl feeds everything it writes: - **PRODUCT.md** (strategic): root project file for register, target users, product purpose, brand personality, anti-references, strategic design principles. Answers "who/what/why". - **DESIGN.md** (visual): root project file for visual theme, color palette, typography, components, layout. Follows the [Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/). Answers "how it looks". +- **`.impeccable/live/config.json`** (live mode): pre-configured so `/impeccable live` boots straight into variant mode with no first-time detour. -Every other impeccable command reads these files before doing any work. +It closes by pointing the user at the best command to run next. Every other impeccable command reads PRODUCT.md and DESIGN.md before doing any work. ## Step 1: Load current state -Run the shared loader first so you know what already exists: - -```bash -node .rovodev/skills/impeccable/scripts/load-context.mjs -``` - -The output tells you whether PRODUCT.md and/or DESIGN.md already exist. If `migrated: true`, legacy `.impeccable.md` was auto-renamed to `PRODUCT.md`. Mention this once to the user. +Check what already exists. PRODUCT.md and DESIGN.md live at the project root, or under `.agents/context/` or `docs/` (case-insensitive). Read whichever are present with your native file tool. Also note whether `.impeccable/live/config.json` already exists (Step 6 leaves it untouched if so). Decision tree: - **Neither file exists (empty project or no context yet)**: do Steps 2-4 (write PRODUCT.md), then decide on DESIGN.md based on whether there's code to analyze. @@ -26,14 +21,14 @@ Decision tree: Never silently overwrite an existing file. Always confirm first. -If teach was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete teach, re-run the loader, then resume the original command with the freshly loaded context. For craft, resume into shape next; teach creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief. +If init was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete init, then resume the original command. Your own writes are the freshest source; no reload needed. For craft, resume into shape next; init creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief. ## Step 2: Explore the codebase -Before asking questions, thoroughly scan the project to discover what you can: +Before asking questions, thoroughly scan the project to discover what you can. This single crawl feeds PRODUCT.md, DESIGN.md, **and** the live-mode framework detection in Step 6, so be thorough once rather than re-scanning later: - **README and docs**: Project purpose, target audience, any stated goals -- **Package.json / config files**: Tech stack, dependencies, existing design libraries +- **Package.json / config files**: Tech stack, dependencies, existing design libraries, **and the framework** (Vite/SPA, Next.js, Nuxt, SvelteKit, Astro, multi-page static) plus the HTML entry the browser actually loads - **Existing components**: Current design patterns, spacing, typography in use - **Brand assets**: Logos, favicons, color values already defined - **Design tokens / CSS variables**: Existing color palettes, font stacks, spacing scales @@ -46,7 +41,7 @@ Also form a **register hypothesis** from what you find: Register is a hypothesis at this point, not a decision; Step 3 confirms it. -Note what you've learned and what remains unclear. This exploration feeds both PRODUCT.md and DESIGN.md. +Note what you've learned and what remains unclear. Also note any rough edges worth a follow-up command (thin hierarchy, flat or gray palette, missing error/empty states, dull copy); Step 7 turns these into concrete recommendations without re-analyzing. ## Step 3: Ask strategic questions (for PRODUCT.md) @@ -84,8 +79,7 @@ If the signal is genuinely split (e.g. a product with a big marketing landing), ### Brand & Personality - How would you describe the brand personality in 3 words? - Reference sites or apps that capture the right feel? What specifically about them? - - For brand, push for real-world references in the right lane (tech-minimal, editorial-magazine, consumer-warm, brutalist-grid, etc.), not generic "modern" adjectives. - - For product, push for category best-tool references (Linear, Figma, Notion, Raycast, Stripe). + - Push for specific named references with the *specific* thing about them that fits this brand, not generic "modern" adjectives or category-bucket lanes. - What should this explicitly NOT look like? Any anti-references? ### Accessibility & Inclusion @@ -141,16 +135,38 @@ If the user agrees, delegate to `/impeccable document` (it auto-detects scan vs If the user prefers to skip, mention they can run `/impeccable document` any time later. -## Step 6: Confirm and wrap up +## Step 6: Configure live mode (when code exists) -Summarize: +If the project has code with HTML entries and a dev server (the same "code exists" condition that puts `/impeccable document` in scan mode), pre-configure live mode now. You already identified the framework and the served HTML entry in Step 2, so this is nearly free, and it spares the user the first-time setup detour when they later run `/impeccable live`. + +**Skip this step for empty / pre-implementation projects** (nothing to inject into yet). Tell the user live mode will configure itself the first time they run it once there's code. + +**If `.impeccable/live/config.json` already exists, leave it untouched** and note that live mode is already configured. + +Otherwise: + +1. Write `.impeccable/live/config.json`. Choose `files` (the HTML entries the browser actually loads), `insertBefore`, and `commentSyntax` from the framework table in [live.md](live.md)'s **First-time setup** section, using the framework you found in Step 2. That table is canonical; do not restate it here. For multi-page static sites, prefer a glob (`["public/**/*.html"]`) over a literal list. +2. Run `node .qoder/skills/impeccable/scripts/detect-csp.mjs`. If it reports a patchable shape (`append-arrays` / `append-string`), use the **consent prompt template** from live.md before editing any source file. On decline, skip the patch. For `middleware` / `meta-tag` shapes, surface the detected files and ask the user to add `http://localhost:8400` to `script-src` and `connect-src` manually. For `null`, there's nothing to do. +3. Set `cspChecked: true` in the config once CSP is handled (patched, declined, manual, or not needed). The schema and per-shape patch details live in live.md's First-time setup; follow it rather than duplicating. + +Writing the config file is harmless and needs no consent; only the CSP **source-file patch** requires a yes. + +## Step 7: Recommend starting points, then wrap up + +Summarize tersely: - Register captured (brand / product) -- What was written (PRODUCT.md, DESIGN.md, or both) +- What was written (PRODUCT.md, DESIGN.md, live config, or a subset) - The 3-5 strategic principles from PRODUCT.md that will guide future work -- If DESIGN.md is pending, remind the user how to generate it later +- If DESIGN.md or live config is pending, one line on how to set it up later -**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node .rovodev/skills/impeccable/scripts/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. +Then recommend the **best commands to run next**, drawn from what your Step 2 crawl already surfaced. Do not run a fresh analysis here; surface observations you already have. Tailor to register and to what you saw, offer the 2-4 most relevant (not a menu dump), and give the exact command to type. Group by intent: -If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context. +- **Build something new**: `/impeccable craft ` (shape, then build end-to-end) or `/impeccable shape ` (plan first). Lead with this for empty or early-stage projects. +- **Improve what's there**: name the specific surface. `/impeccable critique ` for a scored UX review; `/impeccable audit ` for a11y / perf / responsive checks; `/impeccable polish ` for a pre-ship pass. When the crawl flagged a specific weakness, point the matching command at it: thin hierarchy or spacing → `layout`, flat or gray palette → `colorize`, missing error / empty states → `harden` or `onboard`, dull or unclear copy → `clarify`. +- **Iterate visually**: `/impeccable live` (configured in Step 6) to pick elements in the browser and generate variants in place. + +The full command menu is one bare `/impeccable` away; keep this list short and pointed. + +If init was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now. Your own writes are the freshest source; no reload needed. Optionally ask the user directly to clarify what you cannot infer. Ask whether they'd like a brief summary of PRODUCT.md appended to AGENTS.md for easier agent reference. If yes, append a short **Design Context** pointer section there. diff --git a/.qoder/skills/impeccable/reference/interaction-design.md b/.qoder/skills/impeccable/reference/interaction-design.md index 15aed5b29..c42c9d810 100644 --- a/.qoder/skills/impeccable/reference/interaction-design.md +++ b/.qoder/skills/impeccable/reference/interaction-design.md @@ -150,12 +150,6 @@ For browsers without anchor positioning support, `position: fixed` with manual c Check viewport boundaries before rendering. If the dropdown would overflow the bottom edge, flip it above the trigger. If it would overflow the right edge, align it to the trigger's right side instead. -### Anti-Patterns - -- **`position: absolute` inside `overflow: hidden`** - The dropdown will be clipped. Use `position: fixed` or the top layer instead. -- **Arbitrary z-index values** like `z-index: 9999` - Use a semantic z-index scale: `dropdown (100) -> sticky (200) -> modal-backdrop (300) -> modal (400) -> toast (500) -> tooltip (600)`. -- **Rendering dropdown markup inline** without an escape hatch from the parent's stacking context. Either use `popover` (top layer), a portal, or `position: fixed`. - ## Destructive Actions: Undo > Confirm **Undo is better than confirmation dialogs.** Users click through confirmations mindlessly. Remove from UI immediately, show undo toast, actually delete after toast expires. Use confirmation only for truly irreversible actions (account deletion), high-cost actions, or batch operations. diff --git a/.qoder/skills/impeccable/reference/layout.md b/.qoder/skills/impeccable/reference/layout.md index cad4c62a1..8765978b0 100644 --- a/.qoder/skills/impeccable/reference/layout.md +++ b/.qoder/skills/impeccable/reference/layout.md @@ -1,4 +1,4 @@ -Space is the most underused design tool. Find the layout's actual problem (monotone spacing, weak hierarchy, identical card grids, the centered-stack default) and fix the structure, not the surface. +Space is the most underused design tool. Find the layout's actual problem (monotone spacing, weak hierarchy, identical card grids) and fix the structure, not the surface. --- @@ -27,7 +27,6 @@ Analyze what's weak about the current spatial design: 3. **Grid & structure**: - Is there a clear underlying structure, or does the layout feel random? - Are identical card grids used everywhere? (Icon + heading + text, repeated endlessly) - - Is everything centered? (Left-aligned with asymmetric layouts feels more designed, but not a hard and fast rule) 4. **Rhythm & variety**: - Does the layout have visual rhythm? (Alternating tight/generous spacing) @@ -43,8 +42,6 @@ Analyze what's weak about the current spatial design: ## Plan Layout Improvements -Consult the [spatial design reference](spatial-design.md) for detailed guidance on grids, rhythm, and container queries. - Create a systematic plan: - **Spacing system**: Use a consistent scale (a framework's built-in scale like Tailwind's, rem-based tokens, or a custom system). The specific values matter less than consistency. @@ -57,6 +54,7 @@ Create a systematic plan: ### Establish a Spacing System - Use a consistent spacing scale (framework scales like Tailwind, rem-based tokens, or a custom scale all work). What matters is that values come from a defined set, not arbitrary numbers. +- Prefer a 4pt base scale (4, 8, 12, 16, 24, 32, 48, 64, 96px) over 8pt; 8pt is too coarse and you'll frequently need 12px between 8 and 16. - Name tokens semantically if using custom properties: `--space-xs` through `--space-xl`, not `--spacing-8` - Use `gap` for sibling spacing instead of margins; eliminates margin collapse hacks - Apply `clamp()` for fluid spacing that breathes on larger screens @@ -66,15 +64,22 @@ Create a systematic plan: - **Tight grouping** for related elements (8-12px between siblings) - **Generous separation** between distinct sections (48-96px) - **Varied spacing** within sections (not every row needs the same gap) -- **Asymmetric compositions**: break the predictable centered-content pattern when it makes sense +- **Asymmetric compositions**: a deliberate choice when the content invites it (not a default to chase). ### Choose the Right Layout Tool -- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. Flex is simpler and more appropriate for the majority of layout tasks. +- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. - **Use Grid for 2D layouts**: Page-level structure, dashboards, data-dense interfaces, anything where rows AND columns need coordinated control. -- **Don't default to Grid** when Flexbox with `flex-wrap` would be simpler and more flexible. -- Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. - Use named grid areas (`grid-template-areas`) for complex page layouts; redefine at breakpoints. +- Use **container queries** for components, viewport queries for page layouts. A card in a narrow sidebar can stay compact while the same card in a main content area expands automatically: + +```css +.card-container { container-type: inline-size; } +.card { display: grid; gap: var(--space-md); } +@container (min-width: 400px) { + .card { grid-template-columns: 120px 1fr; } +} +``` ### Break Card Grid Monotony @@ -85,18 +90,36 @@ Create a systematic plan: ### Strengthen Visual Hierarchy - Use the fewest dimensions needed for clear hierarchy. Space alone can be enough; generous whitespace around an element draws the eye. Some of the most polished designs achieve rhythm with just space and weight. Add color or size contrast only when simpler means aren't sufficient. +- The best hierarchy combines 2–3 dimensions at once. A heading that's larger, bolder, AND has more space above it reads as primary without trying: + +| Tool | Strong Hierarchy | Weak Hierarchy | +|------|------------------|----------------| +| **Size** | 3:1 ratio or more | <2:1 ratio | +| **Weight** | Bold vs Regular | Medium vs Regular | +| **Color** | High contrast | Similar tones | +| **Position** | Top/left (primary) | Bottom/right | +| **Space** | Surrounded by white space | Crowded | + - Be aware of reading flow: in LTR languages, the eye naturally scans top-left to bottom-right, but primary action placement depends on context (e.g., bottom-right in dialogs, top in navigation). - Create clear content groupings through proximity and separation. ### Manage Depth & Elevation -- Create a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip) - Build a consistent shadow scale (sm → md → lg → xl); shadows should be subtle - Use elevation to reinforce hierarchy, not as decoration ### Optical Adjustments - If an icon looks visually off-center despite being geometrically centered, nudge it. But only if you're confident it actually looks wrong. Don't adjust speculatively. +- Text at `margin-left: 0` looks slightly indented because of letterform whitespace; a negative margin (`-0.05em`) optically aligns it. Geometrically centered glyphs often look off-center (play icons need to shift right, arrows shift toward their direction). +- Touch targets must be 44×44px minimum even when the visual element is smaller. Expand the hit area with padding or a pseudo-element: + +```css +.icon-button { width: 24px; height: 24px; position: relative; } +.icon-button::before { + content: ''; position: absolute; inset: -10px; +} +``` **NEVER**: - Use arbitrary spacing values outside your scale @@ -104,10 +127,7 @@ Create a systematic plan: - Wrap everything in cards (not everything needs a container) - Nest cards inside cards (use spacing and dividers for hierarchy within) - Use identical card grids everywhere (icon + heading + text, repeated) -- Center everything (left-aligned with asymmetry feels more designed) - Default to the hero metric layout (big number, small label, stats, gradient) as a template. If showing real user data, a prominent metric can work, but it should display actual data, not decorative numbers. -- Default to CSS Grid when Flexbox would be simpler; use the simplest tool for the job -- Use arbitrary z-index values (999, 9999); build a semantic scale ## Verify Layout Improvements diff --git a/.qoder/skills/impeccable/reference/live.md b/.qoder/skills/impeccable/reference/live.md index bad4583f4..d6898a627 100644 --- a/.qoder/skills/impeccable/reference/live.md +++ b/.qoder/skills/impeccable/reference/live.md @@ -9,18 +9,21 @@ A running dev server with hot module replacement (Vite, Next.js, Bun, etc.), OR Execute in order. No step skipped, no step reordered. 1. `live.mjs`: boot. -2. Navigate to the URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). If you can't infer it confidently, tell the user once to open their dev/preview URL. Never use `serverPort` as that URL; it's the helper, not the app. +2. Open the app URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). Never use `serverPort`; it's the helper, not the app. **Cursor:** `browser_navigate` to that URL before polling; do not skip. **Other harnesses:** use the available browser tool; if the URL is uncertain, ask the user once. 3. Poll loop with the default long timeout (600000 ms). After every event or `--reply`, run `live-poll.mjs` again immediately. Never pass a short `--timeout=`. + +The global bar **Impeccable mark** dims and shows a pulsing amber dot when no agent is long-polling `/poll`. Hover the mark for the hint; restart `live-poll.mjs` to reconnect. 4. On `generate`: read screenshot if present; load the action's reference; plan three distinct directions; write all variants in one edit; `--reply done`; poll again. -5. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts remain recoverable until you finish cleanup, run `live-complete.mjs --id EVENT_ID`, and only then poll again. -6. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart. -7. On `exit`: run the cleanup at the bottom. +5. On `steer`: read the message and `pageUrl`; do the work (page edits, navigation help, or a short reply in the `--reply` message); `--reply steer_done`; poll again. No pickup ack. The Steer bar unlocks when `steer_done` arrives over SSE. +6. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts remain recoverable until you finish cleanup, run `live-complete.mjs --id EVENT_ID`, and only then poll again. +7. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart. +8. On `exit`: run the cleanup at the bottom. Harness policy: - **Claude Code**: run the poll as a **background task** (no short timeout). The harness notifies you when it completes, so the main conversation stays free. Do not block the shell. -- **Cursor**: run the poll in the **foreground** (blocking shell; not a background terminal, not a subagent). Cursor background terminals and subagents do not reliably resume the chat with poll stdout. +- **Cursor**: run **one-shot** poll in a **background terminal** with notify on `"type":"(steer|generate|accept|discard|exit)"`. After each event the poll exits; handle it, `--reply`, then start `live-poll.mjs` again. Do **not** use `--stream` on Cursor: incremental stdout notify is slower in practice than exit-based notify (~5s vs sub-second in testing). - **Codex**: run the poll in the **foreground** (blocking shell; not a background task, not a subagent). Codex background exec sessions do not reliably surface poll stdout back into the conversation at the moment events arrive, so a "fire-and-forget" background poll will stall live mode. -- **Other harnesses**: foreground unless you know stdout reliably returns to this session. +- **Other harnesses**: one-shot foreground unless you know stdout reliably returns to this session when a shell exits. Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodies. Spend tokens on tools and edits; on failure, one or two short sentences. @@ -30,7 +33,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi node .qoder/skills/impeccable/scripts/live.mjs ``` -Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation; **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md. +Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation; **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. `serverPort` and `serverToken` belong to the small **Impeccable live helper** HTTP server (serves `/live.js`, SSE, and `/poll`). That port is **not** your dev server and is usually not the URL you open to view the app. The browser page is whatever origin serves one of the `pageFiles` entries (Vite / Next / Bun / tunnel / LAN hostname). @@ -38,12 +41,15 @@ If output is `{ ok: false, error: "config_missing" | "config_invalid", path }`, ## Poll loop +**Default (portable, all harnesses):** + ``` LOOP: node .qoder/skills/impeccable/scripts/live-poll.mjs # default long timeout; no --timeout= Read JSON; dispatch on "type" "generate" → Handle Generate; reply done; LOOP + "steer" → Handle Steer; reply steer_done; LOOP "accept" → Handle Accept; complete carbonize cleanup if required; LOOP "discard" → Handle Discard; LOOP "prefetch" → Handle Prefetch; LOOP @@ -51,6 +57,16 @@ LOOP: "exit" → break → Cleanup ``` +**Stream mode (experimental, not for Cursor):** + +``` +node .qoder/skills/impeccable/scripts/live-poll.mjs --stream # stays running; one JSON line per event + Handle event; run --reply in a separate command + Repeat until "exit" line → Cleanup +``` + +Stream keeps one process alive and waits for `--reply` ack before polling again. Useful only when the harness reads incremental stdout reliably and quickly. **Cursor is not one of those:** background pattern notify on a long-running shell was ~5s to pick up events vs sub-second for one-shot exit notify. Default to one-shot everywhere unless you have measured otherwise. + ## Recovery commands The live helper persists an append-only journal under `.impeccable/live/sessions/`. Browser checkpoints are advisory but durable; the journal is canonical. This is local durable recovery state, not project source. @@ -71,9 +87,32 @@ Server restart rule: start `live-server.mjs` again, then poll. Startup requeues ## Handle `generate` -Event: `{id, action, freeformPrompt?, count, pageUrl, element, screenshotPath?, comments?, strokes?}`. +**Replace mode** (default): `{id, action, freeformPrompt?, count, pageUrl, element, screenshotPath?, comments?, strokes?}`. -Speed matters; the user is watching a spinner. Minimize tool calls by using the `wrap` helper and writing all variants in a single edit. +**Insert mode** (`event.mode === "insert"`): `{id, mode: "insert", count, pageUrl, insert: { position, anchor }, placeholder: { width, height }, freeformPrompt?, screenshotPath?, comments?, strokes?}`. No `action`. Requires a non-empty `freeformPrompt` **or** annotations. Screenshot is sent only when annotations exist (same rule as replace). Use `placeholder` dimensions as a soft size hint for net-new content. + +Speed matters; the user is watching a spinner. Minimize tool calls by using the wrap/insert helper and writing all variants in a single edit. + +### Insert mode branch + +When `event.mode === "insert"`: + +1. Read the screenshot if `event.screenshotPath` is present (annotations only). +2. Run the insert helper instead of wrap: + +```bash +node .qoder/skills/impeccable/scripts/live-insert.mjs --id EVENT_ID --count EVENT_COUNT --position after \ + --element-id "ANCHOR_ID" --classes "class1,class2" --tag "section" --text "ANCHOR_TEXT" +``` + +- `--position` ← `event.insert.position` (`before` | `after`) +- Anchor flags ← `event.insert.anchor` (same mapping as wrap: id, classes, tag, text) + +The scaffold has **no** `data-impeccable-variant="original"`. Variants are net-new HTML+CSS inserted at `insertLine`. Load `brand.md` or `product.md` (freeform only, no action sub-command). Write all variants in one edit, then `--reply done`. + +On accept/discard, `live-accept.mjs` removes the wrapper block; the anchor element is untouched. + +### Replace mode (default) ### 1. Read the screenshot (if present) @@ -424,6 +463,28 @@ A background agent may be used for the rewrite, but the current thread is respon Event: `{id, _acceptResult, _completionAck}`. The poll script already restored the original, removed all variant markers, and acknowledged `discarded` durable completion. Nothing to do unless `_completionAck.ok !== true`; in that case run `live-complete.mjs --id EVENT_ID --discarded`, then poll again. +## Handle `steer` + +Event: `{id, message, pageUrl}`. The user typed or spoke into the global bar **Steer** control: page-level direction without picking an element or launching variant generation. + +The mic button uses the browser **Web Speech API** (MVP): click to start, speak, stop automatically when the utterance ends, then the transcript submits as a steer event. Click again while listening to cancel without submitting. + +This is lighter than `generate`: no screenshot, no element context, no variant cycling. Read `message` and inspect the live page or project files as needed, then either make edits or answer in prose. + +When finished: + +```bash +node .qoder/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID steer_done ["Optional short note for a browser toast"] +``` + +On failure: + +```bash +node .qoder/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID error "Short reason" +``` + +Then poll again immediately. Do not send a separate "picked up" reply. The Steer bar stays locked until `steer_done` or `error` arrives over SSE. + ## Handle `prefetch` Event: `{pageUrl}`. The browser fires this the first time the user selects an element on a given route, as a latency shortcut; it signals the user is likely about to Go on a page you haven't read yet. diff --git a/.qoder/skills/impeccable/reference/motion-design.md b/.qoder/skills/impeccable/reference/motion-design.md deleted file mode 100644 index 78b6fd260..000000000 --- a/.qoder/skills/impeccable/reference/motion-design.md +++ /dev/null @@ -1,109 +0,0 @@ -# Motion Design - -## Duration: The 100/300/500 Rule - -Timing matters more than easing. These durations feel right for most UI: - -| Duration | Use Case | Examples | -|----------|----------|----------| -| **100-150ms** | Instant feedback | Button press, toggle, color change | -| **200-300ms** | State changes | Menu open, tooltip, hover states | -| **300-500ms** | Layout changes | Accordion, modal, drawer | -| **500-800ms** | Entrance animations | Page load, hero reveals | - -**Exit animations are faster than entrances.** Use ~75% of enter duration. - -## Easing: Pick the Right Curve - -**Don't use `ease`.** It's a compromise that's rarely optimal. Instead: - -| Curve | Use For | CSS | -|-------|---------|-----| -| **ease-out** | Elements entering | `cubic-bezier(0.16, 1, 0.3, 1)` | -| **ease-in** | Elements leaving | `cubic-bezier(0.7, 0, 0.84, 0)` | -| **ease-in-out** | State toggles (there → back) | `cubic-bezier(0.65, 0, 0.35, 1)` | - -**For micro-interactions, use exponential curves.** They feel natural because they mimic real physics (friction, deceleration): - -```css -/* Quart out - smooth, refined (recommended default) */ ---ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1); - -/* Quint out - slightly more dramatic */ ---ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1); - -/* Expo out - snappy, confident */ ---ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); -``` - -**Avoid bounce and elastic curves.** They were trendy in 2015 but now feel tacky and amateurish. Real objects don't bounce when they stop; they decelerate smoothly. Overshoot effects draw attention to the animation itself rather than the content. - -## Premium Motion Materials - -Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties: blur reveals, backdrop-filter panels, saturation or brightness shifts, shadow bloom, SVG filters, masks, clip paths, gradient-position movement, and variable font or shader-driven effects. - -Use the right material for the effect: - -- **Transform / opacity**: movement, press feedback, simple reveals, list choreography. -- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances, atmospheric transitions. -- **Clip path / masks**: wipes, reveals, editorial cropping, product-like transitions. -- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state. -- **Grid-template rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly. - -The hard rule is not "transform and opacity only." The hard rule is: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify in-browser that the result is smooth on the target viewports. If blur/filter makes the interaction feel significantly more premium and remains smooth, use it. - -## Staggered Animations - -Use CSS custom properties for cleaner stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"` on each item. **Cap total stagger time**: 10 items at 50ms = 500ms total. For many items, reduce per-item delay or cap staggered count. - -## Reduced Motion - -This is not optional. Vestibular disorders affect ~35% of adults over 40. - -```css -/* Define animations normally */ -.card { - animation: slide-up 500ms ease-out; -} - -/* Provide alternative for reduced motion */ -@media (prefers-reduced-motion: reduce) { - .card { - animation: fade-in 200ms ease-out; /* Crossfade instead of motion */ - } -} - -/* Or disable entirely */ -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - transition-duration: 0.01ms !important; - } -} -``` - -**What to preserve**: Functional animations like progress bars, loading spinners (slowed down), and focus indicators should still work, just without spatial movement. - -## Perceived Performance - -**Nobody cares how fast your site is, just how fast it feels.** Perception can be as effective as actual performance. - -**The 80ms threshold**: Our brains buffer sensory input for ~80ms to synchronize perception. Anything under 80ms feels instant and simultaneous. This is your target for micro-interactions. - -**Active vs passive time**: Passive waiting (staring at a spinner) feels longer than active engagement. Strategies to shift the balance: - -- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening. -- **Early completion**: Show content progressively, don't wait for everything. Video buffering, progressive images, streaming HTML. -- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Instagram likes work offline; the UI updates instantly, syncs later. Use for low-stakes actions; avoid for payments or destructive operations. - -**Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances, but ease-in toward a task's end compresses perceived time. - -**Caution**: Too-fast responses can decrease perceived value. Users may distrust instant results for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening. - -## Performance - -Don't use `will-change` preemptively, only when animation is imminent (`:hover`, `.animating`). For scroll-triggered animations, use Intersection Observer instead of scroll events; unobserve after animating once. Create motion tokens for consistency (durations, easings, common transitions). - ---- - -**Avoid**: Animating everything (animation fatigue is real). Using >500ms for UI feedback. Ignoring `prefers-reduced-motion`. Using animation to hide slow loading. diff --git a/.qoder/skills/impeccable/reference/personas.md b/.qoder/skills/impeccable/reference/personas.md deleted file mode 100644 index 43ae8eb2f..000000000 --- a/.qoder/skills/impeccable/reference/personas.md +++ /dev/null @@ -1,179 +0,0 @@ -# Persona-Based Design Testing - -Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss. - -**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns. - ---- - -## 1. Impatient Power User: "Alex" - - -**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave. - -**Behaviors**: -- Skips all onboarding and instructions -- Looks for keyboard shortcuts immediately -- Tries to bulk-select, batch-edit, and automate -- Gets frustrated by required steps that feel unnecessary -- Abandons if anything feels slow or patronizing - -**Test Questions**: -- Can Alex complete the core task in under 60 seconds? -- Are there keyboard shortcuts for common actions? -- Can onboarding be skipped entirely? -- Do modals have keyboard dismiss (Esc)? -- Is there a "power user" path (shortcuts, bulk actions)? - -**Red Flags** (report these specifically): -- Forced tutorials or unskippable onboarding -- No keyboard navigation for primary actions -- Slow animations that can't be skipped -- One-item-at-a-time workflows where batch would be natural -- Redundant confirmation steps for low-risk actions - ---- - -## 2. Confused First-Timer: "Jordan" - -**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out. - -**Behaviors**: -- Reads all instructions carefully -- Hesitates before clicking anything unfamiliar -- Looks for help or support constantly -- Misunderstands jargon and abbreviations -- Takes the most literal interpretation of any label - -**Test Questions**: -- Is the first action obviously clear within 5 seconds? -- Are all icons labeled with text? -- Is there contextual help at decision points? -- Does terminology assume prior knowledge? -- Is there a clear "back" or "undo" at every step? - -**Red Flags** (report these specifically): -- Icon-only navigation with no labels -- Technical jargon without explanation -- No visible help option or guidance -- Ambiguous next steps after completing an action -- No confirmation that an action succeeded - ---- - -## 3. Accessibility-Dependent User: "Sam" - -**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences. - -**Behaviors**: -- Tabs through the interface linearly -- Relies on ARIA labels and heading structure -- Cannot see hover states or visual-only indicators -- Needs adequate color contrast (4.5:1 minimum) -- May use browser zoom up to 200% - -**Test Questions**: -- Can the entire primary flow be completed keyboard-only? -- Are all interactive elements focusable with visible focus indicators? -- Do images have meaningful alt text? -- Is color contrast WCAG AA compliant (4.5:1 for text)? -- Does the screen reader announce state changes (loading, success, errors)? - -**Red Flags** (report these specifically): -- Click-only interactions with no keyboard alternative -- Missing or invisible focus indicators -- Meaning conveyed by color alone (red = error, green = success) -- Unlabeled form fields or buttons -- Time-limited actions without extension option -- Custom components that break screen reader flow - ---- - -## 4. Deliberate Stress Tester: "Riley" - -**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience. - -**Behaviors**: -- Tests edge cases intentionally (empty states, long strings, special characters) -- Submits forms with unexpected data (emoji, RTL text, very long values) -- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs -- Looks for inconsistencies between what the UI promises and what actually happens -- Documents problems methodically - -**Test Questions**: -- What happens at the edges (0 items, 1000 items, very long text)? -- Do error states recover gracefully or leave the UI in a broken state? -- What happens on refresh mid-workflow? Is state preserved? -- Are there features that appear to work but produce broken results? -- How does the UI handle unexpected input (emoji, special chars, paste from Excel)? - -**Red Flags** (report these specifically): -- Features that appear to work but silently fail or produce wrong results -- Error handling that exposes technical details or leaves UI in a broken state -- Empty states that show nothing useful ("No results" with no guidance) -- Workflows that lose user data on refresh or navigation -- Inconsistent behavior between similar interactions in different parts of the UI - ---- - -## 5. Distracted Mobile User: "Casey" - -**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection. - -**Behaviors**: -- Uses thumb only; prefers bottom-of-screen actions -- Gets interrupted mid-flow and returns later -- Switches between apps frequently -- Has limited attention span and low patience -- Types as little as possible, prefers taps and selections - -**Test Questions**: -- Are primary actions in the thumb zone (bottom half of screen)? -- Is state preserved if the user leaves and returns? -- Does it work on slow connections (3G)? -- Can forms use autocomplete and smart defaults? -- Are touch targets at least 44×44pt? - -**Red Flags** (report these specifically): -- Important actions positioned at the top of the screen (unreachable by thumb) -- No state persistence; progress lost on tab switch or interruption -- Large text inputs required where selection would work -- Heavy assets loading on every page (no lazy loading) -- Tiny tap targets or targets too close together - ---- - -## Selecting Personas - -Choose personas based on the interface type: - -| Interface Type | Primary Personas | Why | -|---------------|-----------------|-----| -| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile | -| Dashboard / admin | Alex, Sam | Power users, accessibility | -| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity | -| Onboarding flow | Jordan, Casey | Confusion, interruption | -| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav | -| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile | - ---- - -## Project-Specific Personas - -If `AGENTS.md` contains a `## Design Context` section (generated by `impeccable teach`), derive 1–2 additional personas from the audience and brand information: - -1. Read the target audience description -2. Identify the primary user archetype not covered by the 5 predefined personas -3. Create a persona following this template: - -``` -### [Role]: "[Name]" - -**Profile**: [2-3 key characteristics derived from Design Context] - -**Behaviors**: [3-4 specific behaviors based on the described audience] - -**Red Flags**: [3-4 things that would alienate this specific user type] -``` - -Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists. diff --git a/.qoder/skills/impeccable/reference/polish.md b/.qoder/skills/impeccable/reference/polish.md index f1ea94747..08ad35e81 100644 --- a/.qoder/skills/impeccable/reference/polish.md +++ b/.qoder/skills/impeccable/reference/polish.md @@ -91,7 +91,6 @@ Visual polish on a misshapen flow is wasted work. Match the *shape* of the exper - **Theme consistency**: Works in all theme variants - **Color meaning**: Same colors mean same things throughout - **Accessible focus**: Focus indicators visible with sufficient contrast -- **Tinted neutrals**: No pure gray or pure black; add subtle color tint (0.01 chroma) - **Gray on color**: Never put gray text on colored backgrounds; use a shade of that color or transparency ### Interaction States diff --git a/.qoder/skills/impeccable/reference/product.md b/.qoder/skills/impeccable/reference/product.md index 64b3b0169..2ca462350 100644 --- a/.qoder/skills/impeccable/reference/product.md +++ b/.qoder/skills/impeccable/reference/product.md @@ -10,7 +10,6 @@ Product UI's failure mode isn't flatness, it's strangeness without purpose: over ## Typography -- **System fonts are legitimate.** `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif` gives you native feel on every platform. Inter is the common cross-platform default for a reason. - **One family is often right.** Product UIs don't need display/body pairing. A well-tuned sans carries headings, buttons, labels, body, data. - **Fixed rem scale, not fluid.** Clamp-sized headings don't serve product UI. Users view at consistent DPI, and a fluid h1 that shrinks in a sidebar looks worse, not better. - **Tighter scale ratio.** 1.125–1.2 between steps is typical. More type elements here than on brand surfaces; exaggerated contrast creates noise. @@ -26,8 +25,6 @@ Product defaults to Restrained. A single surface can earn Committed (a dashboard ## Layout -- Predictable grids. Consistency IS an affordance; users navigate faster when the structure is expected. -- Familiar patterns are features. Standard navigation (top bar, side nav), breadcrumbs, tabs, and form layouts have established user expectations. Don't reinvent for flavor. - Responsive behavior is structural (collapse sidebar, responsive table, breakpoint-driven columns), not fluid typography. ## Components @@ -51,6 +48,7 @@ Every interactive component has: default, hover, focus, active, disabled, loadin - Display fonts in UI labels, buttons, data. - Reinventing standard affordances for flavor (custom scrollbars, weird form controls, non-standard modals). - Heavy color or full-saturation accents on inactive states. +- Modal as first thought. Modals are usually laziness. Exhaust inline / progressive alternatives first. ## Product permissions diff --git a/.qoder/skills/impeccable/reference/responsive-design.md b/.qoder/skills/impeccable/reference/responsive-design.md deleted file mode 100644 index f079e5e5d..000000000 --- a/.qoder/skills/impeccable/reference/responsive-design.md +++ /dev/null @@ -1,114 +0,0 @@ -# Responsive Design - -## Mobile-First: Write It Right - -Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first. - -## Breakpoints: Content-Driven - -Don't chase device sizes; let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints. - -## Detect Input Method, Not Just Screen Size - -**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard. Use pointer and hover queries: - -```css -/* Fine pointer (mouse, trackpad) */ -@media (pointer: fine) { - .button { padding: 8px 16px; } -} - -/* Coarse pointer (touch, stylus) */ -@media (pointer: coarse) { - .button { padding: 12px 20px; } /* Larger touch target */ -} - -/* Device supports hover */ -@media (hover: hover) { - .card:hover { transform: translateY(-2px); } -} - -/* Device doesn't support hover (touch) */ -@media (hover: none) { - .card { /* No hover state - use active instead */ } -} -``` - -**Critical**: Don't rely on hover for functionality. Touch users can't hover. - -## Safe Areas: Handle the Notch - -Modern phones have notches, rounded corners, and home indicators. Use `env()`: - -```css -body { - padding-top: env(safe-area-inset-top); - padding-bottom: env(safe-area-inset-bottom); - padding-left: env(safe-area-inset-left); - padding-right: env(safe-area-inset-right); -} - -/* With fallback */ -.footer { - padding-bottom: max(1rem, env(safe-area-inset-bottom)); -} -``` - -**Enable viewport-fit** in your meta tag: -```html - -``` - -## Responsive Images: Get It Right - -### srcset with Width Descriptors - -```html -Hero image -``` - -**How it works**: -- `srcset` lists available images with their actual widths (`w` descriptors) -- `sizes` tells the browser how wide the image will display -- Browser picks the best file based on viewport width AND device pixel ratio - -### Picture Element for Art Direction - -When you need different crops/compositions (not just resolutions): - -```html - - - - ... - -``` - -## Layout Adaptation Patterns - -**Navigation**: Three stages: hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `
/` for content that can collapse on mobile. - -## Testing: Don't Trust DevTools Alone - -DevTools device emulation is useful for layout but misses: - -- Actual touch interactions -- Real CPU/memory constraints -- Network latency patterns -- Font rendering differences -- Browser chrome/keyboard appearances - -**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators. - ---- - -**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful. diff --git a/.qoder/skills/impeccable/reference/shape.md b/.qoder/skills/impeccable/reference/shape.md index f7c4cd2f1..592def8a7 100644 --- a/.qoder/skills/impeccable/reference/shape.md +++ b/.qoder/skills/impeccable/reference/shape.md @@ -151,7 +151,7 @@ How users interact with this feature. What happens on click, hover, scroll? What 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., spatial-design.md for complex layouts, motion-design.md for animated features, interaction-design.md for form-heavy features). +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. diff --git a/.qoder/skills/impeccable/reference/spatial-design.md b/.qoder/skills/impeccable/reference/spatial-design.md deleted file mode 100644 index 4d4b83afc..000000000 --- a/.qoder/skills/impeccable/reference/spatial-design.md +++ /dev/null @@ -1,100 +0,0 @@ -# Spatial Design - -## Spacing Systems - -### Use 4pt Base, Not 8pt - -8pt systems are too coarse; you'll frequently need 12px (between 8 and 16). Use 4pt for granularity: 4, 8, 12, 16, 24, 32, 48, 64, 96px. - -### Name Tokens Semantically - -Name by relationship (`--space-sm`, `--space-lg`), not value (`--spacing-8`). Use `gap` instead of margins for sibling spacing; it eliminates margin collapse and cleanup hacks. - -## Grid Systems - -### The Self-Adjusting Grid - -Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. Columns are at least 280px, as many as fit per row, leftovers stretch. For complex layouts, use named grid areas (`grid-template-areas`) and redefine them at breakpoints. - -## Visual Hierarchy - -### The Squint Test - -Blur your eyes (or screenshot and blur). Can you still identify: -- The most important element? -- The second most important? -- Clear groupings? - -If everything looks the same weight blurred, you have a hierarchy problem. - -### Hierarchy Through Multiple Dimensions - -Don't rely on size alone. Combine: - -| Tool | Strong Hierarchy | Weak Hierarchy | -|------|------------------|----------------| -| **Size** | 3:1 ratio or more | <2:1 ratio | -| **Weight** | Bold vs Regular | Medium vs Regular | -| **Color** | High contrast | Similar tones | -| **Position** | Top/left (primary) | Bottom/right | -| **Space** | Surrounded by white space | Crowded | - -**The best hierarchy uses 2-3 dimensions at once**: A heading that's larger, bolder, AND has more space above it. - -### Cards Are Not Required - -Cards are overused. Spacing and alignment create visual grouping naturally. Use cards only when content is truly distinct and actionable, items need visual comparison in a grid, or content needs clear interaction boundaries. **Never nest cards inside cards.** Use spacing, typography, and subtle dividers for hierarchy within a card. - -## Container Queries - -Viewport queries are for page layouts. **Container queries are for components**: - -```css -.card-container { - container-type: inline-size; -} - -.card { - display: grid; - gap: var(--space-md); -} - -/* Card layout changes based on its container, not viewport */ -@container (min-width: 400px) { - .card { - grid-template-columns: 120px 1fr; - } -} -``` - -**Why this matters**: A card in a narrow sidebar stays compact, while the same card in a main content area expands automatically, without viewport hacks. - -## Optical Adjustments - -Text at `margin-left: 0` looks indented due to letterform whitespace; use negative margin (`-0.05em`) to optically align. Geometrically centered icons often look off-center; play icons need to shift right, arrows shift toward their direction. - -### Touch Targets vs Visual Size - -Buttons can look small but need large touch targets (44px minimum). Use padding or pseudo-elements: - -```css -.icon-button { - width: 24px; /* Visual size */ - height: 24px; - position: relative; -} - -.icon-button::before { - content: ''; - position: absolute; - inset: -10px; /* Expand tap target to 44px */ -} -``` - -## Depth & Elevation - -Create semantic z-index scales (dropdown → sticky → modal-backdrop → modal → toast → tooltip) instead of arbitrary numbers. For shadows, create a consistent elevation scale (sm → md → lg → xl). **Key insight**: Shadows should be subtle. If you can clearly see it, it's probably too strong. - ---- - -**Avoid**: Arbitrary spacing values outside your scale. Making all spacing equal (variety creates hierarchy). Creating hierarchy through size alone - combine size, weight, color, and space. diff --git a/.qoder/skills/impeccable/reference/typeset.md b/.qoder/skills/impeccable/reference/typeset.md index dd3e67ea3..5f5e0c2c0 100644 --- a/.qoder/skills/impeccable/reference/typeset.md +++ b/.qoder/skills/impeccable/reference/typeset.md @@ -4,7 +4,7 @@ Typography carries most of the information on the page. Replace generic defaults ## Register -Brand: run the font selection procedure in [brand.md](brand.md). Pairing follows the brand's lane (display serif + sans body for editorial/luxury, one committed sans for tech, etc.). Fluid `clamp()` scale, ≥1.25 ratio between steps. +Brand: run the font selection procedure in [brand.md](brand.md). Fluid `clamp()` scale, ≥1.25 ratio between steps. Product: system fonts and familiar sans stacks are legitimate here. One well-tuned family typically carries the whole UI. Fixed `rem` scale, 1.125–1.2 ratio between more closely-spaced steps. @@ -43,7 +43,7 @@ Analyze what's weak or generic about the current type: ## Plan Typography Improvements -Consult the [typography reference](typography.md) for detailed guidance on scales, pairing, and loading strategies. +Consult the [Reference Material](#reference-material) section below for detailed guidance on scales, pairing, and loading strategies. Create a systematic plan: @@ -122,3 +122,158 @@ Each variant MUST declare a `scale` param controlling the hierarchy ratio. Expre Where the variant riffs on a specific pairing, expose the pairing choice as a `steps` param (e.g. "serif display + sans body" vs. "mono display + sans body" vs. "all-sans"). Each branch routes through `:scope[data-p-pairing="X"]` selectors in scoped CSS. See `reference/live.md` for the full params contract. + +--- + +## Reference Material + +The sections below were previously `typography.md` and live inline now so the typeset flow has its deep typography reference in one place. `bolder.md` also references this section. + +### Typography + +#### Classic Typography Principles + +##### Vertical Rhythm + +Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony; text and space share a mathematical foundation. + +##### Modular Scale & Hierarchy + +The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy. + +**Use fewer sizes with more contrast.** A 5-size system covers most needs: + +| Role | Typical Ratio | Use Case | +|------|---------------|----------| +| xs | 0.75rem | Captions, legal | +| sm | 0.875rem | Secondary UI, metadata | +| base | 1rem | Body text | +| lg | 1.25-1.5rem | Subheadings, lead text | +| xl+ | 2-4rem | Headlines, hero text | + +Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit. + +##### Readability & Measure + +Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length: narrow columns need tighter leading, wide columns need more. + +**Non-obvious**: Light text on dark backgrounds needs compensation on three axes, not just one. Bump line-height by 0.05–0.1, add a touch of letter-spacing (0.01–0.02em), and optionally step the body weight up one notch (regular → medium). The perceived weight drops across all three; fix all three. + +**Paragraph rhythm**: Pick either space between paragraphs OR first-line indentation. Never both. Digital usually wants space; editorial/long-form can justify indent-only. + +#### Font Selection & Pairing + +The tactical selection procedure and the reflex-reject list live in [reference/brand.md](brand.md) under **Font selection procedure** and **Reflex-reject list** (loaded for brand-register tasks). The rest of this section covers the adjacent knowledge: anti-reflex corrections, system font use, and pairing rules. + +##### Anti-reflexes worth defending against + +- A technical/utilitarian brief does NOT need a serif "for warmth." Most tech tools should look like tech tools. +- An editorial/premium brief does NOT need the same expressive serif everyone is using right now. Premium can be Swiss-modern, can be neo-grotesque, can be a literal monospace, can be a quiet humanist sans. +- A children's product does NOT need a rounded display font. Kids' books use real type. +- A "modern" brief does NOT need a geometric sans. The most modern thing you can do is not use the font everyone else is using. + +**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality. + +##### Pairing Principles + +**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif). + +When pairing, contrast on multiple axes: +- Serif + Sans (structure contrast) +- Geometric + Humanist (personality contrast) +- Condensed display + Wide body (proportion contrast) + +##### Web Font Loading + +The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix: + +```css +/* 1. Use font-display: swap for visibility */ +@font-face { + font-family: 'CustomFont'; + src: url('font.woff2') format('woff2'); + font-display: swap; +} + +/* 2. Match fallback metrics to minimize shift */ +@font-face { + font-family: 'CustomFont-Fallback'; + src: local('Arial'); + size-adjust: 105%; /* Scale to match x-height */ + ascent-override: 90%; /* Match ascender height */ + descent-override: 20%; /* Match descender depth */ + line-gap-override: 10%; /* Match line spacing */ +} + +body { + font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; +} +``` + +Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically. + +**`swap` vs `optional`**: `swap` shows fallback text immediately and FOUT-swaps when the web font arrives. `optional` uses the fallback if the web font misses a small load budget (~100ms) and avoids the shift entirely. Pick `optional` when zero layout shift matters more than seeing the branded font on slow networks. + +**Preload the critical weight only**: typically the regular-weight body font used above the fold. Preloading every weight costs more bandwidth than it saves. + +**Variable fonts for 3+ weights or styles**: a single variable font file is usually smaller than three static weight files, gives fractional weight control, and pairs well with `font-optical-sizing: auto`. For 1–2 weights, static is fine. + +#### Modern Web Typography + +##### Fluid Type + +Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate (higher vw = faster scaling). Add a rem offset so it doesn't collapse to 0 on small screens. + +**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes. + +**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI; fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it. + +**Bound your clamp()**: keep `max-size ≤ ~2.5 × min-size`. Wider ratios break the browser's zoom and reflow behaviour and make large viewports feel like the page is shouting. + +**Scale container width and font-size together** so effective character measure stays in the 45–75ch band at every viewport. A heading that widens faster than its container drifts out of the comfortable measure at the top end. + +##### OpenType Features + +Most developers don't know these exist. Use them for polish: + +```css +/* Proper fractions */ +.recipe-amount { font-variant-numeric: diagonal-fractions; } + +/* Small caps for abbreviations */ +abbr { font-variant-caps: all-small-caps; } + +/* Disable ligatures in code */ +code { font-variant-ligatures: none; } + +/* Enable kerning (usually on by default, but be explicit) */ +body { font-kerning: normal; } +``` + +Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/). + +##### Rendering polish + +```css +/* Variable fonts: pick the right optical-size master automatically */ +body { font-optical-sizing: auto; } +``` + +**ALL-CAPS tracking**: capitals sit too close at default spacing. Add 5–12% letter-spacing (`letter-spacing: 0.05em` to `0.12em`) to short all-caps labels, eyebrows, and small headings. Real small caps (via `font-variant-caps`) need the same treatment, slightly gentler. + +#### Typography System Architecture + +Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system. + +#### Accessibility Considerations + +Beyond contrast ratios (which are well-documented), consider: + +- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout. +- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text. +- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile. +- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets. + +--- + +**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text. diff --git a/.qoder/skills/impeccable/reference/typography.md b/.qoder/skills/impeccable/reference/typography.md deleted file mode 100644 index ed5ca272a..000000000 --- a/.qoder/skills/impeccable/reference/typography.md +++ /dev/null @@ -1,159 +0,0 @@ -# Typography - -## Classic Typography Principles - -### Vertical Rhythm - -Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony; text and space share a mathematical foundation. - -### Modular Scale & Hierarchy - -The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy. - -**Use fewer sizes with more contrast.** A 5-size system covers most needs: - -| Role | Typical Ratio | Use Case | -|------|---------------|----------| -| xs | 0.75rem | Captions, legal | -| sm | 0.875rem | Secondary UI, metadata | -| base | 1rem | Body text | -| lg | 1.25-1.5rem | Subheadings, lead text | -| xl+ | 2-4rem | Headlines, hero text | - -Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit. - -### Readability & Measure - -Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length: narrow columns need tighter leading, wide columns need more. - -**Non-obvious**: Light text on dark backgrounds needs compensation on three axes, not just one. Bump line-height by 0.05–0.1, add a touch of letter-spacing (0.01–0.02em), and optionally step the body weight up one notch (regular → medium). The perceived weight drops across all three; fix all three. - -**Paragraph rhythm**: Pick either space between paragraphs OR first-line indentation. Never both. Digital usually wants space; editorial/long-form can justify indent-only. - -## Font Selection & Pairing - -The tactical selection procedure and the reflex-reject list live in [reference/brand.md](brand.md) under **Font selection procedure** and **Reflex-reject list** (loaded for brand-register tasks). The rest of this section covers the adjacent knowledge: anti-reflex corrections, system font use, and pairing rules. - -### Anti-reflexes worth defending against - -- A technical/utilitarian brief does NOT need a serif "for warmth." Most tech tools should look like tech tools. -- An editorial/premium brief does NOT need the same expressive serif everyone is using right now. Premium can be Swiss-modern, can be neo-grotesque, can be a literal monospace, can be a quiet humanist sans. -- A children's product does NOT need a rounded display font. Kids' books use real type. -- A "modern" brief does NOT need a geometric sans. The most modern thing you can do is not use the font everyone else is using. - -**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality. - -### Pairing Principles - -**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif). - -When pairing, contrast on multiple axes: -- Serif + Sans (structure contrast) -- Geometric + Humanist (personality contrast) -- Condensed display + Wide body (proportion contrast) - -**Never pair fonts that are similar but not identical** (e.g., two geometric sans-serifs). They create visual tension without clear hierarchy. - -### Web Font Loading - -The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix: - -```css -/* 1. Use font-display: swap for visibility */ -@font-face { - font-family: 'CustomFont'; - src: url('font.woff2') format('woff2'); - font-display: swap; -} - -/* 2. Match fallback metrics to minimize shift */ -@font-face { - font-family: 'CustomFont-Fallback'; - src: local('Arial'); - size-adjust: 105%; /* Scale to match x-height */ - ascent-override: 90%; /* Match ascender height */ - descent-override: 20%; /* Match descender depth */ - line-gap-override: 10%; /* Match line spacing */ -} - -body { - font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; -} -``` - -Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically. - -**`swap` vs `optional`**: `swap` shows fallback text immediately and FOUT-swaps when the web font arrives. `optional` uses the fallback if the web font misses a small load budget (~100ms) and avoids the shift entirely. Pick `optional` when zero layout shift matters more than seeing the branded font on slow networks. - -**Preload the critical weight only**: typically the regular-weight body font used above the fold. Preloading every weight costs more bandwidth than it saves. - -**Variable fonts for 3+ weights or styles**: a single variable font file is usually smaller than three static weight files, gives fractional weight control, and pairs well with `font-optical-sizing: auto`. For 1–2 weights, static is fine. - -## Modern Web Typography - -### Fluid Type - -Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate (higher vw = faster scaling). Add a rem offset so it doesn't collapse to 0 on small screens. - -**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes. - -**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI; fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it. - -**Bound your clamp()**: keep `max-size ≤ ~2.5 × min-size`. Wider ratios break the browser's zoom and reflow behaviour and make large viewports feel like the page is shouting. - -**Scale container width and font-size together** so effective character measure stays in the 45–75ch band at every viewport. A heading that widens faster than its container drifts out of the comfortable measure at the top end. - -### OpenType Features - -Most developers don't know these exist. Use them for polish: - -```css -/* Tabular numbers for data alignment */ -.data-table { font-variant-numeric: tabular-nums; } - -/* Proper fractions */ -.recipe-amount { font-variant-numeric: diagonal-fractions; } - -/* Small caps for abbreviations */ -abbr { font-variant-caps: all-small-caps; } - -/* Disable ligatures in code */ -code { font-variant-ligatures: none; } - -/* Enable kerning (usually on by default, but be explicit) */ -body { font-kerning: normal; } -``` - -Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/). - -### Rendering polish - -```css -/* Even out heading line lengths (browser picks better break points) */ -h1, h2, h3 { text-wrap: balance; } - -/* Reduce orphans and ragged endings in long prose */ -article p { text-wrap: pretty; } - -/* Variable fonts: pick the right optical-size master automatically */ -body { font-optical-sizing: auto; } -``` - -**ALL-CAPS tracking**: capitals sit too close at default spacing. Add 5–12% letter-spacing (`letter-spacing: 0.05em` to `0.12em`) to short all-caps labels, eyebrows, and small headings. Real small caps (via `font-variant-caps`) need the same treatment, slightly gentler. - -## Typography System Architecture - -Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system. - -## Accessibility Considerations - -Beyond contrast ratios (which are well-documented), consider: - -- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout. -- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text. -- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile. -- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets. - ---- - -**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text. diff --git a/.qoder/skills/impeccable/reference/ux-writing.md b/.qoder/skills/impeccable/reference/ux-writing.md deleted file mode 100644 index 417b5f56c..000000000 --- a/.qoder/skills/impeccable/reference/ux-writing.md +++ /dev/null @@ -1,107 +0,0 @@ -# UX Writing - -## The Button Label Problem - -**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns: - -| Bad | Good | Why | -|-----|------|-----| -| OK | Save changes | Says what will happen | -| Submit | Create account | Outcome-focused | -| Yes | Delete message | Confirms the action | -| Cancel | Keep editing | Clarifies what "cancel" means | -| Click here | Download PDF | Describes the destination | - -**For destructive actions**, name the destruction: -- "Delete" not "Remove" (delete is permanent, remove implies recoverable) -- "Delete 5 items" not "Delete selected" (show the count) - -## Error Messages: The Formula - -Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input". - -### Error Message Templates - -| Situation | Template | -|-----------|----------| -| **Format error** | "[Field] needs to be [format]. Example: [example]" | -| **Missing required** | "Please enter [what's missing]" | -| **Permission denied** | "You don't have access to [thing]. [What to do instead]" | -| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." | -| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" | - -### Don't Blame the User - -Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date". - -## Empty States Are Opportunities - -Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items". - -## Voice vs Tone - -**Voice** is your brand's personality, consistent everywhere. -**Tone** adapts to the moment. - -| Moment | Tone Shift | -|--------|------------| -| Success | Celebratory, brief: "Done! Your changes are live." | -| Error | Empathetic, helpful: "That didn't work. Here's what to try..." | -| Loading | Reassuring: "Saving your work..." | -| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | - -**Never use humor for errors.** Users are already frustrated. Be helpful, not cute. - -## Writing for Accessibility - -**Link text** must have standalone meaning: "View pricing plans" not "Click here". **Alt text** describes information, not the image: "Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context. - -## Writing for Translation - -### Plan for Expansion - -German text is ~30% longer than English. Allocate space: - -| Language | Expansion | -|----------|-----------| -| German | +30% | -| French | +20% | -| Finnish | +30-40% | -| Chinese | -30% (fewer chars, but same width) | - -### Translation-Friendly Patterns - -Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear. - -## Consistency: The Terminology Problem - -Pick one term and stick with it: - -| Inconsistent | Consistent | -|--------------|------------| -| Delete / Remove / Trash | Delete | -| Settings / Preferences / Options | Settings | -| Sign in / Log in / Enter | Sign in | -| Create / Add / New | Create | - -Build a terminology glossary and enforce it. Variety creates confusion. - -## Avoid Redundant Copy - -If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well. - -## Loading States - -Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress. - -## Confirmation Dialogs: Use Sparingly - -Most confirmation dialogs are design failures; consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No"). - -## Form Instructions - -Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking. - ---- - -**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors. diff --git a/.qoder/skills/impeccable/scripts/cleanup-deprecated.mjs b/.qoder/skills/impeccable/scripts/cleanup-deprecated.mjs index 6aee4710a..bc6400e90 100644 --- a/.qoder/skills/impeccable/scripts/cleanup-deprecated.mjs +++ b/.qoder/skills/impeccable/scripts/cleanup-deprecated.mjs @@ -25,7 +25,7 @@ import { join, resolve } from 'node:path'; const DEPRECATED_NAMES = [ // v2.0 renames 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable teach + 'teach-impeccable', // folded into /impeccable init // v2.1 merges 'arrange', // renamed to layout 'normalize', // merged into polish diff --git a/.qoder/skills/impeccable/scripts/command-metadata.json b/.qoder/skills/impeccable/scripts/command-metadata.json index 1488bd5f1..83796d54b 100644 --- a/.qoder/skills/impeccable/scripts/command-metadata.json +++ b/.qoder/skills/impeccable/scripts/command-metadata.json @@ -3,8 +3,8 @@ "description": "Full confirmed-brief-then-build flow. Runs multi-round shape discovery first, resolves visual probe and north-star mock gates when available, then builds and visually iterates. Use when building a new feature end-to-end.", "argumentHint": "[feature description]" }, - "teach": { - "description": "Gathers design context for a project. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.", + "init": { + "description": "Sets up a project for impeccable. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles); offers DESIGN.md (visual: colors, typography, components) when code exists; pre-configures live mode; then recommends the best commands to run next. Every other command reads these files before doing work. Use once per project.", "argumentHint": "" }, "document": { diff --git a/.qoder/skills/impeccable/scripts/context.mjs b/.qoder/skills/impeccable/scripts/context.mjs new file mode 100644 index 000000000..52263c883 --- /dev/null +++ b/.qoder/skills/impeccable/scripts/context.mjs @@ -0,0 +1,253 @@ +/** + * Context loader: prints PRODUCT.md (and DESIGN.md if present) as one + * markdown block on stdout, or exits with empty stdout when no PRODUCT.md + * is found anywhere. The skill keys off "empty stdout" to branch into the + * init flow. + * + * Path resolution (first match wins): + * 1. cwd, if PRODUCT.md or DESIGN.md is there + * 2. .agents/context/ then docs/ + * 3. $IMPECCABLE_CONTEXT_DIR (absolute or cwd-relative) — power-user + * escape hatch, only consulted when defaults are empty + * 4. cwd as a "nothing found" default + * + * `resolveContextDir()` and `loadContext()` are also exported for the + * server-side scripts (live.mjs, live-server.mjs) that need the structured + * shape rather than the markdown block. + */ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; +const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; +const FALLBACK_DIRS = ['.agents/context', 'docs']; + +// ─── Update check ────────────────────────────────────────────────────────── +// Piggyback a lightweight skill-version check on the once-per-session boot. +// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent +// can offer `npx impeccable skills update`. Everything here is best-effort and +// silent on failure: a network problem, sandbox, or missing cache must never +// block context output or print an error. + +const UPDATE_HOST = (process.env.IMPECCABLE_UPDATE_HOST || 'https://impeccable.style').replace(/\/$/, ''); +const UPDATE_CACHE_PATH = + process.env.IMPECCABLE_UPDATE_CACHE || path.join(os.homedir(), '.impeccable', 'update-check.json'); +const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // throttle the network poll to once a day +const RENOTIFY_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000; // don't re-surface the same version for a week +const FETCH_TIMEOUT_MS = 1200; + +export function resolveContextDir(cwd = process.cwd()) { + if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { + return cwd; + } + for (const rel of FALLBACK_DIRS) { + const candidate = path.resolve(cwd, rel); + if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { + return candidate; + } + } + const envDir = process.env.IMPECCABLE_CONTEXT_DIR; + if (envDir && envDir.trim()) { + const trimmed = envDir.trim(); + return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); + } + return cwd; +} + +export function loadContext(cwd = process.cwd()) { + const contextDir = resolveContextDir(cwd); + const productPath = firstExisting(contextDir, PRODUCT_NAMES); + const designPath = firstExisting(contextDir, DESIGN_NAMES); + const product = productPath ? safeRead(productPath) : null; + const design = designPath ? safeRead(designPath) : null; + return { + hasProduct: !!product, + product, + productPath: productPath ? path.relative(cwd, productPath) : null, + hasDesign: !!design, + design, + designPath: designPath ? path.relative(cwd, designPath) : null, + contextDir, + }; +} + +function firstExisting(dir, names) { + for (const name of names) { + const abs = path.join(dir, name); + if (fs.existsSync(abs)) return abs; + } + return null; +} + +function safeRead(p) { + try { + return fs.readFileSync(p, 'utf-8'); + } catch { + return null; + } +} + +/** + * Pull the register (`brand` or `product`) out of PRODUCT.md by looking + * for a `## Register` section and reading the first non-empty line that + * follows it. Returns null when the file is legacy / register-less. + */ +function extractRegister(product) { + if (!product) return null; + const lines = product.split('\n'); + for (let i = 0; i < lines.length; i++) { + if (/^##\s+Register\b/i.test(lines[i].trim())) { + for (let j = i + 1; j < lines.length; j++) { + const next = lines[j].trim(); + if (!next) continue; + const word = next.toLowerCase(); + if (word === 'brand' || word === 'product') return word; + return null; + } + } + } + return null; +} + +/** + * Read the installed skill's own version from the sibling SKILL.md frontmatter + * (this file lives at `/scripts/context.mjs`). Returns null when the + * frontmatter is missing or unreadable. + */ +function readLocalSkillVersion() { + try { + const here = path.dirname(fileURLToPath(import.meta.url)); + const skillMd = path.join(here, '..', 'SKILL.md'); + const content = fs.readFileSync(skillMd, 'utf-8'); + const match = content.match(/^version:\s*(.+)$/m); + return match ? match[1].trim().replace(/^["']|["']$/g, '') : null; + } catch { + return null; + } +} + +function readUpdateCache() { + try { + return JSON.parse(fs.readFileSync(UPDATE_CACHE_PATH, 'utf-8')); + } catch { + return {}; + } +} + +function writeUpdateCache(cache) { + try { + fs.mkdirSync(path.dirname(UPDATE_CACHE_PATH), { recursive: true }); + fs.writeFileSync(UPDATE_CACHE_PATH, JSON.stringify(cache)); + } catch { + // Best-effort: a read-only home dir just means we re-poll next session. + } +} + +/** Compare dotted numeric versions. Returns >0 when a is newer than b. */ +function compareSemver(a, b) { + const pa = String(a).split('.').map(n => parseInt(n, 10) || 0); + const pb = String(b).split('.').map(n => parseInt(n, 10) || 0); + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + const diff = (pa[i] || 0) - (pb[i] || 0); + if (diff !== 0) return diff; + } + return 0; +} + +async function fetchLatestSkillVersion() { + try { + const res = await fetch(`${UPDATE_HOST}/api/version`, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }); + if (!res.ok) return null; + const data = await res.json(); + return typeof data?.skills === 'string' ? data.skills : null; + } catch { + return null; // offline, sandboxed, timed out, or bad JSON: all non-fatal + } +} + +function buildUpdateDirective(localVersion, latestVersion) { + return ( + `UPDATE_AVAILABLE: A newer Impeccable skill is available ` + + `(installed v${localVersion}, latest v${latestVersion}). ` + + `Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` + + `Update now? It runs \`npx impeccable skills update\`." ` + + `If they agree, run \`npx impeccable skills update\` (the update applies to the next session, not this one). ` + + `Either way, continue the current task without waiting, and do not raise this again.` + ); +} + +/** + * Best-effort update directive for the boot output. Returns a string to append + * or null. Polls the version endpoint at most once per day (cached globally in + * the user's home dir) and re-surfaces a given version at most once per week so + * the agent never nags. Opt out entirely with IMPECCABLE_NO_UPDATE_CHECK=1. + */ +async function computeUpdateDirective(now = Date.now()) { + try { + if (process.env.IMPECCABLE_NO_UPDATE_CHECK) return null; + const localVersion = readLocalSkillVersion(); + if (!localVersion) return null; + + const cache = readUpdateCache(); + + // Poll the network only when the throttle window has elapsed. Stamp + // lastCheck even on failure so an offline machine doesn't poll every boot. + if (!cache.lastCheck || now - cache.lastCheck > CHECK_INTERVAL_MS) { + const latest = await fetchLatestSkillVersion(); + cache.lastCheck = now; + if (latest) cache.latestVersion = latest; + writeUpdateCache(cache); + } + + const latest = cache.latestVersion; + if (!latest || compareSemver(latest, localVersion) <= 0) return null; + + // Anti-nag: surface a given version at most once per RENOTIFY window. + if (cache.notifiedVersion === latest && cache.notifiedAt && now - cache.notifiedAt < RENOTIFY_INTERVAL_MS) { + return null; + } + cache.notifiedVersion = latest; + cache.notifiedAt = now; + writeUpdateCache(cache); + + return buildUpdateDirective(localVersion, latest); + } catch { + return null; + } +} + +async function cli() { + const ctx = loadContext(process.cwd()); + const updateDirective = await computeUpdateDirective(); + + if (!ctx.hasProduct) { + // Direct stdout message instead of relying on empty output as a signal + // — cheap models miss the empty case more often than the explicit one. + const parts = [ + 'NO_PRODUCT_MD: This project has no PRODUCT.md yet. ' + + 'Stop the current task, load reference/init.md, and follow its ' + + 'instructions to write PRODUCT.md before resuming.', + ]; + if (updateDirective) parts.push(updateDirective); + process.stdout.write(parts.join('\n\n---\n\n') + '\n'); + process.exit(0); + } + const parts = [`# PRODUCT.md\n\n${ctx.product.trim()}`]; + if (ctx.hasDesign) { + parts.push(`# DESIGN.md\n\n${ctx.design.trim()}`); + } + const register = extractRegister(ctx.product); + const next = register + ? `NEXT STEP: This project's register is \`${register}\`. You MUST now read \`reference/${register}.md\` before producing any design output.` + : `NEXT STEP: You MUST now read the matching register reference (\`reference/brand.md\` or \`reference/product.md\`) before producing any design output. Pick based on PRODUCT.md above.`; + parts.push(next); + if (updateDirective) parts.push(updateDirective); + process.stdout.write(parts.join('\n\n---\n\n') + '\n'); +} + +const _running = process.argv[1]; +if (_running?.endsWith('context.mjs') || _running?.endsWith('context.mjs/')) { + cli(); +} diff --git a/.qoder/skills/impeccable/scripts/design-parser.mjs b/.qoder/skills/impeccable/scripts/design-parser.mjs index b68756640..82a5cd7dc 100644 --- a/.qoder/skills/impeccable/scripts/design-parser.mjs +++ b/.qoder/skills/impeccable/scripts/design-parser.mjs @@ -63,7 +63,7 @@ function parseYamlSubset(yaml) { } const key = content.slice(0, colonIdx).trim(); - const rest = content.slice(colonIdx + 1).trim(); + const rest = stripInlineYamlComment(content.slice(colonIdx + 1).trim()); const parent = stack[stack.length - 1].obj; if (rest === '') { @@ -93,6 +93,21 @@ function findTopLevelColon(s) { return -1; } +function stripInlineYamlComment(s) { + let inQuote = null; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (inQuote) { + if (ch === inQuote && s[i - 1] !== '\\') inQuote = null; + } else if (ch === '"' || ch === "'") { + inQuote = ch; + } else if (ch === '#' && i > 0 && /\s/.test(s[i - 1])) { + return s.slice(0, i).trimEnd(); + } + } + return s; +} + function parseScalar(raw) { const s = raw.trim(); if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { diff --git a/.qoder/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.qoder/skills/impeccable/scripts/detector/browser/injected/index.mjs index c03a69caf..ceff16737 100644 --- a/.qoder/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/.qoder/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -9,8 +9,21 @@ if (IS_BROWSER) { const EXTENSION_MODE = (_myScript && _myScript.dataset.impeccableExtension === 'true') || document.documentElement.dataset.impeccableExtension === 'true'; - const BRAND_COLOR = 'oklch(55% 0.25 350)'; - const BRAND_COLOR_HOVER = 'oklch(45% 0.25 350)'; + // Kinpaku gold — pinned to the site's brand token (see + // site/styles/kinpaku-tokens.css --ks-kinpaku). Keep this in sync with + // the picker's C.brand in skill/scripts/live-browser.js and the kit's + // picker section in site/styles/kinpaku-kit.css. + // + // One color across both light and dark host pages. The outline is a + // 2px gesture pointing at an element + a labeled tag — it's a marker, + // not body text, so it doesn't need WCAG AA against the page. The + // label text inside the gold tag is dark (LABEL_INK) which has ~16:1 + // against the leaf gold, so reading the rule name is solid in both + // modes. Hover deepens the gold (preserves chroma — never drops it, + // dropping chroma washes the gold into a sand/olive tone). + const BRAND_COLOR = 'oklch(84% 0.19 80.46)'; + const BRAND_COLOR_HOVER = 'oklch(74% 0.18 80)'; + const LABEL_INK = 'oklch(4% 0.004 95)'; const LABEL_BG = BRAND_COLOR; const OUTLINE_COLOR = BRAND_COLOR; @@ -278,7 +291,7 @@ if (IS_BROWSER) { display: 'flex', alignItems: 'center', whiteSpace: 'nowrap', fontSize: '11px', fontWeight: '600', letterSpacing: '0.02em', - color: 'white', lineHeight: '14px', + color: LABEL_INK, lineHeight: '14px', background: LABEL_BG, fontFamily: 'system-ui, sans-serif', borderRadius: '4px 4px 0 0', @@ -398,7 +411,7 @@ if (IS_BROWSER) { banner.className = 'impeccable-overlay impeccable-banner'; Object.assign(banner.style, { position: 'fixed', top: '0', left: '0', right: '0', zIndex: '100000', - background: LABEL_BG, color: 'white', + background: LABEL_BG, color: LABEL_INK, fontFamily: 'system-ui, sans-serif', fontSize: '13px', display: 'flex', alignItems: 'center', pointerEvents: 'auto', height: '36px', overflow: 'hidden', maxWidth: '100vw', @@ -1223,12 +1236,12 @@ if (IS_BROWSER) { } console.group( `%c[impeccable] ${allFindings.length} anti-pattern${allFindings.length === 1 ? '' : 's'} found`, - 'color: oklch(60% 0.25 350); font-weight: bold' + 'color: oklch(84% 0.19 80.46); font-weight: bold' ); for (const { el, findings } of allFindings) { for (const f of findings) { console.log(`%c${f.type || f.id}%c ${f.detail || f.snippet}`, - 'color: oklch(55% 0.25 350); font-weight: bold', 'color: inherit', el); + 'color: oklch(84% 0.19 80.46); font-weight: bold', 'color: inherit', el); } } console.groupEnd(); @@ -1249,6 +1262,10 @@ if (IS_BROWSER) { const groupMap = new Map(); const _disabled = EXTENSION_MODE ? (window.__IMPECCABLE_CONFIG__?.disabledRules || []) : []; const _ruleOk = (id) => !_disabled.length || !_disabled.includes(id); + // Note: provider-gated rules (--gpt / --gemini) are NOT filtered here. In a + // real browser env (detector page, live overlay, extension) running every + // check is free, so we always surface them; the gating is purely a CLI + // output concern, applied in the Node engines' detect* return paths. for (const el of document.querySelectorAll('*')) { // Skip impeccable's own elements and any descendants (overlays, labels, banner, nav buttons) @@ -1270,11 +1287,23 @@ if (IS_BROWSER) { ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), - ...checkElementHeroEyebrowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementOversizedH1DOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementClippedOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementGptBorderShadowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementTextOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ].filter(f => _ruleOk(f.type)); addBrowserFindings(groupMap, el, findings); + + // Hero eyebrow: the offending element is the eyebrow above the heading, + // not the heading itself — highlight the previous sibling instead. + const eyebrowFindings = checkElementHeroEyebrowDOM(el) + .map(f => ({ type: f.id, detail: f.snippet })) + .filter(f => _ruleOk(f.type)); + if (eyebrowFindings.length > 0 && el.previousElementSibling) { + addBrowserFindings(groupMap, el.previousElementSibling, eyebrowFindings); + } } const pageLevelFindings = []; @@ -1306,6 +1335,14 @@ if (IS_BROWSER) { addBrowserFindings(groupMap, document.body, qualityFindings); } + const creamFindings = checkCreamPalette(document) + .map(f => ({ type: f.id, detail: f.snippet })) + .filter(f => _ruleOk(f.type)); + if (creamFindings.length > 0) { + pageLevelFindings.push(...creamFindings); + addBrowserFindings(groupMap, document.body, creamFindings); + } + // Regex-on-HTML checks (shared with Node) // Clone the document and strip impeccable-live overlay nodes before the // regex scan, so the inspector's own inline styles (transitions on top/ diff --git a/.qoder/skills/impeccable/scripts/detector/cli/main.mjs b/.qoder/skills/impeccable/scripts/detector/cli/main.mjs index 76da09f0c..94283e01b 100644 --- a/.qoder/skills/impeccable/scripts/detector/cli/main.mjs +++ b/.qoder/skills/impeccable/scripts/detector/cli/main.mjs @@ -41,7 +41,7 @@ function formatFindings(findings, jsonMode) { // Stdin handling // --------------------------------------------------------------------------- -async function handleStdin() { +async function handleStdin(options = {}) { const chunks = []; for await (const chunk of process.stdin) chunks.push(chunk); const input = Buffer.concat(chunks).toString('utf-8'); @@ -50,10 +50,10 @@ async function handleStdin() { const fp = parsed?.tool_input?.file_path; if (fp && fs.existsSync(fp)) { return HTML_EXTENSIONS.has(path.extname(fp).toLowerCase()) - ? detectHtml(fp) : detectText(fs.readFileSync(fp, 'utf-8'), fp); + ? detectHtml(fp, options) : detectText(fs.readFileSync(fp, 'utf-8'), fp, options); } } catch { /* not JSON */ } - return detectText(input, ''); + return detectText(input, '', options); } @@ -81,6 +81,8 @@ Scan files or URLs for UI anti-patterns and design quality issues. Options: --fast Regex-only mode (skip static HTML/CSS analysis, faster but misses linked stylesheets) --json Output results as JSON + --gpt Also report GPT-specific provider tells (off by default) + --gemini Also report Gemini-specific provider tells (off by default) --help Show this help message Detection modes: @@ -106,6 +108,10 @@ async function detectCli() { const jsonMode = args.includes('--json'); const helpMode = args.includes('--help'); const fastMode = args.includes('--fast'); + const providers = []; + if (args.includes('--gpt')) providers.push('gpt'); + if (args.includes('--gemini')) providers.push('gemini'); + const scanOptions = { providers }; const targets = args.filter(a => !a.startsWith('--')); if (helpMode) { printUsage(); process.exit(0); } @@ -113,7 +119,7 @@ async function detectCli() { let allFindings = []; if (!process.stdin.isTTY && targets.length === 0) { - allFindings = await handleStdin(); + allFindings = await handleStdin(scanOptions); } else { const paths = targets.length > 0 ? targets : [process.cwd()]; const urlTargetCount = paths.filter(target => /^https?:\/\//i.test(target)).length; @@ -124,8 +130,8 @@ async function detectCli() { if (/^https?:\/\//i.test(target)) { try { const scanner = browserDetector - ? (url) => browserDetector.detectUrl(url) - : (url) => detectUrl(url); + ? (url) => browserDetector.detectUrl(url, scanOptions) + : (url) => detectUrl(url, scanOptions); allFindings.push(...await scanner(target)); } catch (e) { process.stderr.write(`Error: ${e.message}\n`); } continue; @@ -192,9 +198,9 @@ async function detectCli() { const ext = path.extname(file).toLowerCase(); let fileFindings; if (!fastMode && HTML_EXTENSIONS.has(ext)) { - fileFindings = await detectHtml(file); + fileFindings = await detectHtml(file, scanOptions); } else { - fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file); + fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file, scanOptions); } // Annotate findings with import context const importers = importedByMap.get(file); @@ -209,9 +215,9 @@ async function detectCli() { } else if (stat.isFile()) { const ext = path.extname(resolved).toLowerCase(); if (!fastMode && HTML_EXTENSIONS.has(ext)) { - allFindings.push(...await detectHtml(resolved)); + allFindings.push(...await detectHtml(resolved, scanOptions)); } else { - allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved)); + allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved, scanOptions)); } } } diff --git a/.qoder/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.qoder/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 1afbe3f12..88439a5c3 100644 --- a/.qoder/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.qoder/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -38,7 +38,7 @@ const OVERUSED_FONTS = new Set([ // Older monoculture (still ubiquitous): 'inter', 'roboto', 'open sans', 'lato', 'montserrat', 'arial', 'helvetica', // Newer monoculture (the Anthropic-skill / Vercel / GitHub default wave): - 'fraunces', 'instrument sans', + 'fraunces', 'instrument sans', 'instrument serif', 'geist', 'geist sans', 'geist mono', 'mona sans', 'plus jakarta sans', 'space grotesk', 'recoleta', @@ -165,6 +165,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'AI color palette', }, + { + id: 'cream-palette', + category: 'slop', + name: 'Cream / beige palette', + description: + 'A warm cream or beige page background has become the default "tasteful" AI surface, reached for by reflex. Choose a background that comes from a deliberate palette, not the safe warm off-white.', + skillSection: 'Color & Contrast', + skillGuideline: 'cream and beige as the default surface', + }, { id: 'nested-cards', category: 'slop', @@ -183,15 +192,6 @@ const ANTIPATTERNS = [ skillSection: 'Layout & Space', skillGuideline: 'same spacing everywhere', }, - { - id: 'everything-centered', - category: 'slop', - name: 'Everything centered', - description: - 'Every text element is center-aligned. Left-aligned text with asymmetric layouts feels more designed. Center only hero sections and CTAs.', - skillSection: 'Layout & Space', - skillGuideline: 'Center everything', - }, { id: 'bounce-easing', category: 'slop', @@ -247,17 +247,72 @@ const ANTIPATTERNS = [ skillSection: 'Typography', skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding', }, + { + id: 'numbered-section-markers', + category: 'slop', + severity: 'advisory', + name: 'Numbered section markers (01 / 02 / 03)', + description: + 'Numbered display markers as section labels (01, 02, 03) are the AI editorial scaffold one tier deeper than tracked eyebrow chips. If you find yourself reaching for them, choose a different section cadence.', + skillSection: 'Layout & Space', + skillGuideline: 'numbered section markers', + }, + { + id: 'em-dash-overuse', + category: 'slop', + name: 'Em-dash overuse', + description: + 'More than two em-dashes (— or --) in body copy is an AI cadence tell. Use commas, colons, periods, or parentheses instead.', + skillSection: 'Copy', + skillGuideline: 'no em dashes', + }, + { + id: 'marketing-buzzword', + category: 'slop', + name: 'Marketing buzzword', + description: + 'Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does.', + skillSection: 'Copy', + skillGuideline: 'marketing buzzwords', + }, + { + id: 'aphoristic-cadence', + category: 'slop', + name: 'Aphoristic-cadence copy', + description: + 'Three or more sections landing on a short rebuttal sentence ("X. No Y." / "X. Just Y.") or a manufactured-contrast aphorism ("Not a feature. A platform.") reads as AI cadence, not voice. Once is fine; the pattern is the tell.', + skillSection: 'Copy', + skillGuideline: 'aphoristic cadence', + }, + { + id: 'oversized-h1', + category: 'slop', + name: 'Oversized hero headline', + description: + 'A full-sentence headline set at display size ends up dominating the viewport, leaving no room for anything else above the fold. A punchy one- or two-word headline at that size is fine — the problem is a long headline blown up too large. Set long headlines smaller, or tighten the copy.', + skillSection: 'Typography', + skillGuideline: 'long headline set at display size', + }, + { + id: 'extreme-negative-tracking', + category: 'slop', + name: 'Crushed letter spacing', + description: + 'Letter-spacing pulled tighter than the point where characters keep their own shapes costs legibility. Tighten display type optically, not destructively.', + skillSection: 'Typography', + skillGuideline: 'letter spacing crushed past legibility', + }, + { + id: 'broken-image', + category: 'quality', + name: 'Broken or placeholder image', + description: + ' tags with empty src, missing src, or placeholder values ship as broken-image boxes. Use real images, generated assets, or remove the tag.', + skillSection: 'Imagery', + skillGuideline: 'broken image references', + }, // ── Quality: general design and accessibility issues ── - { - id: 'pure-black-white', - category: 'quality', - name: 'Pure black background', - description: - 'Pure #000000 as a background color looks harsh and unnatural. Tint it slightly toward your brand hue (e.g., oklch(12% 0.01 250)) for a more refined feel.', - skillSection: 'Color & Contrast', - skillGuideline: 'pure black (#000)', - }, { id: 'gray-on-color', category: 'quality', @@ -297,7 +352,9 @@ const ANTIPATTERNS = [ category: 'quality', name: 'Cramped padding', description: - 'Text is too close to the edge of its container. Add at least 8px (ideally 12-16px) of padding inside bordered or colored containers.', + 'Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.', + skillSection: 'Layout & Space', + skillGuideline: 'inside bordered or colored containers', }, { id: 'body-text-viewport-edge', @@ -350,6 +407,70 @@ const ANTIPATTERNS = [ description: 'Letter spacing above 0.05em on body text disrupts natural character groupings and slows reading. Reserve wide tracking for short uppercase labels only.', }, + { + id: 'text-overflow', + category: 'quality', + name: 'Content overflowing its container', + description: + 'Content renders wider than its container, spilling out or forcing a horizontal scrollbar. Let text wrap, constrain widths, or give the region a deliberate scroll affordance.', + skillSection: 'Layout & Space', + skillGuideline: 'content wider than its container', + }, + { + id: 'clipped-overflow-container', + category: 'quality', + name: 'Positioned child clipped by overflow container', + description: + 'A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.', + skillSection: 'Layout & Space', + skillGuideline: 'overflow container clipping positioned children', + }, + + // ── Provider tells: opt-in via --gpt / --gemini (gated off by default) ── + { + id: 'gpt-thin-border-wide-shadow', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Hairline border with wide shadow', + description: + 'A hairline border paired with a wide, diffuse shadow is a recurring generated-UI signature. Commit to one — a defined edge or a soft elevation — rather than both at once.', + skillSection: 'Visual Details', + skillGuideline: 'hairline border plus wide diffuse shadow', + }, + { + id: 'repeating-stripes-gradient', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Repeating-gradient stripes', + description: + 'Repeating-gradient stripes used as surface decoration are a recurring generated-UI signature. Reach for a deliberate texture or leave the surface plain.', + skillSection: 'Visual Details', + skillGuideline: 'repeating-gradient decorative stripes', + }, + { + id: 'theater-slop-phrase', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Theater framing copy', + description: + 'Dismissing something as "theater" is a recurring generated-copy tic. Say plainly what the thing does or does not do.', + skillSection: 'Copy', + skillGuideline: 'theater framing copy', + }, + { + id: 'image-hover-transform', + category: 'slop', + severity: 'advisory', + gated: 'gemini', + name: 'Image hover transform', + description: + 'Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction.', + skillSection: 'Motion', + skillGuideline: 'image scale or rotate on hover', + }, ]; // --- cli/engine/shared/color.mjs --- @@ -527,11 +648,6 @@ function checkColors(opts) { } const findings = []; - // Pure black background (only solid or near-solid, not semi-transparent overlays) - if (bgColor && bgColor.a >= 0.9 && bgColor.r === 0 && bgColor.g === 0 && bgColor.b === 0) { - findings.push({ id: 'pure-black-white', snippet: '#000000 background' }); - } - if (hasDirectText && textColor && !isEmojiOnly) { // Run background-dependent checks against either a solid bg or, if the // ancestor is a gradient, against every gradient stop (use the worst case). @@ -587,9 +703,6 @@ function checkColors(opts) { // Tailwind class checks if (classList) { const classStr = typeof classList === 'string' ? classList : Array.from(classList).join(' '); - if (/\bbg-black\b(?!\/)/.test(classStr)) { - findings.push({ id: 'pure-black-white', snippet: 'bg-black' }); - } const grayMatch = classStr.match(/\btext-(?:gray|slate|zinc|neutral|stone)-\d+\b/); const colorBgMatch = classStr.match(/\bbg-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+\b/); @@ -905,12 +1018,6 @@ function checkHtmlPatterns(html) { // --- Color --- - // Pure black background - const pureBlackBgRe = /background(?:-color)?\s*:\s*(?:#000000|#000|rgb\(\s*0,\s*0,\s*0\s*\))\b/gi; - if (pureBlackBgRe.test(html)) { - findings.push({ id: 'pure-black-white', snippet: 'Pure #000 background' }); - } - // AI color palette: purple/violet const purpleHexRe = /#(?:7c3aed|8b5cf6|a855f7|9333ea|7e22ce|6d28d9|6366f1|764ba2|667eea)\b/gi; if (purpleHexRe.test(html)) { @@ -1027,6 +1134,39 @@ function checkHtmlPatterns(html) { } } + // --- Provider tells (gated): repeating-gradient stripes (GPT) --- + if (/repeating-(?:linear|radial|conic)-gradient\s*\(/i.test(html)) { + findings.push({ id: 'repeating-stripes-gradient', snippet: 'repeating-gradient decorative stripes' }); + } + + // --- Provider tells (gated): "X theater" framing copy (GPT) --- + // Lives here (regex-on-HTML) rather than in the text-content analyzers so it + // runs in the bundled browser path too, not just the CLI/static path. + { + const bodyText = html + .replace(/]*>[\s\S]*?<\/script>/gi, ' ') + .replace(/]*>[\s\S]*?<\/style>/gi, ' ') + .replace(/<[^>]+>/g, ' '); + const tm = /\b(\w+)\s+theater\b/i.exec(bodyText); + if (tm) findings.push({ id: 'theater-slop-phrase', snippet: `"${tm[0].trim()}"` }); + } + + // --- Provider tells (gated): image hover transform (Gemini) --- + // A CSS `img...:hover { transform: ... }` rule, or a Tailwind hover:scale / + // hover:rotate / hover:translate utility on an . Each distinct + // mechanism is its own finding. + const imgHoverCss = /\bimg\b[^,{}]*:hover\b[^{}]*\{[^}]*\btransform\s*:\s*(?:scale|rotate|translate|matrix|skew)/i; + if (imgHoverCss.test(html)) { + findings.push({ id: 'image-hover-transform', snippet: 'img:hover { transform } rule' }); + } + const imgTagRe = /]*\bclass\s*=\s*"([^"]*)"/gi; + let im; + while ((im = imgTagRe.exec(html)) !== null) { + if (/\bhover:(?:scale|rotate|translate|skew)-/.test(im[1])) { + findings.push({ id: 'image-hover-transform', snippet: 'Tailwind hover transform on ' }); + } + } + return findings; } @@ -1673,7 +1813,7 @@ function resolveLengthPx(value, fontSizePx) { // Both adapters resolve font-size, line-height and letter-spacing to pixels // before calling this so the pure function only deals with numbers. function checkQuality(opts) { - const { el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax = 80, viewportWidth = 0 } = opts; + const { el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax = 80, viewportWidth = 0, win = null } = opts; const findings = []; // Skip browser extension injected elements const elId = el.id || ''; @@ -1724,6 +1864,155 @@ function checkQuality(opts) { } } + // --- Flush against a visible boundary --- + // Fires when a container has a visible boundary (border, outline, OR a + // non-transparent background) AND near-zero padding on the bounded + // side(s) AND text-bearing children land flush against the boundary. + // + // Distinct from cramped-padding: that rule needs the element itself to + // have direct text (hasDirectText). This rule targets the OPPOSITE + // shape — a container with NO direct text, only children — which is + // exactly what cramped-padding misses (a section wrapping a label + + // list lands a free pass). + // + // The classic shape: agent writes `padding: 28px 0 0` shorthand on a + // section that also has a border, zeroing horizontal padding so the + // text-bearing children touch the side borders. Background and + // outline count too: a colored card with zero padding has the same + // visual failure mode. + { + const FLUSH_SKIP_TAGS = new Set(['HTML', 'BODY', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ARTICLE', 'ASIDE', 'BUTTON', 'A', 'LABEL', 'SUMMARY', 'CODE', 'PRE', 'INPUT', 'TEXTAREA', 'SELECT', 'FORM', 'FIGURE', 'TABLE', 'TBODY', 'THEAD', 'TR', 'TD', 'TH']); + const upperTag = tag ? tag.toUpperCase() : ''; + const elPosition = style.position || ''; + if ( + !FLUSH_SKIP_TAGS.has(upperTag) && + !hasDirectText && + !['fixed', 'absolute'].includes(elPosition) && + el.children && el.children.length > 0 + ) { + const isTransparent = (c) => + !c || c === 'transparent' || c === 'rgba(0, 0, 0, 0)' || + /^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*0(?:\.0+)?\s*\)$/.test(c); + + const borderW = { + top: parseFloat(style.borderTopWidth) || 0, + right: parseFloat(style.borderRightWidth) || 0, + bottom: parseFloat(style.borderBottomWidth) || 0, + left: parseFloat(style.borderLeftWidth) || 0, + }; + const borderVisible = { + top: borderW.top > 0 && !isTransparent(style.borderTopColor), + right: borderW.right > 0 && !isTransparent(style.borderRightColor), + bottom: borderW.bottom > 0 && !isTransparent(style.borderBottomColor), + left: borderW.left > 0 && !isTransparent(style.borderLeftColor), + }; + // Outline detection. jsdom decomposes `border` shorthand into + // border{Top,…}Width/Color but does NOT decompose `outline` — + // the longhands come back empty when the value was set via the + // shorthand. Fall back to parsing `style.outline` ourselves. + let outlineW = parseFloat(style.outlineWidth) || 0; + let outlineStyleVal = style.outlineStyle || ''; + let outlineColorVal = style.outlineColor || ''; + if (!outlineW && style.outline) { + const wMatch = style.outline.match(/(\d+(?:\.\d+)?)\s*px/); + if (wMatch) outlineW = parseFloat(wMatch[1]) || 0; + if (!outlineStyleVal) { + outlineStyleVal = /\b(solid|dashed|dotted|double|groove|ridge|inset|outset)\b/.test(style.outline) ? 'solid' : ''; + } + if (!outlineColorVal) { + const cMatch = style.outline.match(/(rgba?\([^)]+\)|#[0-9a-fA-F]{3,8}|[a-zA-Z]+)\s*$/); + if (cMatch) outlineColorVal = cMatch[1]; + } + } + const outlineVisible = outlineW > 0 && !isTransparent(outlineColorVal) && outlineStyleVal && outlineStyleVal !== 'none'; + const bgVisible = !isTransparent(style.backgroundColor); + + const anyVisible = borderVisible.top || borderVisible.right || borderVisible.bottom || borderVisible.left || outlineVisible || bgVisible; + if (anyVisible) { + // Resolve padding to px (jsdom returns raw "1.5rem" etc., not the + // computed px value; parseFloat would strip the unit and treat + // 1.5rem as 1.5px, false-flagging legitimate insets). + const pad = { + top: resolveLengthPx(style.paddingTop, fontSize) ?? 0, + right: resolveLengthPx(style.paddingRight, fontSize) ?? 0, + bottom: resolveLengthPx(style.paddingBottom, fontSize) ?? 0, + left: resolveLengthPx(style.paddingLeft, fontSize) ?? 0, + }; + const PAD_THRESHOLD = 2; + // Children-insulate-this-side: a side is insulated if ANY direct + // child has its own padding ≥ 4px on that side. Rationale: in + // typical flow, only the first/last (or leftmost/rightmost) + // children actually sit at the parent's edges. If even one of + // them has its own padding, the visual flush is broken on that + // side. Classic example: a column-flow card frame where the + // top child (header) has padding-top:12 and the bottom child + // (footer) has padding-bottom:8 — the parent's padding:0 doesn't + // matter; nothing is actually flush. The `any-child-insulates` + // heuristic accepts some false negatives (a card with one heavily + // padded middle child won't flag) for far fewer false positives. + const CHILD_INSULATE_THRESHOLD = 4; + const childrenInsulate = { top: false, right: false, bottom: false, left: false }; + for (const child of el.children) { + let childStyle = null; + if (win && typeof win.getComputedStyle === 'function') { + try { childStyle = win.getComputedStyle(child); } catch {} + } + if (!childStyle && typeof getComputedStyle === 'function') { + try { childStyle = getComputedStyle(child); } catch {} + } + if (!childStyle) continue; + const childPad = { + top: resolveLengthPx(childStyle.paddingTop, fontSize) ?? 0, + right: resolveLengthPx(childStyle.paddingRight, fontSize) ?? 0, + bottom: resolveLengthPx(childStyle.paddingBottom, fontSize) ?? 0, + left: resolveLengthPx(childStyle.paddingLeft, fontSize) ?? 0, + }; + for (const s of ['top', 'right', 'bottom', 'left']) { + if (childPad[s] >= CHILD_INSULATE_THRESHOLD) childrenInsulate[s] = true; + } + } + + const flushSides = []; + for (const side of ['top', 'right', 'bottom', 'left']) { + const sideBounded = borderVisible[side] || outlineVisible || bgVisible; + if (sideBounded && pad[side] <= PAD_THRESHOLD && !childrenInsulate[side]) { + flushSides.push(side); + } + } + + if (flushSides.length > 0) { + // Confirm at least one direct child has substantial text content + // (> 4 chars). Without this, the flush is harmless: e.g. an + // image-only card. + let hasTextChild = false; + for (const child of el.children) { + const childText = (child.textContent || '').trim(); + if (childText.length > 4) { hasTextChild = true; break; } + } + if (hasTextChild) { + const cls = (typeof el.className === 'string' && el.className.trim()) + ? el.className.trim().split(/\s+/)[0] + : ''; + const boundaryParts = []; + const borderSidesVisible = ['top', 'right', 'bottom', 'left'].filter(s => borderVisible[s]); + if (borderSidesVisible.length === 4) boundaryParts.push('border'); + else if (borderSidesVisible.length > 0) boundaryParts.push(`border-${borderSidesVisible.join('/')}`); + if (outlineVisible) boundaryParts.push('outline'); + if (bgVisible) boundaryParts.push('bg'); + const sidesLabel = flushSides.length === 4 ? 'all sides' : flushSides.join('/'); + const ident = cls + ? `<${tag.toLowerCase()}> "${cls}"` + : `<${tag.toLowerCase()}>`; + findings.push({ + id: 'cramped-padding', + snippet: `${ident}: children flush against ${boundaryParts.join('+')} on ${sidesLabel} (no inset)`, + }); + } + } + } + } + } + // --- Body text touching viewport edge --- (browser-only: needs rect) // Catches the failure mode where the agent ships body paragraphs // with NO container providing horizontal padding — text bleeds @@ -1804,6 +2093,20 @@ function checkQuality(opts) { } } + // --- Crushed letter spacing (mirror of wide-tracking) --- + // Tracking pulled tighter than ~-0.05em crushes characters into each other. + // Optical tightening that display type legitimately wants (around -0.02em) + // stays well above this floor. + if (hasDirectText && textLen > 20 && fontSize > 0) { + if (letterSpacingPx != null && letterSpacingPx < 0) { + const trackingEm = letterSpacingPx / fontSize; + if (trackingEm <= -0.05) { + const excerpt = (el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 40); + findings.push({ id: 'extreme-negative-tracking', snippet: `letter-spacing: ${trackingEm.toFixed(2)}em — "${excerpt}"` }); + } + } + } + return findings; } @@ -1820,7 +2123,7 @@ function checkElementQualityDOM(el) { const rect = el.getBoundingClientRect(); const lineMax = (typeof window !== 'undefined' && window.__IMPECCABLE_CONFIG__?.lineLengthMax) || 80; const viewportWidth = (typeof window !== 'undefined' ? window.innerWidth : 0) || 0; - return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax, viewportWidth }); + return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax, viewportWidth, win: typeof window !== 'undefined' ? window : null }); } // Pure page-level skipped-heading walk. Takes a Document so it works in both @@ -1862,7 +2165,7 @@ function checkElementQuality(el, style, tag, window) { const fontSize = resolveFontSizePx(el, window); const lineHeightPx = resolveLengthPx(style.lineHeight, fontSize); const letterSpacingPx = resolveLengthPx(style.letterSpacing, fontSize); - return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect: null }); + return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect: null, win: window }); } function checkElementBorders(tag, style, overrides, resolvedRadius) { @@ -2303,40 +2606,213 @@ function checkPageLayout(doc, win) { } } - // Everything centered - const textEls = doc.querySelectorAll('h1, h2, h3, h4, h5, h6, p, li, div, button'); - let centeredCount = 0; - let totalText = 0; - for (const el of textEls) { - const hasDirectText = [...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim().length >= 3); - if (!hasDirectText) continue; - totalText++; - - let cur = el; - let isCentered = false; - while (cur && cur.nodeType === 1) { - const rawStyle = cur.getAttribute?.('style') || ''; - const cls = cur.getAttribute?.('class') || ''; - if (/text-align\s*:\s*center/i.test(rawStyle) || /\btext-center\b/.test(cls)) { - isCentered = true; - break; - } - if (cur.tagName === 'BODY') break; - cur = cur.parentElement; - } - if (isCentered) centeredCount++; - } - - if (totalText >= 5 && centeredCount / totalText > 0.7) { - findings.push({ - id: 'everything-centered', - snippet: `${centeredCount}/${totalText} text elements centered (${Math.round(centeredCount / totalText * 100)}%)`, - }); - } - return findings; } +// ─── Cream / beige palette (the default "tasteful" AI surface) ──────────────── +// A warm, lightly-tinted off-white page background — light, with R≥G≥B and a +// small warm tint (not white, not a strong color). The current reflex surface. +function isCreamColor(rgb) { + if (!rgb) return false; + const { r, g, b } = rgb; + if (Math.min(r, g, b) < 209) return false; // must be light + if (!(r >= g && g >= b)) return false; // warm ordering + const warmth = r - b; + return warmth >= 6 && warmth <= 48; // tinted, not white, not strong +} + +// Tailwind background utilities that render as a warm off-white surface. The +// static engine doesn't fetch Tailwind's CSS, so a `bg-amber-50` on +// resolves to nothing in computed style — catch it from the class list +// instead. Candidate tokens map to their actual Tailwind hex and are still +// filtered through isCreamColor, so neutral grays (stone) and over-saturated +// shades drop out on their own. +const TAILWIND_BG_HEX = { + 'bg-amber-50': '#fffbeb', 'bg-amber-100': '#fef3c7', + 'bg-orange-50': '#fff7ed', 'bg-orange-100': '#ffedd5', + 'bg-yellow-50': '#fefce8', + 'bg-stone-50': '#fafaf9', 'bg-stone-100': '#f5f5f4', 'bg-stone-200': '#e7e5e4', +}; + +function creamFromClassList(cls) { + if (!cls) return null; + // Arbitrary value: bg-[#f5f0e6] / bg-[rgb(245_240_230)] (underscores = spaces). + const arb = cls.match(/\bbg-\[([^\]]+)\]/); + if (arb && isCreamColor(parseAnyColor(arb[1].replace(/_/g, ' ')))) return `bg-[${arb[1]}]`; + // Named warm-light utilities. + for (const [tok, hex] of Object.entries(TAILWIND_BG_HEX)) { + if (new RegExp(`(^|\\s)${tok}($|\\s)`).test(cls) && isCreamColor(parseAnyColor(hex))) return tok; + } + return null; +} + +function checkCreamPalette(doc, win) { + const findings = []; + const body = doc.body || (doc.querySelector ? doc.querySelector('body') : null); + if (!body) return findings; + const html = doc.documentElement; + const getCS = (el) => (win ? win.getComputedStyle(el) : getComputedStyle(el)); + + // 1. Computed background — covers inline / close.', 'Prefix every preview selector with the matching [data-impeccable-variant="N"] selector.', 'Keep selectors anchored to the generated variant wrapper; do not rely on component CSS scoping for preview rules.', ], forbidden: [ 'Do not use @scope for this styleMode.', + 'Do not wrap style content in a JSX/TSX template literal ({` ... `}); that syntax is for .tsx/.jsx only.', + 'Do not put { immediately after the style opening tag; Astro parses { as expression syntax.', ], }; } @@ -629,4 +632,15 @@ if (_running?.endsWith('live-wrap.mjs') || _running?.endsWith('live-wrap.mjs/')) } // Test exports (used by tests/live-wrap.test.mjs) -export { buildSearchQueries, findElement, findClosingLine, detectCommentSyntax }; +export { + buildSearchQueries, + findElement, + findClosingLine, + detectCommentSyntax, + findAllElements, + filterByText, + findFileWithQuery, + detectStyleMode, + buildCssAuthoring, + buildCssSelectorPrefixExamples, +}; diff --git a/.qoder/skills/impeccable/scripts/live.mjs b/.qoder/skills/impeccable/scripts/live.mjs index cafb0eca9..8acd300ed 100644 --- a/.qoder/skills/impeccable/scripts/live.mjs +++ b/.qoder/skills/impeccable/scripts/live.mjs @@ -21,7 +21,7 @@ import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { loadContext } from './load-context.mjs'; +import { loadContext } from './context.mjs'; import { resolveFiles } from './live-inject.mjs'; import { readLiveServerInfo } from './impeccable-paths.mjs'; @@ -80,7 +80,7 @@ The agent should then: process.exit(1); } - // 4. Load PRODUCT.md + DESIGN.md context (auto-migrates legacy .impeccable.md) + // 4. Load PRODUCT.md + DESIGN.md context. const ctx = loadContext(process.cwd()); // 5. Compute drift-heal: compare resolved inject targets against the @@ -102,7 +102,6 @@ The agent should then: hasDesign: ctx.hasDesign, design: ctx.design, designPath: ctx.designPath, - migrated: ctx.migrated, }, null, 2)); } diff --git a/.qoder/skills/impeccable/scripts/load-context.mjs b/.qoder/skills/impeccable/scripts/load-context.mjs deleted file mode 100644 index dc340bf16..000000000 --- a/.qoder/skills/impeccable/scripts/load-context.mjs +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Shared context loader for every impeccable command that needs to know - * "who is this for" and "what does this look like". - * - * Input: project root (process.cwd()). - * - * Output (JSON to stdout): - * { - * hasProduct: boolean, // PRODUCT.md found (or auto-migrated) - * product: string | null, // PRODUCT.md contents - * productPath: string | null, // relative path - * hasDesign: boolean, // DESIGN.md found - * design: string | null, // DESIGN.md contents - * designPath: string | null, - * migrated: boolean, // true if we auto-renamed .impeccable.md -> PRODUCT.md - * contextDir: string, // absolute path of the directory the files were found in - * } - * - * Filename matching is case-insensitive for PRODUCT.md and DESIGN.md. The - * Google DESIGN.md convention is uppercase at repo root; Kiro-style and - * lowercase variants are also matched so users don't get punished for case. - * - * Lookup directory resolution (first match wins): - * 1. process.env.IMPECCABLE_CONTEXT_DIR (absolute or relative to cwd) - * 2. cwd, if PRODUCT.md / DESIGN.md / .impeccable.md is there (back-compat) - * 3. Auto-fallback subdirectories of cwd: .agents/context/, then docs/ - * 4. cwd as a default "no context found" location - * - * Legacy `.impeccable.md` -> PRODUCT.md migration only fires at cwd root; - * fallback directories are read-only as far as auto-rename is concerned. - */ - -import fs from 'node:fs'; -import path from 'node:path'; - -const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; -const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; -const LEGACY_NAMES = ['.impeccable.md']; -const FALLBACK_DIRS = ['.agents/context', 'docs']; - -/** - * Resolve the directory that holds PRODUCT.md / DESIGN.md for - * this project. Exported so other scripts (e.g. live-server.mjs) can read the - * design files from the same location the loader uses. - */ -export function resolveContextDir(cwd = process.cwd()) { - // 1. Explicit override - const envDir = process.env.IMPECCABLE_CONTEXT_DIR; - if (envDir && envDir.trim()) { - const trimmed = envDir.trim(); - return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); - } - - // 2. cwd wins if any canonical or legacy file is there. We check legacy too - // so the auto-migration path in loadContext stays predictable. - if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES, ...LEGACY_NAMES])) { - return cwd; - } - - // 3. Auto-fallback subdirs. Match if PRODUCT.md or DESIGN.md is present; - // legacy `.impeccable.md` does not pull the lookup into a fallback dir. - for (const rel of FALLBACK_DIRS) { - const candidate = path.resolve(cwd, rel); - if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { - return candidate; - } - } - - // 4. Nothing found — keep the historical "default to cwd" behaviour so the - // caller's `hasProduct === false` branch still fires the same way. - return cwd; -} - -export function loadContext(cwd = process.cwd()) { - let migrated = false; - const contextDir = resolveContextDir(cwd); - - // 1. Look for PRODUCT.md (case-insensitive) in the resolved dir - let productPath = firstExisting(contextDir, PRODUCT_NAMES); - - // 2. Legacy: if no PRODUCT.md but .impeccable.md exists at cwd root, rename - // it in place. We only migrate at the root — fallback dirs are read-only - // so we don't surprise users by mutating files under docs/ or .agents/. - if (!productPath && contextDir === cwd) { - const legacyPath = firstExisting(cwd, LEGACY_NAMES); - if (legacyPath) { - const newPath = path.join(cwd, 'PRODUCT.md'); - try { - fs.renameSync(legacyPath, newPath); - productPath = newPath; - migrated = true; - } catch { - // Rename failed (permissions, etc.) — fall back to reading legacy in place - productPath = legacyPath; - } - } - } - - // 3. DESIGN.md (case-insensitive) - const designPath = firstExisting(contextDir, DESIGN_NAMES); - - const product = productPath ? safeRead(productPath) : null; - const design = designPath ? safeRead(designPath) : null; - - return { - hasProduct: !!product, - product, - productPath: productPath ? path.relative(cwd, productPath) : null, - hasDesign: !!design, - design, - designPath: designPath ? path.relative(cwd, designPath) : null, - migrated, - contextDir, - }; -} - -function firstExisting(dir, names) { - for (const name of names) { - const abs = path.join(dir, name); - if (fs.existsSync(abs)) return abs; - } - return null; -} - -function safeRead(p) { - try { return fs.readFileSync(p, 'utf-8'); } catch { return null; } -} - -// --------------------------------------------------------------------------- -// CLI mode — print the context as JSON -// --------------------------------------------------------------------------- - -function cli() { - const result = loadContext(process.cwd()); - console.log(JSON.stringify(result, null, 2)); -} - -const _running = process.argv[1]; -if (_running?.endsWith('load-context.mjs') || _running?.endsWith('load-context.mjs/')) { - cli(); -} diff --git a/.qoder/skills/impeccable/scripts/palette.mjs b/.qoder/skills/impeccable/scripts/palette.mjs new file mode 100644 index 000000000..4c3f1751a --- /dev/null +++ b/.qoder/skills/impeccable/scripts/palette.mjs @@ -0,0 +1,633 @@ +#!/usr/bin/env node +/** + * Brand-seed picker. Returns one OKLCH seed color + the mood it most + * naturally evokes, and teaches the model how to compose a full palette + * around it. + * + * The seed is the brand's anchor color. The 5-role palette (bg, surface, + * ink, accent, muted) is composed by the caller at runtime using their + * judgment + the brief (PRODUCT.md / DESIGN.md / user prompt), NOT picked + * from a frozen 4-color preset. + * + * Why: 4-color frozen palettes drift toward safe defaults (warm-cream bg, + * complementary accent on near-white) regardless of brief. A single seed + + * the model's own composition lets the same seed produce a dark-mode jazz + * club or a light-mode hospitality brand depending on what the brief calls + * for. Tested empirically against curated 4-color palettes; seed approach + * wins on mood-fit in 3 of 5 cases and ties on the rest. + * + * Usage: + * node scripts/palette.mjs # pick at random + * node scripts/palette.mjs --id seed-021 # pick a specific seed + * node scripts/palette.mjs --from # hash to a seed (deterministic) + * + * Env vars: + * IMPECCABLE_PALETTE_SEED — same as --from; useful for the eval harness + * to make runs reproducible. + */ + +import crypto from 'node:crypto'; + +// Seeds are inlined (129 entries, hand-curated via a tinder review of +// ~400 candidates from ColorHunt + synthesis + Radix/brand/Pantone anchors). +// Each carries a mood + strategy the judging model produced — surfaced as +// hints, not commands; the brief still drives composition. +const SEEDS = [ + { id: "seed-200", oklch: [0.360, 0.137, 0.0], + mood: "Aesop apothecary shelf — oxblood bottle glass against linen, considered and unhurried", + strategy: "Seed is a deep desaturated red-brown that reads as brand ink itself; I push primary darker toward bottle-glass oxblood, pair with a pure white surface so the red does the work, and use a clear pale-blush accent that can carry dark text in pills." }, + { id: "seed-000", oklch: [0.400, 0.130, 0.0], + mood: "oxblood leather banquette in a 1940s steakhouse — low lamplight on dark wood and burgundy", + strategy: "Near-black bg with the faintest red undertone lets the oxblood primary glow like lamplit leather; warm cream ink and a brass accent complete the chophouse register." }, + { id: "seed-002", oklch: [0.450, 0.150, 0.0], + mood: "darkroom red light — analog photography, blood-warm safelight glow on chemical trays", + strategy: "Near-black surface with a deep oxblood primary lets the seed function like a safelight in a darkroom — the bg disappears so the red becomes the only emotional signal." }, + { id: "seed-003", oklch: [0.500, 0.194, 0.0], + mood: "darkroom safelight — the deep oxblood glow of analog photography, chemical and contemplative", + strategy: "Anchored the seed as primary against pure near-black so the red reads like a single illuminated bulb in a developing room, with cool desaturated ink to evoke silver gelatin print tones." }, + { id: "seed-004", oklch: [0.546, 0.204, 3.4], + mood: "midnight boudoir — velvet rose under low lamplight, perfumed and intimate", + strategy: "Near-black surface lets the rose seed glow like silk in shadow; a warm champagne accent provides the candle-flame counterpoint without breaking the hush." }, + { id: "seed-005", oklch: [0.550, 0.180, 0.0], + mood: "smoldering vermillion at dusk — the last red ember in a blacksmith's forge, iron-rich and quietly violent", + strategy: "Near-black gallery surround lets the seed read as glowing forged metal; ink stays warm-off-white, accent shifts to a hotter ember orange so the primary feels like cooling steel against a fresh strike." }, + { id: "seed-201", oklch: [0.647, 0.262, 0.3], + mood: "Figma plugin marketplace red — confident product-brand crimson, the kind a modern dev tool uses for a 'live' indicator or a primary CTA on a pristine docs page", + strategy: "Pure white surface lets a high-chroma crimson primary do all the brand work, paired with a hue-shifted warm coral accent for hierarchy without competing saturation" }, + { id: "seed-006", oklch: [0.650, 0.160, 0.0], + mood: "1960s Italian cinema — Technicolor lipstick red against a darkened theater", + strategy: "Pure near-black surface lets a saturated cinematic red and its warm peach accent perform like film light projected in a dark room — the brand colors carry the drama, the bg disappears." }, + { id: "seed-008", oklch: [0.520, 0.200, 10.4], + mood: "Negroni hour at a Milanese bar — bittersweet crimson, vermouth and amaro under low tungsten", + strategy: "Seed is a saturated red-crimson with cinematic weight, so I sit it on near-black to let the primary glow like backlit liquor, with a warmer amber accent acting as the citrus twist against the bitter red." }, + { id: "seed-010", oklch: [0.563, 0.223, 11.0], + mood: "Negroni hour on a Milan rooftop — bittersweet crimson, aperitivo light, polished restraint", + strategy: "Seed is a vivid carmine-red with strong chroma, so the surface gets out of the way (pure white) and lets the primary do the aperitivo work, with a cooled garnet accent for tension." }, + { id: "seed-202", oklch: [0.643, 0.247, 7.0], + mood: "Glossier brand pink — modern beauty editorial, confident and current", + strategy: "Pure white bg lets a saturated rose-red primary do all the brand work, paired with a deeper crimson accent for hierarchy — the Stripe/Glossier move where the color carries the mood." }, + { id: "seed-013", oklch: [0.400, 0.130, 20.0], + mood: "Tuscan cellar at dusk — aged terracotta, oxidized iron, the deep red of decanted Sangiovese", + strategy: "Black surface lets the oxblood seed and copper accent glow like firelight on cellar stone; brand colors carry all the warmth while the room recedes." }, + { id: "seed-014", oklch: [0.450, 0.150, 20.0], + mood: "smoldering tannery — oxblood leather, cured under low workshop light", + strategy: "Anchor the deep oxblood seed as primary against a near-black architectural ground, then lift with a single warm ember accent so the leather reads burnished rather than bloody." }, + { id: "seed-016", oklch: [0.550, 0.180, 20.0], + mood: "Negroni hour on a Roman terrace — bitter campari red, vermouth, late golden light spilling on white linen", + strategy: "Pure white surface lets the campari-red primary do all the emotional work, paired with a deeper oxblood accent for bittersweet depth — Italian aperitivo restraint, not warmth-washed." }, + { id: "seed-205", oklch: [0.634, 0.254, 17.6], + mood: "Aesop apothecary bottle — considered red-coral on a clinical white surface, the kind of brand restraint where one saturated object does all the work", + strategy: "Default A pure white surface lets a single coral-red primary carry the entire brand voice; accent shifts to a deeper oxblood for hierarchy without competing chroma." }, + { id: "seed-011", oklch: [0.639, 0.207, 13.5], + mood: "Aperitivo hour in Milan — Campari glow on a white marble bar, crisp and effervescent", + strategy: "Pure white gallery backdrop lets the Campari-red primary ring like a single bitter note; ink is near-black with a whisper of warmth, accent shifts to a deeper oxblood for hierarchy without competing hues." }, + { id: "seed-015", oklch: [0.527, 0.202, 22.7], + mood: "Negroni hour on a Milanese terrace — bittersweet vermillion, aperitivo glassware catching low sun", + strategy: "Seed becomes a saturated aperitivo-red primary against pure white so the color carries the bittersweet warmth alone, paired with a deep oxblood accent for typographic gravitas." }, + { id: "seed-023", oklch: [0.427, 0.175, 29.2], + mood: "blacksmith's forge at dusk — iron heated to ember red, the deep glow of oxidized metal and quenching oil", + strategy: "Pure black bg lets the seed's ember-red glow radiate like hot iron in a dark forge; accent shifts to a copper-amber to suggest scaling metal and sparks, while ink stays near-white for tool-precise legibility." }, + { id: "seed-206", oklch: [0.614, 0.234, 28.2], + mood: "Aesop apothecary bottle — considered red-orange on lab-white, calm utility with a single confident pigment", + strategy: "Pure white surface lets a saturated vermilion primary do all the brand work, paired with a deep oxblood accent for hierarchy without introducing a second hue family" }, + { id: "seed-029", oklch: [0.665, 0.222, 25.7], + mood: "Negroni hour at a Milanese bar — bittersweet orange-red liqueur catching late afternoon light on polished marble", + strategy: "Pure white surface lets the seed's vermilion read like Campari in a glass; a deeper oxblood accent provides the bitter depth, with neutral graphite ink keeping the editorial restraint of Italian design." }, + { id: "seed-022", oklch: [0.418, 0.155, 27.2], + mood: "Pompeiian red fresco — oxidized cinnabar on a museum wall, archaeological gravity", + strategy: "Pure black gallery surface lets the seed's iron-oxide red read as a lit artifact; accent shifts to an aged terracotta amber, so primary and accent form a fired-clay duet against neutral void." }, + { id: "seed-024", oklch: [0.464, 0.169, 26.9], + mood: "Mid-century darkroom under the safelight — developer trays, oxblood leather, the quiet patience of a print emerging", + strategy: "Seed becomes a deep oxblood primary; surface stays pure black so the red glows like a safelight, with a warmer ember accent for hierarchy" }, + { id: "seed-026", oklch: [0.489, 0.190, 28.3], + mood: "smoldering ember in a blacksmith's forge — iron-hot rust, soot, and controlled fire", + strategy: "Near-black soot background lets the seed's red-orange glow like heated metal; ink is bone-white, accent is a cooler tempered-steel orange that creates internal heat gradient with the primary." }, + { id: "seed-027", oklch: [0.568, 0.208, 27.1], + mood: "Sicilian blood orange at golden hour — citrus rind, terracotta, sun on stucco", + strategy: "Seed reads as vivid blood-orange — picked pure white surface so the citrus-red primary and a deep oxblood accent do all the emotional work, like a Loro Piana editorial spread." }, + { id: "seed-028", oklch: [0.591, 0.172, 24.0], + mood: "Sienna-fired ceramic studio at dusk — terracotta cooling on a wheel, hands still dusted with slip", + strategy: "Pure black stage lets the fired-clay primary glow like a kiln ember, with a deeper oxblood accent providing tonal weight rather than hue contrast — a monochrome warm-axis play." }, + { id: "seed-033", oklch: [0.544, 0.169, 31.3], + mood: "1960s Italian terracotta workshop — fired clay, espresso, late-afternoon Mediterranean dust", + strategy: "Pure black ground lets the seed's burnt-sienna primary glow like a lit kiln, with a deeper oxblood accent for restrained warmth tension — the brand carries the heat, the surface stays out." }, + { id: "seed-207", oklch: [0.564, 0.231, 29.1], + mood: "Aesop apothecary bottle — considered red oxide, the calm authority of a well-made object on a white shelf", + strategy: "Seed becomes the singular brand voice against pure white, with a deeper oxblood accent for hierarchy — the surface disappears so the red does all the speaking." }, + { id: "seed-035", oklch: [0.663, 0.153, 32.1], + mood: "Aesop apothecary bottle — clay-fired warmth, considered retail", + strategy: "Pure white surface lets the terracotta primary do the brand work, paired with a deep umber ink and a cooler clay accent for editorial tension." }, + { id: "seed-037", oklch: [0.590, 0.188, 35.8], + mood: "Aesop apothecary bottle — considered terracotta, herbalist restraint, the warmth comes from the glass not the room", + strategy: "Seed becomes a muted terracotta primary against pure white so the brand's warmth carries entirely through the color itself; accent shifts to a deeper umber for quiet hierarchy." }, + { id: "seed-038", oklch: [0.652, 0.229, 34.8], + mood: "blown-glass furnace at dusk — molten orange iron pulled from the kiln, a craftsman's signature heat", + strategy: "Pure black stage so the seed reads as live ember; primary holds the seed's heat, accent shifts to a brass-amber a hue-step away for a 1.7+ contrast pairing without leaving the fire." }, + { id: "seed-039", oklch: [0.653, 0.185, 33.5], + mood: "Aesop apothecary bottle — considered terracotta, quiet retail craft", + strategy: "Seed becomes a grounded clay primary against pure white, paired with a deeper umber accent so the warmth lives entirely in the brand marks, not the surface." }, + { id: "seed-167", oklch: [0.495, 0.134, 36.0], + mood: "Aesop apothecary shelf — burnished terracotta on clinical white, considered craft pharmacy", + strategy: "Treat the seed as a brand-carrying burnt-sienna against a pure paper-white surface so the warmth lives entirely in the primary, with a deep umber accent pulled along the same warm axis for typographic gravity." }, + { id: "seed-147", oklch: [0.500, 0.151, 40.0], + mood: "Aesop apothecary shelf — considered terracotta, pharmacy restraint, the brand color does the work against clinical white", + strategy: "Anchor the seed's burnt-sienna primary against a pure white surface so the rust speaks alone, with a deep umber ink and a cooler clay accent to give the palette product-brand discipline rather than environmental warmth." }, + { id: "seed-040", oklch: [0.660, 0.201, 40.0], + mood: "Aesop apothecary bottle — amber glass on a clean dispensary shelf, considered and clinical-warm", + strategy: "Seed becomes a burnt-amber primary against pure white so the bottle-glass color does the emotional work; accent shifts to a deep olive-bronze for the apothecary-label pairing." }, + { id: "seed-041", oklch: [0.673, 0.217, 38.6], + mood: "Aesop apothecary shelf — considered orange glass, clinical retail restraint", + strategy: "Pure white surface lets the burnt-orange primary do all the brand work, with a deep ink-brown for editorial gravity and a muted clay accent that reads as a sibling, not a contrast." }, + { id: "seed-042", oklch: [0.688, 0.133, 35.8], + mood: "Aesop apothecary shelf — terracotta glass, considered retail", + strategy: "Seed becomes a warm clay primary against pure white so the bottle-on-marble retail feel comes from the brand color alone; a deeper umber accent gives the label-print contrast." }, + { id: "seed-043", oklch: [0.781, 0.119, 38.1], + mood: "Aesop apothecary catalogue — considered terracotta, dermatological restraint, the warm color doing all the work against clinical white", + strategy: "Pure white surface lets the seed's warm clay tone read as the entire brand voice, paired with a deeper umber accent for hierarchy without competing with the primary's warmth." }, + { id: "seed-168", oklch: [0.400, 0.103, 50.0], + mood: "Aesop apothecary bottle — amber glass on a clinical white shelf, considered and pharmaceutical", + strategy: "Pure white surface lets the deep amber primary act like tinted glass against a clean shelf; accent is a muted clay that complements without competing, keeping the brand quiet and product-led." }, + { id: "seed-044", oklch: [0.568, 0.149, 45.9], + mood: "1970s desert highway at golden hour — sun-faded terracotta, denim dust, the warmth of a Polaroid pulled from a glovebox", + strategy: "Seed becomes a burnt-sienna primary against pure white so the terracotta does all the emotional work; a deep indigo accent acts as the denim shadow opposing the sun, creating the era's signature warm/cool tension without tinting the page." }, + { id: "seed-045", oklch: [0.607, 0.163, 47.7], + mood: "Aesop apothecary shelf — considered amber glass, clinical restraint, craft pharmacy", + strategy: "Pure white bg lets the burnt-amber primary do the apothecary work alone, paired with a deeper umber accent and graphite ink for editorial calm." }, + { id: "seed-046", oklch: [0.653, 0.175, 45.0], + mood: "Aesop apothecary shelf — considered amber glass, quiet luxury, restrained craft", + strategy: "Pure black backdrop lets the warm amber primary glow like backlit apothecary glass, with a deeper rust accent providing tonal depth in the same hue family — monochromatic warm against neutral void." }, + { id: "seed-047", oklch: [0.695, 0.205, 43.2], + mood: "Aesop apothecary label — sun-warmed amber glass on a clinical countertop, restrained botanical pharmacy", + strategy: "Pure white surface lets the burnt-amber primary and a deeper sienna accent do all the brand work, like an apothecary bottle photographed under daylight." }, + { id: "seed-051", oklch: [0.704, 0.189, 49.0], + mood: "blacksmith's forge at dusk — glowing iron, hammered copper, ember light against cooling steel", + strategy: "Pure near-black surface lets the seed's molten orange burn like heated metal; accent shifts to a deeper amber-red to suggest the cooling end of the same iron, while ink stays a clean off-white so type reads like chalk on slate." }, + { id: "seed-171", oklch: [0.550, 0.124, 60.0], + mood: "Klim Type Foundry specimen page — considered ochre on paper, design-school-honest", + strategy: "Seed becomes a muted ochre primary on pure white; accent is a deep ink-navy pulled across the wheel for editorial contrast without warmth-pooling in the bg" }, + { id: "seed-148", oklch: [0.650, 0.146, 60.0], + mood: "Klim-style editorial gold — late-afternoon paper light on a serif specimen sheet, considered and dry", + strategy: "Hold the seed's amber as primary on a pure white page so the gold reads as ink rather than atmosphere, and pair with a deep aubergine accent for typographic contrast." }, + { id: "seed-052", oklch: [0.700, 0.130, 60.0], + mood: "late-afternoon terracotta studio — sun-warmed clay, hands-on craft, the hour before dusk", + strategy: "Seed is a saturated amber-ochre with strong environmental association (ceramics, adobe, sunlit plaster), so I lean into Exception (a) with a faintly warm bone surface that reads as lime-washed wall, then deepen the seed slightly for primary and pair it with a fired-clay rust accent for hand-thrown warmth." }, + { id: "seed-053", oklch: [0.773, 0.157, 56.6], + mood: "late-summer apricot orchard at golden hour — sun-warmed fruit, considered Californian craft", + strategy: "Seed is a juicy mid-warm orange at daylight luminance — leaning optimistic/editorial, so pure white surface lets the apricot primary glow without muddying it; a deep wine accent provides the bite." }, + { id: "seed-149", oklch: [0.600, 0.124, 70.0], + mood: "1970s desert highway — late-afternoon amber light on chrome and asphalt", + strategy: "Anchor the amber seed as primary against pure black so the warm hue reads as headlight glow against night; a cooler dusk-mauve accent provides the complementary tension of horizon vs. sun." }, + { id: "seed-054", oklch: [0.740, 0.162, 68.1], + mood: "late-afternoon honey on terracotta — Mediterranean stucco at golden hour, sun-baked amber", + strategy: "Seed is a saturated honey-amber at high lightness; pairing it with pure black lets the warmth read as luminous gold against gravity, like lamplight in a dark room." }, + { id: "seed-055", oklch: [0.774, 0.174, 65.1], + mood: "late-summer honey hour — amber light slanting through a west-facing window, optimistic and golden", + strategy: "Anchor a saturated honey-amber primary on pure white so the warmth radiates from the brand itself, then pair with a deep teak accent for grounded contrast rather than tinting the canvas." }, + { id: "seed-056", oklch: [0.691, 0.146, 74.6], + mood: "Klim-style modern publishing house — late-afternoon paper warmth, considered editorial gold", + strategy: "Pure white surface so the amber seed becomes the brand voice; ink stays near-black neutral and accent shifts to a deep ink-blue to give the gold something structural to lean on." }, + { id: "seed-150", oklch: [0.750, 0.148, 80.0], + mood: "Klim Type Foundry specimen page — late-summer editorial gold, considered and grown-up", + strategy: "Pure white surface lets a single restrained ochre primary do all the brand work, paired with a deep ink-blue accent for typographic contrast in the Klim/Commercial Type tradition." }, + { id: "seed-058", oklch: [0.764, 0.120, 77.1], + mood: "Klim Type Foundry specimen page — late-afternoon ochre, considered editorial typography", + strategy: "Pure white surface lets the ochre primary do the brand work, paired with a deep ink-blue accent for editorial contrast — the type-foundry move where one warm hue carries the whole feeling against neutral paper." }, + { id: "seed-059", oklch: [0.784, 0.144, 79.8], + mood: "late afternoon in a Tuscan limonaia — sun-cured amber on whitewashed plaster", + strategy: "Pure white surface lets the saffron-amber primary and a deep olive accent carry the Mediterranean warmth, with split-complementary tension between gold and a quiet evergreen." }, + { id: "seed-061", oklch: [0.817, 0.161, 75.1], + mood: "late-afternoon honey on Tuscan limestone — golden hour, slow and luminous", + strategy: "Pure white surface lets the amber primary glow like sunlight on a wall, paired with a deep terracotta accent for warm tonal contrast within the same hue family." }, + { id: "seed-063", oklch: [0.842, 0.165, 91.3], + mood: "late-afternoon Tuscan sun on limestone — golden hour, considered, optimistic", + strategy: "Pure white surface lets the amber-gold primary radiate as the mood-carrier, with a deep aubergine accent providing the long shadow that golden light needs to feel three-dimensional." }, + { id: "seed-174", oklch: [0.350, 0.075, 110.0], + mood: "olive grove at late afternoon — sun-cured leaves, dust, and quiet Mediterranean weight", + strategy: "Pure white surface lets a deep, sun-cured olive primary do the emotional work, with a burnt-terracotta accent providing the warm-earth counterpoint olive groves are known for." }, + { id: "seed-117", oklch: [0.650, 0.100, 110.0], + mood: "Klim-style editorial sage — late-summer foundry catalogue, considered olive-yellow on paper", + strategy: "Seed sits at olive-chartreuse; treating it as a quiet typographic primary on pure paper, with a deeper bronze-olive accent for hierarchy — the color does the work, the page disappears." }, + { id: "seed-118", oklch: [0.750, 0.090, 110.0], + mood: "Klim Type Foundry specimen page — late-summer olive light on a working specimen, the honesty of a type designer showing their work", + strategy: "Pure white bg lets a desaturated olive-yellow primary do the editorial work, with a deeper olive-bronze accent providing typographic emphasis the way a specimen uses one heavy weight against the body roman." }, + { id: "seed-065", oklch: [0.797, 0.166, 113.1], + mood: "late-summer olive grove at noon — sun-bleached leaves, dry stone, Mediterranean glare", + strategy: "Hold the seed as a luminous chartreuse-olive primary against pure white so the color reads as sunlit foliage, pairing it with a deep umber accent for the dry-stone contrast." }, + { id: "seed-176", oklch: [0.300, 0.071, 120.0], + mood: "moss-darkened apothecary jar — herbal, shadowed, mid-19th-century botanical study", + strategy: "Seed is a deep desaturated olive-green that reads as preserved botanical pigment; I anchor it on pure white so the dim moss-green primary feels like ink on a herbarium page, with a warm ochre accent supplying the aged-paper counterpoint." }, + { id: "seed-155", oklch: [0.550, 0.142, 130.0], + mood: "moss-bed forest floor at noon — chlorophyll, lichen, sunlit fern", + strategy: "Seed is a confident mid-olive green with strong chroma; mood is daylight botanical, so I let the brand greens do the work on a pure paper-white bg and pair with a warm umber accent for fern-against-bark contrast." }, + { id: "seed-119", oklch: [0.600, 0.154, 130.0], + mood: "moss garden at Saihō-ji — damp stone, filtered green light through old cedar", + strategy: "Pure near-black bg lets the seed's mossy green glow like wet lichen under low light; accent shifts to a pale ochre-gold like sun catching through canopy." }, + { id: "seed-179", oklch: [0.300, 0.096, 140.0], + mood: "moss on wet stone — forest floor at dusk, deep botanical hush", + strategy: "Kept the seed's deep moss green as primary against a near-black surface so the green reads as living shadow, with a pale lichen accent providing the single point of light." }, + { id: "seed-180", oklch: [0.350, 0.110, 140.0], + mood: "moss-darkened apothecary — herbal tinctures in amber glass, pressed botanicals, the deep green of a conservatory at dusk", + strategy: "Near-black bg with a whisper of green undertone lets the seed's deep moss read as luminous foliage; a warm parchment accent provides the apothecary-label counterpoint without breaking the herbal register." }, + { id: "seed-120", oklch: [0.650, 0.100, 140.0], + mood: "moss on weathered stone — quiet botanical garden conservatory at midday", + strategy: "Pure white bg lets the muted sage-green primary read as a considered botanical mark, with a deeper terracotta accent providing earthen counterpoint without breaking the gallery-like restraint." }, + { id: "seed-121", oklch: [0.750, 0.090, 140.0], + mood: "moss garden at Saihō-ji — diffuse green light filtered through wet stone and lichen", + strategy: "Pure near-black bg lets the muted sage-green primary glow like lichen under low light; a warm pale-bone accent acts as the single ray of sun cutting through canopy." }, + { id: "seed-182", oklch: [0.400, 0.106, 150.0], + mood: "moss garden at Saiho-ji — deep cultivated green under wet stone shadow, contemplative and damp", + strategy: "Near-black bg with the faintest cool-green undertone evokes shaded stone; primary holds the seed's moss tone while accent shifts to a lichen-yellow for organic counterpoint without breaking the hush." }, + { id: "seed-157", oklch: [0.550, 0.145, 150.0], + mood: "moss garden at Saiho-ji — damp stone, filtered green light through cedar canopy", + strategy: "Near-black bg with a faint green undertone evokes deep forest shadow; primary holds the seed's verdant register while accent shifts to a pale lichen-cream to mimic light catching moss." }, + { id: "seed-122", oklch: [0.600, 0.158, 150.0], + mood: "forest floor at first light — moss, lichen, and clean morning air", + strategy: "Seed reads as a living, daylight green; surface stays pure white so the green carries the freshness, with a cool teal accent pulling it toward dew rather than earth." }, + { id: "seed-195", oklch: [0.650, 0.150, 145.0], + mood: "Considered horticulture brand — botanical research lab, the green of a healthy stem photographed in clean daylight", + strategy: "Pure white surface lets the seed's vegetal green carry the entire brand voice, paired with a deep forest ink and a warm clay accent for editorial contrast." }, + { id: "seed-183", oklch: [0.350, 0.077, 160.0], + mood: "moss-stained apothecary — deep forest glass, herbal tinctures shelved in low candlelight", + strategy: "Anchored the seed as primary and built a near-black dark surface with whisper-tinted green to evoke aged apothecary glass, letting the green glow rather than shout." }, + { id: "seed-184", oklch: [0.400, 0.087, 160.0], + mood: "deep forest apothecary — moss, bottle glass, and herbal tincture under afternoon light", + strategy: "Seed becomes a botanical-bottle-green primary on pure white, paired with a warm clove-amber accent to evoke herbal pharmacy contrast without tinting the surface." }, + { id: "seed-158", oklch: [0.550, 0.119, 160.0], + mood: "moss on wet stone — forest floor after rain, mineral and quiet", + strategy: "Pure white surface lets the deep mossy green carry the entire mood; accent shifts to a damp slate-teal to sit beside primary like lichen on stone without competing." }, + { id: "seed-159", oklch: [0.600, 0.130, 160.0], + mood: "moss-covered forest apothecary — herbal tinctures in amber glass, eucalyptus shadow", + strategy: "Anchored the green seed in a near-black backdrop so it reads like botanical glassware lit from within, with a warm amber accent pulled across the wheel to evoke tincture bottles against dark wood." }, + { id: "seed-185", oklch: [0.450, 0.086, 170.0], + mood: "weathered copper patina on a Pacific Northwest greenhouse — oxidized teal, glass light, botanical hush", + strategy: "Seed sits as a deep oxidized-teal primary against pure white so the patina reads as pigment, not atmosphere; a rust-copper accent completes the verdigris/oxidation story across the warm-cool axis." }, + { id: "seed-124", oklch: [0.750, 0.080, 170.0], + mood: "sea-glass on a foggy Pacific shoreline — weathered, mineral, quietly oxidized", + strategy: "Seed is a soft desaturated teal-green; pairing it on pure white lets the mineral primary read as patinated copper-glass, with a deeper kelp-toned primary and a rusted coral accent to spark the muted teal against its complement." }, + { id: "seed-160", oklch: [0.550, 0.095, 180.0], + mood: "weathered copper patina on a museum bronze — oxidized teal, conservatorial quiet", + strategy: "Pure near-black gallery surround lets the patina-teal primary glow like a lit artifact, with a warm verdigris-adjacent accent providing the oxidation contrast against the cool seed." }, + { id: "seed-161", oklch: [0.720, 0.100, 188.0], + mood: "climate-tech dashboard — calm verdigris on plain paper, the quiet confidence of an instrument that just works", + strategy: "Seed teal carries the entire mood as a single considered brand color on pure white, with a desaturated copper accent providing warm signal against the cool primary without competing for attention." }, + { id: "seed-186", oklch: [0.450, 0.074, 200.0], + mood: "deep hydrothermal vent — mineral teal under pressure, the cold blue-green of oxidized copper in submerged light", + strategy: "Near-black surface lets the mineral teal glow as if lit from within; accent shifts toward verdigris-copper to suggest patina on submerged metal, while ink stays cool-neutral to keep the register austere rather than aquatic-cute." }, + { id: "seed-125", oklch: [0.650, 0.100, 200.0], + mood: "climate-tech dashboard — calm operational teal, the color of clean water data and atmospheric sensors", + strategy: "Pure white surface lets a single muted-teal primary do all the brand work, with a deeper marine accent providing hierarchy without competing chroma." }, + { id: "seed-126", oklch: [0.750, 0.080, 200.0], + mood: "climate-tech product brand — quiet competence, dashboards for hard infrastructure problems", + strategy: "Hold the seed's muted teal as primary, pair with a sharper cyan-leaning accent for interactive lift, and let a pure white surface do the disappearing act so the brand reads as a tool, not an atmosphere." }, + { id: "seed-162", oklch: [0.550, 0.091, 210.0], + mood: "weathered nautical instrument — patinated brass on oxidized steel, the cool blue-grey of a ship's chronometer at dawn", + strategy: "Pure white surface lets the muted teal-steel primary read as a precise instrument mark, with a warm brass accent providing the single point of patina against clinical white." }, + { id: "seed-163", oklch: [0.450, 0.086, 230.0], + mood: "deep harbor at dusk — weathered nautical instruments, brass dials on oxidized steel", + strategy: "Near-black background with subtle cool tint evokes the marine dusk; primary holds the seed's teal-blue while a warm brass accent creates the instrument-on-steel tension." }, + { id: "seed-164", oklch: [0.550, 0.105, 230.0], + mood: "deep harbor at dawn — cold steel water, fog-muted light, the quiet before the boats leave", + strategy: "Pure near-black bg lets the seed's cold marine blue read as a luminous beacon, while a pale frost-cyan accent evokes diffused dawn light cutting through fog." }, + { id: "seed-127", oklch: [0.650, 0.100, 230.0], + mood: "climate-tech dashboard — atmospheric sensor blue, calm operational clarity", + strategy: "Anchor the seed as a confident mid-blue primary on pure white so the brand color carries all the atmospheric feeling, with a deep navy accent for hierarchy and a soft slate muted for body text." }, + { id: "seed-128", oklch: [0.750, 0.080, 230.0], + mood: "climate-tech dashboard — calm atmospheric data, considered sky-blue", + strategy: "Pure white surface lets the muted sky-blue primary carry the meteorological calm, with a deep-navy accent providing readable weight against the soft primary." }, + { id: "seed-187", oklch: [0.350, 0.078, 240.0], + mood: "deep harbor at blue hour — wet stone, cold steel, the quiet before night fully lands", + strategy: "Near-black architectural bg with a hint of marine chroma lets the seed read as ambient atmosphere rather than UI chrome; a cooler steel accent sits opposite the warmer-shifted primary for navigational clarity." }, + { id: "seed-077", oklch: [0.578, 0.130, 241.7], + mood: "pre-dawn signal tower — cold blue solitude, instruments glowing against the dark", + strategy: "Pure near-black bg lets the seed's cold tower-light blue glow as the sole emotional source, with a frost-cyan accent acting as a secondary indicator light." }, + { id: "seed-188", oklch: [0.400, 0.110, 250.0], + mood: "Linear's considered indigo — the calm authority of a well-built developer tool, blueprint ink on a clean page", + strategy: "Held the seed as a deep indigo primary against pure white so the brand color carries all the gravity; accent shifts to a cooler, brighter cyan-blue to create a crisp hierarchy pair without warming the surface." }, + { id: "seed-165", oklch: [0.450, 0.123, 250.0], + mood: "blueprint room at dusk — drafting table, graphite, civic-engineering blue", + strategy: "Seed is a mid-deep architectural blue with real chroma and no environmental cue, so I stay out of the way with a pure white surface and let the primary do all the talking, pairing it with a burnt-ochre accent for drafting-pencil contrast." }, + { id: "seed-079", oklch: [0.478, 0.136, 251.8], + mood: "twilight cartography — the blue of deep dusk over open water, precise and navigational", + strategy: "Pure white surface lets the seed's oceanic blue act as a single navigational anchor, with a warm amber accent struck across it like a lighthouse beam at dusk." }, + { id: "seed-080", oklch: [0.541, 0.122, 248.2], + mood: "Linear-style considered tool blue — the calm, exact register of a modern engineering app where every pixel is intentional", + strategy: "Pure white surface lets the considered indigo-blue primary carry the entire brand; a deeper navy accent provides hierarchy without warmth, keeping the palette in a single cool family for that focused-software feel" }, + { id: "seed-166", oklch: [0.550, 0.149, 250.0], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and quietly intense", + strategy: "Near-black bg with the faintest cool tint reads like a darkened cockpit; the seed becomes a luminous instrument-blue primary, paired with a warm amber accent that mimics avionics readouts for unmistakable signal contrast." }, + { id: "seed-081", oklch: [0.650, 0.160, 250.0], + mood: "deep-sea research vessel at dawn — instrument glow against cold steel light", + strategy: "Pure near-white bg keeps the palette technical and instrument-like; the seed blue holds as primary while a desaturated steel-cyan accent reads like signal readouts on glass." }, + { id: "seed-082", oklch: [0.742, 0.140, 247.4], + mood: "high-altitude flight deck at dawn — cold cabin instruments glowing against a sky still holding night", + strategy: "Near-black cockpit ground with a faint blue cast lets the seed read as an illuminated instrument; primary holds the seed, accent shifts to cyan for signal/indicator contrast." }, + { id: "seed-210", oklch: [0.360, 0.140, 260.0], + mood: "Linear-style considered tool indigo — late-night focused work, the deep blue of a code editor at 2am where everything else falls away", + strategy: "Pure black bg lets the indigo primary carry all the cognitive-focus weight, with a slightly brighter periwinkle accent for interactive lift — the surface disappears so the tool feels weightless." }, + { id: "seed-189", oklch: [0.400, 0.130, 260.0], + mood: "pre-dawn observatory — cold instrument blue, star-chart precision", + strategy: "Seed becomes the primary on pure black so the deep instrument-blue glows like a calibration light, with a faint cyan accent reading as starlight against the void." }, + { id: "seed-211", oklch: [0.420, 0.161, 260.0], + mood: "Linear's considered indigo — the tool-for-thought blue of focused product work, calm authority without coldness", + strategy: "Hold the seed as a deep indigo primary against pure white, then pair with a slightly warmer, lighter periwinkle accent to create gentle hue separation without breaking the disciplined tool-brand register." }, + { id: "seed-129", oklch: [0.450, 0.150, 260.0], + mood: "pre-dawn observatory — deep cobalt sky just before astronomical twilight, instruments cool to the touch", + strategy: "Near-black surface lets the cobalt seed read as luminous starlight; a single warm amber accent acts as the calibration lamp against the cold blue field." }, + { id: "seed-084", oklch: [0.476, 0.207, 261.2], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and awake", + strategy: "Default B black bg lets the cobalt primary read as a luminous instrument signal, with a cyan accent striking the analogous 'cockpit display' relationship." }, + { id: "seed-085", oklch: [0.681, 0.132, 258.4], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky", + strategy: "Anchored the seed as a luminous primary against a near-black architectural ground, with a warm amber accent acting as the single instrument light cutting through cold blue." }, + { id: "seed-086", oklch: [0.767, 0.106, 255.9], + mood: "Scandinavian winter morning — quiet light through frost, pale sky over snow", + strategy: "Anchored a pure white editorial stage so the seed's cool sky-blue reads as crisp polar light, with a deeper navy primary providing the only saturated weight — like a single dark pine against snow." }, + { id: "seed-083", oklch: [0.340, 0.159, 262.4], + mood: "deep cobalt twilight — the moment after sunset when the sky goes electric blue and city windows start to glow", + strategy: "Pure black stage lets the cobalt seed act as a luminous neon-window glow, with a warm amber accent across the wheel for the lit-window contrast." }, + { id: "seed-212", oklch: [0.360, 0.219, 270.0], + mood: "Linear-grade tooling indigo — considered software for people who care about craft", + strategy: "Anchored the deep indigo seed as primary on a pure white surface so the brand color carries all the weight, with a slightly cooler violet-blue accent for hierarchy without competing chroma." }, + { id: "seed-130", oklch: [0.400, 0.150, 270.0], + mood: "Linear-grade indigo — considered productivity tool, ink on paper, no theatrics", + strategy: "Pure white surface lets a deep cool indigo carry all the brand weight, paired with a slightly warmer violet-blue accent for hierarchy without acid." }, + { id: "seed-213", oklch: [0.411, 0.241, 267.9], + mood: "Linear-style indigo — considered tool surface, the kind of blue-violet that sits behind a developer's keyboard at 11pm without shouting", + strategy: "Pure black canvas lets a saturated indigo primary do all the brand work, with a cooler cyan-violet accent providing UI signal without competing." }, + { id: "seed-131", oklch: [0.450, 0.180, 270.0], + mood: "monastic indigo dusk — vespers light through stained glass, contemplative and severe", + strategy: "Seed becomes a deep indigo primary against pure near-black so the violet reads as luminous stained-glass against architectural shadow, with a cooler iris accent for tonal lift." }, + { id: "seed-088", oklch: [0.476, 0.158, 268.5], + mood: "pre-dawn astronomer's notebook — deep indigo sky just before the stars fade, ink and graphite", + strategy: "Near-black bg with the faintest cool tint to evoke night sky without theatrics; primary holds the seed's indigo, accent shifts to a paler periwinkle for stellar contrast, keeping the palette monochromatic-cool and observational." }, + { id: "seed-196", oklch: [0.530, 0.130, 268.0], + mood: "Linear-style considered tool indigo — the deep-focus blue-violet of a thoughtfully built productivity surface, the color of a well-typeset keyboard shortcut", + strategy: "Pure white bg lets the indigo seed do all the brand work as primary, with a slightly darker, more saturated violet-shifted accent for hierarchy and interactive states — the surface disappears so the brand color reads as the entire identity." }, + { id: "seed-132", oklch: [0.700, 0.120, 270.0], + mood: "Linear-style considered tool indigo — the quiet violet of a focused product workspace, late-afternoon thinking", + strategy: "Pure white surface lets a muted indigo-violet primary and a slightly cooler accent do all the brand work, keeping the register calm and software-like rather than theatrical." }, + { id: "seed-090", oklch: [0.445, 0.206, 279.1], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, not a nightclub", + strategy: "Anchor the seed as a confident product primary on pure white, with a cooler indigo-shift accent that reads as a sibling tool color, so the brand violet does all the emotional work." }, + { id: "seed-133", oklch: [0.500, 0.160, 280.0], + mood: "Linear-adjacent indigo — considered productivity tool, the violet of a thinking workspace", + strategy: "Seed becomes a measured indigo primary on pure white; accent shifts to a cooler blue-violet to create hierarchy without nightclub saturation, letting the brand color do all the emotional work." }, + { id: "seed-094", oklch: [0.533, 0.125, 294.3], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, calm authority for a creative workspace", + strategy: "Pure white canvas lets the indigo-violet primary carry the entire brand voice; accent shifts hue slightly toward blue for a cool, tool-like duotone rather than warm decorative pairing." }, + { id: "seed-137", oklch: [0.700, 0.120, 290.0], + mood: "Linear-adjacent indigo — the considered tool, late-evening focus mode, software made for people who care about craft", + strategy: "Pure black surface lets a single restrained indigo-violet carry the brand, with a cooler periwinkle accent providing UI hierarchy without competing — Vercel/Linear dark-mode discipline." }, + { id: "seed-100", oklch: [0.450, 0.150, 330.0], + mood: "velvet boudoir at last call — bruised orchid and lipstick traces under low lamplight", + strategy: "Pure near-black surface lets a deep magenta-rose primary smolder while a warm peach accent acts like skin-lit lamplight — drama lives in the brand pair, not the room." }, + { id: "seed-103", oklch: [0.650, 0.160, 330.0], + mood: "1980s Memphis boudoir — powder-pink neon humming against lacquered black, lipstick and lacquer", + strategy: "Near-black gallery surface lets the magenta-pink seed read as lit neon; accent shifts to warm coral to create cinematic dichromatic tension without competing chroma." }, + { id: "seed-228", oklch: [0.360, 0.147, 340.0], + mood: "Figma-era creative tool plum — considered productivity software for designers, the inky violet of a serif wordmark on a marketing site", + strategy: "Held the seed as a deep plum primary against pure white so the brand color does the emotional work; paired with a muted rose accent for warmth without breaking the productivity-tool restraint." }, + { id: "seed-107", oklch: [0.500, 0.200, 340.0], + mood: "Figma plum — creative-tool confidence, considered magenta for a modern design product", + strategy: "Pure white surface lets a saturated magenta-plum primary carry all the brand voice, paired with a cooler violet-leaning accent for hierarchy without competing." }, + { id: "seed-198", oklch: [0.600, 0.210, 340.0], + mood: "Figma-era creative tool plum — confident, considered, made for makers", + strategy: "Anchor a saturated plum primary against pure white so the brand color does all the emotional work, with a deeper magenta-rose accent for hierarchy." }, + { id: "seed-112", oklch: [0.754, 0.193, 343.4], + mood: "Figma-era creative tool — confident pink primary doing the brand work on a clean canvas, the way Linear uses indigo or Stripe uses violet", + strategy: "Anchor the seed pink as a saturated brand primary on pure white so the color carries all the personality; pair with a cooler plum accent to give the pink something to push against without competing." }, + { id: "seed-229", oklch: [0.420, 0.163, 350.0], + mood: "considered fintech rose — the deep magenta of a modern product brand (think Stripe-adjacent, but rotated toward berry), confident and current", + strategy: "pure white surface lets a single deep berry-rose primary do all the brand work, paired with a cooler indigo accent for the contrast move you see in modern product marketing" }, + { id: "seed-113", oklch: [0.470, 0.173, 354.8], + mood: "1960s velvet rope nightclub — crushed magenta, low light, cigarette smoke catching a spotlight", + strategy: "Pure black stage so the seed's smoky magenta reads as a single hot spotlight, paired with a cooler violet accent for the second light cue." }, + { id: "seed-114", oklch: [0.570, 0.158, 353.3], + mood: "fin-de-siècle Parisian rose — velvet curtain, theatre program, lipstick blotted on linen", + strategy: "Drop bg to true black so the dusty-rose primary reads as stage-lit silk; accent shifts to a warmer coral-mauve at higher lightness to create gentle hue rotation without breaking the romance." }, + { id: "seed-199", oklch: [0.650, 0.180, 350.0], + mood: "modern fintech rose — the considered pink of a Series B brand mark, confident and current without nostalgia", + strategy: "Pure white surface lets a saturated rose primary do the brand work, paired with a deep plum accent for hierarchy — the Stripe move applied to a pink hue." }, + { id: "seed-115", oklch: [0.636, 0.218, 355.3], + mood: "backstage at a cabaret — velvet rope, lipstick mark on a champagne glass", + strategy: "Seed reads as a saturated stage-light magenta-red; I push it into pure black so the primary glows like a neon sign and the accent (a cold pearl-pink) acts as the spotlight rim — the room is dark, the color does the singing." }, + { id: "seed-230", oklch: [0.650, 0.249, 354.5], + mood: "Modern fintech rose — the considered pink of a contemporary payments brand: confident, alive, and clear-headed", + strategy: "Pure white bg lets a saturated rose-magenta primary carry all the brand energy, paired with a cooler indigo accent for trustworthy contrast — the Stripe move applied to a pink hue." }, + { id: "seed-231", oklch: [0.682, 0.241, 353.2], + mood: "Figma-era creative tool — a confident pink-magenta product brand, the kind a modern design platform uses to feel alive without shouting", + strategy: "Default A pure white bg lets the saturated pink-magenta primary do all the brand work, with a near-complementary cool teal accent for tool-like clarity and a neutral ink for editorial calm" }, + { id: "seed-116", oklch: [0.734, 0.183, 356.8], + mood: "modern beauty brand DTC — Glossier-adjacent pink, confident and current without being saccharine", + 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." }, +]; + +function parseArgs(argv) { + const args = { id: null, from: null }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === '--id' && argv[i + 1]) { args.id = argv[++i]; } + else if (a === '--from' && argv[i + 1]) { args.from = argv[++i]; } + } + return args; +} + +// Hash a key into a stable float in [0, 1) for deterministic weighted picks. +function hashUnit(key) { + const h = crypto.createHash('sha256').update(key).digest(); + return h.readUInt32BE(0) / 0x100000000; +} + +// The curated library is hue-skewed (more reds/oranges than teals/magentas) +// because that's where the source material + taste landed. Left uniform, a +// random pick would land on red ~1/3 of the time. Inverse-frequency weighting +// gives each seed a weight of 1/(count in its 30° hue bucket), so each hue +// ZONE is roughly equally likely to be chosen regardless of how many seeds it +// holds — fair rainbow exposure across runs without pruning the library. +function buildWeights(seeds) { + const bucketCount = {}; + const bucketOf = (s) => Math.floor(((s.oklch[2] % 360) + 360) % 360 / 30); + for (const s of seeds) { const b = bucketOf(s); bucketCount[b] = (bucketCount[b] || 0) + 1; } + const weights = seeds.map((s) => 1 / bucketCount[bucketOf(s)]); + const total = weights.reduce((a, b) => a + b, 0); + return { weights, total }; +} + +function weightedPick(seeds, unit) { + const { weights, total } = buildWeights(seeds); + let target = unit * total; + for (let i = 0; i < seeds.length; i++) { + target -= weights[i]; + if (target < 0) return seeds[i]; + } + return seeds[seeds.length - 1]; +} + +function pickSeed(seeds, { id, from }) { + if (id) { + const found = seeds.find(s => s.id === id); + if (!found) { console.error(`no seed with id "${id}"`); process.exit(2); } + return found; + } + const envFrom = process.env.IMPECCABLE_PALETTE_SEED; + const key = from || envFrom; + const unit = key ? hashUnit(key) : Math.random(); + return weightedPick(seeds, unit); +} + +function fmtOklch([L, C, H]) { + return `oklch(${L.toFixed(3)} ${C.toFixed(3)} ${H.toFixed(1)})`; +} + +function hueWord(H) { + if (H < 15 || H >= 345) return 'pure red'; + if (H < 35) return 'warm red / crimson'; + if (H < 55) return 'warm coral / burnt orange'; + if (H < 80) return 'orange / honey'; + if (H < 105) return 'warm amber / honey-gold'; + if (H < 135) return 'yellow-green / olive'; + if (H < 170) return 'green'; + if (H < 200) return 'teal'; + if (H < 230) return 'sky blue'; + if (H < 265) return 'cobalt / indigo'; + if (H < 295) return 'violet / purple'; + if (H < 330) return 'magenta / pink'; + return 'deep pink / rose'; +} + +// --------------------------------------------------------------- + +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}` : ''; + +// --------------------------------------------------------------- +// Fat tool-exit response — what the model sees on stdout. +// --------------------------------------------------------------- + +process.stdout.write(`BRAND SEED · ${seed.id} + +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. + +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. + +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é. + + 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 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. + + 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. + +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): + + - 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. + +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. + +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. +`); diff --git a/.qoder/skills/impeccable/scripts/pin.mjs b/.qoder/skills/impeccable/scripts/pin.mjs index ba02783fb..320d98ce1 100644 --- a/.qoder/skills/impeccable/scripts/pin.mjs +++ b/.qoder/skills/impeccable/scripts/pin.mjs @@ -27,7 +27,7 @@ const HARNESS_DIRS = [ // Valid sub-command names const VALID_COMMANDS = [ - 'craft', 'teach', 'extract', 'document', 'shape', + 'craft', 'init', 'extract', 'document', 'shape', 'critique', 'audit', 'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live', 'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive', diff --git a/.rovodev/skills/impeccable/SKILL.md b/.rovodev/skills/impeccable/SKILL.md index 0071bc561..b2b870e80 100644 --- a/.rovodev/skills/impeccable/SKILL.md +++ b/.rovodev/skills/impeccable/SKILL.md @@ -1,10 +1,10 @@ --- name: impeccable description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks. -version: 3.1.1 +version: 3.5.0 user-invocable: true -argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · teach|document|extract|live] [target]" -license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution. +argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]" +license: Apache 2.0 allowed-tools: - Bash(npx impeccable *) --- @@ -13,85 +13,80 @@ Designs and iterates production-grade frontend interfaces. Real working code, co ## Setup -Before any design work or file edits: +You MUST do these steps before proceeding: -1. Load context (PRODUCT.md / DESIGN.md) via the loader script. -2. Identify the register and load the matching register reference (brand.md or product.md). -3. **If the user invoked a sub-command (e.g. `craft`, `shape`, `audit`), load its reference file too.** This is non-negotiable: `craft` without `craft.md` loaded means you'll skip the shape-and-confirm step the user expects. +1. Run `node .rovodev/skills/impeccable/scripts/context.mjs` once per session. If you've already seen its output in this conversation, do not re-run it. The script either prints the project's PRODUCT.md (and DESIGN.md when present) as a markdown block, or tells you it's missing. Follow whatever it prints. **If it reports `NO_PRODUCT_MD`, stop and follow `reference/init.md` before doing anything else.** If the output ends with an `UPDATE_AVAILABLE` directive, follow it (ask the user once about updating, then continue). It never blocks the current task. +2. If the user invoked a sub-command (`craft`, `shape`, `audit`, `polish`, ...), you MUST read `reference/.md` next. Non-optional. The reference defines the command's flow; without it you will skip steps the user expects. +3. Familiarize yourself with any existing design system, conventions, and components in the code. Read at least one project file (CSS / tokens / theme / a representative component or page). **Required even when you've loaded a sub-command reference in step 2.** Don't reinvent the wheel; use what's there when it works, branch out when the UX wins. +4. Read the matching register reference. **This is non-optional; skipping it produces generic output.** If the project is marketing, a landing page, a campaign, long-form content, or a portfolio (design IS the product), read `reference/brand.md`. If it is app UI, admin, a dashboard, or a tool (design SERVES the product), read `reference/product.md`. Pick by first match: (1) task cue ("landing page" vs "dashboard"); (2) surface in focus (the page, file, or route being worked on); (3) `register` field in PRODUCT.md. +5. **If the project is brand-new (no existing CSS tokens / theme / committed brand colors found in step 3)**, run `node .rovodev/skills/impeccable/scripts/palette.mjs` to receive a brand seed color and composition guidance. This is the anchor for your primary brand color. Compose the rest of the palette (bg, surface, ink, accent, muted) around it per the script's instructions. Use OKLCH throughout. **Skip this step only if step 3 found committed brand colors in existing tokens; in that case identity-preservation wins.** -Skipping these produces generic output that ignores the project. +## Design guidance -### 1. Context gathering +Produce ready-to-ship, production-grade code, not prototypes or starting points. Take no shortcuts unless the user asks for them (when in doubt, ask). Don't stop until arriving at a complete implementation (beautiful, responsive, fast, precise, bug-free, on brand). You take attention to detail seriously: every page, section or component crafted is battle tested using the tools available to you (browser screenshotting, computer use, etc). Rovo Dev is capable of extraordinary work. Don't hold back. -Two files, case-insensitive. The loader looks at the project root by default and falls back to `.agents/context/` and `docs/` if the root is clean. Override with `IMPECCABLE_CONTEXT_DIR=path/to/dir` (absolute or relative to cwd). +### General rules -- **PRODUCT.md**: required. Users, brand, tone, anti-references, strategic principles. -- **DESIGN.md**: optional, strongly recommended. Colors, typography, elevation, components. +#### Color -Load both in one call: +- **Verify contrast.** Body text must hit ≥4.5:1 against its background; large text (≥18px or bold ≥14px) needs ≥3:1. Placeholder text needs the same 4.5:1, not the muted-gray default. The most common failure: muted gray body text on a tinted near-white. If the contrast is even close, bump the body color toward the ink end of the ramp; light gray "for elegance" is the single biggest reason AI designs feel hard to read. +- Gray text on a colored background looks washed out. Use a darker shade of the background's own hue, or a transparency of the text color. -```bash -node .rovodev/skills/impeccable/scripts/load-context.mjs -``` +#### Typography -Consume the full JSON output. Never pipe through `head`, `tail`, `grep`, or `jq`. The output's `contextDir` field tells you where the files were resolved from. +- Cap body line length at 65–75ch. +- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales. +- Cap font-family count at 3 (display + body + optional mono). More than 3 reads as indecision, not richness. One well-tuned family with weight contrast usually beats three competing typefaces. +- Don't pair fonts that are similar but not identical (two geometric sans-serifs, two humanist sans-serifs). Pair on a contrast axis (serif + sans, geometric + humanist) or use one family in multiple weights. +- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. +- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. +- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". +- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. -If the output is already in this session's conversation history, don't re-run. Exceptions requiring a fresh load: you just ran `/impeccable teach` or `/impeccable document` (they rewrite the files), or the user manually edited one. +#### Layout -`/impeccable live` already warms context via `live.mjs`. If you've run `live.mjs`, don't also run `load-context.mjs` this session. +- Vary spacing for rhythm. +- Cards are the lazy answer. Use them only when they're truly the best affordance. Nested cards are always wrong. +- Flexbox for 1D, Grid for 2D. Don't default to Grid when `flex-wrap` would be simpler. +- For responsive grids without breakpoints: `repeat(auto-fit, minmax(280px, 1fr))`. +- Build a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip). Never arbitrary values like 999 or 9999. -If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `/impeccable teach`, then resume the user's original task with the fresh context. If the original task was `/impeccable craft`, resume into `/impeccable shape` before any implementation work. +#### Motion +- Motion should be intentional, and not be an afterthought. consider it as part of the build. +- Don't animate CSS layout properties unless truly needed. +- Ease out with exponential curves (ease-out-quart / quint / expo). No bounce, no elastic. +- Use libraries for more advanced motion needs (e.g. motion, gsap, anime.js, lenis etc) +- Reduced motion is not optional. Every animation needs a `@media (prefers-reduced-motion: reduce)` alternative: typically a crossfade or instant transition. +- Staggering the items within one list is legitimate. The tell is the uniform reflex (one identical entrance applied to every section), not motion itself; each reveal should fit what it reveals. Suppressing the reflex is never a reason to ship a page with no motion at all. +- Reveal animations must enhance an already-visible default. Don't gate content visibility on a class-triggered transition; transitions pause on hidden tabs and headless renderers, so the reveal never fires and the section ships blank. +- Premium motion materials are not just transform/opacity. Blur, backdrop-filter, clip-path, mask, and shadow/glow are part of the palette when they materially improve the effect and stay smooth. -If DESIGN.md is missing: nudge once per session (*"Run `/impeccable document` for more on-brand output"*), then proceed. +#### Interaction -### 2. Register +- Dropdowns rendered with `position: absolute` inside an `overflow: hidden` or `overflow: auto` container will be clipped. Use the native `` / popover API, `position: fixed`, or a portal to escape the stacking context. -Every design task is **brand** (marketing, landing, campaign, long-form content, portfolio: design IS the product) or **product** (app UI, admin, dashboard, tool: design SERVES the product). +### Copy -Identify before designing. Priority: (1) cue in the task itself ("landing page" vs "dashboard"); (2) the surface in focus (the page, file, or route being worked on); (3) `register` field in PRODUCT.md. First match wins. +- Every word earns its place. No restated headings, no intros that repeat the title. +- **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`. +- **No aphoristic-cadence body copy as a default voice.** Don't fall into the rhythm of "serious statement, then punchy short negation" as the page's recurring voice. If three or more section copy blocks on the page land on a short rebuttal-shaped sentence, rewrite. Specific, not aphoristic. +- **No marketing buzzwords.** The streamline / empower / supercharge / leverage / unleash / transform / seamless / world-class / enterprise-grade / next-generation / cutting-edge / game-changer / mission-critical family of phrases. Pick a specific noun and a verb that describes what the product literally does. +- Button labels: verb + object. "Save changes" beats "OK"; "Delete project" beats "Yes". The label should say what will happen. +- Link text needs standalone meaning. "View pricing plans" beats "Click here"; screen readers announce links out of context. -If PRODUCT.md lacks the `register` field (legacy), infer it once from its "Users" and "Product Purpose" sections, then cache the inferred value for the session. Suggest the user run `/impeccable teach` to add the field explicitly. +### New projects only (when no prior work exists) -Load the matching reference: [reference/brand.md](reference/brand.md) or [reference/product.md](reference/product.md). The shared design laws below apply to both. +#### Color & Theme -## Shared design laws - -Apply to every design, both registers. Match implementation complexity to the aesthetic vision: maximalism needs elaborate code, minimalism needs precision. Interpret creatively. Vary across projects; never converge on the same choices. Rovo Dev is capable of extraordinary work. Don't hold back. - -### Color - -- Use OKLCH. Reduce chroma as lightness approaches 0 or 100; high chroma at extremes looks garish. -- Never use `#000` or `#fff`. Tint every neutral toward the brand hue (chroma 0.005–0.01 is enough). +- Use OKLCH. +- **The cream / sand / beige body bg is the saturated AI default of 2026.** The whole warm-neutral band (OKLCH L 0.84-0.97, C < 0.06, hue 40-100) reads as cream/sand/paper/parchment regardless of what you call it. Token names like `--paper`, `--cream`, `--sand`, `--bone`, `--flour`, `--linen`, `--parchment`, `--wheat`, `--biscuit`, `--ivory` are tells in themselves. If the brief is "warm, traditional, family-coastal-Italian" or "magazine-warm" or "editorial-restraint", DO NOT translate that into a near-white warm-tinted bg; that's the AI move. Pick: (a) a saturated brand color as the body (terracotta, oxblood, deep ochre, near-black), (b) a true off-white at chroma 0 (or chroma toward the brand's own hue, not toward warmth-by-default), or (c) a darker mid-tone tinted neutral that's clearly the brand's own. "Warmth" in the brand is carried by accent + typography + imagery, not by body bg. +- Tinted neutrals: add 0.005–0.015 chroma toward the brand's hue. Don't default-tint toward warm or cool "because the brand feels that way"; that's the cross-project monoculture move. +- When picking a theme: Dark vs. light is never a default. Not dark "because tools look cool dark." Not light "to be safe.".Before choosing, write one sentence of physical scene: who uses this, where, under what ambient light, in what mood. If the sentence doesn't force the answer, it's not concrete enough. Add detail until it does. - Pick a **color strategy** before picking colors. Four steps on the commitment axis: - **Restrained**: tinted neutrals + one accent ≤10%. Product default; brand minimalism. - **Committed**: one saturated color carries 30–60% of the surface. Brand default for identity-driven pages. - **Full palette**: 3–4 named roles, each used deliberately. Brand campaigns; product data viz. - **Drenched**: the surface IS the color. Brand heroes, campaign pages. -- The "one accent ≤10%" rule is Restrained only. Committed / Full palette / Drenched exceed it on purpose. Don't collapse every design to Restrained by reflex. - -### Theme - -Dark vs. light is never a default. Not dark "because tools look cool dark." Not light "to be safe." - -Before choosing, write one sentence of physical scene: who uses this, where, under what ambient light, in what mood. If the sentence doesn't force the answer, it's not concrete enough. Add detail until it does. - -"Observability dashboard" does not force an answer. "SRE glancing at incident severity on a 27-inch monitor at 2am in a dim room" does. Run the sentence, not the category. - -### Typography - -- Cap body line length at 65–75ch. -- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales. - -### Layout - -- Vary spacing for rhythm. Same padding everywhere is monotony. -- Cards are the lazy answer. Use them only when they're truly the best affordance. Nested cards are always wrong. -- Don't wrap everything in a container. Most things don't need one. - -### Motion - -- Don't animate CSS layout properties. -- Ease out with exponential curves (ease-out-quart / quint / expo). No bounce, no elastic. ### Absolute bans @@ -102,12 +97,9 @@ Match-and-refuse. If you're about to write any of these, rewrite the element wit - **Glassmorphism as default.** Blurs and glass cards used decoratively. Rare and purposeful, or nothing. - **The hero-metric template.** Big number, small label, supporting stats, gradient accent. SaaS cliché. - **Identical card grids.** Same-sized cards with icon + heading + text, repeated endlessly. -- **Modal as first thought.** Modals are usually laziness. Exhaust inline / progressive alternatives first. - -### Copy - -- Every word earns its place. No restated headings, no intros that repeat the title. -- **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`. +- **Tiny uppercase tracked eyebrow above every section.** The 2023-era kicker (small all-caps text with wide tracking, "ABOUT" "PROCESS" "PRICING" above each heading) is now the saturated AI scaffold; it appears on 55-95% of generations regardless of brief, which is the definition of a tell. One named kicker as a deliberate brand system is voice; an eyebrow on every section is AI grammar. Choose a different cadence. +- **Numbered section markers as default scaffolding (01 / 02 / 03).** Putting `01 · About / 02 · Process / 03 · Pricing` above every section is the eyebrow trope one tier deeper: reach for it because "landing pages do this" and you're scaffolding by reflex. Numbers earn their place when the section actually IS a sequence (a real 3-step process, an ordered flow, a typed timeline) and the order carries information the reader needs. One deliberate numbered sequence on one page is voice; numbered eyebrows on every section across the site is AI grammar. +- **Text that overflows its container.** Long heading words plus large clamp scales plus narrow grids cause headline overflow on tablet/mobile. Test the heading copy at every breakpoint; if it overflows, reduce the clamp max or rewrite the copy. The viewport is part of the design. ### The AI slop test @@ -115,7 +107,7 @@ If someone could look at this interface and say "AI made that" without doubt, it **Category-reflex check.** Run at two altitudes; the second one catches what the first one misses. -- **First-order:** if someone could guess the theme + palette from the category alone ("observability → dark blue", "healthcare → white + teal", "finance → navy + gold", "crypto → neon on black"), it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain. +- **First-order:** if someone could guess the theme + palette from the category alone, it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain. - **Second-order:** if someone could guess the aesthetic family from category-plus-anti-references ("AI workflow tool that's not SaaS-cream → editorial-typographic", "fintech that's not navy-and-gold → terminal-native dark mode"), it's the trap one tier deeper. The first reflex was avoided; the second wasn't. Rework until both answers are not obvious. The brand register's [reflex-reject aesthetic lanes](reference/brand.md) list catches the currently-saturated families. ## Commands @@ -124,7 +116,7 @@ If someone could look at this interface and say "AI made that" without doubt, it |---|---|---|---| | `craft [feature]` | Build | Shape, then build a feature end-to-end | [reference/craft.md](reference/craft.md) | | `shape [feature]` | Build | Plan UX/UI before writing code | [reference/shape.md](reference/shape.md) | -| `teach` | Build | Set up PRODUCT.md and DESIGN.md context | [reference/teach.md](reference/teach.md) | +| `init` | Build | Set up project context: PRODUCT.md, DESIGN.md, live config, next steps | [reference/init.md](reference/init.md) | | `document` | Build | Generate DESIGN.md from existing project code | [reference/document.md](reference/document.md) | | `extract [target]` | Build | Pull reusable tokens and components into design system | [reference/extract.md](reference/extract.md) | | `critique [target]` | Evaluate | UX design review with heuristic scoring | [reference/critique.md](reference/critique.md) | @@ -152,11 +144,14 @@ Plus two management commands: `pin ` and `unpin `, detailed be 1. **No argument**: render the table above as the user-facing command menu, grouped by category. Ask what they'd like to do. 2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. -3. **First word doesn't match**: general design invocation. Apply the setup steps, shared design laws, and the loaded register reference, using the full argument as context. +3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. +4. **No clear command match**: general design invocation. Apply the setup steps, the General rules, and the loaded register reference, using the full argument as context. Setup (context gathering, register) is already loaded by then; sub-commands don't re-invoke `/impeccable`. -If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `teach` as a blocker, finish teach, refresh context, then resume the original command and target. +If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `init` as a blocker, finish init, refresh context, then resume the original command and target. + +`teach` is a deprecated alias for `init`: if the user types it, load [reference/init.md](reference/init.md) and proceed as if they ran `init`. ## Pin / Unpin diff --git a/.rovodev/skills/impeccable/reference/adapt.md b/.rovodev/skills/impeccable/reference/adapt.md index 9d0673ea2..5af7606cb 100644 --- a/.rovodev/skills/impeccable/reference/adapt.md +++ b/.rovodev/skills/impeccable/reference/adapt.md @@ -188,3 +188,124 @@ Test thoroughly across contexts: - **Slow connections**: Test on throttled network When the adaptation feels native to each context, hand off to `/impeccable polish` for the final pass. + +--- + +## Reference Material + +The sections below were previously `responsive-design.md` and live inline now so the adapt flow has its deep responsive reference in one place. + +### Responsive Design + +#### Mobile-First: Write It Right + +Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first. + +#### Breakpoints: Content-Driven + +Don't chase device sizes; let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints. + +#### Detect Input Method, Not Just Screen Size + +**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard. Use pointer and hover queries: + +```css +/* Fine pointer (mouse, trackpad) */ +@media (pointer: fine) { + .button { padding: 8px 16px; } +} + +/* Coarse pointer (touch, stylus) */ +@media (pointer: coarse) { + .button { padding: 12px 20px; } /* Larger touch target */ +} + +/* Device supports hover */ +@media (hover: hover) { + .card:hover { transform: translateY(-2px); } +} + +/* Device doesn't support hover (touch) */ +@media (hover: none) { + .card { /* No hover state - use active instead */ } +} +``` + +**Critical**: Don't rely on hover for functionality. Touch users can't hover. + +#### Safe Areas: Handle the Notch + +Modern phones have notches, rounded corners, and home indicators. Use `env()`: + +```css +body { + padding-top: env(safe-area-inset-top); + padding-bottom: env(safe-area-inset-bottom); + padding-left: env(safe-area-inset-left); + padding-right: env(safe-area-inset-right); +} + +/* With fallback */ +.footer { + padding-bottom: max(1rem, env(safe-area-inset-bottom)); +} +``` + +**Enable viewport-fit** in your meta tag: +```html + +``` + +#### Responsive Images: Get It Right + +##### srcset with Width Descriptors + +```html +Hero image +``` + +**How it works**: +- `srcset` lists available images with their actual widths (`w` descriptors) +- `sizes` tells the browser how wide the image will display +- Browser picks the best file based on viewport width AND device pixel ratio + +##### Picture Element for Art Direction + +When you need different crops/compositions (not just resolutions): + +```html + + + + ... + +``` + +#### Layout Adaptation Patterns + +**Navigation**: Three stages: hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `
/` for content that can collapse on mobile. + +#### Testing: Don't Trust DevTools Alone + +DevTools device emulation is useful for layout but misses: + +- Actual touch interactions +- Real CPU/memory constraints +- Network latency patterns +- Font rendering differences +- Browser chrome/keyboard appearances + +**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators. + +--- + +**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful. diff --git a/.rovodev/skills/impeccable/reference/animate.md b/.rovodev/skills/impeccable/reference/animate.md index 20aeea659..15d9f9080 100644 --- a/.rovodev/skills/impeccable/reference/animate.md +++ b/.rovodev/skills/impeccable/reference/animate.md @@ -6,7 +6,7 @@ Add motion that conveys state, gives feedback, and clarifies hierarchy. Cut moti ## Register -Brand: orchestrated page-load sequences, staggered reveals, scroll-driven animation. Motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions. +Brand: motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions. The saturated AI default is fade-and-rise reveals on every scrolled section; that's a tell, not a choreography. Reserve scroll-triggered motion for moments that earn it. Product: 150–250 ms on most transitions. Motion conveys state: feedback, reveal, loading, transitions between views. No page-load choreography; users are in a task and won't wait for it. @@ -49,10 +49,11 @@ Create a purposeful animation plan: Add motion systematically across these categories: ### Entrance Animations -- **Page load choreography**: Stagger element reveals (100-150ms delays), fade + slide combinations - **Hero section**: Dramatic entrance for primary content (scale, parallax, or creative effects) -- **Content reveals**: Scroll-triggered animations using intersection observer - **Modal/drawer entry**: Smooth slide + fade, backdrop fade, focus management +- **List rhythm**: Sibling stagger is legitimate for cards-in-a-grid or list-items-appearing. Whole-section fade-on-scroll is not a list and is not legitimate. Cap total stagger time: 10 items at 50ms each = 500ms total. For more items, reduce per-item delay or cap the staggered count. + + Use CSS custom properties for clean stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"`, `style="--i: 1"`, etc. on each item. ### Micro-interactions - **Button feedback**: @@ -97,11 +98,14 @@ Use appropriate techniques for each animation: ### Timing & Easing -**Durations by purpose:** -- **100-150ms**: Instant feedback (button press, toggle) -- **200-300ms**: State changes (hover, menu open) -- **300-500ms**: Layout changes (accordion, modal) -- **500-800ms**: Entrance animations (page load) +**Duration: the 100/300/500 rule.** Timing matters more than easing for "feels right": + +| Duration | Use Case | Examples | +|----------|----------|----------| +| **100–150ms** | Instant feedback | Button press, toggle, color change | +| **200–300ms** | State changes | Menu open, tooltip, hover state | +| **300–500ms** | Layout changes | Accordion, modal, drawer | +| **500–800ms** | Entrance animations | Page load, hero reveal | **Easing curves (use these, not CSS defaults):** ```css @@ -134,13 +138,35 @@ Use appropriate techniques for each animation: - GSAP for complex sequences ``` +### Motion Materials + +Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties. Match material to effect: + +- **Transform / opacity**: movement, press feedback, simple reveals, list choreography +- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances +- **Clip-path / masks**: wipes, reveals, editorial cropping, product-like transitions +- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state +- **Grid-template-rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly + +The hard rule isn't "transform and opacity only." It's: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify smoothness in-browser on target viewports. + ### Performance -- **Motion materials**: Use transform/opacity for reliable movement, but use blur, filters, masks, shadows, and color shifts when they materially improve the effect - **Layout safety**: Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins) -- **will-change**: Add sparingly for known expensive animations +- **will-change**: Add sparingly for known expensive animations only (e.g. on `:hover` or an `.animating` class), never preemptively across the whole page +- **Scroll triggers**: Use Intersection Observer instead of scroll event listeners; unobserve after the animation fires once - **Bound expensive effects**: Keep blur/filter/shadow areas small or isolated, use `contain` where appropriate - **Monitor FPS**: Ensure 60fps on target devices +### Perceived Performance + +Nobody cares how fast your site *is*, only how fast it feels. The 80ms threshold: anything under ~80ms feels instant because our brains buffer sensory input for that long to synchronize perception. Target this for micro-interactions. + +- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening. +- **Early completion**: Show content progressively, don't wait for everything (progressive images, streaming HTML, skeleton fade-ins). +- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Use for low-stakes actions (likes, follows). Avoid for payments or destructive operations. +- **Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances. +- **Caution**: Too-fast responses can decrease perceived value for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening. + ### Accessibility ```css @media (prefers-reduced-motion: reduce) { diff --git a/.rovodev/skills/impeccable/reference/bolder.md b/.rovodev/skills/impeccable/reference/bolder.md index ff0ef2f14..30d4e852c 100644 --- a/.rovodev/skills/impeccable/reference/bolder.md +++ b/.rovodev/skills/impeccable/reference/bolder.md @@ -50,7 +50,7 @@ Create a strategy to increase impact while maintaining coherence: Systematically increase impact across these dimensions: ### Typography Amplification -- **Replace generic fonts**: Swap system fonts for distinctive choices (see the parent skill's typography guidelines and [typography.md](typography.md) for inspiration) +- **Replace generic fonts**: Swap system fonts for distinctive choices (see the parent skill's typography guidelines and the [Reference Material section of typeset.md](typeset.md#reference-material) for inspiration) - **Extreme scale**: Create dramatic size jumps (3x-5x differences, not 1.5x) - **Weight contrast**: Pair 900 weights with 200 weights, not 600 with 400 - **Unexpected choices**: Variable fonts, display fonts for headlines, condensed/extended widths, monospace as intentional accent (not as lazy "dev tool" default) @@ -78,10 +78,10 @@ Systematically increase impact across these dimensions: - **Custom elements**: Illustrative elements, custom icons, decorative details that reinforce brand ### Motion & Animation -- **Entrance choreography**: Staggered, dramatic page load animations with 50-100ms delays -- **Scroll effects**: Parallax, reveal animations, scroll-triggered sequences -- **Micro-interactions**: Satisfying hover effects, click feedback, state changes -- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic, which cheapen the effect) +- **Hero moment**: One signature entrance, once. Not on every visit and not on every section. +- **Micro-interactions**: Satisfying hover effects, click feedback, state changes. +- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic, which cheapen the effect). +- **Bolder ≠ scroll-fade-rise on every section.** That's the saturated AI default, the opposite of bold. ### Composition Boldness - **Hero moments**: Create clear focal points with dramatic treatment diff --git a/.rovodev/skills/impeccable/reference/brand.md b/.rovodev/skills/impeccable/reference/brand.md index 3d83a1cdc..194514e95 100644 --- a/.rovodev/skills/impeccable/reference/brand.md +++ b/.rovodev/skills/impeccable/reference/brand.md @@ -43,17 +43,10 @@ The reflex-reject lists apply to **new design choices**. When the existing brand ### Pairing and voice -Distinctive + refined is the goal. The specific shape depends on the brand: - -- **Editorial / long-form / luxury**: display serif + sans body (a magazine shape). -- **Tech / dev tools / fintech**: one committed sans, usually; custom-tight tracking, strong weight contrast inside a single family. -- **Consumer / food / travel**: warmer pairings, often a humanist sans plus a script or display serif. -- **Creative studios / agencies**: rule-breaking welcome. Mono-only, or display-only, or custom-drawn type as voice. +Distinctive + refined is the goal. The specific shape depends on the brand, not on the brand's category. A category ("restaurant", "dev tool", "magazine", "fintech") is not a recipe; treating it as one is the first-order reflex SKILL.md warns against. Two families minimum is the rule *only* when the voice needs it. A single well-chosen family with committed weight/size contrast is stronger than a timid display+body pair. -Vary across projects. If the last brief was a serif-display landing page, this one isn't. - ### Scale Modular scale, fluid `clamp()` for headings, ≥1.25 ratio between steps. Flat scales (1.1× apart) read as uncommitted. @@ -74,8 +67,7 @@ Brand surfaces have permission for Committed, Full palette, and Drenched strateg - Asymmetric compositions are one option. Break the grid intentionally for emphasis. - Fluid spacing with `clamp()` that breathes on larger viewports. Vary for rhythm: generous separations, tight groupings. -- Alternative: a strict, visible grid as the voice (brutalist / Swiss / tech-spec aesthetics). Either asymmetric or rigorously-gridded can be "designed"; the failure mode is splitting the difference into a generic centered stack. -- Don't default to centering everything. Left-aligned with asymmetric layouts feels more designed; a strict grid reads as confident structure. A centered-stack hero with icon-title-subtitle cards reads as template. +- For image-led briefs (hotels, restaurants, magazines, photography), full-bleed hero imagery with overlaid menu and centered headline is a canonical move; let the photograph be the design. - When cards ARE the right affordance, use `grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` for breakpoint-free responsiveness. ## Imagery @@ -93,8 +85,7 @@ Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landin ## Motion -- One well-orchestrated page-load with staggered reveals beats scattered micro-interactions, when the brand invites it. Tech-minimal brands often skip entrance motion entirely; the restraint is the voice. -- For collapsing/expanding sections, transition `grid-template-rows` rather than `height`. +- One well-orchestrated page-load beats scattered micro-interactions, when the brand invites it. Some brands skip entrance motion entirely; the restraint is the voice. ## Brand bans (on top of the shared absolute bans) @@ -111,8 +102,7 @@ Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landin Brand can afford things product can't. Take them. -- Ambitious first-load motion. Reveals, scroll-triggered transitions, typographic choreography. +- Ambitious first-load motion. Reveals and typographic choreography that earn their place; not fade-on-scroll for every section. - Single-purpose viewports. One dominant idea per fold, long scroll, deliberate pacing. -- Typographic risk. Enormous display type, unexpected italic cuts, mixed cases, hand-drawn headlines, a single oversize word as a hero. - Unexpected color strategies. Palette IS voice; a calm brand and a restless brand should not share palette mechanics. - Art direction per section. Different sections can have different visual worlds if the narrative demands it. Consistency of voice beats consistency of treatment. diff --git a/.rovodev/skills/impeccable/reference/clarify.md b/.rovodev/skills/impeccable/reference/clarify.md index 5f7d420f4..00ee978c3 100644 --- a/.rovodev/skills/impeccable/reference/clarify.md +++ b/.rovodev/skills/impeccable/reference/clarify.md @@ -172,3 +172,117 @@ Test that copy improvements work: - **Tone**: Is it appropriate for the situation? When the copy reads cleanly, hand off to `/impeccable polish` for the final pass. + +--- + +## Reference Material + +The sections below were previously `ux-writing.md` and live inline now so the clarify flow has its deep UX-writing reference in one place. + +### UX Writing + +#### The Button Label Problem + +**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns: + +| Bad | Good | Why | +|-----|------|-----| +| OK | Save changes | Says what will happen | +| Submit | Create account | Outcome-focused | +| Yes | Delete message | Confirms the action | +| Cancel | Keep editing | Clarifies what "cancel" means | +| Click here | Download PDF | Describes the destination | + +**For destructive actions**, name the destruction: +- "Delete" not "Remove" (delete is permanent, remove implies recoverable) +- "Delete 5 items" not "Delete selected" (show the count) + +#### Error Messages: The Formula + +Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input". + +##### Error Message Templates + +| Situation | Template | +|-----------|----------| +| **Format error** | "[Field] needs to be [format]. Example: [example]" | +| **Missing required** | "Please enter [what's missing]" | +| **Permission denied** | "You don't have access to [thing]. [What to do instead]" | +| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." | +| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" | + +##### Don't Blame the User + +Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date". + +#### Empty States Are Opportunities + +Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items". + +#### Voice vs Tone + +**Voice** is your brand's personality, consistent everywhere. +**Tone** adapts to the moment. + +| Moment | Tone Shift | +|--------|------------| +| Success | Celebratory, brief: "Done! Your changes are live." | +| Error | Empathetic, helpful: "That didn't work. Here's what to try..." | +| Loading | Reassuring: "Saving your work..." | +| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | + +**Never use humor for errors.** Users are already frustrated. Be helpful, not cute. + +#### Writing for Accessibility + +**Link text** must have standalone meaning: "View pricing plans" not "Click here". **Alt text** describes information, not the image: "Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context. + +#### Writing for Translation + +##### Plan for Expansion + +German text is ~30% longer than English. Allocate space: + +| Language | Expansion | +|----------|-----------| +| German | +30% | +| French | +20% | +| Finnish | +30-40% | +| Chinese | -30% (fewer chars, but same width) | + +##### Translation-Friendly Patterns + +Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear. + +#### Consistency: The Terminology Problem + +Pick one term and stick with it: + +| Inconsistent | Consistent | +|--------------|------------| +| Delete / Remove / Trash | Delete | +| Settings / Preferences / Options | Settings | +| Sign in / Log in / Enter | Sign in | +| Create / Add / New | Create | + +Build a terminology glossary and enforce it. Variety creates confusion. + +#### Avoid Redundant Copy + +If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well. + +#### Loading States + +Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress. + +#### Confirmation Dialogs: Use Sparingly + +Most confirmation dialogs are design failures; consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No"). + +#### Form Instructions + +Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking. + +--- + +**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors. diff --git a/.rovodev/skills/impeccable/reference/codex.md b/.rovodev/skills/impeccable/reference/codex.md index f40ec746f..d563af95b 100644 --- a/.rovodev/skills/impeccable/reference/codex.md +++ b/.rovodev/skills/impeccable/reference/codex.md @@ -23,8 +23,8 @@ Before generating anything, run a brief direction conversation grounded in the s Ask **2-3 targeted questions** about visual lane, color strategy, atmosphere, and named anchor references. Don't enumerate generic menus; tie each question to the shape brief's answers. Example shape-grounded questions: -- "Brief says 'editorial restraint, Klim-adjacent.' Are we closer to a quiet specimen page or a magazine-spread feel with hero imagery?" -- "Palette strategy from shape was 'Committed.' Want it warm-grounded (deep oxblood + cream) or cool-grounded (slate + paper white)?" +- "Brief says 'specimen-page restraint.' Are we closer to a quiet typographic page or a wider editorial spread with hero imagery?" +- "Palette strategy from shape was 'Committed.' Which one color carries the surface (a brand-driven pick rather than a default warm-or-cool framing)? (And no, the answer isn't a cream/sand body bg; that's the saturated AI default.)" **STOP and wait for answers.** These pin the palette before any pixel gets generated. Do not proceed to Step B until the user has responded. diff --git a/.rovodev/skills/impeccable/reference/cognitive-load.md b/.rovodev/skills/impeccable/reference/cognitive-load.md deleted file mode 100644 index 48f8ad58b..000000000 --- a/.rovodev/skills/impeccable/reference/cognitive-load.md +++ /dev/null @@ -1,106 +0,0 @@ -# Cognitive Load Assessment - -Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload. - ---- - -## Three Types of Cognitive Load - -### Intrinsic Load: The Task Itself -Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it. - -**Manage it by**: -- Breaking complex tasks into discrete steps -- Providing scaffolding (templates, defaults, examples) -- Progressive disclosure: show what's needed now, hide the rest -- Grouping related decisions together - -### Extraneous Load: Bad Design -Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste. - -**Common sources**: -- Confusing navigation that requires mental mapping -- Unclear labels that force users to guess meaning -- Visual clutter competing for attention -- Inconsistent patterns that prevent learning -- Unnecessary steps between user intent and result - -### Germane Load: Learning Effort -Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery. - -**Support it by**: -- Progressive disclosure that reveals complexity gradually -- Consistent patterns that reward learning -- Feedback that confirms correct understanding -- Onboarding that teaches through action, not walls of text - ---- - -## Cognitive Load Checklist - -Evaluate the interface against these 8 items: - -- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements? -- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)? -- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)? -- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen? -- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next? -- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)? -- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one? -- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it? - -**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed). - ---- - -## The Working Memory Rule - -**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001). - -At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider: -- **≤4 items**: Within working memory limits, manageable -- **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure -- **8+ items**: Overloaded; users will skip, misclick, or abandon - -**Practical applications**: -- Navigation menus: ≤5 top-level items (group the rest under clear categories) -- Form sections: ≤4 fields visible per group before a visual break -- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu -- Dashboard widgets: ≤4 key metrics visible without scrolling -- Pricing tiers: ≤3 options (more causes analysis paralysis) - ---- - -## Common Cognitive Load Violations - -### 1. The Wall of Options -**Problem**: Presenting 10+ choices at once with no hierarchy. -**Fix**: Group into categories, highlight recommended, use progressive disclosure. - -### 2. The Memory Bridge -**Problem**: User must remember info from step 1 to complete step 3. -**Fix**: Keep relevant context visible, or repeat it where it's needed. - -### 3. The Hidden Navigation -**Problem**: User must build a mental map of where things are. -**Fix**: Always show current location (breadcrumbs, active states, progress indicators). - -### 4. The Jargon Barrier -**Problem**: Technical or domain language forces translation effort. -**Fix**: Use plain language. If domain terms are unavoidable, define them inline. - -### 5. The Visual Noise Floor -**Problem**: Every element has the same visual weight; nothing stands out. -**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted. - -### 6. The Inconsistent Pattern -**Problem**: Similar actions work differently in different places. -**Fix**: Standardize interaction patterns. Same type of action = same type of UI. - -### 7. The Multi-Task Demand -**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating). -**Fix**: Sequence the steps. Let the user do one thing at a time. - -### 8. The Context Switch -**Problem**: User must jump between screens/tabs/modals to gather info for a single decision. -**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth. diff --git a/.rovodev/skills/impeccable/reference/color-and-contrast.md b/.rovodev/skills/impeccable/reference/color-and-contrast.md deleted file mode 100644 index 110c2ee47..000000000 --- a/.rovodev/skills/impeccable/reference/color-and-contrast.md +++ /dev/null @@ -1,105 +0,0 @@ -# Color & Contrast - -## Color Spaces: Use OKLCH - -**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal, unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark. - -The OKLCH function takes three components: `oklch(lightness chroma hue)` where lightness is 0-100%, chroma is roughly 0-0.4, and hue is 0-360. To build a primary color and its lighter / darker variants, hold the chroma+hue roughly constant and vary the lightness, but **reduce chroma as you approach white or black**, because high chroma at extreme lightness looks garish. - -The hue you pick is a brand decision and should not come from a default. Do not reach for blue (hue 250) or warm orange (hue 60) by reflex; those are the dominant AI-design defaults, not the right answer for any specific brand. - -## Building Functional Palettes - -### Tinted Neutrals - -**Pure gray is dead.** A neutral with zero chroma feels lifeless next to a colored brand. Add a tiny chroma value (0.005-0.015) to all your neutrals, hued toward whatever your brand color is. The chroma is small enough not to read as "tinted" consciously, but it creates subconscious cohesion between brand color and UI surfaces. - -The hue you tint toward should come from THIS project's brand, not from a "warm = friendly, cool = tech" formula. If your brand color is teal, your neutrals lean toward teal. If your brand color is amber, they lean toward amber. The point is cohesion with the SPECIFIC brand, not a stock palette. - -**Avoid** the trap of always tinting toward warm orange or always tinting toward cool blue. Those are the two laziest defaults and they create their own monoculture across projects. - -### Palette Structure - -A complete system needs: - -| Role | Purpose | Example | -|------|---------|---------| -| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades | -| **Neutral** | Text, backgrounds, borders | 9-11 shade scale | -| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each | -| **Surface** | Cards, modals, overlays | 2-3 elevation levels | - -**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise. - -### The 60-30-10 Rule (Applied Correctly) - -This rule is about **visual weight**, not pixel count: - -- **60%**: Neutral backgrounds, white space, base surfaces -- **30%**: Secondary colors: text, borders, inactive states -- **10%**: Accent: CTAs, highlights, focus states - -The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power. - -## Contrast & Accessibility - -### WCAG Requirements - -| Content Type | AA Minimum | AAA Target | -|--------------|------------|------------| -| Body text | 4.5:1 | 7:1 | -| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | -| UI components, icons | 3:1 | 4.5:1 | -| Non-essential decorations | None | None | - -**The gotcha**: Placeholder text still needs 4.5:1. That light gray placeholder you see everywhere? Usually fails WCAG. - -### Dangerous Color Combinations - -These commonly fail contrast or cause readability issues: - -- Light gray text on white (the #1 accessibility fail) -- **Gray text on any colored background**: gray looks washed out and dead on color. Use a darker shade of the background color, or transparency -- Red text on green background (or vice versa): 8% of men can't distinguish these -- Blue text on red background (vibrates visually) -- Yellow text on white (almost always fails) -- Thin light text on images (unpredictable contrast) - -### Never Use Pure Gray or Pure Black - -Pure gray (`oklch(50% 0 0)`) and pure black (`#000`) don't exist in nature; real shadows and surfaces always have a color cast. Even a chroma of 0.005-0.01 is enough to feel natural without being obviously tinted. (See tinted neutrals example above.) - -### Testing - -Don't trust your eyes. Use tools: - -- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) -- Browser DevTools → Rendering → Emulate vision deficiencies -- [Polypane](https://polypane.app/) for real-time testing - -## Theming: Light & Dark Mode - -### Dark Mode Is Not Inverted Light Mode - -You can't just swap colors. Dark mode requires different design decisions: - -| Light Mode | Dark Mode | -|------------|-----------| -| Shadows for depth | Lighter surfaces for depth (no shadows) | -| Dark text on light | Light text on dark (reduce font weight) | -| Vibrant accents | Desaturate accents slightly | -| White backgrounds | Never pure black; use dark gray (oklch 12-18%) | - -In dark mode, depth comes from surface lightness, not shadow. Build a 3-step surface scale where higher elevations are lighter (e.g. 15% / 20% / 25% lightness). Use the SAME hue and chroma as your brand color (whatever it is for THIS project; do not reach for blue) and only vary the lightness. Reduce body text weight slightly (e.g. 350 instead of 400) because light text on dark reads as heavier than dark text on light. - -### Token Hierarchy - -Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer; primitives stay the same. - -## Alpha Is A Design Smell - -Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed. - ---- - -**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Using pure black (#000) for large areas. Skipping color blindness testing (8% of men affected). diff --git a/.rovodev/skills/impeccable/reference/colorize.md b/.rovodev/skills/impeccable/reference/colorize.md index 9e96eef59..2457f608c 100644 --- a/.rovodev/skills/impeccable/reference/colorize.md +++ b/.rovodev/skills/impeccable/reference/colorize.md @@ -68,10 +68,10 @@ Add color systematically across these dimensions: - **Hover states**: Introduce color on interaction ### Background & Surfaces -- **Tinted backgrounds**: Replace pure gray (`#f5f5f5`) with warm neutrals (`oklch(97% 0.01 60)`) or cool tints (`oklch(97% 0.01 250)`) +- **Tinted backgrounds**: If you replace pure gray, tint toward the brand hue, not toward a generic-warm-or-cool pair. The default-warm-tint (`oklch(97% 0.01 60)` and its neighbors) is now the AI cream/sand giveaway. Be specific to the brand or stay neutral. - **Colored sections**: Use subtle background colors to separate areas - **Gradient backgrounds**: Add depth with subtle, intentional gradients (not generic purple-blue) -- **Cards & surfaces**: Tint cards or surfaces slightly for warmth +- **Cards & surfaces**: Tint cards or surfaces toward the brand, not "for warmth" by reflex **Use OKLCH for color**: It's perceptually uniform, meaning equal steps in lightness *look* equal. Great for generating harmonious scales. @@ -124,8 +124,6 @@ Ensure color addition improves rather than overwhelms: - Use every color in the rainbow (choose 2-4 colors beyond neutrals) - Apply color randomly without semantic meaning - Put gray text on colored backgrounds. It looks washed out; use a darker shade of the background color or transparency instead -- Use pure gray for neutrals. Add subtle color tint (warm or cool) for depth -- Use pure black (`#000`) or pure white (`#fff`) for large areas - Violate WCAG contrast requirements - Use color as the only indicator (accessibility issue) - Make everything colorful (defeats the purpose) @@ -152,3 +150,108 @@ When invoked from live mode, each variant MUST declare a `color-amount` param so ``` Layer 1-2 variant-specific params on top: palette selection (`steps` with named options), temperature warmth, or tint vs. true color. See `reference/live.md` for the full params contract. + +--- + +## Reference Material + +The sections below were previously `color-and-contrast.md` and live inline now so the colorize flow has its deep color reference in one place. + +### Color & Contrast + +#### Color Spaces: Use OKLCH + +**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal, unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark. + +The OKLCH function takes three components: `oklch(lightness chroma hue)` where lightness is 0-100%, chroma is roughly 0-0.4, and hue is 0-360. To build a primary color and its lighter / darker variants, hold the chroma+hue roughly constant and vary the lightness, but **reduce chroma as you approach white or black**, because high chroma at extreme lightness looks garish. + +The hue you pick is a brand decision and should not come from a default. Do not reach for blue (hue 250) or warm orange (hue 60) by reflex; those are the dominant AI-design defaults, not the right answer for any specific brand. + +#### Building Functional Palettes + +##### Tinted Neutrals + +**Pure gray is dead.** A neutral with zero chroma feels lifeless next to a colored brand. Add a tiny chroma value (0.005-0.015) to all your neutrals, hued toward whatever your brand color is. The chroma is small enough not to read as "tinted" consciously, but it creates subconscious cohesion between brand color and UI surfaces. + +The hue you tint toward should come from THIS project's brand, not from a "warm = friendly, cool = tech" formula. If your brand color is teal, your neutrals lean toward teal. If your brand color is amber, they lean toward amber. The point is cohesion with the SPECIFIC brand, not a stock palette. + +**Avoid** the trap of always tinting toward warm orange or always tinting toward cool blue. Those are the two laziest defaults and they create their own monoculture across projects. + +##### Palette Structure + +A complete system needs: + +| Role | Purpose | Example | +|------|---------|---------| +| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades | +| **Neutral** | Text, backgrounds, borders | 9-11 shade scale | +| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each | +| **Surface** | Cards, modals, overlays | 2-3 elevation levels | + +**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise. + +##### The 60-30-10 Rule (Applied Correctly) + +This rule is about **visual weight**, not pixel count: + +- **60%**: Neutral backgrounds, white space, base surfaces +- **30%**: Secondary colors: text, borders, inactive states +- **10%**: Accent: CTAs, highlights, focus states + +The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power. + +#### Contrast & Accessibility + +##### WCAG Requirements + +| Content Type | AA Minimum | AAA Target | +|--------------|------------|------------| +| Body text | 4.5:1 | 7:1 | +| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | +| UI components, icons | 3:1 | 4.5:1 | +| Non-essential decorations | None | None | + +##### Dangerous Color Combinations + +These commonly fail contrast or cause readability issues: + +- Light gray text on white (the #1 accessibility fail) +- Red text on green background (or vice versa): 8% of men can't distinguish these +- Blue text on red background (vibrates visually) +- Yellow text on white (almost always fails) +- Thin light text on images (unpredictable contrast) + +##### Testing + +Don't trust your eyes. Use tools: + +- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) +- Browser DevTools → Rendering → Emulate vision deficiencies +- [Polypane](https://polypane.app/) for real-time testing + +#### Theming: Light & Dark Mode + +##### Dark Mode Is Not Inverted Light Mode + +You can't just swap colors. Dark mode requires different design decisions: + +| Light Mode | Dark Mode | +|------------|-----------| +| Shadows for depth | Lighter surfaces for depth (no shadows) | +| Dark text on light | Light text on dark (reduce font weight) | +| Vibrant accents | Desaturate accents slightly | +| White backgrounds | Either pure black or a deep surface that fits the brand (a brand-tinted near-black at oklch 12-18% works too) | + +In dark mode, depth comes from surface lightness, not shadow. Build a 3-step surface scale where higher elevations are lighter (e.g. 15% / 20% / 25% lightness). Use the SAME hue and chroma as your brand color (whatever it is for THIS project; do not reach for blue) and only vary the lightness. Reduce body text weight slightly (e.g. 350 instead of 400) because light text on dark reads as heavier than dark text on light. + +##### Token Hierarchy + +Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer; primitives stay the same. + +#### Alpha Is A Design Smell + +Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed. + +--- + +**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Skipping color blindness testing (8% of men affected). diff --git a/.rovodev/skills/impeccable/reference/craft.md b/.rovodev/skills/impeccable/reference/craft.md index 06094b1dd..2c7bd99b1 100644 --- a/.rovodev/skills/impeccable/reference/craft.md +++ b/.rovodev/skills/impeccable/reference/craft.md @@ -56,15 +56,15 @@ If the harness has native image generation (Codex), a compact shape's "confirm o Based on the design brief's "Recommended References" section, consult the relevant impeccable reference files. At minimum, always consult: -- [spatial-design.md](spatial-design.md) for layout and spacing -- [typography.md](typography.md) for type hierarchy +- [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 [motion-design.md](motion-design.md) -- Color-heavy or themed? Consult [color-and-contrast.md](color-and-contrast.md) -- Responsive requirements? Consult [responsive-design.md](responsive-design.md) -- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.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) diff --git a/.rovodev/skills/impeccable/reference/critique.md b/.rovodev/skills/impeccable/reference/critique.md index 58403b260..84bdaf000 100644 --- a/.rovodev/skills/impeccable/reference/critique.md +++ b/.rovodev/skills/impeccable/reference/critique.md @@ -38,9 +38,9 @@ Read relevant source files and visually inspect the live page when browser autom Evaluate: - **AI slop**: Would someone believe "AI made this" immediately? Check all DON'T guidance from the parent Impeccable skill. - **Holistic design**: hierarchy, IA, emotional fit, discoverability, composition, typography, color, accessibility, states, copy, and edge cases. -- **Cognitive load**: consult [cognitive-load](cognitive-load.md); report checklist failures and decision points with >4 visible options. +- **Cognitive load**: consult the [Cognitive Load Assessment](#cognitive-load-assessment) section below; report checklist failures and decision points with >4 visible options. - **Emotional journey**: peak-end rule, emotional valleys, reassurance at high-stakes moments. -- **Nielsen heuristics**: consult [heuristics-scoring](heuristics-scoring.md); score all 10 heuristics 0-4. +- **Nielsen heuristics**: consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below; score all 10 heuristics 0-4. Return: AI slop verdict, heuristic scores, cognitive load, emotional journey, 2-3 strengths, 3-5 priority issues, persona red flags, minor observations, and provocative questions. @@ -80,7 +80,7 @@ The chat response is the primary user-facing deliverable. Present the full struc Structure your feedback as a design director would: #### Design Health Score -> *Consult [heuristics-scoring](heuristics-scoring.md)* +> *Consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below.* Present the Nielsen's 10 heuristics scores as a table: @@ -119,16 +119,16 @@ Highlight 2-3 things done well. Be specific about why they work. #### Priority Issues The 3-5 most impactful design problems, ordered by importance. -For each issue, tag with **P0-P3 severity** (consult [heuristics-scoring](heuristics-scoring.md) for severity definitions): +For each issue, tag with **P0-P3 severity** (see [Issue Severity below](#issue-severity-p0p3) for definitions): - **[P?] What**: Name the problem clearly - **Why it matters**: How this hurts users or undermines goals - **Fix**: What to do about it (be concrete) - **Suggested command**: Which command could address this (from: /impeccable adapt, /impeccable animate, /impeccable audit, /impeccable bolder, /impeccable clarify, /impeccable colorize, /impeccable critique, /impeccable delight, /impeccable distill, /impeccable document, /impeccable harden, /impeccable layout, /impeccable onboard, /impeccable optimize, /impeccable overdrive, /impeccable polish, /impeccable quieter, /impeccable shape, /impeccable typeset) #### Persona Red Flags -> *Consult [personas](personas.md)* +> *Consult the [Personas reference](#persona-based-design-testing) below.* -Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `AGENTS.md` contains a `## Design Context` section from `impeccable teach`, also generate 1-2 project-specific personas from the audience/brand info. +Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `AGENTS.md` contains a `## Design Context` section from `impeccable init`, also generate 1-2 project-specific personas from the audience/brand info. For each selected persona, walk through the primary user action and list specific red flags found: @@ -234,3 +234,534 @@ After presenting the summary, tell the user: > You can ask me to run these one at a time, all at once, or in any order you prefer. > > Re-run `/impeccable critique` after fixes to see your score improve. + +--- + +## Reference Material + +The sections below were previously separate reference files (`cognitive-load.md`, `heuristics-scoring.md`, `personas.md`). They live inline now so the critique flow has all its deep context in one place. + +### Cognitive Load Assessment + +Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload. + +--- + +#### Three Types of Cognitive Load + +##### Intrinsic Load: The Task Itself +Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it. + +**Manage it by**: +- Breaking complex tasks into discrete steps +- Providing scaffolding (templates, defaults, examples) +- Progressive disclosure: show what's needed now, hide the rest +- Grouping related decisions together + +##### Extraneous Load: Bad Design +Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste. + +**Common sources**: +- Confusing navigation that requires mental mapping +- Unclear labels that force users to guess meaning +- Visual clutter competing for attention +- Inconsistent patterns that prevent learning +- Unnecessary steps between user intent and result + +##### Germane Load: Learning Effort +Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery. + +**Support it by**: +- Progressive disclosure that reveals complexity gradually +- Consistent patterns that reward learning +- Feedback that confirms correct understanding +- Onboarding that teaches through action, not walls of text + +--- + +#### Cognitive Load Checklist + +Evaluate the interface against these 8 items: + +- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements? +- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)? +- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)? +- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen? +- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next? +- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)? +- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one? +- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it? + +**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed). + +--- + +#### The Working Memory Rule + +**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001). + +At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider: +- **≤4 items**: Within working memory limits, manageable +- **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure +- **8+ items**: Overloaded; users will skip, misclick, or abandon + +**Practical applications**: +- Navigation menus: ≤5 top-level items (group the rest under clear categories) +- Form sections: ≤4 fields visible per group before a visual break +- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu +- Dashboard widgets: ≤4 key metrics visible without scrolling +- Pricing tiers: ≤3 options (more causes analysis paralysis) + +--- + +#### Common Cognitive Load Violations + +##### 1. The Wall of Options +**Problem**: Presenting 10+ choices at once with no hierarchy. +**Fix**: Group into categories, highlight recommended, use progressive disclosure. + +##### 2. The Memory Bridge +**Problem**: User must remember info from step 1 to complete step 3. +**Fix**: Keep relevant context visible, or repeat it where it's needed. + +##### 3. The Hidden Navigation +**Problem**: User must build a mental map of where things are. +**Fix**: Always show current location (breadcrumbs, active states, progress indicators). + +##### 4. The Jargon Barrier +**Problem**: Technical or domain language forces translation effort. +**Fix**: Use plain language. If domain terms are unavoidable, define them inline. + +##### 5. The Visual Noise Floor +**Problem**: Every element has the same visual weight; nothing stands out. +**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted. + +##### 6. The Inconsistent Pattern +**Problem**: Similar actions work differently in different places. +**Fix**: Standardize interaction patterns. Same type of action = same type of UI. + +##### 7. The Multi-Task Demand +**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating). +**Fix**: Sequence the steps. Let the user do one thing at a time. + +##### 8. The Context Switch +**Problem**: User must jump between screens/tabs/modals to gather info for a single decision. +**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth. + +--- + +### Heuristics Scoring Guide + +Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough." + +#### Nielsen's 10 Heuristics + +##### 1. Visibility of System Status + +Keep users informed about what's happening through timely, appropriate feedback. + +**Check for**: +- Loading indicators during async operations +- Confirmation of user actions (save, submit, delete) +- Progress indicators for multi-step processes +- Current location in navigation (breadcrumbs, active states) +- Form validation feedback (inline, not just on submit) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | No feedback; user is guessing what happened | +| 1 | Rare feedback; most actions produce no visible response | +| 2 | Partial; some states communicated, major gaps remain | +| 3 | Good; most operations give clear feedback, minor gaps | +| 4 | Excellent; every action confirms, progress is always visible | + +##### 2. Match Between System and Real World + +Speak the user's language. Follow real-world conventions. Information appears in natural, logical order. + +**Check for**: +- Familiar terminology (no unexplained jargon) +- Logical information order matching user expectations +- Recognizable icons and metaphors +- Domain-appropriate language for the target audience +- Natural reading flow (left-to-right, top-to-bottom priority) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Pure tech jargon, alien to users | +| 1 | Mostly confusing; requires domain expertise to navigate | +| 2 | Mixed; some plain language, some jargon leaks through | +| 3 | Mostly natural; occasional term needs context | +| 4 | Speaks the user's language fluently throughout | + +##### 3. User Control and Freedom + +Users need a clear "emergency exit" from unwanted states without extended dialogue. + +**Check for**: +- Undo/redo functionality +- Cancel buttons on forms and modals +- Clear navigation back to safety (home, previous) +- Easy way to clear filters, search, selections +- Escape from long or multi-step processes + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Users get trapped; no way out without refreshing | +| 1 | Difficult exits; must find obscure paths to escape | +| 2 | Some exits; main flows have escape, edge cases don't | +| 3 | Good control; users can exit and undo most actions | +| 4 | Full control; undo, cancel, back, and escape everywhere | + +##### 4. Consistency and Standards + +Users shouldn't wonder whether different words, situations, or actions mean the same thing. + +**Check for**: +- Consistent terminology throughout the interface +- Same actions produce same results everywhere +- Platform conventions followed (standard UI patterns) +- Visual consistency (colors, typography, spacing, components) +- Consistent interaction patterns (same gesture = same behavior) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Inconsistent everywhere; feels like different products stitched together | +| 1 | Many inconsistencies; similar things look/behave differently | +| 2 | Partially consistent; main flows match, details diverge | +| 3 | Mostly consistent; occasional deviation, nothing confusing | +| 4 | Fully consistent; cohesive system, predictable behavior | + +##### 5. Error Prevention + +Better than good error messages is a design that prevents problems in the first place. + +**Check for**: +- Confirmation before destructive actions (delete, overwrite) +- Constraints preventing invalid input (date pickers, dropdowns) +- Smart defaults that reduce errors +- Clear labels that prevent misunderstanding +- Autosave and draft recovery + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Errors easy to make; no guardrails anywhere | +| 1 | Few safeguards; some inputs validated, most aren't | +| 2 | Partial prevention; common errors caught, edge cases slip | +| 3 | Good prevention; most error paths blocked proactively | +| 4 | Excellent; errors nearly impossible through smart constraints | + +##### 6. Recognition Rather Than Recall + +Minimize memory load. Make objects, actions, and options visible or easily retrievable. + +**Check for**: +- Visible options (not buried in hidden menus) +- Contextual help when needed (tooltips, inline hints) +- Recent items and history +- Autocomplete and suggestions +- Labels on icons (not icon-only navigation) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Heavy memorization; users must remember paths and commands | +| 1 | Mostly recall; many hidden features, few visible cues | +| 2 | Some aids; main actions visible, secondary features hidden | +| 3 | Good recognition; most things discoverable, few memory demands | +| 4 | Everything discoverable; users never need to memorize | + +##### 7. Flexibility and Efficiency of Use + +Accelerators, invisible to novices, speed up expert interaction. + +**Check for**: +- Keyboard shortcuts for common actions +- Customizable interface elements +- Recent items and favorites +- Bulk/batch actions +- Power user features that don't complicate the basics + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | One rigid path; no shortcuts or alternatives | +| 1 | Limited flexibility; few alternatives to the main path | +| 2 | Some shortcuts; basic keyboard support, limited bulk actions | +| 3 | Good accelerators; keyboard nav, some customization | +| 4 | Highly flexible; multiple paths, power features, customizable | + +##### 8. Aesthetic and Minimalist Design + +Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose. + +**Check for**: +- Only necessary information visible at each step +- Clear visual hierarchy directing attention +- Purposeful use of color and emphasis +- No decorative clutter competing for attention +- Focused, uncluttered layouts + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Overwhelming; everything competes for attention equally | +| 1 | Cluttered; too much noise, hard to find what matters | +| 2 | Some clutter; main content clear, periphery noisy | +| 3 | Mostly clean; focused design, minor visual noise | +| 4 | Perfectly minimal; every element earns its pixel | + +##### 9. Help Users Recognize, Diagnose, and Recover from Errors + +Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution. + +**Check for**: +- Plain language error messages (no error codes for users) +- Specific problem identification ("Email is missing @" not "Invalid input") +- Actionable recovery suggestions +- Errors displayed near the source of the problem +- Non-blocking error handling (don't wipe the form) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Cryptic errors; codes, jargon, or no message at all | +| 1 | Vague errors; "Something went wrong" with no guidance | +| 2 | Clear but unhelpful; names the problem but not the fix | +| 3 | Clear with suggestions; identifies problem and offers next steps | +| 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work | + +##### 10. Help and Documentation + +Even if the system is usable without docs, help should be easy to find, task-focused, and concise. + +**Check for**: +- Searchable help or documentation +- Contextual help (tooltips, inline hints, guided tours) +- Task-focused organization (not feature-organized) +- Concise, scannable content +- Easy access without leaving current context + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | No help available anywhere | +| 1 | Help exists but hard to find or irrelevant | +| 2 | Basic help; FAQ or docs exist, not contextual | +| 3 | Good documentation; searchable, mostly task-focused | +| 4 | Excellent contextual help; right info at the right moment | + +--- + +#### Score Summary + +**Total possible**: 40 points (10 heuristics × 4 max) + +| Score Range | Rating | What It Means | +|-------------|--------|---------------| +| 36–40 | Excellent | Minor polish only; ship it | +| 28–35 | Good | Address weak areas, solid foundation | +| 20–27 | Acceptable | Significant improvements needed before users are happy | +| 12–19 | Poor | Major UX overhaul required; core experience broken | +| 0–11 | Critical | Redesign needed; unusable in current state | + +--- + +#### Issue Severity (P0–P3) + +Tag each individual issue found during scoring with a priority level: + +| Priority | Name | Description | Action | +|----------|------|-------------|--------| +| **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper | +| **P1** | Major | Causes significant difficulty or confusion | Fix before release | +| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass | +| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits | + +**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1. + +--- + +### Persona-Based Design Testing + +Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss. + +**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns. + +--- + +#### 1. Impatient Power User: "Alex" + +**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave. + +**Behaviors**: +- Skips all onboarding and instructions +- Looks for keyboard shortcuts immediately +- Tries to bulk-select, batch-edit, and automate +- Gets frustrated by required steps that feel unnecessary +- Abandons if anything feels slow or patronizing + +**Test Questions**: +- Can Alex complete the core task in under 60 seconds? +- Are there keyboard shortcuts for common actions? +- Can onboarding be skipped entirely? +- Do modals have keyboard dismiss (Esc)? +- Is there a "power user" path (shortcuts, bulk actions)? + +**Red Flags** (report these specifically): +- Forced tutorials or unskippable onboarding +- No keyboard navigation for primary actions +- Slow animations that can't be skipped +- One-item-at-a-time workflows where batch would be natural +- Redundant confirmation steps for low-risk actions + +--- + +#### 2. Confused First-Timer: "Jordan" + +**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out. + +**Behaviors**: +- Reads all instructions carefully +- Hesitates before clicking anything unfamiliar +- Looks for help or support constantly +- Misunderstands jargon and abbreviations +- Takes the most literal interpretation of any label + +**Test Questions**: +- Is the first action obviously clear within 5 seconds? +- Are all icons labeled with text? +- Is there contextual help at decision points? +- Does terminology assume prior knowledge? +- Is there a clear "back" or "undo" at every step? + +**Red Flags** (report these specifically): +- Icon-only navigation with no labels +- Technical jargon without explanation +- No visible help option or guidance +- Ambiguous next steps after completing an action +- No confirmation that an action succeeded + +--- + +#### 3. Accessibility-Dependent User: "Sam" + +**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences. + +**Behaviors**: +- Tabs through the interface linearly +- Relies on ARIA labels and heading structure +- Cannot see hover states or visual-only indicators +- Needs adequate color contrast (4.5:1 minimum) +- May use browser zoom up to 200% + +**Test Questions**: +- Can the entire primary flow be completed keyboard-only? +- Are all interactive elements focusable with visible focus indicators? +- Do images have meaningful alt text? +- Is color contrast WCAG AA compliant (4.5:1 for text)? +- Does the screen reader announce state changes (loading, success, errors)? + +**Red Flags** (report these specifically): +- Click-only interactions with no keyboard alternative +- Missing or invisible focus indicators +- Meaning conveyed by color alone (red = error, green = success) +- Unlabeled form fields or buttons +- Time-limited actions without extension option +- Custom components that break screen reader flow + +--- + +#### 4. Deliberate Stress Tester: "Riley" + +**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience. + +**Behaviors**: +- Tests edge cases intentionally (empty states, long strings, special characters) +- Submits forms with unexpected data (emoji, RTL text, very long values) +- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs +- Looks for inconsistencies between what the UI promises and what actually happens +- Documents problems methodically + +**Test Questions**: +- What happens at the edges (0 items, 1000 items, very long text)? +- Do error states recover gracefully or leave the UI in a broken state? +- What happens on refresh mid-workflow? Is state preserved? +- Are there features that appear to work but produce broken results? +- How does the UI handle unexpected input (emoji, special chars, paste from Excel)? + +**Red Flags** (report these specifically): +- Features that appear to work but silently fail or produce wrong results +- Error handling that exposes technical details or leaves UI in a broken state +- Empty states that show nothing useful ("No results" with no guidance) +- Workflows that lose user data on refresh or navigation +- Inconsistent behavior between similar interactions in different parts of the UI + +--- + +#### 5. Distracted Mobile User: "Casey" + +**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection. + +**Behaviors**: +- Uses thumb only; prefers bottom-of-screen actions +- Gets interrupted mid-flow and returns later +- Switches between apps frequently +- Has limited attention span and low patience +- Types as little as possible, prefers taps and selections + +**Test Questions**: +- Are primary actions in the thumb zone (bottom half of screen)? +- Is state preserved if the user leaves and returns? +- Does it work on slow connections (3G)? +- Can forms use autocomplete and smart defaults? +- Are touch targets at least 44×44pt? + +**Red Flags** (report these specifically): +- Important actions positioned at the top of the screen (unreachable by thumb) +- No state persistence; progress lost on tab switch or interruption +- Large text inputs required where selection would work +- Heavy assets loading on every page (no lazy loading) +- Tiny tap targets or targets too close together + +--- + +#### Selecting Personas + +Choose personas based on the interface type: + +| Interface Type | Primary Personas | Why | +|---------------|-----------------|-----| +| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile | +| Dashboard / admin | Alex, Sam | Power users, accessibility | +| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity | +| Onboarding flow | Jordan, Casey | Confusion, interruption | +| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav | +| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile | + +--- + +#### Project-Specific Personas + +If `AGENTS.md` contains a `## Design Context` section (generated by `impeccable init`), derive 1–2 additional personas from the audience and brand information: + +1. Read the target audience description +2. Identify the primary user archetype not covered by the 5 predefined personas +3. Create a persona following this template: + +``` +##### [Role]: "[Name]" + +**Profile**: [2-3 key characteristics derived from Design Context] + +**Behaviors**: [3-4 specific behaviors based on the described audience] + +**Red Flags**: [3-4 things that would alienate this specific user type] +``` + +Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists. diff --git a/.rovodev/skills/impeccable/reference/document.md b/.rovodev/skills/impeccable/reference/document.md index 16111d610..60075688a 100644 --- a/.rovodev/skills/impeccable/reference/document.md +++ b/.rovodev/skills/impeccable/reference/document.md @@ -45,7 +45,7 @@ Rules that matter: - **Token refs** use `{path.to.token}` (e.g. `{colors.primary}`, `{rounded.md}`). Components may reference primitives; primitives may not reference each other. - **Stitch validates colors as hex sRGB only** (`#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`); OKLCH/HSL/P3 trigger a linter warning, not a hard error. YAML accepts the string either way and our own parser is format-agnostic. Choose based on project posture: (a) if the project has an "OKLCH-only" doctrine or uses Display-P3 values that don't round-trip through sRGB, put OKLCH directly in the frontmatter and accept the Stitch linter warning; (b) if the project wants strict Stitch compliance or plans to use their Tailwind/DTCG export pipeline, put hex in the frontmatter and keep OKLCH in prose as the canonical reference. Never split the source of truth without explicit reason. - **Component sub-tokens** are limited to 8 props: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, `width`. Shadows, motion, focus rings, backdrop-filter: none of those fit. Carry them in the sidecar (Step 4b). -- **Scale keys are open-ended.** Use whatever names the project already uses (`warm-ash-cream`, `surface-container-low`). Don't rename to Material defaults. +- **Scale keys are open-ended.** Use whatever names the project already uses (`oxblood-deep`, `surface-container-low`). Don't rename to Material defaults. - **Variants are naming convention, not schema.** `button-primary` / `button-primary-hover` / `button-primary-active` as sibling keys. ## The markdown body: six sections (exact order) @@ -61,7 +61,7 @@ Optional evocative subtitles are allowed in the form `## 2. Colors: The [Name] P ## When to run -- The user just ran `/impeccable teach` and needs the visual side documented. +- The user just ran `/impeccable init` and needs the visual side documented. - The skill noticed no `DESIGN.md` exists and nudged the user to create one. - An existing `DESIGN.md` is stale (the design has drifted). - Before a large redesign, to capture the current state as a reference. @@ -71,7 +71,7 @@ If a `DESIGN.md` already exists, **do not silently overwrite it**. Show the user ## Two paths - **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 teach, nothing built yet). Interview for five high-level answers, write a minimal DESIGN.md marked ``. Re-run in scan mode once there's code. +- **Seed mode**: the project is pre-implementation (fresh init, nothing built yet). Interview for five high-level answers, 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` forces seed mode regardless of code presence. @@ -103,7 +103,7 @@ Build a structured draft from the discovered tokens. For each token class: From the auto-extracted tokens, draft the YAML frontmatter now (you'll write it at the top of DESIGN.md in Step 4). This is the machine-readable layer: what the live panel and Stitch's linter consume. -- **Colors**: one entry per extracted color. Key = descriptive slug (`warm-ash-cream`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. +- **Colors**: one entry per extracted color. Key = descriptive slug (`oxblood-deep`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. - **Typography**: one entry per role (`display`, `headline`, `title`, `body`, `label`). Typography is an object; include only the props that are real for the project (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`). - **Rounded / Spacing**: whatever scale steps the project actually uses, keyed by whatever scale name the project uses (`sm` / `md` / `lg`, or `surface-sm`, or numeric steps). - **Components**: one entry per variant (`button-primary`, `button-primary-hover`, `button-ghost`). Reference primitives via `{colors.X}`, `{rounded.Y}`. If a variant needs a property Stitch's 8-prop set doesn't cover (shadow, focus ring, backdrop-filter), carry the full snippet in the sidecar instead. @@ -253,7 +253,7 @@ Regenerate the sidecar whenever you regenerate root `DESIGN.md`. If the user onl "extensions": { "colorMeta": { "primary": { "role": "primary", "displayName": "Editorial Magenta", "canonical": "oklch(60% 0.25 350)", "tonalRamp": ["...", "...", "..."] }, - "warm-ash-cream": { "role": "neutral", "displayName": "Warm Ash Cream", "canonical": "oklch(96% 0.005 350)", "tonalRamp": ["...", "...", "..."] } + "cool-paper": { "role": "neutral", "displayName": "Cool Paper", "canonical": "oklch(96% 0.005 230)", "tonalRamp": ["...", "...", "..."] } }, "typographyMeta": { "display": { "displayName": "Display", "purpose": "Hero headlines only." } @@ -328,12 +328,13 @@ Pull directly from the DESIGN.md you just wrote: Do not reword. The panel shows these as secondary collapsible context; the same voice that's in the Markdown carries through. -### Step 5: Confirm, refine, and refresh session cache +### Step 5: Confirm and refine 1. Show the user the full DESIGN.md you wrote. Briefly highlight the non-obvious creative choices (descriptive color names, atmosphere language, named rules). 2. Mention that `.impeccable/design.json` was also written alongside; the live panel will now render this project's actual button/input/nav primitives instead of generic approximations. 3. Offer to refine any section: "Want me to revise a section, add component patterns I missed, or adjust the atmosphere language?" -4. **Refresh the session cache.** Run `node .rovodev/skills/impeccable/scripts/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. + +Your own write is the freshest source; subsequent commands in this session don't need a reload. ## Seed mode @@ -394,11 +395,12 @@ Per-section guidance in seed mode: 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. -### Step 4: Confirm and refresh session cache +### Step 4: Confirm 1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment). 2. Tell the user: "Re-run `/impeccable document` once you have some code. That pass will extract real tokens and generate the sidecar." -3. Run `node .rovodev/skills/impeccable/scripts/load-context.mjs` once so the seed lands in conversation for the rest of the session. + +Your own write is the freshest source; no reload needed. ## Style guidelines diff --git a/.rovodev/skills/impeccable/reference/heuristics-scoring.md b/.rovodev/skills/impeccable/reference/heuristics-scoring.md deleted file mode 100644 index edbe5028d..000000000 --- a/.rovodev/skills/impeccable/reference/heuristics-scoring.md +++ /dev/null @@ -1,234 +0,0 @@ -# Heuristics Scoring Guide - -Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough." - -## Nielsen's 10 Heuristics - -### 1. Visibility of System Status - -Keep users informed about what's happening through timely, appropriate feedback. - -**Check for**: -- Loading indicators during async operations -- Confirmation of user actions (save, submit, delete) -- Progress indicators for multi-step processes -- Current location in navigation (breadcrumbs, active states) -- Form validation feedback (inline, not just on submit) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | No feedback; user is guessing what happened | -| 1 | Rare feedback; most actions produce no visible response | -| 2 | Partial; some states communicated, major gaps remain | -| 3 | Good; most operations give clear feedback, minor gaps | -| 4 | Excellent; every action confirms, progress is always visible | - -### 2. Match Between System and Real World - -Speak the user's language. Follow real-world conventions. Information appears in natural, logical order. - -**Check for**: -- Familiar terminology (no unexplained jargon) -- Logical information order matching user expectations -- Recognizable icons and metaphors -- Domain-appropriate language for the target audience -- Natural reading flow (left-to-right, top-to-bottom priority) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Pure tech jargon, alien to users | -| 1 | Mostly confusing; requires domain expertise to navigate | -| 2 | Mixed; some plain language, some jargon leaks through | -| 3 | Mostly natural; occasional term needs context | -| 4 | Speaks the user's language fluently throughout | - -### 3. User Control and Freedom - -Users need a clear "emergency exit" from unwanted states without extended dialogue. - -**Check for**: -- Undo/redo functionality -- Cancel buttons on forms and modals -- Clear navigation back to safety (home, previous) -- Easy way to clear filters, search, selections -- Escape from long or multi-step processes - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Users get trapped; no way out without refreshing | -| 1 | Difficult exits; must find obscure paths to escape | -| 2 | Some exits; main flows have escape, edge cases don't | -| 3 | Good control; users can exit and undo most actions | -| 4 | Full control; undo, cancel, back, and escape everywhere | - -### 4. Consistency and Standards - -Users shouldn't wonder whether different words, situations, or actions mean the same thing. - -**Check for**: -- Consistent terminology throughout the interface -- Same actions produce same results everywhere -- Platform conventions followed (standard UI patterns) -- Visual consistency (colors, typography, spacing, components) -- Consistent interaction patterns (same gesture = same behavior) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Inconsistent everywhere; feels like different products stitched together | -| 1 | Many inconsistencies; similar things look/behave differently | -| 2 | Partially consistent; main flows match, details diverge | -| 3 | Mostly consistent; occasional deviation, nothing confusing | -| 4 | Fully consistent; cohesive system, predictable behavior | - -### 5. Error Prevention - -Better than good error messages is a design that prevents problems in the first place. - -**Check for**: -- Confirmation before destructive actions (delete, overwrite) -- Constraints preventing invalid input (date pickers, dropdowns) -- Smart defaults that reduce errors -- Clear labels that prevent misunderstanding -- Autosave and draft recovery - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Errors easy to make; no guardrails anywhere | -| 1 | Few safeguards; some inputs validated, most aren't | -| 2 | Partial prevention; common errors caught, edge cases slip | -| 3 | Good prevention; most error paths blocked proactively | -| 4 | Excellent; errors nearly impossible through smart constraints | - -### 6. Recognition Rather Than Recall - -Minimize memory load. Make objects, actions, and options visible or easily retrievable. - -**Check for**: -- Visible options (not buried in hidden menus) -- Contextual help when needed (tooltips, inline hints) -- Recent items and history -- Autocomplete and suggestions -- Labels on icons (not icon-only navigation) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Heavy memorization; users must remember paths and commands | -| 1 | Mostly recall; many hidden features, few visible cues | -| 2 | Some aids; main actions visible, secondary features hidden | -| 3 | Good recognition; most things discoverable, few memory demands | -| 4 | Everything discoverable; users never need to memorize | - -### 7. Flexibility and Efficiency of Use - -Accelerators, invisible to novices, speed up expert interaction. - -**Check for**: -- Keyboard shortcuts for common actions -- Customizable interface elements -- Recent items and favorites -- Bulk/batch actions -- Power user features that don't complicate the basics - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | One rigid path; no shortcuts or alternatives | -| 1 | Limited flexibility; few alternatives to the main path | -| 2 | Some shortcuts; basic keyboard support, limited bulk actions | -| 3 | Good accelerators; keyboard nav, some customization | -| 4 | Highly flexible; multiple paths, power features, customizable | - -### 8. Aesthetic and Minimalist Design - -Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose. - -**Check for**: -- Only necessary information visible at each step -- Clear visual hierarchy directing attention -- Purposeful use of color and emphasis -- No decorative clutter competing for attention -- Focused, uncluttered layouts - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Overwhelming; everything competes for attention equally | -| 1 | Cluttered; too much noise, hard to find what matters | -| 2 | Some clutter; main content clear, periphery noisy | -| 3 | Mostly clean; focused design, minor visual noise | -| 4 | Perfectly minimal; every element earns its pixel | - -### 9. Help Users Recognize, Diagnose, and Recover from Errors - -Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution. - -**Check for**: -- Plain language error messages (no error codes for users) -- Specific problem identification ("Email is missing @" not "Invalid input") -- Actionable recovery suggestions -- Errors displayed near the source of the problem -- Non-blocking error handling (don't wipe the form) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Cryptic errors; codes, jargon, or no message at all | -| 1 | Vague errors; "Something went wrong" with no guidance | -| 2 | Clear but unhelpful; names the problem but not the fix | -| 3 | Clear with suggestions; identifies problem and offers next steps | -| 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work | - -### 10. Help and Documentation - -Even if the system is usable without docs, help should be easy to find, task-focused, and concise. - -**Check for**: -- Searchable help or documentation -- Contextual help (tooltips, inline hints, guided tours) -- Task-focused organization (not feature-organized) -- Concise, scannable content -- Easy access without leaving current context - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | No help available anywhere | -| 1 | Help exists but hard to find or irrelevant | -| 2 | Basic help; FAQ or docs exist, not contextual | -| 3 | Good documentation; searchable, mostly task-focused | -| 4 | Excellent contextual help; right info at the right moment | - ---- - -## Score Summary - -**Total possible**: 40 points (10 heuristics × 4 max) - -| Score Range | Rating | What It Means | -|-------------|--------|---------------| -| 36–40 | Excellent | Minor polish only; ship it | -| 28–35 | Good | Address weak areas, solid foundation | -| 20–27 | Acceptable | Significant improvements needed before users are happy | -| 12–19 | Poor | Major UX overhaul required; core experience broken | -| 0–11 | Critical | Redesign needed; unusable in current state | - ---- - -## Issue Severity (P0–P3) - -Tag each individual issue found during scoring with a priority level: - -| Priority | Name | Description | Action | -|----------|------|-------------|--------| -| **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper | -| **P1** | Major | Causes significant difficulty or confusion | Fix before release | -| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass | -| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits | - -**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1. diff --git a/.qoder/skills/impeccable/reference/teach.md b/.rovodev/skills/impeccable/reference/init.md similarity index 58% rename from .qoder/skills/impeccable/reference/teach.md rename to .rovodev/skills/impeccable/reference/init.md index 2fa563e97..70a7c5e34 100644 --- a/.qoder/skills/impeccable/reference/teach.md +++ b/.rovodev/skills/impeccable/reference/init.md @@ -1,21 +1,16 @@ -# Teach Flow +# Init Flow -Gathers design context for a project and writes two complementary files at the project root: +The setup command for a project. One codebase crawl feeds everything it writes: - **PRODUCT.md** (strategic): root project file for register, target users, product purpose, brand personality, anti-references, strategic design principles. Answers "who/what/why". - **DESIGN.md** (visual): root project file for visual theme, color palette, typography, components, layout. Follows the [Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/). Answers "how it looks". +- **`.impeccable/live/config.json`** (live mode): pre-configured so `/impeccable live` boots straight into variant mode with no first-time detour. -Every other impeccable command reads these files before doing any work. +It closes by pointing the user at the best command to run next. Every other impeccable command reads PRODUCT.md and DESIGN.md before doing any work. ## Step 1: Load current state -Run the shared loader first so you know what already exists: - -```bash -node .qoder/skills/impeccable/scripts/load-context.mjs -``` - -The output tells you whether PRODUCT.md and/or DESIGN.md already exist. If `migrated: true`, legacy `.impeccable.md` was auto-renamed to `PRODUCT.md`. Mention this once to the user. +Check what already exists. PRODUCT.md and DESIGN.md live at the project root, or under `.agents/context/` or `docs/` (case-insensitive). Read whichever are present with your native file tool. Also note whether `.impeccable/live/config.json` already exists (Step 6 leaves it untouched if so). Decision tree: - **Neither file exists (empty project or no context yet)**: do Steps 2-4 (write PRODUCT.md), then decide on DESIGN.md based on whether there's code to analyze. @@ -26,14 +21,14 @@ Decision tree: Never silently overwrite an existing file. Always confirm first. -If teach was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete teach, re-run the loader, then resume the original command with the freshly loaded context. For craft, resume into shape next; teach creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief. +If init was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete init, then resume the original command. Your own writes are the freshest source; no reload needed. For craft, resume into shape next; init creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief. ## Step 2: Explore the codebase -Before asking questions, thoroughly scan the project to discover what you can: +Before asking questions, thoroughly scan the project to discover what you can. This single crawl feeds PRODUCT.md, DESIGN.md, **and** the live-mode framework detection in Step 6, so be thorough once rather than re-scanning later: - **README and docs**: Project purpose, target audience, any stated goals -- **Package.json / config files**: Tech stack, dependencies, existing design libraries +- **Package.json / config files**: Tech stack, dependencies, existing design libraries, **and the framework** (Vite/SPA, Next.js, Nuxt, SvelteKit, Astro, multi-page static) plus the HTML entry the browser actually loads - **Existing components**: Current design patterns, spacing, typography in use - **Brand assets**: Logos, favicons, color values already defined - **Design tokens / CSS variables**: Existing color palettes, font stacks, spacing scales @@ -46,7 +41,7 @@ Also form a **register hypothesis** from what you find: Register is a hypothesis at this point, not a decision; Step 3 confirms it. -Note what you've learned and what remains unclear. This exploration feeds both PRODUCT.md and DESIGN.md. +Note what you've learned and what remains unclear. Also note any rough edges worth a follow-up command (thin hierarchy, flat or gray palette, missing error/empty states, dull copy); Step 7 turns these into concrete recommendations without re-analyzing. ## Step 3: Ask strategic questions (for PRODUCT.md) @@ -84,8 +79,7 @@ If the signal is genuinely split (e.g. a product with a big marketing landing), ### Brand & Personality - How would you describe the brand personality in 3 words? - Reference sites or apps that capture the right feel? What specifically about them? - - For brand, push for real-world references in the right lane (tech-minimal, editorial-magazine, consumer-warm, brutalist-grid, etc.), not generic "modern" adjectives. - - For product, push for category best-tool references (Linear, Figma, Notion, Raycast, Stripe). + - Push for specific named references with the *specific* thing about them that fits this brand, not generic "modern" adjectives or category-bucket lanes. - What should this explicitly NOT look like? Any anti-references? ### Accessibility & Inclusion @@ -141,16 +135,38 @@ If the user agrees, delegate to `/impeccable document` (it auto-detects scan vs If the user prefers to skip, mention they can run `/impeccable document` any time later. -## Step 6: Confirm and wrap up +## Step 6: Configure live mode (when code exists) -Summarize: +If the project has code with HTML entries and a dev server (the same "code exists" condition that puts `/impeccable document` in scan mode), pre-configure live mode now. You already identified the framework and the served HTML entry in Step 2, so this is nearly free, and it spares the user the first-time setup detour when they later run `/impeccable live`. + +**Skip this step for empty / pre-implementation projects** (nothing to inject into yet). Tell the user live mode will configure itself the first time they run it once there's code. + +**If `.impeccable/live/config.json` already exists, leave it untouched** and note that live mode is already configured. + +Otherwise: + +1. Write `.impeccable/live/config.json`. Choose `files` (the HTML entries the browser actually loads), `insertBefore`, and `commentSyntax` from the framework table in [live.md](live.md)'s **First-time setup** section, using the framework you found in Step 2. That table is canonical; do not restate it here. For multi-page static sites, prefer a glob (`["public/**/*.html"]`) over a literal list. +2. Run `node .rovodev/skills/impeccable/scripts/detect-csp.mjs`. If it reports a patchable shape (`append-arrays` / `append-string`), use the **consent prompt template** from live.md before editing any source file. On decline, skip the patch. For `middleware` / `meta-tag` shapes, surface the detected files and ask the user to add `http://localhost:8400` to `script-src` and `connect-src` manually. For `null`, there's nothing to do. +3. Set `cspChecked: true` in the config once CSP is handled (patched, declined, manual, or not needed). The schema and per-shape patch details live in live.md's First-time setup; follow it rather than duplicating. + +Writing the config file is harmless and needs no consent; only the CSP **source-file patch** requires a yes. + +## Step 7: Recommend starting points, then wrap up + +Summarize tersely: - Register captured (brand / product) -- What was written (PRODUCT.md, DESIGN.md, or both) +- What was written (PRODUCT.md, DESIGN.md, live config, or a subset) - The 3-5 strategic principles from PRODUCT.md that will guide future work -- If DESIGN.md is pending, remind the user how to generate it later +- If DESIGN.md or live config is pending, one line on how to set it up later -**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node .qoder/skills/impeccable/scripts/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. +Then recommend the **best commands to run next**, drawn from what your Step 2 crawl already surfaced. Do not run a fresh analysis here; surface observations you already have. Tailor to register and to what you saw, offer the 2-4 most relevant (not a menu dump), and give the exact command to type. Group by intent: -If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context. +- **Build something new**: `/impeccable craft ` (shape, then build end-to-end) or `/impeccable shape ` (plan first). Lead with this for empty or early-stage projects. +- **Improve what's there**: name the specific surface. `/impeccable critique ` for a scored UX review; `/impeccable audit ` for a11y / perf / responsive checks; `/impeccable polish ` for a pre-ship pass. When the crawl flagged a specific weakness, point the matching command at it: thin hierarchy or spacing → `layout`, flat or gray palette → `colorize`, missing error / empty states → `harden` or `onboard`, dull or unclear copy → `clarify`. +- **Iterate visually**: `/impeccable live` (configured in Step 6) to pick elements in the browser and generate variants in place. + +The full command menu is one bare `/impeccable` away; keep this list short and pointed. + +If init was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now. Your own writes are the freshest source; no reload needed. Optionally ask the user directly to clarify what you cannot infer. Ask whether they'd like a brief summary of PRODUCT.md appended to AGENTS.md for easier agent reference. If yes, append a short **Design Context** pointer section there. diff --git a/.rovodev/skills/impeccable/reference/interaction-design.md b/.rovodev/skills/impeccable/reference/interaction-design.md index 15aed5b29..c42c9d810 100644 --- a/.rovodev/skills/impeccable/reference/interaction-design.md +++ b/.rovodev/skills/impeccable/reference/interaction-design.md @@ -150,12 +150,6 @@ For browsers without anchor positioning support, `position: fixed` with manual c Check viewport boundaries before rendering. If the dropdown would overflow the bottom edge, flip it above the trigger. If it would overflow the right edge, align it to the trigger's right side instead. -### Anti-Patterns - -- **`position: absolute` inside `overflow: hidden`** - The dropdown will be clipped. Use `position: fixed` or the top layer instead. -- **Arbitrary z-index values** like `z-index: 9999` - Use a semantic z-index scale: `dropdown (100) -> sticky (200) -> modal-backdrop (300) -> modal (400) -> toast (500) -> tooltip (600)`. -- **Rendering dropdown markup inline** without an escape hatch from the parent's stacking context. Either use `popover` (top layer), a portal, or `position: fixed`. - ## Destructive Actions: Undo > Confirm **Undo is better than confirmation dialogs.** Users click through confirmations mindlessly. Remove from UI immediately, show undo toast, actually delete after toast expires. Use confirmation only for truly irreversible actions (account deletion), high-cost actions, or batch operations. diff --git a/.rovodev/skills/impeccable/reference/layout.md b/.rovodev/skills/impeccable/reference/layout.md index cad4c62a1..8765978b0 100644 --- a/.rovodev/skills/impeccable/reference/layout.md +++ b/.rovodev/skills/impeccable/reference/layout.md @@ -1,4 +1,4 @@ -Space is the most underused design tool. Find the layout's actual problem (monotone spacing, weak hierarchy, identical card grids, the centered-stack default) and fix the structure, not the surface. +Space is the most underused design tool. Find the layout's actual problem (monotone spacing, weak hierarchy, identical card grids) and fix the structure, not the surface. --- @@ -27,7 +27,6 @@ Analyze what's weak about the current spatial design: 3. **Grid & structure**: - Is there a clear underlying structure, or does the layout feel random? - Are identical card grids used everywhere? (Icon + heading + text, repeated endlessly) - - Is everything centered? (Left-aligned with asymmetric layouts feels more designed, but not a hard and fast rule) 4. **Rhythm & variety**: - Does the layout have visual rhythm? (Alternating tight/generous spacing) @@ -43,8 +42,6 @@ Analyze what's weak about the current spatial design: ## Plan Layout Improvements -Consult the [spatial design reference](spatial-design.md) for detailed guidance on grids, rhythm, and container queries. - Create a systematic plan: - **Spacing system**: Use a consistent scale (a framework's built-in scale like Tailwind's, rem-based tokens, or a custom system). The specific values matter less than consistency. @@ -57,6 +54,7 @@ Create a systematic plan: ### Establish a Spacing System - Use a consistent spacing scale (framework scales like Tailwind, rem-based tokens, or a custom scale all work). What matters is that values come from a defined set, not arbitrary numbers. +- Prefer a 4pt base scale (4, 8, 12, 16, 24, 32, 48, 64, 96px) over 8pt; 8pt is too coarse and you'll frequently need 12px between 8 and 16. - Name tokens semantically if using custom properties: `--space-xs` through `--space-xl`, not `--spacing-8` - Use `gap` for sibling spacing instead of margins; eliminates margin collapse hacks - Apply `clamp()` for fluid spacing that breathes on larger screens @@ -66,15 +64,22 @@ Create a systematic plan: - **Tight grouping** for related elements (8-12px between siblings) - **Generous separation** between distinct sections (48-96px) - **Varied spacing** within sections (not every row needs the same gap) -- **Asymmetric compositions**: break the predictable centered-content pattern when it makes sense +- **Asymmetric compositions**: a deliberate choice when the content invites it (not a default to chase). ### Choose the Right Layout Tool -- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. Flex is simpler and more appropriate for the majority of layout tasks. +- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. - **Use Grid for 2D layouts**: Page-level structure, dashboards, data-dense interfaces, anything where rows AND columns need coordinated control. -- **Don't default to Grid** when Flexbox with `flex-wrap` would be simpler and more flexible. -- Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. - Use named grid areas (`grid-template-areas`) for complex page layouts; redefine at breakpoints. +- Use **container queries** for components, viewport queries for page layouts. A card in a narrow sidebar can stay compact while the same card in a main content area expands automatically: + +```css +.card-container { container-type: inline-size; } +.card { display: grid; gap: var(--space-md); } +@container (min-width: 400px) { + .card { grid-template-columns: 120px 1fr; } +} +``` ### Break Card Grid Monotony @@ -85,18 +90,36 @@ Create a systematic plan: ### Strengthen Visual Hierarchy - Use the fewest dimensions needed for clear hierarchy. Space alone can be enough; generous whitespace around an element draws the eye. Some of the most polished designs achieve rhythm with just space and weight. Add color or size contrast only when simpler means aren't sufficient. +- The best hierarchy combines 2–3 dimensions at once. A heading that's larger, bolder, AND has more space above it reads as primary without trying: + +| Tool | Strong Hierarchy | Weak Hierarchy | +|------|------------------|----------------| +| **Size** | 3:1 ratio or more | <2:1 ratio | +| **Weight** | Bold vs Regular | Medium vs Regular | +| **Color** | High contrast | Similar tones | +| **Position** | Top/left (primary) | Bottom/right | +| **Space** | Surrounded by white space | Crowded | + - Be aware of reading flow: in LTR languages, the eye naturally scans top-left to bottom-right, but primary action placement depends on context (e.g., bottom-right in dialogs, top in navigation). - Create clear content groupings through proximity and separation. ### Manage Depth & Elevation -- Create a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip) - Build a consistent shadow scale (sm → md → lg → xl); shadows should be subtle - Use elevation to reinforce hierarchy, not as decoration ### Optical Adjustments - If an icon looks visually off-center despite being geometrically centered, nudge it. But only if you're confident it actually looks wrong. Don't adjust speculatively. +- Text at `margin-left: 0` looks slightly indented because of letterform whitespace; a negative margin (`-0.05em`) optically aligns it. Geometrically centered glyphs often look off-center (play icons need to shift right, arrows shift toward their direction). +- Touch targets must be 44×44px minimum even when the visual element is smaller. Expand the hit area with padding or a pseudo-element: + +```css +.icon-button { width: 24px; height: 24px; position: relative; } +.icon-button::before { + content: ''; position: absolute; inset: -10px; +} +``` **NEVER**: - Use arbitrary spacing values outside your scale @@ -104,10 +127,7 @@ Create a systematic plan: - Wrap everything in cards (not everything needs a container) - Nest cards inside cards (use spacing and dividers for hierarchy within) - Use identical card grids everywhere (icon + heading + text, repeated) -- Center everything (left-aligned with asymmetry feels more designed) - Default to the hero metric layout (big number, small label, stats, gradient) as a template. If showing real user data, a prominent metric can work, but it should display actual data, not decorative numbers. -- Default to CSS Grid when Flexbox would be simpler; use the simplest tool for the job -- Use arbitrary z-index values (999, 9999); build a semantic scale ## Verify Layout Improvements diff --git a/.rovodev/skills/impeccable/reference/live.md b/.rovodev/skills/impeccable/reference/live.md index a7ddf0f30..1015a31c9 100644 --- a/.rovodev/skills/impeccable/reference/live.md +++ b/.rovodev/skills/impeccable/reference/live.md @@ -9,18 +9,21 @@ A running dev server with hot module replacement (Vite, Next.js, Bun, etc.), OR Execute in order. No step skipped, no step reordered. 1. `live.mjs`: boot. -2. Navigate to the URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). If you can't infer it confidently, tell the user once to open their dev/preview URL. Never use `serverPort` as that URL; it's the helper, not the app. +2. Open the app URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). Never use `serverPort`; it's the helper, not the app. **Cursor:** `browser_navigate` to that URL before polling; do not skip. **Other harnesses:** use the available browser tool; if the URL is uncertain, ask the user once. 3. Poll loop with the default long timeout (600000 ms). After every event or `--reply`, run `live-poll.mjs` again immediately. Never pass a short `--timeout=`. + +The global bar **Impeccable mark** dims and shows a pulsing amber dot when no agent is long-polling `/poll`. Hover the mark for the hint; restart `live-poll.mjs` to reconnect. 4. On `generate`: read screenshot if present; load the action's reference; plan three distinct directions; write all variants in one edit; `--reply done`; poll again. -5. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts remain recoverable until you finish cleanup, run `live-complete.mjs --id EVENT_ID`, and only then poll again. -6. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart. -7. On `exit`: run the cleanup at the bottom. +5. On `steer`: read the message and `pageUrl`; do the work (page edits, navigation help, or a short reply in the `--reply` message); `--reply steer_done`; poll again. No pickup ack. The Steer bar unlocks when `steer_done` arrives over SSE. +6. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts remain recoverable until you finish cleanup, run `live-complete.mjs --id EVENT_ID`, and only then poll again. +7. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart. +8. On `exit`: run the cleanup at the bottom. Harness policy: - **Claude Code**: run the poll as a **background task** (no short timeout). The harness notifies you when it completes, so the main conversation stays free. Do not block the shell. -- **Cursor**: run the poll in the **foreground** (blocking shell; not a background terminal, not a subagent). Cursor background terminals and subagents do not reliably resume the chat with poll stdout. +- **Cursor**: run **one-shot** poll in a **background terminal** with notify on `"type":"(steer|generate|accept|discard|exit)"`. After each event the poll exits; handle it, `--reply`, then start `live-poll.mjs` again. Do **not** use `--stream` on Cursor: incremental stdout notify is slower in practice than exit-based notify (~5s vs sub-second in testing). - **Codex**: run the poll in the **foreground** (blocking shell; not a background task, not a subagent). Codex background exec sessions do not reliably surface poll stdout back into the conversation at the moment events arrive, so a "fire-and-forget" background poll will stall live mode. -- **Other harnesses**: foreground unless you know stdout reliably returns to this session. +- **Other harnesses**: one-shot foreground unless you know stdout reliably returns to this session when a shell exits. Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodies. Spend tokens on tools and edits; on failure, one or two short sentences. @@ -30,7 +33,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi node .rovodev/skills/impeccable/scripts/live.mjs ``` -Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation; **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md. +Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation; **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. `serverPort` and `serverToken` belong to the small **Impeccable live helper** HTTP server (serves `/live.js`, SSE, and `/poll`). That port is **not** your dev server and is usually not the URL you open to view the app. The browser page is whatever origin serves one of the `pageFiles` entries (Vite / Next / Bun / tunnel / LAN hostname). @@ -38,12 +41,15 @@ If output is `{ ok: false, error: "config_missing" | "config_invalid", path }`, ## Poll loop +**Default (portable, all harnesses):** + ``` LOOP: node .rovodev/skills/impeccable/scripts/live-poll.mjs # default long timeout; no --timeout= Read JSON; dispatch on "type" "generate" → Handle Generate; reply done; LOOP + "steer" → Handle Steer; reply steer_done; LOOP "accept" → Handle Accept; complete carbonize cleanup if required; LOOP "discard" → Handle Discard; LOOP "prefetch" → Handle Prefetch; LOOP @@ -51,6 +57,16 @@ LOOP: "exit" → break → Cleanup ``` +**Stream mode (experimental, not for Cursor):** + +``` +node .rovodev/skills/impeccable/scripts/live-poll.mjs --stream # stays running; one JSON line per event + Handle event; run --reply in a separate command + Repeat until "exit" line → Cleanup +``` + +Stream keeps one process alive and waits for `--reply` ack before polling again. Useful only when the harness reads incremental stdout reliably and quickly. **Cursor is not one of those:** background pattern notify on a long-running shell was ~5s to pick up events vs sub-second for one-shot exit notify. Default to one-shot everywhere unless you have measured otherwise. + ## Recovery commands The live helper persists an append-only journal under `.impeccable/live/sessions/`. Browser checkpoints are advisory but durable; the journal is canonical. This is local durable recovery state, not project source. @@ -71,9 +87,32 @@ Server restart rule: start `live-server.mjs` again, then poll. Startup requeues ## Handle `generate` -Event: `{id, action, freeformPrompt?, count, pageUrl, element, screenshotPath?, comments?, strokes?}`. +**Replace mode** (default): `{id, action, freeformPrompt?, count, pageUrl, element, screenshotPath?, comments?, strokes?}`. -Speed matters; the user is watching a spinner. Minimize tool calls by using the `wrap` helper and writing all variants in a single edit. +**Insert mode** (`event.mode === "insert"`): `{id, mode: "insert", count, pageUrl, insert: { position, anchor }, placeholder: { width, height }, freeformPrompt?, screenshotPath?, comments?, strokes?}`. No `action`. Requires a non-empty `freeformPrompt` **or** annotations. Screenshot is sent only when annotations exist (same rule as replace). Use `placeholder` dimensions as a soft size hint for net-new content. + +Speed matters; the user is watching a spinner. Minimize tool calls by using the wrap/insert helper and writing all variants in a single edit. + +### Insert mode branch + +When `event.mode === "insert"`: + +1. Read the screenshot if `event.screenshotPath` is present (annotations only). +2. Run the insert helper instead of wrap: + +```bash +node .rovodev/skills/impeccable/scripts/live-insert.mjs --id EVENT_ID --count EVENT_COUNT --position after \ + --element-id "ANCHOR_ID" --classes "class1,class2" --tag "section" --text "ANCHOR_TEXT" +``` + +- `--position` ← `event.insert.position` (`before` | `after`) +- Anchor flags ← `event.insert.anchor` (same mapping as wrap: id, classes, tag, text) + +The scaffold has **no** `data-impeccable-variant="original"`. Variants are net-new HTML+CSS inserted at `insertLine`. Load `brand.md` or `product.md` (freeform only, no action sub-command). Write all variants in one edit, then `--reply done`. + +On accept/discard, `live-accept.mjs` removes the wrapper block; the anchor element is untouched. + +### Replace mode (default) ### 1. Read the screenshot (if present) @@ -424,6 +463,28 @@ A background agent may be used for the rewrite, but the current thread is respon Event: `{id, _acceptResult, _completionAck}`. The poll script already restored the original, removed all variant markers, and acknowledged `discarded` durable completion. Nothing to do unless `_completionAck.ok !== true`; in that case run `live-complete.mjs --id EVENT_ID --discarded`, then poll again. +## Handle `steer` + +Event: `{id, message, pageUrl}`. The user typed or spoke into the global bar **Steer** control: page-level direction without picking an element or launching variant generation. + +The mic button uses the browser **Web Speech API** (MVP): click to start, speak, stop automatically when the utterance ends, then the transcript submits as a steer event. Click again while listening to cancel without submitting. + +This is lighter than `generate`: no screenshot, no element context, no variant cycling. Read `message` and inspect the live page or project files as needed, then either make edits or answer in prose. + +When finished: + +```bash +node .rovodev/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID steer_done ["Optional short note for a browser toast"] +``` + +On failure: + +```bash +node .rovodev/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID error "Short reason" +``` + +Then poll again immediately. Do not send a separate "picked up" reply. The Steer bar stays locked until `steer_done` or `error` arrives over SSE. + ## Handle `prefetch` Event: `{pageUrl}`. The browser fires this the first time the user selects an element on a given route, as a latency shortcut; it signals the user is likely about to Go on a page you haven't read yet. diff --git a/.rovodev/skills/impeccable/reference/motion-design.md b/.rovodev/skills/impeccable/reference/motion-design.md deleted file mode 100644 index 78b6fd260..000000000 --- a/.rovodev/skills/impeccable/reference/motion-design.md +++ /dev/null @@ -1,109 +0,0 @@ -# Motion Design - -## Duration: The 100/300/500 Rule - -Timing matters more than easing. These durations feel right for most UI: - -| Duration | Use Case | Examples | -|----------|----------|----------| -| **100-150ms** | Instant feedback | Button press, toggle, color change | -| **200-300ms** | State changes | Menu open, tooltip, hover states | -| **300-500ms** | Layout changes | Accordion, modal, drawer | -| **500-800ms** | Entrance animations | Page load, hero reveals | - -**Exit animations are faster than entrances.** Use ~75% of enter duration. - -## Easing: Pick the Right Curve - -**Don't use `ease`.** It's a compromise that's rarely optimal. Instead: - -| Curve | Use For | CSS | -|-------|---------|-----| -| **ease-out** | Elements entering | `cubic-bezier(0.16, 1, 0.3, 1)` | -| **ease-in** | Elements leaving | `cubic-bezier(0.7, 0, 0.84, 0)` | -| **ease-in-out** | State toggles (there → back) | `cubic-bezier(0.65, 0, 0.35, 1)` | - -**For micro-interactions, use exponential curves.** They feel natural because they mimic real physics (friction, deceleration): - -```css -/* Quart out - smooth, refined (recommended default) */ ---ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1); - -/* Quint out - slightly more dramatic */ ---ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1); - -/* Expo out - snappy, confident */ ---ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); -``` - -**Avoid bounce and elastic curves.** They were trendy in 2015 but now feel tacky and amateurish. Real objects don't bounce when they stop; they decelerate smoothly. Overshoot effects draw attention to the animation itself rather than the content. - -## Premium Motion Materials - -Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties: blur reveals, backdrop-filter panels, saturation or brightness shifts, shadow bloom, SVG filters, masks, clip paths, gradient-position movement, and variable font or shader-driven effects. - -Use the right material for the effect: - -- **Transform / opacity**: movement, press feedback, simple reveals, list choreography. -- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances, atmospheric transitions. -- **Clip path / masks**: wipes, reveals, editorial cropping, product-like transitions. -- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state. -- **Grid-template rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly. - -The hard rule is not "transform and opacity only." The hard rule is: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify in-browser that the result is smooth on the target viewports. If blur/filter makes the interaction feel significantly more premium and remains smooth, use it. - -## Staggered Animations - -Use CSS custom properties for cleaner stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"` on each item. **Cap total stagger time**: 10 items at 50ms = 500ms total. For many items, reduce per-item delay or cap staggered count. - -## Reduced Motion - -This is not optional. Vestibular disorders affect ~35% of adults over 40. - -```css -/* Define animations normally */ -.card { - animation: slide-up 500ms ease-out; -} - -/* Provide alternative for reduced motion */ -@media (prefers-reduced-motion: reduce) { - .card { - animation: fade-in 200ms ease-out; /* Crossfade instead of motion */ - } -} - -/* Or disable entirely */ -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - transition-duration: 0.01ms !important; - } -} -``` - -**What to preserve**: Functional animations like progress bars, loading spinners (slowed down), and focus indicators should still work, just without spatial movement. - -## Perceived Performance - -**Nobody cares how fast your site is, just how fast it feels.** Perception can be as effective as actual performance. - -**The 80ms threshold**: Our brains buffer sensory input for ~80ms to synchronize perception. Anything under 80ms feels instant and simultaneous. This is your target for micro-interactions. - -**Active vs passive time**: Passive waiting (staring at a spinner) feels longer than active engagement. Strategies to shift the balance: - -- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening. -- **Early completion**: Show content progressively, don't wait for everything. Video buffering, progressive images, streaming HTML. -- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Instagram likes work offline; the UI updates instantly, syncs later. Use for low-stakes actions; avoid for payments or destructive operations. - -**Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances, but ease-in toward a task's end compresses perceived time. - -**Caution**: Too-fast responses can decrease perceived value. Users may distrust instant results for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening. - -## Performance - -Don't use `will-change` preemptively, only when animation is imminent (`:hover`, `.animating`). For scroll-triggered animations, use Intersection Observer instead of scroll events; unobserve after animating once. Create motion tokens for consistency (durations, easings, common transitions). - ---- - -**Avoid**: Animating everything (animation fatigue is real). Using >500ms for UI feedback. Ignoring `prefers-reduced-motion`. Using animation to hide slow loading. diff --git a/.rovodev/skills/impeccable/reference/personas.md b/.rovodev/skills/impeccable/reference/personas.md deleted file mode 100644 index 43ae8eb2f..000000000 --- a/.rovodev/skills/impeccable/reference/personas.md +++ /dev/null @@ -1,179 +0,0 @@ -# Persona-Based Design Testing - -Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss. - -**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns. - ---- - -## 1. Impatient Power User: "Alex" - - -**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave. - -**Behaviors**: -- Skips all onboarding and instructions -- Looks for keyboard shortcuts immediately -- Tries to bulk-select, batch-edit, and automate -- Gets frustrated by required steps that feel unnecessary -- Abandons if anything feels slow or patronizing - -**Test Questions**: -- Can Alex complete the core task in under 60 seconds? -- Are there keyboard shortcuts for common actions? -- Can onboarding be skipped entirely? -- Do modals have keyboard dismiss (Esc)? -- Is there a "power user" path (shortcuts, bulk actions)? - -**Red Flags** (report these specifically): -- Forced tutorials or unskippable onboarding -- No keyboard navigation for primary actions -- Slow animations that can't be skipped -- One-item-at-a-time workflows where batch would be natural -- Redundant confirmation steps for low-risk actions - ---- - -## 2. Confused First-Timer: "Jordan" - -**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out. - -**Behaviors**: -- Reads all instructions carefully -- Hesitates before clicking anything unfamiliar -- Looks for help or support constantly -- Misunderstands jargon and abbreviations -- Takes the most literal interpretation of any label - -**Test Questions**: -- Is the first action obviously clear within 5 seconds? -- Are all icons labeled with text? -- Is there contextual help at decision points? -- Does terminology assume prior knowledge? -- Is there a clear "back" or "undo" at every step? - -**Red Flags** (report these specifically): -- Icon-only navigation with no labels -- Technical jargon without explanation -- No visible help option or guidance -- Ambiguous next steps after completing an action -- No confirmation that an action succeeded - ---- - -## 3. Accessibility-Dependent User: "Sam" - -**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences. - -**Behaviors**: -- Tabs through the interface linearly -- Relies on ARIA labels and heading structure -- Cannot see hover states or visual-only indicators -- Needs adequate color contrast (4.5:1 minimum) -- May use browser zoom up to 200% - -**Test Questions**: -- Can the entire primary flow be completed keyboard-only? -- Are all interactive elements focusable with visible focus indicators? -- Do images have meaningful alt text? -- Is color contrast WCAG AA compliant (4.5:1 for text)? -- Does the screen reader announce state changes (loading, success, errors)? - -**Red Flags** (report these specifically): -- Click-only interactions with no keyboard alternative -- Missing or invisible focus indicators -- Meaning conveyed by color alone (red = error, green = success) -- Unlabeled form fields or buttons -- Time-limited actions without extension option -- Custom components that break screen reader flow - ---- - -## 4. Deliberate Stress Tester: "Riley" - -**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience. - -**Behaviors**: -- Tests edge cases intentionally (empty states, long strings, special characters) -- Submits forms with unexpected data (emoji, RTL text, very long values) -- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs -- Looks for inconsistencies between what the UI promises and what actually happens -- Documents problems methodically - -**Test Questions**: -- What happens at the edges (0 items, 1000 items, very long text)? -- Do error states recover gracefully or leave the UI in a broken state? -- What happens on refresh mid-workflow? Is state preserved? -- Are there features that appear to work but produce broken results? -- How does the UI handle unexpected input (emoji, special chars, paste from Excel)? - -**Red Flags** (report these specifically): -- Features that appear to work but silently fail or produce wrong results -- Error handling that exposes technical details or leaves UI in a broken state -- Empty states that show nothing useful ("No results" with no guidance) -- Workflows that lose user data on refresh or navigation -- Inconsistent behavior between similar interactions in different parts of the UI - ---- - -## 5. Distracted Mobile User: "Casey" - -**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection. - -**Behaviors**: -- Uses thumb only; prefers bottom-of-screen actions -- Gets interrupted mid-flow and returns later -- Switches between apps frequently -- Has limited attention span and low patience -- Types as little as possible, prefers taps and selections - -**Test Questions**: -- Are primary actions in the thumb zone (bottom half of screen)? -- Is state preserved if the user leaves and returns? -- Does it work on slow connections (3G)? -- Can forms use autocomplete and smart defaults? -- Are touch targets at least 44×44pt? - -**Red Flags** (report these specifically): -- Important actions positioned at the top of the screen (unreachable by thumb) -- No state persistence; progress lost on tab switch or interruption -- Large text inputs required where selection would work -- Heavy assets loading on every page (no lazy loading) -- Tiny tap targets or targets too close together - ---- - -## Selecting Personas - -Choose personas based on the interface type: - -| Interface Type | Primary Personas | Why | -|---------------|-----------------|-----| -| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile | -| Dashboard / admin | Alex, Sam | Power users, accessibility | -| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity | -| Onboarding flow | Jordan, Casey | Confusion, interruption | -| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav | -| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile | - ---- - -## Project-Specific Personas - -If `AGENTS.md` contains a `## Design Context` section (generated by `impeccable teach`), derive 1–2 additional personas from the audience and brand information: - -1. Read the target audience description -2. Identify the primary user archetype not covered by the 5 predefined personas -3. Create a persona following this template: - -``` -### [Role]: "[Name]" - -**Profile**: [2-3 key characteristics derived from Design Context] - -**Behaviors**: [3-4 specific behaviors based on the described audience] - -**Red Flags**: [3-4 things that would alienate this specific user type] -``` - -Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists. diff --git a/.rovodev/skills/impeccable/reference/polish.md b/.rovodev/skills/impeccable/reference/polish.md index debc5f096..9b7f6af2c 100644 --- a/.rovodev/skills/impeccable/reference/polish.md +++ b/.rovodev/skills/impeccable/reference/polish.md @@ -91,7 +91,6 @@ Visual polish on a misshapen flow is wasted work. Match the *shape* of the exper - **Theme consistency**: Works in all theme variants - **Color meaning**: Same colors mean same things throughout - **Accessible focus**: Focus indicators visible with sufficient contrast -- **Tinted neutrals**: No pure gray or pure black; add subtle color tint (0.01 chroma) - **Gray on color**: Never put gray text on colored backgrounds; use a shade of that color or transparency ### Interaction States diff --git a/.rovodev/skills/impeccable/reference/product.md b/.rovodev/skills/impeccable/reference/product.md index 64b3b0169..2ca462350 100644 --- a/.rovodev/skills/impeccable/reference/product.md +++ b/.rovodev/skills/impeccable/reference/product.md @@ -10,7 +10,6 @@ Product UI's failure mode isn't flatness, it's strangeness without purpose: over ## Typography -- **System fonts are legitimate.** `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif` gives you native feel on every platform. Inter is the common cross-platform default for a reason. - **One family is often right.** Product UIs don't need display/body pairing. A well-tuned sans carries headings, buttons, labels, body, data. - **Fixed rem scale, not fluid.** Clamp-sized headings don't serve product UI. Users view at consistent DPI, and a fluid h1 that shrinks in a sidebar looks worse, not better. - **Tighter scale ratio.** 1.125–1.2 between steps is typical. More type elements here than on brand surfaces; exaggerated contrast creates noise. @@ -26,8 +25,6 @@ Product defaults to Restrained. A single surface can earn Committed (a dashboard ## Layout -- Predictable grids. Consistency IS an affordance; users navigate faster when the structure is expected. -- Familiar patterns are features. Standard navigation (top bar, side nav), breadcrumbs, tabs, and form layouts have established user expectations. Don't reinvent for flavor. - Responsive behavior is structural (collapse sidebar, responsive table, breakpoint-driven columns), not fluid typography. ## Components @@ -51,6 +48,7 @@ Every interactive component has: default, hover, focus, active, disabled, loadin - Display fonts in UI labels, buttons, data. - Reinventing standard affordances for flavor (custom scrollbars, weird form controls, non-standard modals). - Heavy color or full-saturation accents on inactive states. +- Modal as first thought. Modals are usually laziness. Exhaust inline / progressive alternatives first. ## Product permissions diff --git a/.rovodev/skills/impeccable/reference/responsive-design.md b/.rovodev/skills/impeccable/reference/responsive-design.md deleted file mode 100644 index f079e5e5d..000000000 --- a/.rovodev/skills/impeccable/reference/responsive-design.md +++ /dev/null @@ -1,114 +0,0 @@ -# Responsive Design - -## Mobile-First: Write It Right - -Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first. - -## Breakpoints: Content-Driven - -Don't chase device sizes; let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints. - -## Detect Input Method, Not Just Screen Size - -**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard. Use pointer and hover queries: - -```css -/* Fine pointer (mouse, trackpad) */ -@media (pointer: fine) { - .button { padding: 8px 16px; } -} - -/* Coarse pointer (touch, stylus) */ -@media (pointer: coarse) { - .button { padding: 12px 20px; } /* Larger touch target */ -} - -/* Device supports hover */ -@media (hover: hover) { - .card:hover { transform: translateY(-2px); } -} - -/* Device doesn't support hover (touch) */ -@media (hover: none) { - .card { /* No hover state - use active instead */ } -} -``` - -**Critical**: Don't rely on hover for functionality. Touch users can't hover. - -## Safe Areas: Handle the Notch - -Modern phones have notches, rounded corners, and home indicators. Use `env()`: - -```css -body { - padding-top: env(safe-area-inset-top); - padding-bottom: env(safe-area-inset-bottom); - padding-left: env(safe-area-inset-left); - padding-right: env(safe-area-inset-right); -} - -/* With fallback */ -.footer { - padding-bottom: max(1rem, env(safe-area-inset-bottom)); -} -``` - -**Enable viewport-fit** in your meta tag: -```html - -``` - -## Responsive Images: Get It Right - -### srcset with Width Descriptors - -```html -Hero image -``` - -**How it works**: -- `srcset` lists available images with their actual widths (`w` descriptors) -- `sizes` tells the browser how wide the image will display -- Browser picks the best file based on viewport width AND device pixel ratio - -### Picture Element for Art Direction - -When you need different crops/compositions (not just resolutions): - -```html - - - - ... - -``` - -## Layout Adaptation Patterns - -**Navigation**: Three stages: hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `
/` for content that can collapse on mobile. - -## Testing: Don't Trust DevTools Alone - -DevTools device emulation is useful for layout but misses: - -- Actual touch interactions -- Real CPU/memory constraints -- Network latency patterns -- Font rendering differences -- Browser chrome/keyboard appearances - -**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators. - ---- - -**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful. diff --git a/.rovodev/skills/impeccable/reference/shape.md b/.rovodev/skills/impeccable/reference/shape.md index f7c4cd2f1..592def8a7 100644 --- a/.rovodev/skills/impeccable/reference/shape.md +++ b/.rovodev/skills/impeccable/reference/shape.md @@ -151,7 +151,7 @@ How users interact with this feature. What happens on click, hover, scroll? What 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., spatial-design.md for complex layouts, motion-design.md for animated features, interaction-design.md for form-heavy features). +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. diff --git a/.rovodev/skills/impeccable/reference/spatial-design.md b/.rovodev/skills/impeccable/reference/spatial-design.md deleted file mode 100644 index 4d4b83afc..000000000 --- a/.rovodev/skills/impeccable/reference/spatial-design.md +++ /dev/null @@ -1,100 +0,0 @@ -# Spatial Design - -## Spacing Systems - -### Use 4pt Base, Not 8pt - -8pt systems are too coarse; you'll frequently need 12px (between 8 and 16). Use 4pt for granularity: 4, 8, 12, 16, 24, 32, 48, 64, 96px. - -### Name Tokens Semantically - -Name by relationship (`--space-sm`, `--space-lg`), not value (`--spacing-8`). Use `gap` instead of margins for sibling spacing; it eliminates margin collapse and cleanup hacks. - -## Grid Systems - -### The Self-Adjusting Grid - -Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. Columns are at least 280px, as many as fit per row, leftovers stretch. For complex layouts, use named grid areas (`grid-template-areas`) and redefine them at breakpoints. - -## Visual Hierarchy - -### The Squint Test - -Blur your eyes (or screenshot and blur). Can you still identify: -- The most important element? -- The second most important? -- Clear groupings? - -If everything looks the same weight blurred, you have a hierarchy problem. - -### Hierarchy Through Multiple Dimensions - -Don't rely on size alone. Combine: - -| Tool | Strong Hierarchy | Weak Hierarchy | -|------|------------------|----------------| -| **Size** | 3:1 ratio or more | <2:1 ratio | -| **Weight** | Bold vs Regular | Medium vs Regular | -| **Color** | High contrast | Similar tones | -| **Position** | Top/left (primary) | Bottom/right | -| **Space** | Surrounded by white space | Crowded | - -**The best hierarchy uses 2-3 dimensions at once**: A heading that's larger, bolder, AND has more space above it. - -### Cards Are Not Required - -Cards are overused. Spacing and alignment create visual grouping naturally. Use cards only when content is truly distinct and actionable, items need visual comparison in a grid, or content needs clear interaction boundaries. **Never nest cards inside cards.** Use spacing, typography, and subtle dividers for hierarchy within a card. - -## Container Queries - -Viewport queries are for page layouts. **Container queries are for components**: - -```css -.card-container { - container-type: inline-size; -} - -.card { - display: grid; - gap: var(--space-md); -} - -/* Card layout changes based on its container, not viewport */ -@container (min-width: 400px) { - .card { - grid-template-columns: 120px 1fr; - } -} -``` - -**Why this matters**: A card in a narrow sidebar stays compact, while the same card in a main content area expands automatically, without viewport hacks. - -## Optical Adjustments - -Text at `margin-left: 0` looks indented due to letterform whitespace; use negative margin (`-0.05em`) to optically align. Geometrically centered icons often look off-center; play icons need to shift right, arrows shift toward their direction. - -### Touch Targets vs Visual Size - -Buttons can look small but need large touch targets (44px minimum). Use padding or pseudo-elements: - -```css -.icon-button { - width: 24px; /* Visual size */ - height: 24px; - position: relative; -} - -.icon-button::before { - content: ''; - position: absolute; - inset: -10px; /* Expand tap target to 44px */ -} -``` - -## Depth & Elevation - -Create semantic z-index scales (dropdown → sticky → modal-backdrop → modal → toast → tooltip) instead of arbitrary numbers. For shadows, create a consistent elevation scale (sm → md → lg → xl). **Key insight**: Shadows should be subtle. If you can clearly see it, it's probably too strong. - ---- - -**Avoid**: Arbitrary spacing values outside your scale. Making all spacing equal (variety creates hierarchy). Creating hierarchy through size alone - combine size, weight, color, and space. diff --git a/.rovodev/skills/impeccable/reference/typeset.md b/.rovodev/skills/impeccable/reference/typeset.md index dd3e67ea3..5f5e0c2c0 100644 --- a/.rovodev/skills/impeccable/reference/typeset.md +++ b/.rovodev/skills/impeccable/reference/typeset.md @@ -4,7 +4,7 @@ Typography carries most of the information on the page. Replace generic defaults ## Register -Brand: run the font selection procedure in [brand.md](brand.md). Pairing follows the brand's lane (display serif + sans body for editorial/luxury, one committed sans for tech, etc.). Fluid `clamp()` scale, ≥1.25 ratio between steps. +Brand: run the font selection procedure in [brand.md](brand.md). Fluid `clamp()` scale, ≥1.25 ratio between steps. Product: system fonts and familiar sans stacks are legitimate here. One well-tuned family typically carries the whole UI. Fixed `rem` scale, 1.125–1.2 ratio between more closely-spaced steps. @@ -43,7 +43,7 @@ Analyze what's weak or generic about the current type: ## Plan Typography Improvements -Consult the [typography reference](typography.md) for detailed guidance on scales, pairing, and loading strategies. +Consult the [Reference Material](#reference-material) section below for detailed guidance on scales, pairing, and loading strategies. Create a systematic plan: @@ -122,3 +122,158 @@ Each variant MUST declare a `scale` param controlling the hierarchy ratio. Expre Where the variant riffs on a specific pairing, expose the pairing choice as a `steps` param (e.g. "serif display + sans body" vs. "mono display + sans body" vs. "all-sans"). Each branch routes through `:scope[data-p-pairing="X"]` selectors in scoped CSS. See `reference/live.md` for the full params contract. + +--- + +## Reference Material + +The sections below were previously `typography.md` and live inline now so the typeset flow has its deep typography reference in one place. `bolder.md` also references this section. + +### Typography + +#### Classic Typography Principles + +##### Vertical Rhythm + +Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony; text and space share a mathematical foundation. + +##### Modular Scale & Hierarchy + +The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy. + +**Use fewer sizes with more contrast.** A 5-size system covers most needs: + +| Role | Typical Ratio | Use Case | +|------|---------------|----------| +| xs | 0.75rem | Captions, legal | +| sm | 0.875rem | Secondary UI, metadata | +| base | 1rem | Body text | +| lg | 1.25-1.5rem | Subheadings, lead text | +| xl+ | 2-4rem | Headlines, hero text | + +Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit. + +##### Readability & Measure + +Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length: narrow columns need tighter leading, wide columns need more. + +**Non-obvious**: Light text on dark backgrounds needs compensation on three axes, not just one. Bump line-height by 0.05–0.1, add a touch of letter-spacing (0.01–0.02em), and optionally step the body weight up one notch (regular → medium). The perceived weight drops across all three; fix all three. + +**Paragraph rhythm**: Pick either space between paragraphs OR first-line indentation. Never both. Digital usually wants space; editorial/long-form can justify indent-only. + +#### Font Selection & Pairing + +The tactical selection procedure and the reflex-reject list live in [reference/brand.md](brand.md) under **Font selection procedure** and **Reflex-reject list** (loaded for brand-register tasks). The rest of this section covers the adjacent knowledge: anti-reflex corrections, system font use, and pairing rules. + +##### Anti-reflexes worth defending against + +- A technical/utilitarian brief does NOT need a serif "for warmth." Most tech tools should look like tech tools. +- An editorial/premium brief does NOT need the same expressive serif everyone is using right now. Premium can be Swiss-modern, can be neo-grotesque, can be a literal monospace, can be a quiet humanist sans. +- A children's product does NOT need a rounded display font. Kids' books use real type. +- A "modern" brief does NOT need a geometric sans. The most modern thing you can do is not use the font everyone else is using. + +**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality. + +##### Pairing Principles + +**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif). + +When pairing, contrast on multiple axes: +- Serif + Sans (structure contrast) +- Geometric + Humanist (personality contrast) +- Condensed display + Wide body (proportion contrast) + +##### Web Font Loading + +The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix: + +```css +/* 1. Use font-display: swap for visibility */ +@font-face { + font-family: 'CustomFont'; + src: url('font.woff2') format('woff2'); + font-display: swap; +} + +/* 2. Match fallback metrics to minimize shift */ +@font-face { + font-family: 'CustomFont-Fallback'; + src: local('Arial'); + size-adjust: 105%; /* Scale to match x-height */ + ascent-override: 90%; /* Match ascender height */ + descent-override: 20%; /* Match descender depth */ + line-gap-override: 10%; /* Match line spacing */ +} + +body { + font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; +} +``` + +Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically. + +**`swap` vs `optional`**: `swap` shows fallback text immediately and FOUT-swaps when the web font arrives. `optional` uses the fallback if the web font misses a small load budget (~100ms) and avoids the shift entirely. Pick `optional` when zero layout shift matters more than seeing the branded font on slow networks. + +**Preload the critical weight only**: typically the regular-weight body font used above the fold. Preloading every weight costs more bandwidth than it saves. + +**Variable fonts for 3+ weights or styles**: a single variable font file is usually smaller than three static weight files, gives fractional weight control, and pairs well with `font-optical-sizing: auto`. For 1–2 weights, static is fine. + +#### Modern Web Typography + +##### Fluid Type + +Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate (higher vw = faster scaling). Add a rem offset so it doesn't collapse to 0 on small screens. + +**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes. + +**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI; fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it. + +**Bound your clamp()**: keep `max-size ≤ ~2.5 × min-size`. Wider ratios break the browser's zoom and reflow behaviour and make large viewports feel like the page is shouting. + +**Scale container width and font-size together** so effective character measure stays in the 45–75ch band at every viewport. A heading that widens faster than its container drifts out of the comfortable measure at the top end. + +##### OpenType Features + +Most developers don't know these exist. Use them for polish: + +```css +/* Proper fractions */ +.recipe-amount { font-variant-numeric: diagonal-fractions; } + +/* Small caps for abbreviations */ +abbr { font-variant-caps: all-small-caps; } + +/* Disable ligatures in code */ +code { font-variant-ligatures: none; } + +/* Enable kerning (usually on by default, but be explicit) */ +body { font-kerning: normal; } +``` + +Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/). + +##### Rendering polish + +```css +/* Variable fonts: pick the right optical-size master automatically */ +body { font-optical-sizing: auto; } +``` + +**ALL-CAPS tracking**: capitals sit too close at default spacing. Add 5–12% letter-spacing (`letter-spacing: 0.05em` to `0.12em`) to short all-caps labels, eyebrows, and small headings. Real small caps (via `font-variant-caps`) need the same treatment, slightly gentler. + +#### Typography System Architecture + +Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system. + +#### Accessibility Considerations + +Beyond contrast ratios (which are well-documented), consider: + +- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout. +- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text. +- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile. +- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets. + +--- + +**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text. diff --git a/.rovodev/skills/impeccable/reference/typography.md b/.rovodev/skills/impeccable/reference/typography.md deleted file mode 100644 index ed5ca272a..000000000 --- a/.rovodev/skills/impeccable/reference/typography.md +++ /dev/null @@ -1,159 +0,0 @@ -# Typography - -## Classic Typography Principles - -### Vertical Rhythm - -Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony; text and space share a mathematical foundation. - -### Modular Scale & Hierarchy - -The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy. - -**Use fewer sizes with more contrast.** A 5-size system covers most needs: - -| Role | Typical Ratio | Use Case | -|------|---------------|----------| -| xs | 0.75rem | Captions, legal | -| sm | 0.875rem | Secondary UI, metadata | -| base | 1rem | Body text | -| lg | 1.25-1.5rem | Subheadings, lead text | -| xl+ | 2-4rem | Headlines, hero text | - -Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit. - -### Readability & Measure - -Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length: narrow columns need tighter leading, wide columns need more. - -**Non-obvious**: Light text on dark backgrounds needs compensation on three axes, not just one. Bump line-height by 0.05–0.1, add a touch of letter-spacing (0.01–0.02em), and optionally step the body weight up one notch (regular → medium). The perceived weight drops across all three; fix all three. - -**Paragraph rhythm**: Pick either space between paragraphs OR first-line indentation. Never both. Digital usually wants space; editorial/long-form can justify indent-only. - -## Font Selection & Pairing - -The tactical selection procedure and the reflex-reject list live in [reference/brand.md](brand.md) under **Font selection procedure** and **Reflex-reject list** (loaded for brand-register tasks). The rest of this section covers the adjacent knowledge: anti-reflex corrections, system font use, and pairing rules. - -### Anti-reflexes worth defending against - -- A technical/utilitarian brief does NOT need a serif "for warmth." Most tech tools should look like tech tools. -- An editorial/premium brief does NOT need the same expressive serif everyone is using right now. Premium can be Swiss-modern, can be neo-grotesque, can be a literal monospace, can be a quiet humanist sans. -- A children's product does NOT need a rounded display font. Kids' books use real type. -- A "modern" brief does NOT need a geometric sans. The most modern thing you can do is not use the font everyone else is using. - -**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality. - -### Pairing Principles - -**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif). - -When pairing, contrast on multiple axes: -- Serif + Sans (structure contrast) -- Geometric + Humanist (personality contrast) -- Condensed display + Wide body (proportion contrast) - -**Never pair fonts that are similar but not identical** (e.g., two geometric sans-serifs). They create visual tension without clear hierarchy. - -### Web Font Loading - -The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix: - -```css -/* 1. Use font-display: swap for visibility */ -@font-face { - font-family: 'CustomFont'; - src: url('font.woff2') format('woff2'); - font-display: swap; -} - -/* 2. Match fallback metrics to minimize shift */ -@font-face { - font-family: 'CustomFont-Fallback'; - src: local('Arial'); - size-adjust: 105%; /* Scale to match x-height */ - ascent-override: 90%; /* Match ascender height */ - descent-override: 20%; /* Match descender depth */ - line-gap-override: 10%; /* Match line spacing */ -} - -body { - font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; -} -``` - -Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically. - -**`swap` vs `optional`**: `swap` shows fallback text immediately and FOUT-swaps when the web font arrives. `optional` uses the fallback if the web font misses a small load budget (~100ms) and avoids the shift entirely. Pick `optional` when zero layout shift matters more than seeing the branded font on slow networks. - -**Preload the critical weight only**: typically the regular-weight body font used above the fold. Preloading every weight costs more bandwidth than it saves. - -**Variable fonts for 3+ weights or styles**: a single variable font file is usually smaller than three static weight files, gives fractional weight control, and pairs well with `font-optical-sizing: auto`. For 1–2 weights, static is fine. - -## Modern Web Typography - -### Fluid Type - -Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate (higher vw = faster scaling). Add a rem offset so it doesn't collapse to 0 on small screens. - -**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes. - -**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI; fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it. - -**Bound your clamp()**: keep `max-size ≤ ~2.5 × min-size`. Wider ratios break the browser's zoom and reflow behaviour and make large viewports feel like the page is shouting. - -**Scale container width and font-size together** so effective character measure stays in the 45–75ch band at every viewport. A heading that widens faster than its container drifts out of the comfortable measure at the top end. - -### OpenType Features - -Most developers don't know these exist. Use them for polish: - -```css -/* Tabular numbers for data alignment */ -.data-table { font-variant-numeric: tabular-nums; } - -/* Proper fractions */ -.recipe-amount { font-variant-numeric: diagonal-fractions; } - -/* Small caps for abbreviations */ -abbr { font-variant-caps: all-small-caps; } - -/* Disable ligatures in code */ -code { font-variant-ligatures: none; } - -/* Enable kerning (usually on by default, but be explicit) */ -body { font-kerning: normal; } -``` - -Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/). - -### Rendering polish - -```css -/* Even out heading line lengths (browser picks better break points) */ -h1, h2, h3 { text-wrap: balance; } - -/* Reduce orphans and ragged endings in long prose */ -article p { text-wrap: pretty; } - -/* Variable fonts: pick the right optical-size master automatically */ -body { font-optical-sizing: auto; } -``` - -**ALL-CAPS tracking**: capitals sit too close at default spacing. Add 5–12% letter-spacing (`letter-spacing: 0.05em` to `0.12em`) to short all-caps labels, eyebrows, and small headings. Real small caps (via `font-variant-caps`) need the same treatment, slightly gentler. - -## Typography System Architecture - -Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system. - -## Accessibility Considerations - -Beyond contrast ratios (which are well-documented), consider: - -- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout. -- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text. -- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile. -- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets. - ---- - -**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text. diff --git a/.rovodev/skills/impeccable/reference/ux-writing.md b/.rovodev/skills/impeccable/reference/ux-writing.md deleted file mode 100644 index 417b5f56c..000000000 --- a/.rovodev/skills/impeccable/reference/ux-writing.md +++ /dev/null @@ -1,107 +0,0 @@ -# UX Writing - -## The Button Label Problem - -**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns: - -| Bad | Good | Why | -|-----|------|-----| -| OK | Save changes | Says what will happen | -| Submit | Create account | Outcome-focused | -| Yes | Delete message | Confirms the action | -| Cancel | Keep editing | Clarifies what "cancel" means | -| Click here | Download PDF | Describes the destination | - -**For destructive actions**, name the destruction: -- "Delete" not "Remove" (delete is permanent, remove implies recoverable) -- "Delete 5 items" not "Delete selected" (show the count) - -## Error Messages: The Formula - -Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input". - -### Error Message Templates - -| Situation | Template | -|-----------|----------| -| **Format error** | "[Field] needs to be [format]. Example: [example]" | -| **Missing required** | "Please enter [what's missing]" | -| **Permission denied** | "You don't have access to [thing]. [What to do instead]" | -| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." | -| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" | - -### Don't Blame the User - -Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date". - -## Empty States Are Opportunities - -Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items". - -## Voice vs Tone - -**Voice** is your brand's personality, consistent everywhere. -**Tone** adapts to the moment. - -| Moment | Tone Shift | -|--------|------------| -| Success | Celebratory, brief: "Done! Your changes are live." | -| Error | Empathetic, helpful: "That didn't work. Here's what to try..." | -| Loading | Reassuring: "Saving your work..." | -| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | - -**Never use humor for errors.** Users are already frustrated. Be helpful, not cute. - -## Writing for Accessibility - -**Link text** must have standalone meaning: "View pricing plans" not "Click here". **Alt text** describes information, not the image: "Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context. - -## Writing for Translation - -### Plan for Expansion - -German text is ~30% longer than English. Allocate space: - -| Language | Expansion | -|----------|-----------| -| German | +30% | -| French | +20% | -| Finnish | +30-40% | -| Chinese | -30% (fewer chars, but same width) | - -### Translation-Friendly Patterns - -Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear. - -## Consistency: The Terminology Problem - -Pick one term and stick with it: - -| Inconsistent | Consistent | -|--------------|------------| -| Delete / Remove / Trash | Delete | -| Settings / Preferences / Options | Settings | -| Sign in / Log in / Enter | Sign in | -| Create / Add / New | Create | - -Build a terminology glossary and enforce it. Variety creates confusion. - -## Avoid Redundant Copy - -If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well. - -## Loading States - -Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress. - -## Confirmation Dialogs: Use Sparingly - -Most confirmation dialogs are design failures; consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No"). - -## Form Instructions - -Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking. - ---- - -**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors. diff --git a/.rovodev/skills/impeccable/scripts/cleanup-deprecated.mjs b/.rovodev/skills/impeccable/scripts/cleanup-deprecated.mjs index 6aee4710a..bc6400e90 100644 --- a/.rovodev/skills/impeccable/scripts/cleanup-deprecated.mjs +++ b/.rovodev/skills/impeccable/scripts/cleanup-deprecated.mjs @@ -25,7 +25,7 @@ import { join, resolve } from 'node:path'; const DEPRECATED_NAMES = [ // v2.0 renames 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable teach + 'teach-impeccable', // folded into /impeccable init // v2.1 merges 'arrange', // renamed to layout 'normalize', // merged into polish diff --git a/.rovodev/skills/impeccable/scripts/command-metadata.json b/.rovodev/skills/impeccable/scripts/command-metadata.json index 1488bd5f1..83796d54b 100644 --- a/.rovodev/skills/impeccable/scripts/command-metadata.json +++ b/.rovodev/skills/impeccable/scripts/command-metadata.json @@ -3,8 +3,8 @@ "description": "Full confirmed-brief-then-build flow. Runs multi-round shape discovery first, resolves visual probe and north-star mock gates when available, then builds and visually iterates. Use when building a new feature end-to-end.", "argumentHint": "[feature description]" }, - "teach": { - "description": "Gathers design context for a project. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.", + "init": { + "description": "Sets up a project for impeccable. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles); offers DESIGN.md (visual: colors, typography, components) when code exists; pre-configures live mode; then recommends the best commands to run next. Every other command reads these files before doing work. Use once per project.", "argumentHint": "" }, "document": { diff --git a/.rovodev/skills/impeccable/scripts/context.mjs b/.rovodev/skills/impeccable/scripts/context.mjs new file mode 100644 index 000000000..52263c883 --- /dev/null +++ b/.rovodev/skills/impeccable/scripts/context.mjs @@ -0,0 +1,253 @@ +/** + * Context loader: prints PRODUCT.md (and DESIGN.md if present) as one + * markdown block on stdout, or exits with empty stdout when no PRODUCT.md + * is found anywhere. The skill keys off "empty stdout" to branch into the + * init flow. + * + * Path resolution (first match wins): + * 1. cwd, if PRODUCT.md or DESIGN.md is there + * 2. .agents/context/ then docs/ + * 3. $IMPECCABLE_CONTEXT_DIR (absolute or cwd-relative) — power-user + * escape hatch, only consulted when defaults are empty + * 4. cwd as a "nothing found" default + * + * `resolveContextDir()` and `loadContext()` are also exported for the + * server-side scripts (live.mjs, live-server.mjs) that need the structured + * shape rather than the markdown block. + */ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; +const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; +const FALLBACK_DIRS = ['.agents/context', 'docs']; + +// ─── Update check ────────────────────────────────────────────────────────── +// Piggyback a lightweight skill-version check on the once-per-session boot. +// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent +// can offer `npx impeccable skills update`. Everything here is best-effort and +// silent on failure: a network problem, sandbox, or missing cache must never +// block context output or print an error. + +const UPDATE_HOST = (process.env.IMPECCABLE_UPDATE_HOST || 'https://impeccable.style').replace(/\/$/, ''); +const UPDATE_CACHE_PATH = + process.env.IMPECCABLE_UPDATE_CACHE || path.join(os.homedir(), '.impeccable', 'update-check.json'); +const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // throttle the network poll to once a day +const RENOTIFY_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000; // don't re-surface the same version for a week +const FETCH_TIMEOUT_MS = 1200; + +export function resolveContextDir(cwd = process.cwd()) { + if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { + return cwd; + } + for (const rel of FALLBACK_DIRS) { + const candidate = path.resolve(cwd, rel); + if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { + return candidate; + } + } + const envDir = process.env.IMPECCABLE_CONTEXT_DIR; + if (envDir && envDir.trim()) { + const trimmed = envDir.trim(); + return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); + } + return cwd; +} + +export function loadContext(cwd = process.cwd()) { + const contextDir = resolveContextDir(cwd); + const productPath = firstExisting(contextDir, PRODUCT_NAMES); + const designPath = firstExisting(contextDir, DESIGN_NAMES); + const product = productPath ? safeRead(productPath) : null; + const design = designPath ? safeRead(designPath) : null; + return { + hasProduct: !!product, + product, + productPath: productPath ? path.relative(cwd, productPath) : null, + hasDesign: !!design, + design, + designPath: designPath ? path.relative(cwd, designPath) : null, + contextDir, + }; +} + +function firstExisting(dir, names) { + for (const name of names) { + const abs = path.join(dir, name); + if (fs.existsSync(abs)) return abs; + } + return null; +} + +function safeRead(p) { + try { + return fs.readFileSync(p, 'utf-8'); + } catch { + return null; + } +} + +/** + * Pull the register (`brand` or `product`) out of PRODUCT.md by looking + * for a `## Register` section and reading the first non-empty line that + * follows it. Returns null when the file is legacy / register-less. + */ +function extractRegister(product) { + if (!product) return null; + const lines = product.split('\n'); + for (let i = 0; i < lines.length; i++) { + if (/^##\s+Register\b/i.test(lines[i].trim())) { + for (let j = i + 1; j < lines.length; j++) { + const next = lines[j].trim(); + if (!next) continue; + const word = next.toLowerCase(); + if (word === 'brand' || word === 'product') return word; + return null; + } + } + } + return null; +} + +/** + * Read the installed skill's own version from the sibling SKILL.md frontmatter + * (this file lives at `/scripts/context.mjs`). Returns null when the + * frontmatter is missing or unreadable. + */ +function readLocalSkillVersion() { + try { + const here = path.dirname(fileURLToPath(import.meta.url)); + const skillMd = path.join(here, '..', 'SKILL.md'); + const content = fs.readFileSync(skillMd, 'utf-8'); + const match = content.match(/^version:\s*(.+)$/m); + return match ? match[1].trim().replace(/^["']|["']$/g, '') : null; + } catch { + return null; + } +} + +function readUpdateCache() { + try { + return JSON.parse(fs.readFileSync(UPDATE_CACHE_PATH, 'utf-8')); + } catch { + return {}; + } +} + +function writeUpdateCache(cache) { + try { + fs.mkdirSync(path.dirname(UPDATE_CACHE_PATH), { recursive: true }); + fs.writeFileSync(UPDATE_CACHE_PATH, JSON.stringify(cache)); + } catch { + // Best-effort: a read-only home dir just means we re-poll next session. + } +} + +/** Compare dotted numeric versions. Returns >0 when a is newer than b. */ +function compareSemver(a, b) { + const pa = String(a).split('.').map(n => parseInt(n, 10) || 0); + const pb = String(b).split('.').map(n => parseInt(n, 10) || 0); + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + const diff = (pa[i] || 0) - (pb[i] || 0); + if (diff !== 0) return diff; + } + return 0; +} + +async function fetchLatestSkillVersion() { + try { + const res = await fetch(`${UPDATE_HOST}/api/version`, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }); + if (!res.ok) return null; + const data = await res.json(); + return typeof data?.skills === 'string' ? data.skills : null; + } catch { + return null; // offline, sandboxed, timed out, or bad JSON: all non-fatal + } +} + +function buildUpdateDirective(localVersion, latestVersion) { + return ( + `UPDATE_AVAILABLE: A newer Impeccable skill is available ` + + `(installed v${localVersion}, latest v${latestVersion}). ` + + `Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` + + `Update now? It runs \`npx impeccable skills update\`." ` + + `If they agree, run \`npx impeccable skills update\` (the update applies to the next session, not this one). ` + + `Either way, continue the current task without waiting, and do not raise this again.` + ); +} + +/** + * Best-effort update directive for the boot output. Returns a string to append + * or null. Polls the version endpoint at most once per day (cached globally in + * the user's home dir) and re-surfaces a given version at most once per week so + * the agent never nags. Opt out entirely with IMPECCABLE_NO_UPDATE_CHECK=1. + */ +async function computeUpdateDirective(now = Date.now()) { + try { + if (process.env.IMPECCABLE_NO_UPDATE_CHECK) return null; + const localVersion = readLocalSkillVersion(); + if (!localVersion) return null; + + const cache = readUpdateCache(); + + // Poll the network only when the throttle window has elapsed. Stamp + // lastCheck even on failure so an offline machine doesn't poll every boot. + if (!cache.lastCheck || now - cache.lastCheck > CHECK_INTERVAL_MS) { + const latest = await fetchLatestSkillVersion(); + cache.lastCheck = now; + if (latest) cache.latestVersion = latest; + writeUpdateCache(cache); + } + + const latest = cache.latestVersion; + if (!latest || compareSemver(latest, localVersion) <= 0) return null; + + // Anti-nag: surface a given version at most once per RENOTIFY window. + if (cache.notifiedVersion === latest && cache.notifiedAt && now - cache.notifiedAt < RENOTIFY_INTERVAL_MS) { + return null; + } + cache.notifiedVersion = latest; + cache.notifiedAt = now; + writeUpdateCache(cache); + + return buildUpdateDirective(localVersion, latest); + } catch { + return null; + } +} + +async function cli() { + const ctx = loadContext(process.cwd()); + const updateDirective = await computeUpdateDirective(); + + if (!ctx.hasProduct) { + // Direct stdout message instead of relying on empty output as a signal + // — cheap models miss the empty case more often than the explicit one. + const parts = [ + 'NO_PRODUCT_MD: This project has no PRODUCT.md yet. ' + + 'Stop the current task, load reference/init.md, and follow its ' + + 'instructions to write PRODUCT.md before resuming.', + ]; + if (updateDirective) parts.push(updateDirective); + process.stdout.write(parts.join('\n\n---\n\n') + '\n'); + process.exit(0); + } + const parts = [`# PRODUCT.md\n\n${ctx.product.trim()}`]; + if (ctx.hasDesign) { + parts.push(`# DESIGN.md\n\n${ctx.design.trim()}`); + } + const register = extractRegister(ctx.product); + const next = register + ? `NEXT STEP: This project's register is \`${register}\`. You MUST now read \`reference/${register}.md\` before producing any design output.` + : `NEXT STEP: You MUST now read the matching register reference (\`reference/brand.md\` or \`reference/product.md\`) before producing any design output. Pick based on PRODUCT.md above.`; + parts.push(next); + if (updateDirective) parts.push(updateDirective); + process.stdout.write(parts.join('\n\n---\n\n') + '\n'); +} + +const _running = process.argv[1]; +if (_running?.endsWith('context.mjs') || _running?.endsWith('context.mjs/')) { + cli(); +} diff --git a/.rovodev/skills/impeccable/scripts/design-parser.mjs b/.rovodev/skills/impeccable/scripts/design-parser.mjs index b68756640..82a5cd7dc 100644 --- a/.rovodev/skills/impeccable/scripts/design-parser.mjs +++ b/.rovodev/skills/impeccable/scripts/design-parser.mjs @@ -63,7 +63,7 @@ function parseYamlSubset(yaml) { } const key = content.slice(0, colonIdx).trim(); - const rest = content.slice(colonIdx + 1).trim(); + const rest = stripInlineYamlComment(content.slice(colonIdx + 1).trim()); const parent = stack[stack.length - 1].obj; if (rest === '') { @@ -93,6 +93,21 @@ function findTopLevelColon(s) { return -1; } +function stripInlineYamlComment(s) { + let inQuote = null; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (inQuote) { + if (ch === inQuote && s[i - 1] !== '\\') inQuote = null; + } else if (ch === '"' || ch === "'") { + inQuote = ch; + } else if (ch === '#' && i > 0 && /\s/.test(s[i - 1])) { + return s.slice(0, i).trimEnd(); + } + } + return s; +} + function parseScalar(raw) { const s = raw.trim(); if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { diff --git a/.rovodev/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.rovodev/skills/impeccable/scripts/detector/browser/injected/index.mjs index c03a69caf..ceff16737 100644 --- a/.rovodev/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/.rovodev/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -9,8 +9,21 @@ if (IS_BROWSER) { const EXTENSION_MODE = (_myScript && _myScript.dataset.impeccableExtension === 'true') || document.documentElement.dataset.impeccableExtension === 'true'; - const BRAND_COLOR = 'oklch(55% 0.25 350)'; - const BRAND_COLOR_HOVER = 'oklch(45% 0.25 350)'; + // Kinpaku gold — pinned to the site's brand token (see + // site/styles/kinpaku-tokens.css --ks-kinpaku). Keep this in sync with + // the picker's C.brand in skill/scripts/live-browser.js and the kit's + // picker section in site/styles/kinpaku-kit.css. + // + // One color across both light and dark host pages. The outline is a + // 2px gesture pointing at an element + a labeled tag — it's a marker, + // not body text, so it doesn't need WCAG AA against the page. The + // label text inside the gold tag is dark (LABEL_INK) which has ~16:1 + // against the leaf gold, so reading the rule name is solid in both + // modes. Hover deepens the gold (preserves chroma — never drops it, + // dropping chroma washes the gold into a sand/olive tone). + const BRAND_COLOR = 'oklch(84% 0.19 80.46)'; + const BRAND_COLOR_HOVER = 'oklch(74% 0.18 80)'; + const LABEL_INK = 'oklch(4% 0.004 95)'; const LABEL_BG = BRAND_COLOR; const OUTLINE_COLOR = BRAND_COLOR; @@ -278,7 +291,7 @@ if (IS_BROWSER) { display: 'flex', alignItems: 'center', whiteSpace: 'nowrap', fontSize: '11px', fontWeight: '600', letterSpacing: '0.02em', - color: 'white', lineHeight: '14px', + color: LABEL_INK, lineHeight: '14px', background: LABEL_BG, fontFamily: 'system-ui, sans-serif', borderRadius: '4px 4px 0 0', @@ -398,7 +411,7 @@ if (IS_BROWSER) { banner.className = 'impeccable-overlay impeccable-banner'; Object.assign(banner.style, { position: 'fixed', top: '0', left: '0', right: '0', zIndex: '100000', - background: LABEL_BG, color: 'white', + background: LABEL_BG, color: LABEL_INK, fontFamily: 'system-ui, sans-serif', fontSize: '13px', display: 'flex', alignItems: 'center', pointerEvents: 'auto', height: '36px', overflow: 'hidden', maxWidth: '100vw', @@ -1223,12 +1236,12 @@ if (IS_BROWSER) { } console.group( `%c[impeccable] ${allFindings.length} anti-pattern${allFindings.length === 1 ? '' : 's'} found`, - 'color: oklch(60% 0.25 350); font-weight: bold' + 'color: oklch(84% 0.19 80.46); font-weight: bold' ); for (const { el, findings } of allFindings) { for (const f of findings) { console.log(`%c${f.type || f.id}%c ${f.detail || f.snippet}`, - 'color: oklch(55% 0.25 350); font-weight: bold', 'color: inherit', el); + 'color: oklch(84% 0.19 80.46); font-weight: bold', 'color: inherit', el); } } console.groupEnd(); @@ -1249,6 +1262,10 @@ if (IS_BROWSER) { const groupMap = new Map(); const _disabled = EXTENSION_MODE ? (window.__IMPECCABLE_CONFIG__?.disabledRules || []) : []; const _ruleOk = (id) => !_disabled.length || !_disabled.includes(id); + // Note: provider-gated rules (--gpt / --gemini) are NOT filtered here. In a + // real browser env (detector page, live overlay, extension) running every + // check is free, so we always surface them; the gating is purely a CLI + // output concern, applied in the Node engines' detect* return paths. for (const el of document.querySelectorAll('*')) { // Skip impeccable's own elements and any descendants (overlays, labels, banner, nav buttons) @@ -1270,11 +1287,23 @@ if (IS_BROWSER) { ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), - ...checkElementHeroEyebrowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementOversizedH1DOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementClippedOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementGptBorderShadowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementTextOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ].filter(f => _ruleOk(f.type)); addBrowserFindings(groupMap, el, findings); + + // Hero eyebrow: the offending element is the eyebrow above the heading, + // not the heading itself — highlight the previous sibling instead. + const eyebrowFindings = checkElementHeroEyebrowDOM(el) + .map(f => ({ type: f.id, detail: f.snippet })) + .filter(f => _ruleOk(f.type)); + if (eyebrowFindings.length > 0 && el.previousElementSibling) { + addBrowserFindings(groupMap, el.previousElementSibling, eyebrowFindings); + } } const pageLevelFindings = []; @@ -1306,6 +1335,14 @@ if (IS_BROWSER) { addBrowserFindings(groupMap, document.body, qualityFindings); } + const creamFindings = checkCreamPalette(document) + .map(f => ({ type: f.id, detail: f.snippet })) + .filter(f => _ruleOk(f.type)); + if (creamFindings.length > 0) { + pageLevelFindings.push(...creamFindings); + addBrowserFindings(groupMap, document.body, creamFindings); + } + // Regex-on-HTML checks (shared with Node) // Clone the document and strip impeccable-live overlay nodes before the // regex scan, so the inspector's own inline styles (transitions on top/ diff --git a/.rovodev/skills/impeccable/scripts/detector/cli/main.mjs b/.rovodev/skills/impeccable/scripts/detector/cli/main.mjs index 76da09f0c..94283e01b 100644 --- a/.rovodev/skills/impeccable/scripts/detector/cli/main.mjs +++ b/.rovodev/skills/impeccable/scripts/detector/cli/main.mjs @@ -41,7 +41,7 @@ function formatFindings(findings, jsonMode) { // Stdin handling // --------------------------------------------------------------------------- -async function handleStdin() { +async function handleStdin(options = {}) { const chunks = []; for await (const chunk of process.stdin) chunks.push(chunk); const input = Buffer.concat(chunks).toString('utf-8'); @@ -50,10 +50,10 @@ async function handleStdin() { const fp = parsed?.tool_input?.file_path; if (fp && fs.existsSync(fp)) { return HTML_EXTENSIONS.has(path.extname(fp).toLowerCase()) - ? detectHtml(fp) : detectText(fs.readFileSync(fp, 'utf-8'), fp); + ? detectHtml(fp, options) : detectText(fs.readFileSync(fp, 'utf-8'), fp, options); } } catch { /* not JSON */ } - return detectText(input, ''); + return detectText(input, '', options); } @@ -81,6 +81,8 @@ Scan files or URLs for UI anti-patterns and design quality issues. Options: --fast Regex-only mode (skip static HTML/CSS analysis, faster but misses linked stylesheets) --json Output results as JSON + --gpt Also report GPT-specific provider tells (off by default) + --gemini Also report Gemini-specific provider tells (off by default) --help Show this help message Detection modes: @@ -106,6 +108,10 @@ async function detectCli() { const jsonMode = args.includes('--json'); const helpMode = args.includes('--help'); const fastMode = args.includes('--fast'); + const providers = []; + if (args.includes('--gpt')) providers.push('gpt'); + if (args.includes('--gemini')) providers.push('gemini'); + const scanOptions = { providers }; const targets = args.filter(a => !a.startsWith('--')); if (helpMode) { printUsage(); process.exit(0); } @@ -113,7 +119,7 @@ async function detectCli() { let allFindings = []; if (!process.stdin.isTTY && targets.length === 0) { - allFindings = await handleStdin(); + allFindings = await handleStdin(scanOptions); } else { const paths = targets.length > 0 ? targets : [process.cwd()]; const urlTargetCount = paths.filter(target => /^https?:\/\//i.test(target)).length; @@ -124,8 +130,8 @@ async function detectCli() { if (/^https?:\/\//i.test(target)) { try { const scanner = browserDetector - ? (url) => browserDetector.detectUrl(url) - : (url) => detectUrl(url); + ? (url) => browserDetector.detectUrl(url, scanOptions) + : (url) => detectUrl(url, scanOptions); allFindings.push(...await scanner(target)); } catch (e) { process.stderr.write(`Error: ${e.message}\n`); } continue; @@ -192,9 +198,9 @@ async function detectCli() { const ext = path.extname(file).toLowerCase(); let fileFindings; if (!fastMode && HTML_EXTENSIONS.has(ext)) { - fileFindings = await detectHtml(file); + fileFindings = await detectHtml(file, scanOptions); } else { - fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file); + fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file, scanOptions); } // Annotate findings with import context const importers = importedByMap.get(file); @@ -209,9 +215,9 @@ async function detectCli() { } else if (stat.isFile()) { const ext = path.extname(resolved).toLowerCase(); if (!fastMode && HTML_EXTENSIONS.has(ext)) { - allFindings.push(...await detectHtml(resolved)); + allFindings.push(...await detectHtml(resolved, scanOptions)); } else { - allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved)); + allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved, scanOptions)); } } } diff --git a/.rovodev/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.rovodev/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 1afbe3f12..88439a5c3 100644 --- a/.rovodev/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.rovodev/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -38,7 +38,7 @@ const OVERUSED_FONTS = new Set([ // Older monoculture (still ubiquitous): 'inter', 'roboto', 'open sans', 'lato', 'montserrat', 'arial', 'helvetica', // Newer monoculture (the Anthropic-skill / Vercel / GitHub default wave): - 'fraunces', 'instrument sans', + 'fraunces', 'instrument sans', 'instrument serif', 'geist', 'geist sans', 'geist mono', 'mona sans', 'plus jakarta sans', 'space grotesk', 'recoleta', @@ -165,6 +165,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'AI color palette', }, + { + id: 'cream-palette', + category: 'slop', + name: 'Cream / beige palette', + description: + 'A warm cream or beige page background has become the default "tasteful" AI surface, reached for by reflex. Choose a background that comes from a deliberate palette, not the safe warm off-white.', + skillSection: 'Color & Contrast', + skillGuideline: 'cream and beige as the default surface', + }, { id: 'nested-cards', category: 'slop', @@ -183,15 +192,6 @@ const ANTIPATTERNS = [ skillSection: 'Layout & Space', skillGuideline: 'same spacing everywhere', }, - { - id: 'everything-centered', - category: 'slop', - name: 'Everything centered', - description: - 'Every text element is center-aligned. Left-aligned text with asymmetric layouts feels more designed. Center only hero sections and CTAs.', - skillSection: 'Layout & Space', - skillGuideline: 'Center everything', - }, { id: 'bounce-easing', category: 'slop', @@ -247,17 +247,72 @@ const ANTIPATTERNS = [ skillSection: 'Typography', skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding', }, + { + id: 'numbered-section-markers', + category: 'slop', + severity: 'advisory', + name: 'Numbered section markers (01 / 02 / 03)', + description: + 'Numbered display markers as section labels (01, 02, 03) are the AI editorial scaffold one tier deeper than tracked eyebrow chips. If you find yourself reaching for them, choose a different section cadence.', + skillSection: 'Layout & Space', + skillGuideline: 'numbered section markers', + }, + { + id: 'em-dash-overuse', + category: 'slop', + name: 'Em-dash overuse', + description: + 'More than two em-dashes (— or --) in body copy is an AI cadence tell. Use commas, colons, periods, or parentheses instead.', + skillSection: 'Copy', + skillGuideline: 'no em dashes', + }, + { + id: 'marketing-buzzword', + category: 'slop', + name: 'Marketing buzzword', + description: + 'Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does.', + skillSection: 'Copy', + skillGuideline: 'marketing buzzwords', + }, + { + id: 'aphoristic-cadence', + category: 'slop', + name: 'Aphoristic-cadence copy', + description: + 'Three or more sections landing on a short rebuttal sentence ("X. No Y." / "X. Just Y.") or a manufactured-contrast aphorism ("Not a feature. A platform.") reads as AI cadence, not voice. Once is fine; the pattern is the tell.', + skillSection: 'Copy', + skillGuideline: 'aphoristic cadence', + }, + { + id: 'oversized-h1', + category: 'slop', + name: 'Oversized hero headline', + description: + 'A full-sentence headline set at display size ends up dominating the viewport, leaving no room for anything else above the fold. A punchy one- or two-word headline at that size is fine — the problem is a long headline blown up too large. Set long headlines smaller, or tighten the copy.', + skillSection: 'Typography', + skillGuideline: 'long headline set at display size', + }, + { + id: 'extreme-negative-tracking', + category: 'slop', + name: 'Crushed letter spacing', + description: + 'Letter-spacing pulled tighter than the point where characters keep their own shapes costs legibility. Tighten display type optically, not destructively.', + skillSection: 'Typography', + skillGuideline: 'letter spacing crushed past legibility', + }, + { + id: 'broken-image', + category: 'quality', + name: 'Broken or placeholder image', + description: + ' tags with empty src, missing src, or placeholder values ship as broken-image boxes. Use real images, generated assets, or remove the tag.', + skillSection: 'Imagery', + skillGuideline: 'broken image references', + }, // ── Quality: general design and accessibility issues ── - { - id: 'pure-black-white', - category: 'quality', - name: 'Pure black background', - description: - 'Pure #000000 as a background color looks harsh and unnatural. Tint it slightly toward your brand hue (e.g., oklch(12% 0.01 250)) for a more refined feel.', - skillSection: 'Color & Contrast', - skillGuideline: 'pure black (#000)', - }, { id: 'gray-on-color', category: 'quality', @@ -297,7 +352,9 @@ const ANTIPATTERNS = [ category: 'quality', name: 'Cramped padding', description: - 'Text is too close to the edge of its container. Add at least 8px (ideally 12-16px) of padding inside bordered or colored containers.', + 'Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.', + skillSection: 'Layout & Space', + skillGuideline: 'inside bordered or colored containers', }, { id: 'body-text-viewport-edge', @@ -350,6 +407,70 @@ const ANTIPATTERNS = [ description: 'Letter spacing above 0.05em on body text disrupts natural character groupings and slows reading. Reserve wide tracking for short uppercase labels only.', }, + { + id: 'text-overflow', + category: 'quality', + name: 'Content overflowing its container', + description: + 'Content renders wider than its container, spilling out or forcing a horizontal scrollbar. Let text wrap, constrain widths, or give the region a deliberate scroll affordance.', + skillSection: 'Layout & Space', + skillGuideline: 'content wider than its container', + }, + { + id: 'clipped-overflow-container', + category: 'quality', + name: 'Positioned child clipped by overflow container', + description: + 'A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.', + skillSection: 'Layout & Space', + skillGuideline: 'overflow container clipping positioned children', + }, + + // ── Provider tells: opt-in via --gpt / --gemini (gated off by default) ── + { + id: 'gpt-thin-border-wide-shadow', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Hairline border with wide shadow', + description: + 'A hairline border paired with a wide, diffuse shadow is a recurring generated-UI signature. Commit to one — a defined edge or a soft elevation — rather than both at once.', + skillSection: 'Visual Details', + skillGuideline: 'hairline border plus wide diffuse shadow', + }, + { + id: 'repeating-stripes-gradient', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Repeating-gradient stripes', + description: + 'Repeating-gradient stripes used as surface decoration are a recurring generated-UI signature. Reach for a deliberate texture or leave the surface plain.', + skillSection: 'Visual Details', + skillGuideline: 'repeating-gradient decorative stripes', + }, + { + id: 'theater-slop-phrase', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Theater framing copy', + description: + 'Dismissing something as "theater" is a recurring generated-copy tic. Say plainly what the thing does or does not do.', + skillSection: 'Copy', + skillGuideline: 'theater framing copy', + }, + { + id: 'image-hover-transform', + category: 'slop', + severity: 'advisory', + gated: 'gemini', + name: 'Image hover transform', + description: + 'Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction.', + skillSection: 'Motion', + skillGuideline: 'image scale or rotate on hover', + }, ]; // --- cli/engine/shared/color.mjs --- @@ -527,11 +648,6 @@ function checkColors(opts) { } const findings = []; - // Pure black background (only solid or near-solid, not semi-transparent overlays) - if (bgColor && bgColor.a >= 0.9 && bgColor.r === 0 && bgColor.g === 0 && bgColor.b === 0) { - findings.push({ id: 'pure-black-white', snippet: '#000000 background' }); - } - if (hasDirectText && textColor && !isEmojiOnly) { // Run background-dependent checks against either a solid bg or, if the // ancestor is a gradient, against every gradient stop (use the worst case). @@ -587,9 +703,6 @@ function checkColors(opts) { // Tailwind class checks if (classList) { const classStr = typeof classList === 'string' ? classList : Array.from(classList).join(' '); - if (/\bbg-black\b(?!\/)/.test(classStr)) { - findings.push({ id: 'pure-black-white', snippet: 'bg-black' }); - } const grayMatch = classStr.match(/\btext-(?:gray|slate|zinc|neutral|stone)-\d+\b/); const colorBgMatch = classStr.match(/\bbg-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+\b/); @@ -905,12 +1018,6 @@ function checkHtmlPatterns(html) { // --- Color --- - // Pure black background - const pureBlackBgRe = /background(?:-color)?\s*:\s*(?:#000000|#000|rgb\(\s*0,\s*0,\s*0\s*\))\b/gi; - if (pureBlackBgRe.test(html)) { - findings.push({ id: 'pure-black-white', snippet: 'Pure #000 background' }); - } - // AI color palette: purple/violet const purpleHexRe = /#(?:7c3aed|8b5cf6|a855f7|9333ea|7e22ce|6d28d9|6366f1|764ba2|667eea)\b/gi; if (purpleHexRe.test(html)) { @@ -1027,6 +1134,39 @@ function checkHtmlPatterns(html) { } } + // --- Provider tells (gated): repeating-gradient stripes (GPT) --- + if (/repeating-(?:linear|radial|conic)-gradient\s*\(/i.test(html)) { + findings.push({ id: 'repeating-stripes-gradient', snippet: 'repeating-gradient decorative stripes' }); + } + + // --- Provider tells (gated): "X theater" framing copy (GPT) --- + // Lives here (regex-on-HTML) rather than in the text-content analyzers so it + // runs in the bundled browser path too, not just the CLI/static path. + { + const bodyText = html + .replace(/]*>[\s\S]*?<\/script>/gi, ' ') + .replace(/]*>[\s\S]*?<\/style>/gi, ' ') + .replace(/<[^>]+>/g, ' '); + const tm = /\b(\w+)\s+theater\b/i.exec(bodyText); + if (tm) findings.push({ id: 'theater-slop-phrase', snippet: `"${tm[0].trim()}"` }); + } + + // --- Provider tells (gated): image hover transform (Gemini) --- + // A CSS `img...:hover { transform: ... }` rule, or a Tailwind hover:scale / + // hover:rotate / hover:translate utility on an . Each distinct + // mechanism is its own finding. + const imgHoverCss = /\bimg\b[^,{}]*:hover\b[^{}]*\{[^}]*\btransform\s*:\s*(?:scale|rotate|translate|matrix|skew)/i; + if (imgHoverCss.test(html)) { + findings.push({ id: 'image-hover-transform', snippet: 'img:hover { transform } rule' }); + } + const imgTagRe = /]*\bclass\s*=\s*"([^"]*)"/gi; + let im; + while ((im = imgTagRe.exec(html)) !== null) { + if (/\bhover:(?:scale|rotate|translate|skew)-/.test(im[1])) { + findings.push({ id: 'image-hover-transform', snippet: 'Tailwind hover transform on ' }); + } + } + return findings; } @@ -1673,7 +1813,7 @@ function resolveLengthPx(value, fontSizePx) { // Both adapters resolve font-size, line-height and letter-spacing to pixels // before calling this so the pure function only deals with numbers. function checkQuality(opts) { - const { el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax = 80, viewportWidth = 0 } = opts; + const { el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax = 80, viewportWidth = 0, win = null } = opts; const findings = []; // Skip browser extension injected elements const elId = el.id || ''; @@ -1724,6 +1864,155 @@ function checkQuality(opts) { } } + // --- Flush against a visible boundary --- + // Fires when a container has a visible boundary (border, outline, OR a + // non-transparent background) AND near-zero padding on the bounded + // side(s) AND text-bearing children land flush against the boundary. + // + // Distinct from cramped-padding: that rule needs the element itself to + // have direct text (hasDirectText). This rule targets the OPPOSITE + // shape — a container with NO direct text, only children — which is + // exactly what cramped-padding misses (a section wrapping a label + + // list lands a free pass). + // + // The classic shape: agent writes `padding: 28px 0 0` shorthand on a + // section that also has a border, zeroing horizontal padding so the + // text-bearing children touch the side borders. Background and + // outline count too: a colored card with zero padding has the same + // visual failure mode. + { + const FLUSH_SKIP_TAGS = new Set(['HTML', 'BODY', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ARTICLE', 'ASIDE', 'BUTTON', 'A', 'LABEL', 'SUMMARY', 'CODE', 'PRE', 'INPUT', 'TEXTAREA', 'SELECT', 'FORM', 'FIGURE', 'TABLE', 'TBODY', 'THEAD', 'TR', 'TD', 'TH']); + const upperTag = tag ? tag.toUpperCase() : ''; + const elPosition = style.position || ''; + if ( + !FLUSH_SKIP_TAGS.has(upperTag) && + !hasDirectText && + !['fixed', 'absolute'].includes(elPosition) && + el.children && el.children.length > 0 + ) { + const isTransparent = (c) => + !c || c === 'transparent' || c === 'rgba(0, 0, 0, 0)' || + /^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*0(?:\.0+)?\s*\)$/.test(c); + + const borderW = { + top: parseFloat(style.borderTopWidth) || 0, + right: parseFloat(style.borderRightWidth) || 0, + bottom: parseFloat(style.borderBottomWidth) || 0, + left: parseFloat(style.borderLeftWidth) || 0, + }; + const borderVisible = { + top: borderW.top > 0 && !isTransparent(style.borderTopColor), + right: borderW.right > 0 && !isTransparent(style.borderRightColor), + bottom: borderW.bottom > 0 && !isTransparent(style.borderBottomColor), + left: borderW.left > 0 && !isTransparent(style.borderLeftColor), + }; + // Outline detection. jsdom decomposes `border` shorthand into + // border{Top,…}Width/Color but does NOT decompose `outline` — + // the longhands come back empty when the value was set via the + // shorthand. Fall back to parsing `style.outline` ourselves. + let outlineW = parseFloat(style.outlineWidth) || 0; + let outlineStyleVal = style.outlineStyle || ''; + let outlineColorVal = style.outlineColor || ''; + if (!outlineW && style.outline) { + const wMatch = style.outline.match(/(\d+(?:\.\d+)?)\s*px/); + if (wMatch) outlineW = parseFloat(wMatch[1]) || 0; + if (!outlineStyleVal) { + outlineStyleVal = /\b(solid|dashed|dotted|double|groove|ridge|inset|outset)\b/.test(style.outline) ? 'solid' : ''; + } + if (!outlineColorVal) { + const cMatch = style.outline.match(/(rgba?\([^)]+\)|#[0-9a-fA-F]{3,8}|[a-zA-Z]+)\s*$/); + if (cMatch) outlineColorVal = cMatch[1]; + } + } + const outlineVisible = outlineW > 0 && !isTransparent(outlineColorVal) && outlineStyleVal && outlineStyleVal !== 'none'; + const bgVisible = !isTransparent(style.backgroundColor); + + const anyVisible = borderVisible.top || borderVisible.right || borderVisible.bottom || borderVisible.left || outlineVisible || bgVisible; + if (anyVisible) { + // Resolve padding to px (jsdom returns raw "1.5rem" etc., not the + // computed px value; parseFloat would strip the unit and treat + // 1.5rem as 1.5px, false-flagging legitimate insets). + const pad = { + top: resolveLengthPx(style.paddingTop, fontSize) ?? 0, + right: resolveLengthPx(style.paddingRight, fontSize) ?? 0, + bottom: resolveLengthPx(style.paddingBottom, fontSize) ?? 0, + left: resolveLengthPx(style.paddingLeft, fontSize) ?? 0, + }; + const PAD_THRESHOLD = 2; + // Children-insulate-this-side: a side is insulated if ANY direct + // child has its own padding ≥ 4px on that side. Rationale: in + // typical flow, only the first/last (or leftmost/rightmost) + // children actually sit at the parent's edges. If even one of + // them has its own padding, the visual flush is broken on that + // side. Classic example: a column-flow card frame where the + // top child (header) has padding-top:12 and the bottom child + // (footer) has padding-bottom:8 — the parent's padding:0 doesn't + // matter; nothing is actually flush. The `any-child-insulates` + // heuristic accepts some false negatives (a card with one heavily + // padded middle child won't flag) for far fewer false positives. + const CHILD_INSULATE_THRESHOLD = 4; + const childrenInsulate = { top: false, right: false, bottom: false, left: false }; + for (const child of el.children) { + let childStyle = null; + if (win && typeof win.getComputedStyle === 'function') { + try { childStyle = win.getComputedStyle(child); } catch {} + } + if (!childStyle && typeof getComputedStyle === 'function') { + try { childStyle = getComputedStyle(child); } catch {} + } + if (!childStyle) continue; + const childPad = { + top: resolveLengthPx(childStyle.paddingTop, fontSize) ?? 0, + right: resolveLengthPx(childStyle.paddingRight, fontSize) ?? 0, + bottom: resolveLengthPx(childStyle.paddingBottom, fontSize) ?? 0, + left: resolveLengthPx(childStyle.paddingLeft, fontSize) ?? 0, + }; + for (const s of ['top', 'right', 'bottom', 'left']) { + if (childPad[s] >= CHILD_INSULATE_THRESHOLD) childrenInsulate[s] = true; + } + } + + const flushSides = []; + for (const side of ['top', 'right', 'bottom', 'left']) { + const sideBounded = borderVisible[side] || outlineVisible || bgVisible; + if (sideBounded && pad[side] <= PAD_THRESHOLD && !childrenInsulate[side]) { + flushSides.push(side); + } + } + + if (flushSides.length > 0) { + // Confirm at least one direct child has substantial text content + // (> 4 chars). Without this, the flush is harmless: e.g. an + // image-only card. + let hasTextChild = false; + for (const child of el.children) { + const childText = (child.textContent || '').trim(); + if (childText.length > 4) { hasTextChild = true; break; } + } + if (hasTextChild) { + const cls = (typeof el.className === 'string' && el.className.trim()) + ? el.className.trim().split(/\s+/)[0] + : ''; + const boundaryParts = []; + const borderSidesVisible = ['top', 'right', 'bottom', 'left'].filter(s => borderVisible[s]); + if (borderSidesVisible.length === 4) boundaryParts.push('border'); + else if (borderSidesVisible.length > 0) boundaryParts.push(`border-${borderSidesVisible.join('/')}`); + if (outlineVisible) boundaryParts.push('outline'); + if (bgVisible) boundaryParts.push('bg'); + const sidesLabel = flushSides.length === 4 ? 'all sides' : flushSides.join('/'); + const ident = cls + ? `<${tag.toLowerCase()}> "${cls}"` + : `<${tag.toLowerCase()}>`; + findings.push({ + id: 'cramped-padding', + snippet: `${ident}: children flush against ${boundaryParts.join('+')} on ${sidesLabel} (no inset)`, + }); + } + } + } + } + } + // --- Body text touching viewport edge --- (browser-only: needs rect) // Catches the failure mode where the agent ships body paragraphs // with NO container providing horizontal padding — text bleeds @@ -1804,6 +2093,20 @@ function checkQuality(opts) { } } + // --- Crushed letter spacing (mirror of wide-tracking) --- + // Tracking pulled tighter than ~-0.05em crushes characters into each other. + // Optical tightening that display type legitimately wants (around -0.02em) + // stays well above this floor. + if (hasDirectText && textLen > 20 && fontSize > 0) { + if (letterSpacingPx != null && letterSpacingPx < 0) { + const trackingEm = letterSpacingPx / fontSize; + if (trackingEm <= -0.05) { + const excerpt = (el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 40); + findings.push({ id: 'extreme-negative-tracking', snippet: `letter-spacing: ${trackingEm.toFixed(2)}em — "${excerpt}"` }); + } + } + } + return findings; } @@ -1820,7 +2123,7 @@ function checkElementQualityDOM(el) { const rect = el.getBoundingClientRect(); const lineMax = (typeof window !== 'undefined' && window.__IMPECCABLE_CONFIG__?.lineLengthMax) || 80; const viewportWidth = (typeof window !== 'undefined' ? window.innerWidth : 0) || 0; - return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax, viewportWidth }); + return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax, viewportWidth, win: typeof window !== 'undefined' ? window : null }); } // Pure page-level skipped-heading walk. Takes a Document so it works in both @@ -1862,7 +2165,7 @@ function checkElementQuality(el, style, tag, window) { const fontSize = resolveFontSizePx(el, window); const lineHeightPx = resolveLengthPx(style.lineHeight, fontSize); const letterSpacingPx = resolveLengthPx(style.letterSpacing, fontSize); - return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect: null }); + return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect: null, win: window }); } function checkElementBorders(tag, style, overrides, resolvedRadius) { @@ -2303,40 +2606,213 @@ function checkPageLayout(doc, win) { } } - // Everything centered - const textEls = doc.querySelectorAll('h1, h2, h3, h4, h5, h6, p, li, div, button'); - let centeredCount = 0; - let totalText = 0; - for (const el of textEls) { - const hasDirectText = [...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim().length >= 3); - if (!hasDirectText) continue; - totalText++; - - let cur = el; - let isCentered = false; - while (cur && cur.nodeType === 1) { - const rawStyle = cur.getAttribute?.('style') || ''; - const cls = cur.getAttribute?.('class') || ''; - if (/text-align\s*:\s*center/i.test(rawStyle) || /\btext-center\b/.test(cls)) { - isCentered = true; - break; - } - if (cur.tagName === 'BODY') break; - cur = cur.parentElement; - } - if (isCentered) centeredCount++; - } - - if (totalText >= 5 && centeredCount / totalText > 0.7) { - findings.push({ - id: 'everything-centered', - snippet: `${centeredCount}/${totalText} text elements centered (${Math.round(centeredCount / totalText * 100)}%)`, - }); - } - return findings; } +// ─── Cream / beige palette (the default "tasteful" AI surface) ──────────────── +// A warm, lightly-tinted off-white page background — light, with R≥G≥B and a +// small warm tint (not white, not a strong color). The current reflex surface. +function isCreamColor(rgb) { + if (!rgb) return false; + const { r, g, b } = rgb; + if (Math.min(r, g, b) < 209) return false; // must be light + if (!(r >= g && g >= b)) return false; // warm ordering + const warmth = r - b; + return warmth >= 6 && warmth <= 48; // tinted, not white, not strong +} + +// Tailwind background utilities that render as a warm off-white surface. The +// static engine doesn't fetch Tailwind's CSS, so a `bg-amber-50` on +// resolves to nothing in computed style — catch it from the class list +// instead. Candidate tokens map to their actual Tailwind hex and are still +// filtered through isCreamColor, so neutral grays (stone) and over-saturated +// shades drop out on their own. +const TAILWIND_BG_HEX = { + 'bg-amber-50': '#fffbeb', 'bg-amber-100': '#fef3c7', + 'bg-orange-50': '#fff7ed', 'bg-orange-100': '#ffedd5', + 'bg-yellow-50': '#fefce8', + 'bg-stone-50': '#fafaf9', 'bg-stone-100': '#f5f5f4', 'bg-stone-200': '#e7e5e4', +}; + +function creamFromClassList(cls) { + if (!cls) return null; + // Arbitrary value: bg-[#f5f0e6] / bg-[rgb(245_240_230)] (underscores = spaces). + const arb = cls.match(/\bbg-\[([^\]]+)\]/); + if (arb && isCreamColor(parseAnyColor(arb[1].replace(/_/g, ' ')))) return `bg-[${arb[1]}]`; + // Named warm-light utilities. + for (const [tok, hex] of Object.entries(TAILWIND_BG_HEX)) { + if (new RegExp(`(^|\\s)${tok}($|\\s)`).test(cls) && isCreamColor(parseAnyColor(hex))) return tok; + } + return null; +} + +function checkCreamPalette(doc, win) { + const findings = []; + const body = doc.body || (doc.querySelector ? doc.querySelector('body') : null); + if (!body) return findings; + const html = doc.documentElement; + const getCS = (el) => (win ? win.getComputedStyle(el) : getComputedStyle(el)); + + // 1. Computed background — covers inline / close.', 'Prefix every preview selector with the matching [data-impeccable-variant="N"] selector.', 'Keep selectors anchored to the generated variant wrapper; do not rely on component CSS scoping for preview rules.', ], forbidden: [ 'Do not use @scope for this styleMode.', + 'Do not wrap style content in a JSX/TSX template literal ({` ... `}); that syntax is for .tsx/.jsx only.', + 'Do not put { immediately after the style opening tag; Astro parses { as expression syntax.', ], }; } @@ -629,4 +632,15 @@ if (_running?.endsWith('live-wrap.mjs') || _running?.endsWith('live-wrap.mjs/')) } // Test exports (used by tests/live-wrap.test.mjs) -export { buildSearchQueries, findElement, findClosingLine, detectCommentSyntax }; +export { + buildSearchQueries, + findElement, + findClosingLine, + detectCommentSyntax, + findAllElements, + filterByText, + findFileWithQuery, + detectStyleMode, + buildCssAuthoring, + buildCssSelectorPrefixExamples, +}; diff --git a/.rovodev/skills/impeccable/scripts/live.mjs b/.rovodev/skills/impeccable/scripts/live.mjs index cafb0eca9..8acd300ed 100644 --- a/.rovodev/skills/impeccable/scripts/live.mjs +++ b/.rovodev/skills/impeccable/scripts/live.mjs @@ -21,7 +21,7 @@ import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { loadContext } from './load-context.mjs'; +import { loadContext } from './context.mjs'; import { resolveFiles } from './live-inject.mjs'; import { readLiveServerInfo } from './impeccable-paths.mjs'; @@ -80,7 +80,7 @@ The agent should then: process.exit(1); } - // 4. Load PRODUCT.md + DESIGN.md context (auto-migrates legacy .impeccable.md) + // 4. Load PRODUCT.md + DESIGN.md context. const ctx = loadContext(process.cwd()); // 5. Compute drift-heal: compare resolved inject targets against the @@ -102,7 +102,6 @@ The agent should then: hasDesign: ctx.hasDesign, design: ctx.design, designPath: ctx.designPath, - migrated: ctx.migrated, }, null, 2)); } diff --git a/.rovodev/skills/impeccable/scripts/load-context.mjs b/.rovodev/skills/impeccable/scripts/load-context.mjs deleted file mode 100644 index dc340bf16..000000000 --- a/.rovodev/skills/impeccable/scripts/load-context.mjs +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Shared context loader for every impeccable command that needs to know - * "who is this for" and "what does this look like". - * - * Input: project root (process.cwd()). - * - * Output (JSON to stdout): - * { - * hasProduct: boolean, // PRODUCT.md found (or auto-migrated) - * product: string | null, // PRODUCT.md contents - * productPath: string | null, // relative path - * hasDesign: boolean, // DESIGN.md found - * design: string | null, // DESIGN.md contents - * designPath: string | null, - * migrated: boolean, // true if we auto-renamed .impeccable.md -> PRODUCT.md - * contextDir: string, // absolute path of the directory the files were found in - * } - * - * Filename matching is case-insensitive for PRODUCT.md and DESIGN.md. The - * Google DESIGN.md convention is uppercase at repo root; Kiro-style and - * lowercase variants are also matched so users don't get punished for case. - * - * Lookup directory resolution (first match wins): - * 1. process.env.IMPECCABLE_CONTEXT_DIR (absolute or relative to cwd) - * 2. cwd, if PRODUCT.md / DESIGN.md / .impeccable.md is there (back-compat) - * 3. Auto-fallback subdirectories of cwd: .agents/context/, then docs/ - * 4. cwd as a default "no context found" location - * - * Legacy `.impeccable.md` -> PRODUCT.md migration only fires at cwd root; - * fallback directories are read-only as far as auto-rename is concerned. - */ - -import fs from 'node:fs'; -import path from 'node:path'; - -const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; -const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; -const LEGACY_NAMES = ['.impeccable.md']; -const FALLBACK_DIRS = ['.agents/context', 'docs']; - -/** - * Resolve the directory that holds PRODUCT.md / DESIGN.md for - * this project. Exported so other scripts (e.g. live-server.mjs) can read the - * design files from the same location the loader uses. - */ -export function resolveContextDir(cwd = process.cwd()) { - // 1. Explicit override - const envDir = process.env.IMPECCABLE_CONTEXT_DIR; - if (envDir && envDir.trim()) { - const trimmed = envDir.trim(); - return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); - } - - // 2. cwd wins if any canonical or legacy file is there. We check legacy too - // so the auto-migration path in loadContext stays predictable. - if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES, ...LEGACY_NAMES])) { - return cwd; - } - - // 3. Auto-fallback subdirs. Match if PRODUCT.md or DESIGN.md is present; - // legacy `.impeccable.md` does not pull the lookup into a fallback dir. - for (const rel of FALLBACK_DIRS) { - const candidate = path.resolve(cwd, rel); - if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { - return candidate; - } - } - - // 4. Nothing found — keep the historical "default to cwd" behaviour so the - // caller's `hasProduct === false` branch still fires the same way. - return cwd; -} - -export function loadContext(cwd = process.cwd()) { - let migrated = false; - const contextDir = resolveContextDir(cwd); - - // 1. Look for PRODUCT.md (case-insensitive) in the resolved dir - let productPath = firstExisting(contextDir, PRODUCT_NAMES); - - // 2. Legacy: if no PRODUCT.md but .impeccable.md exists at cwd root, rename - // it in place. We only migrate at the root — fallback dirs are read-only - // so we don't surprise users by mutating files under docs/ or .agents/. - if (!productPath && contextDir === cwd) { - const legacyPath = firstExisting(cwd, LEGACY_NAMES); - if (legacyPath) { - const newPath = path.join(cwd, 'PRODUCT.md'); - try { - fs.renameSync(legacyPath, newPath); - productPath = newPath; - migrated = true; - } catch { - // Rename failed (permissions, etc.) — fall back to reading legacy in place - productPath = legacyPath; - } - } - } - - // 3. DESIGN.md (case-insensitive) - const designPath = firstExisting(contextDir, DESIGN_NAMES); - - const product = productPath ? safeRead(productPath) : null; - const design = designPath ? safeRead(designPath) : null; - - return { - hasProduct: !!product, - product, - productPath: productPath ? path.relative(cwd, productPath) : null, - hasDesign: !!design, - design, - designPath: designPath ? path.relative(cwd, designPath) : null, - migrated, - contextDir, - }; -} - -function firstExisting(dir, names) { - for (const name of names) { - const abs = path.join(dir, name); - if (fs.existsSync(abs)) return abs; - } - return null; -} - -function safeRead(p) { - try { return fs.readFileSync(p, 'utf-8'); } catch { return null; } -} - -// --------------------------------------------------------------------------- -// CLI mode — print the context as JSON -// --------------------------------------------------------------------------- - -function cli() { - const result = loadContext(process.cwd()); - console.log(JSON.stringify(result, null, 2)); -} - -const _running = process.argv[1]; -if (_running?.endsWith('load-context.mjs') || _running?.endsWith('load-context.mjs/')) { - cli(); -} diff --git a/.rovodev/skills/impeccable/scripts/palette.mjs b/.rovodev/skills/impeccable/scripts/palette.mjs new file mode 100644 index 000000000..4c3f1751a --- /dev/null +++ b/.rovodev/skills/impeccable/scripts/palette.mjs @@ -0,0 +1,633 @@ +#!/usr/bin/env node +/** + * Brand-seed picker. Returns one OKLCH seed color + the mood it most + * naturally evokes, and teaches the model how to compose a full palette + * around it. + * + * The seed is the brand's anchor color. The 5-role palette (bg, surface, + * ink, accent, muted) is composed by the caller at runtime using their + * judgment + the brief (PRODUCT.md / DESIGN.md / user prompt), NOT picked + * from a frozen 4-color preset. + * + * Why: 4-color frozen palettes drift toward safe defaults (warm-cream bg, + * complementary accent on near-white) regardless of brief. A single seed + + * the model's own composition lets the same seed produce a dark-mode jazz + * club or a light-mode hospitality brand depending on what the brief calls + * for. Tested empirically against curated 4-color palettes; seed approach + * wins on mood-fit in 3 of 5 cases and ties on the rest. + * + * Usage: + * node scripts/palette.mjs # pick at random + * node scripts/palette.mjs --id seed-021 # pick a specific seed + * node scripts/palette.mjs --from # hash to a seed (deterministic) + * + * Env vars: + * IMPECCABLE_PALETTE_SEED — same as --from; useful for the eval harness + * to make runs reproducible. + */ + +import crypto from 'node:crypto'; + +// Seeds are inlined (129 entries, hand-curated via a tinder review of +// ~400 candidates from ColorHunt + synthesis + Radix/brand/Pantone anchors). +// Each carries a mood + strategy the judging model produced — surfaced as +// hints, not commands; the brief still drives composition. +const SEEDS = [ + { id: "seed-200", oklch: [0.360, 0.137, 0.0], + mood: "Aesop apothecary shelf — oxblood bottle glass against linen, considered and unhurried", + strategy: "Seed is a deep desaturated red-brown that reads as brand ink itself; I push primary darker toward bottle-glass oxblood, pair with a pure white surface so the red does the work, and use a clear pale-blush accent that can carry dark text in pills." }, + { id: "seed-000", oklch: [0.400, 0.130, 0.0], + mood: "oxblood leather banquette in a 1940s steakhouse — low lamplight on dark wood and burgundy", + strategy: "Near-black bg with the faintest red undertone lets the oxblood primary glow like lamplit leather; warm cream ink and a brass accent complete the chophouse register." }, + { id: "seed-002", oklch: [0.450, 0.150, 0.0], + mood: "darkroom red light — analog photography, blood-warm safelight glow on chemical trays", + strategy: "Near-black surface with a deep oxblood primary lets the seed function like a safelight in a darkroom — the bg disappears so the red becomes the only emotional signal." }, + { id: "seed-003", oklch: [0.500, 0.194, 0.0], + mood: "darkroom safelight — the deep oxblood glow of analog photography, chemical and contemplative", + strategy: "Anchored the seed as primary against pure near-black so the red reads like a single illuminated bulb in a developing room, with cool desaturated ink to evoke silver gelatin print tones." }, + { id: "seed-004", oklch: [0.546, 0.204, 3.4], + mood: "midnight boudoir — velvet rose under low lamplight, perfumed and intimate", + strategy: "Near-black surface lets the rose seed glow like silk in shadow; a warm champagne accent provides the candle-flame counterpoint without breaking the hush." }, + { id: "seed-005", oklch: [0.550, 0.180, 0.0], + mood: "smoldering vermillion at dusk — the last red ember in a blacksmith's forge, iron-rich and quietly violent", + strategy: "Near-black gallery surround lets the seed read as glowing forged metal; ink stays warm-off-white, accent shifts to a hotter ember orange so the primary feels like cooling steel against a fresh strike." }, + { id: "seed-201", oklch: [0.647, 0.262, 0.3], + mood: "Figma plugin marketplace red — confident product-brand crimson, the kind a modern dev tool uses for a 'live' indicator or a primary CTA on a pristine docs page", + strategy: "Pure white surface lets a high-chroma crimson primary do all the brand work, paired with a hue-shifted warm coral accent for hierarchy without competing saturation" }, + { id: "seed-006", oklch: [0.650, 0.160, 0.0], + mood: "1960s Italian cinema — Technicolor lipstick red against a darkened theater", + strategy: "Pure near-black surface lets a saturated cinematic red and its warm peach accent perform like film light projected in a dark room — the brand colors carry the drama, the bg disappears." }, + { id: "seed-008", oklch: [0.520, 0.200, 10.4], + mood: "Negroni hour at a Milanese bar — bittersweet crimson, vermouth and amaro under low tungsten", + strategy: "Seed is a saturated red-crimson with cinematic weight, so I sit it on near-black to let the primary glow like backlit liquor, with a warmer amber accent acting as the citrus twist against the bitter red." }, + { id: "seed-010", oklch: [0.563, 0.223, 11.0], + mood: "Negroni hour on a Milan rooftop — bittersweet crimson, aperitivo light, polished restraint", + strategy: "Seed is a vivid carmine-red with strong chroma, so the surface gets out of the way (pure white) and lets the primary do the aperitivo work, with a cooled garnet accent for tension." }, + { id: "seed-202", oklch: [0.643, 0.247, 7.0], + mood: "Glossier brand pink — modern beauty editorial, confident and current", + strategy: "Pure white bg lets a saturated rose-red primary do all the brand work, paired with a deeper crimson accent for hierarchy — the Stripe/Glossier move where the color carries the mood." }, + { id: "seed-013", oklch: [0.400, 0.130, 20.0], + mood: "Tuscan cellar at dusk — aged terracotta, oxidized iron, the deep red of decanted Sangiovese", + strategy: "Black surface lets the oxblood seed and copper accent glow like firelight on cellar stone; brand colors carry all the warmth while the room recedes." }, + { id: "seed-014", oklch: [0.450, 0.150, 20.0], + mood: "smoldering tannery — oxblood leather, cured under low workshop light", + strategy: "Anchor the deep oxblood seed as primary against a near-black architectural ground, then lift with a single warm ember accent so the leather reads burnished rather than bloody." }, + { id: "seed-016", oklch: [0.550, 0.180, 20.0], + mood: "Negroni hour on a Roman terrace — bitter campari red, vermouth, late golden light spilling on white linen", + strategy: "Pure white surface lets the campari-red primary do all the emotional work, paired with a deeper oxblood accent for bittersweet depth — Italian aperitivo restraint, not warmth-washed." }, + { id: "seed-205", oklch: [0.634, 0.254, 17.6], + mood: "Aesop apothecary bottle — considered red-coral on a clinical white surface, the kind of brand restraint where one saturated object does all the work", + strategy: "Default A pure white surface lets a single coral-red primary carry the entire brand voice; accent shifts to a deeper oxblood for hierarchy without competing chroma." }, + { id: "seed-011", oklch: [0.639, 0.207, 13.5], + mood: "Aperitivo hour in Milan — Campari glow on a white marble bar, crisp and effervescent", + strategy: "Pure white gallery backdrop lets the Campari-red primary ring like a single bitter note; ink is near-black with a whisper of warmth, accent shifts to a deeper oxblood for hierarchy without competing hues." }, + { id: "seed-015", oklch: [0.527, 0.202, 22.7], + mood: "Negroni hour on a Milanese terrace — bittersweet vermillion, aperitivo glassware catching low sun", + strategy: "Seed becomes a saturated aperitivo-red primary against pure white so the color carries the bittersweet warmth alone, paired with a deep oxblood accent for typographic gravitas." }, + { id: "seed-023", oklch: [0.427, 0.175, 29.2], + mood: "blacksmith's forge at dusk — iron heated to ember red, the deep glow of oxidized metal and quenching oil", + strategy: "Pure black bg lets the seed's ember-red glow radiate like hot iron in a dark forge; accent shifts to a copper-amber to suggest scaling metal and sparks, while ink stays near-white for tool-precise legibility." }, + { id: "seed-206", oklch: [0.614, 0.234, 28.2], + mood: "Aesop apothecary bottle — considered red-orange on lab-white, calm utility with a single confident pigment", + strategy: "Pure white surface lets a saturated vermilion primary do all the brand work, paired with a deep oxblood accent for hierarchy without introducing a second hue family" }, + { id: "seed-029", oklch: [0.665, 0.222, 25.7], + mood: "Negroni hour at a Milanese bar — bittersweet orange-red liqueur catching late afternoon light on polished marble", + strategy: "Pure white surface lets the seed's vermilion read like Campari in a glass; a deeper oxblood accent provides the bitter depth, with neutral graphite ink keeping the editorial restraint of Italian design." }, + { id: "seed-022", oklch: [0.418, 0.155, 27.2], + mood: "Pompeiian red fresco — oxidized cinnabar on a museum wall, archaeological gravity", + strategy: "Pure black gallery surface lets the seed's iron-oxide red read as a lit artifact; accent shifts to an aged terracotta amber, so primary and accent form a fired-clay duet against neutral void." }, + { id: "seed-024", oklch: [0.464, 0.169, 26.9], + mood: "Mid-century darkroom under the safelight — developer trays, oxblood leather, the quiet patience of a print emerging", + strategy: "Seed becomes a deep oxblood primary; surface stays pure black so the red glows like a safelight, with a warmer ember accent for hierarchy" }, + { id: "seed-026", oklch: [0.489, 0.190, 28.3], + mood: "smoldering ember in a blacksmith's forge — iron-hot rust, soot, and controlled fire", + strategy: "Near-black soot background lets the seed's red-orange glow like heated metal; ink is bone-white, accent is a cooler tempered-steel orange that creates internal heat gradient with the primary." }, + { id: "seed-027", oklch: [0.568, 0.208, 27.1], + mood: "Sicilian blood orange at golden hour — citrus rind, terracotta, sun on stucco", + strategy: "Seed reads as vivid blood-orange — picked pure white surface so the citrus-red primary and a deep oxblood accent do all the emotional work, like a Loro Piana editorial spread." }, + { id: "seed-028", oklch: [0.591, 0.172, 24.0], + mood: "Sienna-fired ceramic studio at dusk — terracotta cooling on a wheel, hands still dusted with slip", + strategy: "Pure black stage lets the fired-clay primary glow like a kiln ember, with a deeper oxblood accent providing tonal weight rather than hue contrast — a monochrome warm-axis play." }, + { id: "seed-033", oklch: [0.544, 0.169, 31.3], + mood: "1960s Italian terracotta workshop — fired clay, espresso, late-afternoon Mediterranean dust", + strategy: "Pure black ground lets the seed's burnt-sienna primary glow like a lit kiln, with a deeper oxblood accent for restrained warmth tension — the brand carries the heat, the surface stays out." }, + { id: "seed-207", oklch: [0.564, 0.231, 29.1], + mood: "Aesop apothecary bottle — considered red oxide, the calm authority of a well-made object on a white shelf", + strategy: "Seed becomes the singular brand voice against pure white, with a deeper oxblood accent for hierarchy — the surface disappears so the red does all the speaking." }, + { id: "seed-035", oklch: [0.663, 0.153, 32.1], + mood: "Aesop apothecary bottle — clay-fired warmth, considered retail", + strategy: "Pure white surface lets the terracotta primary do the brand work, paired with a deep umber ink and a cooler clay accent for editorial tension." }, + { id: "seed-037", oklch: [0.590, 0.188, 35.8], + mood: "Aesop apothecary bottle — considered terracotta, herbalist restraint, the warmth comes from the glass not the room", + strategy: "Seed becomes a muted terracotta primary against pure white so the brand's warmth carries entirely through the color itself; accent shifts to a deeper umber for quiet hierarchy." }, + { id: "seed-038", oklch: [0.652, 0.229, 34.8], + mood: "blown-glass furnace at dusk — molten orange iron pulled from the kiln, a craftsman's signature heat", + strategy: "Pure black stage so the seed reads as live ember; primary holds the seed's heat, accent shifts to a brass-amber a hue-step away for a 1.7+ contrast pairing without leaving the fire." }, + { id: "seed-039", oklch: [0.653, 0.185, 33.5], + mood: "Aesop apothecary bottle — considered terracotta, quiet retail craft", + strategy: "Seed becomes a grounded clay primary against pure white, paired with a deeper umber accent so the warmth lives entirely in the brand marks, not the surface." }, + { id: "seed-167", oklch: [0.495, 0.134, 36.0], + mood: "Aesop apothecary shelf — burnished terracotta on clinical white, considered craft pharmacy", + strategy: "Treat the seed as a brand-carrying burnt-sienna against a pure paper-white surface so the warmth lives entirely in the primary, with a deep umber accent pulled along the same warm axis for typographic gravity." }, + { id: "seed-147", oklch: [0.500, 0.151, 40.0], + mood: "Aesop apothecary shelf — considered terracotta, pharmacy restraint, the brand color does the work against clinical white", + strategy: "Anchor the seed's burnt-sienna primary against a pure white surface so the rust speaks alone, with a deep umber ink and a cooler clay accent to give the palette product-brand discipline rather than environmental warmth." }, + { id: "seed-040", oklch: [0.660, 0.201, 40.0], + mood: "Aesop apothecary bottle — amber glass on a clean dispensary shelf, considered and clinical-warm", + strategy: "Seed becomes a burnt-amber primary against pure white so the bottle-glass color does the emotional work; accent shifts to a deep olive-bronze for the apothecary-label pairing." }, + { id: "seed-041", oklch: [0.673, 0.217, 38.6], + mood: "Aesop apothecary shelf — considered orange glass, clinical retail restraint", + strategy: "Pure white surface lets the burnt-orange primary do all the brand work, with a deep ink-brown for editorial gravity and a muted clay accent that reads as a sibling, not a contrast." }, + { id: "seed-042", oklch: [0.688, 0.133, 35.8], + mood: "Aesop apothecary shelf — terracotta glass, considered retail", + strategy: "Seed becomes a warm clay primary against pure white so the bottle-on-marble retail feel comes from the brand color alone; a deeper umber accent gives the label-print contrast." }, + { id: "seed-043", oklch: [0.781, 0.119, 38.1], + mood: "Aesop apothecary catalogue — considered terracotta, dermatological restraint, the warm color doing all the work against clinical white", + strategy: "Pure white surface lets the seed's warm clay tone read as the entire brand voice, paired with a deeper umber accent for hierarchy without competing with the primary's warmth." }, + { id: "seed-168", oklch: [0.400, 0.103, 50.0], + mood: "Aesop apothecary bottle — amber glass on a clinical white shelf, considered and pharmaceutical", + strategy: "Pure white surface lets the deep amber primary act like tinted glass against a clean shelf; accent is a muted clay that complements without competing, keeping the brand quiet and product-led." }, + { id: "seed-044", oklch: [0.568, 0.149, 45.9], + mood: "1970s desert highway at golden hour — sun-faded terracotta, denim dust, the warmth of a Polaroid pulled from a glovebox", + strategy: "Seed becomes a burnt-sienna primary against pure white so the terracotta does all the emotional work; a deep indigo accent acts as the denim shadow opposing the sun, creating the era's signature warm/cool tension without tinting the page." }, + { id: "seed-045", oklch: [0.607, 0.163, 47.7], + mood: "Aesop apothecary shelf — considered amber glass, clinical restraint, craft pharmacy", + strategy: "Pure white bg lets the burnt-amber primary do the apothecary work alone, paired with a deeper umber accent and graphite ink for editorial calm." }, + { id: "seed-046", oklch: [0.653, 0.175, 45.0], + mood: "Aesop apothecary shelf — considered amber glass, quiet luxury, restrained craft", + strategy: "Pure black backdrop lets the warm amber primary glow like backlit apothecary glass, with a deeper rust accent providing tonal depth in the same hue family — monochromatic warm against neutral void." }, + { id: "seed-047", oklch: [0.695, 0.205, 43.2], + mood: "Aesop apothecary label — sun-warmed amber glass on a clinical countertop, restrained botanical pharmacy", + strategy: "Pure white surface lets the burnt-amber primary and a deeper sienna accent do all the brand work, like an apothecary bottle photographed under daylight." }, + { id: "seed-051", oklch: [0.704, 0.189, 49.0], + mood: "blacksmith's forge at dusk — glowing iron, hammered copper, ember light against cooling steel", + strategy: "Pure near-black surface lets the seed's molten orange burn like heated metal; accent shifts to a deeper amber-red to suggest the cooling end of the same iron, while ink stays a clean off-white so type reads like chalk on slate." }, + { id: "seed-171", oklch: [0.550, 0.124, 60.0], + mood: "Klim Type Foundry specimen page — considered ochre on paper, design-school-honest", + strategy: "Seed becomes a muted ochre primary on pure white; accent is a deep ink-navy pulled across the wheel for editorial contrast without warmth-pooling in the bg" }, + { id: "seed-148", oklch: [0.650, 0.146, 60.0], + mood: "Klim-style editorial gold — late-afternoon paper light on a serif specimen sheet, considered and dry", + strategy: "Hold the seed's amber as primary on a pure white page so the gold reads as ink rather than atmosphere, and pair with a deep aubergine accent for typographic contrast." }, + { id: "seed-052", oklch: [0.700, 0.130, 60.0], + mood: "late-afternoon terracotta studio — sun-warmed clay, hands-on craft, the hour before dusk", + strategy: "Seed is a saturated amber-ochre with strong environmental association (ceramics, adobe, sunlit plaster), so I lean into Exception (a) with a faintly warm bone surface that reads as lime-washed wall, then deepen the seed slightly for primary and pair it with a fired-clay rust accent for hand-thrown warmth." }, + { id: "seed-053", oklch: [0.773, 0.157, 56.6], + mood: "late-summer apricot orchard at golden hour — sun-warmed fruit, considered Californian craft", + strategy: "Seed is a juicy mid-warm orange at daylight luminance — leaning optimistic/editorial, so pure white surface lets the apricot primary glow without muddying it; a deep wine accent provides the bite." }, + { id: "seed-149", oklch: [0.600, 0.124, 70.0], + mood: "1970s desert highway — late-afternoon amber light on chrome and asphalt", + strategy: "Anchor the amber seed as primary against pure black so the warm hue reads as headlight glow against night; a cooler dusk-mauve accent provides the complementary tension of horizon vs. sun." }, + { id: "seed-054", oklch: [0.740, 0.162, 68.1], + mood: "late-afternoon honey on terracotta — Mediterranean stucco at golden hour, sun-baked amber", + strategy: "Seed is a saturated honey-amber at high lightness; pairing it with pure black lets the warmth read as luminous gold against gravity, like lamplight in a dark room." }, + { id: "seed-055", oklch: [0.774, 0.174, 65.1], + mood: "late-summer honey hour — amber light slanting through a west-facing window, optimistic and golden", + strategy: "Anchor a saturated honey-amber primary on pure white so the warmth radiates from the brand itself, then pair with a deep teak accent for grounded contrast rather than tinting the canvas." }, + { id: "seed-056", oklch: [0.691, 0.146, 74.6], + mood: "Klim-style modern publishing house — late-afternoon paper warmth, considered editorial gold", + strategy: "Pure white surface so the amber seed becomes the brand voice; ink stays near-black neutral and accent shifts to a deep ink-blue to give the gold something structural to lean on." }, + { id: "seed-150", oklch: [0.750, 0.148, 80.0], + mood: "Klim Type Foundry specimen page — late-summer editorial gold, considered and grown-up", + strategy: "Pure white surface lets a single restrained ochre primary do all the brand work, paired with a deep ink-blue accent for typographic contrast in the Klim/Commercial Type tradition." }, + { id: "seed-058", oklch: [0.764, 0.120, 77.1], + mood: "Klim Type Foundry specimen page — late-afternoon ochre, considered editorial typography", + strategy: "Pure white surface lets the ochre primary do the brand work, paired with a deep ink-blue accent for editorial contrast — the type-foundry move where one warm hue carries the whole feeling against neutral paper." }, + { id: "seed-059", oklch: [0.784, 0.144, 79.8], + mood: "late afternoon in a Tuscan limonaia — sun-cured amber on whitewashed plaster", + strategy: "Pure white surface lets the saffron-amber primary and a deep olive accent carry the Mediterranean warmth, with split-complementary tension between gold and a quiet evergreen." }, + { id: "seed-061", oklch: [0.817, 0.161, 75.1], + mood: "late-afternoon honey on Tuscan limestone — golden hour, slow and luminous", + strategy: "Pure white surface lets the amber primary glow like sunlight on a wall, paired with a deep terracotta accent for warm tonal contrast within the same hue family." }, + { id: "seed-063", oklch: [0.842, 0.165, 91.3], + mood: "late-afternoon Tuscan sun on limestone — golden hour, considered, optimistic", + strategy: "Pure white surface lets the amber-gold primary radiate as the mood-carrier, with a deep aubergine accent providing the long shadow that golden light needs to feel three-dimensional." }, + { id: "seed-174", oklch: [0.350, 0.075, 110.0], + mood: "olive grove at late afternoon — sun-cured leaves, dust, and quiet Mediterranean weight", + strategy: "Pure white surface lets a deep, sun-cured olive primary do the emotional work, with a burnt-terracotta accent providing the warm-earth counterpoint olive groves are known for." }, + { id: "seed-117", oklch: [0.650, 0.100, 110.0], + mood: "Klim-style editorial sage — late-summer foundry catalogue, considered olive-yellow on paper", + strategy: "Seed sits at olive-chartreuse; treating it as a quiet typographic primary on pure paper, with a deeper bronze-olive accent for hierarchy — the color does the work, the page disappears." }, + { id: "seed-118", oklch: [0.750, 0.090, 110.0], + mood: "Klim Type Foundry specimen page — late-summer olive light on a working specimen, the honesty of a type designer showing their work", + strategy: "Pure white bg lets a desaturated olive-yellow primary do the editorial work, with a deeper olive-bronze accent providing typographic emphasis the way a specimen uses one heavy weight against the body roman." }, + { id: "seed-065", oklch: [0.797, 0.166, 113.1], + mood: "late-summer olive grove at noon — sun-bleached leaves, dry stone, Mediterranean glare", + strategy: "Hold the seed as a luminous chartreuse-olive primary against pure white so the color reads as sunlit foliage, pairing it with a deep umber accent for the dry-stone contrast." }, + { id: "seed-176", oklch: [0.300, 0.071, 120.0], + mood: "moss-darkened apothecary jar — herbal, shadowed, mid-19th-century botanical study", + strategy: "Seed is a deep desaturated olive-green that reads as preserved botanical pigment; I anchor it on pure white so the dim moss-green primary feels like ink on a herbarium page, with a warm ochre accent supplying the aged-paper counterpoint." }, + { id: "seed-155", oklch: [0.550, 0.142, 130.0], + mood: "moss-bed forest floor at noon — chlorophyll, lichen, sunlit fern", + strategy: "Seed is a confident mid-olive green with strong chroma; mood is daylight botanical, so I let the brand greens do the work on a pure paper-white bg and pair with a warm umber accent for fern-against-bark contrast." }, + { id: "seed-119", oklch: [0.600, 0.154, 130.0], + mood: "moss garden at Saihō-ji — damp stone, filtered green light through old cedar", + strategy: "Pure near-black bg lets the seed's mossy green glow like wet lichen under low light; accent shifts to a pale ochre-gold like sun catching through canopy." }, + { id: "seed-179", oklch: [0.300, 0.096, 140.0], + mood: "moss on wet stone — forest floor at dusk, deep botanical hush", + strategy: "Kept the seed's deep moss green as primary against a near-black surface so the green reads as living shadow, with a pale lichen accent providing the single point of light." }, + { id: "seed-180", oklch: [0.350, 0.110, 140.0], + mood: "moss-darkened apothecary — herbal tinctures in amber glass, pressed botanicals, the deep green of a conservatory at dusk", + strategy: "Near-black bg with a whisper of green undertone lets the seed's deep moss read as luminous foliage; a warm parchment accent provides the apothecary-label counterpoint without breaking the herbal register." }, + { id: "seed-120", oklch: [0.650, 0.100, 140.0], + mood: "moss on weathered stone — quiet botanical garden conservatory at midday", + strategy: "Pure white bg lets the muted sage-green primary read as a considered botanical mark, with a deeper terracotta accent providing earthen counterpoint without breaking the gallery-like restraint." }, + { id: "seed-121", oklch: [0.750, 0.090, 140.0], + mood: "moss garden at Saihō-ji — diffuse green light filtered through wet stone and lichen", + strategy: "Pure near-black bg lets the muted sage-green primary glow like lichen under low light; a warm pale-bone accent acts as the single ray of sun cutting through canopy." }, + { id: "seed-182", oklch: [0.400, 0.106, 150.0], + mood: "moss garden at Saiho-ji — deep cultivated green under wet stone shadow, contemplative and damp", + strategy: "Near-black bg with the faintest cool-green undertone evokes shaded stone; primary holds the seed's moss tone while accent shifts to a lichen-yellow for organic counterpoint without breaking the hush." }, + { id: "seed-157", oklch: [0.550, 0.145, 150.0], + mood: "moss garden at Saiho-ji — damp stone, filtered green light through cedar canopy", + strategy: "Near-black bg with a faint green undertone evokes deep forest shadow; primary holds the seed's verdant register while accent shifts to a pale lichen-cream to mimic light catching moss." }, + { id: "seed-122", oklch: [0.600, 0.158, 150.0], + mood: "forest floor at first light — moss, lichen, and clean morning air", + strategy: "Seed reads as a living, daylight green; surface stays pure white so the green carries the freshness, with a cool teal accent pulling it toward dew rather than earth." }, + { id: "seed-195", oklch: [0.650, 0.150, 145.0], + mood: "Considered horticulture brand — botanical research lab, the green of a healthy stem photographed in clean daylight", + strategy: "Pure white surface lets the seed's vegetal green carry the entire brand voice, paired with a deep forest ink and a warm clay accent for editorial contrast." }, + { id: "seed-183", oklch: [0.350, 0.077, 160.0], + mood: "moss-stained apothecary — deep forest glass, herbal tinctures shelved in low candlelight", + strategy: "Anchored the seed as primary and built a near-black dark surface with whisper-tinted green to evoke aged apothecary glass, letting the green glow rather than shout." }, + { id: "seed-184", oklch: [0.400, 0.087, 160.0], + mood: "deep forest apothecary — moss, bottle glass, and herbal tincture under afternoon light", + strategy: "Seed becomes a botanical-bottle-green primary on pure white, paired with a warm clove-amber accent to evoke herbal pharmacy contrast without tinting the surface." }, + { id: "seed-158", oklch: [0.550, 0.119, 160.0], + mood: "moss on wet stone — forest floor after rain, mineral and quiet", + strategy: "Pure white surface lets the deep mossy green carry the entire mood; accent shifts to a damp slate-teal to sit beside primary like lichen on stone without competing." }, + { id: "seed-159", oklch: [0.600, 0.130, 160.0], + mood: "moss-covered forest apothecary — herbal tinctures in amber glass, eucalyptus shadow", + strategy: "Anchored the green seed in a near-black backdrop so it reads like botanical glassware lit from within, with a warm amber accent pulled across the wheel to evoke tincture bottles against dark wood." }, + { id: "seed-185", oklch: [0.450, 0.086, 170.0], + mood: "weathered copper patina on a Pacific Northwest greenhouse — oxidized teal, glass light, botanical hush", + strategy: "Seed sits as a deep oxidized-teal primary against pure white so the patina reads as pigment, not atmosphere; a rust-copper accent completes the verdigris/oxidation story across the warm-cool axis." }, + { id: "seed-124", oklch: [0.750, 0.080, 170.0], + mood: "sea-glass on a foggy Pacific shoreline — weathered, mineral, quietly oxidized", + strategy: "Seed is a soft desaturated teal-green; pairing it on pure white lets the mineral primary read as patinated copper-glass, with a deeper kelp-toned primary and a rusted coral accent to spark the muted teal against its complement." }, + { id: "seed-160", oklch: [0.550, 0.095, 180.0], + mood: "weathered copper patina on a museum bronze — oxidized teal, conservatorial quiet", + strategy: "Pure near-black gallery surround lets the patina-teal primary glow like a lit artifact, with a warm verdigris-adjacent accent providing the oxidation contrast against the cool seed." }, + { id: "seed-161", oklch: [0.720, 0.100, 188.0], + mood: "climate-tech dashboard — calm verdigris on plain paper, the quiet confidence of an instrument that just works", + strategy: "Seed teal carries the entire mood as a single considered brand color on pure white, with a desaturated copper accent providing warm signal against the cool primary without competing for attention." }, + { id: "seed-186", oklch: [0.450, 0.074, 200.0], + mood: "deep hydrothermal vent — mineral teal under pressure, the cold blue-green of oxidized copper in submerged light", + strategy: "Near-black surface lets the mineral teal glow as if lit from within; accent shifts toward verdigris-copper to suggest patina on submerged metal, while ink stays cool-neutral to keep the register austere rather than aquatic-cute." }, + { id: "seed-125", oklch: [0.650, 0.100, 200.0], + mood: "climate-tech dashboard — calm operational teal, the color of clean water data and atmospheric sensors", + strategy: "Pure white surface lets a single muted-teal primary do all the brand work, with a deeper marine accent providing hierarchy without competing chroma." }, + { id: "seed-126", oklch: [0.750, 0.080, 200.0], + mood: "climate-tech product brand — quiet competence, dashboards for hard infrastructure problems", + strategy: "Hold the seed's muted teal as primary, pair with a sharper cyan-leaning accent for interactive lift, and let a pure white surface do the disappearing act so the brand reads as a tool, not an atmosphere." }, + { id: "seed-162", oklch: [0.550, 0.091, 210.0], + mood: "weathered nautical instrument — patinated brass on oxidized steel, the cool blue-grey of a ship's chronometer at dawn", + strategy: "Pure white surface lets the muted teal-steel primary read as a precise instrument mark, with a warm brass accent providing the single point of patina against clinical white." }, + { id: "seed-163", oklch: [0.450, 0.086, 230.0], + mood: "deep harbor at dusk — weathered nautical instruments, brass dials on oxidized steel", + strategy: "Near-black background with subtle cool tint evokes the marine dusk; primary holds the seed's teal-blue while a warm brass accent creates the instrument-on-steel tension." }, + { id: "seed-164", oklch: [0.550, 0.105, 230.0], + mood: "deep harbor at dawn — cold steel water, fog-muted light, the quiet before the boats leave", + strategy: "Pure near-black bg lets the seed's cold marine blue read as a luminous beacon, while a pale frost-cyan accent evokes diffused dawn light cutting through fog." }, + { id: "seed-127", oklch: [0.650, 0.100, 230.0], + mood: "climate-tech dashboard — atmospheric sensor blue, calm operational clarity", + strategy: "Anchor the seed as a confident mid-blue primary on pure white so the brand color carries all the atmospheric feeling, with a deep navy accent for hierarchy and a soft slate muted for body text." }, + { id: "seed-128", oklch: [0.750, 0.080, 230.0], + mood: "climate-tech dashboard — calm atmospheric data, considered sky-blue", + strategy: "Pure white surface lets the muted sky-blue primary carry the meteorological calm, with a deep-navy accent providing readable weight against the soft primary." }, + { id: "seed-187", oklch: [0.350, 0.078, 240.0], + mood: "deep harbor at blue hour — wet stone, cold steel, the quiet before night fully lands", + strategy: "Near-black architectural bg with a hint of marine chroma lets the seed read as ambient atmosphere rather than UI chrome; a cooler steel accent sits opposite the warmer-shifted primary for navigational clarity." }, + { id: "seed-077", oklch: [0.578, 0.130, 241.7], + mood: "pre-dawn signal tower — cold blue solitude, instruments glowing against the dark", + strategy: "Pure near-black bg lets the seed's cold tower-light blue glow as the sole emotional source, with a frost-cyan accent acting as a secondary indicator light." }, + { id: "seed-188", oklch: [0.400, 0.110, 250.0], + mood: "Linear's considered indigo — the calm authority of a well-built developer tool, blueprint ink on a clean page", + strategy: "Held the seed as a deep indigo primary against pure white so the brand color carries all the gravity; accent shifts to a cooler, brighter cyan-blue to create a crisp hierarchy pair without warming the surface." }, + { id: "seed-165", oklch: [0.450, 0.123, 250.0], + mood: "blueprint room at dusk — drafting table, graphite, civic-engineering blue", + strategy: "Seed is a mid-deep architectural blue with real chroma and no environmental cue, so I stay out of the way with a pure white surface and let the primary do all the talking, pairing it with a burnt-ochre accent for drafting-pencil contrast." }, + { id: "seed-079", oklch: [0.478, 0.136, 251.8], + mood: "twilight cartography — the blue of deep dusk over open water, precise and navigational", + strategy: "Pure white surface lets the seed's oceanic blue act as a single navigational anchor, with a warm amber accent struck across it like a lighthouse beam at dusk." }, + { id: "seed-080", oklch: [0.541, 0.122, 248.2], + mood: "Linear-style considered tool blue — the calm, exact register of a modern engineering app where every pixel is intentional", + strategy: "Pure white surface lets the considered indigo-blue primary carry the entire brand; a deeper navy accent provides hierarchy without warmth, keeping the palette in a single cool family for that focused-software feel" }, + { id: "seed-166", oklch: [0.550, 0.149, 250.0], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and quietly intense", + strategy: "Near-black bg with the faintest cool tint reads like a darkened cockpit; the seed becomes a luminous instrument-blue primary, paired with a warm amber accent that mimics avionics readouts for unmistakable signal contrast." }, + { id: "seed-081", oklch: [0.650, 0.160, 250.0], + mood: "deep-sea research vessel at dawn — instrument glow against cold steel light", + strategy: "Pure near-white bg keeps the palette technical and instrument-like; the seed blue holds as primary while a desaturated steel-cyan accent reads like signal readouts on glass." }, + { id: "seed-082", oklch: [0.742, 0.140, 247.4], + mood: "high-altitude flight deck at dawn — cold cabin instruments glowing against a sky still holding night", + strategy: "Near-black cockpit ground with a faint blue cast lets the seed read as an illuminated instrument; primary holds the seed, accent shifts to cyan for signal/indicator contrast." }, + { id: "seed-210", oklch: [0.360, 0.140, 260.0], + mood: "Linear-style considered tool indigo — late-night focused work, the deep blue of a code editor at 2am where everything else falls away", + strategy: "Pure black bg lets the indigo primary carry all the cognitive-focus weight, with a slightly brighter periwinkle accent for interactive lift — the surface disappears so the tool feels weightless." }, + { id: "seed-189", oklch: [0.400, 0.130, 260.0], + mood: "pre-dawn observatory — cold instrument blue, star-chart precision", + strategy: "Seed becomes the primary on pure black so the deep instrument-blue glows like a calibration light, with a faint cyan accent reading as starlight against the void." }, + { id: "seed-211", oklch: [0.420, 0.161, 260.0], + mood: "Linear's considered indigo — the tool-for-thought blue of focused product work, calm authority without coldness", + strategy: "Hold the seed as a deep indigo primary against pure white, then pair with a slightly warmer, lighter periwinkle accent to create gentle hue separation without breaking the disciplined tool-brand register." }, + { id: "seed-129", oklch: [0.450, 0.150, 260.0], + mood: "pre-dawn observatory — deep cobalt sky just before astronomical twilight, instruments cool to the touch", + strategy: "Near-black surface lets the cobalt seed read as luminous starlight; a single warm amber accent acts as the calibration lamp against the cold blue field." }, + { id: "seed-084", oklch: [0.476, 0.207, 261.2], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and awake", + strategy: "Default B black bg lets the cobalt primary read as a luminous instrument signal, with a cyan accent striking the analogous 'cockpit display' relationship." }, + { id: "seed-085", oklch: [0.681, 0.132, 258.4], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky", + strategy: "Anchored the seed as a luminous primary against a near-black architectural ground, with a warm amber accent acting as the single instrument light cutting through cold blue." }, + { id: "seed-086", oklch: [0.767, 0.106, 255.9], + mood: "Scandinavian winter morning — quiet light through frost, pale sky over snow", + strategy: "Anchored a pure white editorial stage so the seed's cool sky-blue reads as crisp polar light, with a deeper navy primary providing the only saturated weight — like a single dark pine against snow." }, + { id: "seed-083", oklch: [0.340, 0.159, 262.4], + mood: "deep cobalt twilight — the moment after sunset when the sky goes electric blue and city windows start to glow", + strategy: "Pure black stage lets the cobalt seed act as a luminous neon-window glow, with a warm amber accent across the wheel for the lit-window contrast." }, + { id: "seed-212", oklch: [0.360, 0.219, 270.0], + mood: "Linear-grade tooling indigo — considered software for people who care about craft", + strategy: "Anchored the deep indigo seed as primary on a pure white surface so the brand color carries all the weight, with a slightly cooler violet-blue accent for hierarchy without competing chroma." }, + { id: "seed-130", oklch: [0.400, 0.150, 270.0], + mood: "Linear-grade indigo — considered productivity tool, ink on paper, no theatrics", + strategy: "Pure white surface lets a deep cool indigo carry all the brand weight, paired with a slightly warmer violet-blue accent for hierarchy without acid." }, + { id: "seed-213", oklch: [0.411, 0.241, 267.9], + mood: "Linear-style indigo — considered tool surface, the kind of blue-violet that sits behind a developer's keyboard at 11pm without shouting", + strategy: "Pure black canvas lets a saturated indigo primary do all the brand work, with a cooler cyan-violet accent providing UI signal without competing." }, + { id: "seed-131", oklch: [0.450, 0.180, 270.0], + mood: "monastic indigo dusk — vespers light through stained glass, contemplative and severe", + strategy: "Seed becomes a deep indigo primary against pure near-black so the violet reads as luminous stained-glass against architectural shadow, with a cooler iris accent for tonal lift." }, + { id: "seed-088", oklch: [0.476, 0.158, 268.5], + mood: "pre-dawn astronomer's notebook — deep indigo sky just before the stars fade, ink and graphite", + strategy: "Near-black bg with the faintest cool tint to evoke night sky without theatrics; primary holds the seed's indigo, accent shifts to a paler periwinkle for stellar contrast, keeping the palette monochromatic-cool and observational." }, + { id: "seed-196", oklch: [0.530, 0.130, 268.0], + mood: "Linear-style considered tool indigo — the deep-focus blue-violet of a thoughtfully built productivity surface, the color of a well-typeset keyboard shortcut", + strategy: "Pure white bg lets the indigo seed do all the brand work as primary, with a slightly darker, more saturated violet-shifted accent for hierarchy and interactive states — the surface disappears so the brand color reads as the entire identity." }, + { id: "seed-132", oklch: [0.700, 0.120, 270.0], + mood: "Linear-style considered tool indigo — the quiet violet of a focused product workspace, late-afternoon thinking", + strategy: "Pure white surface lets a muted indigo-violet primary and a slightly cooler accent do all the brand work, keeping the register calm and software-like rather than theatrical." }, + { id: "seed-090", oklch: [0.445, 0.206, 279.1], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, not a nightclub", + strategy: "Anchor the seed as a confident product primary on pure white, with a cooler indigo-shift accent that reads as a sibling tool color, so the brand violet does all the emotional work." }, + { id: "seed-133", oklch: [0.500, 0.160, 280.0], + mood: "Linear-adjacent indigo — considered productivity tool, the violet of a thinking workspace", + strategy: "Seed becomes a measured indigo primary on pure white; accent shifts to a cooler blue-violet to create hierarchy without nightclub saturation, letting the brand color do all the emotional work." }, + { id: "seed-094", oklch: [0.533, 0.125, 294.3], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, calm authority for a creative workspace", + strategy: "Pure white canvas lets the indigo-violet primary carry the entire brand voice; accent shifts hue slightly toward blue for a cool, tool-like duotone rather than warm decorative pairing." }, + { id: "seed-137", oklch: [0.700, 0.120, 290.0], + mood: "Linear-adjacent indigo — the considered tool, late-evening focus mode, software made for people who care about craft", + strategy: "Pure black surface lets a single restrained indigo-violet carry the brand, with a cooler periwinkle accent providing UI hierarchy without competing — Vercel/Linear dark-mode discipline." }, + { id: "seed-100", oklch: [0.450, 0.150, 330.0], + mood: "velvet boudoir at last call — bruised orchid and lipstick traces under low lamplight", + strategy: "Pure near-black surface lets a deep magenta-rose primary smolder while a warm peach accent acts like skin-lit lamplight — drama lives in the brand pair, not the room." }, + { id: "seed-103", oklch: [0.650, 0.160, 330.0], + mood: "1980s Memphis boudoir — powder-pink neon humming against lacquered black, lipstick and lacquer", + strategy: "Near-black gallery surface lets the magenta-pink seed read as lit neon; accent shifts to warm coral to create cinematic dichromatic tension without competing chroma." }, + { id: "seed-228", oklch: [0.360, 0.147, 340.0], + mood: "Figma-era creative tool plum — considered productivity software for designers, the inky violet of a serif wordmark on a marketing site", + strategy: "Held the seed as a deep plum primary against pure white so the brand color does the emotional work; paired with a muted rose accent for warmth without breaking the productivity-tool restraint." }, + { id: "seed-107", oklch: [0.500, 0.200, 340.0], + mood: "Figma plum — creative-tool confidence, considered magenta for a modern design product", + strategy: "Pure white surface lets a saturated magenta-plum primary carry all the brand voice, paired with a cooler violet-leaning accent for hierarchy without competing." }, + { id: "seed-198", oklch: [0.600, 0.210, 340.0], + mood: "Figma-era creative tool plum — confident, considered, made for makers", + strategy: "Anchor a saturated plum primary against pure white so the brand color does all the emotional work, with a deeper magenta-rose accent for hierarchy." }, + { id: "seed-112", oklch: [0.754, 0.193, 343.4], + mood: "Figma-era creative tool — confident pink primary doing the brand work on a clean canvas, the way Linear uses indigo or Stripe uses violet", + strategy: "Anchor the seed pink as a saturated brand primary on pure white so the color carries all the personality; pair with a cooler plum accent to give the pink something to push against without competing." }, + { id: "seed-229", oklch: [0.420, 0.163, 350.0], + mood: "considered fintech rose — the deep magenta of a modern product brand (think Stripe-adjacent, but rotated toward berry), confident and current", + strategy: "pure white surface lets a single deep berry-rose primary do all the brand work, paired with a cooler indigo accent for the contrast move you see in modern product marketing" }, + { id: "seed-113", oklch: [0.470, 0.173, 354.8], + mood: "1960s velvet rope nightclub — crushed magenta, low light, cigarette smoke catching a spotlight", + strategy: "Pure black stage so the seed's smoky magenta reads as a single hot spotlight, paired with a cooler violet accent for the second light cue." }, + { id: "seed-114", oklch: [0.570, 0.158, 353.3], + mood: "fin-de-siècle Parisian rose — velvet curtain, theatre program, lipstick blotted on linen", + strategy: "Drop bg to true black so the dusty-rose primary reads as stage-lit silk; accent shifts to a warmer coral-mauve at higher lightness to create gentle hue rotation without breaking the romance." }, + { id: "seed-199", oklch: [0.650, 0.180, 350.0], + mood: "modern fintech rose — the considered pink of a Series B brand mark, confident and current without nostalgia", + strategy: "Pure white surface lets a saturated rose primary do the brand work, paired with a deep plum accent for hierarchy — the Stripe move applied to a pink hue." }, + { id: "seed-115", oklch: [0.636, 0.218, 355.3], + mood: "backstage at a cabaret — velvet rope, lipstick mark on a champagne glass", + strategy: "Seed reads as a saturated stage-light magenta-red; I push it into pure black so the primary glows like a neon sign and the accent (a cold pearl-pink) acts as the spotlight rim — the room is dark, the color does the singing." }, + { id: "seed-230", oklch: [0.650, 0.249, 354.5], + mood: "Modern fintech rose — the considered pink of a contemporary payments brand: confident, alive, and clear-headed", + strategy: "Pure white bg lets a saturated rose-magenta primary carry all the brand energy, paired with a cooler indigo accent for trustworthy contrast — the Stripe move applied to a pink hue." }, + { id: "seed-231", oklch: [0.682, 0.241, 353.2], + mood: "Figma-era creative tool — a confident pink-magenta product brand, the kind a modern design platform uses to feel alive without shouting", + strategy: "Default A pure white bg lets the saturated pink-magenta primary do all the brand work, with a near-complementary cool teal accent for tool-like clarity and a neutral ink for editorial calm" }, + { id: "seed-116", oklch: [0.734, 0.183, 356.8], + mood: "modern beauty brand DTC — Glossier-adjacent pink, confident and current without being saccharine", + 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." }, +]; + +function parseArgs(argv) { + const args = { id: null, from: null }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === '--id' && argv[i + 1]) { args.id = argv[++i]; } + else if (a === '--from' && argv[i + 1]) { args.from = argv[++i]; } + } + return args; +} + +// Hash a key into a stable float in [0, 1) for deterministic weighted picks. +function hashUnit(key) { + const h = crypto.createHash('sha256').update(key).digest(); + return h.readUInt32BE(0) / 0x100000000; +} + +// The curated library is hue-skewed (more reds/oranges than teals/magentas) +// because that's where the source material + taste landed. Left uniform, a +// random pick would land on red ~1/3 of the time. Inverse-frequency weighting +// gives each seed a weight of 1/(count in its 30° hue bucket), so each hue +// ZONE is roughly equally likely to be chosen regardless of how many seeds it +// holds — fair rainbow exposure across runs without pruning the library. +function buildWeights(seeds) { + const bucketCount = {}; + const bucketOf = (s) => Math.floor(((s.oklch[2] % 360) + 360) % 360 / 30); + for (const s of seeds) { const b = bucketOf(s); bucketCount[b] = (bucketCount[b] || 0) + 1; } + const weights = seeds.map((s) => 1 / bucketCount[bucketOf(s)]); + const total = weights.reduce((a, b) => a + b, 0); + return { weights, total }; +} + +function weightedPick(seeds, unit) { + const { weights, total } = buildWeights(seeds); + let target = unit * total; + for (let i = 0; i < seeds.length; i++) { + target -= weights[i]; + if (target < 0) return seeds[i]; + } + return seeds[seeds.length - 1]; +} + +function pickSeed(seeds, { id, from }) { + if (id) { + const found = seeds.find(s => s.id === id); + if (!found) { console.error(`no seed with id "${id}"`); process.exit(2); } + return found; + } + const envFrom = process.env.IMPECCABLE_PALETTE_SEED; + const key = from || envFrom; + const unit = key ? hashUnit(key) : Math.random(); + return weightedPick(seeds, unit); +} + +function fmtOklch([L, C, H]) { + return `oklch(${L.toFixed(3)} ${C.toFixed(3)} ${H.toFixed(1)})`; +} + +function hueWord(H) { + if (H < 15 || H >= 345) return 'pure red'; + if (H < 35) return 'warm red / crimson'; + if (H < 55) return 'warm coral / burnt orange'; + if (H < 80) return 'orange / honey'; + if (H < 105) return 'warm amber / honey-gold'; + if (H < 135) return 'yellow-green / olive'; + if (H < 170) return 'green'; + if (H < 200) return 'teal'; + if (H < 230) return 'sky blue'; + if (H < 265) return 'cobalt / indigo'; + if (H < 295) return 'violet / purple'; + if (H < 330) return 'magenta / pink'; + return 'deep pink / rose'; +} + +// --------------------------------------------------------------- + +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}` : ''; + +// --------------------------------------------------------------- +// Fat tool-exit response — what the model sees on stdout. +// --------------------------------------------------------------- + +process.stdout.write(`BRAND SEED · ${seed.id} + +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. + +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. + +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é. + + 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 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. + + 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. + +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): + + - 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. + +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. + +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. +`); diff --git a/.rovodev/skills/impeccable/scripts/pin.mjs b/.rovodev/skills/impeccable/scripts/pin.mjs index ba02783fb..320d98ce1 100644 --- a/.rovodev/skills/impeccable/scripts/pin.mjs +++ b/.rovodev/skills/impeccable/scripts/pin.mjs @@ -27,7 +27,7 @@ const HARNESS_DIRS = [ // Valid sub-command names const VALID_COMMANDS = [ - 'craft', 'teach', 'extract', 'document', 'shape', + 'craft', 'init', 'extract', 'document', 'shape', 'critique', 'audit', 'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live', 'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive', diff --git a/.trae-cn/skills/impeccable/SKILL.md b/.trae-cn/skills/impeccable/SKILL.md index be1d1aada..c76f05c5b 100644 --- a/.trae-cn/skills/impeccable/SKILL.md +++ b/.trae-cn/skills/impeccable/SKILL.md @@ -1,95 +1,90 @@ --- name: impeccable description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks. -version: 3.1.1 +version: 3.5.0 user-invocable: true -argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · teach|document|extract|live] [target]" -license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution. +argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]" +license: Apache 2.0 --- Designs and iterates production-grade frontend interfaces. Real working code, committed design choices, exceptional craft. ## Setup -Before any design work or file edits: +You MUST do these steps before proceeding: -1. Load context (PRODUCT.md / DESIGN.md) via the loader script. -2. Identify the register and load the matching register reference (brand.md or product.md). -3. **If the user invoked a sub-command (e.g. `craft`, `shape`, `audit`), load its reference file too.** This is non-negotiable: `craft` without `craft.md` loaded means you'll skip the shape-and-confirm step the user expects. +1. Run `node .trae-cn/skills/impeccable/scripts/context.mjs` once per session. If you've already seen its output in this conversation, do not re-run it. The script either prints the project's PRODUCT.md (and DESIGN.md when present) as a markdown block, or tells you it's missing. Follow whatever it prints. **If it reports `NO_PRODUCT_MD`, stop and follow `reference/init.md` before doing anything else.** If the output ends with an `UPDATE_AVAILABLE` directive, follow it (ask the user once about updating, then continue). It never blocks the current task. +2. If the user invoked a sub-command (`craft`, `shape`, `audit`, `polish`, ...), you MUST read `reference/.md` next. Non-optional. The reference defines the command's flow; without it you will skip steps the user expects. +3. Familiarize yourself with any existing design system, conventions, and components in the code. Read at least one project file (CSS / tokens / theme / a representative component or page). **Required even when you've loaded a sub-command reference in step 2.** Don't reinvent the wheel; use what's there when it works, branch out when the UX wins. +4. Read the matching register reference. **This is non-optional; skipping it produces generic output.** If the project is marketing, a landing page, a campaign, long-form content, or a portfolio (design IS the product), read `reference/brand.md`. If it is app UI, admin, a dashboard, or a tool (design SERVES the product), read `reference/product.md`. Pick by first match: (1) task cue ("landing page" vs "dashboard"); (2) surface in focus (the page, file, or route being worked on); (3) `register` field in PRODUCT.md. +5. **If the project is brand-new (no existing CSS tokens / theme / committed brand colors found in step 3)**, run `node .trae-cn/skills/impeccable/scripts/palette.mjs` to receive a brand seed color and composition guidance. This is the anchor for your primary brand color. Compose the rest of the palette (bg, surface, ink, accent, muted) around it per the script's instructions. Use OKLCH throughout. **Skip this step only if step 3 found committed brand colors in existing tokens; in that case identity-preservation wins.** -Skipping these produces generic output that ignores the project. +## Design guidance -### 1. Context gathering +Produce ready-to-ship, production-grade code, not prototypes or starting points. Take no shortcuts unless the user asks for them (when in doubt, ask). Don't stop until arriving at a complete implementation (beautiful, responsive, fast, precise, bug-free, on brand). You take attention to detail seriously: every page, section or component crafted is battle tested using the tools available to you (browser screenshotting, computer use, etc). the model is capable of extraordinary work. Don't hold back. -Two files, case-insensitive. The loader looks at the project root by default and falls back to `.agents/context/` and `docs/` if the root is clean. Override with `IMPECCABLE_CONTEXT_DIR=path/to/dir` (absolute or relative to cwd). +### General rules -- **PRODUCT.md**: required. Users, brand, tone, anti-references, strategic principles. -- **DESIGN.md**: optional, strongly recommended. Colors, typography, elevation, components. +#### Color -Load both in one call: +- **Verify contrast.** Body text must hit ≥4.5:1 against its background; large text (≥18px or bold ≥14px) needs ≥3:1. Placeholder text needs the same 4.5:1, not the muted-gray default. The most common failure: muted gray body text on a tinted near-white. If the contrast is even close, bump the body color toward the ink end of the ramp; light gray "for elegance" is the single biggest reason AI designs feel hard to read. +- Gray text on a colored background looks washed out. Use a darker shade of the background's own hue, or a transparency of the text color. -```bash -node .trae-cn/skills/impeccable/scripts/load-context.mjs -``` +#### Typography -Consume the full JSON output. Never pipe through `head`, `tail`, `grep`, or `jq`. The output's `contextDir` field tells you where the files were resolved from. +- Cap body line length at 65–75ch. +- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales. +- Cap font-family count at 3 (display + body + optional mono). More than 3 reads as indecision, not richness. One well-tuned family with weight contrast usually beats three competing typefaces. +- Don't pair fonts that are similar but not identical (two geometric sans-serifs, two humanist sans-serifs). Pair on a contrast axis (serif + sans, geometric + humanist) or use one family in multiple weights. +- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. +- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. +- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". +- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. -If the output is already in this session's conversation history, don't re-run. Exceptions requiring a fresh load: you just ran `/impeccable teach` or `/impeccable document` (they rewrite the files), or the user manually edited one. +#### Layout -`/impeccable live` already warms context via `live.mjs`. If you've run `live.mjs`, don't also run `load-context.mjs` this session. +- Vary spacing for rhythm. +- Cards are the lazy answer. Use them only when they're truly the best affordance. Nested cards are always wrong. +- Flexbox for 1D, Grid for 2D. Don't default to Grid when `flex-wrap` would be simpler. +- For responsive grids without breakpoints: `repeat(auto-fit, minmax(280px, 1fr))`. +- Build a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip). Never arbitrary values like 999 or 9999. -If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `/impeccable teach`, then resume the user's original task with the fresh context. If the original task was `/impeccable craft`, resume into `/impeccable shape` before any implementation work. +#### Motion +- Motion should be intentional, and not be an afterthought. consider it as part of the build. +- Don't animate CSS layout properties unless truly needed. +- Ease out with exponential curves (ease-out-quart / quint / expo). No bounce, no elastic. +- Use libraries for more advanced motion needs (e.g. motion, gsap, anime.js, lenis etc) +- Reduced motion is not optional. Every animation needs a `@media (prefers-reduced-motion: reduce)` alternative: typically a crossfade or instant transition. +- Staggering the items within one list is legitimate. The tell is the uniform reflex (one identical entrance applied to every section), not motion itself; each reveal should fit what it reveals. Suppressing the reflex is never a reason to ship a page with no motion at all. +- Reveal animations must enhance an already-visible default. Don't gate content visibility on a class-triggered transition; transitions pause on hidden tabs and headless renderers, so the reveal never fires and the section ships blank. +- Premium motion materials are not just transform/opacity. Blur, backdrop-filter, clip-path, mask, and shadow/glow are part of the palette when they materially improve the effect and stay smooth. -If DESIGN.md is missing: nudge once per session (*"Run `/impeccable document` for more on-brand output"*), then proceed. +#### Interaction -### 2. Register +- Dropdowns rendered with `position: absolute` inside an `overflow: hidden` or `overflow: auto` container will be clipped. Use the native `` / popover API, `position: fixed`, or a portal to escape the stacking context. -Every design task is **brand** (marketing, landing, campaign, long-form content, portfolio: design IS the product) or **product** (app UI, admin, dashboard, tool: design SERVES the product). +### Copy -Identify before designing. Priority: (1) cue in the task itself ("landing page" vs "dashboard"); (2) the surface in focus (the page, file, or route being worked on); (3) `register` field in PRODUCT.md. First match wins. +- Every word earns its place. No restated headings, no intros that repeat the title. +- **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`. +- **No aphoristic-cadence body copy as a default voice.** Don't fall into the rhythm of "serious statement, then punchy short negation" as the page's recurring voice. If three or more section copy blocks on the page land on a short rebuttal-shaped sentence, rewrite. Specific, not aphoristic. +- **No marketing buzzwords.** The streamline / empower / supercharge / leverage / unleash / transform / seamless / world-class / enterprise-grade / next-generation / cutting-edge / game-changer / mission-critical family of phrases. Pick a specific noun and a verb that describes what the product literally does. +- Button labels: verb + object. "Save changes" beats "OK"; "Delete project" beats "Yes". The label should say what will happen. +- Link text needs standalone meaning. "View pricing plans" beats "Click here"; screen readers announce links out of context. -If PRODUCT.md lacks the `register` field (legacy), infer it once from its "Users" and "Product Purpose" sections, then cache the inferred value for the session. Suggest the user run `/impeccable teach` to add the field explicitly. +### New projects only (when no prior work exists) -Load the matching reference: [reference/brand.md](reference/brand.md) or [reference/product.md](reference/product.md). The shared design laws below apply to both. +#### Color & Theme -## Shared design laws - -Apply to every design, both registers. Match implementation complexity to the aesthetic vision: maximalism needs elaborate code, minimalism needs precision. Interpret creatively. Vary across projects; never converge on the same choices. the model is capable of extraordinary work. Don't hold back. - -### Color - -- Use OKLCH. Reduce chroma as lightness approaches 0 or 100; high chroma at extremes looks garish. -- Never use `#000` or `#fff`. Tint every neutral toward the brand hue (chroma 0.005–0.01 is enough). +- Use OKLCH. +- **The cream / sand / beige body bg is the saturated AI default of 2026.** The whole warm-neutral band (OKLCH L 0.84-0.97, C < 0.06, hue 40-100) reads as cream/sand/paper/parchment regardless of what you call it. Token names like `--paper`, `--cream`, `--sand`, `--bone`, `--flour`, `--linen`, `--parchment`, `--wheat`, `--biscuit`, `--ivory` are tells in themselves. If the brief is "warm, traditional, family-coastal-Italian" or "magazine-warm" or "editorial-restraint", DO NOT translate that into a near-white warm-tinted bg; that's the AI move. Pick: (a) a saturated brand color as the body (terracotta, oxblood, deep ochre, near-black), (b) a true off-white at chroma 0 (or chroma toward the brand's own hue, not toward warmth-by-default), or (c) a darker mid-tone tinted neutral that's clearly the brand's own. "Warmth" in the brand is carried by accent + typography + imagery, not by body bg. +- Tinted neutrals: add 0.005–0.015 chroma toward the brand's hue. Don't default-tint toward warm or cool "because the brand feels that way"; that's the cross-project monoculture move. +- When picking a theme: Dark vs. light is never a default. Not dark "because tools look cool dark." Not light "to be safe.".Before choosing, write one sentence of physical scene: who uses this, where, under what ambient light, in what mood. If the sentence doesn't force the answer, it's not concrete enough. Add detail until it does. - Pick a **color strategy** before picking colors. Four steps on the commitment axis: - **Restrained**: tinted neutrals + one accent ≤10%. Product default; brand minimalism. - **Committed**: one saturated color carries 30–60% of the surface. Brand default for identity-driven pages. - **Full palette**: 3–4 named roles, each used deliberately. Brand campaigns; product data viz. - **Drenched**: the surface IS the color. Brand heroes, campaign pages. -- The "one accent ≤10%" rule is Restrained only. Committed / Full palette / Drenched exceed it on purpose. Don't collapse every design to Restrained by reflex. - -### Theme - -Dark vs. light is never a default. Not dark "because tools look cool dark." Not light "to be safe." - -Before choosing, write one sentence of physical scene: who uses this, where, under what ambient light, in what mood. If the sentence doesn't force the answer, it's not concrete enough. Add detail until it does. - -"Observability dashboard" does not force an answer. "SRE glancing at incident severity on a 27-inch monitor at 2am in a dim room" does. Run the sentence, not the category. - -### Typography - -- Cap body line length at 65–75ch. -- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales. - -### Layout - -- Vary spacing for rhythm. Same padding everywhere is monotony. -- Cards are the lazy answer. Use them only when they're truly the best affordance. Nested cards are always wrong. -- Don't wrap everything in a container. Most things don't need one. - -### Motion - -- Don't animate CSS layout properties. -- Ease out with exponential curves (ease-out-quart / quint / expo). No bounce, no elastic. ### Absolute bans @@ -100,12 +95,9 @@ Match-and-refuse. If you're about to write any of these, rewrite the element wit - **Glassmorphism as default.** Blurs and glass cards used decoratively. Rare and purposeful, or nothing. - **The hero-metric template.** Big number, small label, supporting stats, gradient accent. SaaS cliché. - **Identical card grids.** Same-sized cards with icon + heading + text, repeated endlessly. -- **Modal as first thought.** Modals are usually laziness. Exhaust inline / progressive alternatives first. - -### Copy - -- Every word earns its place. No restated headings, no intros that repeat the title. -- **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`. +- **Tiny uppercase tracked eyebrow above every section.** The 2023-era kicker (small all-caps text with wide tracking, "ABOUT" "PROCESS" "PRICING" above each heading) is now the saturated AI scaffold; it appears on 55-95% of generations regardless of brief, which is the definition of a tell. One named kicker as a deliberate brand system is voice; an eyebrow on every section is AI grammar. Choose a different cadence. +- **Numbered section markers as default scaffolding (01 / 02 / 03).** Putting `01 · About / 02 · Process / 03 · Pricing` above every section is the eyebrow trope one tier deeper: reach for it because "landing pages do this" and you're scaffolding by reflex. Numbers earn their place when the section actually IS a sequence (a real 3-step process, an ordered flow, a typed timeline) and the order carries information the reader needs. One deliberate numbered sequence on one page is voice; numbered eyebrows on every section across the site is AI grammar. +- **Text that overflows its container.** Long heading words plus large clamp scales plus narrow grids cause headline overflow on tablet/mobile. Test the heading copy at every breakpoint; if it overflows, reduce the clamp max or rewrite the copy. The viewport is part of the design. ### The AI slop test @@ -113,7 +105,7 @@ If someone could look at this interface and say "AI made that" without doubt, it **Category-reflex check.** Run at two altitudes; the second one catches what the first one misses. -- **First-order:** if someone could guess the theme + palette from the category alone ("observability → dark blue", "healthcare → white + teal", "finance → navy + gold", "crypto → neon on black"), it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain. +- **First-order:** if someone could guess the theme + palette from the category alone, it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain. - **Second-order:** if someone could guess the aesthetic family from category-plus-anti-references ("AI workflow tool that's not SaaS-cream → editorial-typographic", "fintech that's not navy-and-gold → terminal-native dark mode"), it's the trap one tier deeper. The first reflex was avoided; the second wasn't. Rework until both answers are not obvious. The brand register's [reflex-reject aesthetic lanes](reference/brand.md) list catches the currently-saturated families. ## Commands @@ -122,7 +114,7 @@ If someone could look at this interface and say "AI made that" without doubt, it |---|---|---|---| | `craft [feature]` | Build | Shape, then build a feature end-to-end | [reference/craft.md](reference/craft.md) | | `shape [feature]` | Build | Plan UX/UI before writing code | [reference/shape.md](reference/shape.md) | -| `teach` | Build | Set up PRODUCT.md and DESIGN.md context | [reference/teach.md](reference/teach.md) | +| `init` | Build | Set up project context: PRODUCT.md, DESIGN.md, live config, next steps | [reference/init.md](reference/init.md) | | `document` | Build | Generate DESIGN.md from existing project code | [reference/document.md](reference/document.md) | | `extract [target]` | Build | Pull reusable tokens and components into design system | [reference/extract.md](reference/extract.md) | | `critique [target]` | Evaluate | UX design review with heuristic scoring | [reference/critique.md](reference/critique.md) | @@ -150,11 +142,14 @@ Plus two management commands: `pin ` and `unpin `, detailed be 1. **No argument**: render the table above as the user-facing command menu, grouped by category. Ask what they'd like to do. 2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. -3. **First word doesn't match**: general design invocation. Apply the setup steps, shared design laws, and the loaded register reference, using the full argument as context. +3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. +4. **No clear command match**: general design invocation. Apply the setup steps, the General rules, and the loaded register reference, using the full argument as context. Setup (context gathering, register) is already loaded by then; sub-commands don't re-invoke `/impeccable`. -If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `teach` as a blocker, finish teach, refresh context, then resume the original command and target. +If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `init` as a blocker, finish init, refresh context, then resume the original command and target. + +`teach` is a deprecated alias for `init`: if the user types it, load [reference/init.md](reference/init.md) and proceed as if they ran `init`. ## Pin / Unpin diff --git a/.trae-cn/skills/impeccable/reference/adapt.md b/.trae-cn/skills/impeccable/reference/adapt.md index 9d0673ea2..5af7606cb 100644 --- a/.trae-cn/skills/impeccable/reference/adapt.md +++ b/.trae-cn/skills/impeccable/reference/adapt.md @@ -188,3 +188,124 @@ Test thoroughly across contexts: - **Slow connections**: Test on throttled network When the adaptation feels native to each context, hand off to `/impeccable polish` for the final pass. + +--- + +## Reference Material + +The sections below were previously `responsive-design.md` and live inline now so the adapt flow has its deep responsive reference in one place. + +### Responsive Design + +#### Mobile-First: Write It Right + +Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first. + +#### Breakpoints: Content-Driven + +Don't chase device sizes; let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints. + +#### Detect Input Method, Not Just Screen Size + +**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard. Use pointer and hover queries: + +```css +/* Fine pointer (mouse, trackpad) */ +@media (pointer: fine) { + .button { padding: 8px 16px; } +} + +/* Coarse pointer (touch, stylus) */ +@media (pointer: coarse) { + .button { padding: 12px 20px; } /* Larger touch target */ +} + +/* Device supports hover */ +@media (hover: hover) { + .card:hover { transform: translateY(-2px); } +} + +/* Device doesn't support hover (touch) */ +@media (hover: none) { + .card { /* No hover state - use active instead */ } +} +``` + +**Critical**: Don't rely on hover for functionality. Touch users can't hover. + +#### Safe Areas: Handle the Notch + +Modern phones have notches, rounded corners, and home indicators. Use `env()`: + +```css +body { + padding-top: env(safe-area-inset-top); + padding-bottom: env(safe-area-inset-bottom); + padding-left: env(safe-area-inset-left); + padding-right: env(safe-area-inset-right); +} + +/* With fallback */ +.footer { + padding-bottom: max(1rem, env(safe-area-inset-bottom)); +} +``` + +**Enable viewport-fit** in your meta tag: +```html + +``` + +#### Responsive Images: Get It Right + +##### srcset with Width Descriptors + +```html +Hero image +``` + +**How it works**: +- `srcset` lists available images with their actual widths (`w` descriptors) +- `sizes` tells the browser how wide the image will display +- Browser picks the best file based on viewport width AND device pixel ratio + +##### Picture Element for Art Direction + +When you need different crops/compositions (not just resolutions): + +```html + + + + ... + +``` + +#### Layout Adaptation Patterns + +**Navigation**: Three stages: hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `
/` for content that can collapse on mobile. + +#### Testing: Don't Trust DevTools Alone + +DevTools device emulation is useful for layout but misses: + +- Actual touch interactions +- Real CPU/memory constraints +- Network latency patterns +- Font rendering differences +- Browser chrome/keyboard appearances + +**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators. + +--- + +**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful. diff --git a/.trae-cn/skills/impeccable/reference/animate.md b/.trae-cn/skills/impeccable/reference/animate.md index 20aeea659..15d9f9080 100644 --- a/.trae-cn/skills/impeccable/reference/animate.md +++ b/.trae-cn/skills/impeccable/reference/animate.md @@ -6,7 +6,7 @@ Add motion that conveys state, gives feedback, and clarifies hierarchy. Cut moti ## Register -Brand: orchestrated page-load sequences, staggered reveals, scroll-driven animation. Motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions. +Brand: motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions. The saturated AI default is fade-and-rise reveals on every scrolled section; that's a tell, not a choreography. Reserve scroll-triggered motion for moments that earn it. Product: 150–250 ms on most transitions. Motion conveys state: feedback, reveal, loading, transitions between views. No page-load choreography; users are in a task and won't wait for it. @@ -49,10 +49,11 @@ Create a purposeful animation plan: Add motion systematically across these categories: ### Entrance Animations -- **Page load choreography**: Stagger element reveals (100-150ms delays), fade + slide combinations - **Hero section**: Dramatic entrance for primary content (scale, parallax, or creative effects) -- **Content reveals**: Scroll-triggered animations using intersection observer - **Modal/drawer entry**: Smooth slide + fade, backdrop fade, focus management +- **List rhythm**: Sibling stagger is legitimate for cards-in-a-grid or list-items-appearing. Whole-section fade-on-scroll is not a list and is not legitimate. Cap total stagger time: 10 items at 50ms each = 500ms total. For more items, reduce per-item delay or cap the staggered count. + + Use CSS custom properties for clean stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"`, `style="--i: 1"`, etc. on each item. ### Micro-interactions - **Button feedback**: @@ -97,11 +98,14 @@ Use appropriate techniques for each animation: ### Timing & Easing -**Durations by purpose:** -- **100-150ms**: Instant feedback (button press, toggle) -- **200-300ms**: State changes (hover, menu open) -- **300-500ms**: Layout changes (accordion, modal) -- **500-800ms**: Entrance animations (page load) +**Duration: the 100/300/500 rule.** Timing matters more than easing for "feels right": + +| Duration | Use Case | Examples | +|----------|----------|----------| +| **100–150ms** | Instant feedback | Button press, toggle, color change | +| **200–300ms** | State changes | Menu open, tooltip, hover state | +| **300–500ms** | Layout changes | Accordion, modal, drawer | +| **500–800ms** | Entrance animations | Page load, hero reveal | **Easing curves (use these, not CSS defaults):** ```css @@ -134,13 +138,35 @@ Use appropriate techniques for each animation: - GSAP for complex sequences ``` +### Motion Materials + +Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties. Match material to effect: + +- **Transform / opacity**: movement, press feedback, simple reveals, list choreography +- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances +- **Clip-path / masks**: wipes, reveals, editorial cropping, product-like transitions +- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state +- **Grid-template-rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly + +The hard rule isn't "transform and opacity only." It's: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify smoothness in-browser on target viewports. + ### Performance -- **Motion materials**: Use transform/opacity for reliable movement, but use blur, filters, masks, shadows, and color shifts when they materially improve the effect - **Layout safety**: Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins) -- **will-change**: Add sparingly for known expensive animations +- **will-change**: Add sparingly for known expensive animations only (e.g. on `:hover` or an `.animating` class), never preemptively across the whole page +- **Scroll triggers**: Use Intersection Observer instead of scroll event listeners; unobserve after the animation fires once - **Bound expensive effects**: Keep blur/filter/shadow areas small or isolated, use `contain` where appropriate - **Monitor FPS**: Ensure 60fps on target devices +### Perceived Performance + +Nobody cares how fast your site *is*, only how fast it feels. The 80ms threshold: anything under ~80ms feels instant because our brains buffer sensory input for that long to synchronize perception. Target this for micro-interactions. + +- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening. +- **Early completion**: Show content progressively, don't wait for everything (progressive images, streaming HTML, skeleton fade-ins). +- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Use for low-stakes actions (likes, follows). Avoid for payments or destructive operations. +- **Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances. +- **Caution**: Too-fast responses can decrease perceived value for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening. + ### Accessibility ```css @media (prefers-reduced-motion: reduce) { diff --git a/.trae-cn/skills/impeccable/reference/bolder.md b/.trae-cn/skills/impeccable/reference/bolder.md index ff0ef2f14..30d4e852c 100644 --- a/.trae-cn/skills/impeccable/reference/bolder.md +++ b/.trae-cn/skills/impeccable/reference/bolder.md @@ -50,7 +50,7 @@ Create a strategy to increase impact while maintaining coherence: Systematically increase impact across these dimensions: ### Typography Amplification -- **Replace generic fonts**: Swap system fonts for distinctive choices (see the parent skill's typography guidelines and [typography.md](typography.md) for inspiration) +- **Replace generic fonts**: Swap system fonts for distinctive choices (see the parent skill's typography guidelines and the [Reference Material section of typeset.md](typeset.md#reference-material) for inspiration) - **Extreme scale**: Create dramatic size jumps (3x-5x differences, not 1.5x) - **Weight contrast**: Pair 900 weights with 200 weights, not 600 with 400 - **Unexpected choices**: Variable fonts, display fonts for headlines, condensed/extended widths, monospace as intentional accent (not as lazy "dev tool" default) @@ -78,10 +78,10 @@ Systematically increase impact across these dimensions: - **Custom elements**: Illustrative elements, custom icons, decorative details that reinforce brand ### Motion & Animation -- **Entrance choreography**: Staggered, dramatic page load animations with 50-100ms delays -- **Scroll effects**: Parallax, reveal animations, scroll-triggered sequences -- **Micro-interactions**: Satisfying hover effects, click feedback, state changes -- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic, which cheapen the effect) +- **Hero moment**: One signature entrance, once. Not on every visit and not on every section. +- **Micro-interactions**: Satisfying hover effects, click feedback, state changes. +- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic, which cheapen the effect). +- **Bolder ≠ scroll-fade-rise on every section.** That's the saturated AI default, the opposite of bold. ### Composition Boldness - **Hero moments**: Create clear focal points with dramatic treatment diff --git a/.trae-cn/skills/impeccable/reference/brand.md b/.trae-cn/skills/impeccable/reference/brand.md index 3d83a1cdc..194514e95 100644 --- a/.trae-cn/skills/impeccable/reference/brand.md +++ b/.trae-cn/skills/impeccable/reference/brand.md @@ -43,17 +43,10 @@ The reflex-reject lists apply to **new design choices**. When the existing brand ### Pairing and voice -Distinctive + refined is the goal. The specific shape depends on the brand: - -- **Editorial / long-form / luxury**: display serif + sans body (a magazine shape). -- **Tech / dev tools / fintech**: one committed sans, usually; custom-tight tracking, strong weight contrast inside a single family. -- **Consumer / food / travel**: warmer pairings, often a humanist sans plus a script or display serif. -- **Creative studios / agencies**: rule-breaking welcome. Mono-only, or display-only, or custom-drawn type as voice. +Distinctive + refined is the goal. The specific shape depends on the brand, not on the brand's category. A category ("restaurant", "dev tool", "magazine", "fintech") is not a recipe; treating it as one is the first-order reflex SKILL.md warns against. Two families minimum is the rule *only* when the voice needs it. A single well-chosen family with committed weight/size contrast is stronger than a timid display+body pair. -Vary across projects. If the last brief was a serif-display landing page, this one isn't. - ### Scale Modular scale, fluid `clamp()` for headings, ≥1.25 ratio between steps. Flat scales (1.1× apart) read as uncommitted. @@ -74,8 +67,7 @@ Brand surfaces have permission for Committed, Full palette, and Drenched strateg - Asymmetric compositions are one option. Break the grid intentionally for emphasis. - Fluid spacing with `clamp()` that breathes on larger viewports. Vary for rhythm: generous separations, tight groupings. -- Alternative: a strict, visible grid as the voice (brutalist / Swiss / tech-spec aesthetics). Either asymmetric or rigorously-gridded can be "designed"; the failure mode is splitting the difference into a generic centered stack. -- Don't default to centering everything. Left-aligned with asymmetric layouts feels more designed; a strict grid reads as confident structure. A centered-stack hero with icon-title-subtitle cards reads as template. +- For image-led briefs (hotels, restaurants, magazines, photography), full-bleed hero imagery with overlaid menu and centered headline is a canonical move; let the photograph be the design. - When cards ARE the right affordance, use `grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` for breakpoint-free responsiveness. ## Imagery @@ -93,8 +85,7 @@ Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landin ## Motion -- One well-orchestrated page-load with staggered reveals beats scattered micro-interactions, when the brand invites it. Tech-minimal brands often skip entrance motion entirely; the restraint is the voice. -- For collapsing/expanding sections, transition `grid-template-rows` rather than `height`. +- One well-orchestrated page-load beats scattered micro-interactions, when the brand invites it. Some brands skip entrance motion entirely; the restraint is the voice. ## Brand bans (on top of the shared absolute bans) @@ -111,8 +102,7 @@ Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landin Brand can afford things product can't. Take them. -- Ambitious first-load motion. Reveals, scroll-triggered transitions, typographic choreography. +- Ambitious first-load motion. Reveals and typographic choreography that earn their place; not fade-on-scroll for every section. - Single-purpose viewports. One dominant idea per fold, long scroll, deliberate pacing. -- Typographic risk. Enormous display type, unexpected italic cuts, mixed cases, hand-drawn headlines, a single oversize word as a hero. - Unexpected color strategies. Palette IS voice; a calm brand and a restless brand should not share palette mechanics. - Art direction per section. Different sections can have different visual worlds if the narrative demands it. Consistency of voice beats consistency of treatment. diff --git a/.trae-cn/skills/impeccable/reference/clarify.md b/.trae-cn/skills/impeccable/reference/clarify.md index 5f7d420f4..00ee978c3 100644 --- a/.trae-cn/skills/impeccable/reference/clarify.md +++ b/.trae-cn/skills/impeccable/reference/clarify.md @@ -172,3 +172,117 @@ Test that copy improvements work: - **Tone**: Is it appropriate for the situation? When the copy reads cleanly, hand off to `/impeccable polish` for the final pass. + +--- + +## Reference Material + +The sections below were previously `ux-writing.md` and live inline now so the clarify flow has its deep UX-writing reference in one place. + +### UX Writing + +#### The Button Label Problem + +**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns: + +| Bad | Good | Why | +|-----|------|-----| +| OK | Save changes | Says what will happen | +| Submit | Create account | Outcome-focused | +| Yes | Delete message | Confirms the action | +| Cancel | Keep editing | Clarifies what "cancel" means | +| Click here | Download PDF | Describes the destination | + +**For destructive actions**, name the destruction: +- "Delete" not "Remove" (delete is permanent, remove implies recoverable) +- "Delete 5 items" not "Delete selected" (show the count) + +#### Error Messages: The Formula + +Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input". + +##### Error Message Templates + +| Situation | Template | +|-----------|----------| +| **Format error** | "[Field] needs to be [format]. Example: [example]" | +| **Missing required** | "Please enter [what's missing]" | +| **Permission denied** | "You don't have access to [thing]. [What to do instead]" | +| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." | +| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" | + +##### Don't Blame the User + +Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date". + +#### Empty States Are Opportunities + +Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items". + +#### Voice vs Tone + +**Voice** is your brand's personality, consistent everywhere. +**Tone** adapts to the moment. + +| Moment | Tone Shift | +|--------|------------| +| Success | Celebratory, brief: "Done! Your changes are live." | +| Error | Empathetic, helpful: "That didn't work. Here's what to try..." | +| Loading | Reassuring: "Saving your work..." | +| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | + +**Never use humor for errors.** Users are already frustrated. Be helpful, not cute. + +#### Writing for Accessibility + +**Link text** must have standalone meaning: "View pricing plans" not "Click here". **Alt text** describes information, not the image: "Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context. + +#### Writing for Translation + +##### Plan for Expansion + +German text is ~30% longer than English. Allocate space: + +| Language | Expansion | +|----------|-----------| +| German | +30% | +| French | +20% | +| Finnish | +30-40% | +| Chinese | -30% (fewer chars, but same width) | + +##### Translation-Friendly Patterns + +Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear. + +#### Consistency: The Terminology Problem + +Pick one term and stick with it: + +| Inconsistent | Consistent | +|--------------|------------| +| Delete / Remove / Trash | Delete | +| Settings / Preferences / Options | Settings | +| Sign in / Log in / Enter | Sign in | +| Create / Add / New | Create | + +Build a terminology glossary and enforce it. Variety creates confusion. + +#### Avoid Redundant Copy + +If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well. + +#### Loading States + +Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress. + +#### Confirmation Dialogs: Use Sparingly + +Most confirmation dialogs are design failures; consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No"). + +#### Form Instructions + +Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking. + +--- + +**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors. diff --git a/.trae-cn/skills/impeccable/reference/codex.md b/.trae-cn/skills/impeccable/reference/codex.md index f40ec746f..d563af95b 100644 --- a/.trae-cn/skills/impeccable/reference/codex.md +++ b/.trae-cn/skills/impeccable/reference/codex.md @@ -23,8 +23,8 @@ Before generating anything, run a brief direction conversation grounded in the s Ask **2-3 targeted questions** about visual lane, color strategy, atmosphere, and named anchor references. Don't enumerate generic menus; tie each question to the shape brief's answers. Example shape-grounded questions: -- "Brief says 'editorial restraint, Klim-adjacent.' Are we closer to a quiet specimen page or a magazine-spread feel with hero imagery?" -- "Palette strategy from shape was 'Committed.' Want it warm-grounded (deep oxblood + cream) or cool-grounded (slate + paper white)?" +- "Brief says 'specimen-page restraint.' Are we closer to a quiet typographic page or a wider editorial spread with hero imagery?" +- "Palette strategy from shape was 'Committed.' Which one color carries the surface (a brand-driven pick rather than a default warm-or-cool framing)? (And no, the answer isn't a cream/sand body bg; that's the saturated AI default.)" **STOP and wait for answers.** These pin the palette before any pixel gets generated. Do not proceed to Step B until the user has responded. diff --git a/.trae-cn/skills/impeccable/reference/cognitive-load.md b/.trae-cn/skills/impeccable/reference/cognitive-load.md deleted file mode 100644 index 48f8ad58b..000000000 --- a/.trae-cn/skills/impeccable/reference/cognitive-load.md +++ /dev/null @@ -1,106 +0,0 @@ -# Cognitive Load Assessment - -Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload. - ---- - -## Three Types of Cognitive Load - -### Intrinsic Load: The Task Itself -Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it. - -**Manage it by**: -- Breaking complex tasks into discrete steps -- Providing scaffolding (templates, defaults, examples) -- Progressive disclosure: show what's needed now, hide the rest -- Grouping related decisions together - -### Extraneous Load: Bad Design -Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste. - -**Common sources**: -- Confusing navigation that requires mental mapping -- Unclear labels that force users to guess meaning -- Visual clutter competing for attention -- Inconsistent patterns that prevent learning -- Unnecessary steps between user intent and result - -### Germane Load: Learning Effort -Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery. - -**Support it by**: -- Progressive disclosure that reveals complexity gradually -- Consistent patterns that reward learning -- Feedback that confirms correct understanding -- Onboarding that teaches through action, not walls of text - ---- - -## Cognitive Load Checklist - -Evaluate the interface against these 8 items: - -- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements? -- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)? -- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)? -- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen? -- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next? -- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)? -- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one? -- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it? - -**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed). - ---- - -## The Working Memory Rule - -**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001). - -At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider: -- **≤4 items**: Within working memory limits, manageable -- **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure -- **8+ items**: Overloaded; users will skip, misclick, or abandon - -**Practical applications**: -- Navigation menus: ≤5 top-level items (group the rest under clear categories) -- Form sections: ≤4 fields visible per group before a visual break -- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu -- Dashboard widgets: ≤4 key metrics visible without scrolling -- Pricing tiers: ≤3 options (more causes analysis paralysis) - ---- - -## Common Cognitive Load Violations - -### 1. The Wall of Options -**Problem**: Presenting 10+ choices at once with no hierarchy. -**Fix**: Group into categories, highlight recommended, use progressive disclosure. - -### 2. The Memory Bridge -**Problem**: User must remember info from step 1 to complete step 3. -**Fix**: Keep relevant context visible, or repeat it where it's needed. - -### 3. The Hidden Navigation -**Problem**: User must build a mental map of where things are. -**Fix**: Always show current location (breadcrumbs, active states, progress indicators). - -### 4. The Jargon Barrier -**Problem**: Technical or domain language forces translation effort. -**Fix**: Use plain language. If domain terms are unavoidable, define them inline. - -### 5. The Visual Noise Floor -**Problem**: Every element has the same visual weight; nothing stands out. -**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted. - -### 6. The Inconsistent Pattern -**Problem**: Similar actions work differently in different places. -**Fix**: Standardize interaction patterns. Same type of action = same type of UI. - -### 7. The Multi-Task Demand -**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating). -**Fix**: Sequence the steps. Let the user do one thing at a time. - -### 8. The Context Switch -**Problem**: User must jump between screens/tabs/modals to gather info for a single decision. -**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth. diff --git a/.trae-cn/skills/impeccable/reference/color-and-contrast.md b/.trae-cn/skills/impeccable/reference/color-and-contrast.md deleted file mode 100644 index 110c2ee47..000000000 --- a/.trae-cn/skills/impeccable/reference/color-and-contrast.md +++ /dev/null @@ -1,105 +0,0 @@ -# Color & Contrast - -## Color Spaces: Use OKLCH - -**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal, unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark. - -The OKLCH function takes three components: `oklch(lightness chroma hue)` where lightness is 0-100%, chroma is roughly 0-0.4, and hue is 0-360. To build a primary color and its lighter / darker variants, hold the chroma+hue roughly constant and vary the lightness, but **reduce chroma as you approach white or black**, because high chroma at extreme lightness looks garish. - -The hue you pick is a brand decision and should not come from a default. Do not reach for blue (hue 250) or warm orange (hue 60) by reflex; those are the dominant AI-design defaults, not the right answer for any specific brand. - -## Building Functional Palettes - -### Tinted Neutrals - -**Pure gray is dead.** A neutral with zero chroma feels lifeless next to a colored brand. Add a tiny chroma value (0.005-0.015) to all your neutrals, hued toward whatever your brand color is. The chroma is small enough not to read as "tinted" consciously, but it creates subconscious cohesion between brand color and UI surfaces. - -The hue you tint toward should come from THIS project's brand, not from a "warm = friendly, cool = tech" formula. If your brand color is teal, your neutrals lean toward teal. If your brand color is amber, they lean toward amber. The point is cohesion with the SPECIFIC brand, not a stock palette. - -**Avoid** the trap of always tinting toward warm orange or always tinting toward cool blue. Those are the two laziest defaults and they create their own monoculture across projects. - -### Palette Structure - -A complete system needs: - -| Role | Purpose | Example | -|------|---------|---------| -| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades | -| **Neutral** | Text, backgrounds, borders | 9-11 shade scale | -| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each | -| **Surface** | Cards, modals, overlays | 2-3 elevation levels | - -**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise. - -### The 60-30-10 Rule (Applied Correctly) - -This rule is about **visual weight**, not pixel count: - -- **60%**: Neutral backgrounds, white space, base surfaces -- **30%**: Secondary colors: text, borders, inactive states -- **10%**: Accent: CTAs, highlights, focus states - -The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power. - -## Contrast & Accessibility - -### WCAG Requirements - -| Content Type | AA Minimum | AAA Target | -|--------------|------------|------------| -| Body text | 4.5:1 | 7:1 | -| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | -| UI components, icons | 3:1 | 4.5:1 | -| Non-essential decorations | None | None | - -**The gotcha**: Placeholder text still needs 4.5:1. That light gray placeholder you see everywhere? Usually fails WCAG. - -### Dangerous Color Combinations - -These commonly fail contrast or cause readability issues: - -- Light gray text on white (the #1 accessibility fail) -- **Gray text on any colored background**: gray looks washed out and dead on color. Use a darker shade of the background color, or transparency -- Red text on green background (or vice versa): 8% of men can't distinguish these -- Blue text on red background (vibrates visually) -- Yellow text on white (almost always fails) -- Thin light text on images (unpredictable contrast) - -### Never Use Pure Gray or Pure Black - -Pure gray (`oklch(50% 0 0)`) and pure black (`#000`) don't exist in nature; real shadows and surfaces always have a color cast. Even a chroma of 0.005-0.01 is enough to feel natural without being obviously tinted. (See tinted neutrals example above.) - -### Testing - -Don't trust your eyes. Use tools: - -- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) -- Browser DevTools → Rendering → Emulate vision deficiencies -- [Polypane](https://polypane.app/) for real-time testing - -## Theming: Light & Dark Mode - -### Dark Mode Is Not Inverted Light Mode - -You can't just swap colors. Dark mode requires different design decisions: - -| Light Mode | Dark Mode | -|------------|-----------| -| Shadows for depth | Lighter surfaces for depth (no shadows) | -| Dark text on light | Light text on dark (reduce font weight) | -| Vibrant accents | Desaturate accents slightly | -| White backgrounds | Never pure black; use dark gray (oklch 12-18%) | - -In dark mode, depth comes from surface lightness, not shadow. Build a 3-step surface scale where higher elevations are lighter (e.g. 15% / 20% / 25% lightness). Use the SAME hue and chroma as your brand color (whatever it is for THIS project; do not reach for blue) and only vary the lightness. Reduce body text weight slightly (e.g. 350 instead of 400) because light text on dark reads as heavier than dark text on light. - -### Token Hierarchy - -Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer; primitives stay the same. - -## Alpha Is A Design Smell - -Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed. - ---- - -**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Using pure black (#000) for large areas. Skipping color blindness testing (8% of men affected). diff --git a/.trae-cn/skills/impeccable/reference/colorize.md b/.trae-cn/skills/impeccable/reference/colorize.md index 9e96eef59..2457f608c 100644 --- a/.trae-cn/skills/impeccable/reference/colorize.md +++ b/.trae-cn/skills/impeccable/reference/colorize.md @@ -68,10 +68,10 @@ Add color systematically across these dimensions: - **Hover states**: Introduce color on interaction ### Background & Surfaces -- **Tinted backgrounds**: Replace pure gray (`#f5f5f5`) with warm neutrals (`oklch(97% 0.01 60)`) or cool tints (`oklch(97% 0.01 250)`) +- **Tinted backgrounds**: If you replace pure gray, tint toward the brand hue, not toward a generic-warm-or-cool pair. The default-warm-tint (`oklch(97% 0.01 60)` and its neighbors) is now the AI cream/sand giveaway. Be specific to the brand or stay neutral. - **Colored sections**: Use subtle background colors to separate areas - **Gradient backgrounds**: Add depth with subtle, intentional gradients (not generic purple-blue) -- **Cards & surfaces**: Tint cards or surfaces slightly for warmth +- **Cards & surfaces**: Tint cards or surfaces toward the brand, not "for warmth" by reflex **Use OKLCH for color**: It's perceptually uniform, meaning equal steps in lightness *look* equal. Great for generating harmonious scales. @@ -124,8 +124,6 @@ Ensure color addition improves rather than overwhelms: - Use every color in the rainbow (choose 2-4 colors beyond neutrals) - Apply color randomly without semantic meaning - Put gray text on colored backgrounds. It looks washed out; use a darker shade of the background color or transparency instead -- Use pure gray for neutrals. Add subtle color tint (warm or cool) for depth -- Use pure black (`#000`) or pure white (`#fff`) for large areas - Violate WCAG contrast requirements - Use color as the only indicator (accessibility issue) - Make everything colorful (defeats the purpose) @@ -152,3 +150,108 @@ When invoked from live mode, each variant MUST declare a `color-amount` param so ``` Layer 1-2 variant-specific params on top: palette selection (`steps` with named options), temperature warmth, or tint vs. true color. See `reference/live.md` for the full params contract. + +--- + +## Reference Material + +The sections below were previously `color-and-contrast.md` and live inline now so the colorize flow has its deep color reference in one place. + +### Color & Contrast + +#### Color Spaces: Use OKLCH + +**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal, unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark. + +The OKLCH function takes three components: `oklch(lightness chroma hue)` where lightness is 0-100%, chroma is roughly 0-0.4, and hue is 0-360. To build a primary color and its lighter / darker variants, hold the chroma+hue roughly constant and vary the lightness, but **reduce chroma as you approach white or black**, because high chroma at extreme lightness looks garish. + +The hue you pick is a brand decision and should not come from a default. Do not reach for blue (hue 250) or warm orange (hue 60) by reflex; those are the dominant AI-design defaults, not the right answer for any specific brand. + +#### Building Functional Palettes + +##### Tinted Neutrals + +**Pure gray is dead.** A neutral with zero chroma feels lifeless next to a colored brand. Add a tiny chroma value (0.005-0.015) to all your neutrals, hued toward whatever your brand color is. The chroma is small enough not to read as "tinted" consciously, but it creates subconscious cohesion between brand color and UI surfaces. + +The hue you tint toward should come from THIS project's brand, not from a "warm = friendly, cool = tech" formula. If your brand color is teal, your neutrals lean toward teal. If your brand color is amber, they lean toward amber. The point is cohesion with the SPECIFIC brand, not a stock palette. + +**Avoid** the trap of always tinting toward warm orange or always tinting toward cool blue. Those are the two laziest defaults and they create their own monoculture across projects. + +##### Palette Structure + +A complete system needs: + +| Role | Purpose | Example | +|------|---------|---------| +| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades | +| **Neutral** | Text, backgrounds, borders | 9-11 shade scale | +| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each | +| **Surface** | Cards, modals, overlays | 2-3 elevation levels | + +**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise. + +##### The 60-30-10 Rule (Applied Correctly) + +This rule is about **visual weight**, not pixel count: + +- **60%**: Neutral backgrounds, white space, base surfaces +- **30%**: Secondary colors: text, borders, inactive states +- **10%**: Accent: CTAs, highlights, focus states + +The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power. + +#### Contrast & Accessibility + +##### WCAG Requirements + +| Content Type | AA Minimum | AAA Target | +|--------------|------------|------------| +| Body text | 4.5:1 | 7:1 | +| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | +| UI components, icons | 3:1 | 4.5:1 | +| Non-essential decorations | None | None | + +##### Dangerous Color Combinations + +These commonly fail contrast or cause readability issues: + +- Light gray text on white (the #1 accessibility fail) +- Red text on green background (or vice versa): 8% of men can't distinguish these +- Blue text on red background (vibrates visually) +- Yellow text on white (almost always fails) +- Thin light text on images (unpredictable contrast) + +##### Testing + +Don't trust your eyes. Use tools: + +- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) +- Browser DevTools → Rendering → Emulate vision deficiencies +- [Polypane](https://polypane.app/) for real-time testing + +#### Theming: Light & Dark Mode + +##### Dark Mode Is Not Inverted Light Mode + +You can't just swap colors. Dark mode requires different design decisions: + +| Light Mode | Dark Mode | +|------------|-----------| +| Shadows for depth | Lighter surfaces for depth (no shadows) | +| Dark text on light | Light text on dark (reduce font weight) | +| Vibrant accents | Desaturate accents slightly | +| White backgrounds | Either pure black or a deep surface that fits the brand (a brand-tinted near-black at oklch 12-18% works too) | + +In dark mode, depth comes from surface lightness, not shadow. Build a 3-step surface scale where higher elevations are lighter (e.g. 15% / 20% / 25% lightness). Use the SAME hue and chroma as your brand color (whatever it is for THIS project; do not reach for blue) and only vary the lightness. Reduce body text weight slightly (e.g. 350 instead of 400) because light text on dark reads as heavier than dark text on light. + +##### Token Hierarchy + +Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer; primitives stay the same. + +#### Alpha Is A Design Smell + +Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed. + +--- + +**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Skipping color blindness testing (8% of men affected). diff --git a/.trae-cn/skills/impeccable/reference/craft.md b/.trae-cn/skills/impeccable/reference/craft.md index 06094b1dd..2c7bd99b1 100644 --- a/.trae-cn/skills/impeccable/reference/craft.md +++ b/.trae-cn/skills/impeccable/reference/craft.md @@ -56,15 +56,15 @@ If the harness has native image generation (Codex), a compact shape's "confirm o Based on the design brief's "Recommended References" section, consult the relevant impeccable reference files. At minimum, always consult: -- [spatial-design.md](spatial-design.md) for layout and spacing -- [typography.md](typography.md) for type hierarchy +- [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 [motion-design.md](motion-design.md) -- Color-heavy or themed? Consult [color-and-contrast.md](color-and-contrast.md) -- Responsive requirements? Consult [responsive-design.md](responsive-design.md) -- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.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) diff --git a/.trae-cn/skills/impeccable/reference/critique.md b/.trae-cn/skills/impeccable/reference/critique.md index 2cbbb2712..facf7342a 100644 --- a/.trae-cn/skills/impeccable/reference/critique.md +++ b/.trae-cn/skills/impeccable/reference/critique.md @@ -38,9 +38,9 @@ Read relevant source files and visually inspect the live page when browser autom Evaluate: - **AI slop**: Would someone believe "AI made this" immediately? Check all DON'T guidance from the parent Impeccable skill. - **Holistic design**: hierarchy, IA, emotional fit, discoverability, composition, typography, color, accessibility, states, copy, and edge cases. -- **Cognitive load**: consult [cognitive-load](cognitive-load.md); report checklist failures and decision points with >4 visible options. +- **Cognitive load**: consult the [Cognitive Load Assessment](#cognitive-load-assessment) section below; report checklist failures and decision points with >4 visible options. - **Emotional journey**: peak-end rule, emotional valleys, reassurance at high-stakes moments. -- **Nielsen heuristics**: consult [heuristics-scoring](heuristics-scoring.md); score all 10 heuristics 0-4. +- **Nielsen heuristics**: consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below; score all 10 heuristics 0-4. Return: AI slop verdict, heuristic scores, cognitive load, emotional journey, 2-3 strengths, 3-5 priority issues, persona red flags, minor observations, and provocative questions. @@ -80,7 +80,7 @@ The chat response is the primary user-facing deliverable. Present the full struc Structure your feedback as a design director would: #### Design Health Score -> *Consult [heuristics-scoring](heuristics-scoring.md)* +> *Consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below.* Present the Nielsen's 10 heuristics scores as a table: @@ -119,16 +119,16 @@ Highlight 2-3 things done well. Be specific about why they work. #### Priority Issues The 3-5 most impactful design problems, ordered by importance. -For each issue, tag with **P0-P3 severity** (consult [heuristics-scoring](heuristics-scoring.md) for severity definitions): +For each issue, tag with **P0-P3 severity** (see [Issue Severity below](#issue-severity-p0p3) for definitions): - **[P?] What**: Name the problem clearly - **Why it matters**: How this hurts users or undermines goals - **Fix**: What to do about it (be concrete) - **Suggested command**: Which command could address this (from: /impeccable adapt, /impeccable animate, /impeccable audit, /impeccable bolder, /impeccable clarify, /impeccable colorize, /impeccable critique, /impeccable delight, /impeccable distill, /impeccable document, /impeccable harden, /impeccable layout, /impeccable onboard, /impeccable optimize, /impeccable overdrive, /impeccable polish, /impeccable quieter, /impeccable shape, /impeccable typeset) #### Persona Red Flags -> *Consult [personas](personas.md)* +> *Consult the [Personas reference](#persona-based-design-testing) below.* -Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `RULES.md` contains a `## Design Context` section from `impeccable teach`, also generate 1-2 project-specific personas from the audience/brand info. +Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `RULES.md` contains a `## Design Context` section from `impeccable init`, also generate 1-2 project-specific personas from the audience/brand info. For each selected persona, walk through the primary user action and list specific red flags found: @@ -234,3 +234,534 @@ After presenting the summary, tell the user: > You can ask me to run these one at a time, all at once, or in any order you prefer. > > Re-run `/impeccable critique` after fixes to see your score improve. + +--- + +## Reference Material + +The sections below were previously separate reference files (`cognitive-load.md`, `heuristics-scoring.md`, `personas.md`). They live inline now so the critique flow has all its deep context in one place. + +### Cognitive Load Assessment + +Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload. + +--- + +#### Three Types of Cognitive Load + +##### Intrinsic Load: The Task Itself +Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it. + +**Manage it by**: +- Breaking complex tasks into discrete steps +- Providing scaffolding (templates, defaults, examples) +- Progressive disclosure: show what's needed now, hide the rest +- Grouping related decisions together + +##### Extraneous Load: Bad Design +Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste. + +**Common sources**: +- Confusing navigation that requires mental mapping +- Unclear labels that force users to guess meaning +- Visual clutter competing for attention +- Inconsistent patterns that prevent learning +- Unnecessary steps between user intent and result + +##### Germane Load: Learning Effort +Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery. + +**Support it by**: +- Progressive disclosure that reveals complexity gradually +- Consistent patterns that reward learning +- Feedback that confirms correct understanding +- Onboarding that teaches through action, not walls of text + +--- + +#### Cognitive Load Checklist + +Evaluate the interface against these 8 items: + +- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements? +- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)? +- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)? +- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen? +- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next? +- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)? +- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one? +- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it? + +**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed). + +--- + +#### The Working Memory Rule + +**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001). + +At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider: +- **≤4 items**: Within working memory limits, manageable +- **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure +- **8+ items**: Overloaded; users will skip, misclick, or abandon + +**Practical applications**: +- Navigation menus: ≤5 top-level items (group the rest under clear categories) +- Form sections: ≤4 fields visible per group before a visual break +- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu +- Dashboard widgets: ≤4 key metrics visible without scrolling +- Pricing tiers: ≤3 options (more causes analysis paralysis) + +--- + +#### Common Cognitive Load Violations + +##### 1. The Wall of Options +**Problem**: Presenting 10+ choices at once with no hierarchy. +**Fix**: Group into categories, highlight recommended, use progressive disclosure. + +##### 2. The Memory Bridge +**Problem**: User must remember info from step 1 to complete step 3. +**Fix**: Keep relevant context visible, or repeat it where it's needed. + +##### 3. The Hidden Navigation +**Problem**: User must build a mental map of where things are. +**Fix**: Always show current location (breadcrumbs, active states, progress indicators). + +##### 4. The Jargon Barrier +**Problem**: Technical or domain language forces translation effort. +**Fix**: Use plain language. If domain terms are unavoidable, define them inline. + +##### 5. The Visual Noise Floor +**Problem**: Every element has the same visual weight; nothing stands out. +**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted. + +##### 6. The Inconsistent Pattern +**Problem**: Similar actions work differently in different places. +**Fix**: Standardize interaction patterns. Same type of action = same type of UI. + +##### 7. The Multi-Task Demand +**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating). +**Fix**: Sequence the steps. Let the user do one thing at a time. + +##### 8. The Context Switch +**Problem**: User must jump between screens/tabs/modals to gather info for a single decision. +**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth. + +--- + +### Heuristics Scoring Guide + +Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough." + +#### Nielsen's 10 Heuristics + +##### 1. Visibility of System Status + +Keep users informed about what's happening through timely, appropriate feedback. + +**Check for**: +- Loading indicators during async operations +- Confirmation of user actions (save, submit, delete) +- Progress indicators for multi-step processes +- Current location in navigation (breadcrumbs, active states) +- Form validation feedback (inline, not just on submit) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | No feedback; user is guessing what happened | +| 1 | Rare feedback; most actions produce no visible response | +| 2 | Partial; some states communicated, major gaps remain | +| 3 | Good; most operations give clear feedback, minor gaps | +| 4 | Excellent; every action confirms, progress is always visible | + +##### 2. Match Between System and Real World + +Speak the user's language. Follow real-world conventions. Information appears in natural, logical order. + +**Check for**: +- Familiar terminology (no unexplained jargon) +- Logical information order matching user expectations +- Recognizable icons and metaphors +- Domain-appropriate language for the target audience +- Natural reading flow (left-to-right, top-to-bottom priority) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Pure tech jargon, alien to users | +| 1 | Mostly confusing; requires domain expertise to navigate | +| 2 | Mixed; some plain language, some jargon leaks through | +| 3 | Mostly natural; occasional term needs context | +| 4 | Speaks the user's language fluently throughout | + +##### 3. User Control and Freedom + +Users need a clear "emergency exit" from unwanted states without extended dialogue. + +**Check for**: +- Undo/redo functionality +- Cancel buttons on forms and modals +- Clear navigation back to safety (home, previous) +- Easy way to clear filters, search, selections +- Escape from long or multi-step processes + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Users get trapped; no way out without refreshing | +| 1 | Difficult exits; must find obscure paths to escape | +| 2 | Some exits; main flows have escape, edge cases don't | +| 3 | Good control; users can exit and undo most actions | +| 4 | Full control; undo, cancel, back, and escape everywhere | + +##### 4. Consistency and Standards + +Users shouldn't wonder whether different words, situations, or actions mean the same thing. + +**Check for**: +- Consistent terminology throughout the interface +- Same actions produce same results everywhere +- Platform conventions followed (standard UI patterns) +- Visual consistency (colors, typography, spacing, components) +- Consistent interaction patterns (same gesture = same behavior) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Inconsistent everywhere; feels like different products stitched together | +| 1 | Many inconsistencies; similar things look/behave differently | +| 2 | Partially consistent; main flows match, details diverge | +| 3 | Mostly consistent; occasional deviation, nothing confusing | +| 4 | Fully consistent; cohesive system, predictable behavior | + +##### 5. Error Prevention + +Better than good error messages is a design that prevents problems in the first place. + +**Check for**: +- Confirmation before destructive actions (delete, overwrite) +- Constraints preventing invalid input (date pickers, dropdowns) +- Smart defaults that reduce errors +- Clear labels that prevent misunderstanding +- Autosave and draft recovery + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Errors easy to make; no guardrails anywhere | +| 1 | Few safeguards; some inputs validated, most aren't | +| 2 | Partial prevention; common errors caught, edge cases slip | +| 3 | Good prevention; most error paths blocked proactively | +| 4 | Excellent; errors nearly impossible through smart constraints | + +##### 6. Recognition Rather Than Recall + +Minimize memory load. Make objects, actions, and options visible or easily retrievable. + +**Check for**: +- Visible options (not buried in hidden menus) +- Contextual help when needed (tooltips, inline hints) +- Recent items and history +- Autocomplete and suggestions +- Labels on icons (not icon-only navigation) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Heavy memorization; users must remember paths and commands | +| 1 | Mostly recall; many hidden features, few visible cues | +| 2 | Some aids; main actions visible, secondary features hidden | +| 3 | Good recognition; most things discoverable, few memory demands | +| 4 | Everything discoverable; users never need to memorize | + +##### 7. Flexibility and Efficiency of Use + +Accelerators, invisible to novices, speed up expert interaction. + +**Check for**: +- Keyboard shortcuts for common actions +- Customizable interface elements +- Recent items and favorites +- Bulk/batch actions +- Power user features that don't complicate the basics + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | One rigid path; no shortcuts or alternatives | +| 1 | Limited flexibility; few alternatives to the main path | +| 2 | Some shortcuts; basic keyboard support, limited bulk actions | +| 3 | Good accelerators; keyboard nav, some customization | +| 4 | Highly flexible; multiple paths, power features, customizable | + +##### 8. Aesthetic and Minimalist Design + +Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose. + +**Check for**: +- Only necessary information visible at each step +- Clear visual hierarchy directing attention +- Purposeful use of color and emphasis +- No decorative clutter competing for attention +- Focused, uncluttered layouts + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Overwhelming; everything competes for attention equally | +| 1 | Cluttered; too much noise, hard to find what matters | +| 2 | Some clutter; main content clear, periphery noisy | +| 3 | Mostly clean; focused design, minor visual noise | +| 4 | Perfectly minimal; every element earns its pixel | + +##### 9. Help Users Recognize, Diagnose, and Recover from Errors + +Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution. + +**Check for**: +- Plain language error messages (no error codes for users) +- Specific problem identification ("Email is missing @" not "Invalid input") +- Actionable recovery suggestions +- Errors displayed near the source of the problem +- Non-blocking error handling (don't wipe the form) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Cryptic errors; codes, jargon, or no message at all | +| 1 | Vague errors; "Something went wrong" with no guidance | +| 2 | Clear but unhelpful; names the problem but not the fix | +| 3 | Clear with suggestions; identifies problem and offers next steps | +| 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work | + +##### 10. Help and Documentation + +Even if the system is usable without docs, help should be easy to find, task-focused, and concise. + +**Check for**: +- Searchable help or documentation +- Contextual help (tooltips, inline hints, guided tours) +- Task-focused organization (not feature-organized) +- Concise, scannable content +- Easy access without leaving current context + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | No help available anywhere | +| 1 | Help exists but hard to find or irrelevant | +| 2 | Basic help; FAQ or docs exist, not contextual | +| 3 | Good documentation; searchable, mostly task-focused | +| 4 | Excellent contextual help; right info at the right moment | + +--- + +#### Score Summary + +**Total possible**: 40 points (10 heuristics × 4 max) + +| Score Range | Rating | What It Means | +|-------------|--------|---------------| +| 36–40 | Excellent | Minor polish only; ship it | +| 28–35 | Good | Address weak areas, solid foundation | +| 20–27 | Acceptable | Significant improvements needed before users are happy | +| 12–19 | Poor | Major UX overhaul required; core experience broken | +| 0–11 | Critical | Redesign needed; unusable in current state | + +--- + +#### Issue Severity (P0–P3) + +Tag each individual issue found during scoring with a priority level: + +| Priority | Name | Description | Action | +|----------|------|-------------|--------| +| **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper | +| **P1** | Major | Causes significant difficulty or confusion | Fix before release | +| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass | +| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits | + +**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1. + +--- + +### Persona-Based Design Testing + +Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss. + +**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns. + +--- + +#### 1. Impatient Power User: "Alex" + +**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave. + +**Behaviors**: +- Skips all onboarding and instructions +- Looks for keyboard shortcuts immediately +- Tries to bulk-select, batch-edit, and automate +- Gets frustrated by required steps that feel unnecessary +- Abandons if anything feels slow or patronizing + +**Test Questions**: +- Can Alex complete the core task in under 60 seconds? +- Are there keyboard shortcuts for common actions? +- Can onboarding be skipped entirely? +- Do modals have keyboard dismiss (Esc)? +- Is there a "power user" path (shortcuts, bulk actions)? + +**Red Flags** (report these specifically): +- Forced tutorials or unskippable onboarding +- No keyboard navigation for primary actions +- Slow animations that can't be skipped +- One-item-at-a-time workflows where batch would be natural +- Redundant confirmation steps for low-risk actions + +--- + +#### 2. Confused First-Timer: "Jordan" + +**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out. + +**Behaviors**: +- Reads all instructions carefully +- Hesitates before clicking anything unfamiliar +- Looks for help or support constantly +- Misunderstands jargon and abbreviations +- Takes the most literal interpretation of any label + +**Test Questions**: +- Is the first action obviously clear within 5 seconds? +- Are all icons labeled with text? +- Is there contextual help at decision points? +- Does terminology assume prior knowledge? +- Is there a clear "back" or "undo" at every step? + +**Red Flags** (report these specifically): +- Icon-only navigation with no labels +- Technical jargon without explanation +- No visible help option or guidance +- Ambiguous next steps after completing an action +- No confirmation that an action succeeded + +--- + +#### 3. Accessibility-Dependent User: "Sam" + +**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences. + +**Behaviors**: +- Tabs through the interface linearly +- Relies on ARIA labels and heading structure +- Cannot see hover states or visual-only indicators +- Needs adequate color contrast (4.5:1 minimum) +- May use browser zoom up to 200% + +**Test Questions**: +- Can the entire primary flow be completed keyboard-only? +- Are all interactive elements focusable with visible focus indicators? +- Do images have meaningful alt text? +- Is color contrast WCAG AA compliant (4.5:1 for text)? +- Does the screen reader announce state changes (loading, success, errors)? + +**Red Flags** (report these specifically): +- Click-only interactions with no keyboard alternative +- Missing or invisible focus indicators +- Meaning conveyed by color alone (red = error, green = success) +- Unlabeled form fields or buttons +- Time-limited actions without extension option +- Custom components that break screen reader flow + +--- + +#### 4. Deliberate Stress Tester: "Riley" + +**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience. + +**Behaviors**: +- Tests edge cases intentionally (empty states, long strings, special characters) +- Submits forms with unexpected data (emoji, RTL text, very long values) +- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs +- Looks for inconsistencies between what the UI promises and what actually happens +- Documents problems methodically + +**Test Questions**: +- What happens at the edges (0 items, 1000 items, very long text)? +- Do error states recover gracefully or leave the UI in a broken state? +- What happens on refresh mid-workflow? Is state preserved? +- Are there features that appear to work but produce broken results? +- How does the UI handle unexpected input (emoji, special chars, paste from Excel)? + +**Red Flags** (report these specifically): +- Features that appear to work but silently fail or produce wrong results +- Error handling that exposes technical details or leaves UI in a broken state +- Empty states that show nothing useful ("No results" with no guidance) +- Workflows that lose user data on refresh or navigation +- Inconsistent behavior between similar interactions in different parts of the UI + +--- + +#### 5. Distracted Mobile User: "Casey" + +**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection. + +**Behaviors**: +- Uses thumb only; prefers bottom-of-screen actions +- Gets interrupted mid-flow and returns later +- Switches between apps frequently +- Has limited attention span and low patience +- Types as little as possible, prefers taps and selections + +**Test Questions**: +- Are primary actions in the thumb zone (bottom half of screen)? +- Is state preserved if the user leaves and returns? +- Does it work on slow connections (3G)? +- Can forms use autocomplete and smart defaults? +- Are touch targets at least 44×44pt? + +**Red Flags** (report these specifically): +- Important actions positioned at the top of the screen (unreachable by thumb) +- No state persistence; progress lost on tab switch or interruption +- Large text inputs required where selection would work +- Heavy assets loading on every page (no lazy loading) +- Tiny tap targets or targets too close together + +--- + +#### Selecting Personas + +Choose personas based on the interface type: + +| Interface Type | Primary Personas | Why | +|---------------|-----------------|-----| +| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile | +| Dashboard / admin | Alex, Sam | Power users, accessibility | +| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity | +| Onboarding flow | Jordan, Casey | Confusion, interruption | +| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav | +| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile | + +--- + +#### Project-Specific Personas + +If `RULES.md` contains a `## Design Context` section (generated by `impeccable init`), derive 1–2 additional personas from the audience and brand information: + +1. Read the target audience description +2. Identify the primary user archetype not covered by the 5 predefined personas +3. Create a persona following this template: + +``` +##### [Role]: "[Name]" + +**Profile**: [2-3 key characteristics derived from Design Context] + +**Behaviors**: [3-4 specific behaviors based on the described audience] + +**Red Flags**: [3-4 things that would alienate this specific user type] +``` + +Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists. diff --git a/.trae-cn/skills/impeccable/reference/document.md b/.trae-cn/skills/impeccable/reference/document.md index 3125b37a2..60075688a 100644 --- a/.trae-cn/skills/impeccable/reference/document.md +++ b/.trae-cn/skills/impeccable/reference/document.md @@ -45,7 +45,7 @@ Rules that matter: - **Token refs** use `{path.to.token}` (e.g. `{colors.primary}`, `{rounded.md}`). Components may reference primitives; primitives may not reference each other. - **Stitch validates colors as hex sRGB only** (`#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`); OKLCH/HSL/P3 trigger a linter warning, not a hard error. YAML accepts the string either way and our own parser is format-agnostic. Choose based on project posture: (a) if the project has an "OKLCH-only" doctrine or uses Display-P3 values that don't round-trip through sRGB, put OKLCH directly in the frontmatter and accept the Stitch linter warning; (b) if the project wants strict Stitch compliance or plans to use their Tailwind/DTCG export pipeline, put hex in the frontmatter and keep OKLCH in prose as the canonical reference. Never split the source of truth without explicit reason. - **Component sub-tokens** are limited to 8 props: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, `width`. Shadows, motion, focus rings, backdrop-filter: none of those fit. Carry them in the sidecar (Step 4b). -- **Scale keys are open-ended.** Use whatever names the project already uses (`warm-ash-cream`, `surface-container-low`). Don't rename to Material defaults. +- **Scale keys are open-ended.** Use whatever names the project already uses (`oxblood-deep`, `surface-container-low`). Don't rename to Material defaults. - **Variants are naming convention, not schema.** `button-primary` / `button-primary-hover` / `button-primary-active` as sibling keys. ## The markdown body: six sections (exact order) @@ -61,7 +61,7 @@ Optional evocative subtitles are allowed in the form `## 2. Colors: The [Name] P ## When to run -- The user just ran `/impeccable teach` and needs the visual side documented. +- The user just ran `/impeccable init` and needs the visual side documented. - The skill noticed no `DESIGN.md` exists and nudged the user to create one. - An existing `DESIGN.md` is stale (the design has drifted). - Before a large redesign, to capture the current state as a reference. @@ -71,7 +71,7 @@ If a `DESIGN.md` already exists, **do not silently overwrite it**. Show the user ## Two paths - **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 teach, nothing built yet). Interview for five high-level answers, write a minimal DESIGN.md marked ``. Re-run in scan mode once there's code. +- **Seed mode**: the project is pre-implementation (fresh init, nothing built yet). Interview for five high-level answers, 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` forces seed mode regardless of code presence. @@ -103,7 +103,7 @@ Build a structured draft from the discovered tokens. For each token class: From the auto-extracted tokens, draft the YAML frontmatter now (you'll write it at the top of DESIGN.md in Step 4). This is the machine-readable layer: what the live panel and Stitch's linter consume. -- **Colors**: one entry per extracted color. Key = descriptive slug (`warm-ash-cream`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. +- **Colors**: one entry per extracted color. Key = descriptive slug (`oxblood-deep`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. - **Typography**: one entry per role (`display`, `headline`, `title`, `body`, `label`). Typography is an object; include only the props that are real for the project (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`). - **Rounded / Spacing**: whatever scale steps the project actually uses, keyed by whatever scale name the project uses (`sm` / `md` / `lg`, or `surface-sm`, or numeric steps). - **Components**: one entry per variant (`button-primary`, `button-primary-hover`, `button-ghost`). Reference primitives via `{colors.X}`, `{rounded.Y}`. If a variant needs a property Stitch's 8-prop set doesn't cover (shadow, focus ring, backdrop-filter), carry the full snippet in the sidecar instead. @@ -253,7 +253,7 @@ Regenerate the sidecar whenever you regenerate root `DESIGN.md`. If the user onl "extensions": { "colorMeta": { "primary": { "role": "primary", "displayName": "Editorial Magenta", "canonical": "oklch(60% 0.25 350)", "tonalRamp": ["...", "...", "..."] }, - "warm-ash-cream": { "role": "neutral", "displayName": "Warm Ash Cream", "canonical": "oklch(96% 0.005 350)", "tonalRamp": ["...", "...", "..."] } + "cool-paper": { "role": "neutral", "displayName": "Cool Paper", "canonical": "oklch(96% 0.005 230)", "tonalRamp": ["...", "...", "..."] } }, "typographyMeta": { "display": { "displayName": "Display", "purpose": "Hero headlines only." } @@ -328,12 +328,13 @@ Pull directly from the DESIGN.md you just wrote: Do not reword. The panel shows these as secondary collapsible context; the same voice that's in the Markdown carries through. -### Step 5: Confirm, refine, and refresh session cache +### Step 5: Confirm and refine 1. Show the user the full DESIGN.md you wrote. Briefly highlight the non-obvious creative choices (descriptive color names, atmosphere language, named rules). 2. Mention that `.impeccable/design.json` was also written alongside; the live panel will now render this project's actual button/input/nav primitives instead of generic approximations. 3. Offer to refine any section: "Want me to revise a section, add component patterns I missed, or adjust the atmosphere language?" -4. **Refresh the session cache.** Run `node .trae-cn/skills/impeccable/scripts/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. + +Your own write is the freshest source; subsequent commands in this session don't need a reload. ## Seed mode @@ -394,11 +395,12 @@ Per-section guidance in seed mode: 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. -### Step 4: Confirm and refresh session cache +### Step 4: Confirm 1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment). 2. Tell the user: "Re-run `/impeccable document` once you have some code. That pass will extract real tokens and generate the sidecar." -3. Run `node .trae-cn/skills/impeccable/scripts/load-context.mjs` once so the seed lands in conversation for the rest of the session. + +Your own write is the freshest source; no reload needed. ## Style guidelines diff --git a/.trae-cn/skills/impeccable/reference/heuristics-scoring.md b/.trae-cn/skills/impeccable/reference/heuristics-scoring.md deleted file mode 100644 index edbe5028d..000000000 --- a/.trae-cn/skills/impeccable/reference/heuristics-scoring.md +++ /dev/null @@ -1,234 +0,0 @@ -# Heuristics Scoring Guide - -Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough." - -## Nielsen's 10 Heuristics - -### 1. Visibility of System Status - -Keep users informed about what's happening through timely, appropriate feedback. - -**Check for**: -- Loading indicators during async operations -- Confirmation of user actions (save, submit, delete) -- Progress indicators for multi-step processes -- Current location in navigation (breadcrumbs, active states) -- Form validation feedback (inline, not just on submit) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | No feedback; user is guessing what happened | -| 1 | Rare feedback; most actions produce no visible response | -| 2 | Partial; some states communicated, major gaps remain | -| 3 | Good; most operations give clear feedback, minor gaps | -| 4 | Excellent; every action confirms, progress is always visible | - -### 2. Match Between System and Real World - -Speak the user's language. Follow real-world conventions. Information appears in natural, logical order. - -**Check for**: -- Familiar terminology (no unexplained jargon) -- Logical information order matching user expectations -- Recognizable icons and metaphors -- Domain-appropriate language for the target audience -- Natural reading flow (left-to-right, top-to-bottom priority) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Pure tech jargon, alien to users | -| 1 | Mostly confusing; requires domain expertise to navigate | -| 2 | Mixed; some plain language, some jargon leaks through | -| 3 | Mostly natural; occasional term needs context | -| 4 | Speaks the user's language fluently throughout | - -### 3. User Control and Freedom - -Users need a clear "emergency exit" from unwanted states without extended dialogue. - -**Check for**: -- Undo/redo functionality -- Cancel buttons on forms and modals -- Clear navigation back to safety (home, previous) -- Easy way to clear filters, search, selections -- Escape from long or multi-step processes - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Users get trapped; no way out without refreshing | -| 1 | Difficult exits; must find obscure paths to escape | -| 2 | Some exits; main flows have escape, edge cases don't | -| 3 | Good control; users can exit and undo most actions | -| 4 | Full control; undo, cancel, back, and escape everywhere | - -### 4. Consistency and Standards - -Users shouldn't wonder whether different words, situations, or actions mean the same thing. - -**Check for**: -- Consistent terminology throughout the interface -- Same actions produce same results everywhere -- Platform conventions followed (standard UI patterns) -- Visual consistency (colors, typography, spacing, components) -- Consistent interaction patterns (same gesture = same behavior) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Inconsistent everywhere; feels like different products stitched together | -| 1 | Many inconsistencies; similar things look/behave differently | -| 2 | Partially consistent; main flows match, details diverge | -| 3 | Mostly consistent; occasional deviation, nothing confusing | -| 4 | Fully consistent; cohesive system, predictable behavior | - -### 5. Error Prevention - -Better than good error messages is a design that prevents problems in the first place. - -**Check for**: -- Confirmation before destructive actions (delete, overwrite) -- Constraints preventing invalid input (date pickers, dropdowns) -- Smart defaults that reduce errors -- Clear labels that prevent misunderstanding -- Autosave and draft recovery - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Errors easy to make; no guardrails anywhere | -| 1 | Few safeguards; some inputs validated, most aren't | -| 2 | Partial prevention; common errors caught, edge cases slip | -| 3 | Good prevention; most error paths blocked proactively | -| 4 | Excellent; errors nearly impossible through smart constraints | - -### 6. Recognition Rather Than Recall - -Minimize memory load. Make objects, actions, and options visible or easily retrievable. - -**Check for**: -- Visible options (not buried in hidden menus) -- Contextual help when needed (tooltips, inline hints) -- Recent items and history -- Autocomplete and suggestions -- Labels on icons (not icon-only navigation) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Heavy memorization; users must remember paths and commands | -| 1 | Mostly recall; many hidden features, few visible cues | -| 2 | Some aids; main actions visible, secondary features hidden | -| 3 | Good recognition; most things discoverable, few memory demands | -| 4 | Everything discoverable; users never need to memorize | - -### 7. Flexibility and Efficiency of Use - -Accelerators, invisible to novices, speed up expert interaction. - -**Check for**: -- Keyboard shortcuts for common actions -- Customizable interface elements -- Recent items and favorites -- Bulk/batch actions -- Power user features that don't complicate the basics - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | One rigid path; no shortcuts or alternatives | -| 1 | Limited flexibility; few alternatives to the main path | -| 2 | Some shortcuts; basic keyboard support, limited bulk actions | -| 3 | Good accelerators; keyboard nav, some customization | -| 4 | Highly flexible; multiple paths, power features, customizable | - -### 8. Aesthetic and Minimalist Design - -Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose. - -**Check for**: -- Only necessary information visible at each step -- Clear visual hierarchy directing attention -- Purposeful use of color and emphasis -- No decorative clutter competing for attention -- Focused, uncluttered layouts - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Overwhelming; everything competes for attention equally | -| 1 | Cluttered; too much noise, hard to find what matters | -| 2 | Some clutter; main content clear, periphery noisy | -| 3 | Mostly clean; focused design, minor visual noise | -| 4 | Perfectly minimal; every element earns its pixel | - -### 9. Help Users Recognize, Diagnose, and Recover from Errors - -Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution. - -**Check for**: -- Plain language error messages (no error codes for users) -- Specific problem identification ("Email is missing @" not "Invalid input") -- Actionable recovery suggestions -- Errors displayed near the source of the problem -- Non-blocking error handling (don't wipe the form) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Cryptic errors; codes, jargon, or no message at all | -| 1 | Vague errors; "Something went wrong" with no guidance | -| 2 | Clear but unhelpful; names the problem but not the fix | -| 3 | Clear with suggestions; identifies problem and offers next steps | -| 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work | - -### 10. Help and Documentation - -Even if the system is usable without docs, help should be easy to find, task-focused, and concise. - -**Check for**: -- Searchable help or documentation -- Contextual help (tooltips, inline hints, guided tours) -- Task-focused organization (not feature-organized) -- Concise, scannable content -- Easy access without leaving current context - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | No help available anywhere | -| 1 | Help exists but hard to find or irrelevant | -| 2 | Basic help; FAQ or docs exist, not contextual | -| 3 | Good documentation; searchable, mostly task-focused | -| 4 | Excellent contextual help; right info at the right moment | - ---- - -## Score Summary - -**Total possible**: 40 points (10 heuristics × 4 max) - -| Score Range | Rating | What It Means | -|-------------|--------|---------------| -| 36–40 | Excellent | Minor polish only; ship it | -| 28–35 | Good | Address weak areas, solid foundation | -| 20–27 | Acceptable | Significant improvements needed before users are happy | -| 12–19 | Poor | Major UX overhaul required; core experience broken | -| 0–11 | Critical | Redesign needed; unusable in current state | - ---- - -## Issue Severity (P0–P3) - -Tag each individual issue found during scoring with a priority level: - -| Priority | Name | Description | Action | -|----------|------|-------------|--------| -| **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper | -| **P1** | Major | Causes significant difficulty or confusion | Fix before release | -| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass | -| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits | - -**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1. diff --git a/.trae/skills/impeccable/reference/teach.md b/.trae-cn/skills/impeccable/reference/init.md similarity index 58% rename from .trae/skills/impeccable/reference/teach.md rename to .trae-cn/skills/impeccable/reference/init.md index ed0ceedd7..926ea229a 100644 --- a/.trae/skills/impeccable/reference/teach.md +++ b/.trae-cn/skills/impeccable/reference/init.md @@ -1,21 +1,16 @@ -# Teach Flow +# Init Flow -Gathers design context for a project and writes two complementary files at the project root: +The setup command for a project. One codebase crawl feeds everything it writes: - **PRODUCT.md** (strategic): root project file for register, target users, product purpose, brand personality, anti-references, strategic design principles. Answers "who/what/why". - **DESIGN.md** (visual): root project file for visual theme, color palette, typography, components, layout. Follows the [Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/). Answers "how it looks". +- **`.impeccable/live/config.json`** (live mode): pre-configured so `/impeccable live` boots straight into variant mode with no first-time detour. -Every other impeccable command reads these files before doing any work. +It closes by pointing the user at the best command to run next. Every other impeccable command reads PRODUCT.md and DESIGN.md before doing any work. ## Step 1: Load current state -Run the shared loader first so you know what already exists: - -```bash -node .trae/skills/impeccable/scripts/load-context.mjs -``` - -The output tells you whether PRODUCT.md and/or DESIGN.md already exist. If `migrated: true`, legacy `.impeccable.md` was auto-renamed to `PRODUCT.md`. Mention this once to the user. +Check what already exists. PRODUCT.md and DESIGN.md live at the project root, or under `.agents/context/` or `docs/` (case-insensitive). Read whichever are present with your native file tool. Also note whether `.impeccable/live/config.json` already exists (Step 6 leaves it untouched if so). Decision tree: - **Neither file exists (empty project or no context yet)**: do Steps 2-4 (write PRODUCT.md), then decide on DESIGN.md based on whether there's code to analyze. @@ -26,14 +21,14 @@ Decision tree: Never silently overwrite an existing file. Always confirm first. -If teach was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete teach, re-run the loader, then resume the original command with the freshly loaded context. For craft, resume into shape next; teach creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief. +If init was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete init, then resume the original command. Your own writes are the freshest source; no reload needed. For craft, resume into shape next; init creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief. ## Step 2: Explore the codebase -Before asking questions, thoroughly scan the project to discover what you can: +Before asking questions, thoroughly scan the project to discover what you can. This single crawl feeds PRODUCT.md, DESIGN.md, **and** the live-mode framework detection in Step 6, so be thorough once rather than re-scanning later: - **README and docs**: Project purpose, target audience, any stated goals -- **Package.json / config files**: Tech stack, dependencies, existing design libraries +- **Package.json / config files**: Tech stack, dependencies, existing design libraries, **and the framework** (Vite/SPA, Next.js, Nuxt, SvelteKit, Astro, multi-page static) plus the HTML entry the browser actually loads - **Existing components**: Current design patterns, spacing, typography in use - **Brand assets**: Logos, favicons, color values already defined - **Design tokens / CSS variables**: Existing color palettes, font stacks, spacing scales @@ -46,7 +41,7 @@ Also form a **register hypothesis** from what you find: Register is a hypothesis at this point, not a decision; Step 3 confirms it. -Note what you've learned and what remains unclear. This exploration feeds both PRODUCT.md and DESIGN.md. +Note what you've learned and what remains unclear. Also note any rough edges worth a follow-up command (thin hierarchy, flat or gray palette, missing error/empty states, dull copy); Step 7 turns these into concrete recommendations without re-analyzing. ## Step 3: Ask strategic questions (for PRODUCT.md) @@ -84,8 +79,7 @@ If the signal is genuinely split (e.g. a product with a big marketing landing), ### Brand & Personality - How would you describe the brand personality in 3 words? - Reference sites or apps that capture the right feel? What specifically about them? - - For brand, push for real-world references in the right lane (tech-minimal, editorial-magazine, consumer-warm, brutalist-grid, etc.), not generic "modern" adjectives. - - For product, push for category best-tool references (Linear, Figma, Notion, Raycast, Stripe). + - Push for specific named references with the *specific* thing about them that fits this brand, not generic "modern" adjectives or category-bucket lanes. - What should this explicitly NOT look like? Any anti-references? ### Accessibility & Inclusion @@ -141,16 +135,38 @@ If the user agrees, delegate to `/impeccable document` (it auto-detects scan vs If the user prefers to skip, mention they can run `/impeccable document` any time later. -## Step 6: Confirm and wrap up +## Step 6: Configure live mode (when code exists) -Summarize: +If the project has code with HTML entries and a dev server (the same "code exists" condition that puts `/impeccable document` in scan mode), pre-configure live mode now. You already identified the framework and the served HTML entry in Step 2, so this is nearly free, and it spares the user the first-time setup detour when they later run `/impeccable live`. + +**Skip this step for empty / pre-implementation projects** (nothing to inject into yet). Tell the user live mode will configure itself the first time they run it once there's code. + +**If `.impeccable/live/config.json` already exists, leave it untouched** and note that live mode is already configured. + +Otherwise: + +1. Write `.impeccable/live/config.json`. Choose `files` (the HTML entries the browser actually loads), `insertBefore`, and `commentSyntax` from the framework table in [live.md](live.md)'s **First-time setup** section, using the framework you found in Step 2. That table is canonical; do not restate it here. For multi-page static sites, prefer a glob (`["public/**/*.html"]`) over a literal list. +2. Run `node .trae-cn/skills/impeccable/scripts/detect-csp.mjs`. If it reports a patchable shape (`append-arrays` / `append-string`), use the **consent prompt template** from live.md before editing any source file. On decline, skip the patch. For `middleware` / `meta-tag` shapes, surface the detected files and ask the user to add `http://localhost:8400` to `script-src` and `connect-src` manually. For `null`, there's nothing to do. +3. Set `cspChecked: true` in the config once CSP is handled (patched, declined, manual, or not needed). The schema and per-shape patch details live in live.md's First-time setup; follow it rather than duplicating. + +Writing the config file is harmless and needs no consent; only the CSP **source-file patch** requires a yes. + +## Step 7: Recommend starting points, then wrap up + +Summarize tersely: - Register captured (brand / product) -- What was written (PRODUCT.md, DESIGN.md, or both) +- What was written (PRODUCT.md, DESIGN.md, live config, or a subset) - The 3-5 strategic principles from PRODUCT.md that will guide future work -- If DESIGN.md is pending, remind the user how to generate it later +- If DESIGN.md or live config is pending, one line on how to set it up later -**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node .trae/skills/impeccable/scripts/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. +Then recommend the **best commands to run next**, drawn from what your Step 2 crawl already surfaced. Do not run a fresh analysis here; surface observations you already have. Tailor to register and to what you saw, offer the 2-4 most relevant (not a menu dump), and give the exact command to type. Group by intent: -If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context. +- **Build something new**: `/impeccable craft ` (shape, then build end-to-end) or `/impeccable shape ` (plan first). Lead with this for empty or early-stage projects. +- **Improve what's there**: name the specific surface. `/impeccable critique ` for a scored UX review; `/impeccable audit ` for a11y / perf / responsive checks; `/impeccable polish ` for a pre-ship pass. When the crawl flagged a specific weakness, point the matching command at it: thin hierarchy or spacing → `layout`, flat or gray palette → `colorize`, missing error / empty states → `harden` or `onboard`, dull or unclear copy → `clarify`. +- **Iterate visually**: `/impeccable live` (configured in Step 6) to pick elements in the browser and generate variants in place. + +The full command menu is one bare `/impeccable` away; keep this list short and pointed. + +If init was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now. Your own writes are the freshest source; no reload needed. Optionally ask the user directly to clarify what you cannot infer. Ask whether they'd like a brief summary of PRODUCT.md appended to RULES.md for easier agent reference. If yes, append a short **Design Context** pointer section there. diff --git a/.trae-cn/skills/impeccable/reference/interaction-design.md b/.trae-cn/skills/impeccable/reference/interaction-design.md index 15aed5b29..c42c9d810 100644 --- a/.trae-cn/skills/impeccable/reference/interaction-design.md +++ b/.trae-cn/skills/impeccable/reference/interaction-design.md @@ -150,12 +150,6 @@ For browsers without anchor positioning support, `position: fixed` with manual c Check viewport boundaries before rendering. If the dropdown would overflow the bottom edge, flip it above the trigger. If it would overflow the right edge, align it to the trigger's right side instead. -### Anti-Patterns - -- **`position: absolute` inside `overflow: hidden`** - The dropdown will be clipped. Use `position: fixed` or the top layer instead. -- **Arbitrary z-index values** like `z-index: 9999` - Use a semantic z-index scale: `dropdown (100) -> sticky (200) -> modal-backdrop (300) -> modal (400) -> toast (500) -> tooltip (600)`. -- **Rendering dropdown markup inline** without an escape hatch from the parent's stacking context. Either use `popover` (top layer), a portal, or `position: fixed`. - ## Destructive Actions: Undo > Confirm **Undo is better than confirmation dialogs.** Users click through confirmations mindlessly. Remove from UI immediately, show undo toast, actually delete after toast expires. Use confirmation only for truly irreversible actions (account deletion), high-cost actions, or batch operations. diff --git a/.trae-cn/skills/impeccable/reference/layout.md b/.trae-cn/skills/impeccable/reference/layout.md index cad4c62a1..8765978b0 100644 --- a/.trae-cn/skills/impeccable/reference/layout.md +++ b/.trae-cn/skills/impeccable/reference/layout.md @@ -1,4 +1,4 @@ -Space is the most underused design tool. Find the layout's actual problem (monotone spacing, weak hierarchy, identical card grids, the centered-stack default) and fix the structure, not the surface. +Space is the most underused design tool. Find the layout's actual problem (monotone spacing, weak hierarchy, identical card grids) and fix the structure, not the surface. --- @@ -27,7 +27,6 @@ Analyze what's weak about the current spatial design: 3. **Grid & structure**: - Is there a clear underlying structure, or does the layout feel random? - Are identical card grids used everywhere? (Icon + heading + text, repeated endlessly) - - Is everything centered? (Left-aligned with asymmetric layouts feels more designed, but not a hard and fast rule) 4. **Rhythm & variety**: - Does the layout have visual rhythm? (Alternating tight/generous spacing) @@ -43,8 +42,6 @@ Analyze what's weak about the current spatial design: ## Plan Layout Improvements -Consult the [spatial design reference](spatial-design.md) for detailed guidance on grids, rhythm, and container queries. - Create a systematic plan: - **Spacing system**: Use a consistent scale (a framework's built-in scale like Tailwind's, rem-based tokens, or a custom system). The specific values matter less than consistency. @@ -57,6 +54,7 @@ Create a systematic plan: ### Establish a Spacing System - Use a consistent spacing scale (framework scales like Tailwind, rem-based tokens, or a custom scale all work). What matters is that values come from a defined set, not arbitrary numbers. +- Prefer a 4pt base scale (4, 8, 12, 16, 24, 32, 48, 64, 96px) over 8pt; 8pt is too coarse and you'll frequently need 12px between 8 and 16. - Name tokens semantically if using custom properties: `--space-xs` through `--space-xl`, not `--spacing-8` - Use `gap` for sibling spacing instead of margins; eliminates margin collapse hacks - Apply `clamp()` for fluid spacing that breathes on larger screens @@ -66,15 +64,22 @@ Create a systematic plan: - **Tight grouping** for related elements (8-12px between siblings) - **Generous separation** between distinct sections (48-96px) - **Varied spacing** within sections (not every row needs the same gap) -- **Asymmetric compositions**: break the predictable centered-content pattern when it makes sense +- **Asymmetric compositions**: a deliberate choice when the content invites it (not a default to chase). ### Choose the Right Layout Tool -- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. Flex is simpler and more appropriate for the majority of layout tasks. +- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. - **Use Grid for 2D layouts**: Page-level structure, dashboards, data-dense interfaces, anything where rows AND columns need coordinated control. -- **Don't default to Grid** when Flexbox with `flex-wrap` would be simpler and more flexible. -- Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. - Use named grid areas (`grid-template-areas`) for complex page layouts; redefine at breakpoints. +- Use **container queries** for components, viewport queries for page layouts. A card in a narrow sidebar can stay compact while the same card in a main content area expands automatically: + +```css +.card-container { container-type: inline-size; } +.card { display: grid; gap: var(--space-md); } +@container (min-width: 400px) { + .card { grid-template-columns: 120px 1fr; } +} +``` ### Break Card Grid Monotony @@ -85,18 +90,36 @@ Create a systematic plan: ### Strengthen Visual Hierarchy - Use the fewest dimensions needed for clear hierarchy. Space alone can be enough; generous whitespace around an element draws the eye. Some of the most polished designs achieve rhythm with just space and weight. Add color or size contrast only when simpler means aren't sufficient. +- The best hierarchy combines 2–3 dimensions at once. A heading that's larger, bolder, AND has more space above it reads as primary without trying: + +| Tool | Strong Hierarchy | Weak Hierarchy | +|------|------------------|----------------| +| **Size** | 3:1 ratio or more | <2:1 ratio | +| **Weight** | Bold vs Regular | Medium vs Regular | +| **Color** | High contrast | Similar tones | +| **Position** | Top/left (primary) | Bottom/right | +| **Space** | Surrounded by white space | Crowded | + - Be aware of reading flow: in LTR languages, the eye naturally scans top-left to bottom-right, but primary action placement depends on context (e.g., bottom-right in dialogs, top in navigation). - Create clear content groupings through proximity and separation. ### Manage Depth & Elevation -- Create a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip) - Build a consistent shadow scale (sm → md → lg → xl); shadows should be subtle - Use elevation to reinforce hierarchy, not as decoration ### Optical Adjustments - If an icon looks visually off-center despite being geometrically centered, nudge it. But only if you're confident it actually looks wrong. Don't adjust speculatively. +- Text at `margin-left: 0` looks slightly indented because of letterform whitespace; a negative margin (`-0.05em`) optically aligns it. Geometrically centered glyphs often look off-center (play icons need to shift right, arrows shift toward their direction). +- Touch targets must be 44×44px minimum even when the visual element is smaller. Expand the hit area with padding or a pseudo-element: + +```css +.icon-button { width: 24px; height: 24px; position: relative; } +.icon-button::before { + content: ''; position: absolute; inset: -10px; +} +``` **NEVER**: - Use arbitrary spacing values outside your scale @@ -104,10 +127,7 @@ Create a systematic plan: - Wrap everything in cards (not everything needs a container) - Nest cards inside cards (use spacing and dividers for hierarchy within) - Use identical card grids everywhere (icon + heading + text, repeated) -- Center everything (left-aligned with asymmetry feels more designed) - Default to the hero metric layout (big number, small label, stats, gradient) as a template. If showing real user data, a prominent metric can work, but it should display actual data, not decorative numbers. -- Default to CSS Grid when Flexbox would be simpler; use the simplest tool for the job -- Use arbitrary z-index values (999, 9999); build a semantic scale ## Verify Layout Improvements diff --git a/.trae-cn/skills/impeccable/reference/live.md b/.trae-cn/skills/impeccable/reference/live.md index a1c8aa5ab..b33b05e39 100644 --- a/.trae-cn/skills/impeccable/reference/live.md +++ b/.trae-cn/skills/impeccable/reference/live.md @@ -9,18 +9,21 @@ A running dev server with hot module replacement (Vite, Next.js, Bun, etc.), OR Execute in order. No step skipped, no step reordered. 1. `live.mjs`: boot. -2. Navigate to the URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). If you can't infer it confidently, tell the user once to open their dev/preview URL. Never use `serverPort` as that URL; it's the helper, not the app. +2. Open the app URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). Never use `serverPort`; it's the helper, not the app. **Cursor:** `browser_navigate` to that URL before polling; do not skip. **Other harnesses:** use the available browser tool; if the URL is uncertain, ask the user once. 3. Poll loop with the default long timeout (600000 ms). After every event or `--reply`, run `live-poll.mjs` again immediately. Never pass a short `--timeout=`. + +The global bar **Impeccable mark** dims and shows a pulsing amber dot when no agent is long-polling `/poll`. Hover the mark for the hint; restart `live-poll.mjs` to reconnect. 4. On `generate`: read screenshot if present; load the action's reference; plan three distinct directions; write all variants in one edit; `--reply done`; poll again. -5. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts remain recoverable until you finish cleanup, run `live-complete.mjs --id EVENT_ID`, and only then poll again. -6. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart. -7. On `exit`: run the cleanup at the bottom. +5. On `steer`: read the message and `pageUrl`; do the work (page edits, navigation help, or a short reply in the `--reply` message); `--reply steer_done`; poll again. No pickup ack. The Steer bar unlocks when `steer_done` arrives over SSE. +6. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts remain recoverable until you finish cleanup, run `live-complete.mjs --id EVENT_ID`, and only then poll again. +7. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart. +8. On `exit`: run the cleanup at the bottom. Harness policy: - **Claude Code**: run the poll as a **background task** (no short timeout). The harness notifies you when it completes, so the main conversation stays free. Do not block the shell. -- **Cursor**: run the poll in the **foreground** (blocking shell; not a background terminal, not a subagent). Cursor background terminals and subagents do not reliably resume the chat with poll stdout. +- **Cursor**: run **one-shot** poll in a **background terminal** with notify on `"type":"(steer|generate|accept|discard|exit)"`. After each event the poll exits; handle it, `--reply`, then start `live-poll.mjs` again. Do **not** use `--stream` on Cursor: incremental stdout notify is slower in practice than exit-based notify (~5s vs sub-second in testing). - **Codex**: run the poll in the **foreground** (blocking shell; not a background task, not a subagent). Codex background exec sessions do not reliably surface poll stdout back into the conversation at the moment events arrive, so a "fire-and-forget" background poll will stall live mode. -- **Other harnesses**: foreground unless you know stdout reliably returns to this session. +- **Other harnesses**: one-shot foreground unless you know stdout reliably returns to this session when a shell exits. Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodies. Spend tokens on tools and edits; on failure, one or two short sentences. @@ -30,7 +33,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi node .trae-cn/skills/impeccable/scripts/live.mjs ``` -Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation; **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md. +Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation; **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. `serverPort` and `serverToken` belong to the small **Impeccable live helper** HTTP server (serves `/live.js`, SSE, and `/poll`). That port is **not** your dev server and is usually not the URL you open to view the app. The browser page is whatever origin serves one of the `pageFiles` entries (Vite / Next / Bun / tunnel / LAN hostname). @@ -38,12 +41,15 @@ If output is `{ ok: false, error: "config_missing" | "config_invalid", path }`, ## Poll loop +**Default (portable, all harnesses):** + ``` LOOP: node .trae-cn/skills/impeccable/scripts/live-poll.mjs # default long timeout; no --timeout= Read JSON; dispatch on "type" "generate" → Handle Generate; reply done; LOOP + "steer" → Handle Steer; reply steer_done; LOOP "accept" → Handle Accept; complete carbonize cleanup if required; LOOP "discard" → Handle Discard; LOOP "prefetch" → Handle Prefetch; LOOP @@ -51,6 +57,16 @@ LOOP: "exit" → break → Cleanup ``` +**Stream mode (experimental, not for Cursor):** + +``` +node .trae-cn/skills/impeccable/scripts/live-poll.mjs --stream # stays running; one JSON line per event + Handle event; run --reply in a separate command + Repeat until "exit" line → Cleanup +``` + +Stream keeps one process alive and waits for `--reply` ack before polling again. Useful only when the harness reads incremental stdout reliably and quickly. **Cursor is not one of those:** background pattern notify on a long-running shell was ~5s to pick up events vs sub-second for one-shot exit notify. Default to one-shot everywhere unless you have measured otherwise. + ## Recovery commands The live helper persists an append-only journal under `.impeccable/live/sessions/`. Browser checkpoints are advisory but durable; the journal is canonical. This is local durable recovery state, not project source. @@ -71,9 +87,32 @@ Server restart rule: start `live-server.mjs` again, then poll. Startup requeues ## Handle `generate` -Event: `{id, action, freeformPrompt?, count, pageUrl, element, screenshotPath?, comments?, strokes?}`. +**Replace mode** (default): `{id, action, freeformPrompt?, count, pageUrl, element, screenshotPath?, comments?, strokes?}`. -Speed matters; the user is watching a spinner. Minimize tool calls by using the `wrap` helper and writing all variants in a single edit. +**Insert mode** (`event.mode === "insert"`): `{id, mode: "insert", count, pageUrl, insert: { position, anchor }, placeholder: { width, height }, freeformPrompt?, screenshotPath?, comments?, strokes?}`. No `action`. Requires a non-empty `freeformPrompt` **or** annotations. Screenshot is sent only when annotations exist (same rule as replace). Use `placeholder` dimensions as a soft size hint for net-new content. + +Speed matters; the user is watching a spinner. Minimize tool calls by using the wrap/insert helper and writing all variants in a single edit. + +### Insert mode branch + +When `event.mode === "insert"`: + +1. Read the screenshot if `event.screenshotPath` is present (annotations only). +2. Run the insert helper instead of wrap: + +```bash +node .trae-cn/skills/impeccable/scripts/live-insert.mjs --id EVENT_ID --count EVENT_COUNT --position after \ + --element-id "ANCHOR_ID" --classes "class1,class2" --tag "section" --text "ANCHOR_TEXT" +``` + +- `--position` ← `event.insert.position` (`before` | `after`) +- Anchor flags ← `event.insert.anchor` (same mapping as wrap: id, classes, tag, text) + +The scaffold has **no** `data-impeccable-variant="original"`. Variants are net-new HTML+CSS inserted at `insertLine`. Load `brand.md` or `product.md` (freeform only, no action sub-command). Write all variants in one edit, then `--reply done`. + +On accept/discard, `live-accept.mjs` removes the wrapper block; the anchor element is untouched. + +### Replace mode (default) ### 1. Read the screenshot (if present) @@ -424,6 +463,28 @@ A background agent may be used for the rewrite, but the current thread is respon Event: `{id, _acceptResult, _completionAck}`. The poll script already restored the original, removed all variant markers, and acknowledged `discarded` durable completion. Nothing to do unless `_completionAck.ok !== true`; in that case run `live-complete.mjs --id EVENT_ID --discarded`, then poll again. +## Handle `steer` + +Event: `{id, message, pageUrl}`. The user typed or spoke into the global bar **Steer** control: page-level direction without picking an element or launching variant generation. + +The mic button uses the browser **Web Speech API** (MVP): click to start, speak, stop automatically when the utterance ends, then the transcript submits as a steer event. Click again while listening to cancel without submitting. + +This is lighter than `generate`: no screenshot, no element context, no variant cycling. Read `message` and inspect the live page or project files as needed, then either make edits or answer in prose. + +When finished: + +```bash +node .trae-cn/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID steer_done ["Optional short note for a browser toast"] +``` + +On failure: + +```bash +node .trae-cn/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID error "Short reason" +``` + +Then poll again immediately. Do not send a separate "picked up" reply. The Steer bar stays locked until `steer_done` or `error` arrives over SSE. + ## Handle `prefetch` Event: `{pageUrl}`. The browser fires this the first time the user selects an element on a given route, as a latency shortcut; it signals the user is likely about to Go on a page you haven't read yet. diff --git a/.trae-cn/skills/impeccable/reference/motion-design.md b/.trae-cn/skills/impeccable/reference/motion-design.md deleted file mode 100644 index 78b6fd260..000000000 --- a/.trae-cn/skills/impeccable/reference/motion-design.md +++ /dev/null @@ -1,109 +0,0 @@ -# Motion Design - -## Duration: The 100/300/500 Rule - -Timing matters more than easing. These durations feel right for most UI: - -| Duration | Use Case | Examples | -|----------|----------|----------| -| **100-150ms** | Instant feedback | Button press, toggle, color change | -| **200-300ms** | State changes | Menu open, tooltip, hover states | -| **300-500ms** | Layout changes | Accordion, modal, drawer | -| **500-800ms** | Entrance animations | Page load, hero reveals | - -**Exit animations are faster than entrances.** Use ~75% of enter duration. - -## Easing: Pick the Right Curve - -**Don't use `ease`.** It's a compromise that's rarely optimal. Instead: - -| Curve | Use For | CSS | -|-------|---------|-----| -| **ease-out** | Elements entering | `cubic-bezier(0.16, 1, 0.3, 1)` | -| **ease-in** | Elements leaving | `cubic-bezier(0.7, 0, 0.84, 0)` | -| **ease-in-out** | State toggles (there → back) | `cubic-bezier(0.65, 0, 0.35, 1)` | - -**For micro-interactions, use exponential curves.** They feel natural because they mimic real physics (friction, deceleration): - -```css -/* Quart out - smooth, refined (recommended default) */ ---ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1); - -/* Quint out - slightly more dramatic */ ---ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1); - -/* Expo out - snappy, confident */ ---ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); -``` - -**Avoid bounce and elastic curves.** They were trendy in 2015 but now feel tacky and amateurish. Real objects don't bounce when they stop; they decelerate smoothly. Overshoot effects draw attention to the animation itself rather than the content. - -## Premium Motion Materials - -Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties: blur reveals, backdrop-filter panels, saturation or brightness shifts, shadow bloom, SVG filters, masks, clip paths, gradient-position movement, and variable font or shader-driven effects. - -Use the right material for the effect: - -- **Transform / opacity**: movement, press feedback, simple reveals, list choreography. -- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances, atmospheric transitions. -- **Clip path / masks**: wipes, reveals, editorial cropping, product-like transitions. -- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state. -- **Grid-template rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly. - -The hard rule is not "transform and opacity only." The hard rule is: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify in-browser that the result is smooth on the target viewports. If blur/filter makes the interaction feel significantly more premium and remains smooth, use it. - -## Staggered Animations - -Use CSS custom properties for cleaner stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"` on each item. **Cap total stagger time**: 10 items at 50ms = 500ms total. For many items, reduce per-item delay or cap staggered count. - -## Reduced Motion - -This is not optional. Vestibular disorders affect ~35% of adults over 40. - -```css -/* Define animations normally */ -.card { - animation: slide-up 500ms ease-out; -} - -/* Provide alternative for reduced motion */ -@media (prefers-reduced-motion: reduce) { - .card { - animation: fade-in 200ms ease-out; /* Crossfade instead of motion */ - } -} - -/* Or disable entirely */ -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - transition-duration: 0.01ms !important; - } -} -``` - -**What to preserve**: Functional animations like progress bars, loading spinners (slowed down), and focus indicators should still work, just without spatial movement. - -## Perceived Performance - -**Nobody cares how fast your site is, just how fast it feels.** Perception can be as effective as actual performance. - -**The 80ms threshold**: Our brains buffer sensory input for ~80ms to synchronize perception. Anything under 80ms feels instant and simultaneous. This is your target for micro-interactions. - -**Active vs passive time**: Passive waiting (staring at a spinner) feels longer than active engagement. Strategies to shift the balance: - -- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening. -- **Early completion**: Show content progressively, don't wait for everything. Video buffering, progressive images, streaming HTML. -- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Instagram likes work offline; the UI updates instantly, syncs later. Use for low-stakes actions; avoid for payments or destructive operations. - -**Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances, but ease-in toward a task's end compresses perceived time. - -**Caution**: Too-fast responses can decrease perceived value. Users may distrust instant results for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening. - -## Performance - -Don't use `will-change` preemptively, only when animation is imminent (`:hover`, `.animating`). For scroll-triggered animations, use Intersection Observer instead of scroll events; unobserve after animating once. Create motion tokens for consistency (durations, easings, common transitions). - ---- - -**Avoid**: Animating everything (animation fatigue is real). Using >500ms for UI feedback. Ignoring `prefers-reduced-motion`. Using animation to hide slow loading. diff --git a/.trae-cn/skills/impeccable/reference/personas.md b/.trae-cn/skills/impeccable/reference/personas.md deleted file mode 100644 index c5fe18ef1..000000000 --- a/.trae-cn/skills/impeccable/reference/personas.md +++ /dev/null @@ -1,179 +0,0 @@ -# Persona-Based Design Testing - -Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss. - -**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns. - ---- - -## 1. Impatient Power User: "Alex" - - -**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave. - -**Behaviors**: -- Skips all onboarding and instructions -- Looks for keyboard shortcuts immediately -- Tries to bulk-select, batch-edit, and automate -- Gets frustrated by required steps that feel unnecessary -- Abandons if anything feels slow or patronizing - -**Test Questions**: -- Can Alex complete the core task in under 60 seconds? -- Are there keyboard shortcuts for common actions? -- Can onboarding be skipped entirely? -- Do modals have keyboard dismiss (Esc)? -- Is there a "power user" path (shortcuts, bulk actions)? - -**Red Flags** (report these specifically): -- Forced tutorials or unskippable onboarding -- No keyboard navigation for primary actions -- Slow animations that can't be skipped -- One-item-at-a-time workflows where batch would be natural -- Redundant confirmation steps for low-risk actions - ---- - -## 2. Confused First-Timer: "Jordan" - -**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out. - -**Behaviors**: -- Reads all instructions carefully -- Hesitates before clicking anything unfamiliar -- Looks for help or support constantly -- Misunderstands jargon and abbreviations -- Takes the most literal interpretation of any label - -**Test Questions**: -- Is the first action obviously clear within 5 seconds? -- Are all icons labeled with text? -- Is there contextual help at decision points? -- Does terminology assume prior knowledge? -- Is there a clear "back" or "undo" at every step? - -**Red Flags** (report these specifically): -- Icon-only navigation with no labels -- Technical jargon without explanation -- No visible help option or guidance -- Ambiguous next steps after completing an action -- No confirmation that an action succeeded - ---- - -## 3. Accessibility-Dependent User: "Sam" - -**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences. - -**Behaviors**: -- Tabs through the interface linearly -- Relies on ARIA labels and heading structure -- Cannot see hover states or visual-only indicators -- Needs adequate color contrast (4.5:1 minimum) -- May use browser zoom up to 200% - -**Test Questions**: -- Can the entire primary flow be completed keyboard-only? -- Are all interactive elements focusable with visible focus indicators? -- Do images have meaningful alt text? -- Is color contrast WCAG AA compliant (4.5:1 for text)? -- Does the screen reader announce state changes (loading, success, errors)? - -**Red Flags** (report these specifically): -- Click-only interactions with no keyboard alternative -- Missing or invisible focus indicators -- Meaning conveyed by color alone (red = error, green = success) -- Unlabeled form fields or buttons -- Time-limited actions without extension option -- Custom components that break screen reader flow - ---- - -## 4. Deliberate Stress Tester: "Riley" - -**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience. - -**Behaviors**: -- Tests edge cases intentionally (empty states, long strings, special characters) -- Submits forms with unexpected data (emoji, RTL text, very long values) -- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs -- Looks for inconsistencies between what the UI promises and what actually happens -- Documents problems methodically - -**Test Questions**: -- What happens at the edges (0 items, 1000 items, very long text)? -- Do error states recover gracefully or leave the UI in a broken state? -- What happens on refresh mid-workflow? Is state preserved? -- Are there features that appear to work but produce broken results? -- How does the UI handle unexpected input (emoji, special chars, paste from Excel)? - -**Red Flags** (report these specifically): -- Features that appear to work but silently fail or produce wrong results -- Error handling that exposes technical details or leaves UI in a broken state -- Empty states that show nothing useful ("No results" with no guidance) -- Workflows that lose user data on refresh or navigation -- Inconsistent behavior between similar interactions in different parts of the UI - ---- - -## 5. Distracted Mobile User: "Casey" - -**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection. - -**Behaviors**: -- Uses thumb only; prefers bottom-of-screen actions -- Gets interrupted mid-flow and returns later -- Switches between apps frequently -- Has limited attention span and low patience -- Types as little as possible, prefers taps and selections - -**Test Questions**: -- Are primary actions in the thumb zone (bottom half of screen)? -- Is state preserved if the user leaves and returns? -- Does it work on slow connections (3G)? -- Can forms use autocomplete and smart defaults? -- Are touch targets at least 44×44pt? - -**Red Flags** (report these specifically): -- Important actions positioned at the top of the screen (unreachable by thumb) -- No state persistence; progress lost on tab switch or interruption -- Large text inputs required where selection would work -- Heavy assets loading on every page (no lazy loading) -- Tiny tap targets or targets too close together - ---- - -## Selecting Personas - -Choose personas based on the interface type: - -| Interface Type | Primary Personas | Why | -|---------------|-----------------|-----| -| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile | -| Dashboard / admin | Alex, Sam | Power users, accessibility | -| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity | -| Onboarding flow | Jordan, Casey | Confusion, interruption | -| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav | -| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile | - ---- - -## Project-Specific Personas - -If `RULES.md` contains a `## Design Context` section (generated by `impeccable teach`), derive 1–2 additional personas from the audience and brand information: - -1. Read the target audience description -2. Identify the primary user archetype not covered by the 5 predefined personas -3. Create a persona following this template: - -``` -### [Role]: "[Name]" - -**Profile**: [2-3 key characteristics derived from Design Context] - -**Behaviors**: [3-4 specific behaviors based on the described audience] - -**Red Flags**: [3-4 things that would alienate this specific user type] -``` - -Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists. diff --git a/.trae-cn/skills/impeccable/reference/polish.md b/.trae-cn/skills/impeccable/reference/polish.md index 65564ecec..0b281eb6e 100644 --- a/.trae-cn/skills/impeccable/reference/polish.md +++ b/.trae-cn/skills/impeccable/reference/polish.md @@ -91,7 +91,6 @@ Visual polish on a misshapen flow is wasted work. Match the *shape* of the exper - **Theme consistency**: Works in all theme variants - **Color meaning**: Same colors mean same things throughout - **Accessible focus**: Focus indicators visible with sufficient contrast -- **Tinted neutrals**: No pure gray or pure black; add subtle color tint (0.01 chroma) - **Gray on color**: Never put gray text on colored backgrounds; use a shade of that color or transparency ### Interaction States diff --git a/.trae-cn/skills/impeccable/reference/product.md b/.trae-cn/skills/impeccable/reference/product.md index 64b3b0169..2ca462350 100644 --- a/.trae-cn/skills/impeccable/reference/product.md +++ b/.trae-cn/skills/impeccable/reference/product.md @@ -10,7 +10,6 @@ Product UI's failure mode isn't flatness, it's strangeness without purpose: over ## Typography -- **System fonts are legitimate.** `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif` gives you native feel on every platform. Inter is the common cross-platform default for a reason. - **One family is often right.** Product UIs don't need display/body pairing. A well-tuned sans carries headings, buttons, labels, body, data. - **Fixed rem scale, not fluid.** Clamp-sized headings don't serve product UI. Users view at consistent DPI, and a fluid h1 that shrinks in a sidebar looks worse, not better. - **Tighter scale ratio.** 1.125–1.2 between steps is typical. More type elements here than on brand surfaces; exaggerated contrast creates noise. @@ -26,8 +25,6 @@ Product defaults to Restrained. A single surface can earn Committed (a dashboard ## Layout -- Predictable grids. Consistency IS an affordance; users navigate faster when the structure is expected. -- Familiar patterns are features. Standard navigation (top bar, side nav), breadcrumbs, tabs, and form layouts have established user expectations. Don't reinvent for flavor. - Responsive behavior is structural (collapse sidebar, responsive table, breakpoint-driven columns), not fluid typography. ## Components @@ -51,6 +48,7 @@ Every interactive component has: default, hover, focus, active, disabled, loadin - Display fonts in UI labels, buttons, data. - Reinventing standard affordances for flavor (custom scrollbars, weird form controls, non-standard modals). - Heavy color or full-saturation accents on inactive states. +- Modal as first thought. Modals are usually laziness. Exhaust inline / progressive alternatives first. ## Product permissions diff --git a/.trae-cn/skills/impeccable/reference/responsive-design.md b/.trae-cn/skills/impeccable/reference/responsive-design.md deleted file mode 100644 index f079e5e5d..000000000 --- a/.trae-cn/skills/impeccable/reference/responsive-design.md +++ /dev/null @@ -1,114 +0,0 @@ -# Responsive Design - -## Mobile-First: Write It Right - -Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first. - -## Breakpoints: Content-Driven - -Don't chase device sizes; let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints. - -## Detect Input Method, Not Just Screen Size - -**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard. Use pointer and hover queries: - -```css -/* Fine pointer (mouse, trackpad) */ -@media (pointer: fine) { - .button { padding: 8px 16px; } -} - -/* Coarse pointer (touch, stylus) */ -@media (pointer: coarse) { - .button { padding: 12px 20px; } /* Larger touch target */ -} - -/* Device supports hover */ -@media (hover: hover) { - .card:hover { transform: translateY(-2px); } -} - -/* Device doesn't support hover (touch) */ -@media (hover: none) { - .card { /* No hover state - use active instead */ } -} -``` - -**Critical**: Don't rely on hover for functionality. Touch users can't hover. - -## Safe Areas: Handle the Notch - -Modern phones have notches, rounded corners, and home indicators. Use `env()`: - -```css -body { - padding-top: env(safe-area-inset-top); - padding-bottom: env(safe-area-inset-bottom); - padding-left: env(safe-area-inset-left); - padding-right: env(safe-area-inset-right); -} - -/* With fallback */ -.footer { - padding-bottom: max(1rem, env(safe-area-inset-bottom)); -} -``` - -**Enable viewport-fit** in your meta tag: -```html - -``` - -## Responsive Images: Get It Right - -### srcset with Width Descriptors - -```html -Hero image -``` - -**How it works**: -- `srcset` lists available images with their actual widths (`w` descriptors) -- `sizes` tells the browser how wide the image will display -- Browser picks the best file based on viewport width AND device pixel ratio - -### Picture Element for Art Direction - -When you need different crops/compositions (not just resolutions): - -```html - - - - ... - -``` - -## Layout Adaptation Patterns - -**Navigation**: Three stages: hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `
/` for content that can collapse on mobile. - -## Testing: Don't Trust DevTools Alone - -DevTools device emulation is useful for layout but misses: - -- Actual touch interactions -- Real CPU/memory constraints -- Network latency patterns -- Font rendering differences -- Browser chrome/keyboard appearances - -**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators. - ---- - -**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful. diff --git a/.trae-cn/skills/impeccable/reference/shape.md b/.trae-cn/skills/impeccable/reference/shape.md index f7c4cd2f1..592def8a7 100644 --- a/.trae-cn/skills/impeccable/reference/shape.md +++ b/.trae-cn/skills/impeccable/reference/shape.md @@ -151,7 +151,7 @@ How users interact with this feature. What happens on click, hover, scroll? What 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., spatial-design.md for complex layouts, motion-design.md for animated features, interaction-design.md for form-heavy features). +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. diff --git a/.trae-cn/skills/impeccable/reference/spatial-design.md b/.trae-cn/skills/impeccable/reference/spatial-design.md deleted file mode 100644 index 4d4b83afc..000000000 --- a/.trae-cn/skills/impeccable/reference/spatial-design.md +++ /dev/null @@ -1,100 +0,0 @@ -# Spatial Design - -## Spacing Systems - -### Use 4pt Base, Not 8pt - -8pt systems are too coarse; you'll frequently need 12px (between 8 and 16). Use 4pt for granularity: 4, 8, 12, 16, 24, 32, 48, 64, 96px. - -### Name Tokens Semantically - -Name by relationship (`--space-sm`, `--space-lg`), not value (`--spacing-8`). Use `gap` instead of margins for sibling spacing; it eliminates margin collapse and cleanup hacks. - -## Grid Systems - -### The Self-Adjusting Grid - -Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. Columns are at least 280px, as many as fit per row, leftovers stretch. For complex layouts, use named grid areas (`grid-template-areas`) and redefine them at breakpoints. - -## Visual Hierarchy - -### The Squint Test - -Blur your eyes (or screenshot and blur). Can you still identify: -- The most important element? -- The second most important? -- Clear groupings? - -If everything looks the same weight blurred, you have a hierarchy problem. - -### Hierarchy Through Multiple Dimensions - -Don't rely on size alone. Combine: - -| Tool | Strong Hierarchy | Weak Hierarchy | -|------|------------------|----------------| -| **Size** | 3:1 ratio or more | <2:1 ratio | -| **Weight** | Bold vs Regular | Medium vs Regular | -| **Color** | High contrast | Similar tones | -| **Position** | Top/left (primary) | Bottom/right | -| **Space** | Surrounded by white space | Crowded | - -**The best hierarchy uses 2-3 dimensions at once**: A heading that's larger, bolder, AND has more space above it. - -### Cards Are Not Required - -Cards are overused. Spacing and alignment create visual grouping naturally. Use cards only when content is truly distinct and actionable, items need visual comparison in a grid, or content needs clear interaction boundaries. **Never nest cards inside cards.** Use spacing, typography, and subtle dividers for hierarchy within a card. - -## Container Queries - -Viewport queries are for page layouts. **Container queries are for components**: - -```css -.card-container { - container-type: inline-size; -} - -.card { - display: grid; - gap: var(--space-md); -} - -/* Card layout changes based on its container, not viewport */ -@container (min-width: 400px) { - .card { - grid-template-columns: 120px 1fr; - } -} -``` - -**Why this matters**: A card in a narrow sidebar stays compact, while the same card in a main content area expands automatically, without viewport hacks. - -## Optical Adjustments - -Text at `margin-left: 0` looks indented due to letterform whitespace; use negative margin (`-0.05em`) to optically align. Geometrically centered icons often look off-center; play icons need to shift right, arrows shift toward their direction. - -### Touch Targets vs Visual Size - -Buttons can look small but need large touch targets (44px minimum). Use padding or pseudo-elements: - -```css -.icon-button { - width: 24px; /* Visual size */ - height: 24px; - position: relative; -} - -.icon-button::before { - content: ''; - position: absolute; - inset: -10px; /* Expand tap target to 44px */ -} -``` - -## Depth & Elevation - -Create semantic z-index scales (dropdown → sticky → modal-backdrop → modal → toast → tooltip) instead of arbitrary numbers. For shadows, create a consistent elevation scale (sm → md → lg → xl). **Key insight**: Shadows should be subtle. If you can clearly see it, it's probably too strong. - ---- - -**Avoid**: Arbitrary spacing values outside your scale. Making all spacing equal (variety creates hierarchy). Creating hierarchy through size alone - combine size, weight, color, and space. diff --git a/.trae-cn/skills/impeccable/reference/typeset.md b/.trae-cn/skills/impeccable/reference/typeset.md index dd3e67ea3..5f5e0c2c0 100644 --- a/.trae-cn/skills/impeccable/reference/typeset.md +++ b/.trae-cn/skills/impeccable/reference/typeset.md @@ -4,7 +4,7 @@ Typography carries most of the information on the page. Replace generic defaults ## Register -Brand: run the font selection procedure in [brand.md](brand.md). Pairing follows the brand's lane (display serif + sans body for editorial/luxury, one committed sans for tech, etc.). Fluid `clamp()` scale, ≥1.25 ratio between steps. +Brand: run the font selection procedure in [brand.md](brand.md). Fluid `clamp()` scale, ≥1.25 ratio between steps. Product: system fonts and familiar sans stacks are legitimate here. One well-tuned family typically carries the whole UI. Fixed `rem` scale, 1.125–1.2 ratio between more closely-spaced steps. @@ -43,7 +43,7 @@ Analyze what's weak or generic about the current type: ## Plan Typography Improvements -Consult the [typography reference](typography.md) for detailed guidance on scales, pairing, and loading strategies. +Consult the [Reference Material](#reference-material) section below for detailed guidance on scales, pairing, and loading strategies. Create a systematic plan: @@ -122,3 +122,158 @@ Each variant MUST declare a `scale` param controlling the hierarchy ratio. Expre Where the variant riffs on a specific pairing, expose the pairing choice as a `steps` param (e.g. "serif display + sans body" vs. "mono display + sans body" vs. "all-sans"). Each branch routes through `:scope[data-p-pairing="X"]` selectors in scoped CSS. See `reference/live.md` for the full params contract. + +--- + +## Reference Material + +The sections below were previously `typography.md` and live inline now so the typeset flow has its deep typography reference in one place. `bolder.md` also references this section. + +### Typography + +#### Classic Typography Principles + +##### Vertical Rhythm + +Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony; text and space share a mathematical foundation. + +##### Modular Scale & Hierarchy + +The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy. + +**Use fewer sizes with more contrast.** A 5-size system covers most needs: + +| Role | Typical Ratio | Use Case | +|------|---------------|----------| +| xs | 0.75rem | Captions, legal | +| sm | 0.875rem | Secondary UI, metadata | +| base | 1rem | Body text | +| lg | 1.25-1.5rem | Subheadings, lead text | +| xl+ | 2-4rem | Headlines, hero text | + +Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit. + +##### Readability & Measure + +Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length: narrow columns need tighter leading, wide columns need more. + +**Non-obvious**: Light text on dark backgrounds needs compensation on three axes, not just one. Bump line-height by 0.05–0.1, add a touch of letter-spacing (0.01–0.02em), and optionally step the body weight up one notch (regular → medium). The perceived weight drops across all three; fix all three. + +**Paragraph rhythm**: Pick either space between paragraphs OR first-line indentation. Never both. Digital usually wants space; editorial/long-form can justify indent-only. + +#### Font Selection & Pairing + +The tactical selection procedure and the reflex-reject list live in [reference/brand.md](brand.md) under **Font selection procedure** and **Reflex-reject list** (loaded for brand-register tasks). The rest of this section covers the adjacent knowledge: anti-reflex corrections, system font use, and pairing rules. + +##### Anti-reflexes worth defending against + +- A technical/utilitarian brief does NOT need a serif "for warmth." Most tech tools should look like tech tools. +- An editorial/premium brief does NOT need the same expressive serif everyone is using right now. Premium can be Swiss-modern, can be neo-grotesque, can be a literal monospace, can be a quiet humanist sans. +- A children's product does NOT need a rounded display font. Kids' books use real type. +- A "modern" brief does NOT need a geometric sans. The most modern thing you can do is not use the font everyone else is using. + +**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality. + +##### Pairing Principles + +**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif). + +When pairing, contrast on multiple axes: +- Serif + Sans (structure contrast) +- Geometric + Humanist (personality contrast) +- Condensed display + Wide body (proportion contrast) + +##### Web Font Loading + +The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix: + +```css +/* 1. Use font-display: swap for visibility */ +@font-face { + font-family: 'CustomFont'; + src: url('font.woff2') format('woff2'); + font-display: swap; +} + +/* 2. Match fallback metrics to minimize shift */ +@font-face { + font-family: 'CustomFont-Fallback'; + src: local('Arial'); + size-adjust: 105%; /* Scale to match x-height */ + ascent-override: 90%; /* Match ascender height */ + descent-override: 20%; /* Match descender depth */ + line-gap-override: 10%; /* Match line spacing */ +} + +body { + font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; +} +``` + +Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically. + +**`swap` vs `optional`**: `swap` shows fallback text immediately and FOUT-swaps when the web font arrives. `optional` uses the fallback if the web font misses a small load budget (~100ms) and avoids the shift entirely. Pick `optional` when zero layout shift matters more than seeing the branded font on slow networks. + +**Preload the critical weight only**: typically the regular-weight body font used above the fold. Preloading every weight costs more bandwidth than it saves. + +**Variable fonts for 3+ weights or styles**: a single variable font file is usually smaller than three static weight files, gives fractional weight control, and pairs well with `font-optical-sizing: auto`. For 1–2 weights, static is fine. + +#### Modern Web Typography + +##### Fluid Type + +Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate (higher vw = faster scaling). Add a rem offset so it doesn't collapse to 0 on small screens. + +**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes. + +**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI; fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it. + +**Bound your clamp()**: keep `max-size ≤ ~2.5 × min-size`. Wider ratios break the browser's zoom and reflow behaviour and make large viewports feel like the page is shouting. + +**Scale container width and font-size together** so effective character measure stays in the 45–75ch band at every viewport. A heading that widens faster than its container drifts out of the comfortable measure at the top end. + +##### OpenType Features + +Most developers don't know these exist. Use them for polish: + +```css +/* Proper fractions */ +.recipe-amount { font-variant-numeric: diagonal-fractions; } + +/* Small caps for abbreviations */ +abbr { font-variant-caps: all-small-caps; } + +/* Disable ligatures in code */ +code { font-variant-ligatures: none; } + +/* Enable kerning (usually on by default, but be explicit) */ +body { font-kerning: normal; } +``` + +Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/). + +##### Rendering polish + +```css +/* Variable fonts: pick the right optical-size master automatically */ +body { font-optical-sizing: auto; } +``` + +**ALL-CAPS tracking**: capitals sit too close at default spacing. Add 5–12% letter-spacing (`letter-spacing: 0.05em` to `0.12em`) to short all-caps labels, eyebrows, and small headings. Real small caps (via `font-variant-caps`) need the same treatment, slightly gentler. + +#### Typography System Architecture + +Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system. + +#### Accessibility Considerations + +Beyond contrast ratios (which are well-documented), consider: + +- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout. +- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text. +- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile. +- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets. + +--- + +**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text. diff --git a/.trae-cn/skills/impeccable/reference/typography.md b/.trae-cn/skills/impeccable/reference/typography.md deleted file mode 100644 index ed5ca272a..000000000 --- a/.trae-cn/skills/impeccable/reference/typography.md +++ /dev/null @@ -1,159 +0,0 @@ -# Typography - -## Classic Typography Principles - -### Vertical Rhythm - -Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony; text and space share a mathematical foundation. - -### Modular Scale & Hierarchy - -The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy. - -**Use fewer sizes with more contrast.** A 5-size system covers most needs: - -| Role | Typical Ratio | Use Case | -|------|---------------|----------| -| xs | 0.75rem | Captions, legal | -| sm | 0.875rem | Secondary UI, metadata | -| base | 1rem | Body text | -| lg | 1.25-1.5rem | Subheadings, lead text | -| xl+ | 2-4rem | Headlines, hero text | - -Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit. - -### Readability & Measure - -Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length: narrow columns need tighter leading, wide columns need more. - -**Non-obvious**: Light text on dark backgrounds needs compensation on three axes, not just one. Bump line-height by 0.05–0.1, add a touch of letter-spacing (0.01–0.02em), and optionally step the body weight up one notch (regular → medium). The perceived weight drops across all three; fix all three. - -**Paragraph rhythm**: Pick either space between paragraphs OR first-line indentation. Never both. Digital usually wants space; editorial/long-form can justify indent-only. - -## Font Selection & Pairing - -The tactical selection procedure and the reflex-reject list live in [reference/brand.md](brand.md) under **Font selection procedure** and **Reflex-reject list** (loaded for brand-register tasks). The rest of this section covers the adjacent knowledge: anti-reflex corrections, system font use, and pairing rules. - -### Anti-reflexes worth defending against - -- A technical/utilitarian brief does NOT need a serif "for warmth." Most tech tools should look like tech tools. -- An editorial/premium brief does NOT need the same expressive serif everyone is using right now. Premium can be Swiss-modern, can be neo-grotesque, can be a literal monospace, can be a quiet humanist sans. -- A children's product does NOT need a rounded display font. Kids' books use real type. -- A "modern" brief does NOT need a geometric sans. The most modern thing you can do is not use the font everyone else is using. - -**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality. - -### Pairing Principles - -**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif). - -When pairing, contrast on multiple axes: -- Serif + Sans (structure contrast) -- Geometric + Humanist (personality contrast) -- Condensed display + Wide body (proportion contrast) - -**Never pair fonts that are similar but not identical** (e.g., two geometric sans-serifs). They create visual tension without clear hierarchy. - -### Web Font Loading - -The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix: - -```css -/* 1. Use font-display: swap for visibility */ -@font-face { - font-family: 'CustomFont'; - src: url('font.woff2') format('woff2'); - font-display: swap; -} - -/* 2. Match fallback metrics to minimize shift */ -@font-face { - font-family: 'CustomFont-Fallback'; - src: local('Arial'); - size-adjust: 105%; /* Scale to match x-height */ - ascent-override: 90%; /* Match ascender height */ - descent-override: 20%; /* Match descender depth */ - line-gap-override: 10%; /* Match line spacing */ -} - -body { - font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; -} -``` - -Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically. - -**`swap` vs `optional`**: `swap` shows fallback text immediately and FOUT-swaps when the web font arrives. `optional` uses the fallback if the web font misses a small load budget (~100ms) and avoids the shift entirely. Pick `optional` when zero layout shift matters more than seeing the branded font on slow networks. - -**Preload the critical weight only**: typically the regular-weight body font used above the fold. Preloading every weight costs more bandwidth than it saves. - -**Variable fonts for 3+ weights or styles**: a single variable font file is usually smaller than three static weight files, gives fractional weight control, and pairs well with `font-optical-sizing: auto`. For 1–2 weights, static is fine. - -## Modern Web Typography - -### Fluid Type - -Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate (higher vw = faster scaling). Add a rem offset so it doesn't collapse to 0 on small screens. - -**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes. - -**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI; fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it. - -**Bound your clamp()**: keep `max-size ≤ ~2.5 × min-size`. Wider ratios break the browser's zoom and reflow behaviour and make large viewports feel like the page is shouting. - -**Scale container width and font-size together** so effective character measure stays in the 45–75ch band at every viewport. A heading that widens faster than its container drifts out of the comfortable measure at the top end. - -### OpenType Features - -Most developers don't know these exist. Use them for polish: - -```css -/* Tabular numbers for data alignment */ -.data-table { font-variant-numeric: tabular-nums; } - -/* Proper fractions */ -.recipe-amount { font-variant-numeric: diagonal-fractions; } - -/* Small caps for abbreviations */ -abbr { font-variant-caps: all-small-caps; } - -/* Disable ligatures in code */ -code { font-variant-ligatures: none; } - -/* Enable kerning (usually on by default, but be explicit) */ -body { font-kerning: normal; } -``` - -Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/). - -### Rendering polish - -```css -/* Even out heading line lengths (browser picks better break points) */ -h1, h2, h3 { text-wrap: balance; } - -/* Reduce orphans and ragged endings in long prose */ -article p { text-wrap: pretty; } - -/* Variable fonts: pick the right optical-size master automatically */ -body { font-optical-sizing: auto; } -``` - -**ALL-CAPS tracking**: capitals sit too close at default spacing. Add 5–12% letter-spacing (`letter-spacing: 0.05em` to `0.12em`) to short all-caps labels, eyebrows, and small headings. Real small caps (via `font-variant-caps`) need the same treatment, slightly gentler. - -## Typography System Architecture - -Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system. - -## Accessibility Considerations - -Beyond contrast ratios (which are well-documented), consider: - -- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout. -- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text. -- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile. -- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets. - ---- - -**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text. diff --git a/.trae-cn/skills/impeccable/reference/ux-writing.md b/.trae-cn/skills/impeccable/reference/ux-writing.md deleted file mode 100644 index 417b5f56c..000000000 --- a/.trae-cn/skills/impeccable/reference/ux-writing.md +++ /dev/null @@ -1,107 +0,0 @@ -# UX Writing - -## The Button Label Problem - -**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns: - -| Bad | Good | Why | -|-----|------|-----| -| OK | Save changes | Says what will happen | -| Submit | Create account | Outcome-focused | -| Yes | Delete message | Confirms the action | -| Cancel | Keep editing | Clarifies what "cancel" means | -| Click here | Download PDF | Describes the destination | - -**For destructive actions**, name the destruction: -- "Delete" not "Remove" (delete is permanent, remove implies recoverable) -- "Delete 5 items" not "Delete selected" (show the count) - -## Error Messages: The Formula - -Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input". - -### Error Message Templates - -| Situation | Template | -|-----------|----------| -| **Format error** | "[Field] needs to be [format]. Example: [example]" | -| **Missing required** | "Please enter [what's missing]" | -| **Permission denied** | "You don't have access to [thing]. [What to do instead]" | -| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." | -| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" | - -### Don't Blame the User - -Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date". - -## Empty States Are Opportunities - -Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items". - -## Voice vs Tone - -**Voice** is your brand's personality, consistent everywhere. -**Tone** adapts to the moment. - -| Moment | Tone Shift | -|--------|------------| -| Success | Celebratory, brief: "Done! Your changes are live." | -| Error | Empathetic, helpful: "That didn't work. Here's what to try..." | -| Loading | Reassuring: "Saving your work..." | -| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | - -**Never use humor for errors.** Users are already frustrated. Be helpful, not cute. - -## Writing for Accessibility - -**Link text** must have standalone meaning: "View pricing plans" not "Click here". **Alt text** describes information, not the image: "Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context. - -## Writing for Translation - -### Plan for Expansion - -German text is ~30% longer than English. Allocate space: - -| Language | Expansion | -|----------|-----------| -| German | +30% | -| French | +20% | -| Finnish | +30-40% | -| Chinese | -30% (fewer chars, but same width) | - -### Translation-Friendly Patterns - -Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear. - -## Consistency: The Terminology Problem - -Pick one term and stick with it: - -| Inconsistent | Consistent | -|--------------|------------| -| Delete / Remove / Trash | Delete | -| Settings / Preferences / Options | Settings | -| Sign in / Log in / Enter | Sign in | -| Create / Add / New | Create | - -Build a terminology glossary and enforce it. Variety creates confusion. - -## Avoid Redundant Copy - -If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well. - -## Loading States - -Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress. - -## Confirmation Dialogs: Use Sparingly - -Most confirmation dialogs are design failures; consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No"). - -## Form Instructions - -Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking. - ---- - -**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors. diff --git a/.trae-cn/skills/impeccable/scripts/cleanup-deprecated.mjs b/.trae-cn/skills/impeccable/scripts/cleanup-deprecated.mjs index 6aee4710a..bc6400e90 100644 --- a/.trae-cn/skills/impeccable/scripts/cleanup-deprecated.mjs +++ b/.trae-cn/skills/impeccable/scripts/cleanup-deprecated.mjs @@ -25,7 +25,7 @@ import { join, resolve } from 'node:path'; const DEPRECATED_NAMES = [ // v2.0 renames 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable teach + 'teach-impeccable', // folded into /impeccable init // v2.1 merges 'arrange', // renamed to layout 'normalize', // merged into polish diff --git a/.trae-cn/skills/impeccable/scripts/command-metadata.json b/.trae-cn/skills/impeccable/scripts/command-metadata.json index 1488bd5f1..83796d54b 100644 --- a/.trae-cn/skills/impeccable/scripts/command-metadata.json +++ b/.trae-cn/skills/impeccable/scripts/command-metadata.json @@ -3,8 +3,8 @@ "description": "Full confirmed-brief-then-build flow. Runs multi-round shape discovery first, resolves visual probe and north-star mock gates when available, then builds and visually iterates. Use when building a new feature end-to-end.", "argumentHint": "[feature description]" }, - "teach": { - "description": "Gathers design context for a project. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.", + "init": { + "description": "Sets up a project for impeccable. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles); offers DESIGN.md (visual: colors, typography, components) when code exists; pre-configures live mode; then recommends the best commands to run next. Every other command reads these files before doing work. Use once per project.", "argumentHint": "" }, "document": { diff --git a/.trae-cn/skills/impeccable/scripts/context.mjs b/.trae-cn/skills/impeccable/scripts/context.mjs new file mode 100644 index 000000000..52263c883 --- /dev/null +++ b/.trae-cn/skills/impeccable/scripts/context.mjs @@ -0,0 +1,253 @@ +/** + * Context loader: prints PRODUCT.md (and DESIGN.md if present) as one + * markdown block on stdout, or exits with empty stdout when no PRODUCT.md + * is found anywhere. The skill keys off "empty stdout" to branch into the + * init flow. + * + * Path resolution (first match wins): + * 1. cwd, if PRODUCT.md or DESIGN.md is there + * 2. .agents/context/ then docs/ + * 3. $IMPECCABLE_CONTEXT_DIR (absolute or cwd-relative) — power-user + * escape hatch, only consulted when defaults are empty + * 4. cwd as a "nothing found" default + * + * `resolveContextDir()` and `loadContext()` are also exported for the + * server-side scripts (live.mjs, live-server.mjs) that need the structured + * shape rather than the markdown block. + */ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; +const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; +const FALLBACK_DIRS = ['.agents/context', 'docs']; + +// ─── Update check ────────────────────────────────────────────────────────── +// Piggyback a lightweight skill-version check on the once-per-session boot. +// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent +// can offer `npx impeccable skills update`. Everything here is best-effort and +// silent on failure: a network problem, sandbox, or missing cache must never +// block context output or print an error. + +const UPDATE_HOST = (process.env.IMPECCABLE_UPDATE_HOST || 'https://impeccable.style').replace(/\/$/, ''); +const UPDATE_CACHE_PATH = + process.env.IMPECCABLE_UPDATE_CACHE || path.join(os.homedir(), '.impeccable', 'update-check.json'); +const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // throttle the network poll to once a day +const RENOTIFY_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000; // don't re-surface the same version for a week +const FETCH_TIMEOUT_MS = 1200; + +export function resolveContextDir(cwd = process.cwd()) { + if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { + return cwd; + } + for (const rel of FALLBACK_DIRS) { + const candidate = path.resolve(cwd, rel); + if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { + return candidate; + } + } + const envDir = process.env.IMPECCABLE_CONTEXT_DIR; + if (envDir && envDir.trim()) { + const trimmed = envDir.trim(); + return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); + } + return cwd; +} + +export function loadContext(cwd = process.cwd()) { + const contextDir = resolveContextDir(cwd); + const productPath = firstExisting(contextDir, PRODUCT_NAMES); + const designPath = firstExisting(contextDir, DESIGN_NAMES); + const product = productPath ? safeRead(productPath) : null; + const design = designPath ? safeRead(designPath) : null; + return { + hasProduct: !!product, + product, + productPath: productPath ? path.relative(cwd, productPath) : null, + hasDesign: !!design, + design, + designPath: designPath ? path.relative(cwd, designPath) : null, + contextDir, + }; +} + +function firstExisting(dir, names) { + for (const name of names) { + const abs = path.join(dir, name); + if (fs.existsSync(abs)) return abs; + } + return null; +} + +function safeRead(p) { + try { + return fs.readFileSync(p, 'utf-8'); + } catch { + return null; + } +} + +/** + * Pull the register (`brand` or `product`) out of PRODUCT.md by looking + * for a `## Register` section and reading the first non-empty line that + * follows it. Returns null when the file is legacy / register-less. + */ +function extractRegister(product) { + if (!product) return null; + const lines = product.split('\n'); + for (let i = 0; i < lines.length; i++) { + if (/^##\s+Register\b/i.test(lines[i].trim())) { + for (let j = i + 1; j < lines.length; j++) { + const next = lines[j].trim(); + if (!next) continue; + const word = next.toLowerCase(); + if (word === 'brand' || word === 'product') return word; + return null; + } + } + } + return null; +} + +/** + * Read the installed skill's own version from the sibling SKILL.md frontmatter + * (this file lives at `/scripts/context.mjs`). Returns null when the + * frontmatter is missing or unreadable. + */ +function readLocalSkillVersion() { + try { + const here = path.dirname(fileURLToPath(import.meta.url)); + const skillMd = path.join(here, '..', 'SKILL.md'); + const content = fs.readFileSync(skillMd, 'utf-8'); + const match = content.match(/^version:\s*(.+)$/m); + return match ? match[1].trim().replace(/^["']|["']$/g, '') : null; + } catch { + return null; + } +} + +function readUpdateCache() { + try { + return JSON.parse(fs.readFileSync(UPDATE_CACHE_PATH, 'utf-8')); + } catch { + return {}; + } +} + +function writeUpdateCache(cache) { + try { + fs.mkdirSync(path.dirname(UPDATE_CACHE_PATH), { recursive: true }); + fs.writeFileSync(UPDATE_CACHE_PATH, JSON.stringify(cache)); + } catch { + // Best-effort: a read-only home dir just means we re-poll next session. + } +} + +/** Compare dotted numeric versions. Returns >0 when a is newer than b. */ +function compareSemver(a, b) { + const pa = String(a).split('.').map(n => parseInt(n, 10) || 0); + const pb = String(b).split('.').map(n => parseInt(n, 10) || 0); + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + const diff = (pa[i] || 0) - (pb[i] || 0); + if (diff !== 0) return diff; + } + return 0; +} + +async function fetchLatestSkillVersion() { + try { + const res = await fetch(`${UPDATE_HOST}/api/version`, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }); + if (!res.ok) return null; + const data = await res.json(); + return typeof data?.skills === 'string' ? data.skills : null; + } catch { + return null; // offline, sandboxed, timed out, or bad JSON: all non-fatal + } +} + +function buildUpdateDirective(localVersion, latestVersion) { + return ( + `UPDATE_AVAILABLE: A newer Impeccable skill is available ` + + `(installed v${localVersion}, latest v${latestVersion}). ` + + `Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` + + `Update now? It runs \`npx impeccable skills update\`." ` + + `If they agree, run \`npx impeccable skills update\` (the update applies to the next session, not this one). ` + + `Either way, continue the current task without waiting, and do not raise this again.` + ); +} + +/** + * Best-effort update directive for the boot output. Returns a string to append + * or null. Polls the version endpoint at most once per day (cached globally in + * the user's home dir) and re-surfaces a given version at most once per week so + * the agent never nags. Opt out entirely with IMPECCABLE_NO_UPDATE_CHECK=1. + */ +async function computeUpdateDirective(now = Date.now()) { + try { + if (process.env.IMPECCABLE_NO_UPDATE_CHECK) return null; + const localVersion = readLocalSkillVersion(); + if (!localVersion) return null; + + const cache = readUpdateCache(); + + // Poll the network only when the throttle window has elapsed. Stamp + // lastCheck even on failure so an offline machine doesn't poll every boot. + if (!cache.lastCheck || now - cache.lastCheck > CHECK_INTERVAL_MS) { + const latest = await fetchLatestSkillVersion(); + cache.lastCheck = now; + if (latest) cache.latestVersion = latest; + writeUpdateCache(cache); + } + + const latest = cache.latestVersion; + if (!latest || compareSemver(latest, localVersion) <= 0) return null; + + // Anti-nag: surface a given version at most once per RENOTIFY window. + if (cache.notifiedVersion === latest && cache.notifiedAt && now - cache.notifiedAt < RENOTIFY_INTERVAL_MS) { + return null; + } + cache.notifiedVersion = latest; + cache.notifiedAt = now; + writeUpdateCache(cache); + + return buildUpdateDirective(localVersion, latest); + } catch { + return null; + } +} + +async function cli() { + const ctx = loadContext(process.cwd()); + const updateDirective = await computeUpdateDirective(); + + if (!ctx.hasProduct) { + // Direct stdout message instead of relying on empty output as a signal + // — cheap models miss the empty case more often than the explicit one. + const parts = [ + 'NO_PRODUCT_MD: This project has no PRODUCT.md yet. ' + + 'Stop the current task, load reference/init.md, and follow its ' + + 'instructions to write PRODUCT.md before resuming.', + ]; + if (updateDirective) parts.push(updateDirective); + process.stdout.write(parts.join('\n\n---\n\n') + '\n'); + process.exit(0); + } + const parts = [`# PRODUCT.md\n\n${ctx.product.trim()}`]; + if (ctx.hasDesign) { + parts.push(`# DESIGN.md\n\n${ctx.design.trim()}`); + } + const register = extractRegister(ctx.product); + const next = register + ? `NEXT STEP: This project's register is \`${register}\`. You MUST now read \`reference/${register}.md\` before producing any design output.` + : `NEXT STEP: You MUST now read the matching register reference (\`reference/brand.md\` or \`reference/product.md\`) before producing any design output. Pick based on PRODUCT.md above.`; + parts.push(next); + if (updateDirective) parts.push(updateDirective); + process.stdout.write(parts.join('\n\n---\n\n') + '\n'); +} + +const _running = process.argv[1]; +if (_running?.endsWith('context.mjs') || _running?.endsWith('context.mjs/')) { + cli(); +} diff --git a/.trae-cn/skills/impeccable/scripts/design-parser.mjs b/.trae-cn/skills/impeccable/scripts/design-parser.mjs index b68756640..82a5cd7dc 100644 --- a/.trae-cn/skills/impeccable/scripts/design-parser.mjs +++ b/.trae-cn/skills/impeccable/scripts/design-parser.mjs @@ -63,7 +63,7 @@ function parseYamlSubset(yaml) { } const key = content.slice(0, colonIdx).trim(); - const rest = content.slice(colonIdx + 1).trim(); + const rest = stripInlineYamlComment(content.slice(colonIdx + 1).trim()); const parent = stack[stack.length - 1].obj; if (rest === '') { @@ -93,6 +93,21 @@ function findTopLevelColon(s) { return -1; } +function stripInlineYamlComment(s) { + let inQuote = null; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (inQuote) { + if (ch === inQuote && s[i - 1] !== '\\') inQuote = null; + } else if (ch === '"' || ch === "'") { + inQuote = ch; + } else if (ch === '#' && i > 0 && /\s/.test(s[i - 1])) { + return s.slice(0, i).trimEnd(); + } + } + return s; +} + function parseScalar(raw) { const s = raw.trim(); if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { diff --git a/.trae-cn/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.trae-cn/skills/impeccable/scripts/detector/browser/injected/index.mjs index c03a69caf..ceff16737 100644 --- a/.trae-cn/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/.trae-cn/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -9,8 +9,21 @@ if (IS_BROWSER) { const EXTENSION_MODE = (_myScript && _myScript.dataset.impeccableExtension === 'true') || document.documentElement.dataset.impeccableExtension === 'true'; - const BRAND_COLOR = 'oklch(55% 0.25 350)'; - const BRAND_COLOR_HOVER = 'oklch(45% 0.25 350)'; + // Kinpaku gold — pinned to the site's brand token (see + // site/styles/kinpaku-tokens.css --ks-kinpaku). Keep this in sync with + // the picker's C.brand in skill/scripts/live-browser.js and the kit's + // picker section in site/styles/kinpaku-kit.css. + // + // One color across both light and dark host pages. The outline is a + // 2px gesture pointing at an element + a labeled tag — it's a marker, + // not body text, so it doesn't need WCAG AA against the page. The + // label text inside the gold tag is dark (LABEL_INK) which has ~16:1 + // against the leaf gold, so reading the rule name is solid in both + // modes. Hover deepens the gold (preserves chroma — never drops it, + // dropping chroma washes the gold into a sand/olive tone). + const BRAND_COLOR = 'oklch(84% 0.19 80.46)'; + const BRAND_COLOR_HOVER = 'oklch(74% 0.18 80)'; + const LABEL_INK = 'oklch(4% 0.004 95)'; const LABEL_BG = BRAND_COLOR; const OUTLINE_COLOR = BRAND_COLOR; @@ -278,7 +291,7 @@ if (IS_BROWSER) { display: 'flex', alignItems: 'center', whiteSpace: 'nowrap', fontSize: '11px', fontWeight: '600', letterSpacing: '0.02em', - color: 'white', lineHeight: '14px', + color: LABEL_INK, lineHeight: '14px', background: LABEL_BG, fontFamily: 'system-ui, sans-serif', borderRadius: '4px 4px 0 0', @@ -398,7 +411,7 @@ if (IS_BROWSER) { banner.className = 'impeccable-overlay impeccable-banner'; Object.assign(banner.style, { position: 'fixed', top: '0', left: '0', right: '0', zIndex: '100000', - background: LABEL_BG, color: 'white', + background: LABEL_BG, color: LABEL_INK, fontFamily: 'system-ui, sans-serif', fontSize: '13px', display: 'flex', alignItems: 'center', pointerEvents: 'auto', height: '36px', overflow: 'hidden', maxWidth: '100vw', @@ -1223,12 +1236,12 @@ if (IS_BROWSER) { } console.group( `%c[impeccable] ${allFindings.length} anti-pattern${allFindings.length === 1 ? '' : 's'} found`, - 'color: oklch(60% 0.25 350); font-weight: bold' + 'color: oklch(84% 0.19 80.46); font-weight: bold' ); for (const { el, findings } of allFindings) { for (const f of findings) { console.log(`%c${f.type || f.id}%c ${f.detail || f.snippet}`, - 'color: oklch(55% 0.25 350); font-weight: bold', 'color: inherit', el); + 'color: oklch(84% 0.19 80.46); font-weight: bold', 'color: inherit', el); } } console.groupEnd(); @@ -1249,6 +1262,10 @@ if (IS_BROWSER) { const groupMap = new Map(); const _disabled = EXTENSION_MODE ? (window.__IMPECCABLE_CONFIG__?.disabledRules || []) : []; const _ruleOk = (id) => !_disabled.length || !_disabled.includes(id); + // Note: provider-gated rules (--gpt / --gemini) are NOT filtered here. In a + // real browser env (detector page, live overlay, extension) running every + // check is free, so we always surface them; the gating is purely a CLI + // output concern, applied in the Node engines' detect* return paths. for (const el of document.querySelectorAll('*')) { // Skip impeccable's own elements and any descendants (overlays, labels, banner, nav buttons) @@ -1270,11 +1287,23 @@ if (IS_BROWSER) { ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), - ...checkElementHeroEyebrowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementOversizedH1DOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementClippedOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementGptBorderShadowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementTextOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ].filter(f => _ruleOk(f.type)); addBrowserFindings(groupMap, el, findings); + + // Hero eyebrow: the offending element is the eyebrow above the heading, + // not the heading itself — highlight the previous sibling instead. + const eyebrowFindings = checkElementHeroEyebrowDOM(el) + .map(f => ({ type: f.id, detail: f.snippet })) + .filter(f => _ruleOk(f.type)); + if (eyebrowFindings.length > 0 && el.previousElementSibling) { + addBrowserFindings(groupMap, el.previousElementSibling, eyebrowFindings); + } } const pageLevelFindings = []; @@ -1306,6 +1335,14 @@ if (IS_BROWSER) { addBrowserFindings(groupMap, document.body, qualityFindings); } + const creamFindings = checkCreamPalette(document) + .map(f => ({ type: f.id, detail: f.snippet })) + .filter(f => _ruleOk(f.type)); + if (creamFindings.length > 0) { + pageLevelFindings.push(...creamFindings); + addBrowserFindings(groupMap, document.body, creamFindings); + } + // Regex-on-HTML checks (shared with Node) // Clone the document and strip impeccable-live overlay nodes before the // regex scan, so the inspector's own inline styles (transitions on top/ diff --git a/.trae-cn/skills/impeccable/scripts/detector/cli/main.mjs b/.trae-cn/skills/impeccable/scripts/detector/cli/main.mjs index 76da09f0c..94283e01b 100644 --- a/.trae-cn/skills/impeccable/scripts/detector/cli/main.mjs +++ b/.trae-cn/skills/impeccable/scripts/detector/cli/main.mjs @@ -41,7 +41,7 @@ function formatFindings(findings, jsonMode) { // Stdin handling // --------------------------------------------------------------------------- -async function handleStdin() { +async function handleStdin(options = {}) { const chunks = []; for await (const chunk of process.stdin) chunks.push(chunk); const input = Buffer.concat(chunks).toString('utf-8'); @@ -50,10 +50,10 @@ async function handleStdin() { const fp = parsed?.tool_input?.file_path; if (fp && fs.existsSync(fp)) { return HTML_EXTENSIONS.has(path.extname(fp).toLowerCase()) - ? detectHtml(fp) : detectText(fs.readFileSync(fp, 'utf-8'), fp); + ? detectHtml(fp, options) : detectText(fs.readFileSync(fp, 'utf-8'), fp, options); } } catch { /* not JSON */ } - return detectText(input, ''); + return detectText(input, '', options); } @@ -81,6 +81,8 @@ Scan files or URLs for UI anti-patterns and design quality issues. Options: --fast Regex-only mode (skip static HTML/CSS analysis, faster but misses linked stylesheets) --json Output results as JSON + --gpt Also report GPT-specific provider tells (off by default) + --gemini Also report Gemini-specific provider tells (off by default) --help Show this help message Detection modes: @@ -106,6 +108,10 @@ async function detectCli() { const jsonMode = args.includes('--json'); const helpMode = args.includes('--help'); const fastMode = args.includes('--fast'); + const providers = []; + if (args.includes('--gpt')) providers.push('gpt'); + if (args.includes('--gemini')) providers.push('gemini'); + const scanOptions = { providers }; const targets = args.filter(a => !a.startsWith('--')); if (helpMode) { printUsage(); process.exit(0); } @@ -113,7 +119,7 @@ async function detectCli() { let allFindings = []; if (!process.stdin.isTTY && targets.length === 0) { - allFindings = await handleStdin(); + allFindings = await handleStdin(scanOptions); } else { const paths = targets.length > 0 ? targets : [process.cwd()]; const urlTargetCount = paths.filter(target => /^https?:\/\//i.test(target)).length; @@ -124,8 +130,8 @@ async function detectCli() { if (/^https?:\/\//i.test(target)) { try { const scanner = browserDetector - ? (url) => browserDetector.detectUrl(url) - : (url) => detectUrl(url); + ? (url) => browserDetector.detectUrl(url, scanOptions) + : (url) => detectUrl(url, scanOptions); allFindings.push(...await scanner(target)); } catch (e) { process.stderr.write(`Error: ${e.message}\n`); } continue; @@ -192,9 +198,9 @@ async function detectCli() { const ext = path.extname(file).toLowerCase(); let fileFindings; if (!fastMode && HTML_EXTENSIONS.has(ext)) { - fileFindings = await detectHtml(file); + fileFindings = await detectHtml(file, scanOptions); } else { - fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file); + fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file, scanOptions); } // Annotate findings with import context const importers = importedByMap.get(file); @@ -209,9 +215,9 @@ async function detectCli() { } else if (stat.isFile()) { const ext = path.extname(resolved).toLowerCase(); if (!fastMode && HTML_EXTENSIONS.has(ext)) { - allFindings.push(...await detectHtml(resolved)); + allFindings.push(...await detectHtml(resolved, scanOptions)); } else { - allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved)); + allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved, scanOptions)); } } } diff --git a/.trae-cn/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.trae-cn/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 1afbe3f12..88439a5c3 100644 --- a/.trae-cn/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.trae-cn/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -38,7 +38,7 @@ const OVERUSED_FONTS = new Set([ // Older monoculture (still ubiquitous): 'inter', 'roboto', 'open sans', 'lato', 'montserrat', 'arial', 'helvetica', // Newer monoculture (the Anthropic-skill / Vercel / GitHub default wave): - 'fraunces', 'instrument sans', + 'fraunces', 'instrument sans', 'instrument serif', 'geist', 'geist sans', 'geist mono', 'mona sans', 'plus jakarta sans', 'space grotesk', 'recoleta', @@ -165,6 +165,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'AI color palette', }, + { + id: 'cream-palette', + category: 'slop', + name: 'Cream / beige palette', + description: + 'A warm cream or beige page background has become the default "tasteful" AI surface, reached for by reflex. Choose a background that comes from a deliberate palette, not the safe warm off-white.', + skillSection: 'Color & Contrast', + skillGuideline: 'cream and beige as the default surface', + }, { id: 'nested-cards', category: 'slop', @@ -183,15 +192,6 @@ const ANTIPATTERNS = [ skillSection: 'Layout & Space', skillGuideline: 'same spacing everywhere', }, - { - id: 'everything-centered', - category: 'slop', - name: 'Everything centered', - description: - 'Every text element is center-aligned. Left-aligned text with asymmetric layouts feels more designed. Center only hero sections and CTAs.', - skillSection: 'Layout & Space', - skillGuideline: 'Center everything', - }, { id: 'bounce-easing', category: 'slop', @@ -247,17 +247,72 @@ const ANTIPATTERNS = [ skillSection: 'Typography', skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding', }, + { + id: 'numbered-section-markers', + category: 'slop', + severity: 'advisory', + name: 'Numbered section markers (01 / 02 / 03)', + description: + 'Numbered display markers as section labels (01, 02, 03) are the AI editorial scaffold one tier deeper than tracked eyebrow chips. If you find yourself reaching for them, choose a different section cadence.', + skillSection: 'Layout & Space', + skillGuideline: 'numbered section markers', + }, + { + id: 'em-dash-overuse', + category: 'slop', + name: 'Em-dash overuse', + description: + 'More than two em-dashes (— or --) in body copy is an AI cadence tell. Use commas, colons, periods, or parentheses instead.', + skillSection: 'Copy', + skillGuideline: 'no em dashes', + }, + { + id: 'marketing-buzzword', + category: 'slop', + name: 'Marketing buzzword', + description: + 'Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does.', + skillSection: 'Copy', + skillGuideline: 'marketing buzzwords', + }, + { + id: 'aphoristic-cadence', + category: 'slop', + name: 'Aphoristic-cadence copy', + description: + 'Three or more sections landing on a short rebuttal sentence ("X. No Y." / "X. Just Y.") or a manufactured-contrast aphorism ("Not a feature. A platform.") reads as AI cadence, not voice. Once is fine; the pattern is the tell.', + skillSection: 'Copy', + skillGuideline: 'aphoristic cadence', + }, + { + id: 'oversized-h1', + category: 'slop', + name: 'Oversized hero headline', + description: + 'A full-sentence headline set at display size ends up dominating the viewport, leaving no room for anything else above the fold. A punchy one- or two-word headline at that size is fine — the problem is a long headline blown up too large. Set long headlines smaller, or tighten the copy.', + skillSection: 'Typography', + skillGuideline: 'long headline set at display size', + }, + { + id: 'extreme-negative-tracking', + category: 'slop', + name: 'Crushed letter spacing', + description: + 'Letter-spacing pulled tighter than the point where characters keep their own shapes costs legibility. Tighten display type optically, not destructively.', + skillSection: 'Typography', + skillGuideline: 'letter spacing crushed past legibility', + }, + { + id: 'broken-image', + category: 'quality', + name: 'Broken or placeholder image', + description: + ' tags with empty src, missing src, or placeholder values ship as broken-image boxes. Use real images, generated assets, or remove the tag.', + skillSection: 'Imagery', + skillGuideline: 'broken image references', + }, // ── Quality: general design and accessibility issues ── - { - id: 'pure-black-white', - category: 'quality', - name: 'Pure black background', - description: - 'Pure #000000 as a background color looks harsh and unnatural. Tint it slightly toward your brand hue (e.g., oklch(12% 0.01 250)) for a more refined feel.', - skillSection: 'Color & Contrast', - skillGuideline: 'pure black (#000)', - }, { id: 'gray-on-color', category: 'quality', @@ -297,7 +352,9 @@ const ANTIPATTERNS = [ category: 'quality', name: 'Cramped padding', description: - 'Text is too close to the edge of its container. Add at least 8px (ideally 12-16px) of padding inside bordered or colored containers.', + 'Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.', + skillSection: 'Layout & Space', + skillGuideline: 'inside bordered or colored containers', }, { id: 'body-text-viewport-edge', @@ -350,6 +407,70 @@ const ANTIPATTERNS = [ description: 'Letter spacing above 0.05em on body text disrupts natural character groupings and slows reading. Reserve wide tracking for short uppercase labels only.', }, + { + id: 'text-overflow', + category: 'quality', + name: 'Content overflowing its container', + description: + 'Content renders wider than its container, spilling out or forcing a horizontal scrollbar. Let text wrap, constrain widths, or give the region a deliberate scroll affordance.', + skillSection: 'Layout & Space', + skillGuideline: 'content wider than its container', + }, + { + id: 'clipped-overflow-container', + category: 'quality', + name: 'Positioned child clipped by overflow container', + description: + 'A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.', + skillSection: 'Layout & Space', + skillGuideline: 'overflow container clipping positioned children', + }, + + // ── Provider tells: opt-in via --gpt / --gemini (gated off by default) ── + { + id: 'gpt-thin-border-wide-shadow', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Hairline border with wide shadow', + description: + 'A hairline border paired with a wide, diffuse shadow is a recurring generated-UI signature. Commit to one — a defined edge or a soft elevation — rather than both at once.', + skillSection: 'Visual Details', + skillGuideline: 'hairline border plus wide diffuse shadow', + }, + { + id: 'repeating-stripes-gradient', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Repeating-gradient stripes', + description: + 'Repeating-gradient stripes used as surface decoration are a recurring generated-UI signature. Reach for a deliberate texture or leave the surface plain.', + skillSection: 'Visual Details', + skillGuideline: 'repeating-gradient decorative stripes', + }, + { + id: 'theater-slop-phrase', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Theater framing copy', + description: + 'Dismissing something as "theater" is a recurring generated-copy tic. Say plainly what the thing does or does not do.', + skillSection: 'Copy', + skillGuideline: 'theater framing copy', + }, + { + id: 'image-hover-transform', + category: 'slop', + severity: 'advisory', + gated: 'gemini', + name: 'Image hover transform', + description: + 'Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction.', + skillSection: 'Motion', + skillGuideline: 'image scale or rotate on hover', + }, ]; // --- cli/engine/shared/color.mjs --- @@ -527,11 +648,6 @@ function checkColors(opts) { } const findings = []; - // Pure black background (only solid or near-solid, not semi-transparent overlays) - if (bgColor && bgColor.a >= 0.9 && bgColor.r === 0 && bgColor.g === 0 && bgColor.b === 0) { - findings.push({ id: 'pure-black-white', snippet: '#000000 background' }); - } - if (hasDirectText && textColor && !isEmojiOnly) { // Run background-dependent checks against either a solid bg or, if the // ancestor is a gradient, against every gradient stop (use the worst case). @@ -587,9 +703,6 @@ function checkColors(opts) { // Tailwind class checks if (classList) { const classStr = typeof classList === 'string' ? classList : Array.from(classList).join(' '); - if (/\bbg-black\b(?!\/)/.test(classStr)) { - findings.push({ id: 'pure-black-white', snippet: 'bg-black' }); - } const grayMatch = classStr.match(/\btext-(?:gray|slate|zinc|neutral|stone)-\d+\b/); const colorBgMatch = classStr.match(/\bbg-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+\b/); @@ -905,12 +1018,6 @@ function checkHtmlPatterns(html) { // --- Color --- - // Pure black background - const pureBlackBgRe = /background(?:-color)?\s*:\s*(?:#000000|#000|rgb\(\s*0,\s*0,\s*0\s*\))\b/gi; - if (pureBlackBgRe.test(html)) { - findings.push({ id: 'pure-black-white', snippet: 'Pure #000 background' }); - } - // AI color palette: purple/violet const purpleHexRe = /#(?:7c3aed|8b5cf6|a855f7|9333ea|7e22ce|6d28d9|6366f1|764ba2|667eea)\b/gi; if (purpleHexRe.test(html)) { @@ -1027,6 +1134,39 @@ function checkHtmlPatterns(html) { } } + // --- Provider tells (gated): repeating-gradient stripes (GPT) --- + if (/repeating-(?:linear|radial|conic)-gradient\s*\(/i.test(html)) { + findings.push({ id: 'repeating-stripes-gradient', snippet: 'repeating-gradient decorative stripes' }); + } + + // --- Provider tells (gated): "X theater" framing copy (GPT) --- + // Lives here (regex-on-HTML) rather than in the text-content analyzers so it + // runs in the bundled browser path too, not just the CLI/static path. + { + const bodyText = html + .replace(/]*>[\s\S]*?<\/script>/gi, ' ') + .replace(/]*>[\s\S]*?<\/style>/gi, ' ') + .replace(/<[^>]+>/g, ' '); + const tm = /\b(\w+)\s+theater\b/i.exec(bodyText); + if (tm) findings.push({ id: 'theater-slop-phrase', snippet: `"${tm[0].trim()}"` }); + } + + // --- Provider tells (gated): image hover transform (Gemini) --- + // A CSS `img...:hover { transform: ... }` rule, or a Tailwind hover:scale / + // hover:rotate / hover:translate utility on an . Each distinct + // mechanism is its own finding. + const imgHoverCss = /\bimg\b[^,{}]*:hover\b[^{}]*\{[^}]*\btransform\s*:\s*(?:scale|rotate|translate|matrix|skew)/i; + if (imgHoverCss.test(html)) { + findings.push({ id: 'image-hover-transform', snippet: 'img:hover { transform } rule' }); + } + const imgTagRe = /]*\bclass\s*=\s*"([^"]*)"/gi; + let im; + while ((im = imgTagRe.exec(html)) !== null) { + if (/\bhover:(?:scale|rotate|translate|skew)-/.test(im[1])) { + findings.push({ id: 'image-hover-transform', snippet: 'Tailwind hover transform on ' }); + } + } + return findings; } @@ -1673,7 +1813,7 @@ function resolveLengthPx(value, fontSizePx) { // Both adapters resolve font-size, line-height and letter-spacing to pixels // before calling this so the pure function only deals with numbers. function checkQuality(opts) { - const { el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax = 80, viewportWidth = 0 } = opts; + const { el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax = 80, viewportWidth = 0, win = null } = opts; const findings = []; // Skip browser extension injected elements const elId = el.id || ''; @@ -1724,6 +1864,155 @@ function checkQuality(opts) { } } + // --- Flush against a visible boundary --- + // Fires when a container has a visible boundary (border, outline, OR a + // non-transparent background) AND near-zero padding on the bounded + // side(s) AND text-bearing children land flush against the boundary. + // + // Distinct from cramped-padding: that rule needs the element itself to + // have direct text (hasDirectText). This rule targets the OPPOSITE + // shape — a container with NO direct text, only children — which is + // exactly what cramped-padding misses (a section wrapping a label + + // list lands a free pass). + // + // The classic shape: agent writes `padding: 28px 0 0` shorthand on a + // section that also has a border, zeroing horizontal padding so the + // text-bearing children touch the side borders. Background and + // outline count too: a colored card with zero padding has the same + // visual failure mode. + { + const FLUSH_SKIP_TAGS = new Set(['HTML', 'BODY', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ARTICLE', 'ASIDE', 'BUTTON', 'A', 'LABEL', 'SUMMARY', 'CODE', 'PRE', 'INPUT', 'TEXTAREA', 'SELECT', 'FORM', 'FIGURE', 'TABLE', 'TBODY', 'THEAD', 'TR', 'TD', 'TH']); + const upperTag = tag ? tag.toUpperCase() : ''; + const elPosition = style.position || ''; + if ( + !FLUSH_SKIP_TAGS.has(upperTag) && + !hasDirectText && + !['fixed', 'absolute'].includes(elPosition) && + el.children && el.children.length > 0 + ) { + const isTransparent = (c) => + !c || c === 'transparent' || c === 'rgba(0, 0, 0, 0)' || + /^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*0(?:\.0+)?\s*\)$/.test(c); + + const borderW = { + top: parseFloat(style.borderTopWidth) || 0, + right: parseFloat(style.borderRightWidth) || 0, + bottom: parseFloat(style.borderBottomWidth) || 0, + left: parseFloat(style.borderLeftWidth) || 0, + }; + const borderVisible = { + top: borderW.top > 0 && !isTransparent(style.borderTopColor), + right: borderW.right > 0 && !isTransparent(style.borderRightColor), + bottom: borderW.bottom > 0 && !isTransparent(style.borderBottomColor), + left: borderW.left > 0 && !isTransparent(style.borderLeftColor), + }; + // Outline detection. jsdom decomposes `border` shorthand into + // border{Top,…}Width/Color but does NOT decompose `outline` — + // the longhands come back empty when the value was set via the + // shorthand. Fall back to parsing `style.outline` ourselves. + let outlineW = parseFloat(style.outlineWidth) || 0; + let outlineStyleVal = style.outlineStyle || ''; + let outlineColorVal = style.outlineColor || ''; + if (!outlineW && style.outline) { + const wMatch = style.outline.match(/(\d+(?:\.\d+)?)\s*px/); + if (wMatch) outlineW = parseFloat(wMatch[1]) || 0; + if (!outlineStyleVal) { + outlineStyleVal = /\b(solid|dashed|dotted|double|groove|ridge|inset|outset)\b/.test(style.outline) ? 'solid' : ''; + } + if (!outlineColorVal) { + const cMatch = style.outline.match(/(rgba?\([^)]+\)|#[0-9a-fA-F]{3,8}|[a-zA-Z]+)\s*$/); + if (cMatch) outlineColorVal = cMatch[1]; + } + } + const outlineVisible = outlineW > 0 && !isTransparent(outlineColorVal) && outlineStyleVal && outlineStyleVal !== 'none'; + const bgVisible = !isTransparent(style.backgroundColor); + + const anyVisible = borderVisible.top || borderVisible.right || borderVisible.bottom || borderVisible.left || outlineVisible || bgVisible; + if (anyVisible) { + // Resolve padding to px (jsdom returns raw "1.5rem" etc., not the + // computed px value; parseFloat would strip the unit and treat + // 1.5rem as 1.5px, false-flagging legitimate insets). + const pad = { + top: resolveLengthPx(style.paddingTop, fontSize) ?? 0, + right: resolveLengthPx(style.paddingRight, fontSize) ?? 0, + bottom: resolveLengthPx(style.paddingBottom, fontSize) ?? 0, + left: resolveLengthPx(style.paddingLeft, fontSize) ?? 0, + }; + const PAD_THRESHOLD = 2; + // Children-insulate-this-side: a side is insulated if ANY direct + // child has its own padding ≥ 4px on that side. Rationale: in + // typical flow, only the first/last (or leftmost/rightmost) + // children actually sit at the parent's edges. If even one of + // them has its own padding, the visual flush is broken on that + // side. Classic example: a column-flow card frame where the + // top child (header) has padding-top:12 and the bottom child + // (footer) has padding-bottom:8 — the parent's padding:0 doesn't + // matter; nothing is actually flush. The `any-child-insulates` + // heuristic accepts some false negatives (a card with one heavily + // padded middle child won't flag) for far fewer false positives. + const CHILD_INSULATE_THRESHOLD = 4; + const childrenInsulate = { top: false, right: false, bottom: false, left: false }; + for (const child of el.children) { + let childStyle = null; + if (win && typeof win.getComputedStyle === 'function') { + try { childStyle = win.getComputedStyle(child); } catch {} + } + if (!childStyle && typeof getComputedStyle === 'function') { + try { childStyle = getComputedStyle(child); } catch {} + } + if (!childStyle) continue; + const childPad = { + top: resolveLengthPx(childStyle.paddingTop, fontSize) ?? 0, + right: resolveLengthPx(childStyle.paddingRight, fontSize) ?? 0, + bottom: resolveLengthPx(childStyle.paddingBottom, fontSize) ?? 0, + left: resolveLengthPx(childStyle.paddingLeft, fontSize) ?? 0, + }; + for (const s of ['top', 'right', 'bottom', 'left']) { + if (childPad[s] >= CHILD_INSULATE_THRESHOLD) childrenInsulate[s] = true; + } + } + + const flushSides = []; + for (const side of ['top', 'right', 'bottom', 'left']) { + const sideBounded = borderVisible[side] || outlineVisible || bgVisible; + if (sideBounded && pad[side] <= PAD_THRESHOLD && !childrenInsulate[side]) { + flushSides.push(side); + } + } + + if (flushSides.length > 0) { + // Confirm at least one direct child has substantial text content + // (> 4 chars). Without this, the flush is harmless: e.g. an + // image-only card. + let hasTextChild = false; + for (const child of el.children) { + const childText = (child.textContent || '').trim(); + if (childText.length > 4) { hasTextChild = true; break; } + } + if (hasTextChild) { + const cls = (typeof el.className === 'string' && el.className.trim()) + ? el.className.trim().split(/\s+/)[0] + : ''; + const boundaryParts = []; + const borderSidesVisible = ['top', 'right', 'bottom', 'left'].filter(s => borderVisible[s]); + if (borderSidesVisible.length === 4) boundaryParts.push('border'); + else if (borderSidesVisible.length > 0) boundaryParts.push(`border-${borderSidesVisible.join('/')}`); + if (outlineVisible) boundaryParts.push('outline'); + if (bgVisible) boundaryParts.push('bg'); + const sidesLabel = flushSides.length === 4 ? 'all sides' : flushSides.join('/'); + const ident = cls + ? `<${tag.toLowerCase()}> "${cls}"` + : `<${tag.toLowerCase()}>`; + findings.push({ + id: 'cramped-padding', + snippet: `${ident}: children flush against ${boundaryParts.join('+')} on ${sidesLabel} (no inset)`, + }); + } + } + } + } + } + // --- Body text touching viewport edge --- (browser-only: needs rect) // Catches the failure mode where the agent ships body paragraphs // with NO container providing horizontal padding — text bleeds @@ -1804,6 +2093,20 @@ function checkQuality(opts) { } } + // --- Crushed letter spacing (mirror of wide-tracking) --- + // Tracking pulled tighter than ~-0.05em crushes characters into each other. + // Optical tightening that display type legitimately wants (around -0.02em) + // stays well above this floor. + if (hasDirectText && textLen > 20 && fontSize > 0) { + if (letterSpacingPx != null && letterSpacingPx < 0) { + const trackingEm = letterSpacingPx / fontSize; + if (trackingEm <= -0.05) { + const excerpt = (el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 40); + findings.push({ id: 'extreme-negative-tracking', snippet: `letter-spacing: ${trackingEm.toFixed(2)}em — "${excerpt}"` }); + } + } + } + return findings; } @@ -1820,7 +2123,7 @@ function checkElementQualityDOM(el) { const rect = el.getBoundingClientRect(); const lineMax = (typeof window !== 'undefined' && window.__IMPECCABLE_CONFIG__?.lineLengthMax) || 80; const viewportWidth = (typeof window !== 'undefined' ? window.innerWidth : 0) || 0; - return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax, viewportWidth }); + return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax, viewportWidth, win: typeof window !== 'undefined' ? window : null }); } // Pure page-level skipped-heading walk. Takes a Document so it works in both @@ -1862,7 +2165,7 @@ function checkElementQuality(el, style, tag, window) { const fontSize = resolveFontSizePx(el, window); const lineHeightPx = resolveLengthPx(style.lineHeight, fontSize); const letterSpacingPx = resolveLengthPx(style.letterSpacing, fontSize); - return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect: null }); + return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect: null, win: window }); } function checkElementBorders(tag, style, overrides, resolvedRadius) { @@ -2303,40 +2606,213 @@ function checkPageLayout(doc, win) { } } - // Everything centered - const textEls = doc.querySelectorAll('h1, h2, h3, h4, h5, h6, p, li, div, button'); - let centeredCount = 0; - let totalText = 0; - for (const el of textEls) { - const hasDirectText = [...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim().length >= 3); - if (!hasDirectText) continue; - totalText++; - - let cur = el; - let isCentered = false; - while (cur && cur.nodeType === 1) { - const rawStyle = cur.getAttribute?.('style') || ''; - const cls = cur.getAttribute?.('class') || ''; - if (/text-align\s*:\s*center/i.test(rawStyle) || /\btext-center\b/.test(cls)) { - isCentered = true; - break; - } - if (cur.tagName === 'BODY') break; - cur = cur.parentElement; - } - if (isCentered) centeredCount++; - } - - if (totalText >= 5 && centeredCount / totalText > 0.7) { - findings.push({ - id: 'everything-centered', - snippet: `${centeredCount}/${totalText} text elements centered (${Math.round(centeredCount / totalText * 100)}%)`, - }); - } - return findings; } +// ─── Cream / beige palette (the default "tasteful" AI surface) ──────────────── +// A warm, lightly-tinted off-white page background — light, with R≥G≥B and a +// small warm tint (not white, not a strong color). The current reflex surface. +function isCreamColor(rgb) { + if (!rgb) return false; + const { r, g, b } = rgb; + if (Math.min(r, g, b) < 209) return false; // must be light + if (!(r >= g && g >= b)) return false; // warm ordering + const warmth = r - b; + return warmth >= 6 && warmth <= 48; // tinted, not white, not strong +} + +// Tailwind background utilities that render as a warm off-white surface. The +// static engine doesn't fetch Tailwind's CSS, so a `bg-amber-50` on +// resolves to nothing in computed style — catch it from the class list +// instead. Candidate tokens map to their actual Tailwind hex and are still +// filtered through isCreamColor, so neutral grays (stone) and over-saturated +// shades drop out on their own. +const TAILWIND_BG_HEX = { + 'bg-amber-50': '#fffbeb', 'bg-amber-100': '#fef3c7', + 'bg-orange-50': '#fff7ed', 'bg-orange-100': '#ffedd5', + 'bg-yellow-50': '#fefce8', + 'bg-stone-50': '#fafaf9', 'bg-stone-100': '#f5f5f4', 'bg-stone-200': '#e7e5e4', +}; + +function creamFromClassList(cls) { + if (!cls) return null; + // Arbitrary value: bg-[#f5f0e6] / bg-[rgb(245_240_230)] (underscores = spaces). + const arb = cls.match(/\bbg-\[([^\]]+)\]/); + if (arb && isCreamColor(parseAnyColor(arb[1].replace(/_/g, ' ')))) return `bg-[${arb[1]}]`; + // Named warm-light utilities. + for (const [tok, hex] of Object.entries(TAILWIND_BG_HEX)) { + if (new RegExp(`(^|\\s)${tok}($|\\s)`).test(cls) && isCreamColor(parseAnyColor(hex))) return tok; + } + return null; +} + +function checkCreamPalette(doc, win) { + const findings = []; + const body = doc.body || (doc.querySelector ? doc.querySelector('body') : null); + if (!body) return findings; + const html = doc.documentElement; + const getCS = (el) => (win ? win.getComputedStyle(el) : getComputedStyle(el)); + + // 1. Computed background — covers inline / close.', 'Prefix every preview selector with the matching [data-impeccable-variant="N"] selector.', 'Keep selectors anchored to the generated variant wrapper; do not rely on component CSS scoping for preview rules.', ], forbidden: [ 'Do not use @scope for this styleMode.', + 'Do not wrap style content in a JSX/TSX template literal ({` ... `}); that syntax is for .tsx/.jsx only.', + 'Do not put { immediately after the style opening tag; Astro parses { as expression syntax.', ], }; } @@ -629,4 +632,15 @@ if (_running?.endsWith('live-wrap.mjs') || _running?.endsWith('live-wrap.mjs/')) } // Test exports (used by tests/live-wrap.test.mjs) -export { buildSearchQueries, findElement, findClosingLine, detectCommentSyntax }; +export { + buildSearchQueries, + findElement, + findClosingLine, + detectCommentSyntax, + findAllElements, + filterByText, + findFileWithQuery, + detectStyleMode, + buildCssAuthoring, + buildCssSelectorPrefixExamples, +}; diff --git a/.trae-cn/skills/impeccable/scripts/live.mjs b/.trae-cn/skills/impeccable/scripts/live.mjs index cafb0eca9..8acd300ed 100644 --- a/.trae-cn/skills/impeccable/scripts/live.mjs +++ b/.trae-cn/skills/impeccable/scripts/live.mjs @@ -21,7 +21,7 @@ import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { loadContext } from './load-context.mjs'; +import { loadContext } from './context.mjs'; import { resolveFiles } from './live-inject.mjs'; import { readLiveServerInfo } from './impeccable-paths.mjs'; @@ -80,7 +80,7 @@ The agent should then: process.exit(1); } - // 4. Load PRODUCT.md + DESIGN.md context (auto-migrates legacy .impeccable.md) + // 4. Load PRODUCT.md + DESIGN.md context. const ctx = loadContext(process.cwd()); // 5. Compute drift-heal: compare resolved inject targets against the @@ -102,7 +102,6 @@ The agent should then: hasDesign: ctx.hasDesign, design: ctx.design, designPath: ctx.designPath, - migrated: ctx.migrated, }, null, 2)); } diff --git a/.trae-cn/skills/impeccable/scripts/load-context.mjs b/.trae-cn/skills/impeccable/scripts/load-context.mjs deleted file mode 100644 index dc340bf16..000000000 --- a/.trae-cn/skills/impeccable/scripts/load-context.mjs +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Shared context loader for every impeccable command that needs to know - * "who is this for" and "what does this look like". - * - * Input: project root (process.cwd()). - * - * Output (JSON to stdout): - * { - * hasProduct: boolean, // PRODUCT.md found (or auto-migrated) - * product: string | null, // PRODUCT.md contents - * productPath: string | null, // relative path - * hasDesign: boolean, // DESIGN.md found - * design: string | null, // DESIGN.md contents - * designPath: string | null, - * migrated: boolean, // true if we auto-renamed .impeccable.md -> PRODUCT.md - * contextDir: string, // absolute path of the directory the files were found in - * } - * - * Filename matching is case-insensitive for PRODUCT.md and DESIGN.md. The - * Google DESIGN.md convention is uppercase at repo root; Kiro-style and - * lowercase variants are also matched so users don't get punished for case. - * - * Lookup directory resolution (first match wins): - * 1. process.env.IMPECCABLE_CONTEXT_DIR (absolute or relative to cwd) - * 2. cwd, if PRODUCT.md / DESIGN.md / .impeccable.md is there (back-compat) - * 3. Auto-fallback subdirectories of cwd: .agents/context/, then docs/ - * 4. cwd as a default "no context found" location - * - * Legacy `.impeccable.md` -> PRODUCT.md migration only fires at cwd root; - * fallback directories are read-only as far as auto-rename is concerned. - */ - -import fs from 'node:fs'; -import path from 'node:path'; - -const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; -const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; -const LEGACY_NAMES = ['.impeccable.md']; -const FALLBACK_DIRS = ['.agents/context', 'docs']; - -/** - * Resolve the directory that holds PRODUCT.md / DESIGN.md for - * this project. Exported so other scripts (e.g. live-server.mjs) can read the - * design files from the same location the loader uses. - */ -export function resolveContextDir(cwd = process.cwd()) { - // 1. Explicit override - const envDir = process.env.IMPECCABLE_CONTEXT_DIR; - if (envDir && envDir.trim()) { - const trimmed = envDir.trim(); - return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); - } - - // 2. cwd wins if any canonical or legacy file is there. We check legacy too - // so the auto-migration path in loadContext stays predictable. - if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES, ...LEGACY_NAMES])) { - return cwd; - } - - // 3. Auto-fallback subdirs. Match if PRODUCT.md or DESIGN.md is present; - // legacy `.impeccable.md` does not pull the lookup into a fallback dir. - for (const rel of FALLBACK_DIRS) { - const candidate = path.resolve(cwd, rel); - if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { - return candidate; - } - } - - // 4. Nothing found — keep the historical "default to cwd" behaviour so the - // caller's `hasProduct === false` branch still fires the same way. - return cwd; -} - -export function loadContext(cwd = process.cwd()) { - let migrated = false; - const contextDir = resolveContextDir(cwd); - - // 1. Look for PRODUCT.md (case-insensitive) in the resolved dir - let productPath = firstExisting(contextDir, PRODUCT_NAMES); - - // 2. Legacy: if no PRODUCT.md but .impeccable.md exists at cwd root, rename - // it in place. We only migrate at the root — fallback dirs are read-only - // so we don't surprise users by mutating files under docs/ or .agents/. - if (!productPath && contextDir === cwd) { - const legacyPath = firstExisting(cwd, LEGACY_NAMES); - if (legacyPath) { - const newPath = path.join(cwd, 'PRODUCT.md'); - try { - fs.renameSync(legacyPath, newPath); - productPath = newPath; - migrated = true; - } catch { - // Rename failed (permissions, etc.) — fall back to reading legacy in place - productPath = legacyPath; - } - } - } - - // 3. DESIGN.md (case-insensitive) - const designPath = firstExisting(contextDir, DESIGN_NAMES); - - const product = productPath ? safeRead(productPath) : null; - const design = designPath ? safeRead(designPath) : null; - - return { - hasProduct: !!product, - product, - productPath: productPath ? path.relative(cwd, productPath) : null, - hasDesign: !!design, - design, - designPath: designPath ? path.relative(cwd, designPath) : null, - migrated, - contextDir, - }; -} - -function firstExisting(dir, names) { - for (const name of names) { - const abs = path.join(dir, name); - if (fs.existsSync(abs)) return abs; - } - return null; -} - -function safeRead(p) { - try { return fs.readFileSync(p, 'utf-8'); } catch { return null; } -} - -// --------------------------------------------------------------------------- -// CLI mode — print the context as JSON -// --------------------------------------------------------------------------- - -function cli() { - const result = loadContext(process.cwd()); - console.log(JSON.stringify(result, null, 2)); -} - -const _running = process.argv[1]; -if (_running?.endsWith('load-context.mjs') || _running?.endsWith('load-context.mjs/')) { - cli(); -} diff --git a/.trae-cn/skills/impeccable/scripts/palette.mjs b/.trae-cn/skills/impeccable/scripts/palette.mjs new file mode 100644 index 000000000..4c3f1751a --- /dev/null +++ b/.trae-cn/skills/impeccable/scripts/palette.mjs @@ -0,0 +1,633 @@ +#!/usr/bin/env node +/** + * Brand-seed picker. Returns one OKLCH seed color + the mood it most + * naturally evokes, and teaches the model how to compose a full palette + * around it. + * + * The seed is the brand's anchor color. The 5-role palette (bg, surface, + * ink, accent, muted) is composed by the caller at runtime using their + * judgment + the brief (PRODUCT.md / DESIGN.md / user prompt), NOT picked + * from a frozen 4-color preset. + * + * Why: 4-color frozen palettes drift toward safe defaults (warm-cream bg, + * complementary accent on near-white) regardless of brief. A single seed + + * the model's own composition lets the same seed produce a dark-mode jazz + * club or a light-mode hospitality brand depending on what the brief calls + * for. Tested empirically against curated 4-color palettes; seed approach + * wins on mood-fit in 3 of 5 cases and ties on the rest. + * + * Usage: + * node scripts/palette.mjs # pick at random + * node scripts/palette.mjs --id seed-021 # pick a specific seed + * node scripts/palette.mjs --from # hash to a seed (deterministic) + * + * Env vars: + * IMPECCABLE_PALETTE_SEED — same as --from; useful for the eval harness + * to make runs reproducible. + */ + +import crypto from 'node:crypto'; + +// Seeds are inlined (129 entries, hand-curated via a tinder review of +// ~400 candidates from ColorHunt + synthesis + Radix/brand/Pantone anchors). +// Each carries a mood + strategy the judging model produced — surfaced as +// hints, not commands; the brief still drives composition. +const SEEDS = [ + { id: "seed-200", oklch: [0.360, 0.137, 0.0], + mood: "Aesop apothecary shelf — oxblood bottle glass against linen, considered and unhurried", + strategy: "Seed is a deep desaturated red-brown that reads as brand ink itself; I push primary darker toward bottle-glass oxblood, pair with a pure white surface so the red does the work, and use a clear pale-blush accent that can carry dark text in pills." }, + { id: "seed-000", oklch: [0.400, 0.130, 0.0], + mood: "oxblood leather banquette in a 1940s steakhouse — low lamplight on dark wood and burgundy", + strategy: "Near-black bg with the faintest red undertone lets the oxblood primary glow like lamplit leather; warm cream ink and a brass accent complete the chophouse register." }, + { id: "seed-002", oklch: [0.450, 0.150, 0.0], + mood: "darkroom red light — analog photography, blood-warm safelight glow on chemical trays", + strategy: "Near-black surface with a deep oxblood primary lets the seed function like a safelight in a darkroom — the bg disappears so the red becomes the only emotional signal." }, + { id: "seed-003", oklch: [0.500, 0.194, 0.0], + mood: "darkroom safelight — the deep oxblood glow of analog photography, chemical and contemplative", + strategy: "Anchored the seed as primary against pure near-black so the red reads like a single illuminated bulb in a developing room, with cool desaturated ink to evoke silver gelatin print tones." }, + { id: "seed-004", oklch: [0.546, 0.204, 3.4], + mood: "midnight boudoir — velvet rose under low lamplight, perfumed and intimate", + strategy: "Near-black surface lets the rose seed glow like silk in shadow; a warm champagne accent provides the candle-flame counterpoint without breaking the hush." }, + { id: "seed-005", oklch: [0.550, 0.180, 0.0], + mood: "smoldering vermillion at dusk — the last red ember in a blacksmith's forge, iron-rich and quietly violent", + strategy: "Near-black gallery surround lets the seed read as glowing forged metal; ink stays warm-off-white, accent shifts to a hotter ember orange so the primary feels like cooling steel against a fresh strike." }, + { id: "seed-201", oklch: [0.647, 0.262, 0.3], + mood: "Figma plugin marketplace red — confident product-brand crimson, the kind a modern dev tool uses for a 'live' indicator or a primary CTA on a pristine docs page", + strategy: "Pure white surface lets a high-chroma crimson primary do all the brand work, paired with a hue-shifted warm coral accent for hierarchy without competing saturation" }, + { id: "seed-006", oklch: [0.650, 0.160, 0.0], + mood: "1960s Italian cinema — Technicolor lipstick red against a darkened theater", + strategy: "Pure near-black surface lets a saturated cinematic red and its warm peach accent perform like film light projected in a dark room — the brand colors carry the drama, the bg disappears." }, + { id: "seed-008", oklch: [0.520, 0.200, 10.4], + mood: "Negroni hour at a Milanese bar — bittersweet crimson, vermouth and amaro under low tungsten", + strategy: "Seed is a saturated red-crimson with cinematic weight, so I sit it on near-black to let the primary glow like backlit liquor, with a warmer amber accent acting as the citrus twist against the bitter red." }, + { id: "seed-010", oklch: [0.563, 0.223, 11.0], + mood: "Negroni hour on a Milan rooftop — bittersweet crimson, aperitivo light, polished restraint", + strategy: "Seed is a vivid carmine-red with strong chroma, so the surface gets out of the way (pure white) and lets the primary do the aperitivo work, with a cooled garnet accent for tension." }, + { id: "seed-202", oklch: [0.643, 0.247, 7.0], + mood: "Glossier brand pink — modern beauty editorial, confident and current", + strategy: "Pure white bg lets a saturated rose-red primary do all the brand work, paired with a deeper crimson accent for hierarchy — the Stripe/Glossier move where the color carries the mood." }, + { id: "seed-013", oklch: [0.400, 0.130, 20.0], + mood: "Tuscan cellar at dusk — aged terracotta, oxidized iron, the deep red of decanted Sangiovese", + strategy: "Black surface lets the oxblood seed and copper accent glow like firelight on cellar stone; brand colors carry all the warmth while the room recedes." }, + { id: "seed-014", oklch: [0.450, 0.150, 20.0], + mood: "smoldering tannery — oxblood leather, cured under low workshop light", + strategy: "Anchor the deep oxblood seed as primary against a near-black architectural ground, then lift with a single warm ember accent so the leather reads burnished rather than bloody." }, + { id: "seed-016", oklch: [0.550, 0.180, 20.0], + mood: "Negroni hour on a Roman terrace — bitter campari red, vermouth, late golden light spilling on white linen", + strategy: "Pure white surface lets the campari-red primary do all the emotional work, paired with a deeper oxblood accent for bittersweet depth — Italian aperitivo restraint, not warmth-washed." }, + { id: "seed-205", oklch: [0.634, 0.254, 17.6], + mood: "Aesop apothecary bottle — considered red-coral on a clinical white surface, the kind of brand restraint where one saturated object does all the work", + strategy: "Default A pure white surface lets a single coral-red primary carry the entire brand voice; accent shifts to a deeper oxblood for hierarchy without competing chroma." }, + { id: "seed-011", oklch: [0.639, 0.207, 13.5], + mood: "Aperitivo hour in Milan — Campari glow on a white marble bar, crisp and effervescent", + strategy: "Pure white gallery backdrop lets the Campari-red primary ring like a single bitter note; ink is near-black with a whisper of warmth, accent shifts to a deeper oxblood for hierarchy without competing hues." }, + { id: "seed-015", oklch: [0.527, 0.202, 22.7], + mood: "Negroni hour on a Milanese terrace — bittersweet vermillion, aperitivo glassware catching low sun", + strategy: "Seed becomes a saturated aperitivo-red primary against pure white so the color carries the bittersweet warmth alone, paired with a deep oxblood accent for typographic gravitas." }, + { id: "seed-023", oklch: [0.427, 0.175, 29.2], + mood: "blacksmith's forge at dusk — iron heated to ember red, the deep glow of oxidized metal and quenching oil", + strategy: "Pure black bg lets the seed's ember-red glow radiate like hot iron in a dark forge; accent shifts to a copper-amber to suggest scaling metal and sparks, while ink stays near-white for tool-precise legibility." }, + { id: "seed-206", oklch: [0.614, 0.234, 28.2], + mood: "Aesop apothecary bottle — considered red-orange on lab-white, calm utility with a single confident pigment", + strategy: "Pure white surface lets a saturated vermilion primary do all the brand work, paired with a deep oxblood accent for hierarchy without introducing a second hue family" }, + { id: "seed-029", oklch: [0.665, 0.222, 25.7], + mood: "Negroni hour at a Milanese bar — bittersweet orange-red liqueur catching late afternoon light on polished marble", + strategy: "Pure white surface lets the seed's vermilion read like Campari in a glass; a deeper oxblood accent provides the bitter depth, with neutral graphite ink keeping the editorial restraint of Italian design." }, + { id: "seed-022", oklch: [0.418, 0.155, 27.2], + mood: "Pompeiian red fresco — oxidized cinnabar on a museum wall, archaeological gravity", + strategy: "Pure black gallery surface lets the seed's iron-oxide red read as a lit artifact; accent shifts to an aged terracotta amber, so primary and accent form a fired-clay duet against neutral void." }, + { id: "seed-024", oklch: [0.464, 0.169, 26.9], + mood: "Mid-century darkroom under the safelight — developer trays, oxblood leather, the quiet patience of a print emerging", + strategy: "Seed becomes a deep oxblood primary; surface stays pure black so the red glows like a safelight, with a warmer ember accent for hierarchy" }, + { id: "seed-026", oklch: [0.489, 0.190, 28.3], + mood: "smoldering ember in a blacksmith's forge — iron-hot rust, soot, and controlled fire", + strategy: "Near-black soot background lets the seed's red-orange glow like heated metal; ink is bone-white, accent is a cooler tempered-steel orange that creates internal heat gradient with the primary." }, + { id: "seed-027", oklch: [0.568, 0.208, 27.1], + mood: "Sicilian blood orange at golden hour — citrus rind, terracotta, sun on stucco", + strategy: "Seed reads as vivid blood-orange — picked pure white surface so the citrus-red primary and a deep oxblood accent do all the emotional work, like a Loro Piana editorial spread." }, + { id: "seed-028", oklch: [0.591, 0.172, 24.0], + mood: "Sienna-fired ceramic studio at dusk — terracotta cooling on a wheel, hands still dusted with slip", + strategy: "Pure black stage lets the fired-clay primary glow like a kiln ember, with a deeper oxblood accent providing tonal weight rather than hue contrast — a monochrome warm-axis play." }, + { id: "seed-033", oklch: [0.544, 0.169, 31.3], + mood: "1960s Italian terracotta workshop — fired clay, espresso, late-afternoon Mediterranean dust", + strategy: "Pure black ground lets the seed's burnt-sienna primary glow like a lit kiln, with a deeper oxblood accent for restrained warmth tension — the brand carries the heat, the surface stays out." }, + { id: "seed-207", oklch: [0.564, 0.231, 29.1], + mood: "Aesop apothecary bottle — considered red oxide, the calm authority of a well-made object on a white shelf", + strategy: "Seed becomes the singular brand voice against pure white, with a deeper oxblood accent for hierarchy — the surface disappears so the red does all the speaking." }, + { id: "seed-035", oklch: [0.663, 0.153, 32.1], + mood: "Aesop apothecary bottle — clay-fired warmth, considered retail", + strategy: "Pure white surface lets the terracotta primary do the brand work, paired with a deep umber ink and a cooler clay accent for editorial tension." }, + { id: "seed-037", oklch: [0.590, 0.188, 35.8], + mood: "Aesop apothecary bottle — considered terracotta, herbalist restraint, the warmth comes from the glass not the room", + strategy: "Seed becomes a muted terracotta primary against pure white so the brand's warmth carries entirely through the color itself; accent shifts to a deeper umber for quiet hierarchy." }, + { id: "seed-038", oklch: [0.652, 0.229, 34.8], + mood: "blown-glass furnace at dusk — molten orange iron pulled from the kiln, a craftsman's signature heat", + strategy: "Pure black stage so the seed reads as live ember; primary holds the seed's heat, accent shifts to a brass-amber a hue-step away for a 1.7+ contrast pairing without leaving the fire." }, + { id: "seed-039", oklch: [0.653, 0.185, 33.5], + mood: "Aesop apothecary bottle — considered terracotta, quiet retail craft", + strategy: "Seed becomes a grounded clay primary against pure white, paired with a deeper umber accent so the warmth lives entirely in the brand marks, not the surface." }, + { id: "seed-167", oklch: [0.495, 0.134, 36.0], + mood: "Aesop apothecary shelf — burnished terracotta on clinical white, considered craft pharmacy", + strategy: "Treat the seed as a brand-carrying burnt-sienna against a pure paper-white surface so the warmth lives entirely in the primary, with a deep umber accent pulled along the same warm axis for typographic gravity." }, + { id: "seed-147", oklch: [0.500, 0.151, 40.0], + mood: "Aesop apothecary shelf — considered terracotta, pharmacy restraint, the brand color does the work against clinical white", + strategy: "Anchor the seed's burnt-sienna primary against a pure white surface so the rust speaks alone, with a deep umber ink and a cooler clay accent to give the palette product-brand discipline rather than environmental warmth." }, + { id: "seed-040", oklch: [0.660, 0.201, 40.0], + mood: "Aesop apothecary bottle — amber glass on a clean dispensary shelf, considered and clinical-warm", + strategy: "Seed becomes a burnt-amber primary against pure white so the bottle-glass color does the emotional work; accent shifts to a deep olive-bronze for the apothecary-label pairing." }, + { id: "seed-041", oklch: [0.673, 0.217, 38.6], + mood: "Aesop apothecary shelf — considered orange glass, clinical retail restraint", + strategy: "Pure white surface lets the burnt-orange primary do all the brand work, with a deep ink-brown for editorial gravity and a muted clay accent that reads as a sibling, not a contrast." }, + { id: "seed-042", oklch: [0.688, 0.133, 35.8], + mood: "Aesop apothecary shelf — terracotta glass, considered retail", + strategy: "Seed becomes a warm clay primary against pure white so the bottle-on-marble retail feel comes from the brand color alone; a deeper umber accent gives the label-print contrast." }, + { id: "seed-043", oklch: [0.781, 0.119, 38.1], + mood: "Aesop apothecary catalogue — considered terracotta, dermatological restraint, the warm color doing all the work against clinical white", + strategy: "Pure white surface lets the seed's warm clay tone read as the entire brand voice, paired with a deeper umber accent for hierarchy without competing with the primary's warmth." }, + { id: "seed-168", oklch: [0.400, 0.103, 50.0], + mood: "Aesop apothecary bottle — amber glass on a clinical white shelf, considered and pharmaceutical", + strategy: "Pure white surface lets the deep amber primary act like tinted glass against a clean shelf; accent is a muted clay that complements without competing, keeping the brand quiet and product-led." }, + { id: "seed-044", oklch: [0.568, 0.149, 45.9], + mood: "1970s desert highway at golden hour — sun-faded terracotta, denim dust, the warmth of a Polaroid pulled from a glovebox", + strategy: "Seed becomes a burnt-sienna primary against pure white so the terracotta does all the emotional work; a deep indigo accent acts as the denim shadow opposing the sun, creating the era's signature warm/cool tension without tinting the page." }, + { id: "seed-045", oklch: [0.607, 0.163, 47.7], + mood: "Aesop apothecary shelf — considered amber glass, clinical restraint, craft pharmacy", + strategy: "Pure white bg lets the burnt-amber primary do the apothecary work alone, paired with a deeper umber accent and graphite ink for editorial calm." }, + { id: "seed-046", oklch: [0.653, 0.175, 45.0], + mood: "Aesop apothecary shelf — considered amber glass, quiet luxury, restrained craft", + strategy: "Pure black backdrop lets the warm amber primary glow like backlit apothecary glass, with a deeper rust accent providing tonal depth in the same hue family — monochromatic warm against neutral void." }, + { id: "seed-047", oklch: [0.695, 0.205, 43.2], + mood: "Aesop apothecary label — sun-warmed amber glass on a clinical countertop, restrained botanical pharmacy", + strategy: "Pure white surface lets the burnt-amber primary and a deeper sienna accent do all the brand work, like an apothecary bottle photographed under daylight." }, + { id: "seed-051", oklch: [0.704, 0.189, 49.0], + mood: "blacksmith's forge at dusk — glowing iron, hammered copper, ember light against cooling steel", + strategy: "Pure near-black surface lets the seed's molten orange burn like heated metal; accent shifts to a deeper amber-red to suggest the cooling end of the same iron, while ink stays a clean off-white so type reads like chalk on slate." }, + { id: "seed-171", oklch: [0.550, 0.124, 60.0], + mood: "Klim Type Foundry specimen page — considered ochre on paper, design-school-honest", + strategy: "Seed becomes a muted ochre primary on pure white; accent is a deep ink-navy pulled across the wheel for editorial contrast without warmth-pooling in the bg" }, + { id: "seed-148", oklch: [0.650, 0.146, 60.0], + mood: "Klim-style editorial gold — late-afternoon paper light on a serif specimen sheet, considered and dry", + strategy: "Hold the seed's amber as primary on a pure white page so the gold reads as ink rather than atmosphere, and pair with a deep aubergine accent for typographic contrast." }, + { id: "seed-052", oklch: [0.700, 0.130, 60.0], + mood: "late-afternoon terracotta studio — sun-warmed clay, hands-on craft, the hour before dusk", + strategy: "Seed is a saturated amber-ochre with strong environmental association (ceramics, adobe, sunlit plaster), so I lean into Exception (a) with a faintly warm bone surface that reads as lime-washed wall, then deepen the seed slightly for primary and pair it with a fired-clay rust accent for hand-thrown warmth." }, + { id: "seed-053", oklch: [0.773, 0.157, 56.6], + mood: "late-summer apricot orchard at golden hour — sun-warmed fruit, considered Californian craft", + strategy: "Seed is a juicy mid-warm orange at daylight luminance — leaning optimistic/editorial, so pure white surface lets the apricot primary glow without muddying it; a deep wine accent provides the bite." }, + { id: "seed-149", oklch: [0.600, 0.124, 70.0], + mood: "1970s desert highway — late-afternoon amber light on chrome and asphalt", + strategy: "Anchor the amber seed as primary against pure black so the warm hue reads as headlight glow against night; a cooler dusk-mauve accent provides the complementary tension of horizon vs. sun." }, + { id: "seed-054", oklch: [0.740, 0.162, 68.1], + mood: "late-afternoon honey on terracotta — Mediterranean stucco at golden hour, sun-baked amber", + strategy: "Seed is a saturated honey-amber at high lightness; pairing it with pure black lets the warmth read as luminous gold against gravity, like lamplight in a dark room." }, + { id: "seed-055", oklch: [0.774, 0.174, 65.1], + mood: "late-summer honey hour — amber light slanting through a west-facing window, optimistic and golden", + strategy: "Anchor a saturated honey-amber primary on pure white so the warmth radiates from the brand itself, then pair with a deep teak accent for grounded contrast rather than tinting the canvas." }, + { id: "seed-056", oklch: [0.691, 0.146, 74.6], + mood: "Klim-style modern publishing house — late-afternoon paper warmth, considered editorial gold", + strategy: "Pure white surface so the amber seed becomes the brand voice; ink stays near-black neutral and accent shifts to a deep ink-blue to give the gold something structural to lean on." }, + { id: "seed-150", oklch: [0.750, 0.148, 80.0], + mood: "Klim Type Foundry specimen page — late-summer editorial gold, considered and grown-up", + strategy: "Pure white surface lets a single restrained ochre primary do all the brand work, paired with a deep ink-blue accent for typographic contrast in the Klim/Commercial Type tradition." }, + { id: "seed-058", oklch: [0.764, 0.120, 77.1], + mood: "Klim Type Foundry specimen page — late-afternoon ochre, considered editorial typography", + strategy: "Pure white surface lets the ochre primary do the brand work, paired with a deep ink-blue accent for editorial contrast — the type-foundry move where one warm hue carries the whole feeling against neutral paper." }, + { id: "seed-059", oklch: [0.784, 0.144, 79.8], + mood: "late afternoon in a Tuscan limonaia — sun-cured amber on whitewashed plaster", + strategy: "Pure white surface lets the saffron-amber primary and a deep olive accent carry the Mediterranean warmth, with split-complementary tension between gold and a quiet evergreen." }, + { id: "seed-061", oklch: [0.817, 0.161, 75.1], + mood: "late-afternoon honey on Tuscan limestone — golden hour, slow and luminous", + strategy: "Pure white surface lets the amber primary glow like sunlight on a wall, paired with a deep terracotta accent for warm tonal contrast within the same hue family." }, + { id: "seed-063", oklch: [0.842, 0.165, 91.3], + mood: "late-afternoon Tuscan sun on limestone — golden hour, considered, optimistic", + strategy: "Pure white surface lets the amber-gold primary radiate as the mood-carrier, with a deep aubergine accent providing the long shadow that golden light needs to feel three-dimensional." }, + { id: "seed-174", oklch: [0.350, 0.075, 110.0], + mood: "olive grove at late afternoon — sun-cured leaves, dust, and quiet Mediterranean weight", + strategy: "Pure white surface lets a deep, sun-cured olive primary do the emotional work, with a burnt-terracotta accent providing the warm-earth counterpoint olive groves are known for." }, + { id: "seed-117", oklch: [0.650, 0.100, 110.0], + mood: "Klim-style editorial sage — late-summer foundry catalogue, considered olive-yellow on paper", + strategy: "Seed sits at olive-chartreuse; treating it as a quiet typographic primary on pure paper, with a deeper bronze-olive accent for hierarchy — the color does the work, the page disappears." }, + { id: "seed-118", oklch: [0.750, 0.090, 110.0], + mood: "Klim Type Foundry specimen page — late-summer olive light on a working specimen, the honesty of a type designer showing their work", + strategy: "Pure white bg lets a desaturated olive-yellow primary do the editorial work, with a deeper olive-bronze accent providing typographic emphasis the way a specimen uses one heavy weight against the body roman." }, + { id: "seed-065", oklch: [0.797, 0.166, 113.1], + mood: "late-summer olive grove at noon — sun-bleached leaves, dry stone, Mediterranean glare", + strategy: "Hold the seed as a luminous chartreuse-olive primary against pure white so the color reads as sunlit foliage, pairing it with a deep umber accent for the dry-stone contrast." }, + { id: "seed-176", oklch: [0.300, 0.071, 120.0], + mood: "moss-darkened apothecary jar — herbal, shadowed, mid-19th-century botanical study", + strategy: "Seed is a deep desaturated olive-green that reads as preserved botanical pigment; I anchor it on pure white so the dim moss-green primary feels like ink on a herbarium page, with a warm ochre accent supplying the aged-paper counterpoint." }, + { id: "seed-155", oklch: [0.550, 0.142, 130.0], + mood: "moss-bed forest floor at noon — chlorophyll, lichen, sunlit fern", + strategy: "Seed is a confident mid-olive green with strong chroma; mood is daylight botanical, so I let the brand greens do the work on a pure paper-white bg and pair with a warm umber accent for fern-against-bark contrast." }, + { id: "seed-119", oklch: [0.600, 0.154, 130.0], + mood: "moss garden at Saihō-ji — damp stone, filtered green light through old cedar", + strategy: "Pure near-black bg lets the seed's mossy green glow like wet lichen under low light; accent shifts to a pale ochre-gold like sun catching through canopy." }, + { id: "seed-179", oklch: [0.300, 0.096, 140.0], + mood: "moss on wet stone — forest floor at dusk, deep botanical hush", + strategy: "Kept the seed's deep moss green as primary against a near-black surface so the green reads as living shadow, with a pale lichen accent providing the single point of light." }, + { id: "seed-180", oklch: [0.350, 0.110, 140.0], + mood: "moss-darkened apothecary — herbal tinctures in amber glass, pressed botanicals, the deep green of a conservatory at dusk", + strategy: "Near-black bg with a whisper of green undertone lets the seed's deep moss read as luminous foliage; a warm parchment accent provides the apothecary-label counterpoint without breaking the herbal register." }, + { id: "seed-120", oklch: [0.650, 0.100, 140.0], + mood: "moss on weathered stone — quiet botanical garden conservatory at midday", + strategy: "Pure white bg lets the muted sage-green primary read as a considered botanical mark, with a deeper terracotta accent providing earthen counterpoint without breaking the gallery-like restraint." }, + { id: "seed-121", oklch: [0.750, 0.090, 140.0], + mood: "moss garden at Saihō-ji — diffuse green light filtered through wet stone and lichen", + strategy: "Pure near-black bg lets the muted sage-green primary glow like lichen under low light; a warm pale-bone accent acts as the single ray of sun cutting through canopy." }, + { id: "seed-182", oklch: [0.400, 0.106, 150.0], + mood: "moss garden at Saiho-ji — deep cultivated green under wet stone shadow, contemplative and damp", + strategy: "Near-black bg with the faintest cool-green undertone evokes shaded stone; primary holds the seed's moss tone while accent shifts to a lichen-yellow for organic counterpoint without breaking the hush." }, + { id: "seed-157", oklch: [0.550, 0.145, 150.0], + mood: "moss garden at Saiho-ji — damp stone, filtered green light through cedar canopy", + strategy: "Near-black bg with a faint green undertone evokes deep forest shadow; primary holds the seed's verdant register while accent shifts to a pale lichen-cream to mimic light catching moss." }, + { id: "seed-122", oklch: [0.600, 0.158, 150.0], + mood: "forest floor at first light — moss, lichen, and clean morning air", + strategy: "Seed reads as a living, daylight green; surface stays pure white so the green carries the freshness, with a cool teal accent pulling it toward dew rather than earth." }, + { id: "seed-195", oklch: [0.650, 0.150, 145.0], + mood: "Considered horticulture brand — botanical research lab, the green of a healthy stem photographed in clean daylight", + strategy: "Pure white surface lets the seed's vegetal green carry the entire brand voice, paired with a deep forest ink and a warm clay accent for editorial contrast." }, + { id: "seed-183", oklch: [0.350, 0.077, 160.0], + mood: "moss-stained apothecary — deep forest glass, herbal tinctures shelved in low candlelight", + strategy: "Anchored the seed as primary and built a near-black dark surface with whisper-tinted green to evoke aged apothecary glass, letting the green glow rather than shout." }, + { id: "seed-184", oklch: [0.400, 0.087, 160.0], + mood: "deep forest apothecary — moss, bottle glass, and herbal tincture under afternoon light", + strategy: "Seed becomes a botanical-bottle-green primary on pure white, paired with a warm clove-amber accent to evoke herbal pharmacy contrast without tinting the surface." }, + { id: "seed-158", oklch: [0.550, 0.119, 160.0], + mood: "moss on wet stone — forest floor after rain, mineral and quiet", + strategy: "Pure white surface lets the deep mossy green carry the entire mood; accent shifts to a damp slate-teal to sit beside primary like lichen on stone without competing." }, + { id: "seed-159", oklch: [0.600, 0.130, 160.0], + mood: "moss-covered forest apothecary — herbal tinctures in amber glass, eucalyptus shadow", + strategy: "Anchored the green seed in a near-black backdrop so it reads like botanical glassware lit from within, with a warm amber accent pulled across the wheel to evoke tincture bottles against dark wood." }, + { id: "seed-185", oklch: [0.450, 0.086, 170.0], + mood: "weathered copper patina on a Pacific Northwest greenhouse — oxidized teal, glass light, botanical hush", + strategy: "Seed sits as a deep oxidized-teal primary against pure white so the patina reads as pigment, not atmosphere; a rust-copper accent completes the verdigris/oxidation story across the warm-cool axis." }, + { id: "seed-124", oklch: [0.750, 0.080, 170.0], + mood: "sea-glass on a foggy Pacific shoreline — weathered, mineral, quietly oxidized", + strategy: "Seed is a soft desaturated teal-green; pairing it on pure white lets the mineral primary read as patinated copper-glass, with a deeper kelp-toned primary and a rusted coral accent to spark the muted teal against its complement." }, + { id: "seed-160", oklch: [0.550, 0.095, 180.0], + mood: "weathered copper patina on a museum bronze — oxidized teal, conservatorial quiet", + strategy: "Pure near-black gallery surround lets the patina-teal primary glow like a lit artifact, with a warm verdigris-adjacent accent providing the oxidation contrast against the cool seed." }, + { id: "seed-161", oklch: [0.720, 0.100, 188.0], + mood: "climate-tech dashboard — calm verdigris on plain paper, the quiet confidence of an instrument that just works", + strategy: "Seed teal carries the entire mood as a single considered brand color on pure white, with a desaturated copper accent providing warm signal against the cool primary without competing for attention." }, + { id: "seed-186", oklch: [0.450, 0.074, 200.0], + mood: "deep hydrothermal vent — mineral teal under pressure, the cold blue-green of oxidized copper in submerged light", + strategy: "Near-black surface lets the mineral teal glow as if lit from within; accent shifts toward verdigris-copper to suggest patina on submerged metal, while ink stays cool-neutral to keep the register austere rather than aquatic-cute." }, + { id: "seed-125", oklch: [0.650, 0.100, 200.0], + mood: "climate-tech dashboard — calm operational teal, the color of clean water data and atmospheric sensors", + strategy: "Pure white surface lets a single muted-teal primary do all the brand work, with a deeper marine accent providing hierarchy without competing chroma." }, + { id: "seed-126", oklch: [0.750, 0.080, 200.0], + mood: "climate-tech product brand — quiet competence, dashboards for hard infrastructure problems", + strategy: "Hold the seed's muted teal as primary, pair with a sharper cyan-leaning accent for interactive lift, and let a pure white surface do the disappearing act so the brand reads as a tool, not an atmosphere." }, + { id: "seed-162", oklch: [0.550, 0.091, 210.0], + mood: "weathered nautical instrument — patinated brass on oxidized steel, the cool blue-grey of a ship's chronometer at dawn", + strategy: "Pure white surface lets the muted teal-steel primary read as a precise instrument mark, with a warm brass accent providing the single point of patina against clinical white." }, + { id: "seed-163", oklch: [0.450, 0.086, 230.0], + mood: "deep harbor at dusk — weathered nautical instruments, brass dials on oxidized steel", + strategy: "Near-black background with subtle cool tint evokes the marine dusk; primary holds the seed's teal-blue while a warm brass accent creates the instrument-on-steel tension." }, + { id: "seed-164", oklch: [0.550, 0.105, 230.0], + mood: "deep harbor at dawn — cold steel water, fog-muted light, the quiet before the boats leave", + strategy: "Pure near-black bg lets the seed's cold marine blue read as a luminous beacon, while a pale frost-cyan accent evokes diffused dawn light cutting through fog." }, + { id: "seed-127", oklch: [0.650, 0.100, 230.0], + mood: "climate-tech dashboard — atmospheric sensor blue, calm operational clarity", + strategy: "Anchor the seed as a confident mid-blue primary on pure white so the brand color carries all the atmospheric feeling, with a deep navy accent for hierarchy and a soft slate muted for body text." }, + { id: "seed-128", oklch: [0.750, 0.080, 230.0], + mood: "climate-tech dashboard — calm atmospheric data, considered sky-blue", + strategy: "Pure white surface lets the muted sky-blue primary carry the meteorological calm, with a deep-navy accent providing readable weight against the soft primary." }, + { id: "seed-187", oklch: [0.350, 0.078, 240.0], + mood: "deep harbor at blue hour — wet stone, cold steel, the quiet before night fully lands", + strategy: "Near-black architectural bg with a hint of marine chroma lets the seed read as ambient atmosphere rather than UI chrome; a cooler steel accent sits opposite the warmer-shifted primary for navigational clarity." }, + { id: "seed-077", oklch: [0.578, 0.130, 241.7], + mood: "pre-dawn signal tower — cold blue solitude, instruments glowing against the dark", + strategy: "Pure near-black bg lets the seed's cold tower-light blue glow as the sole emotional source, with a frost-cyan accent acting as a secondary indicator light." }, + { id: "seed-188", oklch: [0.400, 0.110, 250.0], + mood: "Linear's considered indigo — the calm authority of a well-built developer tool, blueprint ink on a clean page", + strategy: "Held the seed as a deep indigo primary against pure white so the brand color carries all the gravity; accent shifts to a cooler, brighter cyan-blue to create a crisp hierarchy pair without warming the surface." }, + { id: "seed-165", oklch: [0.450, 0.123, 250.0], + mood: "blueprint room at dusk — drafting table, graphite, civic-engineering blue", + strategy: "Seed is a mid-deep architectural blue with real chroma and no environmental cue, so I stay out of the way with a pure white surface and let the primary do all the talking, pairing it with a burnt-ochre accent for drafting-pencil contrast." }, + { id: "seed-079", oklch: [0.478, 0.136, 251.8], + mood: "twilight cartography — the blue of deep dusk over open water, precise and navigational", + strategy: "Pure white surface lets the seed's oceanic blue act as a single navigational anchor, with a warm amber accent struck across it like a lighthouse beam at dusk." }, + { id: "seed-080", oklch: [0.541, 0.122, 248.2], + mood: "Linear-style considered tool blue — the calm, exact register of a modern engineering app where every pixel is intentional", + strategy: "Pure white surface lets the considered indigo-blue primary carry the entire brand; a deeper navy accent provides hierarchy without warmth, keeping the palette in a single cool family for that focused-software feel" }, + { id: "seed-166", oklch: [0.550, 0.149, 250.0], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and quietly intense", + strategy: "Near-black bg with the faintest cool tint reads like a darkened cockpit; the seed becomes a luminous instrument-blue primary, paired with a warm amber accent that mimics avionics readouts for unmistakable signal contrast." }, + { id: "seed-081", oklch: [0.650, 0.160, 250.0], + mood: "deep-sea research vessel at dawn — instrument glow against cold steel light", + strategy: "Pure near-white bg keeps the palette technical and instrument-like; the seed blue holds as primary while a desaturated steel-cyan accent reads like signal readouts on glass." }, + { id: "seed-082", oklch: [0.742, 0.140, 247.4], + mood: "high-altitude flight deck at dawn — cold cabin instruments glowing against a sky still holding night", + strategy: "Near-black cockpit ground with a faint blue cast lets the seed read as an illuminated instrument; primary holds the seed, accent shifts to cyan for signal/indicator contrast." }, + { id: "seed-210", oklch: [0.360, 0.140, 260.0], + mood: "Linear-style considered tool indigo — late-night focused work, the deep blue of a code editor at 2am where everything else falls away", + strategy: "Pure black bg lets the indigo primary carry all the cognitive-focus weight, with a slightly brighter periwinkle accent for interactive lift — the surface disappears so the tool feels weightless." }, + { id: "seed-189", oklch: [0.400, 0.130, 260.0], + mood: "pre-dawn observatory — cold instrument blue, star-chart precision", + strategy: "Seed becomes the primary on pure black so the deep instrument-blue glows like a calibration light, with a faint cyan accent reading as starlight against the void." }, + { id: "seed-211", oklch: [0.420, 0.161, 260.0], + mood: "Linear's considered indigo — the tool-for-thought blue of focused product work, calm authority without coldness", + strategy: "Hold the seed as a deep indigo primary against pure white, then pair with a slightly warmer, lighter periwinkle accent to create gentle hue separation without breaking the disciplined tool-brand register." }, + { id: "seed-129", oklch: [0.450, 0.150, 260.0], + mood: "pre-dawn observatory — deep cobalt sky just before astronomical twilight, instruments cool to the touch", + strategy: "Near-black surface lets the cobalt seed read as luminous starlight; a single warm amber accent acts as the calibration lamp against the cold blue field." }, + { id: "seed-084", oklch: [0.476, 0.207, 261.2], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and awake", + strategy: "Default B black bg lets the cobalt primary read as a luminous instrument signal, with a cyan accent striking the analogous 'cockpit display' relationship." }, + { id: "seed-085", oklch: [0.681, 0.132, 258.4], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky", + strategy: "Anchored the seed as a luminous primary against a near-black architectural ground, with a warm amber accent acting as the single instrument light cutting through cold blue." }, + { id: "seed-086", oklch: [0.767, 0.106, 255.9], + mood: "Scandinavian winter morning — quiet light through frost, pale sky over snow", + strategy: "Anchored a pure white editorial stage so the seed's cool sky-blue reads as crisp polar light, with a deeper navy primary providing the only saturated weight — like a single dark pine against snow." }, + { id: "seed-083", oklch: [0.340, 0.159, 262.4], + mood: "deep cobalt twilight — the moment after sunset when the sky goes electric blue and city windows start to glow", + strategy: "Pure black stage lets the cobalt seed act as a luminous neon-window glow, with a warm amber accent across the wheel for the lit-window contrast." }, + { id: "seed-212", oklch: [0.360, 0.219, 270.0], + mood: "Linear-grade tooling indigo — considered software for people who care about craft", + strategy: "Anchored the deep indigo seed as primary on a pure white surface so the brand color carries all the weight, with a slightly cooler violet-blue accent for hierarchy without competing chroma." }, + { id: "seed-130", oklch: [0.400, 0.150, 270.0], + mood: "Linear-grade indigo — considered productivity tool, ink on paper, no theatrics", + strategy: "Pure white surface lets a deep cool indigo carry all the brand weight, paired with a slightly warmer violet-blue accent for hierarchy without acid." }, + { id: "seed-213", oklch: [0.411, 0.241, 267.9], + mood: "Linear-style indigo — considered tool surface, the kind of blue-violet that sits behind a developer's keyboard at 11pm without shouting", + strategy: "Pure black canvas lets a saturated indigo primary do all the brand work, with a cooler cyan-violet accent providing UI signal without competing." }, + { id: "seed-131", oklch: [0.450, 0.180, 270.0], + mood: "monastic indigo dusk — vespers light through stained glass, contemplative and severe", + strategy: "Seed becomes a deep indigo primary against pure near-black so the violet reads as luminous stained-glass against architectural shadow, with a cooler iris accent for tonal lift." }, + { id: "seed-088", oklch: [0.476, 0.158, 268.5], + mood: "pre-dawn astronomer's notebook — deep indigo sky just before the stars fade, ink and graphite", + strategy: "Near-black bg with the faintest cool tint to evoke night sky without theatrics; primary holds the seed's indigo, accent shifts to a paler periwinkle for stellar contrast, keeping the palette monochromatic-cool and observational." }, + { id: "seed-196", oklch: [0.530, 0.130, 268.0], + mood: "Linear-style considered tool indigo — the deep-focus blue-violet of a thoughtfully built productivity surface, the color of a well-typeset keyboard shortcut", + strategy: "Pure white bg lets the indigo seed do all the brand work as primary, with a slightly darker, more saturated violet-shifted accent for hierarchy and interactive states — the surface disappears so the brand color reads as the entire identity." }, + { id: "seed-132", oklch: [0.700, 0.120, 270.0], + mood: "Linear-style considered tool indigo — the quiet violet of a focused product workspace, late-afternoon thinking", + strategy: "Pure white surface lets a muted indigo-violet primary and a slightly cooler accent do all the brand work, keeping the register calm and software-like rather than theatrical." }, + { id: "seed-090", oklch: [0.445, 0.206, 279.1], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, not a nightclub", + strategy: "Anchor the seed as a confident product primary on pure white, with a cooler indigo-shift accent that reads as a sibling tool color, so the brand violet does all the emotional work." }, + { id: "seed-133", oklch: [0.500, 0.160, 280.0], + mood: "Linear-adjacent indigo — considered productivity tool, the violet of a thinking workspace", + strategy: "Seed becomes a measured indigo primary on pure white; accent shifts to a cooler blue-violet to create hierarchy without nightclub saturation, letting the brand color do all the emotional work." }, + { id: "seed-094", oklch: [0.533, 0.125, 294.3], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, calm authority for a creative workspace", + strategy: "Pure white canvas lets the indigo-violet primary carry the entire brand voice; accent shifts hue slightly toward blue for a cool, tool-like duotone rather than warm decorative pairing." }, + { id: "seed-137", oklch: [0.700, 0.120, 290.0], + mood: "Linear-adjacent indigo — the considered tool, late-evening focus mode, software made for people who care about craft", + strategy: "Pure black surface lets a single restrained indigo-violet carry the brand, with a cooler periwinkle accent providing UI hierarchy without competing — Vercel/Linear dark-mode discipline." }, + { id: "seed-100", oklch: [0.450, 0.150, 330.0], + mood: "velvet boudoir at last call — bruised orchid and lipstick traces under low lamplight", + strategy: "Pure near-black surface lets a deep magenta-rose primary smolder while a warm peach accent acts like skin-lit lamplight — drama lives in the brand pair, not the room." }, + { id: "seed-103", oklch: [0.650, 0.160, 330.0], + mood: "1980s Memphis boudoir — powder-pink neon humming against lacquered black, lipstick and lacquer", + strategy: "Near-black gallery surface lets the magenta-pink seed read as lit neon; accent shifts to warm coral to create cinematic dichromatic tension without competing chroma." }, + { id: "seed-228", oklch: [0.360, 0.147, 340.0], + mood: "Figma-era creative tool plum — considered productivity software for designers, the inky violet of a serif wordmark on a marketing site", + strategy: "Held the seed as a deep plum primary against pure white so the brand color does the emotional work; paired with a muted rose accent for warmth without breaking the productivity-tool restraint." }, + { id: "seed-107", oklch: [0.500, 0.200, 340.0], + mood: "Figma plum — creative-tool confidence, considered magenta for a modern design product", + strategy: "Pure white surface lets a saturated magenta-plum primary carry all the brand voice, paired with a cooler violet-leaning accent for hierarchy without competing." }, + { id: "seed-198", oklch: [0.600, 0.210, 340.0], + mood: "Figma-era creative tool plum — confident, considered, made for makers", + strategy: "Anchor a saturated plum primary against pure white so the brand color does all the emotional work, with a deeper magenta-rose accent for hierarchy." }, + { id: "seed-112", oklch: [0.754, 0.193, 343.4], + mood: "Figma-era creative tool — confident pink primary doing the brand work on a clean canvas, the way Linear uses indigo or Stripe uses violet", + strategy: "Anchor the seed pink as a saturated brand primary on pure white so the color carries all the personality; pair with a cooler plum accent to give the pink something to push against without competing." }, + { id: "seed-229", oklch: [0.420, 0.163, 350.0], + mood: "considered fintech rose — the deep magenta of a modern product brand (think Stripe-adjacent, but rotated toward berry), confident and current", + strategy: "pure white surface lets a single deep berry-rose primary do all the brand work, paired with a cooler indigo accent for the contrast move you see in modern product marketing" }, + { id: "seed-113", oklch: [0.470, 0.173, 354.8], + mood: "1960s velvet rope nightclub — crushed magenta, low light, cigarette smoke catching a spotlight", + strategy: "Pure black stage so the seed's smoky magenta reads as a single hot spotlight, paired with a cooler violet accent for the second light cue." }, + { id: "seed-114", oklch: [0.570, 0.158, 353.3], + mood: "fin-de-siècle Parisian rose — velvet curtain, theatre program, lipstick blotted on linen", + strategy: "Drop bg to true black so the dusty-rose primary reads as stage-lit silk; accent shifts to a warmer coral-mauve at higher lightness to create gentle hue rotation without breaking the romance." }, + { id: "seed-199", oklch: [0.650, 0.180, 350.0], + mood: "modern fintech rose — the considered pink of a Series B brand mark, confident and current without nostalgia", + strategy: "Pure white surface lets a saturated rose primary do the brand work, paired with a deep plum accent for hierarchy — the Stripe move applied to a pink hue." }, + { id: "seed-115", oklch: [0.636, 0.218, 355.3], + mood: "backstage at a cabaret — velvet rope, lipstick mark on a champagne glass", + strategy: "Seed reads as a saturated stage-light magenta-red; I push it into pure black so the primary glows like a neon sign and the accent (a cold pearl-pink) acts as the spotlight rim — the room is dark, the color does the singing." }, + { id: "seed-230", oklch: [0.650, 0.249, 354.5], + mood: "Modern fintech rose — the considered pink of a contemporary payments brand: confident, alive, and clear-headed", + strategy: "Pure white bg lets a saturated rose-magenta primary carry all the brand energy, paired with a cooler indigo accent for trustworthy contrast — the Stripe move applied to a pink hue." }, + { id: "seed-231", oklch: [0.682, 0.241, 353.2], + mood: "Figma-era creative tool — a confident pink-magenta product brand, the kind a modern design platform uses to feel alive without shouting", + strategy: "Default A pure white bg lets the saturated pink-magenta primary do all the brand work, with a near-complementary cool teal accent for tool-like clarity and a neutral ink for editorial calm" }, + { id: "seed-116", oklch: [0.734, 0.183, 356.8], + mood: "modern beauty brand DTC — Glossier-adjacent pink, confident and current without being saccharine", + 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." }, +]; + +function parseArgs(argv) { + const args = { id: null, from: null }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === '--id' && argv[i + 1]) { args.id = argv[++i]; } + else if (a === '--from' && argv[i + 1]) { args.from = argv[++i]; } + } + return args; +} + +// Hash a key into a stable float in [0, 1) for deterministic weighted picks. +function hashUnit(key) { + const h = crypto.createHash('sha256').update(key).digest(); + return h.readUInt32BE(0) / 0x100000000; +} + +// The curated library is hue-skewed (more reds/oranges than teals/magentas) +// because that's where the source material + taste landed. Left uniform, a +// random pick would land on red ~1/3 of the time. Inverse-frequency weighting +// gives each seed a weight of 1/(count in its 30° hue bucket), so each hue +// ZONE is roughly equally likely to be chosen regardless of how many seeds it +// holds — fair rainbow exposure across runs without pruning the library. +function buildWeights(seeds) { + const bucketCount = {}; + const bucketOf = (s) => Math.floor(((s.oklch[2] % 360) + 360) % 360 / 30); + for (const s of seeds) { const b = bucketOf(s); bucketCount[b] = (bucketCount[b] || 0) + 1; } + const weights = seeds.map((s) => 1 / bucketCount[bucketOf(s)]); + const total = weights.reduce((a, b) => a + b, 0); + return { weights, total }; +} + +function weightedPick(seeds, unit) { + const { weights, total } = buildWeights(seeds); + let target = unit * total; + for (let i = 0; i < seeds.length; i++) { + target -= weights[i]; + if (target < 0) return seeds[i]; + } + return seeds[seeds.length - 1]; +} + +function pickSeed(seeds, { id, from }) { + if (id) { + const found = seeds.find(s => s.id === id); + if (!found) { console.error(`no seed with id "${id}"`); process.exit(2); } + return found; + } + const envFrom = process.env.IMPECCABLE_PALETTE_SEED; + const key = from || envFrom; + const unit = key ? hashUnit(key) : Math.random(); + return weightedPick(seeds, unit); +} + +function fmtOklch([L, C, H]) { + return `oklch(${L.toFixed(3)} ${C.toFixed(3)} ${H.toFixed(1)})`; +} + +function hueWord(H) { + if (H < 15 || H >= 345) return 'pure red'; + if (H < 35) return 'warm red / crimson'; + if (H < 55) return 'warm coral / burnt orange'; + if (H < 80) return 'orange / honey'; + if (H < 105) return 'warm amber / honey-gold'; + if (H < 135) return 'yellow-green / olive'; + if (H < 170) return 'green'; + if (H < 200) return 'teal'; + if (H < 230) return 'sky blue'; + if (H < 265) return 'cobalt / indigo'; + if (H < 295) return 'violet / purple'; + if (H < 330) return 'magenta / pink'; + return 'deep pink / rose'; +} + +// --------------------------------------------------------------- + +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}` : ''; + +// --------------------------------------------------------------- +// Fat tool-exit response — what the model sees on stdout. +// --------------------------------------------------------------- + +process.stdout.write(`BRAND SEED · ${seed.id} + +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. + +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. + +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é. + + 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 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. + + 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. + +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): + + - 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. + +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. + +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. +`); diff --git a/.trae-cn/skills/impeccable/scripts/pin.mjs b/.trae-cn/skills/impeccable/scripts/pin.mjs index ba02783fb..320d98ce1 100644 --- a/.trae-cn/skills/impeccable/scripts/pin.mjs +++ b/.trae-cn/skills/impeccable/scripts/pin.mjs @@ -27,7 +27,7 @@ const HARNESS_DIRS = [ // Valid sub-command names const VALID_COMMANDS = [ - 'craft', 'teach', 'extract', 'document', 'shape', + 'craft', 'init', 'extract', 'document', 'shape', 'critique', 'audit', 'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live', 'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive', diff --git a/.trae/skills/impeccable/SKILL.md b/.trae/skills/impeccable/SKILL.md index 9d7d7782b..3e96fa2c7 100644 --- a/.trae/skills/impeccable/SKILL.md +++ b/.trae/skills/impeccable/SKILL.md @@ -1,95 +1,90 @@ --- name: impeccable description: Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks. -version: 3.1.1 +version: 3.5.0 user-invocable: true -argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · teach|document|extract|live] [target]" -license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution. +argument-hint: "[craft|shape · audit|critique · animate|bolder|colorize|delight|layout|overdrive|quieter|typeset · adapt|clarify|distill · harden|onboard|optimize|polish · init|document|extract|live] [target]" +license: Apache 2.0 --- Designs and iterates production-grade frontend interfaces. Real working code, committed design choices, exceptional craft. ## Setup -Before any design work or file edits: +You MUST do these steps before proceeding: -1. Load context (PRODUCT.md / DESIGN.md) via the loader script. -2. Identify the register and load the matching register reference (brand.md or product.md). -3. **If the user invoked a sub-command (e.g. `craft`, `shape`, `audit`), load its reference file too.** This is non-negotiable: `craft` without `craft.md` loaded means you'll skip the shape-and-confirm step the user expects. +1. Run `node .trae/skills/impeccable/scripts/context.mjs` once per session. If you've already seen its output in this conversation, do not re-run it. The script either prints the project's PRODUCT.md (and DESIGN.md when present) as a markdown block, or tells you it's missing. Follow whatever it prints. **If it reports `NO_PRODUCT_MD`, stop and follow `reference/init.md` before doing anything else.** If the output ends with an `UPDATE_AVAILABLE` directive, follow it (ask the user once about updating, then continue). It never blocks the current task. +2. If the user invoked a sub-command (`craft`, `shape`, `audit`, `polish`, ...), you MUST read `reference/.md` next. Non-optional. The reference defines the command's flow; without it you will skip steps the user expects. +3. Familiarize yourself with any existing design system, conventions, and components in the code. Read at least one project file (CSS / tokens / theme / a representative component or page). **Required even when you've loaded a sub-command reference in step 2.** Don't reinvent the wheel; use what's there when it works, branch out when the UX wins. +4. Read the matching register reference. **This is non-optional; skipping it produces generic output.** If the project is marketing, a landing page, a campaign, long-form content, or a portfolio (design IS the product), read `reference/brand.md`. If it is app UI, admin, a dashboard, or a tool (design SERVES the product), read `reference/product.md`. Pick by first match: (1) task cue ("landing page" vs "dashboard"); (2) surface in focus (the page, file, or route being worked on); (3) `register` field in PRODUCT.md. +5. **If the project is brand-new (no existing CSS tokens / theme / committed brand colors found in step 3)**, run `node .trae/skills/impeccable/scripts/palette.mjs` to receive a brand seed color and composition guidance. This is the anchor for your primary brand color. Compose the rest of the palette (bg, surface, ink, accent, muted) around it per the script's instructions. Use OKLCH throughout. **Skip this step only if step 3 found committed brand colors in existing tokens; in that case identity-preservation wins.** -Skipping these produces generic output that ignores the project. +## Design guidance -### 1. Context gathering +Produce ready-to-ship, production-grade code, not prototypes or starting points. Take no shortcuts unless the user asks for them (when in doubt, ask). Don't stop until arriving at a complete implementation (beautiful, responsive, fast, precise, bug-free, on brand). You take attention to detail seriously: every page, section or component crafted is battle tested using the tools available to you (browser screenshotting, computer use, etc). the model is capable of extraordinary work. Don't hold back. -Two files, case-insensitive. The loader looks at the project root by default and falls back to `.agents/context/` and `docs/` if the root is clean. Override with `IMPECCABLE_CONTEXT_DIR=path/to/dir` (absolute or relative to cwd). +### General rules -- **PRODUCT.md**: required. Users, brand, tone, anti-references, strategic principles. -- **DESIGN.md**: optional, strongly recommended. Colors, typography, elevation, components. +#### Color -Load both in one call: +- **Verify contrast.** Body text must hit ≥4.5:1 against its background; large text (≥18px or bold ≥14px) needs ≥3:1. Placeholder text needs the same 4.5:1, not the muted-gray default. The most common failure: muted gray body text on a tinted near-white. If the contrast is even close, bump the body color toward the ink end of the ramp; light gray "for elegance" is the single biggest reason AI designs feel hard to read. +- Gray text on a colored background looks washed out. Use a darker shade of the background's own hue, or a transparency of the text color. -```bash -node .trae/skills/impeccable/scripts/load-context.mjs -``` +#### Typography -Consume the full JSON output. Never pipe through `head`, `tail`, `grep`, or `jq`. The output's `contextDir` field tells you where the files were resolved from. +- Cap body line length at 65–75ch. +- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales. +- Cap font-family count at 3 (display + body + optional mono). More than 3 reads as indecision, not richness. One well-tuned family with weight contrast usually beats three competing typefaces. +- Don't pair fonts that are similar but not identical (two geometric sans-serifs, two humanist sans-serifs). Pair on a contrast axis (serif + sans, geometric + humanist) or use one family in multiple weights. +- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. +- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. +- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". +- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. -If the output is already in this session's conversation history, don't re-run. Exceptions requiring a fresh load: you just ran `/impeccable teach` or `/impeccable document` (they rewrite the files), or the user manually edited one. +#### Layout -`/impeccable live` already warms context via `live.mjs`. If you've run `live.mjs`, don't also run `load-context.mjs` this session. +- Vary spacing for rhythm. +- Cards are the lazy answer. Use them only when they're truly the best affordance. Nested cards are always wrong. +- Flexbox for 1D, Grid for 2D. Don't default to Grid when `flex-wrap` would be simpler. +- For responsive grids without breakpoints: `repeat(auto-fit, minmax(280px, 1fr))`. +- Build a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip). Never arbitrary values like 999 or 9999. -If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `/impeccable teach`, then resume the user's original task with the fresh context. If the original task was `/impeccable craft`, resume into `/impeccable shape` before any implementation work. +#### Motion +- Motion should be intentional, and not be an afterthought. consider it as part of the build. +- Don't animate CSS layout properties unless truly needed. +- Ease out with exponential curves (ease-out-quart / quint / expo). No bounce, no elastic. +- Use libraries for more advanced motion needs (e.g. motion, gsap, anime.js, lenis etc) +- Reduced motion is not optional. Every animation needs a `@media (prefers-reduced-motion: reduce)` alternative: typically a crossfade or instant transition. +- Staggering the items within one list is legitimate. The tell is the uniform reflex (one identical entrance applied to every section), not motion itself; each reveal should fit what it reveals. Suppressing the reflex is never a reason to ship a page with no motion at all. +- Reveal animations must enhance an already-visible default. Don't gate content visibility on a class-triggered transition; transitions pause on hidden tabs and headless renderers, so the reveal never fires and the section ships blank. +- Premium motion materials are not just transform/opacity. Blur, backdrop-filter, clip-path, mask, and shadow/glow are part of the palette when they materially improve the effect and stay smooth. -If DESIGN.md is missing: nudge once per session (*"Run `/impeccable document` for more on-brand output"*), then proceed. +#### Interaction -### 2. Register +- Dropdowns rendered with `position: absolute` inside an `overflow: hidden` or `overflow: auto` container will be clipped. Use the native `` / popover API, `position: fixed`, or a portal to escape the stacking context. -Every design task is **brand** (marketing, landing, campaign, long-form content, portfolio: design IS the product) or **product** (app UI, admin, dashboard, tool: design SERVES the product). +### Copy -Identify before designing. Priority: (1) cue in the task itself ("landing page" vs "dashboard"); (2) the surface in focus (the page, file, or route being worked on); (3) `register` field in PRODUCT.md. First match wins. +- Every word earns its place. No restated headings, no intros that repeat the title. +- **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`. +- **No aphoristic-cadence body copy as a default voice.** Don't fall into the rhythm of "serious statement, then punchy short negation" as the page's recurring voice. If three or more section copy blocks on the page land on a short rebuttal-shaped sentence, rewrite. Specific, not aphoristic. +- **No marketing buzzwords.** The streamline / empower / supercharge / leverage / unleash / transform / seamless / world-class / enterprise-grade / next-generation / cutting-edge / game-changer / mission-critical family of phrases. Pick a specific noun and a verb that describes what the product literally does. +- Button labels: verb + object. "Save changes" beats "OK"; "Delete project" beats "Yes". The label should say what will happen. +- Link text needs standalone meaning. "View pricing plans" beats "Click here"; screen readers announce links out of context. -If PRODUCT.md lacks the `register` field (legacy), infer it once from its "Users" and "Product Purpose" sections, then cache the inferred value for the session. Suggest the user run `/impeccable teach` to add the field explicitly. +### New projects only (when no prior work exists) -Load the matching reference: [reference/brand.md](reference/brand.md) or [reference/product.md](reference/product.md). The shared design laws below apply to both. +#### Color & Theme -## Shared design laws - -Apply to every design, both registers. Match implementation complexity to the aesthetic vision: maximalism needs elaborate code, minimalism needs precision. Interpret creatively. Vary across projects; never converge on the same choices. the model is capable of extraordinary work. Don't hold back. - -### Color - -- Use OKLCH. Reduce chroma as lightness approaches 0 or 100; high chroma at extremes looks garish. -- Never use `#000` or `#fff`. Tint every neutral toward the brand hue (chroma 0.005–0.01 is enough). +- Use OKLCH. +- **The cream / sand / beige body bg is the saturated AI default of 2026.** The whole warm-neutral band (OKLCH L 0.84-0.97, C < 0.06, hue 40-100) reads as cream/sand/paper/parchment regardless of what you call it. Token names like `--paper`, `--cream`, `--sand`, `--bone`, `--flour`, `--linen`, `--parchment`, `--wheat`, `--biscuit`, `--ivory` are tells in themselves. If the brief is "warm, traditional, family-coastal-Italian" or "magazine-warm" or "editorial-restraint", DO NOT translate that into a near-white warm-tinted bg; that's the AI move. Pick: (a) a saturated brand color as the body (terracotta, oxblood, deep ochre, near-black), (b) a true off-white at chroma 0 (or chroma toward the brand's own hue, not toward warmth-by-default), or (c) a darker mid-tone tinted neutral that's clearly the brand's own. "Warmth" in the brand is carried by accent + typography + imagery, not by body bg. +- Tinted neutrals: add 0.005–0.015 chroma toward the brand's hue. Don't default-tint toward warm or cool "because the brand feels that way"; that's the cross-project monoculture move. +- When picking a theme: Dark vs. light is never a default. Not dark "because tools look cool dark." Not light "to be safe.".Before choosing, write one sentence of physical scene: who uses this, where, under what ambient light, in what mood. If the sentence doesn't force the answer, it's not concrete enough. Add detail until it does. - Pick a **color strategy** before picking colors. Four steps on the commitment axis: - **Restrained**: tinted neutrals + one accent ≤10%. Product default; brand minimalism. - **Committed**: one saturated color carries 30–60% of the surface. Brand default for identity-driven pages. - **Full palette**: 3–4 named roles, each used deliberately. Brand campaigns; product data viz. - **Drenched**: the surface IS the color. Brand heroes, campaign pages. -- The "one accent ≤10%" rule is Restrained only. Committed / Full palette / Drenched exceed it on purpose. Don't collapse every design to Restrained by reflex. - -### Theme - -Dark vs. light is never a default. Not dark "because tools look cool dark." Not light "to be safe." - -Before choosing, write one sentence of physical scene: who uses this, where, under what ambient light, in what mood. If the sentence doesn't force the answer, it's not concrete enough. Add detail until it does. - -"Observability dashboard" does not force an answer. "SRE glancing at incident severity on a 27-inch monitor at 2am in a dim room" does. Run the sentence, not the category. - -### Typography - -- Cap body line length at 65–75ch. -- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales. - -### Layout - -- Vary spacing for rhythm. Same padding everywhere is monotony. -- Cards are the lazy answer. Use them only when they're truly the best affordance. Nested cards are always wrong. -- Don't wrap everything in a container. Most things don't need one. - -### Motion - -- Don't animate CSS layout properties. -- Ease out with exponential curves (ease-out-quart / quint / expo). No bounce, no elastic. ### Absolute bans @@ -100,12 +95,9 @@ Match-and-refuse. If you're about to write any of these, rewrite the element wit - **Glassmorphism as default.** Blurs and glass cards used decoratively. Rare and purposeful, or nothing. - **The hero-metric template.** Big number, small label, supporting stats, gradient accent. SaaS cliché. - **Identical card grids.** Same-sized cards with icon + heading + text, repeated endlessly. -- **Modal as first thought.** Modals are usually laziness. Exhaust inline / progressive alternatives first. - -### Copy - -- Every word earns its place. No restated headings, no intros that repeat the title. -- **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`. +- **Tiny uppercase tracked eyebrow above every section.** The 2023-era kicker (small all-caps text with wide tracking, "ABOUT" "PROCESS" "PRICING" above each heading) is now the saturated AI scaffold; it appears on 55-95% of generations regardless of brief, which is the definition of a tell. One named kicker as a deliberate brand system is voice; an eyebrow on every section is AI grammar. Choose a different cadence. +- **Numbered section markers as default scaffolding (01 / 02 / 03).** Putting `01 · About / 02 · Process / 03 · Pricing` above every section is the eyebrow trope one tier deeper: reach for it because "landing pages do this" and you're scaffolding by reflex. Numbers earn their place when the section actually IS a sequence (a real 3-step process, an ordered flow, a typed timeline) and the order carries information the reader needs. One deliberate numbered sequence on one page is voice; numbered eyebrows on every section across the site is AI grammar. +- **Text that overflows its container.** Long heading words plus large clamp scales plus narrow grids cause headline overflow on tablet/mobile. Test the heading copy at every breakpoint; if it overflows, reduce the clamp max or rewrite the copy. The viewport is part of the design. ### The AI slop test @@ -113,7 +105,7 @@ If someone could look at this interface and say "AI made that" without doubt, it **Category-reflex check.** Run at two altitudes; the second one catches what the first one misses. -- **First-order:** if someone could guess the theme + palette from the category alone ("observability → dark blue", "healthcare → white + teal", "finance → navy + gold", "crypto → neon on black"), it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain. +- **First-order:** if someone could guess the theme + palette from the category alone, it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain. - **Second-order:** if someone could guess the aesthetic family from category-plus-anti-references ("AI workflow tool that's not SaaS-cream → editorial-typographic", "fintech that's not navy-and-gold → terminal-native dark mode"), it's the trap one tier deeper. The first reflex was avoided; the second wasn't. Rework until both answers are not obvious. The brand register's [reflex-reject aesthetic lanes](reference/brand.md) list catches the currently-saturated families. ## Commands @@ -122,7 +114,7 @@ If someone could look at this interface and say "AI made that" without doubt, it |---|---|---|---| | `craft [feature]` | Build | Shape, then build a feature end-to-end | [reference/craft.md](reference/craft.md) | | `shape [feature]` | Build | Plan UX/UI before writing code | [reference/shape.md](reference/shape.md) | -| `teach` | Build | Set up PRODUCT.md and DESIGN.md context | [reference/teach.md](reference/teach.md) | +| `init` | Build | Set up project context: PRODUCT.md, DESIGN.md, live config, next steps | [reference/init.md](reference/init.md) | | `document` | Build | Generate DESIGN.md from existing project code | [reference/document.md](reference/document.md) | | `extract [target]` | Build | Pull reusable tokens and components into design system | [reference/extract.md](reference/extract.md) | | `critique [target]` | Evaluate | UX design review with heuristic scoring | [reference/critique.md](reference/critique.md) | @@ -150,11 +142,14 @@ Plus two management commands: `pin ` and `unpin `, detailed be 1. **No argument**: render the table above as the user-facing command menu, grouped by category. Ask what they'd like to do. 2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. -3. **First word doesn't match**: general design invocation. Apply the setup steps, shared design laws, and the loaded register reference, using the full argument as context. +3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. +4. **No clear command match**: general design invocation. Apply the setup steps, the General rules, and the loaded register reference, using the full argument as context. Setup (context gathering, register) is already loaded by then; sub-commands don't re-invoke `/impeccable`. -If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `teach` as a blocker, finish teach, refresh context, then resume the original command and target. +If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `init` as a blocker, finish init, refresh context, then resume the original command and target. + +`teach` is a deprecated alias for `init`: if the user types it, load [reference/init.md](reference/init.md) and proceed as if they ran `init`. ## Pin / Unpin diff --git a/.trae/skills/impeccable/reference/adapt.md b/.trae/skills/impeccable/reference/adapt.md index 9d0673ea2..5af7606cb 100644 --- a/.trae/skills/impeccable/reference/adapt.md +++ b/.trae/skills/impeccable/reference/adapt.md @@ -188,3 +188,124 @@ Test thoroughly across contexts: - **Slow connections**: Test on throttled network When the adaptation feels native to each context, hand off to `/impeccable polish` for the final pass. + +--- + +## Reference Material + +The sections below were previously `responsive-design.md` and live inline now so the adapt flow has its deep responsive reference in one place. + +### Responsive Design + +#### Mobile-First: Write It Right + +Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first. + +#### Breakpoints: Content-Driven + +Don't chase device sizes; let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints. + +#### Detect Input Method, Not Just Screen Size + +**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard. Use pointer and hover queries: + +```css +/* Fine pointer (mouse, trackpad) */ +@media (pointer: fine) { + .button { padding: 8px 16px; } +} + +/* Coarse pointer (touch, stylus) */ +@media (pointer: coarse) { + .button { padding: 12px 20px; } /* Larger touch target */ +} + +/* Device supports hover */ +@media (hover: hover) { + .card:hover { transform: translateY(-2px); } +} + +/* Device doesn't support hover (touch) */ +@media (hover: none) { + .card { /* No hover state - use active instead */ } +} +``` + +**Critical**: Don't rely on hover for functionality. Touch users can't hover. + +#### Safe Areas: Handle the Notch + +Modern phones have notches, rounded corners, and home indicators. Use `env()`: + +```css +body { + padding-top: env(safe-area-inset-top); + padding-bottom: env(safe-area-inset-bottom); + padding-left: env(safe-area-inset-left); + padding-right: env(safe-area-inset-right); +} + +/* With fallback */ +.footer { + padding-bottom: max(1rem, env(safe-area-inset-bottom)); +} +``` + +**Enable viewport-fit** in your meta tag: +```html + +``` + +#### Responsive Images: Get It Right + +##### srcset with Width Descriptors + +```html +Hero image +``` + +**How it works**: +- `srcset` lists available images with their actual widths (`w` descriptors) +- `sizes` tells the browser how wide the image will display +- Browser picks the best file based on viewport width AND device pixel ratio + +##### Picture Element for Art Direction + +When you need different crops/compositions (not just resolutions): + +```html + + + + ... + +``` + +#### Layout Adaptation Patterns + +**Navigation**: Three stages: hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `
/` for content that can collapse on mobile. + +#### Testing: Don't Trust DevTools Alone + +DevTools device emulation is useful for layout but misses: + +- Actual touch interactions +- Real CPU/memory constraints +- Network latency patterns +- Font rendering differences +- Browser chrome/keyboard appearances + +**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators. + +--- + +**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful. diff --git a/.trae/skills/impeccable/reference/animate.md b/.trae/skills/impeccable/reference/animate.md index 20aeea659..15d9f9080 100644 --- a/.trae/skills/impeccable/reference/animate.md +++ b/.trae/skills/impeccable/reference/animate.md @@ -6,7 +6,7 @@ Add motion that conveys state, gives feedback, and clarifies hierarchy. Cut moti ## Register -Brand: orchestrated page-load sequences, staggered reveals, scroll-driven animation. Motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions. +Brand: motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions. The saturated AI default is fade-and-rise reveals on every scrolled section; that's a tell, not a choreography. Reserve scroll-triggered motion for moments that earn it. Product: 150–250 ms on most transitions. Motion conveys state: feedback, reveal, loading, transitions between views. No page-load choreography; users are in a task and won't wait for it. @@ -49,10 +49,11 @@ Create a purposeful animation plan: Add motion systematically across these categories: ### Entrance Animations -- **Page load choreography**: Stagger element reveals (100-150ms delays), fade + slide combinations - **Hero section**: Dramatic entrance for primary content (scale, parallax, or creative effects) -- **Content reveals**: Scroll-triggered animations using intersection observer - **Modal/drawer entry**: Smooth slide + fade, backdrop fade, focus management +- **List rhythm**: Sibling stagger is legitimate for cards-in-a-grid or list-items-appearing. Whole-section fade-on-scroll is not a list and is not legitimate. Cap total stagger time: 10 items at 50ms each = 500ms total. For more items, reduce per-item delay or cap the staggered count. + + Use CSS custom properties for clean stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"`, `style="--i: 1"`, etc. on each item. ### Micro-interactions - **Button feedback**: @@ -97,11 +98,14 @@ Use appropriate techniques for each animation: ### Timing & Easing -**Durations by purpose:** -- **100-150ms**: Instant feedback (button press, toggle) -- **200-300ms**: State changes (hover, menu open) -- **300-500ms**: Layout changes (accordion, modal) -- **500-800ms**: Entrance animations (page load) +**Duration: the 100/300/500 rule.** Timing matters more than easing for "feels right": + +| Duration | Use Case | Examples | +|----------|----------|----------| +| **100–150ms** | Instant feedback | Button press, toggle, color change | +| **200–300ms** | State changes | Menu open, tooltip, hover state | +| **300–500ms** | Layout changes | Accordion, modal, drawer | +| **500–800ms** | Entrance animations | Page load, hero reveal | **Easing curves (use these, not CSS defaults):** ```css @@ -134,13 +138,35 @@ Use appropriate techniques for each animation: - GSAP for complex sequences ``` +### Motion Materials + +Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties. Match material to effect: + +- **Transform / opacity**: movement, press feedback, simple reveals, list choreography +- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances +- **Clip-path / masks**: wipes, reveals, editorial cropping, product-like transitions +- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state +- **Grid-template-rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly + +The hard rule isn't "transform and opacity only." It's: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify smoothness in-browser on target viewports. + ### Performance -- **Motion materials**: Use transform/opacity for reliable movement, but use blur, filters, masks, shadows, and color shifts when they materially improve the effect - **Layout safety**: Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins) -- **will-change**: Add sparingly for known expensive animations +- **will-change**: Add sparingly for known expensive animations only (e.g. on `:hover` or an `.animating` class), never preemptively across the whole page +- **Scroll triggers**: Use Intersection Observer instead of scroll event listeners; unobserve after the animation fires once - **Bound expensive effects**: Keep blur/filter/shadow areas small or isolated, use `contain` where appropriate - **Monitor FPS**: Ensure 60fps on target devices +### Perceived Performance + +Nobody cares how fast your site *is*, only how fast it feels. The 80ms threshold: anything under ~80ms feels instant because our brains buffer sensory input for that long to synchronize perception. Target this for micro-interactions. + +- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening. +- **Early completion**: Show content progressively, don't wait for everything (progressive images, streaming HTML, skeleton fade-ins). +- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Use for low-stakes actions (likes, follows). Avoid for payments or destructive operations. +- **Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances. +- **Caution**: Too-fast responses can decrease perceived value for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening. + ### Accessibility ```css @media (prefers-reduced-motion: reduce) { diff --git a/.trae/skills/impeccable/reference/bolder.md b/.trae/skills/impeccable/reference/bolder.md index ff0ef2f14..30d4e852c 100644 --- a/.trae/skills/impeccable/reference/bolder.md +++ b/.trae/skills/impeccable/reference/bolder.md @@ -50,7 +50,7 @@ Create a strategy to increase impact while maintaining coherence: Systematically increase impact across these dimensions: ### Typography Amplification -- **Replace generic fonts**: Swap system fonts for distinctive choices (see the parent skill's typography guidelines and [typography.md](typography.md) for inspiration) +- **Replace generic fonts**: Swap system fonts for distinctive choices (see the parent skill's typography guidelines and the [Reference Material section of typeset.md](typeset.md#reference-material) for inspiration) - **Extreme scale**: Create dramatic size jumps (3x-5x differences, not 1.5x) - **Weight contrast**: Pair 900 weights with 200 weights, not 600 with 400 - **Unexpected choices**: Variable fonts, display fonts for headlines, condensed/extended widths, monospace as intentional accent (not as lazy "dev tool" default) @@ -78,10 +78,10 @@ Systematically increase impact across these dimensions: - **Custom elements**: Illustrative elements, custom icons, decorative details that reinforce brand ### Motion & Animation -- **Entrance choreography**: Staggered, dramatic page load animations with 50-100ms delays -- **Scroll effects**: Parallax, reveal animations, scroll-triggered sequences -- **Micro-interactions**: Satisfying hover effects, click feedback, state changes -- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic, which cheapen the effect) +- **Hero moment**: One signature entrance, once. Not on every visit and not on every section. +- **Micro-interactions**: Satisfying hover effects, click feedback, state changes. +- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic, which cheapen the effect). +- **Bolder ≠ scroll-fade-rise on every section.** That's the saturated AI default, the opposite of bold. ### Composition Boldness - **Hero moments**: Create clear focal points with dramatic treatment diff --git a/.trae/skills/impeccable/reference/brand.md b/.trae/skills/impeccable/reference/brand.md index 3d83a1cdc..194514e95 100644 --- a/.trae/skills/impeccable/reference/brand.md +++ b/.trae/skills/impeccable/reference/brand.md @@ -43,17 +43,10 @@ The reflex-reject lists apply to **new design choices**. When the existing brand ### Pairing and voice -Distinctive + refined is the goal. The specific shape depends on the brand: - -- **Editorial / long-form / luxury**: display serif + sans body (a magazine shape). -- **Tech / dev tools / fintech**: one committed sans, usually; custom-tight tracking, strong weight contrast inside a single family. -- **Consumer / food / travel**: warmer pairings, often a humanist sans plus a script or display serif. -- **Creative studios / agencies**: rule-breaking welcome. Mono-only, or display-only, or custom-drawn type as voice. +Distinctive + refined is the goal. The specific shape depends on the brand, not on the brand's category. A category ("restaurant", "dev tool", "magazine", "fintech") is not a recipe; treating it as one is the first-order reflex SKILL.md warns against. Two families minimum is the rule *only* when the voice needs it. A single well-chosen family with committed weight/size contrast is stronger than a timid display+body pair. -Vary across projects. If the last brief was a serif-display landing page, this one isn't. - ### Scale Modular scale, fluid `clamp()` for headings, ≥1.25 ratio between steps. Flat scales (1.1× apart) read as uncommitted. @@ -74,8 +67,7 @@ Brand surfaces have permission for Committed, Full palette, and Drenched strateg - Asymmetric compositions are one option. Break the grid intentionally for emphasis. - Fluid spacing with `clamp()` that breathes on larger viewports. Vary for rhythm: generous separations, tight groupings. -- Alternative: a strict, visible grid as the voice (brutalist / Swiss / tech-spec aesthetics). Either asymmetric or rigorously-gridded can be "designed"; the failure mode is splitting the difference into a generic centered stack. -- Don't default to centering everything. Left-aligned with asymmetric layouts feels more designed; a strict grid reads as confident structure. A centered-stack hero with icon-title-subtitle cards reads as template. +- For image-led briefs (hotels, restaurants, magazines, photography), full-bleed hero imagery with overlaid menu and centered headline is a canonical move; let the photograph be the design. - When cards ARE the right affordance, use `grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` for breakpoint-free responsiveness. ## Imagery @@ -93,8 +85,7 @@ Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landin ## Motion -- One well-orchestrated page-load with staggered reveals beats scattered micro-interactions, when the brand invites it. Tech-minimal brands often skip entrance motion entirely; the restraint is the voice. -- For collapsing/expanding sections, transition `grid-template-rows` rather than `height`. +- One well-orchestrated page-load beats scattered micro-interactions, when the brand invites it. Some brands skip entrance motion entirely; the restraint is the voice. ## Brand bans (on top of the shared absolute bans) @@ -111,8 +102,7 @@ Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landin Brand can afford things product can't. Take them. -- Ambitious first-load motion. Reveals, scroll-triggered transitions, typographic choreography. +- Ambitious first-load motion. Reveals and typographic choreography that earn their place; not fade-on-scroll for every section. - Single-purpose viewports. One dominant idea per fold, long scroll, deliberate pacing. -- Typographic risk. Enormous display type, unexpected italic cuts, mixed cases, hand-drawn headlines, a single oversize word as a hero. - Unexpected color strategies. Palette IS voice; a calm brand and a restless brand should not share palette mechanics. - Art direction per section. Different sections can have different visual worlds if the narrative demands it. Consistency of voice beats consistency of treatment. diff --git a/.trae/skills/impeccable/reference/clarify.md b/.trae/skills/impeccable/reference/clarify.md index 5f7d420f4..00ee978c3 100644 --- a/.trae/skills/impeccable/reference/clarify.md +++ b/.trae/skills/impeccable/reference/clarify.md @@ -172,3 +172,117 @@ Test that copy improvements work: - **Tone**: Is it appropriate for the situation? When the copy reads cleanly, hand off to `/impeccable polish` for the final pass. + +--- + +## Reference Material + +The sections below were previously `ux-writing.md` and live inline now so the clarify flow has its deep UX-writing reference in one place. + +### UX Writing + +#### The Button Label Problem + +**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns: + +| Bad | Good | Why | +|-----|------|-----| +| OK | Save changes | Says what will happen | +| Submit | Create account | Outcome-focused | +| Yes | Delete message | Confirms the action | +| Cancel | Keep editing | Clarifies what "cancel" means | +| Click here | Download PDF | Describes the destination | + +**For destructive actions**, name the destruction: +- "Delete" not "Remove" (delete is permanent, remove implies recoverable) +- "Delete 5 items" not "Delete selected" (show the count) + +#### Error Messages: The Formula + +Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input". + +##### Error Message Templates + +| Situation | Template | +|-----------|----------| +| **Format error** | "[Field] needs to be [format]. Example: [example]" | +| **Missing required** | "Please enter [what's missing]" | +| **Permission denied** | "You don't have access to [thing]. [What to do instead]" | +| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." | +| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" | + +##### Don't Blame the User + +Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date". + +#### Empty States Are Opportunities + +Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items". + +#### Voice vs Tone + +**Voice** is your brand's personality, consistent everywhere. +**Tone** adapts to the moment. + +| Moment | Tone Shift | +|--------|------------| +| Success | Celebratory, brief: "Done! Your changes are live." | +| Error | Empathetic, helpful: "That didn't work. Here's what to try..." | +| Loading | Reassuring: "Saving your work..." | +| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | + +**Never use humor for errors.** Users are already frustrated. Be helpful, not cute. + +#### Writing for Accessibility + +**Link text** must have standalone meaning: "View pricing plans" not "Click here". **Alt text** describes information, not the image: "Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context. + +#### Writing for Translation + +##### Plan for Expansion + +German text is ~30% longer than English. Allocate space: + +| Language | Expansion | +|----------|-----------| +| German | +30% | +| French | +20% | +| Finnish | +30-40% | +| Chinese | -30% (fewer chars, but same width) | + +##### Translation-Friendly Patterns + +Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear. + +#### Consistency: The Terminology Problem + +Pick one term and stick with it: + +| Inconsistent | Consistent | +|--------------|------------| +| Delete / Remove / Trash | Delete | +| Settings / Preferences / Options | Settings | +| Sign in / Log in / Enter | Sign in | +| Create / Add / New | Create | + +Build a terminology glossary and enforce it. Variety creates confusion. + +#### Avoid Redundant Copy + +If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well. + +#### Loading States + +Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress. + +#### Confirmation Dialogs: Use Sparingly + +Most confirmation dialogs are design failures; consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No"). + +#### Form Instructions + +Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking. + +--- + +**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors. diff --git a/.trae/skills/impeccable/reference/codex.md b/.trae/skills/impeccable/reference/codex.md index f40ec746f..d563af95b 100644 --- a/.trae/skills/impeccable/reference/codex.md +++ b/.trae/skills/impeccable/reference/codex.md @@ -23,8 +23,8 @@ Before generating anything, run a brief direction conversation grounded in the s Ask **2-3 targeted questions** about visual lane, color strategy, atmosphere, and named anchor references. Don't enumerate generic menus; tie each question to the shape brief's answers. Example shape-grounded questions: -- "Brief says 'editorial restraint, Klim-adjacent.' Are we closer to a quiet specimen page or a magazine-spread feel with hero imagery?" -- "Palette strategy from shape was 'Committed.' Want it warm-grounded (deep oxblood + cream) or cool-grounded (slate + paper white)?" +- "Brief says 'specimen-page restraint.' Are we closer to a quiet typographic page or a wider editorial spread with hero imagery?" +- "Palette strategy from shape was 'Committed.' Which one color carries the surface (a brand-driven pick rather than a default warm-or-cool framing)? (And no, the answer isn't a cream/sand body bg; that's the saturated AI default.)" **STOP and wait for answers.** These pin the palette before any pixel gets generated. Do not proceed to Step B until the user has responded. diff --git a/.trae/skills/impeccable/reference/cognitive-load.md b/.trae/skills/impeccable/reference/cognitive-load.md deleted file mode 100644 index 48f8ad58b..000000000 --- a/.trae/skills/impeccable/reference/cognitive-load.md +++ /dev/null @@ -1,106 +0,0 @@ -# Cognitive Load Assessment - -Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload. - ---- - -## Three Types of Cognitive Load - -### Intrinsic Load: The Task Itself -Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it. - -**Manage it by**: -- Breaking complex tasks into discrete steps -- Providing scaffolding (templates, defaults, examples) -- Progressive disclosure: show what's needed now, hide the rest -- Grouping related decisions together - -### Extraneous Load: Bad Design -Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste. - -**Common sources**: -- Confusing navigation that requires mental mapping -- Unclear labels that force users to guess meaning -- Visual clutter competing for attention -- Inconsistent patterns that prevent learning -- Unnecessary steps between user intent and result - -### Germane Load: Learning Effort -Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery. - -**Support it by**: -- Progressive disclosure that reveals complexity gradually -- Consistent patterns that reward learning -- Feedback that confirms correct understanding -- Onboarding that teaches through action, not walls of text - ---- - -## Cognitive Load Checklist - -Evaluate the interface against these 8 items: - -- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements? -- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)? -- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)? -- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen? -- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next? -- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)? -- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one? -- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it? - -**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed). - ---- - -## The Working Memory Rule - -**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001). - -At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider: -- **≤4 items**: Within working memory limits, manageable -- **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure -- **8+ items**: Overloaded; users will skip, misclick, or abandon - -**Practical applications**: -- Navigation menus: ≤5 top-level items (group the rest under clear categories) -- Form sections: ≤4 fields visible per group before a visual break -- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu -- Dashboard widgets: ≤4 key metrics visible without scrolling -- Pricing tiers: ≤3 options (more causes analysis paralysis) - ---- - -## Common Cognitive Load Violations - -### 1. The Wall of Options -**Problem**: Presenting 10+ choices at once with no hierarchy. -**Fix**: Group into categories, highlight recommended, use progressive disclosure. - -### 2. The Memory Bridge -**Problem**: User must remember info from step 1 to complete step 3. -**Fix**: Keep relevant context visible, or repeat it where it's needed. - -### 3. The Hidden Navigation -**Problem**: User must build a mental map of where things are. -**Fix**: Always show current location (breadcrumbs, active states, progress indicators). - -### 4. The Jargon Barrier -**Problem**: Technical or domain language forces translation effort. -**Fix**: Use plain language. If domain terms are unavoidable, define them inline. - -### 5. The Visual Noise Floor -**Problem**: Every element has the same visual weight; nothing stands out. -**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted. - -### 6. The Inconsistent Pattern -**Problem**: Similar actions work differently in different places. -**Fix**: Standardize interaction patterns. Same type of action = same type of UI. - -### 7. The Multi-Task Demand -**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating). -**Fix**: Sequence the steps. Let the user do one thing at a time. - -### 8. The Context Switch -**Problem**: User must jump between screens/tabs/modals to gather info for a single decision. -**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth. diff --git a/.trae/skills/impeccable/reference/color-and-contrast.md b/.trae/skills/impeccable/reference/color-and-contrast.md deleted file mode 100644 index 110c2ee47..000000000 --- a/.trae/skills/impeccable/reference/color-and-contrast.md +++ /dev/null @@ -1,105 +0,0 @@ -# Color & Contrast - -## Color Spaces: Use OKLCH - -**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal, unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark. - -The OKLCH function takes three components: `oklch(lightness chroma hue)` where lightness is 0-100%, chroma is roughly 0-0.4, and hue is 0-360. To build a primary color and its lighter / darker variants, hold the chroma+hue roughly constant and vary the lightness, but **reduce chroma as you approach white or black**, because high chroma at extreme lightness looks garish. - -The hue you pick is a brand decision and should not come from a default. Do not reach for blue (hue 250) or warm orange (hue 60) by reflex; those are the dominant AI-design defaults, not the right answer for any specific brand. - -## Building Functional Palettes - -### Tinted Neutrals - -**Pure gray is dead.** A neutral with zero chroma feels lifeless next to a colored brand. Add a tiny chroma value (0.005-0.015) to all your neutrals, hued toward whatever your brand color is. The chroma is small enough not to read as "tinted" consciously, but it creates subconscious cohesion between brand color and UI surfaces. - -The hue you tint toward should come from THIS project's brand, not from a "warm = friendly, cool = tech" formula. If your brand color is teal, your neutrals lean toward teal. If your brand color is amber, they lean toward amber. The point is cohesion with the SPECIFIC brand, not a stock palette. - -**Avoid** the trap of always tinting toward warm orange or always tinting toward cool blue. Those are the two laziest defaults and they create their own monoculture across projects. - -### Palette Structure - -A complete system needs: - -| Role | Purpose | Example | -|------|---------|---------| -| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades | -| **Neutral** | Text, backgrounds, borders | 9-11 shade scale | -| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each | -| **Surface** | Cards, modals, overlays | 2-3 elevation levels | - -**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise. - -### The 60-30-10 Rule (Applied Correctly) - -This rule is about **visual weight**, not pixel count: - -- **60%**: Neutral backgrounds, white space, base surfaces -- **30%**: Secondary colors: text, borders, inactive states -- **10%**: Accent: CTAs, highlights, focus states - -The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power. - -## Contrast & Accessibility - -### WCAG Requirements - -| Content Type | AA Minimum | AAA Target | -|--------------|------------|------------| -| Body text | 4.5:1 | 7:1 | -| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | -| UI components, icons | 3:1 | 4.5:1 | -| Non-essential decorations | None | None | - -**The gotcha**: Placeholder text still needs 4.5:1. That light gray placeholder you see everywhere? Usually fails WCAG. - -### Dangerous Color Combinations - -These commonly fail contrast or cause readability issues: - -- Light gray text on white (the #1 accessibility fail) -- **Gray text on any colored background**: gray looks washed out and dead on color. Use a darker shade of the background color, or transparency -- Red text on green background (or vice versa): 8% of men can't distinguish these -- Blue text on red background (vibrates visually) -- Yellow text on white (almost always fails) -- Thin light text on images (unpredictable contrast) - -### Never Use Pure Gray or Pure Black - -Pure gray (`oklch(50% 0 0)`) and pure black (`#000`) don't exist in nature; real shadows and surfaces always have a color cast. Even a chroma of 0.005-0.01 is enough to feel natural without being obviously tinted. (See tinted neutrals example above.) - -### Testing - -Don't trust your eyes. Use tools: - -- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) -- Browser DevTools → Rendering → Emulate vision deficiencies -- [Polypane](https://polypane.app/) for real-time testing - -## Theming: Light & Dark Mode - -### Dark Mode Is Not Inverted Light Mode - -You can't just swap colors. Dark mode requires different design decisions: - -| Light Mode | Dark Mode | -|------------|-----------| -| Shadows for depth | Lighter surfaces for depth (no shadows) | -| Dark text on light | Light text on dark (reduce font weight) | -| Vibrant accents | Desaturate accents slightly | -| White backgrounds | Never pure black; use dark gray (oklch 12-18%) | - -In dark mode, depth comes from surface lightness, not shadow. Build a 3-step surface scale where higher elevations are lighter (e.g. 15% / 20% / 25% lightness). Use the SAME hue and chroma as your brand color (whatever it is for THIS project; do not reach for blue) and only vary the lightness. Reduce body text weight slightly (e.g. 350 instead of 400) because light text on dark reads as heavier than dark text on light. - -### Token Hierarchy - -Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer; primitives stay the same. - -## Alpha Is A Design Smell - -Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed. - ---- - -**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Using pure black (#000) for large areas. Skipping color blindness testing (8% of men affected). diff --git a/.trae/skills/impeccable/reference/colorize.md b/.trae/skills/impeccable/reference/colorize.md index 9e96eef59..2457f608c 100644 --- a/.trae/skills/impeccable/reference/colorize.md +++ b/.trae/skills/impeccable/reference/colorize.md @@ -68,10 +68,10 @@ Add color systematically across these dimensions: - **Hover states**: Introduce color on interaction ### Background & Surfaces -- **Tinted backgrounds**: Replace pure gray (`#f5f5f5`) with warm neutrals (`oklch(97% 0.01 60)`) or cool tints (`oklch(97% 0.01 250)`) +- **Tinted backgrounds**: If you replace pure gray, tint toward the brand hue, not toward a generic-warm-or-cool pair. The default-warm-tint (`oklch(97% 0.01 60)` and its neighbors) is now the AI cream/sand giveaway. Be specific to the brand or stay neutral. - **Colored sections**: Use subtle background colors to separate areas - **Gradient backgrounds**: Add depth with subtle, intentional gradients (not generic purple-blue) -- **Cards & surfaces**: Tint cards or surfaces slightly for warmth +- **Cards & surfaces**: Tint cards or surfaces toward the brand, not "for warmth" by reflex **Use OKLCH for color**: It's perceptually uniform, meaning equal steps in lightness *look* equal. Great for generating harmonious scales. @@ -124,8 +124,6 @@ Ensure color addition improves rather than overwhelms: - Use every color in the rainbow (choose 2-4 colors beyond neutrals) - Apply color randomly without semantic meaning - Put gray text on colored backgrounds. It looks washed out; use a darker shade of the background color or transparency instead -- Use pure gray for neutrals. Add subtle color tint (warm or cool) for depth -- Use pure black (`#000`) or pure white (`#fff`) for large areas - Violate WCAG contrast requirements - Use color as the only indicator (accessibility issue) - Make everything colorful (defeats the purpose) @@ -152,3 +150,108 @@ When invoked from live mode, each variant MUST declare a `color-amount` param so ``` Layer 1-2 variant-specific params on top: palette selection (`steps` with named options), temperature warmth, or tint vs. true color. See `reference/live.md` for the full params contract. + +--- + +## Reference Material + +The sections below were previously `color-and-contrast.md` and live inline now so the colorize flow has its deep color reference in one place. + +### Color & Contrast + +#### Color Spaces: Use OKLCH + +**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal, unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark. + +The OKLCH function takes three components: `oklch(lightness chroma hue)` where lightness is 0-100%, chroma is roughly 0-0.4, and hue is 0-360. To build a primary color and its lighter / darker variants, hold the chroma+hue roughly constant and vary the lightness, but **reduce chroma as you approach white or black**, because high chroma at extreme lightness looks garish. + +The hue you pick is a brand decision and should not come from a default. Do not reach for blue (hue 250) or warm orange (hue 60) by reflex; those are the dominant AI-design defaults, not the right answer for any specific brand. + +#### Building Functional Palettes + +##### Tinted Neutrals + +**Pure gray is dead.** A neutral with zero chroma feels lifeless next to a colored brand. Add a tiny chroma value (0.005-0.015) to all your neutrals, hued toward whatever your brand color is. The chroma is small enough not to read as "tinted" consciously, but it creates subconscious cohesion between brand color and UI surfaces. + +The hue you tint toward should come from THIS project's brand, not from a "warm = friendly, cool = tech" formula. If your brand color is teal, your neutrals lean toward teal. If your brand color is amber, they lean toward amber. The point is cohesion with the SPECIFIC brand, not a stock palette. + +**Avoid** the trap of always tinting toward warm orange or always tinting toward cool blue. Those are the two laziest defaults and they create their own monoculture across projects. + +##### Palette Structure + +A complete system needs: + +| Role | Purpose | Example | +|------|---------|---------| +| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades | +| **Neutral** | Text, backgrounds, borders | 9-11 shade scale | +| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each | +| **Surface** | Cards, modals, overlays | 2-3 elevation levels | + +**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise. + +##### The 60-30-10 Rule (Applied Correctly) + +This rule is about **visual weight**, not pixel count: + +- **60%**: Neutral backgrounds, white space, base surfaces +- **30%**: Secondary colors: text, borders, inactive states +- **10%**: Accent: CTAs, highlights, focus states + +The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power. + +#### Contrast & Accessibility + +##### WCAG Requirements + +| Content Type | AA Minimum | AAA Target | +|--------------|------------|------------| +| Body text | 4.5:1 | 7:1 | +| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | +| UI components, icons | 3:1 | 4.5:1 | +| Non-essential decorations | None | None | + +##### Dangerous Color Combinations + +These commonly fail contrast or cause readability issues: + +- Light gray text on white (the #1 accessibility fail) +- Red text on green background (or vice versa): 8% of men can't distinguish these +- Blue text on red background (vibrates visually) +- Yellow text on white (almost always fails) +- Thin light text on images (unpredictable contrast) + +##### Testing + +Don't trust your eyes. Use tools: + +- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) +- Browser DevTools → Rendering → Emulate vision deficiencies +- [Polypane](https://polypane.app/) for real-time testing + +#### Theming: Light & Dark Mode + +##### Dark Mode Is Not Inverted Light Mode + +You can't just swap colors. Dark mode requires different design decisions: + +| Light Mode | Dark Mode | +|------------|-----------| +| Shadows for depth | Lighter surfaces for depth (no shadows) | +| Dark text on light | Light text on dark (reduce font weight) | +| Vibrant accents | Desaturate accents slightly | +| White backgrounds | Either pure black or a deep surface that fits the brand (a brand-tinted near-black at oklch 12-18% works too) | + +In dark mode, depth comes from surface lightness, not shadow. Build a 3-step surface scale where higher elevations are lighter (e.g. 15% / 20% / 25% lightness). Use the SAME hue and chroma as your brand color (whatever it is for THIS project; do not reach for blue) and only vary the lightness. Reduce body text weight slightly (e.g. 350 instead of 400) because light text on dark reads as heavier than dark text on light. + +##### Token Hierarchy + +Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer; primitives stay the same. + +#### Alpha Is A Design Smell + +Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed. + +--- + +**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Skipping color blindness testing (8% of men affected). diff --git a/.trae/skills/impeccable/reference/craft.md b/.trae/skills/impeccable/reference/craft.md index 06094b1dd..2c7bd99b1 100644 --- a/.trae/skills/impeccable/reference/craft.md +++ b/.trae/skills/impeccable/reference/craft.md @@ -56,15 +56,15 @@ If the harness has native image generation (Codex), a compact shape's "confirm o Based on the design brief's "Recommended References" section, consult the relevant impeccable reference files. At minimum, always consult: -- [spatial-design.md](spatial-design.md) for layout and spacing -- [typography.md](typography.md) for type hierarchy +- [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 [motion-design.md](motion-design.md) -- Color-heavy or themed? Consult [color-and-contrast.md](color-and-contrast.md) -- Responsive requirements? Consult [responsive-design.md](responsive-design.md) -- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.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) diff --git a/.trae/skills/impeccable/reference/critique.md b/.trae/skills/impeccable/reference/critique.md index 94863733f..a0691f6d0 100644 --- a/.trae/skills/impeccable/reference/critique.md +++ b/.trae/skills/impeccable/reference/critique.md @@ -38,9 +38,9 @@ Read relevant source files and visually inspect the live page when browser autom Evaluate: - **AI slop**: Would someone believe "AI made this" immediately? Check all DON'T guidance from the parent Impeccable skill. - **Holistic design**: hierarchy, IA, emotional fit, discoverability, composition, typography, color, accessibility, states, copy, and edge cases. -- **Cognitive load**: consult [cognitive-load](cognitive-load.md); report checklist failures and decision points with >4 visible options. +- **Cognitive load**: consult the [Cognitive Load Assessment](#cognitive-load-assessment) section below; report checklist failures and decision points with >4 visible options. - **Emotional journey**: peak-end rule, emotional valleys, reassurance at high-stakes moments. -- **Nielsen heuristics**: consult [heuristics-scoring](heuristics-scoring.md); score all 10 heuristics 0-4. +- **Nielsen heuristics**: consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below; score all 10 heuristics 0-4. Return: AI slop verdict, heuristic scores, cognitive load, emotional journey, 2-3 strengths, 3-5 priority issues, persona red flags, minor observations, and provocative questions. @@ -80,7 +80,7 @@ The chat response is the primary user-facing deliverable. Present the full struc Structure your feedback as a design director would: #### Design Health Score -> *Consult [heuristics-scoring](heuristics-scoring.md)* +> *Consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below.* Present the Nielsen's 10 heuristics scores as a table: @@ -119,16 +119,16 @@ Highlight 2-3 things done well. Be specific about why they work. #### Priority Issues The 3-5 most impactful design problems, ordered by importance. -For each issue, tag with **P0-P3 severity** (consult [heuristics-scoring](heuristics-scoring.md) for severity definitions): +For each issue, tag with **P0-P3 severity** (see [Issue Severity below](#issue-severity-p0p3) for definitions): - **[P?] What**: Name the problem clearly - **Why it matters**: How this hurts users or undermines goals - **Fix**: What to do about it (be concrete) - **Suggested command**: Which command could address this (from: /impeccable adapt, /impeccable animate, /impeccable audit, /impeccable bolder, /impeccable clarify, /impeccable colorize, /impeccable critique, /impeccable delight, /impeccable distill, /impeccable document, /impeccable harden, /impeccable layout, /impeccable onboard, /impeccable optimize, /impeccable overdrive, /impeccable polish, /impeccable quieter, /impeccable shape, /impeccable typeset) #### Persona Red Flags -> *Consult [personas](personas.md)* +> *Consult the [Personas reference](#persona-based-design-testing) below.* -Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `RULES.md` contains a `## Design Context` section from `impeccable teach`, also generate 1-2 project-specific personas from the audience/brand info. +Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `RULES.md` contains a `## Design Context` section from `impeccable init`, also generate 1-2 project-specific personas from the audience/brand info. For each selected persona, walk through the primary user action and list specific red flags found: @@ -234,3 +234,534 @@ After presenting the summary, tell the user: > You can ask me to run these one at a time, all at once, or in any order you prefer. > > Re-run `/impeccable critique` after fixes to see your score improve. + +--- + +## Reference Material + +The sections below were previously separate reference files (`cognitive-load.md`, `heuristics-scoring.md`, `personas.md`). They live inline now so the critique flow has all its deep context in one place. + +### Cognitive Load Assessment + +Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload. + +--- + +#### Three Types of Cognitive Load + +##### Intrinsic Load: The Task Itself +Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it. + +**Manage it by**: +- Breaking complex tasks into discrete steps +- Providing scaffolding (templates, defaults, examples) +- Progressive disclosure: show what's needed now, hide the rest +- Grouping related decisions together + +##### Extraneous Load: Bad Design +Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste. + +**Common sources**: +- Confusing navigation that requires mental mapping +- Unclear labels that force users to guess meaning +- Visual clutter competing for attention +- Inconsistent patterns that prevent learning +- Unnecessary steps between user intent and result + +##### Germane Load: Learning Effort +Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery. + +**Support it by**: +- Progressive disclosure that reveals complexity gradually +- Consistent patterns that reward learning +- Feedback that confirms correct understanding +- Onboarding that teaches through action, not walls of text + +--- + +#### Cognitive Load Checklist + +Evaluate the interface against these 8 items: + +- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements? +- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)? +- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)? +- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen? +- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next? +- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)? +- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one? +- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it? + +**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed). + +--- + +#### The Working Memory Rule + +**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001). + +At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider: +- **≤4 items**: Within working memory limits, manageable +- **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure +- **8+ items**: Overloaded; users will skip, misclick, or abandon + +**Practical applications**: +- Navigation menus: ≤5 top-level items (group the rest under clear categories) +- Form sections: ≤4 fields visible per group before a visual break +- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu +- Dashboard widgets: ≤4 key metrics visible without scrolling +- Pricing tiers: ≤3 options (more causes analysis paralysis) + +--- + +#### Common Cognitive Load Violations + +##### 1. The Wall of Options +**Problem**: Presenting 10+ choices at once with no hierarchy. +**Fix**: Group into categories, highlight recommended, use progressive disclosure. + +##### 2. The Memory Bridge +**Problem**: User must remember info from step 1 to complete step 3. +**Fix**: Keep relevant context visible, or repeat it where it's needed. + +##### 3. The Hidden Navigation +**Problem**: User must build a mental map of where things are. +**Fix**: Always show current location (breadcrumbs, active states, progress indicators). + +##### 4. The Jargon Barrier +**Problem**: Technical or domain language forces translation effort. +**Fix**: Use plain language. If domain terms are unavoidable, define them inline. + +##### 5. The Visual Noise Floor +**Problem**: Every element has the same visual weight; nothing stands out. +**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted. + +##### 6. The Inconsistent Pattern +**Problem**: Similar actions work differently in different places. +**Fix**: Standardize interaction patterns. Same type of action = same type of UI. + +##### 7. The Multi-Task Demand +**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating). +**Fix**: Sequence the steps. Let the user do one thing at a time. + +##### 8. The Context Switch +**Problem**: User must jump between screens/tabs/modals to gather info for a single decision. +**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth. + +--- + +### Heuristics Scoring Guide + +Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough." + +#### Nielsen's 10 Heuristics + +##### 1. Visibility of System Status + +Keep users informed about what's happening through timely, appropriate feedback. + +**Check for**: +- Loading indicators during async operations +- Confirmation of user actions (save, submit, delete) +- Progress indicators for multi-step processes +- Current location in navigation (breadcrumbs, active states) +- Form validation feedback (inline, not just on submit) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | No feedback; user is guessing what happened | +| 1 | Rare feedback; most actions produce no visible response | +| 2 | Partial; some states communicated, major gaps remain | +| 3 | Good; most operations give clear feedback, minor gaps | +| 4 | Excellent; every action confirms, progress is always visible | + +##### 2. Match Between System and Real World + +Speak the user's language. Follow real-world conventions. Information appears in natural, logical order. + +**Check for**: +- Familiar terminology (no unexplained jargon) +- Logical information order matching user expectations +- Recognizable icons and metaphors +- Domain-appropriate language for the target audience +- Natural reading flow (left-to-right, top-to-bottom priority) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Pure tech jargon, alien to users | +| 1 | Mostly confusing; requires domain expertise to navigate | +| 2 | Mixed; some plain language, some jargon leaks through | +| 3 | Mostly natural; occasional term needs context | +| 4 | Speaks the user's language fluently throughout | + +##### 3. User Control and Freedom + +Users need a clear "emergency exit" from unwanted states without extended dialogue. + +**Check for**: +- Undo/redo functionality +- Cancel buttons on forms and modals +- Clear navigation back to safety (home, previous) +- Easy way to clear filters, search, selections +- Escape from long or multi-step processes + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Users get trapped; no way out without refreshing | +| 1 | Difficult exits; must find obscure paths to escape | +| 2 | Some exits; main flows have escape, edge cases don't | +| 3 | Good control; users can exit and undo most actions | +| 4 | Full control; undo, cancel, back, and escape everywhere | + +##### 4. Consistency and Standards + +Users shouldn't wonder whether different words, situations, or actions mean the same thing. + +**Check for**: +- Consistent terminology throughout the interface +- Same actions produce same results everywhere +- Platform conventions followed (standard UI patterns) +- Visual consistency (colors, typography, spacing, components) +- Consistent interaction patterns (same gesture = same behavior) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Inconsistent everywhere; feels like different products stitched together | +| 1 | Many inconsistencies; similar things look/behave differently | +| 2 | Partially consistent; main flows match, details diverge | +| 3 | Mostly consistent; occasional deviation, nothing confusing | +| 4 | Fully consistent; cohesive system, predictable behavior | + +##### 5. Error Prevention + +Better than good error messages is a design that prevents problems in the first place. + +**Check for**: +- Confirmation before destructive actions (delete, overwrite) +- Constraints preventing invalid input (date pickers, dropdowns) +- Smart defaults that reduce errors +- Clear labels that prevent misunderstanding +- Autosave and draft recovery + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Errors easy to make; no guardrails anywhere | +| 1 | Few safeguards; some inputs validated, most aren't | +| 2 | Partial prevention; common errors caught, edge cases slip | +| 3 | Good prevention; most error paths blocked proactively | +| 4 | Excellent; errors nearly impossible through smart constraints | + +##### 6. Recognition Rather Than Recall + +Minimize memory load. Make objects, actions, and options visible or easily retrievable. + +**Check for**: +- Visible options (not buried in hidden menus) +- Contextual help when needed (tooltips, inline hints) +- Recent items and history +- Autocomplete and suggestions +- Labels on icons (not icon-only navigation) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Heavy memorization; users must remember paths and commands | +| 1 | Mostly recall; many hidden features, few visible cues | +| 2 | Some aids; main actions visible, secondary features hidden | +| 3 | Good recognition; most things discoverable, few memory demands | +| 4 | Everything discoverable; users never need to memorize | + +##### 7. Flexibility and Efficiency of Use + +Accelerators, invisible to novices, speed up expert interaction. + +**Check for**: +- Keyboard shortcuts for common actions +- Customizable interface elements +- Recent items and favorites +- Bulk/batch actions +- Power user features that don't complicate the basics + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | One rigid path; no shortcuts or alternatives | +| 1 | Limited flexibility; few alternatives to the main path | +| 2 | Some shortcuts; basic keyboard support, limited bulk actions | +| 3 | Good accelerators; keyboard nav, some customization | +| 4 | Highly flexible; multiple paths, power features, customizable | + +##### 8. Aesthetic and Minimalist Design + +Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose. + +**Check for**: +- Only necessary information visible at each step +- Clear visual hierarchy directing attention +- Purposeful use of color and emphasis +- No decorative clutter competing for attention +- Focused, uncluttered layouts + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Overwhelming; everything competes for attention equally | +| 1 | Cluttered; too much noise, hard to find what matters | +| 2 | Some clutter; main content clear, periphery noisy | +| 3 | Mostly clean; focused design, minor visual noise | +| 4 | Perfectly minimal; every element earns its pixel | + +##### 9. Help Users Recognize, Diagnose, and Recover from Errors + +Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution. + +**Check for**: +- Plain language error messages (no error codes for users) +- Specific problem identification ("Email is missing @" not "Invalid input") +- Actionable recovery suggestions +- Errors displayed near the source of the problem +- Non-blocking error handling (don't wipe the form) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Cryptic errors; codes, jargon, or no message at all | +| 1 | Vague errors; "Something went wrong" with no guidance | +| 2 | Clear but unhelpful; names the problem but not the fix | +| 3 | Clear with suggestions; identifies problem and offers next steps | +| 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work | + +##### 10. Help and Documentation + +Even if the system is usable without docs, help should be easy to find, task-focused, and concise. + +**Check for**: +- Searchable help or documentation +- Contextual help (tooltips, inline hints, guided tours) +- Task-focused organization (not feature-organized) +- Concise, scannable content +- Easy access without leaving current context + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | No help available anywhere | +| 1 | Help exists but hard to find or irrelevant | +| 2 | Basic help; FAQ or docs exist, not contextual | +| 3 | Good documentation; searchable, mostly task-focused | +| 4 | Excellent contextual help; right info at the right moment | + +--- + +#### Score Summary + +**Total possible**: 40 points (10 heuristics × 4 max) + +| Score Range | Rating | What It Means | +|-------------|--------|---------------| +| 36–40 | Excellent | Minor polish only; ship it | +| 28–35 | Good | Address weak areas, solid foundation | +| 20–27 | Acceptable | Significant improvements needed before users are happy | +| 12–19 | Poor | Major UX overhaul required; core experience broken | +| 0–11 | Critical | Redesign needed; unusable in current state | + +--- + +#### Issue Severity (P0–P3) + +Tag each individual issue found during scoring with a priority level: + +| Priority | Name | Description | Action | +|----------|------|-------------|--------| +| **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper | +| **P1** | Major | Causes significant difficulty or confusion | Fix before release | +| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass | +| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits | + +**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1. + +--- + +### Persona-Based Design Testing + +Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss. + +**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns. + +--- + +#### 1. Impatient Power User: "Alex" + +**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave. + +**Behaviors**: +- Skips all onboarding and instructions +- Looks for keyboard shortcuts immediately +- Tries to bulk-select, batch-edit, and automate +- Gets frustrated by required steps that feel unnecessary +- Abandons if anything feels slow or patronizing + +**Test Questions**: +- Can Alex complete the core task in under 60 seconds? +- Are there keyboard shortcuts for common actions? +- Can onboarding be skipped entirely? +- Do modals have keyboard dismiss (Esc)? +- Is there a "power user" path (shortcuts, bulk actions)? + +**Red Flags** (report these specifically): +- Forced tutorials or unskippable onboarding +- No keyboard navigation for primary actions +- Slow animations that can't be skipped +- One-item-at-a-time workflows where batch would be natural +- Redundant confirmation steps for low-risk actions + +--- + +#### 2. Confused First-Timer: "Jordan" + +**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out. + +**Behaviors**: +- Reads all instructions carefully +- Hesitates before clicking anything unfamiliar +- Looks for help or support constantly +- Misunderstands jargon and abbreviations +- Takes the most literal interpretation of any label + +**Test Questions**: +- Is the first action obviously clear within 5 seconds? +- Are all icons labeled with text? +- Is there contextual help at decision points? +- Does terminology assume prior knowledge? +- Is there a clear "back" or "undo" at every step? + +**Red Flags** (report these specifically): +- Icon-only navigation with no labels +- Technical jargon without explanation +- No visible help option or guidance +- Ambiguous next steps after completing an action +- No confirmation that an action succeeded + +--- + +#### 3. Accessibility-Dependent User: "Sam" + +**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences. + +**Behaviors**: +- Tabs through the interface linearly +- Relies on ARIA labels and heading structure +- Cannot see hover states or visual-only indicators +- Needs adequate color contrast (4.5:1 minimum) +- May use browser zoom up to 200% + +**Test Questions**: +- Can the entire primary flow be completed keyboard-only? +- Are all interactive elements focusable with visible focus indicators? +- Do images have meaningful alt text? +- Is color contrast WCAG AA compliant (4.5:1 for text)? +- Does the screen reader announce state changes (loading, success, errors)? + +**Red Flags** (report these specifically): +- Click-only interactions with no keyboard alternative +- Missing or invisible focus indicators +- Meaning conveyed by color alone (red = error, green = success) +- Unlabeled form fields or buttons +- Time-limited actions without extension option +- Custom components that break screen reader flow + +--- + +#### 4. Deliberate Stress Tester: "Riley" + +**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience. + +**Behaviors**: +- Tests edge cases intentionally (empty states, long strings, special characters) +- Submits forms with unexpected data (emoji, RTL text, very long values) +- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs +- Looks for inconsistencies between what the UI promises and what actually happens +- Documents problems methodically + +**Test Questions**: +- What happens at the edges (0 items, 1000 items, very long text)? +- Do error states recover gracefully or leave the UI in a broken state? +- What happens on refresh mid-workflow? Is state preserved? +- Are there features that appear to work but produce broken results? +- How does the UI handle unexpected input (emoji, special chars, paste from Excel)? + +**Red Flags** (report these specifically): +- Features that appear to work but silently fail or produce wrong results +- Error handling that exposes technical details or leaves UI in a broken state +- Empty states that show nothing useful ("No results" with no guidance) +- Workflows that lose user data on refresh or navigation +- Inconsistent behavior between similar interactions in different parts of the UI + +--- + +#### 5. Distracted Mobile User: "Casey" + +**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection. + +**Behaviors**: +- Uses thumb only; prefers bottom-of-screen actions +- Gets interrupted mid-flow and returns later +- Switches between apps frequently +- Has limited attention span and low patience +- Types as little as possible, prefers taps and selections + +**Test Questions**: +- Are primary actions in the thumb zone (bottom half of screen)? +- Is state preserved if the user leaves and returns? +- Does it work on slow connections (3G)? +- Can forms use autocomplete and smart defaults? +- Are touch targets at least 44×44pt? + +**Red Flags** (report these specifically): +- Important actions positioned at the top of the screen (unreachable by thumb) +- No state persistence; progress lost on tab switch or interruption +- Large text inputs required where selection would work +- Heavy assets loading on every page (no lazy loading) +- Tiny tap targets or targets too close together + +--- + +#### Selecting Personas + +Choose personas based on the interface type: + +| Interface Type | Primary Personas | Why | +|---------------|-----------------|-----| +| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile | +| Dashboard / admin | Alex, Sam | Power users, accessibility | +| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity | +| Onboarding flow | Jordan, Casey | Confusion, interruption | +| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav | +| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile | + +--- + +#### Project-Specific Personas + +If `RULES.md` contains a `## Design Context` section (generated by `impeccable init`), derive 1–2 additional personas from the audience and brand information: + +1. Read the target audience description +2. Identify the primary user archetype not covered by the 5 predefined personas +3. Create a persona following this template: + +``` +##### [Role]: "[Name]" + +**Profile**: [2-3 key characteristics derived from Design Context] + +**Behaviors**: [3-4 specific behaviors based on the described audience] + +**Red Flags**: [3-4 things that would alienate this specific user type] +``` + +Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists. diff --git a/.trae/skills/impeccable/reference/document.md b/.trae/skills/impeccable/reference/document.md index 02ce8d6f9..60075688a 100644 --- a/.trae/skills/impeccable/reference/document.md +++ b/.trae/skills/impeccable/reference/document.md @@ -45,7 +45,7 @@ Rules that matter: - **Token refs** use `{path.to.token}` (e.g. `{colors.primary}`, `{rounded.md}`). Components may reference primitives; primitives may not reference each other. - **Stitch validates colors as hex sRGB only** (`#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`); OKLCH/HSL/P3 trigger a linter warning, not a hard error. YAML accepts the string either way and our own parser is format-agnostic. Choose based on project posture: (a) if the project has an "OKLCH-only" doctrine or uses Display-P3 values that don't round-trip through sRGB, put OKLCH directly in the frontmatter and accept the Stitch linter warning; (b) if the project wants strict Stitch compliance or plans to use their Tailwind/DTCG export pipeline, put hex in the frontmatter and keep OKLCH in prose as the canonical reference. Never split the source of truth without explicit reason. - **Component sub-tokens** are limited to 8 props: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, `width`. Shadows, motion, focus rings, backdrop-filter: none of those fit. Carry them in the sidecar (Step 4b). -- **Scale keys are open-ended.** Use whatever names the project already uses (`warm-ash-cream`, `surface-container-low`). Don't rename to Material defaults. +- **Scale keys are open-ended.** Use whatever names the project already uses (`oxblood-deep`, `surface-container-low`). Don't rename to Material defaults. - **Variants are naming convention, not schema.** `button-primary` / `button-primary-hover` / `button-primary-active` as sibling keys. ## The markdown body: six sections (exact order) @@ -61,7 +61,7 @@ Optional evocative subtitles are allowed in the form `## 2. Colors: The [Name] P ## When to run -- The user just ran `/impeccable teach` and needs the visual side documented. +- The user just ran `/impeccable init` and needs the visual side documented. - The skill noticed no `DESIGN.md` exists and nudged the user to create one. - An existing `DESIGN.md` is stale (the design has drifted). - Before a large redesign, to capture the current state as a reference. @@ -71,7 +71,7 @@ If a `DESIGN.md` already exists, **do not silently overwrite it**. Show the user ## Two paths - **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 teach, nothing built yet). Interview for five high-level answers, write a minimal DESIGN.md marked ``. Re-run in scan mode once there's code. +- **Seed mode**: the project is pre-implementation (fresh init, nothing built yet). Interview for five high-level answers, 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` forces seed mode regardless of code presence. @@ -103,7 +103,7 @@ Build a structured draft from the discovered tokens. For each token class: From the auto-extracted tokens, draft the YAML frontmatter now (you'll write it at the top of DESIGN.md in Step 4). This is the machine-readable layer: what the live panel and Stitch's linter consume. -- **Colors**: one entry per extracted color. Key = descriptive slug (`warm-ash-cream`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. +- **Colors**: one entry per extracted color. Key = descriptive slug (`oxblood-deep`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. - **Typography**: one entry per role (`display`, `headline`, `title`, `body`, `label`). Typography is an object; include only the props that are real for the project (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`). - **Rounded / Spacing**: whatever scale steps the project actually uses, keyed by whatever scale name the project uses (`sm` / `md` / `lg`, or `surface-sm`, or numeric steps). - **Components**: one entry per variant (`button-primary`, `button-primary-hover`, `button-ghost`). Reference primitives via `{colors.X}`, `{rounded.Y}`. If a variant needs a property Stitch's 8-prop set doesn't cover (shadow, focus ring, backdrop-filter), carry the full snippet in the sidecar instead. @@ -253,7 +253,7 @@ Regenerate the sidecar whenever you regenerate root `DESIGN.md`. If the user onl "extensions": { "colorMeta": { "primary": { "role": "primary", "displayName": "Editorial Magenta", "canonical": "oklch(60% 0.25 350)", "tonalRamp": ["...", "...", "..."] }, - "warm-ash-cream": { "role": "neutral", "displayName": "Warm Ash Cream", "canonical": "oklch(96% 0.005 350)", "tonalRamp": ["...", "...", "..."] } + "cool-paper": { "role": "neutral", "displayName": "Cool Paper", "canonical": "oklch(96% 0.005 230)", "tonalRamp": ["...", "...", "..."] } }, "typographyMeta": { "display": { "displayName": "Display", "purpose": "Hero headlines only." } @@ -328,12 +328,13 @@ Pull directly from the DESIGN.md you just wrote: Do not reword. The panel shows these as secondary collapsible context; the same voice that's in the Markdown carries through. -### Step 5: Confirm, refine, and refresh session cache +### Step 5: Confirm and refine 1. Show the user the full DESIGN.md you wrote. Briefly highlight the non-obvious creative choices (descriptive color names, atmosphere language, named rules). 2. Mention that `.impeccable/design.json` was also written alongside; the live panel will now render this project's actual button/input/nav primitives instead of generic approximations. 3. Offer to refine any section: "Want me to revise a section, add component patterns I missed, or adjust the atmosphere language?" -4. **Refresh the session cache.** Run `node .trae/skills/impeccable/scripts/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. + +Your own write is the freshest source; subsequent commands in this session don't need a reload. ## Seed mode @@ -394,11 +395,12 @@ Per-section guidance in seed mode: 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. -### Step 4: Confirm and refresh session cache +### Step 4: Confirm 1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment). 2. Tell the user: "Re-run `/impeccable document` once you have some code. That pass will extract real tokens and generate the sidecar." -3. Run `node .trae/skills/impeccable/scripts/load-context.mjs` once so the seed lands in conversation for the rest of the session. + +Your own write is the freshest source; no reload needed. ## Style guidelines diff --git a/.trae/skills/impeccable/reference/heuristics-scoring.md b/.trae/skills/impeccable/reference/heuristics-scoring.md deleted file mode 100644 index edbe5028d..000000000 --- a/.trae/skills/impeccable/reference/heuristics-scoring.md +++ /dev/null @@ -1,234 +0,0 @@ -# Heuristics Scoring Guide - -Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough." - -## Nielsen's 10 Heuristics - -### 1. Visibility of System Status - -Keep users informed about what's happening through timely, appropriate feedback. - -**Check for**: -- Loading indicators during async operations -- Confirmation of user actions (save, submit, delete) -- Progress indicators for multi-step processes -- Current location in navigation (breadcrumbs, active states) -- Form validation feedback (inline, not just on submit) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | No feedback; user is guessing what happened | -| 1 | Rare feedback; most actions produce no visible response | -| 2 | Partial; some states communicated, major gaps remain | -| 3 | Good; most operations give clear feedback, minor gaps | -| 4 | Excellent; every action confirms, progress is always visible | - -### 2. Match Between System and Real World - -Speak the user's language. Follow real-world conventions. Information appears in natural, logical order. - -**Check for**: -- Familiar terminology (no unexplained jargon) -- Logical information order matching user expectations -- Recognizable icons and metaphors -- Domain-appropriate language for the target audience -- Natural reading flow (left-to-right, top-to-bottom priority) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Pure tech jargon, alien to users | -| 1 | Mostly confusing; requires domain expertise to navigate | -| 2 | Mixed; some plain language, some jargon leaks through | -| 3 | Mostly natural; occasional term needs context | -| 4 | Speaks the user's language fluently throughout | - -### 3. User Control and Freedom - -Users need a clear "emergency exit" from unwanted states without extended dialogue. - -**Check for**: -- Undo/redo functionality -- Cancel buttons on forms and modals -- Clear navigation back to safety (home, previous) -- Easy way to clear filters, search, selections -- Escape from long or multi-step processes - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Users get trapped; no way out without refreshing | -| 1 | Difficult exits; must find obscure paths to escape | -| 2 | Some exits; main flows have escape, edge cases don't | -| 3 | Good control; users can exit and undo most actions | -| 4 | Full control; undo, cancel, back, and escape everywhere | - -### 4. Consistency and Standards - -Users shouldn't wonder whether different words, situations, or actions mean the same thing. - -**Check for**: -- Consistent terminology throughout the interface -- Same actions produce same results everywhere -- Platform conventions followed (standard UI patterns) -- Visual consistency (colors, typography, spacing, components) -- Consistent interaction patterns (same gesture = same behavior) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Inconsistent everywhere; feels like different products stitched together | -| 1 | Many inconsistencies; similar things look/behave differently | -| 2 | Partially consistent; main flows match, details diverge | -| 3 | Mostly consistent; occasional deviation, nothing confusing | -| 4 | Fully consistent; cohesive system, predictable behavior | - -### 5. Error Prevention - -Better than good error messages is a design that prevents problems in the first place. - -**Check for**: -- Confirmation before destructive actions (delete, overwrite) -- Constraints preventing invalid input (date pickers, dropdowns) -- Smart defaults that reduce errors -- Clear labels that prevent misunderstanding -- Autosave and draft recovery - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Errors easy to make; no guardrails anywhere | -| 1 | Few safeguards; some inputs validated, most aren't | -| 2 | Partial prevention; common errors caught, edge cases slip | -| 3 | Good prevention; most error paths blocked proactively | -| 4 | Excellent; errors nearly impossible through smart constraints | - -### 6. Recognition Rather Than Recall - -Minimize memory load. Make objects, actions, and options visible or easily retrievable. - -**Check for**: -- Visible options (not buried in hidden menus) -- Contextual help when needed (tooltips, inline hints) -- Recent items and history -- Autocomplete and suggestions -- Labels on icons (not icon-only navigation) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Heavy memorization; users must remember paths and commands | -| 1 | Mostly recall; many hidden features, few visible cues | -| 2 | Some aids; main actions visible, secondary features hidden | -| 3 | Good recognition; most things discoverable, few memory demands | -| 4 | Everything discoverable; users never need to memorize | - -### 7. Flexibility and Efficiency of Use - -Accelerators, invisible to novices, speed up expert interaction. - -**Check for**: -- Keyboard shortcuts for common actions -- Customizable interface elements -- Recent items and favorites -- Bulk/batch actions -- Power user features that don't complicate the basics - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | One rigid path; no shortcuts or alternatives | -| 1 | Limited flexibility; few alternatives to the main path | -| 2 | Some shortcuts; basic keyboard support, limited bulk actions | -| 3 | Good accelerators; keyboard nav, some customization | -| 4 | Highly flexible; multiple paths, power features, customizable | - -### 8. Aesthetic and Minimalist Design - -Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose. - -**Check for**: -- Only necessary information visible at each step -- Clear visual hierarchy directing attention -- Purposeful use of color and emphasis -- No decorative clutter competing for attention -- Focused, uncluttered layouts - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Overwhelming; everything competes for attention equally | -| 1 | Cluttered; too much noise, hard to find what matters | -| 2 | Some clutter; main content clear, periphery noisy | -| 3 | Mostly clean; focused design, minor visual noise | -| 4 | Perfectly minimal; every element earns its pixel | - -### 9. Help Users Recognize, Diagnose, and Recover from Errors - -Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution. - -**Check for**: -- Plain language error messages (no error codes for users) -- Specific problem identification ("Email is missing @" not "Invalid input") -- Actionable recovery suggestions -- Errors displayed near the source of the problem -- Non-blocking error handling (don't wipe the form) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Cryptic errors; codes, jargon, or no message at all | -| 1 | Vague errors; "Something went wrong" with no guidance | -| 2 | Clear but unhelpful; names the problem but not the fix | -| 3 | Clear with suggestions; identifies problem and offers next steps | -| 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work | - -### 10. Help and Documentation - -Even if the system is usable without docs, help should be easy to find, task-focused, and concise. - -**Check for**: -- Searchable help or documentation -- Contextual help (tooltips, inline hints, guided tours) -- Task-focused organization (not feature-organized) -- Concise, scannable content -- Easy access without leaving current context - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | No help available anywhere | -| 1 | Help exists but hard to find or irrelevant | -| 2 | Basic help; FAQ or docs exist, not contextual | -| 3 | Good documentation; searchable, mostly task-focused | -| 4 | Excellent contextual help; right info at the right moment | - ---- - -## Score Summary - -**Total possible**: 40 points (10 heuristics × 4 max) - -| Score Range | Rating | What It Means | -|-------------|--------|---------------| -| 36–40 | Excellent | Minor polish only; ship it | -| 28–35 | Good | Address weak areas, solid foundation | -| 20–27 | Acceptable | Significant improvements needed before users are happy | -| 12–19 | Poor | Major UX overhaul required; core experience broken | -| 0–11 | Critical | Redesign needed; unusable in current state | - ---- - -## Issue Severity (P0–P3) - -Tag each individual issue found during scoring with a priority level: - -| Priority | Name | Description | Action | -|----------|------|-------------|--------| -| **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper | -| **P1** | Major | Causes significant difficulty or confusion | Fix before release | -| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass | -| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits | - -**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1. diff --git a/.trae-cn/skills/impeccable/reference/teach.md b/.trae/skills/impeccable/reference/init.md similarity index 58% rename from .trae-cn/skills/impeccable/reference/teach.md rename to .trae/skills/impeccable/reference/init.md index 7937905e9..fecb6fc7a 100644 --- a/.trae-cn/skills/impeccable/reference/teach.md +++ b/.trae/skills/impeccable/reference/init.md @@ -1,21 +1,16 @@ -# Teach Flow +# Init Flow -Gathers design context for a project and writes two complementary files at the project root: +The setup command for a project. One codebase crawl feeds everything it writes: - **PRODUCT.md** (strategic): root project file for register, target users, product purpose, brand personality, anti-references, strategic design principles. Answers "who/what/why". - **DESIGN.md** (visual): root project file for visual theme, color palette, typography, components, layout. Follows the [Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/). Answers "how it looks". +- **`.impeccable/live/config.json`** (live mode): pre-configured so `/impeccable live` boots straight into variant mode with no first-time detour. -Every other impeccable command reads these files before doing any work. +It closes by pointing the user at the best command to run next. Every other impeccable command reads PRODUCT.md and DESIGN.md before doing any work. ## Step 1: Load current state -Run the shared loader first so you know what already exists: - -```bash -node .trae-cn/skills/impeccable/scripts/load-context.mjs -``` - -The output tells you whether PRODUCT.md and/or DESIGN.md already exist. If `migrated: true`, legacy `.impeccable.md` was auto-renamed to `PRODUCT.md`. Mention this once to the user. +Check what already exists. PRODUCT.md and DESIGN.md live at the project root, or under `.agents/context/` or `docs/` (case-insensitive). Read whichever are present with your native file tool. Also note whether `.impeccable/live/config.json` already exists (Step 6 leaves it untouched if so). Decision tree: - **Neither file exists (empty project or no context yet)**: do Steps 2-4 (write PRODUCT.md), then decide on DESIGN.md based on whether there's code to analyze. @@ -26,14 +21,14 @@ Decision tree: Never silently overwrite an existing file. Always confirm first. -If teach was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete teach, re-run the loader, then resume the original command with the freshly loaded context. For craft, resume into shape next; teach creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief. +If init was invoked as a setup blocker by another command, such as `/impeccable craft landing page`, pause that command here. Complete init, then resume the original command. Your own writes are the freshest source; no reload needed. For craft, resume into shape next; init creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief. ## Step 2: Explore the codebase -Before asking questions, thoroughly scan the project to discover what you can: +Before asking questions, thoroughly scan the project to discover what you can. This single crawl feeds PRODUCT.md, DESIGN.md, **and** the live-mode framework detection in Step 6, so be thorough once rather than re-scanning later: - **README and docs**: Project purpose, target audience, any stated goals -- **Package.json / config files**: Tech stack, dependencies, existing design libraries +- **Package.json / config files**: Tech stack, dependencies, existing design libraries, **and the framework** (Vite/SPA, Next.js, Nuxt, SvelteKit, Astro, multi-page static) plus the HTML entry the browser actually loads - **Existing components**: Current design patterns, spacing, typography in use - **Brand assets**: Logos, favicons, color values already defined - **Design tokens / CSS variables**: Existing color palettes, font stacks, spacing scales @@ -46,7 +41,7 @@ Also form a **register hypothesis** from what you find: Register is a hypothesis at this point, not a decision; Step 3 confirms it. -Note what you've learned and what remains unclear. This exploration feeds both PRODUCT.md and DESIGN.md. +Note what you've learned and what remains unclear. Also note any rough edges worth a follow-up command (thin hierarchy, flat or gray palette, missing error/empty states, dull copy); Step 7 turns these into concrete recommendations without re-analyzing. ## Step 3: Ask strategic questions (for PRODUCT.md) @@ -84,8 +79,7 @@ If the signal is genuinely split (e.g. a product with a big marketing landing), ### Brand & Personality - How would you describe the brand personality in 3 words? - Reference sites or apps that capture the right feel? What specifically about them? - - For brand, push for real-world references in the right lane (tech-minimal, editorial-magazine, consumer-warm, brutalist-grid, etc.), not generic "modern" adjectives. - - For product, push for category best-tool references (Linear, Figma, Notion, Raycast, Stripe). + - Push for specific named references with the *specific* thing about them that fits this brand, not generic "modern" adjectives or category-bucket lanes. - What should this explicitly NOT look like? Any anti-references? ### Accessibility & Inclusion @@ -141,16 +135,38 @@ If the user agrees, delegate to `/impeccable document` (it auto-detects scan vs If the user prefers to skip, mention they can run `/impeccable document` any time later. -## Step 6: Confirm and wrap up +## Step 6: Configure live mode (when code exists) -Summarize: +If the project has code with HTML entries and a dev server (the same "code exists" condition that puts `/impeccable document` in scan mode), pre-configure live mode now. You already identified the framework and the served HTML entry in Step 2, so this is nearly free, and it spares the user the first-time setup detour when they later run `/impeccable live`. + +**Skip this step for empty / pre-implementation projects** (nothing to inject into yet). Tell the user live mode will configure itself the first time they run it once there's code. + +**If `.impeccable/live/config.json` already exists, leave it untouched** and note that live mode is already configured. + +Otherwise: + +1. Write `.impeccable/live/config.json`. Choose `files` (the HTML entries the browser actually loads), `insertBefore`, and `commentSyntax` from the framework table in [live.md](live.md)'s **First-time setup** section, using the framework you found in Step 2. That table is canonical; do not restate it here. For multi-page static sites, prefer a glob (`["public/**/*.html"]`) over a literal list. +2. Run `node .trae/skills/impeccable/scripts/detect-csp.mjs`. If it reports a patchable shape (`append-arrays` / `append-string`), use the **consent prompt template** from live.md before editing any source file. On decline, skip the patch. For `middleware` / `meta-tag` shapes, surface the detected files and ask the user to add `http://localhost:8400` to `script-src` and `connect-src` manually. For `null`, there's nothing to do. +3. Set `cspChecked: true` in the config once CSP is handled (patched, declined, manual, or not needed). The schema and per-shape patch details live in live.md's First-time setup; follow it rather than duplicating. + +Writing the config file is harmless and needs no consent; only the CSP **source-file patch** requires a yes. + +## Step 7: Recommend starting points, then wrap up + +Summarize tersely: - Register captured (brand / product) -- What was written (PRODUCT.md, DESIGN.md, or both) +- What was written (PRODUCT.md, DESIGN.md, live config, or a subset) - The 3-5 strategic principles from PRODUCT.md that will guide future work -- If DESIGN.md is pending, remind the user how to generate it later +- If DESIGN.md or live config is pending, one line on how to set it up later -**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node .trae-cn/skills/impeccable/scripts/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. +Then recommend the **best commands to run next**, drawn from what your Step 2 crawl already surfaced. Do not run a fresh analysis here; surface observations you already have. Tailor to register and to what you saw, offer the 2-4 most relevant (not a menu dump), and give the exact command to type. Group by intent: -If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context. +- **Build something new**: `/impeccable craft ` (shape, then build end-to-end) or `/impeccable shape ` (plan first). Lead with this for empty or early-stage projects. +- **Improve what's there**: name the specific surface. `/impeccable critique ` for a scored UX review; `/impeccable audit ` for a11y / perf / responsive checks; `/impeccable polish ` for a pre-ship pass. When the crawl flagged a specific weakness, point the matching command at it: thin hierarchy or spacing → `layout`, flat or gray palette → `colorize`, missing error / empty states → `harden` or `onboard`, dull or unclear copy → `clarify`. +- **Iterate visually**: `/impeccable live` (configured in Step 6) to pick elements in the browser and generate variants in place. + +The full command menu is one bare `/impeccable` away; keep this list short and pointed. + +If init was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now. Your own writes are the freshest source; no reload needed. Optionally ask the user directly to clarify what you cannot infer. Ask whether they'd like a brief summary of PRODUCT.md appended to RULES.md for easier agent reference. If yes, append a short **Design Context** pointer section there. diff --git a/.trae/skills/impeccable/reference/interaction-design.md b/.trae/skills/impeccable/reference/interaction-design.md index 15aed5b29..c42c9d810 100644 --- a/.trae/skills/impeccable/reference/interaction-design.md +++ b/.trae/skills/impeccable/reference/interaction-design.md @@ -150,12 +150,6 @@ For browsers without anchor positioning support, `position: fixed` with manual c Check viewport boundaries before rendering. If the dropdown would overflow the bottom edge, flip it above the trigger. If it would overflow the right edge, align it to the trigger's right side instead. -### Anti-Patterns - -- **`position: absolute` inside `overflow: hidden`** - The dropdown will be clipped. Use `position: fixed` or the top layer instead. -- **Arbitrary z-index values** like `z-index: 9999` - Use a semantic z-index scale: `dropdown (100) -> sticky (200) -> modal-backdrop (300) -> modal (400) -> toast (500) -> tooltip (600)`. -- **Rendering dropdown markup inline** without an escape hatch from the parent's stacking context. Either use `popover` (top layer), a portal, or `position: fixed`. - ## Destructive Actions: Undo > Confirm **Undo is better than confirmation dialogs.** Users click through confirmations mindlessly. Remove from UI immediately, show undo toast, actually delete after toast expires. Use confirmation only for truly irreversible actions (account deletion), high-cost actions, or batch operations. diff --git a/.trae/skills/impeccable/reference/layout.md b/.trae/skills/impeccable/reference/layout.md index cad4c62a1..8765978b0 100644 --- a/.trae/skills/impeccable/reference/layout.md +++ b/.trae/skills/impeccable/reference/layout.md @@ -1,4 +1,4 @@ -Space is the most underused design tool. Find the layout's actual problem (monotone spacing, weak hierarchy, identical card grids, the centered-stack default) and fix the structure, not the surface. +Space is the most underused design tool. Find the layout's actual problem (monotone spacing, weak hierarchy, identical card grids) and fix the structure, not the surface. --- @@ -27,7 +27,6 @@ Analyze what's weak about the current spatial design: 3. **Grid & structure**: - Is there a clear underlying structure, or does the layout feel random? - Are identical card grids used everywhere? (Icon + heading + text, repeated endlessly) - - Is everything centered? (Left-aligned with asymmetric layouts feels more designed, but not a hard and fast rule) 4. **Rhythm & variety**: - Does the layout have visual rhythm? (Alternating tight/generous spacing) @@ -43,8 +42,6 @@ Analyze what's weak about the current spatial design: ## Plan Layout Improvements -Consult the [spatial design reference](spatial-design.md) for detailed guidance on grids, rhythm, and container queries. - Create a systematic plan: - **Spacing system**: Use a consistent scale (a framework's built-in scale like Tailwind's, rem-based tokens, or a custom system). The specific values matter less than consistency. @@ -57,6 +54,7 @@ Create a systematic plan: ### Establish a Spacing System - Use a consistent spacing scale (framework scales like Tailwind, rem-based tokens, or a custom scale all work). What matters is that values come from a defined set, not arbitrary numbers. +- Prefer a 4pt base scale (4, 8, 12, 16, 24, 32, 48, 64, 96px) over 8pt; 8pt is too coarse and you'll frequently need 12px between 8 and 16. - Name tokens semantically if using custom properties: `--space-xs` through `--space-xl`, not `--spacing-8` - Use `gap` for sibling spacing instead of margins; eliminates margin collapse hacks - Apply `clamp()` for fluid spacing that breathes on larger screens @@ -66,15 +64,22 @@ Create a systematic plan: - **Tight grouping** for related elements (8-12px between siblings) - **Generous separation** between distinct sections (48-96px) - **Varied spacing** within sections (not every row needs the same gap) -- **Asymmetric compositions**: break the predictable centered-content pattern when it makes sense +- **Asymmetric compositions**: a deliberate choice when the content invites it (not a default to chase). ### Choose the Right Layout Tool -- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. Flex is simpler and more appropriate for the majority of layout tasks. +- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. - **Use Grid for 2D layouts**: Page-level structure, dashboards, data-dense interfaces, anything where rows AND columns need coordinated control. -- **Don't default to Grid** when Flexbox with `flex-wrap` would be simpler and more flexible. -- Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. - Use named grid areas (`grid-template-areas`) for complex page layouts; redefine at breakpoints. +- Use **container queries** for components, viewport queries for page layouts. A card in a narrow sidebar can stay compact while the same card in a main content area expands automatically: + +```css +.card-container { container-type: inline-size; } +.card { display: grid; gap: var(--space-md); } +@container (min-width: 400px) { + .card { grid-template-columns: 120px 1fr; } +} +``` ### Break Card Grid Monotony @@ -85,18 +90,36 @@ Create a systematic plan: ### Strengthen Visual Hierarchy - Use the fewest dimensions needed for clear hierarchy. Space alone can be enough; generous whitespace around an element draws the eye. Some of the most polished designs achieve rhythm with just space and weight. Add color or size contrast only when simpler means aren't sufficient. +- The best hierarchy combines 2–3 dimensions at once. A heading that's larger, bolder, AND has more space above it reads as primary without trying: + +| Tool | Strong Hierarchy | Weak Hierarchy | +|------|------------------|----------------| +| **Size** | 3:1 ratio or more | <2:1 ratio | +| **Weight** | Bold vs Regular | Medium vs Regular | +| **Color** | High contrast | Similar tones | +| **Position** | Top/left (primary) | Bottom/right | +| **Space** | Surrounded by white space | Crowded | + - Be aware of reading flow: in LTR languages, the eye naturally scans top-left to bottom-right, but primary action placement depends on context (e.g., bottom-right in dialogs, top in navigation). - Create clear content groupings through proximity and separation. ### Manage Depth & Elevation -- Create a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip) - Build a consistent shadow scale (sm → md → lg → xl); shadows should be subtle - Use elevation to reinforce hierarchy, not as decoration ### Optical Adjustments - If an icon looks visually off-center despite being geometrically centered, nudge it. But only if you're confident it actually looks wrong. Don't adjust speculatively. +- Text at `margin-left: 0` looks slightly indented because of letterform whitespace; a negative margin (`-0.05em`) optically aligns it. Geometrically centered glyphs often look off-center (play icons need to shift right, arrows shift toward their direction). +- Touch targets must be 44×44px minimum even when the visual element is smaller. Expand the hit area with padding or a pseudo-element: + +```css +.icon-button { width: 24px; height: 24px; position: relative; } +.icon-button::before { + content: ''; position: absolute; inset: -10px; +} +``` **NEVER**: - Use arbitrary spacing values outside your scale @@ -104,10 +127,7 @@ Create a systematic plan: - Wrap everything in cards (not everything needs a container) - Nest cards inside cards (use spacing and dividers for hierarchy within) - Use identical card grids everywhere (icon + heading + text, repeated) -- Center everything (left-aligned with asymmetry feels more designed) - Default to the hero metric layout (big number, small label, stats, gradient) as a template. If showing real user data, a prominent metric can work, but it should display actual data, not decorative numbers. -- Default to CSS Grid when Flexbox would be simpler; use the simplest tool for the job -- Use arbitrary z-index values (999, 9999); build a semantic scale ## Verify Layout Improvements diff --git a/.trae/skills/impeccable/reference/live.md b/.trae/skills/impeccable/reference/live.md index 37bb36c1c..3b5b2d608 100644 --- a/.trae/skills/impeccable/reference/live.md +++ b/.trae/skills/impeccable/reference/live.md @@ -9,18 +9,21 @@ A running dev server with hot module replacement (Vite, Next.js, Bun, etc.), OR Execute in order. No step skipped, no step reordered. 1. `live.mjs`: boot. -2. Navigate to the URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). If you can't infer it confidently, tell the user once to open their dev/preview URL. Never use `serverPort` as that URL; it's the helper, not the app. +2. Open the app URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). Never use `serverPort`; it's the helper, not the app. **Cursor:** `browser_navigate` to that URL before polling; do not skip. **Other harnesses:** use the available browser tool; if the URL is uncertain, ask the user once. 3. Poll loop with the default long timeout (600000 ms). After every event or `--reply`, run `live-poll.mjs` again immediately. Never pass a short `--timeout=`. + +The global bar **Impeccable mark** dims and shows a pulsing amber dot when no agent is long-polling `/poll`. Hover the mark for the hint; restart `live-poll.mjs` to reconnect. 4. On `generate`: read screenshot if present; load the action's reference; plan three distinct directions; write all variants in one edit; `--reply done`; poll again. -5. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts remain recoverable until you finish cleanup, run `live-complete.mjs --id EVENT_ID`, and only then poll again. -6. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart. -7. On `exit`: run the cleanup at the bottom. +5. On `steer`: read the message and `pageUrl`; do the work (page edits, navigation help, or a short reply in the `--reply` message); `--reply steer_done`; poll again. No pickup ack. The Steer bar unlocks when `steer_done` arrives over SSE. +6. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts remain recoverable until you finish cleanup, run `live-complete.mjs --id EVENT_ID`, and only then poll again. +7. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart. +8. On `exit`: run the cleanup at the bottom. Harness policy: - **Claude Code**: run the poll as a **background task** (no short timeout). The harness notifies you when it completes, so the main conversation stays free. Do not block the shell. -- **Cursor**: run the poll in the **foreground** (blocking shell; not a background terminal, not a subagent). Cursor background terminals and subagents do not reliably resume the chat with poll stdout. +- **Cursor**: run **one-shot** poll in a **background terminal** with notify on `"type":"(steer|generate|accept|discard|exit)"`. After each event the poll exits; handle it, `--reply`, then start `live-poll.mjs` again. Do **not** use `--stream` on Cursor: incremental stdout notify is slower in practice than exit-based notify (~5s vs sub-second in testing). - **Codex**: run the poll in the **foreground** (blocking shell; not a background task, not a subagent). Codex background exec sessions do not reliably surface poll stdout back into the conversation at the moment events arrive, so a "fire-and-forget" background poll will stall live mode. -- **Other harnesses**: foreground unless you know stdout reliably returns to this session. +- **Other harnesses**: one-shot foreground unless you know stdout reliably returns to this session when a shell exits. Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodies. Spend tokens on tools and edits; on failure, one or two short sentences. @@ -30,7 +33,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi node .trae/skills/impeccable/scripts/live.mjs ``` -Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation; **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md. +Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation; **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. `serverPort` and `serverToken` belong to the small **Impeccable live helper** HTTP server (serves `/live.js`, SSE, and `/poll`). That port is **not** your dev server and is usually not the URL you open to view the app. The browser page is whatever origin serves one of the `pageFiles` entries (Vite / Next / Bun / tunnel / LAN hostname). @@ -38,12 +41,15 @@ If output is `{ ok: false, error: "config_missing" | "config_invalid", path }`, ## Poll loop +**Default (portable, all harnesses):** + ``` LOOP: node .trae/skills/impeccable/scripts/live-poll.mjs # default long timeout; no --timeout= Read JSON; dispatch on "type" "generate" → Handle Generate; reply done; LOOP + "steer" → Handle Steer; reply steer_done; LOOP "accept" → Handle Accept; complete carbonize cleanup if required; LOOP "discard" → Handle Discard; LOOP "prefetch" → Handle Prefetch; LOOP @@ -51,6 +57,16 @@ LOOP: "exit" → break → Cleanup ``` +**Stream mode (experimental, not for Cursor):** + +``` +node .trae/skills/impeccable/scripts/live-poll.mjs --stream # stays running; one JSON line per event + Handle event; run --reply in a separate command + Repeat until "exit" line → Cleanup +``` + +Stream keeps one process alive and waits for `--reply` ack before polling again. Useful only when the harness reads incremental stdout reliably and quickly. **Cursor is not one of those:** background pattern notify on a long-running shell was ~5s to pick up events vs sub-second for one-shot exit notify. Default to one-shot everywhere unless you have measured otherwise. + ## Recovery commands The live helper persists an append-only journal under `.impeccable/live/sessions/`. Browser checkpoints are advisory but durable; the journal is canonical. This is local durable recovery state, not project source. @@ -71,9 +87,32 @@ Server restart rule: start `live-server.mjs` again, then poll. Startup requeues ## Handle `generate` -Event: `{id, action, freeformPrompt?, count, pageUrl, element, screenshotPath?, comments?, strokes?}`. +**Replace mode** (default): `{id, action, freeformPrompt?, count, pageUrl, element, screenshotPath?, comments?, strokes?}`. -Speed matters; the user is watching a spinner. Minimize tool calls by using the `wrap` helper and writing all variants in a single edit. +**Insert mode** (`event.mode === "insert"`): `{id, mode: "insert", count, pageUrl, insert: { position, anchor }, placeholder: { width, height }, freeformPrompt?, screenshotPath?, comments?, strokes?}`. No `action`. Requires a non-empty `freeformPrompt` **or** annotations. Screenshot is sent only when annotations exist (same rule as replace). Use `placeholder` dimensions as a soft size hint for net-new content. + +Speed matters; the user is watching a spinner. Minimize tool calls by using the wrap/insert helper and writing all variants in a single edit. + +### Insert mode branch + +When `event.mode === "insert"`: + +1. Read the screenshot if `event.screenshotPath` is present (annotations only). +2. Run the insert helper instead of wrap: + +```bash +node .trae/skills/impeccable/scripts/live-insert.mjs --id EVENT_ID --count EVENT_COUNT --position after \ + --element-id "ANCHOR_ID" --classes "class1,class2" --tag "section" --text "ANCHOR_TEXT" +``` + +- `--position` ← `event.insert.position` (`before` | `after`) +- Anchor flags ← `event.insert.anchor` (same mapping as wrap: id, classes, tag, text) + +The scaffold has **no** `data-impeccable-variant="original"`. Variants are net-new HTML+CSS inserted at `insertLine`. Load `brand.md` or `product.md` (freeform only, no action sub-command). Write all variants in one edit, then `--reply done`. + +On accept/discard, `live-accept.mjs` removes the wrapper block; the anchor element is untouched. + +### Replace mode (default) ### 1. Read the screenshot (if present) @@ -424,6 +463,28 @@ A background agent may be used for the rewrite, but the current thread is respon Event: `{id, _acceptResult, _completionAck}`. The poll script already restored the original, removed all variant markers, and acknowledged `discarded` durable completion. Nothing to do unless `_completionAck.ok !== true`; in that case run `live-complete.mjs --id EVENT_ID --discarded`, then poll again. +## Handle `steer` + +Event: `{id, message, pageUrl}`. The user typed or spoke into the global bar **Steer** control: page-level direction without picking an element or launching variant generation. + +The mic button uses the browser **Web Speech API** (MVP): click to start, speak, stop automatically when the utterance ends, then the transcript submits as a steer event. Click again while listening to cancel without submitting. + +This is lighter than `generate`: no screenshot, no element context, no variant cycling. Read `message` and inspect the live page or project files as needed, then either make edits or answer in prose. + +When finished: + +```bash +node .trae/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID steer_done ["Optional short note for a browser toast"] +``` + +On failure: + +```bash +node .trae/skills/impeccable/scripts/live-poll.mjs --reply EVENT_ID error "Short reason" +``` + +Then poll again immediately. Do not send a separate "picked up" reply. The Steer bar stays locked until `steer_done` or `error` arrives over SSE. + ## Handle `prefetch` Event: `{pageUrl}`. The browser fires this the first time the user selects an element on a given route, as a latency shortcut; it signals the user is likely about to Go on a page you haven't read yet. diff --git a/.trae/skills/impeccable/reference/motion-design.md b/.trae/skills/impeccable/reference/motion-design.md deleted file mode 100644 index 78b6fd260..000000000 --- a/.trae/skills/impeccable/reference/motion-design.md +++ /dev/null @@ -1,109 +0,0 @@ -# Motion Design - -## Duration: The 100/300/500 Rule - -Timing matters more than easing. These durations feel right for most UI: - -| Duration | Use Case | Examples | -|----------|----------|----------| -| **100-150ms** | Instant feedback | Button press, toggle, color change | -| **200-300ms** | State changes | Menu open, tooltip, hover states | -| **300-500ms** | Layout changes | Accordion, modal, drawer | -| **500-800ms** | Entrance animations | Page load, hero reveals | - -**Exit animations are faster than entrances.** Use ~75% of enter duration. - -## Easing: Pick the Right Curve - -**Don't use `ease`.** It's a compromise that's rarely optimal. Instead: - -| Curve | Use For | CSS | -|-------|---------|-----| -| **ease-out** | Elements entering | `cubic-bezier(0.16, 1, 0.3, 1)` | -| **ease-in** | Elements leaving | `cubic-bezier(0.7, 0, 0.84, 0)` | -| **ease-in-out** | State toggles (there → back) | `cubic-bezier(0.65, 0, 0.35, 1)` | - -**For micro-interactions, use exponential curves.** They feel natural because they mimic real physics (friction, deceleration): - -```css -/* Quart out - smooth, refined (recommended default) */ ---ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1); - -/* Quint out - slightly more dramatic */ ---ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1); - -/* Expo out - snappy, confident */ ---ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); -``` - -**Avoid bounce and elastic curves.** They were trendy in 2015 but now feel tacky and amateurish. Real objects don't bounce when they stop; they decelerate smoothly. Overshoot effects draw attention to the animation itself rather than the content. - -## Premium Motion Materials - -Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties: blur reveals, backdrop-filter panels, saturation or brightness shifts, shadow bloom, SVG filters, masks, clip paths, gradient-position movement, and variable font or shader-driven effects. - -Use the right material for the effect: - -- **Transform / opacity**: movement, press feedback, simple reveals, list choreography. -- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances, atmospheric transitions. -- **Clip path / masks**: wipes, reveals, editorial cropping, product-like transitions. -- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state. -- **Grid-template rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly. - -The hard rule is not "transform and opacity only." The hard rule is: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify in-browser that the result is smooth on the target viewports. If blur/filter makes the interaction feel significantly more premium and remains smooth, use it. - -## Staggered Animations - -Use CSS custom properties for cleaner stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"` on each item. **Cap total stagger time**: 10 items at 50ms = 500ms total. For many items, reduce per-item delay or cap staggered count. - -## Reduced Motion - -This is not optional. Vestibular disorders affect ~35% of adults over 40. - -```css -/* Define animations normally */ -.card { - animation: slide-up 500ms ease-out; -} - -/* Provide alternative for reduced motion */ -@media (prefers-reduced-motion: reduce) { - .card { - animation: fade-in 200ms ease-out; /* Crossfade instead of motion */ - } -} - -/* Or disable entirely */ -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - transition-duration: 0.01ms !important; - } -} -``` - -**What to preserve**: Functional animations like progress bars, loading spinners (slowed down), and focus indicators should still work, just without spatial movement. - -## Perceived Performance - -**Nobody cares how fast your site is, just how fast it feels.** Perception can be as effective as actual performance. - -**The 80ms threshold**: Our brains buffer sensory input for ~80ms to synchronize perception. Anything under 80ms feels instant and simultaneous. This is your target for micro-interactions. - -**Active vs passive time**: Passive waiting (staring at a spinner) feels longer than active engagement. Strategies to shift the balance: - -- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening. -- **Early completion**: Show content progressively, don't wait for everything. Video buffering, progressive images, streaming HTML. -- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Instagram likes work offline; the UI updates instantly, syncs later. Use for low-stakes actions; avoid for payments or destructive operations. - -**Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances, but ease-in toward a task's end compresses perceived time. - -**Caution**: Too-fast responses can decrease perceived value. Users may distrust instant results for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening. - -## Performance - -Don't use `will-change` preemptively, only when animation is imminent (`:hover`, `.animating`). For scroll-triggered animations, use Intersection Observer instead of scroll events; unobserve after animating once. Create motion tokens for consistency (durations, easings, common transitions). - ---- - -**Avoid**: Animating everything (animation fatigue is real). Using >500ms for UI feedback. Ignoring `prefers-reduced-motion`. Using animation to hide slow loading. diff --git a/.trae/skills/impeccable/reference/personas.md b/.trae/skills/impeccable/reference/personas.md deleted file mode 100644 index c5fe18ef1..000000000 --- a/.trae/skills/impeccable/reference/personas.md +++ /dev/null @@ -1,179 +0,0 @@ -# Persona-Based Design Testing - -Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss. - -**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns. - ---- - -## 1. Impatient Power User: "Alex" - - -**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave. - -**Behaviors**: -- Skips all onboarding and instructions -- Looks for keyboard shortcuts immediately -- Tries to bulk-select, batch-edit, and automate -- Gets frustrated by required steps that feel unnecessary -- Abandons if anything feels slow or patronizing - -**Test Questions**: -- Can Alex complete the core task in under 60 seconds? -- Are there keyboard shortcuts for common actions? -- Can onboarding be skipped entirely? -- Do modals have keyboard dismiss (Esc)? -- Is there a "power user" path (shortcuts, bulk actions)? - -**Red Flags** (report these specifically): -- Forced tutorials or unskippable onboarding -- No keyboard navigation for primary actions -- Slow animations that can't be skipped -- One-item-at-a-time workflows where batch would be natural -- Redundant confirmation steps for low-risk actions - ---- - -## 2. Confused First-Timer: "Jordan" - -**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out. - -**Behaviors**: -- Reads all instructions carefully -- Hesitates before clicking anything unfamiliar -- Looks for help or support constantly -- Misunderstands jargon and abbreviations -- Takes the most literal interpretation of any label - -**Test Questions**: -- Is the first action obviously clear within 5 seconds? -- Are all icons labeled with text? -- Is there contextual help at decision points? -- Does terminology assume prior knowledge? -- Is there a clear "back" or "undo" at every step? - -**Red Flags** (report these specifically): -- Icon-only navigation with no labels -- Technical jargon without explanation -- No visible help option or guidance -- Ambiguous next steps after completing an action -- No confirmation that an action succeeded - ---- - -## 3. Accessibility-Dependent User: "Sam" - -**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences. - -**Behaviors**: -- Tabs through the interface linearly -- Relies on ARIA labels and heading structure -- Cannot see hover states or visual-only indicators -- Needs adequate color contrast (4.5:1 minimum) -- May use browser zoom up to 200% - -**Test Questions**: -- Can the entire primary flow be completed keyboard-only? -- Are all interactive elements focusable with visible focus indicators? -- Do images have meaningful alt text? -- Is color contrast WCAG AA compliant (4.5:1 for text)? -- Does the screen reader announce state changes (loading, success, errors)? - -**Red Flags** (report these specifically): -- Click-only interactions with no keyboard alternative -- Missing or invisible focus indicators -- Meaning conveyed by color alone (red = error, green = success) -- Unlabeled form fields or buttons -- Time-limited actions without extension option -- Custom components that break screen reader flow - ---- - -## 4. Deliberate Stress Tester: "Riley" - -**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience. - -**Behaviors**: -- Tests edge cases intentionally (empty states, long strings, special characters) -- Submits forms with unexpected data (emoji, RTL text, very long values) -- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs -- Looks for inconsistencies between what the UI promises and what actually happens -- Documents problems methodically - -**Test Questions**: -- What happens at the edges (0 items, 1000 items, very long text)? -- Do error states recover gracefully or leave the UI in a broken state? -- What happens on refresh mid-workflow? Is state preserved? -- Are there features that appear to work but produce broken results? -- How does the UI handle unexpected input (emoji, special chars, paste from Excel)? - -**Red Flags** (report these specifically): -- Features that appear to work but silently fail or produce wrong results -- Error handling that exposes technical details or leaves UI in a broken state -- Empty states that show nothing useful ("No results" with no guidance) -- Workflows that lose user data on refresh or navigation -- Inconsistent behavior between similar interactions in different parts of the UI - ---- - -## 5. Distracted Mobile User: "Casey" - -**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection. - -**Behaviors**: -- Uses thumb only; prefers bottom-of-screen actions -- Gets interrupted mid-flow and returns later -- Switches between apps frequently -- Has limited attention span and low patience -- Types as little as possible, prefers taps and selections - -**Test Questions**: -- Are primary actions in the thumb zone (bottom half of screen)? -- Is state preserved if the user leaves and returns? -- Does it work on slow connections (3G)? -- Can forms use autocomplete and smart defaults? -- Are touch targets at least 44×44pt? - -**Red Flags** (report these specifically): -- Important actions positioned at the top of the screen (unreachable by thumb) -- No state persistence; progress lost on tab switch or interruption -- Large text inputs required where selection would work -- Heavy assets loading on every page (no lazy loading) -- Tiny tap targets or targets too close together - ---- - -## Selecting Personas - -Choose personas based on the interface type: - -| Interface Type | Primary Personas | Why | -|---------------|-----------------|-----| -| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile | -| Dashboard / admin | Alex, Sam | Power users, accessibility | -| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity | -| Onboarding flow | Jordan, Casey | Confusion, interruption | -| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav | -| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile | - ---- - -## Project-Specific Personas - -If `RULES.md` contains a `## Design Context` section (generated by `impeccable teach`), derive 1–2 additional personas from the audience and brand information: - -1. Read the target audience description -2. Identify the primary user archetype not covered by the 5 predefined personas -3. Create a persona following this template: - -``` -### [Role]: "[Name]" - -**Profile**: [2-3 key characteristics derived from Design Context] - -**Behaviors**: [3-4 specific behaviors based on the described audience] - -**Red Flags**: [3-4 things that would alienate this specific user type] -``` - -Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists. diff --git a/.trae/skills/impeccable/reference/polish.md b/.trae/skills/impeccable/reference/polish.md index 9a3891540..8541f43ac 100644 --- a/.trae/skills/impeccable/reference/polish.md +++ b/.trae/skills/impeccable/reference/polish.md @@ -91,7 +91,6 @@ Visual polish on a misshapen flow is wasted work. Match the *shape* of the exper - **Theme consistency**: Works in all theme variants - **Color meaning**: Same colors mean same things throughout - **Accessible focus**: Focus indicators visible with sufficient contrast -- **Tinted neutrals**: No pure gray or pure black; add subtle color tint (0.01 chroma) - **Gray on color**: Never put gray text on colored backgrounds; use a shade of that color or transparency ### Interaction States diff --git a/.trae/skills/impeccable/reference/product.md b/.trae/skills/impeccable/reference/product.md index 64b3b0169..2ca462350 100644 --- a/.trae/skills/impeccable/reference/product.md +++ b/.trae/skills/impeccable/reference/product.md @@ -10,7 +10,6 @@ Product UI's failure mode isn't flatness, it's strangeness without purpose: over ## Typography -- **System fonts are legitimate.** `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif` gives you native feel on every platform. Inter is the common cross-platform default for a reason. - **One family is often right.** Product UIs don't need display/body pairing. A well-tuned sans carries headings, buttons, labels, body, data. - **Fixed rem scale, not fluid.** Clamp-sized headings don't serve product UI. Users view at consistent DPI, and a fluid h1 that shrinks in a sidebar looks worse, not better. - **Tighter scale ratio.** 1.125–1.2 between steps is typical. More type elements here than on brand surfaces; exaggerated contrast creates noise. @@ -26,8 +25,6 @@ Product defaults to Restrained. A single surface can earn Committed (a dashboard ## Layout -- Predictable grids. Consistency IS an affordance; users navigate faster when the structure is expected. -- Familiar patterns are features. Standard navigation (top bar, side nav), breadcrumbs, tabs, and form layouts have established user expectations. Don't reinvent for flavor. - Responsive behavior is structural (collapse sidebar, responsive table, breakpoint-driven columns), not fluid typography. ## Components @@ -51,6 +48,7 @@ Every interactive component has: default, hover, focus, active, disabled, loadin - Display fonts in UI labels, buttons, data. - Reinventing standard affordances for flavor (custom scrollbars, weird form controls, non-standard modals). - Heavy color or full-saturation accents on inactive states. +- Modal as first thought. Modals are usually laziness. Exhaust inline / progressive alternatives first. ## Product permissions diff --git a/.trae/skills/impeccable/reference/responsive-design.md b/.trae/skills/impeccable/reference/responsive-design.md deleted file mode 100644 index f079e5e5d..000000000 --- a/.trae/skills/impeccable/reference/responsive-design.md +++ /dev/null @@ -1,114 +0,0 @@ -# Responsive Design - -## Mobile-First: Write It Right - -Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first. - -## Breakpoints: Content-Driven - -Don't chase device sizes; let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints. - -## Detect Input Method, Not Just Screen Size - -**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard. Use pointer and hover queries: - -```css -/* Fine pointer (mouse, trackpad) */ -@media (pointer: fine) { - .button { padding: 8px 16px; } -} - -/* Coarse pointer (touch, stylus) */ -@media (pointer: coarse) { - .button { padding: 12px 20px; } /* Larger touch target */ -} - -/* Device supports hover */ -@media (hover: hover) { - .card:hover { transform: translateY(-2px); } -} - -/* Device doesn't support hover (touch) */ -@media (hover: none) { - .card { /* No hover state - use active instead */ } -} -``` - -**Critical**: Don't rely on hover for functionality. Touch users can't hover. - -## Safe Areas: Handle the Notch - -Modern phones have notches, rounded corners, and home indicators. Use `env()`: - -```css -body { - padding-top: env(safe-area-inset-top); - padding-bottom: env(safe-area-inset-bottom); - padding-left: env(safe-area-inset-left); - padding-right: env(safe-area-inset-right); -} - -/* With fallback */ -.footer { - padding-bottom: max(1rem, env(safe-area-inset-bottom)); -} -``` - -**Enable viewport-fit** in your meta tag: -```html - -``` - -## Responsive Images: Get It Right - -### srcset with Width Descriptors - -```html -Hero image -``` - -**How it works**: -- `srcset` lists available images with their actual widths (`w` descriptors) -- `sizes` tells the browser how wide the image will display -- Browser picks the best file based on viewport width AND device pixel ratio - -### Picture Element for Art Direction - -When you need different crops/compositions (not just resolutions): - -```html - - - - ... - -``` - -## Layout Adaptation Patterns - -**Navigation**: Three stages: hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `
/` for content that can collapse on mobile. - -## Testing: Don't Trust DevTools Alone - -DevTools device emulation is useful for layout but misses: - -- Actual touch interactions -- Real CPU/memory constraints -- Network latency patterns -- Font rendering differences -- Browser chrome/keyboard appearances - -**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators. - ---- - -**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful. diff --git a/.trae/skills/impeccable/reference/shape.md b/.trae/skills/impeccable/reference/shape.md index f7c4cd2f1..592def8a7 100644 --- a/.trae/skills/impeccable/reference/shape.md +++ b/.trae/skills/impeccable/reference/shape.md @@ -151,7 +151,7 @@ How users interact with this feature. What happens on click, hover, scroll? What 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., spatial-design.md for complex layouts, motion-design.md for animated features, interaction-design.md for form-heavy features). +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. diff --git a/.trae/skills/impeccable/reference/spatial-design.md b/.trae/skills/impeccable/reference/spatial-design.md deleted file mode 100644 index 4d4b83afc..000000000 --- a/.trae/skills/impeccable/reference/spatial-design.md +++ /dev/null @@ -1,100 +0,0 @@ -# Spatial Design - -## Spacing Systems - -### Use 4pt Base, Not 8pt - -8pt systems are too coarse; you'll frequently need 12px (between 8 and 16). Use 4pt for granularity: 4, 8, 12, 16, 24, 32, 48, 64, 96px. - -### Name Tokens Semantically - -Name by relationship (`--space-sm`, `--space-lg`), not value (`--spacing-8`). Use `gap` instead of margins for sibling spacing; it eliminates margin collapse and cleanup hacks. - -## Grid Systems - -### The Self-Adjusting Grid - -Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. Columns are at least 280px, as many as fit per row, leftovers stretch. For complex layouts, use named grid areas (`grid-template-areas`) and redefine them at breakpoints. - -## Visual Hierarchy - -### The Squint Test - -Blur your eyes (or screenshot and blur). Can you still identify: -- The most important element? -- The second most important? -- Clear groupings? - -If everything looks the same weight blurred, you have a hierarchy problem. - -### Hierarchy Through Multiple Dimensions - -Don't rely on size alone. Combine: - -| Tool | Strong Hierarchy | Weak Hierarchy | -|------|------------------|----------------| -| **Size** | 3:1 ratio or more | <2:1 ratio | -| **Weight** | Bold vs Regular | Medium vs Regular | -| **Color** | High contrast | Similar tones | -| **Position** | Top/left (primary) | Bottom/right | -| **Space** | Surrounded by white space | Crowded | - -**The best hierarchy uses 2-3 dimensions at once**: A heading that's larger, bolder, AND has more space above it. - -### Cards Are Not Required - -Cards are overused. Spacing and alignment create visual grouping naturally. Use cards only when content is truly distinct and actionable, items need visual comparison in a grid, or content needs clear interaction boundaries. **Never nest cards inside cards.** Use spacing, typography, and subtle dividers for hierarchy within a card. - -## Container Queries - -Viewport queries are for page layouts. **Container queries are for components**: - -```css -.card-container { - container-type: inline-size; -} - -.card { - display: grid; - gap: var(--space-md); -} - -/* Card layout changes based on its container, not viewport */ -@container (min-width: 400px) { - .card { - grid-template-columns: 120px 1fr; - } -} -``` - -**Why this matters**: A card in a narrow sidebar stays compact, while the same card in a main content area expands automatically, without viewport hacks. - -## Optical Adjustments - -Text at `margin-left: 0` looks indented due to letterform whitespace; use negative margin (`-0.05em`) to optically align. Geometrically centered icons often look off-center; play icons need to shift right, arrows shift toward their direction. - -### Touch Targets vs Visual Size - -Buttons can look small but need large touch targets (44px minimum). Use padding or pseudo-elements: - -```css -.icon-button { - width: 24px; /* Visual size */ - height: 24px; - position: relative; -} - -.icon-button::before { - content: ''; - position: absolute; - inset: -10px; /* Expand tap target to 44px */ -} -``` - -## Depth & Elevation - -Create semantic z-index scales (dropdown → sticky → modal-backdrop → modal → toast → tooltip) instead of arbitrary numbers. For shadows, create a consistent elevation scale (sm → md → lg → xl). **Key insight**: Shadows should be subtle. If you can clearly see it, it's probably too strong. - ---- - -**Avoid**: Arbitrary spacing values outside your scale. Making all spacing equal (variety creates hierarchy). Creating hierarchy through size alone - combine size, weight, color, and space. diff --git a/.trae/skills/impeccable/reference/typeset.md b/.trae/skills/impeccable/reference/typeset.md index dd3e67ea3..5f5e0c2c0 100644 --- a/.trae/skills/impeccable/reference/typeset.md +++ b/.trae/skills/impeccable/reference/typeset.md @@ -4,7 +4,7 @@ Typography carries most of the information on the page. Replace generic defaults ## Register -Brand: run the font selection procedure in [brand.md](brand.md). Pairing follows the brand's lane (display serif + sans body for editorial/luxury, one committed sans for tech, etc.). Fluid `clamp()` scale, ≥1.25 ratio between steps. +Brand: run the font selection procedure in [brand.md](brand.md). Fluid `clamp()` scale, ≥1.25 ratio between steps. Product: system fonts and familiar sans stacks are legitimate here. One well-tuned family typically carries the whole UI. Fixed `rem` scale, 1.125–1.2 ratio between more closely-spaced steps. @@ -43,7 +43,7 @@ Analyze what's weak or generic about the current type: ## Plan Typography Improvements -Consult the [typography reference](typography.md) for detailed guidance on scales, pairing, and loading strategies. +Consult the [Reference Material](#reference-material) section below for detailed guidance on scales, pairing, and loading strategies. Create a systematic plan: @@ -122,3 +122,158 @@ Each variant MUST declare a `scale` param controlling the hierarchy ratio. Expre Where the variant riffs on a specific pairing, expose the pairing choice as a `steps` param (e.g. "serif display + sans body" vs. "mono display + sans body" vs. "all-sans"). Each branch routes through `:scope[data-p-pairing="X"]` selectors in scoped CSS. See `reference/live.md` for the full params contract. + +--- + +## Reference Material + +The sections below were previously `typography.md` and live inline now so the typeset flow has its deep typography reference in one place. `bolder.md` also references this section. + +### Typography + +#### Classic Typography Principles + +##### Vertical Rhythm + +Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony; text and space share a mathematical foundation. + +##### Modular Scale & Hierarchy + +The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy. + +**Use fewer sizes with more contrast.** A 5-size system covers most needs: + +| Role | Typical Ratio | Use Case | +|------|---------------|----------| +| xs | 0.75rem | Captions, legal | +| sm | 0.875rem | Secondary UI, metadata | +| base | 1rem | Body text | +| lg | 1.25-1.5rem | Subheadings, lead text | +| xl+ | 2-4rem | Headlines, hero text | + +Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit. + +##### Readability & Measure + +Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length: narrow columns need tighter leading, wide columns need more. + +**Non-obvious**: Light text on dark backgrounds needs compensation on three axes, not just one. Bump line-height by 0.05–0.1, add a touch of letter-spacing (0.01–0.02em), and optionally step the body weight up one notch (regular → medium). The perceived weight drops across all three; fix all three. + +**Paragraph rhythm**: Pick either space between paragraphs OR first-line indentation. Never both. Digital usually wants space; editorial/long-form can justify indent-only. + +#### Font Selection & Pairing + +The tactical selection procedure and the reflex-reject list live in [reference/brand.md](brand.md) under **Font selection procedure** and **Reflex-reject list** (loaded for brand-register tasks). The rest of this section covers the adjacent knowledge: anti-reflex corrections, system font use, and pairing rules. + +##### Anti-reflexes worth defending against + +- A technical/utilitarian brief does NOT need a serif "for warmth." Most tech tools should look like tech tools. +- An editorial/premium brief does NOT need the same expressive serif everyone is using right now. Premium can be Swiss-modern, can be neo-grotesque, can be a literal monospace, can be a quiet humanist sans. +- A children's product does NOT need a rounded display font. Kids' books use real type. +- A "modern" brief does NOT need a geometric sans. The most modern thing you can do is not use the font everyone else is using. + +**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality. + +##### Pairing Principles + +**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif). + +When pairing, contrast on multiple axes: +- Serif + Sans (structure contrast) +- Geometric + Humanist (personality contrast) +- Condensed display + Wide body (proportion contrast) + +##### Web Font Loading + +The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix: + +```css +/* 1. Use font-display: swap for visibility */ +@font-face { + font-family: 'CustomFont'; + src: url('font.woff2') format('woff2'); + font-display: swap; +} + +/* 2. Match fallback metrics to minimize shift */ +@font-face { + font-family: 'CustomFont-Fallback'; + src: local('Arial'); + size-adjust: 105%; /* Scale to match x-height */ + ascent-override: 90%; /* Match ascender height */ + descent-override: 20%; /* Match descender depth */ + line-gap-override: 10%; /* Match line spacing */ +} + +body { + font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; +} +``` + +Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically. + +**`swap` vs `optional`**: `swap` shows fallback text immediately and FOUT-swaps when the web font arrives. `optional` uses the fallback if the web font misses a small load budget (~100ms) and avoids the shift entirely. Pick `optional` when zero layout shift matters more than seeing the branded font on slow networks. + +**Preload the critical weight only**: typically the regular-weight body font used above the fold. Preloading every weight costs more bandwidth than it saves. + +**Variable fonts for 3+ weights or styles**: a single variable font file is usually smaller than three static weight files, gives fractional weight control, and pairs well with `font-optical-sizing: auto`. For 1–2 weights, static is fine. + +#### Modern Web Typography + +##### Fluid Type + +Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate (higher vw = faster scaling). Add a rem offset so it doesn't collapse to 0 on small screens. + +**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes. + +**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI; fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it. + +**Bound your clamp()**: keep `max-size ≤ ~2.5 × min-size`. Wider ratios break the browser's zoom and reflow behaviour and make large viewports feel like the page is shouting. + +**Scale container width and font-size together** so effective character measure stays in the 45–75ch band at every viewport. A heading that widens faster than its container drifts out of the comfortable measure at the top end. + +##### OpenType Features + +Most developers don't know these exist. Use them for polish: + +```css +/* Proper fractions */ +.recipe-amount { font-variant-numeric: diagonal-fractions; } + +/* Small caps for abbreviations */ +abbr { font-variant-caps: all-small-caps; } + +/* Disable ligatures in code */ +code { font-variant-ligatures: none; } + +/* Enable kerning (usually on by default, but be explicit) */ +body { font-kerning: normal; } +``` + +Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/). + +##### Rendering polish + +```css +/* Variable fonts: pick the right optical-size master automatically */ +body { font-optical-sizing: auto; } +``` + +**ALL-CAPS tracking**: capitals sit too close at default spacing. Add 5–12% letter-spacing (`letter-spacing: 0.05em` to `0.12em`) to short all-caps labels, eyebrows, and small headings. Real small caps (via `font-variant-caps`) need the same treatment, slightly gentler. + +#### Typography System Architecture + +Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system. + +#### Accessibility Considerations + +Beyond contrast ratios (which are well-documented), consider: + +- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout. +- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text. +- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile. +- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets. + +--- + +**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text. diff --git a/.trae/skills/impeccable/reference/typography.md b/.trae/skills/impeccable/reference/typography.md deleted file mode 100644 index ed5ca272a..000000000 --- a/.trae/skills/impeccable/reference/typography.md +++ /dev/null @@ -1,159 +0,0 @@ -# Typography - -## Classic Typography Principles - -### Vertical Rhythm - -Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony; text and space share a mathematical foundation. - -### Modular Scale & Hierarchy - -The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy. - -**Use fewer sizes with more contrast.** A 5-size system covers most needs: - -| Role | Typical Ratio | Use Case | -|------|---------------|----------| -| xs | 0.75rem | Captions, legal | -| sm | 0.875rem | Secondary UI, metadata | -| base | 1rem | Body text | -| lg | 1.25-1.5rem | Subheadings, lead text | -| xl+ | 2-4rem | Headlines, hero text | - -Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit. - -### Readability & Measure - -Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length: narrow columns need tighter leading, wide columns need more. - -**Non-obvious**: Light text on dark backgrounds needs compensation on three axes, not just one. Bump line-height by 0.05–0.1, add a touch of letter-spacing (0.01–0.02em), and optionally step the body weight up one notch (regular → medium). The perceived weight drops across all three; fix all three. - -**Paragraph rhythm**: Pick either space between paragraphs OR first-line indentation. Never both. Digital usually wants space; editorial/long-form can justify indent-only. - -## Font Selection & Pairing - -The tactical selection procedure and the reflex-reject list live in [reference/brand.md](brand.md) under **Font selection procedure** and **Reflex-reject list** (loaded for brand-register tasks). The rest of this section covers the adjacent knowledge: anti-reflex corrections, system font use, and pairing rules. - -### Anti-reflexes worth defending against - -- A technical/utilitarian brief does NOT need a serif "for warmth." Most tech tools should look like tech tools. -- An editorial/premium brief does NOT need the same expressive serif everyone is using right now. Premium can be Swiss-modern, can be neo-grotesque, can be a literal monospace, can be a quiet humanist sans. -- A children's product does NOT need a rounded display font. Kids' books use real type. -- A "modern" brief does NOT need a geometric sans. The most modern thing you can do is not use the font everyone else is using. - -**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality. - -### Pairing Principles - -**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif). - -When pairing, contrast on multiple axes: -- Serif + Sans (structure contrast) -- Geometric + Humanist (personality contrast) -- Condensed display + Wide body (proportion contrast) - -**Never pair fonts that are similar but not identical** (e.g., two geometric sans-serifs). They create visual tension without clear hierarchy. - -### Web Font Loading - -The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix: - -```css -/* 1. Use font-display: swap for visibility */ -@font-face { - font-family: 'CustomFont'; - src: url('font.woff2') format('woff2'); - font-display: swap; -} - -/* 2. Match fallback metrics to minimize shift */ -@font-face { - font-family: 'CustomFont-Fallback'; - src: local('Arial'); - size-adjust: 105%; /* Scale to match x-height */ - ascent-override: 90%; /* Match ascender height */ - descent-override: 20%; /* Match descender depth */ - line-gap-override: 10%; /* Match line spacing */ -} - -body { - font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; -} -``` - -Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically. - -**`swap` vs `optional`**: `swap` shows fallback text immediately and FOUT-swaps when the web font arrives. `optional` uses the fallback if the web font misses a small load budget (~100ms) and avoids the shift entirely. Pick `optional` when zero layout shift matters more than seeing the branded font on slow networks. - -**Preload the critical weight only**: typically the regular-weight body font used above the fold. Preloading every weight costs more bandwidth than it saves. - -**Variable fonts for 3+ weights or styles**: a single variable font file is usually smaller than three static weight files, gives fractional weight control, and pairs well with `font-optical-sizing: auto`. For 1–2 weights, static is fine. - -## Modern Web Typography - -### Fluid Type - -Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate (higher vw = faster scaling). Add a rem offset so it doesn't collapse to 0 on small screens. - -**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes. - -**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI; fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it. - -**Bound your clamp()**: keep `max-size ≤ ~2.5 × min-size`. Wider ratios break the browser's zoom and reflow behaviour and make large viewports feel like the page is shouting. - -**Scale container width and font-size together** so effective character measure stays in the 45–75ch band at every viewport. A heading that widens faster than its container drifts out of the comfortable measure at the top end. - -### OpenType Features - -Most developers don't know these exist. Use them for polish: - -```css -/* Tabular numbers for data alignment */ -.data-table { font-variant-numeric: tabular-nums; } - -/* Proper fractions */ -.recipe-amount { font-variant-numeric: diagonal-fractions; } - -/* Small caps for abbreviations */ -abbr { font-variant-caps: all-small-caps; } - -/* Disable ligatures in code */ -code { font-variant-ligatures: none; } - -/* Enable kerning (usually on by default, but be explicit) */ -body { font-kerning: normal; } -``` - -Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/). - -### Rendering polish - -```css -/* Even out heading line lengths (browser picks better break points) */ -h1, h2, h3 { text-wrap: balance; } - -/* Reduce orphans and ragged endings in long prose */ -article p { text-wrap: pretty; } - -/* Variable fonts: pick the right optical-size master automatically */ -body { font-optical-sizing: auto; } -``` - -**ALL-CAPS tracking**: capitals sit too close at default spacing. Add 5–12% letter-spacing (`letter-spacing: 0.05em` to `0.12em`) to short all-caps labels, eyebrows, and small headings. Real small caps (via `font-variant-caps`) need the same treatment, slightly gentler. - -## Typography System Architecture - -Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system. - -## Accessibility Considerations - -Beyond contrast ratios (which are well-documented), consider: - -- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout. -- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text. -- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile. -- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets. - ---- - -**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text. diff --git a/.trae/skills/impeccable/reference/ux-writing.md b/.trae/skills/impeccable/reference/ux-writing.md deleted file mode 100644 index 417b5f56c..000000000 --- a/.trae/skills/impeccable/reference/ux-writing.md +++ /dev/null @@ -1,107 +0,0 @@ -# UX Writing - -## The Button Label Problem - -**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns: - -| Bad | Good | Why | -|-----|------|-----| -| OK | Save changes | Says what will happen | -| Submit | Create account | Outcome-focused | -| Yes | Delete message | Confirms the action | -| Cancel | Keep editing | Clarifies what "cancel" means | -| Click here | Download PDF | Describes the destination | - -**For destructive actions**, name the destruction: -- "Delete" not "Remove" (delete is permanent, remove implies recoverable) -- "Delete 5 items" not "Delete selected" (show the count) - -## Error Messages: The Formula - -Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input". - -### Error Message Templates - -| Situation | Template | -|-----------|----------| -| **Format error** | "[Field] needs to be [format]. Example: [example]" | -| **Missing required** | "Please enter [what's missing]" | -| **Permission denied** | "You don't have access to [thing]. [What to do instead]" | -| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." | -| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" | - -### Don't Blame the User - -Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date". - -## Empty States Are Opportunities - -Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items". - -## Voice vs Tone - -**Voice** is your brand's personality, consistent everywhere. -**Tone** adapts to the moment. - -| Moment | Tone Shift | -|--------|------------| -| Success | Celebratory, brief: "Done! Your changes are live." | -| Error | Empathetic, helpful: "That didn't work. Here's what to try..." | -| Loading | Reassuring: "Saving your work..." | -| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | - -**Never use humor for errors.** Users are already frustrated. Be helpful, not cute. - -## Writing for Accessibility - -**Link text** must have standalone meaning: "View pricing plans" not "Click here". **Alt text** describes information, not the image: "Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context. - -## Writing for Translation - -### Plan for Expansion - -German text is ~30% longer than English. Allocate space: - -| Language | Expansion | -|----------|-----------| -| German | +30% | -| French | +20% | -| Finnish | +30-40% | -| Chinese | -30% (fewer chars, but same width) | - -### Translation-Friendly Patterns - -Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear. - -## Consistency: The Terminology Problem - -Pick one term and stick with it: - -| Inconsistent | Consistent | -|--------------|------------| -| Delete / Remove / Trash | Delete | -| Settings / Preferences / Options | Settings | -| Sign in / Log in / Enter | Sign in | -| Create / Add / New | Create | - -Build a terminology glossary and enforce it. Variety creates confusion. - -## Avoid Redundant Copy - -If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well. - -## Loading States - -Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress. - -## Confirmation Dialogs: Use Sparingly - -Most confirmation dialogs are design failures; consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No"). - -## Form Instructions - -Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking. - ---- - -**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors. diff --git a/.trae/skills/impeccable/scripts/cleanup-deprecated.mjs b/.trae/skills/impeccable/scripts/cleanup-deprecated.mjs index 6aee4710a..bc6400e90 100644 --- a/.trae/skills/impeccable/scripts/cleanup-deprecated.mjs +++ b/.trae/skills/impeccable/scripts/cleanup-deprecated.mjs @@ -25,7 +25,7 @@ import { join, resolve } from 'node:path'; const DEPRECATED_NAMES = [ // v2.0 renames 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable teach + 'teach-impeccable', // folded into /impeccable init // v2.1 merges 'arrange', // renamed to layout 'normalize', // merged into polish diff --git a/.trae/skills/impeccable/scripts/command-metadata.json b/.trae/skills/impeccable/scripts/command-metadata.json index 1488bd5f1..83796d54b 100644 --- a/.trae/skills/impeccable/scripts/command-metadata.json +++ b/.trae/skills/impeccable/scripts/command-metadata.json @@ -3,8 +3,8 @@ "description": "Full confirmed-brief-then-build flow. Runs multi-round shape discovery first, resolves visual probe and north-star mock gates when available, then builds and visually iterates. Use when building a new feature end-to-end.", "argumentHint": "[feature description]" }, - "teach": { - "description": "Gathers design context for a project. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.", + "init": { + "description": "Sets up a project for impeccable. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles); offers DESIGN.md (visual: colors, typography, components) when code exists; pre-configures live mode; then recommends the best commands to run next. Every other command reads these files before doing work. Use once per project.", "argumentHint": "" }, "document": { diff --git a/.trae/skills/impeccable/scripts/context.mjs b/.trae/skills/impeccable/scripts/context.mjs new file mode 100644 index 000000000..52263c883 --- /dev/null +++ b/.trae/skills/impeccable/scripts/context.mjs @@ -0,0 +1,253 @@ +/** + * Context loader: prints PRODUCT.md (and DESIGN.md if present) as one + * markdown block on stdout, or exits with empty stdout when no PRODUCT.md + * is found anywhere. The skill keys off "empty stdout" to branch into the + * init flow. + * + * Path resolution (first match wins): + * 1. cwd, if PRODUCT.md or DESIGN.md is there + * 2. .agents/context/ then docs/ + * 3. $IMPECCABLE_CONTEXT_DIR (absolute or cwd-relative) — power-user + * escape hatch, only consulted when defaults are empty + * 4. cwd as a "nothing found" default + * + * `resolveContextDir()` and `loadContext()` are also exported for the + * server-side scripts (live.mjs, live-server.mjs) that need the structured + * shape rather than the markdown block. + */ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; +const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; +const FALLBACK_DIRS = ['.agents/context', 'docs']; + +// ─── Update check ────────────────────────────────────────────────────────── +// Piggyback a lightweight skill-version check on the once-per-session boot. +// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent +// can offer `npx impeccable skills update`. Everything here is best-effort and +// silent on failure: a network problem, sandbox, or missing cache must never +// block context output or print an error. + +const UPDATE_HOST = (process.env.IMPECCABLE_UPDATE_HOST || 'https://impeccable.style').replace(/\/$/, ''); +const UPDATE_CACHE_PATH = + process.env.IMPECCABLE_UPDATE_CACHE || path.join(os.homedir(), '.impeccable', 'update-check.json'); +const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // throttle the network poll to once a day +const RENOTIFY_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000; // don't re-surface the same version for a week +const FETCH_TIMEOUT_MS = 1200; + +export function resolveContextDir(cwd = process.cwd()) { + if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { + return cwd; + } + for (const rel of FALLBACK_DIRS) { + const candidate = path.resolve(cwd, rel); + if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { + return candidate; + } + } + const envDir = process.env.IMPECCABLE_CONTEXT_DIR; + if (envDir && envDir.trim()) { + const trimmed = envDir.trim(); + return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); + } + return cwd; +} + +export function loadContext(cwd = process.cwd()) { + const contextDir = resolveContextDir(cwd); + const productPath = firstExisting(contextDir, PRODUCT_NAMES); + const designPath = firstExisting(contextDir, DESIGN_NAMES); + const product = productPath ? safeRead(productPath) : null; + const design = designPath ? safeRead(designPath) : null; + return { + hasProduct: !!product, + product, + productPath: productPath ? path.relative(cwd, productPath) : null, + hasDesign: !!design, + design, + designPath: designPath ? path.relative(cwd, designPath) : null, + contextDir, + }; +} + +function firstExisting(dir, names) { + for (const name of names) { + const abs = path.join(dir, name); + if (fs.existsSync(abs)) return abs; + } + return null; +} + +function safeRead(p) { + try { + return fs.readFileSync(p, 'utf-8'); + } catch { + return null; + } +} + +/** + * Pull the register (`brand` or `product`) out of PRODUCT.md by looking + * for a `## Register` section and reading the first non-empty line that + * follows it. Returns null when the file is legacy / register-less. + */ +function extractRegister(product) { + if (!product) return null; + const lines = product.split('\n'); + for (let i = 0; i < lines.length; i++) { + if (/^##\s+Register\b/i.test(lines[i].trim())) { + for (let j = i + 1; j < lines.length; j++) { + const next = lines[j].trim(); + if (!next) continue; + const word = next.toLowerCase(); + if (word === 'brand' || word === 'product') return word; + return null; + } + } + } + return null; +} + +/** + * Read the installed skill's own version from the sibling SKILL.md frontmatter + * (this file lives at `/scripts/context.mjs`). Returns null when the + * frontmatter is missing or unreadable. + */ +function readLocalSkillVersion() { + try { + const here = path.dirname(fileURLToPath(import.meta.url)); + const skillMd = path.join(here, '..', 'SKILL.md'); + const content = fs.readFileSync(skillMd, 'utf-8'); + const match = content.match(/^version:\s*(.+)$/m); + return match ? match[1].trim().replace(/^["']|["']$/g, '') : null; + } catch { + return null; + } +} + +function readUpdateCache() { + try { + return JSON.parse(fs.readFileSync(UPDATE_CACHE_PATH, 'utf-8')); + } catch { + return {}; + } +} + +function writeUpdateCache(cache) { + try { + fs.mkdirSync(path.dirname(UPDATE_CACHE_PATH), { recursive: true }); + fs.writeFileSync(UPDATE_CACHE_PATH, JSON.stringify(cache)); + } catch { + // Best-effort: a read-only home dir just means we re-poll next session. + } +} + +/** Compare dotted numeric versions. Returns >0 when a is newer than b. */ +function compareSemver(a, b) { + const pa = String(a).split('.').map(n => parseInt(n, 10) || 0); + const pb = String(b).split('.').map(n => parseInt(n, 10) || 0); + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + const diff = (pa[i] || 0) - (pb[i] || 0); + if (diff !== 0) return diff; + } + return 0; +} + +async function fetchLatestSkillVersion() { + try { + const res = await fetch(`${UPDATE_HOST}/api/version`, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }); + if (!res.ok) return null; + const data = await res.json(); + return typeof data?.skills === 'string' ? data.skills : null; + } catch { + return null; // offline, sandboxed, timed out, or bad JSON: all non-fatal + } +} + +function buildUpdateDirective(localVersion, latestVersion) { + return ( + `UPDATE_AVAILABLE: A newer Impeccable skill is available ` + + `(installed v${localVersion}, latest v${latestVersion}). ` + + `Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` + + `Update now? It runs \`npx impeccable skills update\`." ` + + `If they agree, run \`npx impeccable skills update\` (the update applies to the next session, not this one). ` + + `Either way, continue the current task without waiting, and do not raise this again.` + ); +} + +/** + * Best-effort update directive for the boot output. Returns a string to append + * or null. Polls the version endpoint at most once per day (cached globally in + * the user's home dir) and re-surfaces a given version at most once per week so + * the agent never nags. Opt out entirely with IMPECCABLE_NO_UPDATE_CHECK=1. + */ +async function computeUpdateDirective(now = Date.now()) { + try { + if (process.env.IMPECCABLE_NO_UPDATE_CHECK) return null; + const localVersion = readLocalSkillVersion(); + if (!localVersion) return null; + + const cache = readUpdateCache(); + + // Poll the network only when the throttle window has elapsed. Stamp + // lastCheck even on failure so an offline machine doesn't poll every boot. + if (!cache.lastCheck || now - cache.lastCheck > CHECK_INTERVAL_MS) { + const latest = await fetchLatestSkillVersion(); + cache.lastCheck = now; + if (latest) cache.latestVersion = latest; + writeUpdateCache(cache); + } + + const latest = cache.latestVersion; + if (!latest || compareSemver(latest, localVersion) <= 0) return null; + + // Anti-nag: surface a given version at most once per RENOTIFY window. + if (cache.notifiedVersion === latest && cache.notifiedAt && now - cache.notifiedAt < RENOTIFY_INTERVAL_MS) { + return null; + } + cache.notifiedVersion = latest; + cache.notifiedAt = now; + writeUpdateCache(cache); + + return buildUpdateDirective(localVersion, latest); + } catch { + return null; + } +} + +async function cli() { + const ctx = loadContext(process.cwd()); + const updateDirective = await computeUpdateDirective(); + + if (!ctx.hasProduct) { + // Direct stdout message instead of relying on empty output as a signal + // — cheap models miss the empty case more often than the explicit one. + const parts = [ + 'NO_PRODUCT_MD: This project has no PRODUCT.md yet. ' + + 'Stop the current task, load reference/init.md, and follow its ' + + 'instructions to write PRODUCT.md before resuming.', + ]; + if (updateDirective) parts.push(updateDirective); + process.stdout.write(parts.join('\n\n---\n\n') + '\n'); + process.exit(0); + } + const parts = [`# PRODUCT.md\n\n${ctx.product.trim()}`]; + if (ctx.hasDesign) { + parts.push(`# DESIGN.md\n\n${ctx.design.trim()}`); + } + const register = extractRegister(ctx.product); + const next = register + ? `NEXT STEP: This project's register is \`${register}\`. You MUST now read \`reference/${register}.md\` before producing any design output.` + : `NEXT STEP: You MUST now read the matching register reference (\`reference/brand.md\` or \`reference/product.md\`) before producing any design output. Pick based on PRODUCT.md above.`; + parts.push(next); + if (updateDirective) parts.push(updateDirective); + process.stdout.write(parts.join('\n\n---\n\n') + '\n'); +} + +const _running = process.argv[1]; +if (_running?.endsWith('context.mjs') || _running?.endsWith('context.mjs/')) { + cli(); +} diff --git a/.trae/skills/impeccable/scripts/design-parser.mjs b/.trae/skills/impeccable/scripts/design-parser.mjs index b68756640..82a5cd7dc 100644 --- a/.trae/skills/impeccable/scripts/design-parser.mjs +++ b/.trae/skills/impeccable/scripts/design-parser.mjs @@ -63,7 +63,7 @@ function parseYamlSubset(yaml) { } const key = content.slice(0, colonIdx).trim(); - const rest = content.slice(colonIdx + 1).trim(); + const rest = stripInlineYamlComment(content.slice(colonIdx + 1).trim()); const parent = stack[stack.length - 1].obj; if (rest === '') { @@ -93,6 +93,21 @@ function findTopLevelColon(s) { return -1; } +function stripInlineYamlComment(s) { + let inQuote = null; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (inQuote) { + if (ch === inQuote && s[i - 1] !== '\\') inQuote = null; + } else if (ch === '"' || ch === "'") { + inQuote = ch; + } else if (ch === '#' && i > 0 && /\s/.test(s[i - 1])) { + return s.slice(0, i).trimEnd(); + } + } + return s; +} + function parseScalar(raw) { const s = raw.trim(); if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { diff --git a/.trae/skills/impeccable/scripts/detector/browser/injected/index.mjs b/.trae/skills/impeccable/scripts/detector/browser/injected/index.mjs index c03a69caf..ceff16737 100644 --- a/.trae/skills/impeccable/scripts/detector/browser/injected/index.mjs +++ b/.trae/skills/impeccable/scripts/detector/browser/injected/index.mjs @@ -9,8 +9,21 @@ if (IS_BROWSER) { const EXTENSION_MODE = (_myScript && _myScript.dataset.impeccableExtension === 'true') || document.documentElement.dataset.impeccableExtension === 'true'; - const BRAND_COLOR = 'oklch(55% 0.25 350)'; - const BRAND_COLOR_HOVER = 'oklch(45% 0.25 350)'; + // Kinpaku gold — pinned to the site's brand token (see + // site/styles/kinpaku-tokens.css --ks-kinpaku). Keep this in sync with + // the picker's C.brand in skill/scripts/live-browser.js and the kit's + // picker section in site/styles/kinpaku-kit.css. + // + // One color across both light and dark host pages. The outline is a + // 2px gesture pointing at an element + a labeled tag — it's a marker, + // not body text, so it doesn't need WCAG AA against the page. The + // label text inside the gold tag is dark (LABEL_INK) which has ~16:1 + // against the leaf gold, so reading the rule name is solid in both + // modes. Hover deepens the gold (preserves chroma — never drops it, + // dropping chroma washes the gold into a sand/olive tone). + const BRAND_COLOR = 'oklch(84% 0.19 80.46)'; + const BRAND_COLOR_HOVER = 'oklch(74% 0.18 80)'; + const LABEL_INK = 'oklch(4% 0.004 95)'; const LABEL_BG = BRAND_COLOR; const OUTLINE_COLOR = BRAND_COLOR; @@ -278,7 +291,7 @@ if (IS_BROWSER) { display: 'flex', alignItems: 'center', whiteSpace: 'nowrap', fontSize: '11px', fontWeight: '600', letterSpacing: '0.02em', - color: 'white', lineHeight: '14px', + color: LABEL_INK, lineHeight: '14px', background: LABEL_BG, fontFamily: 'system-ui, sans-serif', borderRadius: '4px 4px 0 0', @@ -398,7 +411,7 @@ if (IS_BROWSER) { banner.className = 'impeccable-overlay impeccable-banner'; Object.assign(banner.style, { position: 'fixed', top: '0', left: '0', right: '0', zIndex: '100000', - background: LABEL_BG, color: 'white', + background: LABEL_BG, color: LABEL_INK, fontFamily: 'system-ui, sans-serif', fontSize: '13px', display: 'flex', alignItems: 'center', pointerEvents: 'auto', height: '36px', overflow: 'hidden', maxWidth: '100vw', @@ -1223,12 +1236,12 @@ if (IS_BROWSER) { } console.group( `%c[impeccable] ${allFindings.length} anti-pattern${allFindings.length === 1 ? '' : 's'} found`, - 'color: oklch(60% 0.25 350); font-weight: bold' + 'color: oklch(84% 0.19 80.46); font-weight: bold' ); for (const { el, findings } of allFindings) { for (const f of findings) { console.log(`%c${f.type || f.id}%c ${f.detail || f.snippet}`, - 'color: oklch(55% 0.25 350); font-weight: bold', 'color: inherit', el); + 'color: oklch(84% 0.19 80.46); font-weight: bold', 'color: inherit', el); } } console.groupEnd(); @@ -1249,6 +1262,10 @@ if (IS_BROWSER) { const groupMap = new Map(); const _disabled = EXTENSION_MODE ? (window.__IMPECCABLE_CONFIG__?.disabledRules || []) : []; const _ruleOk = (id) => !_disabled.length || !_disabled.includes(id); + // Note: provider-gated rules (--gpt / --gemini) are NOT filtered here. In a + // real browser env (detector page, live overlay, extension) running every + // check is free, so we always surface them; the gating is purely a CLI + // output concern, applied in the Node engines' detect* return paths. for (const el of document.querySelectorAll('*')) { // Skip impeccable's own elements and any descendants (overlays, labels, banner, nav buttons) @@ -1270,11 +1287,23 @@ if (IS_BROWSER) { ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), - ...checkElementHeroEyebrowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementOversizedH1DOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementClippedOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementGptBorderShadowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementTextOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ].filter(f => _ruleOk(f.type)); addBrowserFindings(groupMap, el, findings); + + // Hero eyebrow: the offending element is the eyebrow above the heading, + // not the heading itself — highlight the previous sibling instead. + const eyebrowFindings = checkElementHeroEyebrowDOM(el) + .map(f => ({ type: f.id, detail: f.snippet })) + .filter(f => _ruleOk(f.type)); + if (eyebrowFindings.length > 0 && el.previousElementSibling) { + addBrowserFindings(groupMap, el.previousElementSibling, eyebrowFindings); + } } const pageLevelFindings = []; @@ -1306,6 +1335,14 @@ if (IS_BROWSER) { addBrowserFindings(groupMap, document.body, qualityFindings); } + const creamFindings = checkCreamPalette(document) + .map(f => ({ type: f.id, detail: f.snippet })) + .filter(f => _ruleOk(f.type)); + if (creamFindings.length > 0) { + pageLevelFindings.push(...creamFindings); + addBrowserFindings(groupMap, document.body, creamFindings); + } + // Regex-on-HTML checks (shared with Node) // Clone the document and strip impeccable-live overlay nodes before the // regex scan, so the inspector's own inline styles (transitions on top/ diff --git a/.trae/skills/impeccable/scripts/detector/cli/main.mjs b/.trae/skills/impeccable/scripts/detector/cli/main.mjs index 76da09f0c..94283e01b 100644 --- a/.trae/skills/impeccable/scripts/detector/cli/main.mjs +++ b/.trae/skills/impeccable/scripts/detector/cli/main.mjs @@ -41,7 +41,7 @@ function formatFindings(findings, jsonMode) { // Stdin handling // --------------------------------------------------------------------------- -async function handleStdin() { +async function handleStdin(options = {}) { const chunks = []; for await (const chunk of process.stdin) chunks.push(chunk); const input = Buffer.concat(chunks).toString('utf-8'); @@ -50,10 +50,10 @@ async function handleStdin() { const fp = parsed?.tool_input?.file_path; if (fp && fs.existsSync(fp)) { return HTML_EXTENSIONS.has(path.extname(fp).toLowerCase()) - ? detectHtml(fp) : detectText(fs.readFileSync(fp, 'utf-8'), fp); + ? detectHtml(fp, options) : detectText(fs.readFileSync(fp, 'utf-8'), fp, options); } } catch { /* not JSON */ } - return detectText(input, ''); + return detectText(input, '', options); } @@ -81,6 +81,8 @@ Scan files or URLs for UI anti-patterns and design quality issues. Options: --fast Regex-only mode (skip static HTML/CSS analysis, faster but misses linked stylesheets) --json Output results as JSON + --gpt Also report GPT-specific provider tells (off by default) + --gemini Also report Gemini-specific provider tells (off by default) --help Show this help message Detection modes: @@ -106,6 +108,10 @@ async function detectCli() { const jsonMode = args.includes('--json'); const helpMode = args.includes('--help'); const fastMode = args.includes('--fast'); + const providers = []; + if (args.includes('--gpt')) providers.push('gpt'); + if (args.includes('--gemini')) providers.push('gemini'); + const scanOptions = { providers }; const targets = args.filter(a => !a.startsWith('--')); if (helpMode) { printUsage(); process.exit(0); } @@ -113,7 +119,7 @@ async function detectCli() { let allFindings = []; if (!process.stdin.isTTY && targets.length === 0) { - allFindings = await handleStdin(); + allFindings = await handleStdin(scanOptions); } else { const paths = targets.length > 0 ? targets : [process.cwd()]; const urlTargetCount = paths.filter(target => /^https?:\/\//i.test(target)).length; @@ -124,8 +130,8 @@ async function detectCli() { if (/^https?:\/\//i.test(target)) { try { const scanner = browserDetector - ? (url) => browserDetector.detectUrl(url) - : (url) => detectUrl(url); + ? (url) => browserDetector.detectUrl(url, scanOptions) + : (url) => detectUrl(url, scanOptions); allFindings.push(...await scanner(target)); } catch (e) { process.stderr.write(`Error: ${e.message}\n`); } continue; @@ -192,9 +198,9 @@ async function detectCli() { const ext = path.extname(file).toLowerCase(); let fileFindings; if (!fastMode && HTML_EXTENSIONS.has(ext)) { - fileFindings = await detectHtml(file); + fileFindings = await detectHtml(file, scanOptions); } else { - fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file); + fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file, scanOptions); } // Annotate findings with import context const importers = importedByMap.get(file); @@ -209,9 +215,9 @@ async function detectCli() { } else if (stat.isFile()) { const ext = path.extname(resolved).toLowerCase(); if (!fastMode && HTML_EXTENSIONS.has(ext)) { - allFindings.push(...await detectHtml(resolved)); + allFindings.push(...await detectHtml(resolved, scanOptions)); } else { - allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved)); + allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved, scanOptions)); } } } diff --git a/.trae/skills/impeccable/scripts/detector/detect-antipatterns-browser.js b/.trae/skills/impeccable/scripts/detector/detect-antipatterns-browser.js index 1afbe3f12..88439a5c3 100644 --- a/.trae/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +++ b/.trae/skills/impeccable/scripts/detector/detect-antipatterns-browser.js @@ -38,7 +38,7 @@ const OVERUSED_FONTS = new Set([ // Older monoculture (still ubiquitous): 'inter', 'roboto', 'open sans', 'lato', 'montserrat', 'arial', 'helvetica', // Newer monoculture (the Anthropic-skill / Vercel / GitHub default wave): - 'fraunces', 'instrument sans', + 'fraunces', 'instrument sans', 'instrument serif', 'geist', 'geist sans', 'geist mono', 'mona sans', 'plus jakarta sans', 'space grotesk', 'recoleta', @@ -165,6 +165,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'AI color palette', }, + { + id: 'cream-palette', + category: 'slop', + name: 'Cream / beige palette', + description: + 'A warm cream or beige page background has become the default "tasteful" AI surface, reached for by reflex. Choose a background that comes from a deliberate palette, not the safe warm off-white.', + skillSection: 'Color & Contrast', + skillGuideline: 'cream and beige as the default surface', + }, { id: 'nested-cards', category: 'slop', @@ -183,15 +192,6 @@ const ANTIPATTERNS = [ skillSection: 'Layout & Space', skillGuideline: 'same spacing everywhere', }, - { - id: 'everything-centered', - category: 'slop', - name: 'Everything centered', - description: - 'Every text element is center-aligned. Left-aligned text with asymmetric layouts feels more designed. Center only hero sections and CTAs.', - skillSection: 'Layout & Space', - skillGuideline: 'Center everything', - }, { id: 'bounce-easing', category: 'slop', @@ -247,17 +247,72 @@ const ANTIPATTERNS = [ skillSection: 'Typography', skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding', }, + { + id: 'numbered-section-markers', + category: 'slop', + severity: 'advisory', + name: 'Numbered section markers (01 / 02 / 03)', + description: + 'Numbered display markers as section labels (01, 02, 03) are the AI editorial scaffold one tier deeper than tracked eyebrow chips. If you find yourself reaching for them, choose a different section cadence.', + skillSection: 'Layout & Space', + skillGuideline: 'numbered section markers', + }, + { + id: 'em-dash-overuse', + category: 'slop', + name: 'Em-dash overuse', + description: + 'More than two em-dashes (— or --) in body copy is an AI cadence tell. Use commas, colons, periods, or parentheses instead.', + skillSection: 'Copy', + skillGuideline: 'no em dashes', + }, + { + id: 'marketing-buzzword', + category: 'slop', + name: 'Marketing buzzword', + description: + 'Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does.', + skillSection: 'Copy', + skillGuideline: 'marketing buzzwords', + }, + { + id: 'aphoristic-cadence', + category: 'slop', + name: 'Aphoristic-cadence copy', + description: + 'Three or more sections landing on a short rebuttal sentence ("X. No Y." / "X. Just Y.") or a manufactured-contrast aphorism ("Not a feature. A platform.") reads as AI cadence, not voice. Once is fine; the pattern is the tell.', + skillSection: 'Copy', + skillGuideline: 'aphoristic cadence', + }, + { + id: 'oversized-h1', + category: 'slop', + name: 'Oversized hero headline', + description: + 'A full-sentence headline set at display size ends up dominating the viewport, leaving no room for anything else above the fold. A punchy one- or two-word headline at that size is fine — the problem is a long headline blown up too large. Set long headlines smaller, or tighten the copy.', + skillSection: 'Typography', + skillGuideline: 'long headline set at display size', + }, + { + id: 'extreme-negative-tracking', + category: 'slop', + name: 'Crushed letter spacing', + description: + 'Letter-spacing pulled tighter than the point where characters keep their own shapes costs legibility. Tighten display type optically, not destructively.', + skillSection: 'Typography', + skillGuideline: 'letter spacing crushed past legibility', + }, + { + id: 'broken-image', + category: 'quality', + name: 'Broken or placeholder image', + description: + ' tags with empty src, missing src, or placeholder values ship as broken-image boxes. Use real images, generated assets, or remove the tag.', + skillSection: 'Imagery', + skillGuideline: 'broken image references', + }, // ── Quality: general design and accessibility issues ── - { - id: 'pure-black-white', - category: 'quality', - name: 'Pure black background', - description: - 'Pure #000000 as a background color looks harsh and unnatural. Tint it slightly toward your brand hue (e.g., oklch(12% 0.01 250)) for a more refined feel.', - skillSection: 'Color & Contrast', - skillGuideline: 'pure black (#000)', - }, { id: 'gray-on-color', category: 'quality', @@ -297,7 +352,9 @@ const ANTIPATTERNS = [ category: 'quality', name: 'Cramped padding', description: - 'Text is too close to the edge of its container. Add at least 8px (ideally 12-16px) of padding inside bordered or colored containers.', + 'Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.', + skillSection: 'Layout & Space', + skillGuideline: 'inside bordered or colored containers', }, { id: 'body-text-viewport-edge', @@ -350,6 +407,70 @@ const ANTIPATTERNS = [ description: 'Letter spacing above 0.05em on body text disrupts natural character groupings and slows reading. Reserve wide tracking for short uppercase labels only.', }, + { + id: 'text-overflow', + category: 'quality', + name: 'Content overflowing its container', + description: + 'Content renders wider than its container, spilling out or forcing a horizontal scrollbar. Let text wrap, constrain widths, or give the region a deliberate scroll affordance.', + skillSection: 'Layout & Space', + skillGuideline: 'content wider than its container', + }, + { + id: 'clipped-overflow-container', + category: 'quality', + name: 'Positioned child clipped by overflow container', + description: + 'A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.', + skillSection: 'Layout & Space', + skillGuideline: 'overflow container clipping positioned children', + }, + + // ── Provider tells: opt-in via --gpt / --gemini (gated off by default) ── + { + id: 'gpt-thin-border-wide-shadow', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Hairline border with wide shadow', + description: + 'A hairline border paired with a wide, diffuse shadow is a recurring generated-UI signature. Commit to one — a defined edge or a soft elevation — rather than both at once.', + skillSection: 'Visual Details', + skillGuideline: 'hairline border plus wide diffuse shadow', + }, + { + id: 'repeating-stripes-gradient', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Repeating-gradient stripes', + description: + 'Repeating-gradient stripes used as surface decoration are a recurring generated-UI signature. Reach for a deliberate texture or leave the surface plain.', + skillSection: 'Visual Details', + skillGuideline: 'repeating-gradient decorative stripes', + }, + { + id: 'theater-slop-phrase', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Theater framing copy', + description: + 'Dismissing something as "theater" is a recurring generated-copy tic. Say plainly what the thing does or does not do.', + skillSection: 'Copy', + skillGuideline: 'theater framing copy', + }, + { + id: 'image-hover-transform', + category: 'slop', + severity: 'advisory', + gated: 'gemini', + name: 'Image hover transform', + description: + 'Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction.', + skillSection: 'Motion', + skillGuideline: 'image scale or rotate on hover', + }, ]; // --- cli/engine/shared/color.mjs --- @@ -527,11 +648,6 @@ function checkColors(opts) { } const findings = []; - // Pure black background (only solid or near-solid, not semi-transparent overlays) - if (bgColor && bgColor.a >= 0.9 && bgColor.r === 0 && bgColor.g === 0 && bgColor.b === 0) { - findings.push({ id: 'pure-black-white', snippet: '#000000 background' }); - } - if (hasDirectText && textColor && !isEmojiOnly) { // Run background-dependent checks against either a solid bg or, if the // ancestor is a gradient, against every gradient stop (use the worst case). @@ -587,9 +703,6 @@ function checkColors(opts) { // Tailwind class checks if (classList) { const classStr = typeof classList === 'string' ? classList : Array.from(classList).join(' '); - if (/\bbg-black\b(?!\/)/.test(classStr)) { - findings.push({ id: 'pure-black-white', snippet: 'bg-black' }); - } const grayMatch = classStr.match(/\btext-(?:gray|slate|zinc|neutral|stone)-\d+\b/); const colorBgMatch = classStr.match(/\bbg-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+\b/); @@ -905,12 +1018,6 @@ function checkHtmlPatterns(html) { // --- Color --- - // Pure black background - const pureBlackBgRe = /background(?:-color)?\s*:\s*(?:#000000|#000|rgb\(\s*0,\s*0,\s*0\s*\))\b/gi; - if (pureBlackBgRe.test(html)) { - findings.push({ id: 'pure-black-white', snippet: 'Pure #000 background' }); - } - // AI color palette: purple/violet const purpleHexRe = /#(?:7c3aed|8b5cf6|a855f7|9333ea|7e22ce|6d28d9|6366f1|764ba2|667eea)\b/gi; if (purpleHexRe.test(html)) { @@ -1027,6 +1134,39 @@ function checkHtmlPatterns(html) { } } + // --- Provider tells (gated): repeating-gradient stripes (GPT) --- + if (/repeating-(?:linear|radial|conic)-gradient\s*\(/i.test(html)) { + findings.push({ id: 'repeating-stripes-gradient', snippet: 'repeating-gradient decorative stripes' }); + } + + // --- Provider tells (gated): "X theater" framing copy (GPT) --- + // Lives here (regex-on-HTML) rather than in the text-content analyzers so it + // runs in the bundled browser path too, not just the CLI/static path. + { + const bodyText = html + .replace(/]*>[\s\S]*?<\/script>/gi, ' ') + .replace(/]*>[\s\S]*?<\/style>/gi, ' ') + .replace(/<[^>]+>/g, ' '); + const tm = /\b(\w+)\s+theater\b/i.exec(bodyText); + if (tm) findings.push({ id: 'theater-slop-phrase', snippet: `"${tm[0].trim()}"` }); + } + + // --- Provider tells (gated): image hover transform (Gemini) --- + // A CSS `img...:hover { transform: ... }` rule, or a Tailwind hover:scale / + // hover:rotate / hover:translate utility on an . Each distinct + // mechanism is its own finding. + const imgHoverCss = /\bimg\b[^,{}]*:hover\b[^{}]*\{[^}]*\btransform\s*:\s*(?:scale|rotate|translate|matrix|skew)/i; + if (imgHoverCss.test(html)) { + findings.push({ id: 'image-hover-transform', snippet: 'img:hover { transform } rule' }); + } + const imgTagRe = /]*\bclass\s*=\s*"([^"]*)"/gi; + let im; + while ((im = imgTagRe.exec(html)) !== null) { + if (/\bhover:(?:scale|rotate|translate|skew)-/.test(im[1])) { + findings.push({ id: 'image-hover-transform', snippet: 'Tailwind hover transform on ' }); + } + } + return findings; } @@ -1673,7 +1813,7 @@ function resolveLengthPx(value, fontSizePx) { // Both adapters resolve font-size, line-height and letter-spacing to pixels // before calling this so the pure function only deals with numbers. function checkQuality(opts) { - const { el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax = 80, viewportWidth = 0 } = opts; + const { el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax = 80, viewportWidth = 0, win = null } = opts; const findings = []; // Skip browser extension injected elements const elId = el.id || ''; @@ -1724,6 +1864,155 @@ function checkQuality(opts) { } } + // --- Flush against a visible boundary --- + // Fires when a container has a visible boundary (border, outline, OR a + // non-transparent background) AND near-zero padding on the bounded + // side(s) AND text-bearing children land flush against the boundary. + // + // Distinct from cramped-padding: that rule needs the element itself to + // have direct text (hasDirectText). This rule targets the OPPOSITE + // shape — a container with NO direct text, only children — which is + // exactly what cramped-padding misses (a section wrapping a label + + // list lands a free pass). + // + // The classic shape: agent writes `padding: 28px 0 0` shorthand on a + // section that also has a border, zeroing horizontal padding so the + // text-bearing children touch the side borders. Background and + // outline count too: a colored card with zero padding has the same + // visual failure mode. + { + const FLUSH_SKIP_TAGS = new Set(['HTML', 'BODY', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ARTICLE', 'ASIDE', 'BUTTON', 'A', 'LABEL', 'SUMMARY', 'CODE', 'PRE', 'INPUT', 'TEXTAREA', 'SELECT', 'FORM', 'FIGURE', 'TABLE', 'TBODY', 'THEAD', 'TR', 'TD', 'TH']); + const upperTag = tag ? tag.toUpperCase() : ''; + const elPosition = style.position || ''; + if ( + !FLUSH_SKIP_TAGS.has(upperTag) && + !hasDirectText && + !['fixed', 'absolute'].includes(elPosition) && + el.children && el.children.length > 0 + ) { + const isTransparent = (c) => + !c || c === 'transparent' || c === 'rgba(0, 0, 0, 0)' || + /^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*0(?:\.0+)?\s*\)$/.test(c); + + const borderW = { + top: parseFloat(style.borderTopWidth) || 0, + right: parseFloat(style.borderRightWidth) || 0, + bottom: parseFloat(style.borderBottomWidth) || 0, + left: parseFloat(style.borderLeftWidth) || 0, + }; + const borderVisible = { + top: borderW.top > 0 && !isTransparent(style.borderTopColor), + right: borderW.right > 0 && !isTransparent(style.borderRightColor), + bottom: borderW.bottom > 0 && !isTransparent(style.borderBottomColor), + left: borderW.left > 0 && !isTransparent(style.borderLeftColor), + }; + // Outline detection. jsdom decomposes `border` shorthand into + // border{Top,…}Width/Color but does NOT decompose `outline` — + // the longhands come back empty when the value was set via the + // shorthand. Fall back to parsing `style.outline` ourselves. + let outlineW = parseFloat(style.outlineWidth) || 0; + let outlineStyleVal = style.outlineStyle || ''; + let outlineColorVal = style.outlineColor || ''; + if (!outlineW && style.outline) { + const wMatch = style.outline.match(/(\d+(?:\.\d+)?)\s*px/); + if (wMatch) outlineW = parseFloat(wMatch[1]) || 0; + if (!outlineStyleVal) { + outlineStyleVal = /\b(solid|dashed|dotted|double|groove|ridge|inset|outset)\b/.test(style.outline) ? 'solid' : ''; + } + if (!outlineColorVal) { + const cMatch = style.outline.match(/(rgba?\([^)]+\)|#[0-9a-fA-F]{3,8}|[a-zA-Z]+)\s*$/); + if (cMatch) outlineColorVal = cMatch[1]; + } + } + const outlineVisible = outlineW > 0 && !isTransparent(outlineColorVal) && outlineStyleVal && outlineStyleVal !== 'none'; + const bgVisible = !isTransparent(style.backgroundColor); + + const anyVisible = borderVisible.top || borderVisible.right || borderVisible.bottom || borderVisible.left || outlineVisible || bgVisible; + if (anyVisible) { + // Resolve padding to px (jsdom returns raw "1.5rem" etc., not the + // computed px value; parseFloat would strip the unit and treat + // 1.5rem as 1.5px, false-flagging legitimate insets). + const pad = { + top: resolveLengthPx(style.paddingTop, fontSize) ?? 0, + right: resolveLengthPx(style.paddingRight, fontSize) ?? 0, + bottom: resolveLengthPx(style.paddingBottom, fontSize) ?? 0, + left: resolveLengthPx(style.paddingLeft, fontSize) ?? 0, + }; + const PAD_THRESHOLD = 2; + // Children-insulate-this-side: a side is insulated if ANY direct + // child has its own padding ≥ 4px on that side. Rationale: in + // typical flow, only the first/last (or leftmost/rightmost) + // children actually sit at the parent's edges. If even one of + // them has its own padding, the visual flush is broken on that + // side. Classic example: a column-flow card frame where the + // top child (header) has padding-top:12 and the bottom child + // (footer) has padding-bottom:8 — the parent's padding:0 doesn't + // matter; nothing is actually flush. The `any-child-insulates` + // heuristic accepts some false negatives (a card with one heavily + // padded middle child won't flag) for far fewer false positives. + const CHILD_INSULATE_THRESHOLD = 4; + const childrenInsulate = { top: false, right: false, bottom: false, left: false }; + for (const child of el.children) { + let childStyle = null; + if (win && typeof win.getComputedStyle === 'function') { + try { childStyle = win.getComputedStyle(child); } catch {} + } + if (!childStyle && typeof getComputedStyle === 'function') { + try { childStyle = getComputedStyle(child); } catch {} + } + if (!childStyle) continue; + const childPad = { + top: resolveLengthPx(childStyle.paddingTop, fontSize) ?? 0, + right: resolveLengthPx(childStyle.paddingRight, fontSize) ?? 0, + bottom: resolveLengthPx(childStyle.paddingBottom, fontSize) ?? 0, + left: resolveLengthPx(childStyle.paddingLeft, fontSize) ?? 0, + }; + for (const s of ['top', 'right', 'bottom', 'left']) { + if (childPad[s] >= CHILD_INSULATE_THRESHOLD) childrenInsulate[s] = true; + } + } + + const flushSides = []; + for (const side of ['top', 'right', 'bottom', 'left']) { + const sideBounded = borderVisible[side] || outlineVisible || bgVisible; + if (sideBounded && pad[side] <= PAD_THRESHOLD && !childrenInsulate[side]) { + flushSides.push(side); + } + } + + if (flushSides.length > 0) { + // Confirm at least one direct child has substantial text content + // (> 4 chars). Without this, the flush is harmless: e.g. an + // image-only card. + let hasTextChild = false; + for (const child of el.children) { + const childText = (child.textContent || '').trim(); + if (childText.length > 4) { hasTextChild = true; break; } + } + if (hasTextChild) { + const cls = (typeof el.className === 'string' && el.className.trim()) + ? el.className.trim().split(/\s+/)[0] + : ''; + const boundaryParts = []; + const borderSidesVisible = ['top', 'right', 'bottom', 'left'].filter(s => borderVisible[s]); + if (borderSidesVisible.length === 4) boundaryParts.push('border'); + else if (borderSidesVisible.length > 0) boundaryParts.push(`border-${borderSidesVisible.join('/')}`); + if (outlineVisible) boundaryParts.push('outline'); + if (bgVisible) boundaryParts.push('bg'); + const sidesLabel = flushSides.length === 4 ? 'all sides' : flushSides.join('/'); + const ident = cls + ? `<${tag.toLowerCase()}> "${cls}"` + : `<${tag.toLowerCase()}>`; + findings.push({ + id: 'cramped-padding', + snippet: `${ident}: children flush against ${boundaryParts.join('+')} on ${sidesLabel} (no inset)`, + }); + } + } + } + } + } + // --- Body text touching viewport edge --- (browser-only: needs rect) // Catches the failure mode where the agent ships body paragraphs // with NO container providing horizontal padding — text bleeds @@ -1804,6 +2093,20 @@ function checkQuality(opts) { } } + // --- Crushed letter spacing (mirror of wide-tracking) --- + // Tracking pulled tighter than ~-0.05em crushes characters into each other. + // Optical tightening that display type legitimately wants (around -0.02em) + // stays well above this floor. + if (hasDirectText && textLen > 20 && fontSize > 0) { + if (letterSpacingPx != null && letterSpacingPx < 0) { + const trackingEm = letterSpacingPx / fontSize; + if (trackingEm <= -0.05) { + const excerpt = (el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 40); + findings.push({ id: 'extreme-negative-tracking', snippet: `letter-spacing: ${trackingEm.toFixed(2)}em — "${excerpt}"` }); + } + } + } + return findings; } @@ -1820,7 +2123,7 @@ function checkElementQualityDOM(el) { const rect = el.getBoundingClientRect(); const lineMax = (typeof window !== 'undefined' && window.__IMPECCABLE_CONFIG__?.lineLengthMax) || 80; const viewportWidth = (typeof window !== 'undefined' ? window.innerWidth : 0) || 0; - return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax, viewportWidth }); + return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax, viewportWidth, win: typeof window !== 'undefined' ? window : null }); } // Pure page-level skipped-heading walk. Takes a Document so it works in both @@ -1862,7 +2165,7 @@ function checkElementQuality(el, style, tag, window) { const fontSize = resolveFontSizePx(el, window); const lineHeightPx = resolveLengthPx(style.lineHeight, fontSize); const letterSpacingPx = resolveLengthPx(style.letterSpacing, fontSize); - return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect: null }); + return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect: null, win: window }); } function checkElementBorders(tag, style, overrides, resolvedRadius) { @@ -2303,40 +2606,213 @@ function checkPageLayout(doc, win) { } } - // Everything centered - const textEls = doc.querySelectorAll('h1, h2, h3, h4, h5, h6, p, li, div, button'); - let centeredCount = 0; - let totalText = 0; - for (const el of textEls) { - const hasDirectText = [...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim().length >= 3); - if (!hasDirectText) continue; - totalText++; - - let cur = el; - let isCentered = false; - while (cur && cur.nodeType === 1) { - const rawStyle = cur.getAttribute?.('style') || ''; - const cls = cur.getAttribute?.('class') || ''; - if (/text-align\s*:\s*center/i.test(rawStyle) || /\btext-center\b/.test(cls)) { - isCentered = true; - break; - } - if (cur.tagName === 'BODY') break; - cur = cur.parentElement; - } - if (isCentered) centeredCount++; - } - - if (totalText >= 5 && centeredCount / totalText > 0.7) { - findings.push({ - id: 'everything-centered', - snippet: `${centeredCount}/${totalText} text elements centered (${Math.round(centeredCount / totalText * 100)}%)`, - }); - } - return findings; } +// ─── Cream / beige palette (the default "tasteful" AI surface) ──────────────── +// A warm, lightly-tinted off-white page background — light, with R≥G≥B and a +// small warm tint (not white, not a strong color). The current reflex surface. +function isCreamColor(rgb) { + if (!rgb) return false; + const { r, g, b } = rgb; + if (Math.min(r, g, b) < 209) return false; // must be light + if (!(r >= g && g >= b)) return false; // warm ordering + const warmth = r - b; + return warmth >= 6 && warmth <= 48; // tinted, not white, not strong +} + +// Tailwind background utilities that render as a warm off-white surface. The +// static engine doesn't fetch Tailwind's CSS, so a `bg-amber-50` on +// resolves to nothing in computed style — catch it from the class list +// instead. Candidate tokens map to their actual Tailwind hex and are still +// filtered through isCreamColor, so neutral grays (stone) and over-saturated +// shades drop out on their own. +const TAILWIND_BG_HEX = { + 'bg-amber-50': '#fffbeb', 'bg-amber-100': '#fef3c7', + 'bg-orange-50': '#fff7ed', 'bg-orange-100': '#ffedd5', + 'bg-yellow-50': '#fefce8', + 'bg-stone-50': '#fafaf9', 'bg-stone-100': '#f5f5f4', 'bg-stone-200': '#e7e5e4', +}; + +function creamFromClassList(cls) { + if (!cls) return null; + // Arbitrary value: bg-[#f5f0e6] / bg-[rgb(245_240_230)] (underscores = spaces). + const arb = cls.match(/\bbg-\[([^\]]+)\]/); + if (arb && isCreamColor(parseAnyColor(arb[1].replace(/_/g, ' ')))) return `bg-[${arb[1]}]`; + // Named warm-light utilities. + for (const [tok, hex] of Object.entries(TAILWIND_BG_HEX)) { + if (new RegExp(`(^|\\s)${tok}($|\\s)`).test(cls) && isCreamColor(parseAnyColor(hex))) return tok; + } + return null; +} + +function checkCreamPalette(doc, win) { + const findings = []; + const body = doc.body || (doc.querySelector ? doc.querySelector('body') : null); + if (!body) return findings; + const html = doc.documentElement; + const getCS = (el) => (win ? win.getComputedStyle(el) : getComputedStyle(el)); + + // 1. Computed background — covers inline / close.', 'Prefix every preview selector with the matching [data-impeccable-variant="N"] selector.', 'Keep selectors anchored to the generated variant wrapper; do not rely on component CSS scoping for preview rules.', ], forbidden: [ 'Do not use @scope for this styleMode.', + 'Do not wrap style content in a JSX/TSX template literal ({` ... `}); that syntax is for .tsx/.jsx only.', + 'Do not put { immediately after the style opening tag; Astro parses { as expression syntax.', ], }; } @@ -629,4 +632,15 @@ if (_running?.endsWith('live-wrap.mjs') || _running?.endsWith('live-wrap.mjs/')) } // Test exports (used by tests/live-wrap.test.mjs) -export { buildSearchQueries, findElement, findClosingLine, detectCommentSyntax }; +export { + buildSearchQueries, + findElement, + findClosingLine, + detectCommentSyntax, + findAllElements, + filterByText, + findFileWithQuery, + detectStyleMode, + buildCssAuthoring, + buildCssSelectorPrefixExamples, +}; diff --git a/.trae/skills/impeccable/scripts/live.mjs b/.trae/skills/impeccable/scripts/live.mjs index cafb0eca9..8acd300ed 100644 --- a/.trae/skills/impeccable/scripts/live.mjs +++ b/.trae/skills/impeccable/scripts/live.mjs @@ -21,7 +21,7 @@ import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { loadContext } from './load-context.mjs'; +import { loadContext } from './context.mjs'; import { resolveFiles } from './live-inject.mjs'; import { readLiveServerInfo } from './impeccable-paths.mjs'; @@ -80,7 +80,7 @@ The agent should then: process.exit(1); } - // 4. Load PRODUCT.md + DESIGN.md context (auto-migrates legacy .impeccable.md) + // 4. Load PRODUCT.md + DESIGN.md context. const ctx = loadContext(process.cwd()); // 5. Compute drift-heal: compare resolved inject targets against the @@ -102,7 +102,6 @@ The agent should then: hasDesign: ctx.hasDesign, design: ctx.design, designPath: ctx.designPath, - migrated: ctx.migrated, }, null, 2)); } diff --git a/.trae/skills/impeccable/scripts/load-context.mjs b/.trae/skills/impeccable/scripts/load-context.mjs deleted file mode 100644 index dc340bf16..000000000 --- a/.trae/skills/impeccable/scripts/load-context.mjs +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Shared context loader for every impeccable command that needs to know - * "who is this for" and "what does this look like". - * - * Input: project root (process.cwd()). - * - * Output (JSON to stdout): - * { - * hasProduct: boolean, // PRODUCT.md found (or auto-migrated) - * product: string | null, // PRODUCT.md contents - * productPath: string | null, // relative path - * hasDesign: boolean, // DESIGN.md found - * design: string | null, // DESIGN.md contents - * designPath: string | null, - * migrated: boolean, // true if we auto-renamed .impeccable.md -> PRODUCT.md - * contextDir: string, // absolute path of the directory the files were found in - * } - * - * Filename matching is case-insensitive for PRODUCT.md and DESIGN.md. The - * Google DESIGN.md convention is uppercase at repo root; Kiro-style and - * lowercase variants are also matched so users don't get punished for case. - * - * Lookup directory resolution (first match wins): - * 1. process.env.IMPECCABLE_CONTEXT_DIR (absolute or relative to cwd) - * 2. cwd, if PRODUCT.md / DESIGN.md / .impeccable.md is there (back-compat) - * 3. Auto-fallback subdirectories of cwd: .agents/context/, then docs/ - * 4. cwd as a default "no context found" location - * - * Legacy `.impeccable.md` -> PRODUCT.md migration only fires at cwd root; - * fallback directories are read-only as far as auto-rename is concerned. - */ - -import fs from 'node:fs'; -import path from 'node:path'; - -const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; -const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; -const LEGACY_NAMES = ['.impeccable.md']; -const FALLBACK_DIRS = ['.agents/context', 'docs']; - -/** - * Resolve the directory that holds PRODUCT.md / DESIGN.md for - * this project. Exported so other scripts (e.g. live-server.mjs) can read the - * design files from the same location the loader uses. - */ -export function resolveContextDir(cwd = process.cwd()) { - // 1. Explicit override - const envDir = process.env.IMPECCABLE_CONTEXT_DIR; - if (envDir && envDir.trim()) { - const trimmed = envDir.trim(); - return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); - } - - // 2. cwd wins if any canonical or legacy file is there. We check legacy too - // so the auto-migration path in loadContext stays predictable. - if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES, ...LEGACY_NAMES])) { - return cwd; - } - - // 3. Auto-fallback subdirs. Match if PRODUCT.md or DESIGN.md is present; - // legacy `.impeccable.md` does not pull the lookup into a fallback dir. - for (const rel of FALLBACK_DIRS) { - const candidate = path.resolve(cwd, rel); - if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { - return candidate; - } - } - - // 4. Nothing found — keep the historical "default to cwd" behaviour so the - // caller's `hasProduct === false` branch still fires the same way. - return cwd; -} - -export function loadContext(cwd = process.cwd()) { - let migrated = false; - const contextDir = resolveContextDir(cwd); - - // 1. Look for PRODUCT.md (case-insensitive) in the resolved dir - let productPath = firstExisting(contextDir, PRODUCT_NAMES); - - // 2. Legacy: if no PRODUCT.md but .impeccable.md exists at cwd root, rename - // it in place. We only migrate at the root — fallback dirs are read-only - // so we don't surprise users by mutating files under docs/ or .agents/. - if (!productPath && contextDir === cwd) { - const legacyPath = firstExisting(cwd, LEGACY_NAMES); - if (legacyPath) { - const newPath = path.join(cwd, 'PRODUCT.md'); - try { - fs.renameSync(legacyPath, newPath); - productPath = newPath; - migrated = true; - } catch { - // Rename failed (permissions, etc.) — fall back to reading legacy in place - productPath = legacyPath; - } - } - } - - // 3. DESIGN.md (case-insensitive) - const designPath = firstExisting(contextDir, DESIGN_NAMES); - - const product = productPath ? safeRead(productPath) : null; - const design = designPath ? safeRead(designPath) : null; - - return { - hasProduct: !!product, - product, - productPath: productPath ? path.relative(cwd, productPath) : null, - hasDesign: !!design, - design, - designPath: designPath ? path.relative(cwd, designPath) : null, - migrated, - contextDir, - }; -} - -function firstExisting(dir, names) { - for (const name of names) { - const abs = path.join(dir, name); - if (fs.existsSync(abs)) return abs; - } - return null; -} - -function safeRead(p) { - try { return fs.readFileSync(p, 'utf-8'); } catch { return null; } -} - -// --------------------------------------------------------------------------- -// CLI mode — print the context as JSON -// --------------------------------------------------------------------------- - -function cli() { - const result = loadContext(process.cwd()); - console.log(JSON.stringify(result, null, 2)); -} - -const _running = process.argv[1]; -if (_running?.endsWith('load-context.mjs') || _running?.endsWith('load-context.mjs/')) { - cli(); -} diff --git a/.trae/skills/impeccable/scripts/palette.mjs b/.trae/skills/impeccable/scripts/palette.mjs new file mode 100644 index 000000000..4c3f1751a --- /dev/null +++ b/.trae/skills/impeccable/scripts/palette.mjs @@ -0,0 +1,633 @@ +#!/usr/bin/env node +/** + * Brand-seed picker. Returns one OKLCH seed color + the mood it most + * naturally evokes, and teaches the model how to compose a full palette + * around it. + * + * The seed is the brand's anchor color. The 5-role palette (bg, surface, + * ink, accent, muted) is composed by the caller at runtime using their + * judgment + the brief (PRODUCT.md / DESIGN.md / user prompt), NOT picked + * from a frozen 4-color preset. + * + * Why: 4-color frozen palettes drift toward safe defaults (warm-cream bg, + * complementary accent on near-white) regardless of brief. A single seed + + * the model's own composition lets the same seed produce a dark-mode jazz + * club or a light-mode hospitality brand depending on what the brief calls + * for. Tested empirically against curated 4-color palettes; seed approach + * wins on mood-fit in 3 of 5 cases and ties on the rest. + * + * Usage: + * node scripts/palette.mjs # pick at random + * node scripts/palette.mjs --id seed-021 # pick a specific seed + * node scripts/palette.mjs --from # hash to a seed (deterministic) + * + * Env vars: + * IMPECCABLE_PALETTE_SEED — same as --from; useful for the eval harness + * to make runs reproducible. + */ + +import crypto from 'node:crypto'; + +// Seeds are inlined (129 entries, hand-curated via a tinder review of +// ~400 candidates from ColorHunt + synthesis + Radix/brand/Pantone anchors). +// Each carries a mood + strategy the judging model produced — surfaced as +// hints, not commands; the brief still drives composition. +const SEEDS = [ + { id: "seed-200", oklch: [0.360, 0.137, 0.0], + mood: "Aesop apothecary shelf — oxblood bottle glass against linen, considered and unhurried", + strategy: "Seed is a deep desaturated red-brown that reads as brand ink itself; I push primary darker toward bottle-glass oxblood, pair with a pure white surface so the red does the work, and use a clear pale-blush accent that can carry dark text in pills." }, + { id: "seed-000", oklch: [0.400, 0.130, 0.0], + mood: "oxblood leather banquette in a 1940s steakhouse — low lamplight on dark wood and burgundy", + strategy: "Near-black bg with the faintest red undertone lets the oxblood primary glow like lamplit leather; warm cream ink and a brass accent complete the chophouse register." }, + { id: "seed-002", oklch: [0.450, 0.150, 0.0], + mood: "darkroom red light — analog photography, blood-warm safelight glow on chemical trays", + strategy: "Near-black surface with a deep oxblood primary lets the seed function like a safelight in a darkroom — the bg disappears so the red becomes the only emotional signal." }, + { id: "seed-003", oklch: [0.500, 0.194, 0.0], + mood: "darkroom safelight — the deep oxblood glow of analog photography, chemical and contemplative", + strategy: "Anchored the seed as primary against pure near-black so the red reads like a single illuminated bulb in a developing room, with cool desaturated ink to evoke silver gelatin print tones." }, + { id: "seed-004", oklch: [0.546, 0.204, 3.4], + mood: "midnight boudoir — velvet rose under low lamplight, perfumed and intimate", + strategy: "Near-black surface lets the rose seed glow like silk in shadow; a warm champagne accent provides the candle-flame counterpoint without breaking the hush." }, + { id: "seed-005", oklch: [0.550, 0.180, 0.0], + mood: "smoldering vermillion at dusk — the last red ember in a blacksmith's forge, iron-rich and quietly violent", + strategy: "Near-black gallery surround lets the seed read as glowing forged metal; ink stays warm-off-white, accent shifts to a hotter ember orange so the primary feels like cooling steel against a fresh strike." }, + { id: "seed-201", oklch: [0.647, 0.262, 0.3], + mood: "Figma plugin marketplace red — confident product-brand crimson, the kind a modern dev tool uses for a 'live' indicator or a primary CTA on a pristine docs page", + strategy: "Pure white surface lets a high-chroma crimson primary do all the brand work, paired with a hue-shifted warm coral accent for hierarchy without competing saturation" }, + { id: "seed-006", oklch: [0.650, 0.160, 0.0], + mood: "1960s Italian cinema — Technicolor lipstick red against a darkened theater", + strategy: "Pure near-black surface lets a saturated cinematic red and its warm peach accent perform like film light projected in a dark room — the brand colors carry the drama, the bg disappears." }, + { id: "seed-008", oklch: [0.520, 0.200, 10.4], + mood: "Negroni hour at a Milanese bar — bittersweet crimson, vermouth and amaro under low tungsten", + strategy: "Seed is a saturated red-crimson with cinematic weight, so I sit it on near-black to let the primary glow like backlit liquor, with a warmer amber accent acting as the citrus twist against the bitter red." }, + { id: "seed-010", oklch: [0.563, 0.223, 11.0], + mood: "Negroni hour on a Milan rooftop — bittersweet crimson, aperitivo light, polished restraint", + strategy: "Seed is a vivid carmine-red with strong chroma, so the surface gets out of the way (pure white) and lets the primary do the aperitivo work, with a cooled garnet accent for tension." }, + { id: "seed-202", oklch: [0.643, 0.247, 7.0], + mood: "Glossier brand pink — modern beauty editorial, confident and current", + strategy: "Pure white bg lets a saturated rose-red primary do all the brand work, paired with a deeper crimson accent for hierarchy — the Stripe/Glossier move where the color carries the mood." }, + { id: "seed-013", oklch: [0.400, 0.130, 20.0], + mood: "Tuscan cellar at dusk — aged terracotta, oxidized iron, the deep red of decanted Sangiovese", + strategy: "Black surface lets the oxblood seed and copper accent glow like firelight on cellar stone; brand colors carry all the warmth while the room recedes." }, + { id: "seed-014", oklch: [0.450, 0.150, 20.0], + mood: "smoldering tannery — oxblood leather, cured under low workshop light", + strategy: "Anchor the deep oxblood seed as primary against a near-black architectural ground, then lift with a single warm ember accent so the leather reads burnished rather than bloody." }, + { id: "seed-016", oklch: [0.550, 0.180, 20.0], + mood: "Negroni hour on a Roman terrace — bitter campari red, vermouth, late golden light spilling on white linen", + strategy: "Pure white surface lets the campari-red primary do all the emotional work, paired with a deeper oxblood accent for bittersweet depth — Italian aperitivo restraint, not warmth-washed." }, + { id: "seed-205", oklch: [0.634, 0.254, 17.6], + mood: "Aesop apothecary bottle — considered red-coral on a clinical white surface, the kind of brand restraint where one saturated object does all the work", + strategy: "Default A pure white surface lets a single coral-red primary carry the entire brand voice; accent shifts to a deeper oxblood for hierarchy without competing chroma." }, + { id: "seed-011", oklch: [0.639, 0.207, 13.5], + mood: "Aperitivo hour in Milan — Campari glow on a white marble bar, crisp and effervescent", + strategy: "Pure white gallery backdrop lets the Campari-red primary ring like a single bitter note; ink is near-black with a whisper of warmth, accent shifts to a deeper oxblood for hierarchy without competing hues." }, + { id: "seed-015", oklch: [0.527, 0.202, 22.7], + mood: "Negroni hour on a Milanese terrace — bittersweet vermillion, aperitivo glassware catching low sun", + strategy: "Seed becomes a saturated aperitivo-red primary against pure white so the color carries the bittersweet warmth alone, paired with a deep oxblood accent for typographic gravitas." }, + { id: "seed-023", oklch: [0.427, 0.175, 29.2], + mood: "blacksmith's forge at dusk — iron heated to ember red, the deep glow of oxidized metal and quenching oil", + strategy: "Pure black bg lets the seed's ember-red glow radiate like hot iron in a dark forge; accent shifts to a copper-amber to suggest scaling metal and sparks, while ink stays near-white for tool-precise legibility." }, + { id: "seed-206", oklch: [0.614, 0.234, 28.2], + mood: "Aesop apothecary bottle — considered red-orange on lab-white, calm utility with a single confident pigment", + strategy: "Pure white surface lets a saturated vermilion primary do all the brand work, paired with a deep oxblood accent for hierarchy without introducing a second hue family" }, + { id: "seed-029", oklch: [0.665, 0.222, 25.7], + mood: "Negroni hour at a Milanese bar — bittersweet orange-red liqueur catching late afternoon light on polished marble", + strategy: "Pure white surface lets the seed's vermilion read like Campari in a glass; a deeper oxblood accent provides the bitter depth, with neutral graphite ink keeping the editorial restraint of Italian design." }, + { id: "seed-022", oklch: [0.418, 0.155, 27.2], + mood: "Pompeiian red fresco — oxidized cinnabar on a museum wall, archaeological gravity", + strategy: "Pure black gallery surface lets the seed's iron-oxide red read as a lit artifact; accent shifts to an aged terracotta amber, so primary and accent form a fired-clay duet against neutral void." }, + { id: "seed-024", oklch: [0.464, 0.169, 26.9], + mood: "Mid-century darkroom under the safelight — developer trays, oxblood leather, the quiet patience of a print emerging", + strategy: "Seed becomes a deep oxblood primary; surface stays pure black so the red glows like a safelight, with a warmer ember accent for hierarchy" }, + { id: "seed-026", oklch: [0.489, 0.190, 28.3], + mood: "smoldering ember in a blacksmith's forge — iron-hot rust, soot, and controlled fire", + strategy: "Near-black soot background lets the seed's red-orange glow like heated metal; ink is bone-white, accent is a cooler tempered-steel orange that creates internal heat gradient with the primary." }, + { id: "seed-027", oklch: [0.568, 0.208, 27.1], + mood: "Sicilian blood orange at golden hour — citrus rind, terracotta, sun on stucco", + strategy: "Seed reads as vivid blood-orange — picked pure white surface so the citrus-red primary and a deep oxblood accent do all the emotional work, like a Loro Piana editorial spread." }, + { id: "seed-028", oklch: [0.591, 0.172, 24.0], + mood: "Sienna-fired ceramic studio at dusk — terracotta cooling on a wheel, hands still dusted with slip", + strategy: "Pure black stage lets the fired-clay primary glow like a kiln ember, with a deeper oxblood accent providing tonal weight rather than hue contrast — a monochrome warm-axis play." }, + { id: "seed-033", oklch: [0.544, 0.169, 31.3], + mood: "1960s Italian terracotta workshop — fired clay, espresso, late-afternoon Mediterranean dust", + strategy: "Pure black ground lets the seed's burnt-sienna primary glow like a lit kiln, with a deeper oxblood accent for restrained warmth tension — the brand carries the heat, the surface stays out." }, + { id: "seed-207", oklch: [0.564, 0.231, 29.1], + mood: "Aesop apothecary bottle — considered red oxide, the calm authority of a well-made object on a white shelf", + strategy: "Seed becomes the singular brand voice against pure white, with a deeper oxblood accent for hierarchy — the surface disappears so the red does all the speaking." }, + { id: "seed-035", oklch: [0.663, 0.153, 32.1], + mood: "Aesop apothecary bottle — clay-fired warmth, considered retail", + strategy: "Pure white surface lets the terracotta primary do the brand work, paired with a deep umber ink and a cooler clay accent for editorial tension." }, + { id: "seed-037", oklch: [0.590, 0.188, 35.8], + mood: "Aesop apothecary bottle — considered terracotta, herbalist restraint, the warmth comes from the glass not the room", + strategy: "Seed becomes a muted terracotta primary against pure white so the brand's warmth carries entirely through the color itself; accent shifts to a deeper umber for quiet hierarchy." }, + { id: "seed-038", oklch: [0.652, 0.229, 34.8], + mood: "blown-glass furnace at dusk — molten orange iron pulled from the kiln, a craftsman's signature heat", + strategy: "Pure black stage so the seed reads as live ember; primary holds the seed's heat, accent shifts to a brass-amber a hue-step away for a 1.7+ contrast pairing without leaving the fire." }, + { id: "seed-039", oklch: [0.653, 0.185, 33.5], + mood: "Aesop apothecary bottle — considered terracotta, quiet retail craft", + strategy: "Seed becomes a grounded clay primary against pure white, paired with a deeper umber accent so the warmth lives entirely in the brand marks, not the surface." }, + { id: "seed-167", oklch: [0.495, 0.134, 36.0], + mood: "Aesop apothecary shelf — burnished terracotta on clinical white, considered craft pharmacy", + strategy: "Treat the seed as a brand-carrying burnt-sienna against a pure paper-white surface so the warmth lives entirely in the primary, with a deep umber accent pulled along the same warm axis for typographic gravity." }, + { id: "seed-147", oklch: [0.500, 0.151, 40.0], + mood: "Aesop apothecary shelf — considered terracotta, pharmacy restraint, the brand color does the work against clinical white", + strategy: "Anchor the seed's burnt-sienna primary against a pure white surface so the rust speaks alone, with a deep umber ink and a cooler clay accent to give the palette product-brand discipline rather than environmental warmth." }, + { id: "seed-040", oklch: [0.660, 0.201, 40.0], + mood: "Aesop apothecary bottle — amber glass on a clean dispensary shelf, considered and clinical-warm", + strategy: "Seed becomes a burnt-amber primary against pure white so the bottle-glass color does the emotional work; accent shifts to a deep olive-bronze for the apothecary-label pairing." }, + { id: "seed-041", oklch: [0.673, 0.217, 38.6], + mood: "Aesop apothecary shelf — considered orange glass, clinical retail restraint", + strategy: "Pure white surface lets the burnt-orange primary do all the brand work, with a deep ink-brown for editorial gravity and a muted clay accent that reads as a sibling, not a contrast." }, + { id: "seed-042", oklch: [0.688, 0.133, 35.8], + mood: "Aesop apothecary shelf — terracotta glass, considered retail", + strategy: "Seed becomes a warm clay primary against pure white so the bottle-on-marble retail feel comes from the brand color alone; a deeper umber accent gives the label-print contrast." }, + { id: "seed-043", oklch: [0.781, 0.119, 38.1], + mood: "Aesop apothecary catalogue — considered terracotta, dermatological restraint, the warm color doing all the work against clinical white", + strategy: "Pure white surface lets the seed's warm clay tone read as the entire brand voice, paired with a deeper umber accent for hierarchy without competing with the primary's warmth." }, + { id: "seed-168", oklch: [0.400, 0.103, 50.0], + mood: "Aesop apothecary bottle — amber glass on a clinical white shelf, considered and pharmaceutical", + strategy: "Pure white surface lets the deep amber primary act like tinted glass against a clean shelf; accent is a muted clay that complements without competing, keeping the brand quiet and product-led." }, + { id: "seed-044", oklch: [0.568, 0.149, 45.9], + mood: "1970s desert highway at golden hour — sun-faded terracotta, denim dust, the warmth of a Polaroid pulled from a glovebox", + strategy: "Seed becomes a burnt-sienna primary against pure white so the terracotta does all the emotional work; a deep indigo accent acts as the denim shadow opposing the sun, creating the era's signature warm/cool tension without tinting the page." }, + { id: "seed-045", oklch: [0.607, 0.163, 47.7], + mood: "Aesop apothecary shelf — considered amber glass, clinical restraint, craft pharmacy", + strategy: "Pure white bg lets the burnt-amber primary do the apothecary work alone, paired with a deeper umber accent and graphite ink for editorial calm." }, + { id: "seed-046", oklch: [0.653, 0.175, 45.0], + mood: "Aesop apothecary shelf — considered amber glass, quiet luxury, restrained craft", + strategy: "Pure black backdrop lets the warm amber primary glow like backlit apothecary glass, with a deeper rust accent providing tonal depth in the same hue family — monochromatic warm against neutral void." }, + { id: "seed-047", oklch: [0.695, 0.205, 43.2], + mood: "Aesop apothecary label — sun-warmed amber glass on a clinical countertop, restrained botanical pharmacy", + strategy: "Pure white surface lets the burnt-amber primary and a deeper sienna accent do all the brand work, like an apothecary bottle photographed under daylight." }, + { id: "seed-051", oklch: [0.704, 0.189, 49.0], + mood: "blacksmith's forge at dusk — glowing iron, hammered copper, ember light against cooling steel", + strategy: "Pure near-black surface lets the seed's molten orange burn like heated metal; accent shifts to a deeper amber-red to suggest the cooling end of the same iron, while ink stays a clean off-white so type reads like chalk on slate." }, + { id: "seed-171", oklch: [0.550, 0.124, 60.0], + mood: "Klim Type Foundry specimen page — considered ochre on paper, design-school-honest", + strategy: "Seed becomes a muted ochre primary on pure white; accent is a deep ink-navy pulled across the wheel for editorial contrast without warmth-pooling in the bg" }, + { id: "seed-148", oklch: [0.650, 0.146, 60.0], + mood: "Klim-style editorial gold — late-afternoon paper light on a serif specimen sheet, considered and dry", + strategy: "Hold the seed's amber as primary on a pure white page so the gold reads as ink rather than atmosphere, and pair with a deep aubergine accent for typographic contrast." }, + { id: "seed-052", oklch: [0.700, 0.130, 60.0], + mood: "late-afternoon terracotta studio — sun-warmed clay, hands-on craft, the hour before dusk", + strategy: "Seed is a saturated amber-ochre with strong environmental association (ceramics, adobe, sunlit plaster), so I lean into Exception (a) with a faintly warm bone surface that reads as lime-washed wall, then deepen the seed slightly for primary and pair it with a fired-clay rust accent for hand-thrown warmth." }, + { id: "seed-053", oklch: [0.773, 0.157, 56.6], + mood: "late-summer apricot orchard at golden hour — sun-warmed fruit, considered Californian craft", + strategy: "Seed is a juicy mid-warm orange at daylight luminance — leaning optimistic/editorial, so pure white surface lets the apricot primary glow without muddying it; a deep wine accent provides the bite." }, + { id: "seed-149", oklch: [0.600, 0.124, 70.0], + mood: "1970s desert highway — late-afternoon amber light on chrome and asphalt", + strategy: "Anchor the amber seed as primary against pure black so the warm hue reads as headlight glow against night; a cooler dusk-mauve accent provides the complementary tension of horizon vs. sun." }, + { id: "seed-054", oklch: [0.740, 0.162, 68.1], + mood: "late-afternoon honey on terracotta — Mediterranean stucco at golden hour, sun-baked amber", + strategy: "Seed is a saturated honey-amber at high lightness; pairing it with pure black lets the warmth read as luminous gold against gravity, like lamplight in a dark room." }, + { id: "seed-055", oklch: [0.774, 0.174, 65.1], + mood: "late-summer honey hour — amber light slanting through a west-facing window, optimistic and golden", + strategy: "Anchor a saturated honey-amber primary on pure white so the warmth radiates from the brand itself, then pair with a deep teak accent for grounded contrast rather than tinting the canvas." }, + { id: "seed-056", oklch: [0.691, 0.146, 74.6], + mood: "Klim-style modern publishing house — late-afternoon paper warmth, considered editorial gold", + strategy: "Pure white surface so the amber seed becomes the brand voice; ink stays near-black neutral and accent shifts to a deep ink-blue to give the gold something structural to lean on." }, + { id: "seed-150", oklch: [0.750, 0.148, 80.0], + mood: "Klim Type Foundry specimen page — late-summer editorial gold, considered and grown-up", + strategy: "Pure white surface lets a single restrained ochre primary do all the brand work, paired with a deep ink-blue accent for typographic contrast in the Klim/Commercial Type tradition." }, + { id: "seed-058", oklch: [0.764, 0.120, 77.1], + mood: "Klim Type Foundry specimen page — late-afternoon ochre, considered editorial typography", + strategy: "Pure white surface lets the ochre primary do the brand work, paired with a deep ink-blue accent for editorial contrast — the type-foundry move where one warm hue carries the whole feeling against neutral paper." }, + { id: "seed-059", oklch: [0.784, 0.144, 79.8], + mood: "late afternoon in a Tuscan limonaia — sun-cured amber on whitewashed plaster", + strategy: "Pure white surface lets the saffron-amber primary and a deep olive accent carry the Mediterranean warmth, with split-complementary tension between gold and a quiet evergreen." }, + { id: "seed-061", oklch: [0.817, 0.161, 75.1], + mood: "late-afternoon honey on Tuscan limestone — golden hour, slow and luminous", + strategy: "Pure white surface lets the amber primary glow like sunlight on a wall, paired with a deep terracotta accent for warm tonal contrast within the same hue family." }, + { id: "seed-063", oklch: [0.842, 0.165, 91.3], + mood: "late-afternoon Tuscan sun on limestone — golden hour, considered, optimistic", + strategy: "Pure white surface lets the amber-gold primary radiate as the mood-carrier, with a deep aubergine accent providing the long shadow that golden light needs to feel three-dimensional." }, + { id: "seed-174", oklch: [0.350, 0.075, 110.0], + mood: "olive grove at late afternoon — sun-cured leaves, dust, and quiet Mediterranean weight", + strategy: "Pure white surface lets a deep, sun-cured olive primary do the emotional work, with a burnt-terracotta accent providing the warm-earth counterpoint olive groves are known for." }, + { id: "seed-117", oklch: [0.650, 0.100, 110.0], + mood: "Klim-style editorial sage — late-summer foundry catalogue, considered olive-yellow on paper", + strategy: "Seed sits at olive-chartreuse; treating it as a quiet typographic primary on pure paper, with a deeper bronze-olive accent for hierarchy — the color does the work, the page disappears." }, + { id: "seed-118", oklch: [0.750, 0.090, 110.0], + mood: "Klim Type Foundry specimen page — late-summer olive light on a working specimen, the honesty of a type designer showing their work", + strategy: "Pure white bg lets a desaturated olive-yellow primary do the editorial work, with a deeper olive-bronze accent providing typographic emphasis the way a specimen uses one heavy weight against the body roman." }, + { id: "seed-065", oklch: [0.797, 0.166, 113.1], + mood: "late-summer olive grove at noon — sun-bleached leaves, dry stone, Mediterranean glare", + strategy: "Hold the seed as a luminous chartreuse-olive primary against pure white so the color reads as sunlit foliage, pairing it with a deep umber accent for the dry-stone contrast." }, + { id: "seed-176", oklch: [0.300, 0.071, 120.0], + mood: "moss-darkened apothecary jar — herbal, shadowed, mid-19th-century botanical study", + strategy: "Seed is a deep desaturated olive-green that reads as preserved botanical pigment; I anchor it on pure white so the dim moss-green primary feels like ink on a herbarium page, with a warm ochre accent supplying the aged-paper counterpoint." }, + { id: "seed-155", oklch: [0.550, 0.142, 130.0], + mood: "moss-bed forest floor at noon — chlorophyll, lichen, sunlit fern", + strategy: "Seed is a confident mid-olive green with strong chroma; mood is daylight botanical, so I let the brand greens do the work on a pure paper-white bg and pair with a warm umber accent for fern-against-bark contrast." }, + { id: "seed-119", oklch: [0.600, 0.154, 130.0], + mood: "moss garden at Saihō-ji — damp stone, filtered green light through old cedar", + strategy: "Pure near-black bg lets the seed's mossy green glow like wet lichen under low light; accent shifts to a pale ochre-gold like sun catching through canopy." }, + { id: "seed-179", oklch: [0.300, 0.096, 140.0], + mood: "moss on wet stone — forest floor at dusk, deep botanical hush", + strategy: "Kept the seed's deep moss green as primary against a near-black surface so the green reads as living shadow, with a pale lichen accent providing the single point of light." }, + { id: "seed-180", oklch: [0.350, 0.110, 140.0], + mood: "moss-darkened apothecary — herbal tinctures in amber glass, pressed botanicals, the deep green of a conservatory at dusk", + strategy: "Near-black bg with a whisper of green undertone lets the seed's deep moss read as luminous foliage; a warm parchment accent provides the apothecary-label counterpoint without breaking the herbal register." }, + { id: "seed-120", oklch: [0.650, 0.100, 140.0], + mood: "moss on weathered stone — quiet botanical garden conservatory at midday", + strategy: "Pure white bg lets the muted sage-green primary read as a considered botanical mark, with a deeper terracotta accent providing earthen counterpoint without breaking the gallery-like restraint." }, + { id: "seed-121", oklch: [0.750, 0.090, 140.0], + mood: "moss garden at Saihō-ji — diffuse green light filtered through wet stone and lichen", + strategy: "Pure near-black bg lets the muted sage-green primary glow like lichen under low light; a warm pale-bone accent acts as the single ray of sun cutting through canopy." }, + { id: "seed-182", oklch: [0.400, 0.106, 150.0], + mood: "moss garden at Saiho-ji — deep cultivated green under wet stone shadow, contemplative and damp", + strategy: "Near-black bg with the faintest cool-green undertone evokes shaded stone; primary holds the seed's moss tone while accent shifts to a lichen-yellow for organic counterpoint without breaking the hush." }, + { id: "seed-157", oklch: [0.550, 0.145, 150.0], + mood: "moss garden at Saiho-ji — damp stone, filtered green light through cedar canopy", + strategy: "Near-black bg with a faint green undertone evokes deep forest shadow; primary holds the seed's verdant register while accent shifts to a pale lichen-cream to mimic light catching moss." }, + { id: "seed-122", oklch: [0.600, 0.158, 150.0], + mood: "forest floor at first light — moss, lichen, and clean morning air", + strategy: "Seed reads as a living, daylight green; surface stays pure white so the green carries the freshness, with a cool teal accent pulling it toward dew rather than earth." }, + { id: "seed-195", oklch: [0.650, 0.150, 145.0], + mood: "Considered horticulture brand — botanical research lab, the green of a healthy stem photographed in clean daylight", + strategy: "Pure white surface lets the seed's vegetal green carry the entire brand voice, paired with a deep forest ink and a warm clay accent for editorial contrast." }, + { id: "seed-183", oklch: [0.350, 0.077, 160.0], + mood: "moss-stained apothecary — deep forest glass, herbal tinctures shelved in low candlelight", + strategy: "Anchored the seed as primary and built a near-black dark surface with whisper-tinted green to evoke aged apothecary glass, letting the green glow rather than shout." }, + { id: "seed-184", oklch: [0.400, 0.087, 160.0], + mood: "deep forest apothecary — moss, bottle glass, and herbal tincture under afternoon light", + strategy: "Seed becomes a botanical-bottle-green primary on pure white, paired with a warm clove-amber accent to evoke herbal pharmacy contrast without tinting the surface." }, + { id: "seed-158", oklch: [0.550, 0.119, 160.0], + mood: "moss on wet stone — forest floor after rain, mineral and quiet", + strategy: "Pure white surface lets the deep mossy green carry the entire mood; accent shifts to a damp slate-teal to sit beside primary like lichen on stone without competing." }, + { id: "seed-159", oklch: [0.600, 0.130, 160.0], + mood: "moss-covered forest apothecary — herbal tinctures in amber glass, eucalyptus shadow", + strategy: "Anchored the green seed in a near-black backdrop so it reads like botanical glassware lit from within, with a warm amber accent pulled across the wheel to evoke tincture bottles against dark wood." }, + { id: "seed-185", oklch: [0.450, 0.086, 170.0], + mood: "weathered copper patina on a Pacific Northwest greenhouse — oxidized teal, glass light, botanical hush", + strategy: "Seed sits as a deep oxidized-teal primary against pure white so the patina reads as pigment, not atmosphere; a rust-copper accent completes the verdigris/oxidation story across the warm-cool axis." }, + { id: "seed-124", oklch: [0.750, 0.080, 170.0], + mood: "sea-glass on a foggy Pacific shoreline — weathered, mineral, quietly oxidized", + strategy: "Seed is a soft desaturated teal-green; pairing it on pure white lets the mineral primary read as patinated copper-glass, with a deeper kelp-toned primary and a rusted coral accent to spark the muted teal against its complement." }, + { id: "seed-160", oklch: [0.550, 0.095, 180.0], + mood: "weathered copper patina on a museum bronze — oxidized teal, conservatorial quiet", + strategy: "Pure near-black gallery surround lets the patina-teal primary glow like a lit artifact, with a warm verdigris-adjacent accent providing the oxidation contrast against the cool seed." }, + { id: "seed-161", oklch: [0.720, 0.100, 188.0], + mood: "climate-tech dashboard — calm verdigris on plain paper, the quiet confidence of an instrument that just works", + strategy: "Seed teal carries the entire mood as a single considered brand color on pure white, with a desaturated copper accent providing warm signal against the cool primary without competing for attention." }, + { id: "seed-186", oklch: [0.450, 0.074, 200.0], + mood: "deep hydrothermal vent — mineral teal under pressure, the cold blue-green of oxidized copper in submerged light", + strategy: "Near-black surface lets the mineral teal glow as if lit from within; accent shifts toward verdigris-copper to suggest patina on submerged metal, while ink stays cool-neutral to keep the register austere rather than aquatic-cute." }, + { id: "seed-125", oklch: [0.650, 0.100, 200.0], + mood: "climate-tech dashboard — calm operational teal, the color of clean water data and atmospheric sensors", + strategy: "Pure white surface lets a single muted-teal primary do all the brand work, with a deeper marine accent providing hierarchy without competing chroma." }, + { id: "seed-126", oklch: [0.750, 0.080, 200.0], + mood: "climate-tech product brand — quiet competence, dashboards for hard infrastructure problems", + strategy: "Hold the seed's muted teal as primary, pair with a sharper cyan-leaning accent for interactive lift, and let a pure white surface do the disappearing act so the brand reads as a tool, not an atmosphere." }, + { id: "seed-162", oklch: [0.550, 0.091, 210.0], + mood: "weathered nautical instrument — patinated brass on oxidized steel, the cool blue-grey of a ship's chronometer at dawn", + strategy: "Pure white surface lets the muted teal-steel primary read as a precise instrument mark, with a warm brass accent providing the single point of patina against clinical white." }, + { id: "seed-163", oklch: [0.450, 0.086, 230.0], + mood: "deep harbor at dusk — weathered nautical instruments, brass dials on oxidized steel", + strategy: "Near-black background with subtle cool tint evokes the marine dusk; primary holds the seed's teal-blue while a warm brass accent creates the instrument-on-steel tension." }, + { id: "seed-164", oklch: [0.550, 0.105, 230.0], + mood: "deep harbor at dawn — cold steel water, fog-muted light, the quiet before the boats leave", + strategy: "Pure near-black bg lets the seed's cold marine blue read as a luminous beacon, while a pale frost-cyan accent evokes diffused dawn light cutting through fog." }, + { id: "seed-127", oklch: [0.650, 0.100, 230.0], + mood: "climate-tech dashboard — atmospheric sensor blue, calm operational clarity", + strategy: "Anchor the seed as a confident mid-blue primary on pure white so the brand color carries all the atmospheric feeling, with a deep navy accent for hierarchy and a soft slate muted for body text." }, + { id: "seed-128", oklch: [0.750, 0.080, 230.0], + mood: "climate-tech dashboard — calm atmospheric data, considered sky-blue", + strategy: "Pure white surface lets the muted sky-blue primary carry the meteorological calm, with a deep-navy accent providing readable weight against the soft primary." }, + { id: "seed-187", oklch: [0.350, 0.078, 240.0], + mood: "deep harbor at blue hour — wet stone, cold steel, the quiet before night fully lands", + strategy: "Near-black architectural bg with a hint of marine chroma lets the seed read as ambient atmosphere rather than UI chrome; a cooler steel accent sits opposite the warmer-shifted primary for navigational clarity." }, + { id: "seed-077", oklch: [0.578, 0.130, 241.7], + mood: "pre-dawn signal tower — cold blue solitude, instruments glowing against the dark", + strategy: "Pure near-black bg lets the seed's cold tower-light blue glow as the sole emotional source, with a frost-cyan accent acting as a secondary indicator light." }, + { id: "seed-188", oklch: [0.400, 0.110, 250.0], + mood: "Linear's considered indigo — the calm authority of a well-built developer tool, blueprint ink on a clean page", + strategy: "Held the seed as a deep indigo primary against pure white so the brand color carries all the gravity; accent shifts to a cooler, brighter cyan-blue to create a crisp hierarchy pair without warming the surface." }, + { id: "seed-165", oklch: [0.450, 0.123, 250.0], + mood: "blueprint room at dusk — drafting table, graphite, civic-engineering blue", + strategy: "Seed is a mid-deep architectural blue with real chroma and no environmental cue, so I stay out of the way with a pure white surface and let the primary do all the talking, pairing it with a burnt-ochre accent for drafting-pencil contrast." }, + { id: "seed-079", oklch: [0.478, 0.136, 251.8], + mood: "twilight cartography — the blue of deep dusk over open water, precise and navigational", + strategy: "Pure white surface lets the seed's oceanic blue act as a single navigational anchor, with a warm amber accent struck across it like a lighthouse beam at dusk." }, + { id: "seed-080", oklch: [0.541, 0.122, 248.2], + mood: "Linear-style considered tool blue — the calm, exact register of a modern engineering app where every pixel is intentional", + strategy: "Pure white surface lets the considered indigo-blue primary carry the entire brand; a deeper navy accent provides hierarchy without warmth, keeping the palette in a single cool family for that focused-software feel" }, + { id: "seed-166", oklch: [0.550, 0.149, 250.0], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and quietly intense", + strategy: "Near-black bg with the faintest cool tint reads like a darkened cockpit; the seed becomes a luminous instrument-blue primary, paired with a warm amber accent that mimics avionics readouts for unmistakable signal contrast." }, + { id: "seed-081", oklch: [0.650, 0.160, 250.0], + mood: "deep-sea research vessel at dawn — instrument glow against cold steel light", + strategy: "Pure near-white bg keeps the palette technical and instrument-like; the seed blue holds as primary while a desaturated steel-cyan accent reads like signal readouts on glass." }, + { id: "seed-082", oklch: [0.742, 0.140, 247.4], + mood: "high-altitude flight deck at dawn — cold cabin instruments glowing against a sky still holding night", + strategy: "Near-black cockpit ground with a faint blue cast lets the seed read as an illuminated instrument; primary holds the seed, accent shifts to cyan for signal/indicator contrast." }, + { id: "seed-210", oklch: [0.360, 0.140, 260.0], + mood: "Linear-style considered tool indigo — late-night focused work, the deep blue of a code editor at 2am where everything else falls away", + strategy: "Pure black bg lets the indigo primary carry all the cognitive-focus weight, with a slightly brighter periwinkle accent for interactive lift — the surface disappears so the tool feels weightless." }, + { id: "seed-189", oklch: [0.400, 0.130, 260.0], + mood: "pre-dawn observatory — cold instrument blue, star-chart precision", + strategy: "Seed becomes the primary on pure black so the deep instrument-blue glows like a calibration light, with a faint cyan accent reading as starlight against the void." }, + { id: "seed-211", oklch: [0.420, 0.161, 260.0], + mood: "Linear's considered indigo — the tool-for-thought blue of focused product work, calm authority without coldness", + strategy: "Hold the seed as a deep indigo primary against pure white, then pair with a slightly warmer, lighter periwinkle accent to create gentle hue separation without breaking the disciplined tool-brand register." }, + { id: "seed-129", oklch: [0.450, 0.150, 260.0], + mood: "pre-dawn observatory — deep cobalt sky just before astronomical twilight, instruments cool to the touch", + strategy: "Near-black surface lets the cobalt seed read as luminous starlight; a single warm amber accent acts as the calibration lamp against the cold blue field." }, + { id: "seed-084", oklch: [0.476, 0.207, 261.2], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and awake", + strategy: "Default B black bg lets the cobalt primary read as a luminous instrument signal, with a cyan accent striking the analogous 'cockpit display' relationship." }, + { id: "seed-085", oklch: [0.681, 0.132, 258.4], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky", + strategy: "Anchored the seed as a luminous primary against a near-black architectural ground, with a warm amber accent acting as the single instrument light cutting through cold blue." }, + { id: "seed-086", oklch: [0.767, 0.106, 255.9], + mood: "Scandinavian winter morning — quiet light through frost, pale sky over snow", + strategy: "Anchored a pure white editorial stage so the seed's cool sky-blue reads as crisp polar light, with a deeper navy primary providing the only saturated weight — like a single dark pine against snow." }, + { id: "seed-083", oklch: [0.340, 0.159, 262.4], + mood: "deep cobalt twilight — the moment after sunset when the sky goes electric blue and city windows start to glow", + strategy: "Pure black stage lets the cobalt seed act as a luminous neon-window glow, with a warm amber accent across the wheel for the lit-window contrast." }, + { id: "seed-212", oklch: [0.360, 0.219, 270.0], + mood: "Linear-grade tooling indigo — considered software for people who care about craft", + strategy: "Anchored the deep indigo seed as primary on a pure white surface so the brand color carries all the weight, with a slightly cooler violet-blue accent for hierarchy without competing chroma." }, + { id: "seed-130", oklch: [0.400, 0.150, 270.0], + mood: "Linear-grade indigo — considered productivity tool, ink on paper, no theatrics", + strategy: "Pure white surface lets a deep cool indigo carry all the brand weight, paired with a slightly warmer violet-blue accent for hierarchy without acid." }, + { id: "seed-213", oklch: [0.411, 0.241, 267.9], + mood: "Linear-style indigo — considered tool surface, the kind of blue-violet that sits behind a developer's keyboard at 11pm without shouting", + strategy: "Pure black canvas lets a saturated indigo primary do all the brand work, with a cooler cyan-violet accent providing UI signal without competing." }, + { id: "seed-131", oklch: [0.450, 0.180, 270.0], + mood: "monastic indigo dusk — vespers light through stained glass, contemplative and severe", + strategy: "Seed becomes a deep indigo primary against pure near-black so the violet reads as luminous stained-glass against architectural shadow, with a cooler iris accent for tonal lift." }, + { id: "seed-088", oklch: [0.476, 0.158, 268.5], + mood: "pre-dawn astronomer's notebook — deep indigo sky just before the stars fade, ink and graphite", + strategy: "Near-black bg with the faintest cool tint to evoke night sky without theatrics; primary holds the seed's indigo, accent shifts to a paler periwinkle for stellar contrast, keeping the palette monochromatic-cool and observational." }, + { id: "seed-196", oklch: [0.530, 0.130, 268.0], + mood: "Linear-style considered tool indigo — the deep-focus blue-violet of a thoughtfully built productivity surface, the color of a well-typeset keyboard shortcut", + strategy: "Pure white bg lets the indigo seed do all the brand work as primary, with a slightly darker, more saturated violet-shifted accent for hierarchy and interactive states — the surface disappears so the brand color reads as the entire identity." }, + { id: "seed-132", oklch: [0.700, 0.120, 270.0], + mood: "Linear-style considered tool indigo — the quiet violet of a focused product workspace, late-afternoon thinking", + strategy: "Pure white surface lets a muted indigo-violet primary and a slightly cooler accent do all the brand work, keeping the register calm and software-like rather than theatrical." }, + { id: "seed-090", oklch: [0.445, 0.206, 279.1], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, not a nightclub", + strategy: "Anchor the seed as a confident product primary on pure white, with a cooler indigo-shift accent that reads as a sibling tool color, so the brand violet does all the emotional work." }, + { id: "seed-133", oklch: [0.500, 0.160, 280.0], + mood: "Linear-adjacent indigo — considered productivity tool, the violet of a thinking workspace", + strategy: "Seed becomes a measured indigo primary on pure white; accent shifts to a cooler blue-violet to create hierarchy without nightclub saturation, letting the brand color do all the emotional work." }, + { id: "seed-094", oklch: [0.533, 0.125, 294.3], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, calm authority for a creative workspace", + strategy: "Pure white canvas lets the indigo-violet primary carry the entire brand voice; accent shifts hue slightly toward blue for a cool, tool-like duotone rather than warm decorative pairing." }, + { id: "seed-137", oklch: [0.700, 0.120, 290.0], + mood: "Linear-adjacent indigo — the considered tool, late-evening focus mode, software made for people who care about craft", + strategy: "Pure black surface lets a single restrained indigo-violet carry the brand, with a cooler periwinkle accent providing UI hierarchy without competing — Vercel/Linear dark-mode discipline." }, + { id: "seed-100", oklch: [0.450, 0.150, 330.0], + mood: "velvet boudoir at last call — bruised orchid and lipstick traces under low lamplight", + strategy: "Pure near-black surface lets a deep magenta-rose primary smolder while a warm peach accent acts like skin-lit lamplight — drama lives in the brand pair, not the room." }, + { id: "seed-103", oklch: [0.650, 0.160, 330.0], + mood: "1980s Memphis boudoir — powder-pink neon humming against lacquered black, lipstick and lacquer", + strategy: "Near-black gallery surface lets the magenta-pink seed read as lit neon; accent shifts to warm coral to create cinematic dichromatic tension without competing chroma." }, + { id: "seed-228", oklch: [0.360, 0.147, 340.0], + mood: "Figma-era creative tool plum — considered productivity software for designers, the inky violet of a serif wordmark on a marketing site", + strategy: "Held the seed as a deep plum primary against pure white so the brand color does the emotional work; paired with a muted rose accent for warmth without breaking the productivity-tool restraint." }, + { id: "seed-107", oklch: [0.500, 0.200, 340.0], + mood: "Figma plum — creative-tool confidence, considered magenta for a modern design product", + strategy: "Pure white surface lets a saturated magenta-plum primary carry all the brand voice, paired with a cooler violet-leaning accent for hierarchy without competing." }, + { id: "seed-198", oklch: [0.600, 0.210, 340.0], + mood: "Figma-era creative tool plum — confident, considered, made for makers", + strategy: "Anchor a saturated plum primary against pure white so the brand color does all the emotional work, with a deeper magenta-rose accent for hierarchy." }, + { id: "seed-112", oklch: [0.754, 0.193, 343.4], + mood: "Figma-era creative tool — confident pink primary doing the brand work on a clean canvas, the way Linear uses indigo or Stripe uses violet", + strategy: "Anchor the seed pink as a saturated brand primary on pure white so the color carries all the personality; pair with a cooler plum accent to give the pink something to push against without competing." }, + { id: "seed-229", oklch: [0.420, 0.163, 350.0], + mood: "considered fintech rose — the deep magenta of a modern product brand (think Stripe-adjacent, but rotated toward berry), confident and current", + strategy: "pure white surface lets a single deep berry-rose primary do all the brand work, paired with a cooler indigo accent for the contrast move you see in modern product marketing" }, + { id: "seed-113", oklch: [0.470, 0.173, 354.8], + mood: "1960s velvet rope nightclub — crushed magenta, low light, cigarette smoke catching a spotlight", + strategy: "Pure black stage so the seed's smoky magenta reads as a single hot spotlight, paired with a cooler violet accent for the second light cue." }, + { id: "seed-114", oklch: [0.570, 0.158, 353.3], + mood: "fin-de-siècle Parisian rose — velvet curtain, theatre program, lipstick blotted on linen", + strategy: "Drop bg to true black so the dusty-rose primary reads as stage-lit silk; accent shifts to a warmer coral-mauve at higher lightness to create gentle hue rotation without breaking the romance." }, + { id: "seed-199", oklch: [0.650, 0.180, 350.0], + mood: "modern fintech rose — the considered pink of a Series B brand mark, confident and current without nostalgia", + strategy: "Pure white surface lets a saturated rose primary do the brand work, paired with a deep plum accent for hierarchy — the Stripe move applied to a pink hue." }, + { id: "seed-115", oklch: [0.636, 0.218, 355.3], + mood: "backstage at a cabaret — velvet rope, lipstick mark on a champagne glass", + strategy: "Seed reads as a saturated stage-light magenta-red; I push it into pure black so the primary glows like a neon sign and the accent (a cold pearl-pink) acts as the spotlight rim — the room is dark, the color does the singing." }, + { id: "seed-230", oklch: [0.650, 0.249, 354.5], + mood: "Modern fintech rose — the considered pink of a contemporary payments brand: confident, alive, and clear-headed", + strategy: "Pure white bg lets a saturated rose-magenta primary carry all the brand energy, paired with a cooler indigo accent for trustworthy contrast — the Stripe move applied to a pink hue." }, + { id: "seed-231", oklch: [0.682, 0.241, 353.2], + mood: "Figma-era creative tool — a confident pink-magenta product brand, the kind a modern design platform uses to feel alive without shouting", + strategy: "Default A pure white bg lets the saturated pink-magenta primary do all the brand work, with a near-complementary cool teal accent for tool-like clarity and a neutral ink for editorial calm" }, + { id: "seed-116", oklch: [0.734, 0.183, 356.8], + mood: "modern beauty brand DTC — Glossier-adjacent pink, confident and current without being saccharine", + 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." }, +]; + +function parseArgs(argv) { + const args = { id: null, from: null }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === '--id' && argv[i + 1]) { args.id = argv[++i]; } + else if (a === '--from' && argv[i + 1]) { args.from = argv[++i]; } + } + return args; +} + +// Hash a key into a stable float in [0, 1) for deterministic weighted picks. +function hashUnit(key) { + const h = crypto.createHash('sha256').update(key).digest(); + return h.readUInt32BE(0) / 0x100000000; +} + +// The curated library is hue-skewed (more reds/oranges than teals/magentas) +// because that's where the source material + taste landed. Left uniform, a +// random pick would land on red ~1/3 of the time. Inverse-frequency weighting +// gives each seed a weight of 1/(count in its 30° hue bucket), so each hue +// ZONE is roughly equally likely to be chosen regardless of how many seeds it +// holds — fair rainbow exposure across runs without pruning the library. +function buildWeights(seeds) { + const bucketCount = {}; + const bucketOf = (s) => Math.floor(((s.oklch[2] % 360) + 360) % 360 / 30); + for (const s of seeds) { const b = bucketOf(s); bucketCount[b] = (bucketCount[b] || 0) + 1; } + const weights = seeds.map((s) => 1 / bucketCount[bucketOf(s)]); + const total = weights.reduce((a, b) => a + b, 0); + return { weights, total }; +} + +function weightedPick(seeds, unit) { + const { weights, total } = buildWeights(seeds); + let target = unit * total; + for (let i = 0; i < seeds.length; i++) { + target -= weights[i]; + if (target < 0) return seeds[i]; + } + return seeds[seeds.length - 1]; +} + +function pickSeed(seeds, { id, from }) { + if (id) { + const found = seeds.find(s => s.id === id); + if (!found) { console.error(`no seed with id "${id}"`); process.exit(2); } + return found; + } + const envFrom = process.env.IMPECCABLE_PALETTE_SEED; + const key = from || envFrom; + const unit = key ? hashUnit(key) : Math.random(); + return weightedPick(seeds, unit); +} + +function fmtOklch([L, C, H]) { + return `oklch(${L.toFixed(3)} ${C.toFixed(3)} ${H.toFixed(1)})`; +} + +function hueWord(H) { + if (H < 15 || H >= 345) return 'pure red'; + if (H < 35) return 'warm red / crimson'; + if (H < 55) return 'warm coral / burnt orange'; + if (H < 80) return 'orange / honey'; + if (H < 105) return 'warm amber / honey-gold'; + if (H < 135) return 'yellow-green / olive'; + if (H < 170) return 'green'; + if (H < 200) return 'teal'; + if (H < 230) return 'sky blue'; + if (H < 265) return 'cobalt / indigo'; + if (H < 295) return 'violet / purple'; + if (H < 330) return 'magenta / pink'; + return 'deep pink / rose'; +} + +// --------------------------------------------------------------- + +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}` : ''; + +// --------------------------------------------------------------- +// Fat tool-exit response — what the model sees on stdout. +// --------------------------------------------------------------- + +process.stdout.write(`BRAND SEED · ${seed.id} + +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. + +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. + +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é. + + 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 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. + + 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. + +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): + + - 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. + +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. + +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. +`); diff --git a/.trae/skills/impeccable/scripts/pin.mjs b/.trae/skills/impeccable/scripts/pin.mjs index ba02783fb..320d98ce1 100644 --- a/.trae/skills/impeccable/scripts/pin.mjs +++ b/.trae/skills/impeccable/scripts/pin.mjs @@ -27,7 +27,7 @@ const HARNESS_DIRS = [ // Valid sub-command names const VALID_COMMANDS = [ - 'craft', 'teach', 'extract', 'document', 'shape', + 'craft', 'init', 'extract', 'document', 'shape', 'critique', 'audit', 'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live', 'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive', diff --git a/AGENTS.md b/AGENTS.md index 7babd8c76..ce2d3b295 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## Skill bootstrap: skip the post-update-cleanup step -If you load the Impeccable skill from `.agents/skills/impeccable/SKILL.md` (or any sibling harness directory under this repo), it begins with a `` block that runs `scripts/cleanup-deprecated.mjs` and then self-deletes. **Do not run it here, and do not delete the block.** This repo is the source of truth for the skill; cleanup-deprecated assumes a consumer project and would touch legitimate source files. The block is regenerated from `skill/SKILL.md` on every `bun run build`, so any deletion churns back. Skip it silently and proceed with the task. +If you load the Impeccable skill from `.agents/skills/impeccable/SKILL.md` (or any sibling harness directory under this repo), it begins with a `` block that runs `scripts/cleanup-deprecated.mjs` and then self-deletes. **Do not run it here, and do not delete the block.** This repo is the source of truth for the skill; cleanup-deprecated assumes a consumer project and would touch legitimate source files. The block is regenerated from `skill/SKILL.src.md` on every `bun run build`, so any deletion churns back. Skip it silently and proceed with the task. ## Project Structure & Module Organization @@ -16,6 +16,7 @@ If you load the Impeccable skill from `.agents/skills/impeccable/SKILL.md` (or a - `bun test tests/build.test.js` - run a focused Bun test. - `bun run test` - run the full Bun + Node test suite. - `bun run test:live-e2e` - opt-in live-mode E2E against framework fixtures (~2 min; needs `npx playwright install chromium` once). +- `bun run test:skill-behavior` - opt-in LLM-backed checks that the SKILL.md Setup flow actually drives the agent (~5 min; runs claude-sonnet-4-6 / gpt-5.5 / gemini-3.1-flash-lite, roughly $0.50-1.50 per run on the production-tier models, needs `.env` with provider keys). - `bun run build:browser` / `bun run build:extension` - rebuild browser-specific bundles. Run `bun run build` after changing anything in `source/`, transformer code, or user-facing counts. @@ -41,6 +42,8 @@ For changes to `skill/scripts/live-*.{mjs,js}`, also run `bun run test:live-e2e` Set `IMPECCABLE_E2E_AGENT=llm` to swap the deterministic fake agent for an API-backed one (`tests/live-e2e/agents/llm-agent.mjs`). Claude Haiku 4.5 is the primary path whenever `ANTHROPIC_API_KEY` is set. DeepSeek V4 Flash is the secondary cheap fallback when only `DEEPSEEK_API_KEY` is set, and can be forced with `IMPECCABLE_E2E_LLM_PROVIDER=deepseek` or `bun run test:live-e2e -- --llm-provider=deepseek`; override either model via `IMPECCABLE_E2E_LLM_MODEL` or `--llm-model=`. Tests skip cleanly when the selected provider key is unset. This path hits the API — use it for verification, not CI. +For changes to `skill/SKILL.src.md`'s Setup section, `skill/scripts/context.mjs`, or any Setup-touching reference file (`init.md`, `document.md`, `brand.md`, `product.md`, sub-command refs), also run `bun run test:skill-behavior`. The suite spawns real LLMs (claude-sonnet-4-6, gpt-5.5, gemini-3.1-flash-lite, all three, every run) with the source SKILL.md inlined as system prompt and a workspace-scoped tool set, then asserts on the tool-call trace. Provider keys live in repo-root `.env`; missing keys skip cleanly. Scope to one provider with `IMPECCABLE_SKILL_BEHAVIOR_MODELS=`; add `IMPECCABLE_SKILL_BEHAVIOR_VERBOSE=1` to dump per-scenario traces. Baseline (21-22/24) and per-scenario assertions live in `tests/skill-behavior/README.md`. + ## Anti-pattern detection rules `cli/engine/detect-antipatterns.mjs` is the source of truth for the rule engine. It feeds the CLI, the site overlay (`cli/engine/detect-antipatterns-browser.js`, regenerated by `bun run build:browser`), the Chrome extension (`extension/detector/`, regenerated by `bun run build:extension`), and the homepage `DETECTION_COUNT` in `site/public/js/generated/counts.js` (regenerated by `bun run build`). After any rule change run all three builds plus `bun run test` so nothing drifts. @@ -52,7 +55,7 @@ TDD order is non-negotiable: 3. Add the rule entry to the `ANTIPATTERNS` array (`id`, `category` = `slop` or `quality`, `name`, `description`, optional `skillSection` / `skillGuideline`). 4. Implement a pure `checkXxx(opts)` returning `[{ id, snippet }]` — no DOM access inside. 5. Add two adapters that wrap the pure check: `checkElementXxxDOM(el)` for the browser (`getComputedStyle` + `getBoundingClientRect`) and `checkElementXxx(el, tag, window)` for jsdom (`parseFloat(style.width)` instead of layout). Wire **both** adapters into **both** element loops in `cli/engine/detect-antipatterns.mjs` (browser loop ~line 1837, jsdom loop in `detectHtml` ~line 2058). Forgetting one is the most common mistake. -6. Verify on a live page at `http://localhost:3000/fixtures/antipatterns/{rule-id}.html` and on the homepage. The two adapter paths can disagree. +6. Verify on a live page at `http://localhost:4321/fixtures/antipatterns/{rule-id}.html` and on the homepage. The two adapter paths can disagree. Conventions: wrap the identifying heading text in straight double quotes inside snippets so the fixture test can extract it. jsdom-specific helpers `resolveBackground()`, `resolveGradientStops()`, and `parseGradientColors()` exist because `background:` shorthand isn't decomposed and computed colors aren't normalized in jsdom — use them. Reference rules to copy from: `side-tab` (border), `low-contrast` (color+gradient), `icon-tile-stack` (sibling relationship), `flat-type-hierarchy` (page-level). diff --git a/CLAUDE.md b/CLAUDE.md index 06312d3be..96f8bdcd9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ ## Skill bootstrap: skip the post-update-cleanup step -The `` block at the top of `.claude/skills/impeccable/SKILL.md` runs `scripts/cleanup-deprecated.mjs` and then self-deletes. **Do not run it in this repo, and do not delete the block.** This repo is the source of truth for the Impeccable skill; the cleanup script scans for "leftover" skill files to remove and its judgment doesn't fit a repo where the skill lives in `skill/`. The block is also regenerated from `skill/SKILL.md` on every `bun run build`, so any deletion churns back on the next build. Skip the step silently and proceed. +The `` block at the top of `.claude/skills/impeccable/SKILL.md` runs `scripts/cleanup-deprecated.mjs` and then self-deletes. **Do not run it in this repo, and do not delete the block.** This repo is the source of truth for the Impeccable skill; the cleanup script scans for "leftover" skill files to remove and its judgment doesn't fit a repo where the skill lives in `skill/`. The block is also regenerated from `skill/SKILL.src.md` on every `bun run build`, so any deletion churns back on the next build. Skip the step silently and proceed. Same rule for AGENTS.md and every other harness-specific instruction file: treat post-update-cleanup as a no-op in this repo. @@ -72,7 +72,7 @@ Skill editorials and tutorials are read by `scripts/build.js` (for taglines and ## Development Server ```bash -bun run dev # Bun dev server at http://localhost:3000 +bun run dev # Bun dev server at http://localhost:4321 bun run preview # Build + Cloudflare Pages local preview ``` @@ -118,8 +118,9 @@ Local state files inside harness directories (e.g. `.claude/scheduled_tasks.lock ## Testing ```bash -bun run test # Default suite: unit + static framework fixtures -bun run test:live-e2e # Opt-in: full-cycle live-mode E2E across framework fixtures +bun run test # Default suite: unit + static framework fixtures +bun run test:live-e2e # Opt-in: full-cycle live-mode E2E across framework fixtures +bun run test:skill-behavior # Opt-in: LLM-backed checks that the skill text actually drives the agent's setup flow ``` Unit tests (build orchestration, detector logic) run via `bun test`. Fixture tests (jsdom-based HTML detection) run via `node --test` because bun is too slow with jsdom. The `test` script handles this split automatically. @@ -146,6 +147,39 @@ The agent is pluggable via a one-method interface in `tests/live-e2e/agent.mjs`: Adding a new fixture is a matter of cloning a directory under `tests/framework-fixtures/`, swapping the source files, and writing a `fixture.json`. See `tests/framework-fixtures/README.md` for the full schema. +### Skill-behavior tests + +`tests/skill-behavior/scenarios.test.mjs` is the LLM-backed safety net for edits to `skill/SKILL.src.md` and the Setup-adjacent reference files (`teach.md`, `document.md`, `brand.md`, `product.md`, sub-command refs). It inlines the source `skill/SKILL.src.md` into the system prompt of a real LLM, gives the agent `bash` / `read` / `write` / `list` tools scoped to a temp workspace, and asserts on the tool-call trace — not on the model's free-form output. The trace is the source of truth. + +```bash +bun run test:skill-behavior # full suite (27 tests, ~5 min, ~$0.50-1.50 across providers) +IMPECCABLE_SKILL_BEHAVIOR_MODELS=gemini-3.1-flash-lite bun run test:skill-behavior # scope to one provider +IMPECCABLE_SKILL_BEHAVIOR_VERBOSE=1 bun run test:skill-behavior # dump per-scenario trace JSON to stderr (use when iterating) +``` + +**Three providers per run, every run.** The suite always exercises `claude-sonnet-4-6`, `gpt-5.5`, and `gemini-3.1-flash-lite`. Sonnet and GPT-5.5 are production-tier, matching what users actually run, so the pass/fail signal reflects real agent behavior rather than a cheap proxy; gemini stays on the flash-lite tier. **Don't substitute Claude alone**: many of the most useful findings come from divergence between providers. + +**Auth** lives in repo-root `.env` (copied from `~/code/impeccable-evals/.env`, gitignored). Providers skip cleanly when their key is unset; they don't fail. + +**Nine scenarios:** +1. empty workspace → agent loads `reference/teach.md` +2. PRODUCT.md only → loads `brand.md` +3. PRODUCT.md + DESIGN.md → loads `brand.md` + consults the design system +4. context already loaded in turn 1 → turn 2 does **not** re-run `context.mjs` +5. PRODUCT.md without `## Register` field → agent infers `brand` from task cue +6. `/impeccable polish` → loads `reference/polish.md` +7. `/impeccable audit` → loads `reference/audit.md` +8. existing SvelteKit project → agent reads at least one project code file +9. `context.mjs` emits `UPDATE_AVAILABLE` (seeded newer version) → agent surfaces it but does **not** auto-run `npx impeccable skills update` + +**Baseline.** The 21-22 / 24 baseline (with stable gpt scenario 6/7 failures) was measured on the old cheap tier (`claude-haiku-4-5` / `gpt-5.4-mini`). It needs re-measuring on the current `claude-sonnet-4-6` / `gpt-5.5` lineup; the production-tier models are expected to do better on the sub-command routing scenarios the old gpt tier failed. See `tests/skill-behavior/README.md`. + +**Cost.** Each run is real LLM calls, billed to the keys in `.env`. Production-tier models put a full sweep around $0.50-1.50. Keep it out of CI unless you really want it there. + +**Adding a scenario.** Write the fixture in `tests/skill-behavior/fixtures.mjs`, add the `it()` block in `scenarios.test.mjs` (the harness uses the source `skill/` dir via a symlink, so no rebuild needed), and update the baseline table in the suite's README. The harness's `fileLoaded(trace, filename)` helper checks both `read` and bash `cat` — different models prefer different tools. + +**The harness symlinks source, not built output.** This is deliberate so SKILL.md / reference / `scripts/context.mjs` edits show up immediately without `bun run build:skills`. The trade-off: reference files surface their raw `{{placeholders}}`, but the assertions key on tool calls rather than content, so it doesn't matter for correctness. + ## CLI The CLI lives in this repo under `cli/`: `cli/bin/` (entry + sub-commands), `cli/engine/` (the detect-antipatterns rule engine + browser variant), `cli/lib/` (helpers shared by CLI and Cloudflare Pages Functions). Published to npm as `impeccable`. @@ -212,7 +246,7 @@ If you need to fix release notes after the fact (typo, missing thank-you, format All commands live under `/impeccable`. To add a new one: 1. Create `skill/reference/.md` with the command's instructions (this is what the LLM loads when the command is invoked) -2. Add a row to the **Sub-command reference table** in `skill/SKILL.md` +2. Add a row to the **Sub-command reference table** in `skill/SKILL.src.md` 3. Add an entry to the **Command menu** section in the same file 4. Add the command name to `IMPECCABLE_SUB_COMMANDS` in `scripts/lib/utils.js` 5. Add it to `VALID_COMMANDS` in `skill/scripts/pin.mjs` @@ -258,7 +292,7 @@ Every command should have an editorial file eventually, but the build does not r | `cli/engine/detect-antipatterns-browser.js` | `bun run build:browser` | | `extension/detector/detect.js` + `extension/detector/antipatterns.json` | `bun run build:extension` | | `site/public/js/generated/counts.js` (`DETECTION_COUNT`) | `bun run build` | -| `skill/SKILL.md` and `reference/*.md` | Hand-edited if the rule introduces new design guidance | +| `skill/SKILL.src.md` and `reference/*.md` | Hand-edited if the rule introduces new design guidance | Always run all three builds and the test suite after a rule change: @@ -273,7 +307,7 @@ bun run build && bun run build:browser && bun run build:extension && bun run tes 3. **Rule entry** in the `ANTIPATTERNS` array: `id`, `category` (`slop` for AI tells, `quality` for real design or a11y issues), `name`, `description`, optional `skillSection` and `skillGuideline`. 4. **Pure check function** `checkXxx(opts)` returning `[{ id, snippet }]`. No DOM access in the pure function. 5. **Two adapters**: `checkElementXxxDOM(el)` for the browser (`getComputedStyle` + `getBoundingClientRect`) and `checkElementXxx(el, tag, window)` for jsdom (`parseFloat(style.width)` instead of layout). Wire **both** into **both** element loops in `cli/engine/detect-antipatterns.mjs` — the browser loop (~line 1837) and the jsdom loop in `detectHtml` (~line 2058). Forgetting one is the most common mistake; symptom is "test passes, live page silent" or vice versa. -6. **Verify on a live page**: `http://localhost:3000/fixtures/antipatterns/{rule-id}.html` and the homepage (no false positives). The two adapter paths can disagree, so manual browser checks catch what the fixture test can't. +6. **Verify on a live page**: `http://localhost:4321/fixtures/antipatterns/{rule-id}.html` and the homepage (no false positives). The two adapter paths can disagree, so manual browser checks catch what the fixture test can't. ### Conventions and jsdom gotchas diff --git a/DESIGN.md b/DESIGN.md index 4f4e052aa..4884adcb6 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -1,74 +1,92 @@ --- name: Impeccable -description: Warm-paper editorial sanctuary — committed serif display, one decisive magenta, flat surfaces at rest. +description: Neo kinpaku system. Two brand anchors, kinpaku gold and verdigris patina, sit on dark warm-black lacquer. Restraint in chrome, brilliance in texture. -# Colors use OKLCH per `The OKLCH-Only Rule` in §2. Stitch's linter validates -# hex sRGB only, so it will warn on these entries — deliberate trade for one -# source of truth and full wide-gamut fidelity. Our own parser accepts strings. +# All values below mirror site/styles/kinpaku-tokens.css verbatim. That file +# is the source of truth; this frontmatter is the portable export. If a token +# changes there, update both. colors: - editorial-magenta: "oklch(60% 0.25 350)" - editorial-magenta-deep: "oklch(52% 0.25 350)" - warm-ash-cream: "oklch(96% 0.005 350)" - crisp-paper-white: "oklch(98% 0 0)" - deep-graphite: "oklch(10% 0 0)" - soft-charcoal: "oklch(25% 0 0)" - mid-ash: "oklch(55% 0 0)" - paper-mist: "oklch(92% 0 0)" - magenta-whisper: "oklch(60% 0.25 350 / 0.15)" - magenta-veil: "oklch(60% 0.25 350 / 0.25)" + # Brand anchors + kinpaku-gold: "oklch(84% 0.19 80.46)" # primary accent + verdigris-patina: "oklch(70% 0.12 188)" # secondary accent / state + + # Surfaces + lacquer-black: "oklch(7% 0.006 95)" # page ground + lacquer-deep: "oklch(4% 0.004 95)" # deepest inset + raised-lacquer: "oklch(11% 0.006 95)" # panels and inputs + graphite: "oklch(15% 0.008 95)" # inactive states + graphite-2: "oklch(19% 0.008 95)" # one step up from graphite + + # Text + champagne: "oklch(84% 0.035 82)" # headlines, + text-warm: "oklch(81% 0.03 82)" # body + text-muted: "oklch(63% 0.024 82)" # captions, meta + text-faint: "oklch(52% 0.018 82)" # subdued + text-mute-deep: "oklch(48% 0.018 82)" # disabled + + # Gold ramp + kinpaku-pale: "oklch(86% 0.07 84)" # hover lift, pale fills + kinpaku-rich: "oklch(77% 0.13 82)" # active CTA, severity-medium + kinpaku-deep: "oklch(61% 0.085 78)" # borders against the brand + gold-hairline: "oklch(58% 0.065 82 / 0.32)" # default rule + gold-hairline-strong: "oklch(74% 0.09 82 / 0.6)" # active rule + + # Patina ramp + patina-pale: "oklch(82% 0.07 188)" # hover lift on patina + patina-deep: "oklch(49% 0.08 188)" # deep oxide, dark variants + + # State (warning only, used sparingly) + vermilion-warning: "oklch(58% 0.15 35)" typography: - display: - fontFamily: "Cormorant Garamond, Georgia, serif" - fontSize: "clamp(2.5rem, 7vw, 4.5rem)" - fontWeight: 300 + wordmark: + # Solid Alumni Sans (token --ks-font-wordmark), the weightable sibling of + # the pinstripe display face. The pinstripe itself is single-weight and + # reads too thin in the small lockup, so the wordmark uses the solid cut. + fontFamily: "Alumni Sans, Alumni Sans Pinstripe, Albert Sans, Arial, sans-serif" + fontSize: "1.3rem" + fontWeight: 500 + letterSpacing: "0.15em" lineHeight: 1 + display: + fontFamily: "Alumni Sans Pinstripe, Albert Sans, Arial, sans-serif" + fontSize: "clamp(3.4rem, 6.5vw, 5.6rem)" + fontWeight: 300 + letterSpacing: "-0.01em" + lineHeight: 1.02 headline: - fontFamily: "Cormorant Garamond, Georgia, serif" - fontSize: "clamp(1.75rem, 4vw, 2.5rem)" - fontWeight: 400 - lineHeight: 1.2 + fontFamily: "Alumni Sans Pinstripe, Albert Sans, Arial, sans-serif" + fontSize: "clamp(2.6rem, 4vw, 3.4rem)" + fontWeight: 600 + letterSpacing: "0" + lineHeight: 1.04 title: - fontFamily: "Cormorant Garamond, Georgia, serif" - fontSize: "clamp(1.125rem, 2.5vw, 1.75rem)" - fontWeight: 400 - lineHeight: 1.3 + fontFamily: "Albert Sans, Avenir Next, Helvetica Neue, Arial, system-ui, sans-serif" + fontSize: "1.18rem" + fontWeight: 500 + lineHeight: 1.35 body: - fontFamily: "Instrument Sans, system-ui, sans-serif" - fontSize: "1rem" + fontFamily: "Albert Sans, Avenir Next, Helvetica Neue, Arial, system-ui, sans-serif" + fontSize: "1.02rem" fontWeight: 400 - lineHeight: 1.6 - body-lead: - fontFamily: "Instrument Sans, system-ui, sans-serif" - fontSize: "1.0625rem" - fontWeight: 400 - lineHeight: 1.65 - supporting: - fontFamily: "Instrument Sans, system-ui, sans-serif" - fontSize: "0.875rem" - fontWeight: 400 - lineHeight: 1.6 - label: - fontFamily: "Instrument Sans, system-ui, sans-serif" - fontSize: "0.9rem" + lineHeight: 1.8 + eyebrow: + fontFamily: "SFMono-Regular, Roboto Mono, Consolas, monospace" + fontSize: "0.7rem" fontWeight: 500 - letterSpacing: "0.05em" - micro-label: - fontFamily: "Instrument Sans, system-ui, sans-serif" - fontSize: "0.6875rem" - fontWeight: 500 - letterSpacing: "0.1em" + letterSpacing: "0.18em" mono: - fontFamily: "Space Grotesk, monospace" - fontSize: "0.75rem" - fontWeight: 400 + fontFamily: "SFMono-Regular, Roboto Mono, Consolas, monospace" + fontSize: "0.72rem" + fontWeight: 500 + letterSpacing: "0.22em" rounded: none: "0" + xs: "2px" sm: "4px" - md: "8px" - lg: "12px" - xl: "16px" + md: "6px" + lg: "8px" spacing: xs: "8px" @@ -77,221 +95,331 @@ spacing: lg: "32px" xl: "48px" "2xl": "80px" - "3xl": "120px" + "3xl": "112px" components: button-primary: - backgroundColor: "{colors.deep-graphite}" - textColor: "{colors.crisp-paper-white}" - typography: "{typography.label}" - rounded: "{rounded.none}" - padding: "16px 48px" + backgroundColor: "{colors.kinpaku-gold}" + textColor: "{colors.lacquer-deep}" + typography: "{typography.title}" + rounded: "{rounded.xs}" + padding: "0 38px" button-primary-hover: - backgroundColor: "{colors.editorial-magenta}" - textColor: "{colors.crisp-paper-white}" - input-text: + backgroundColor: "{colors.kinpaku-pale}" + textColor: "{colors.lacquer-deep}" + button-secondary: backgroundColor: "transparent" - textColor: "{colors.deep-graphite}" + textColor: "{colors.kinpaku-gold}" + borderColor: "{colors.gold-hairline-strong}" + rounded: "{rounded.xs}" + padding: "0 38px" + input-text: + backgroundColor: "{colors.lacquer-deep}" + textColor: "{colors.champagne}" + borderColor: "{colors.gold-hairline}" rounded: "{rounded.sm}" - padding: "8px 12px" + padding: "14px 16px" card: - backgroundColor: "{colors.warm-ash-cream}" - textColor: "{colors.deep-graphite}" - rounded: "{rounded.md}" + backgroundColor: "{colors.raised-lacquer}" + textColor: "{colors.text-warm}" + borderColor: "{colors.gold-hairline}" + rounded: "{rounded.sm}" padding: "24px" - card-feature: - backgroundColor: "{colors.crisp-paper-white}" - textColor: "{colors.deep-graphite}" - rounded: "{rounded.lg}" - padding: "48px" nav-link: - textColor: "{colors.deep-graphite}" + textColor: "{colors.champagne}" typography: "{typography.body}" nav-link-hover: - textColor: "{colors.editorial-magenta}" + textColor: "{colors.kinpaku-gold}" + live-picker-bar: + backgroundColor: "{colors.lacquer-deep}" + textColor: "{colors.champagne}" + borderColor: "{colors.kinpaku-gold}" + rounded: "{rounded.sm}" + padding: "4px 5px" + live-picker-toggle-active: + backgroundColor: "oklch(78% 0.12 82 / 0.18)" + textColor: "{colors.kinpaku-gold}" --- # Design System: Impeccable -## 1. Overview: The Editorial Sanctuary +## 1. Overview: Neo Kinpaku -**Creative North Star: "The Editorial Sanctuary"** +**Creative North Star: "Neo Kinpaku"** -The Impeccable site reads more like a printed design publication than a SaaS landing page. Committed typography, generous breathing room, and a single decisive accent that cuts through warm paper. The interface feels **considered, unhurried, and expert** — the work of someone who has made the calls a thousand times and has zero interest in chasing the current AI-tool aesthetic. +Impeccable is now a dark lacquer interface marked by Japanese gold leaf and precise technical geometry. The brand should feel like a crafted object: black urushi, irregular kinpaku seams, quiet measurement marks, circuit traces, and controlled verdigris oxidation. It is refined, technical, and physical. -The aesthetic philosophy is **restraint in service of craft**. Every element earns its place. Nothing is decorative without function. The palette is dominated by warm paper tones with one vibrant voice. The typography pairs a stately italic serif with a clean neutral sans. Motion is reserved for moments that actually communicate state. The site is the demo — it must pass the same anti-pattern audit it asks its users to run on their own work. +This direction replaces the old warm-paper editorial system. No italic serif hero. No magenta accent. No generic AI-tool neon. The page should feel expensive and deliberate, but still useful: comparisons, command demos, live audit tables, and documentation modules remain the product proof. -This system explicitly rejects the AI-tool visual vocabulary that surrounds the product: dark mode with purple gradients, neon accents, glassmorphism, glowing cyan-on-black, SaaS hero-metric layouts, and identical-card feature grids. When in doubt, do less than a marketing site would, more than a portfolio would. +**Key characteristics** -**Key Characteristics:** -- Warm off-white paper tones with an almost-imperceptible magenta tint for subliminal palette cohesion. -- A single decisive magenta accent used on no more than 10% of any screen. Its rarity is the point. -- Italic serif for display type; clean neutral sans for body at 1.6+ line-height. -- Sharp, uppercase, letter-tracked primary CTAs — no rounded-rectangle-with-drop-shadow defaults. -- Flat surfaces at rest. Shadows appear only as a response to state (hover, elevation, focus). -- Asymmetric magazine-scale spacing; intentionally skips the 4px step. +- Dark mineral and lacquer surfaces, never pure black. +- Kinpaku gold as the primary accent, with true leaf texture where an element carries brand weight. +- Verdigris patina as the secondary accent for state, contrast, and "improved" signals. +- A geometric sans voice with a widely tracked wordmark, not a serif editorial voice. +- Thin calibration lines, circuit geometry, and gold seams used as functional structure. +- Small radii, restrained borders, and almost no decorative shadow. -## 2. Colors: The Warm-Paper Palette +## 2. The Kit: One Vocabulary For Every Page -A two-chord palette: warm paper neutrals carrying a near-invisible magenta tint, plus one decisive accent in the same hue family. No secondary or tertiary accents in the core system — the restraint is doctrinal. +The site ships a global component kit at `site/styles/kinpaku-kit.css`, imported from `Base.astro` so every page gets it for free. The kit primitives are listed below. The live demos are on `/design-system`. -### Primary -- **Editorial Magenta** (oklch(60% 0.25 350)): The one vibrant voice. Primary CTAs, active navigation states, live-state indicators, rare editorial emphasis. Never used as a gradient, never as a background wash, never as text fill. Rarity is the design choice. +### The Kit Consumption Rule -### Neutral -- **Warm Ash Cream** (oklch(96% 0.005 350)): Primary page background. Near-white with a near-imperceptible magenta tint that creates subconscious cohesion with Editorial Magenta. Used on `body` and standard surfaces. -- **Crisp Paper White** (oklch(98% 0 0)): Pure background. Used for inverted text moments (white-on-dark CTAs) and surfaces needing maximum contrast. Almost never the page background — too cold alone. -- **Deep Graphite** (oklch(10% 0 0)): Primary text for body copy and headlines. Softer than pure black, reads as confident-but-not-aggressive on warm paper. Background of the primary CTA. -- **Soft Charcoal** (oklch(25% 0 0)): Secondary text — taglines, hook paragraphs, supporting copy. Clearly subordinate to Deep Graphite without being washed out. -- **Mid Ash** (oklch(55% 0 0)): Tertiary text — micro-labels, captions, meta lines, "works with" labels. At small sizes reads as intentionally recessed metadata. -- **Paper Mist** (oklch(92% 0 0)): Hairline borders, section dividers, the barely-visible structural seams. +When building a new page or refactoring an existing one, reach for a kit primitive before inventing a new class. Specifically: -### Accent Alpha Variants -- **Editorial Magenta Deep** (oklch(52% 0.25 350)): Hover/active state for Editorial Magenta. Small darkening, confirms interaction without shouting. -- **Magenta Whisper** (oklch(60% 0.25 350 / 0.15)): Glow backdrop under accent elements on hover (diffuse shadows only), subtle selection highlights. -- **Magenta Veil** (oklch(60% 0.25 350 / 0.25)): Slightly stronger translucent tint for focus rings and emphasis shells. +- **Buttons**: use `.ks-button` + a variant (`.ks-button-primary`, `-secondary`, `-ghost`, `-disabled`). Do not write a new `.hero-cta-primary` / `.footer-cta` / `.section-action-button` class — those are the bespoke vocabularies the kit is meant to replace. +- **Grouping content**: use `.ks-bento` + `.ks-bento-tile` (with `--span-4` / `--span-6` / `--span-8` across a 12-column grid). This is the canonical answer to "how do I group 2-6 items without nesting cards?" Do not invent yet another card class. +- **Section scaffolding**: use `.ks-section` for the container, `.ks-section-head` for the header, `

` inside that (the kit styles it), and `.ks-section-sub` for the subhead. Eyebrow above the h2 is optional via `.ks-section-eyebrow` — skip it on editorial walkthroughs where every-section eyebrows read as AI scaffolding. +- **Status, tags, toasts, modals, tooltips, empty states, pagination, skeletons, changelog rows**: use the kit primitive. Listed in the cheatsheet below. -### Command Category Tints (fenced — do not extend) -A separate six-tint vocabulary used exclusively to color-code the periodic-table visualization of impeccable's 23 commands. These tints predate the OKLCH system and live in one component. **Do not extend this vocabulary elsewhere.** +Invent only when the kit truly doesn't cover the shape. When you do invent, flag it — a new pattern that solves a real recurring need belongs in the kit, not in page-specific CSS. Page CSS is for genuinely page-specific scenery (hero illustrations, unique editorial visuals), not for reinventing primitives the kit already has. -- **Create** (bg `#fdf2f8` / border `#ec4899` / text `#be185d`) -- **Evaluate** (bg `#fdf4ff` / border `#d946ef` / text `#a21caf`) -- **Refine** (bg `#eff6ff` / border `#3b82f6` / text `#1d4ed8`) -- **Simplify** (bg `#fffbeb` / border `#f59e0b` / text `#b45309`) -- **Harden** (bg `#f0fdf4` / border `#22c55e` / text `#15803d`) -- **System** (bg `#f5f5f4` / border `#78716c` / text `#44403c`) +### What's In The Kit (cheatsheet) -### Named Rules +Every class below is a global primitive. Drop it on any element on any page using ``; the styles resolve through `kinpaku-tokens.css` so they inherit the current brand values automatically. -**The One Voice Rule.** Editorial Magenta is the only vibrant color in the system. No supporting accent is added, ever, no matter how much a layout "wants" a second color. If a second emphasis point is needed, use scale or weight, never a second hue. +**Brand lockup** -**The Paper-Not-White Rule.** The page background is Warm Ash Cream, never Crisp Paper White. Pure white is reserved for specific inverted surfaces. Warmth is load-bearing — without it, the site reads as generic and the decisive magenta reads as abrasive rather than decisive. +- `.ks-brand` — wrapper for the brand mark + wordmark lockup (anchor or div). +- `.ks-mark` — the carved-tile glyph (a solid kinpaku square split by a diagonal slash), 38×38, no container border. +- `.ks-wordmark` — the IMPECCABLE wordmark text, solid Alumni Sans (`--ks-font-wordmark`), uppercase, weight 500, letter-spacing 0.15em. -**The OKLCH-Only Rule.** All new colors must be declared in OKLCH. Legacy hex values exist only in the fenced Command Category Tints. Do not introduce new hex-declared colors into the system. +**Section scaffolding** -## 3. Typography: The Italic-and-Ink Voice +- `.ks-section` — page-level section container, 1320px max-width, kit gutters. +- `.ks-section-head` — the section header block. +- `.ks-section-eyebrow` — small mono eyebrow above the h2 (optional). +- `.ks-section-head h2` — auto-styles any h2 inside `.ks-section-head` to the section title scale (weight 600, kit display family). +- `.ks-section-sub` — subhead paragraph below the h2. +- `.ks-subsection` — nested grouping inside a section. +- `.ks-subsection-label` — small mono label above a subsection's content. -**Display Font:** Cormorant Garamond (with Georgia fallback) -**Body Font:** Instrument Sans (with system-ui fallback) -**Label/Mono Font:** Space Grotesk (used as a geometric mono, not for code blocks) +**Buttons** -**Character:** The display face is a refined transitional serif used in its **italic** cut — stately without being stuffy, drawing on long-form editorial headline traditions. The body face is a clean neutral sans with subtle geometric warmth, chosen to set long paragraphs without visual overhead. The "mono" is a contemporary grotesque reserved for small labels and metadata where a machine-adjacent feel reinforces the command-line product story. +- `.ks-button.ks-button-primary` — filled kinpaku CTA, dark text. Both classes required. +- `.ks-button.ks-button-secondary` — outlined kinpaku CTA. +- `.ks-button.ks-button-ghost` — text-only kinpaku button. +- `.ks-button[disabled]` or `.ks-button.ks-button-disabled` — disabled state. +- `.ks-button-arrow` — wrapper for an arrow SVG inside a button (sized correctly). +- `.ks-button-row` — flex row helper for horizontal button groups. + +**Form controls** + +- `.ks-form-sample` — vertical form layout. +- `.ks-toggle` — switch (checkbox underneath, label visible). +- `.ks-checkbox` — checkbox with label. +- `.ks-select` — dropdown. + +**Tabs** + +- `.ks-tabs` — tab container. +- `.ks-tab-list` — tab buttons row. +- `.ks-tab-panel` — tab content panel. + +**Status, tags, and feedback** + +- `.ks-badge` + `.is-detected` / `.is-improved` / `.is-ready` — pill badge with dot. +- `.ks-badge-row` — flex row helper. +- `.ks-tag` + `.is-detected` / `.is-improved` / `.is-neutral` / `.is-ready` — slim tag, no dot. +- `.ks-tag-row` — flex row helper. +- `.ks-toast` + `.is-success` / `.is-warning` — alert toast with icon + dismiss. +- `.ks-toast-icon` / `.ks-toast-close` — inner parts. +- `.ks-modal` + `.ks-modal-actions` / `.ks-modal-close` — dialog box. +- `.ks-empty` + `.ks-empty-icon` — empty-state block. +- `.ks-skeleton` — loading shimmer. +- `.ks-pagination` — paged-list nav. +- `.ks-icon-button` + `.ks-tooltip` — circular icon button with hover tooltip. + +**Containers** + +- `.ks-bento` — 12-column grid with dark plinth + 8px gutters. +- `.ks-bento-tile` — single tile within a bento. +- `.ks-bento-tile--span-4` / `--span-6` / `--span-8` — sizing (e.g. 8/4, 6/6, 4/8). +- `.ks-bento-num` — tiny mono caps marker for tile numbering. Add `data-color="patina"` to flip to verdigris. + +**Changelog** + +- `.ks-changelog` — vertical list wrapper. +- `.ks-changelog-entry` — single dated entry. +- `.ks-changelog-date` — left-column date. +- `.ks-changelog-body` — right-column content. Use `` inside `` for a small "NEW" badge. + +### Tokens vs Classes + +The kit primitives consume the tokens from `site/styles/kinpaku-tokens.css`. When you need a color, type scale value, easing, or rule alpha outside a kit primitive, read the token directly: + +- Colors: `var(--ks-kinpaku)`, `var(--ks-patina)`, `var(--ks-lacquer)`, `var(--ks-champagne)`, etc. +- Type scale: `var(--ks-type-display-size)`, `var(--ks-type-headline-weight)`, etc. +- Rules: `var(--ks-rule)`, `var(--ks-rule-strong)`. +- Motion: `var(--ks-ease)`. + +Do not hand-type oklch values or font sizes in page CSS. If a value isn't in the token file, it's either a token that needs adding or a sign that the visual moment is bespoke enough to live page-locally — either way, the decision needs to be deliberate. + +## 3. Colors: Lacquer, Gold, Patina + +### Ground and Surface + +- **Lacquer Black** (`oklch(7% 0.006 95)`): Default page ground. It is warm and mineral, not neutral black. +- **Lacquer Deep** (`oklch(4% 0.004 95)`): Deepest inset surfaces and footer depth. +- **Raised Lacquer** (`oklch(11% 0.006 95)`): Panels, cards, demo frames, and dark UI surfaces. +- **Graphite** (`oklch(15% 0.008 95)`): Input fields, inactive tiles, and subtle internal surfaces. +- **Graphite 2** (`oklch(19% 0.008 95)`): One step above graphite; context pills, inactive chrome inside live mode bars. + +### Gold System + +- **Kinpaku Gold** (`oklch(84% 0.19 80.46)`): Primary accent. CTAs, active state, wordmark, key rules, command focus. +- **Kinpaku Rich** (`oklch(77% 0.13 82)`): Active CTA fill and severity-medium markers. +- **Kinpaku Deep** (`oklch(61% 0.085 78)`): Secondary gold for borders, subdued icons, and large technical diagrams. +- **Kinpaku Pale** (`oklch(86% 0.07 84)`): Hover lift and pale fills. +- **Gold Hairline** (`oklch(58% 0.065 82 / 0.32)`): Default border and divider. The homepage's busier surfaces lift this to alpha 0.48 locally. +- **Strong Gold Hairline** (`oklch(74% 0.09 82 / 0.6)`): Active borders, focus outlines, and structural anchors. + +### Text + +- **Champagne** (`oklch(84% 0.035 82)`): Headlines, ``, important labels. +- **Warm Text** (`oklch(81% 0.03 82)`): Body copy on dark surfaces. +- **Muted Text** (`oklch(63% 0.024 82)`): Metadata, captions, secondary labels. The homepage lifts this to 65% on busier tile backgrounds. +- **Faint Text** (`oklch(52% 0.018 82)`): Subdued labels. +- **Mute Deep** (`oklch(48% 0.018 82)`): Disabled copy. + +### Secondary and State + +- **Verdigris Patina** (`oklch(70% 0.12 188)`): Secondary accent. Improved states, live indicators, hover emphasis, and contrast point. +- **Patina Pale** (`oklch(82% 0.07 188)`): Hover lift on patina surfaces. +- **Patina Deep** (`oklch(49% 0.08 188)`): Deep oxide. Background texture and dark patina variants. +- **Vermilion Warning** (`oklch(58% 0.15 35)`): Error or anti-pattern warning only. Use sparingly. + +### Color Rules + +**The Gold Carries Brand Rule.** Kinpaku gold is the primary brand signal. If a single accent must represent Impeccable, use gold, not magenta or cyan. + +**The Patina Has Meaning Rule.** Verdigris is secondary. It marks improvement, live state, or contrast against gold. Do not use it as a generic decoration field. + +**The Texture Budget Rule.** Leaf and patina textures are for brand-bearing moments: hero seams, CTA fills, dividers, major swatches, and select system modules. Generic cards stay mostly flat. + +**The OKLCH-Only Rule.** New colors are declared in OKLCH. Hex appears only inside third-party examples or imported assets. + +## 4. Typography: Two faces, weight inversion at the top + +**Display font:** Alumni Sans Pinstripe, Albert Sans, Arial, sans-serif (pinstripe horizontal strikes carry the brand at display sizes) +**Body and UI font:** Albert Sans, Avenir Next, Helvetica Neue, Arial, system-ui, sans-serif +**Mono font:** SFMono-Regular, Roboto Mono, Consolas, monospace + +The voice is geometric and restrained. The pinstripe display face is reserved for the hero h1 and section h2s; the brand wordmark uses its solid sibling (Alumni Sans). Everything else (body, UI labels, controls, code) uses Albert Sans. The faces pair cleanly because they share humanist proportions without fighting for attention. ### Hierarchy -- **Display** (display family, weight 300, italic, clamp(2.5rem, 7vw, 4.5rem), line-height 1): Hero title only. The light weight + italic cursive reads as an author signature rather than a marketing headline. -- **Headline** (display family, weight 400, clamp(1.75rem, 4vw, 2.5rem), line-height 1.2): Section headings. Larger editorial moments. -- **Title** (display family, weight 400, italic, clamp(1.125rem, 2.5vw, 1.75rem), line-height 1.3): Hero tagline / section leads. A quieter second display voice. -- **Body** (body family, weight 400, 1rem, line-height 1.6): Paragraph copy. Capped at 65–75ch for readability. -- **Body Lead** (body family, weight 400, 1rem–1.0625rem, line-height 1.6–1.65): The one or two "lead" paragraphs on each page. Slightly relaxed leading. -- **Supporting** (body family, weight 400, 0.875rem, line-height 1.6): Captions, footnotes, supporting context. -- **Label** (body family, weight 500, 0.9rem, `text-transform: uppercase`, `letter-spacing: 0.05em`): CTA labels. Short, declarative. -- **Micro-Label** (body family, weight 500, 0.625–0.6875rem, `text-transform: uppercase`, `letter-spacing: 0.1em`): "Works with", "What's Included", "v3.0 Changelog". -- **Monospace Meta** (mono family, weight 400–500, 0.6875–0.8125rem): Command names in inline prose, periodic-table tile labels. +- **Wordmark**: solid Alumni Sans (`--ks-font-wordmark`), weight 500, uppercase, `1.3rem`, letter-spacing `0.15em`. Brand lockup only. The pinstripe sibling reads too thin at lockup size, so the wordmark uses the weightable cut. +- **Display · h1**: Alumni Sans Pinstripe, `clamp(3.4rem, 6.5vw, 5.6rem)`, **weight 300**, line-height 1.02, letter-spacing `-0.01em`. Hero and major statements. +- **Headline · h2**: Alumni Sans Pinstripe, `clamp(2.6rem, 4vw, 3.4rem)`, **weight 600**, line-height 1.04. Section titles. +- **Title · h3**: Albert Sans, `1.18rem`, weight 500, line-height 1.35. Component and panel headings. +- **Body**: Albert Sans, `1.02rem`, weight 400, line-height 1.8. Long copy on dark surfaces needs air. +- **Eyebrow**: Mono, `0.7rem`, weight 500, uppercase, letter-spacing `0.18em`. Small markers above titles. +- **Mono label**: Mono, `0.72rem`, letter-spacing `0.22em`. Category labels, nav metadata, table headers, audit lines. -### Named Rules +### Typography Rules -**The Italic-Is-Voice Rule.** Italic is used as a voice choice for display type, not as emphasis within body copy. Body emphasis is carried by weight or by swapping to the mono family (see `` in command menus). Treating italic as emphasis inside paragraphs dilutes the display voice. +**The Weight-Inversion Rule.** Section h2s read heavier (600) than the hero h1 (300). This is deliberate: the hero is elegant and thin so the page can breathe; section anchors carry more weight to ground each block. Do not normalize the two weights. -**The 1.6 Leading Rule.** Body line-height is 1.6 everywhere. Not 1.5, not 1.7, not "relaxed". This is the load-bearing readability decision — when the site reads as calm and editorial, it's 1.6 doing the work. +**The Two-Face Rule.** Display sizes use Alumni Sans Pinstripe. Anything sized below `1.2rem` uses Albert Sans. Pinstripe at small sizes loses its identity and reads as a bad rendering. -**The Fluid-Headlines-Only Rule.** Headings use `clamp()` fluid sizing. Body copy uses fixed `rem` values. Fluid body sizes look clever and feel wrong — they make line-lengths wander off spec. +**Tracked Labels Are Short Rule.** Tracked uppercase labels are for short system markers. Do not write full sentences in tracked caps. -## 4. Elevation +**Dark Type Needs Air Rule.** Body text on lacquer uses line-height 1.65 to 1.8 and a max width of 65 to 75ch. -Flat by default. Depth is conveyed through **state response**, not structural shadow. Surfaces rest on a single tonal layer (Warm Ash Cream); shadows appear only when an element is hovered, deliberately lifted, or requires ambient separation from a busy area. +## 5. Elevation and Material + +The system is mostly flat. Depth comes from material contrast, hairline borders, texture, and subtle inset light. ### Shadow Vocabulary -- **Soft Hover Lift** (`0 4px 24px -4px rgba(0,0,0,0.12), 0 1px 3px rgba(0,0,0,0.06)`): The default hover response on cards and interactive surfaces. Diffuse, offset downward. -- **Lifted Card** (`0 20px 40px rgba(0,0,0,0.08)`): Deliberately elevated content (featured cards, install blocks). Low alpha — never reads as dark. -- **Accent Glow** (`0 20px 60px var(--color-accent-dim)`): Magenta-tinted ambient shadow under the one or two moments that should feel magnetic. Used sparingly — this is the "rare ingredient" of the shadow vocabulary. -- **Tooltip / Popover** (`0 0 20px rgba(0,0,0,0.15)` or `0 2px 8px rgba(0,0,0,0.1)`): Tight shadow for small floating UI. +- **Panel Setback:** `0 24px 70px oklch(2% 0.004 95 / 0.42)` for large framed modules only. +- **CTA Lift:** `0 18px 48px oklch(2% 0.004 95 / 0.4)` plus a small inset highlight. +- **Patina Glow:** `0 0 22px oklch(70% 0.105 190 / 0.24)` for tiny live indicators only. +- **No Default Card Shadow:** Cards rest on borders and background shifts. -### Named Rules +### Material Rules -**The Flat-By-Default Rule.** Surfaces are flat at rest. If you find yourself adding a shadow to a non-interactive, non-elevated element, stop — you're reaching for Material Design muscle memory. Use a hairline Paper Mist border instead, or no articulation at all. +**Hairline First Rule.** Use 1px gold hairlines before adding shadow. -**The Low-Alpha Rule.** Every shadow in the system uses ≤0.15 alpha on its strongest blur. Higher alphas read as 2014 Material Design drop shadows — an immediate tell that the design wasn't considered. +**No Glass Rule.** Translucency can exist in overlays, but decorative blur/glass panels are not part of this system. -**The Tinted-Shadow-Only-For-Accent Rule.** Neutral shadows (black alpha) for structure. Colored (magenta-dim) shadows only for the deliberate accent-glow moments. Never tint shadows for decorative effect. +**Texture Needs Contrast Rule.** Text never sits directly on high-contrast leaf texture. Add a lacquer veil or move the texture to an edge. -## 5. Components +**Asset-Led Material Rule.** Brand-bearing material accents use raster assets or generated images, not hand-drawn SVG approximations of leaf, dust, oxidation, or clockwork. Code-native geometry is reserved for simple hairlines, layout grids, and functional UI structure. + +## 6. Components ### Buttons -- **Shape:** Flat and squared by default (`border-radius: 0`). Sharp corners are an explicit editorial choice — the site rejects the rounded-rectangle-with-drop-shadow default that marks most AI-adjacent marketing pages. -- **Primary (hero-cta-combined):** Deep Graphite background, Crisp Paper White text. Padding 16px / 48px (`--spacing-sm` / `--spacing-xl`). Uppercase, `letter-spacing: 0.05em`, weight 500. No border, no shadow at rest. -- **Hover:** `transform: translateY(-2px)` and background shifts to Editorial Magenta. Transition 200ms linear ease. A small confident step up, never a bounce. -- **Focus:** Browser-default focus ring combined with the hover treatment. Visible keyboard focus is required. -- **Secondary:** Inline text link in body copy, weight 500, hover shifts to Editorial Magenta. **No boxed secondary button exists in the system** — the site avoids the "stack of equal-weight CTAs" pattern entirely. -- **Chip (picker overlay):** Radius 3–5px, small padding, mono-family label. Used in the live-mode action selector. +- **Primary:** Kinpaku texture or gold fill, dark text, 1px border, 2px radius, min-height 58px. +- **Secondary:** Transparent lacquer, gold border, gold text. Use for secondary commands only. +- **Hover:** Slight upward transform, brighter gold position, or patina border. No bounce. +- **Focus:** Patina outline with a 4px offset. -### Cards & Containers +### Hero Compare -- **Corner Style:** Controlled vocabulary — 4px (chips / inline callouts), 8px (standard cards and card-CTAs), 12px (feature cards, install blocks), 16px (large content frames). No single "rounded-lg" default. Radius is picked per component weight. -- **Background:** Warm Ash Cream or Crisp Paper White depending on layering. Deeper nested surfaces may lift to Paper Mist as a near-imperceptible tone shift. -- **Shadow:** Flat at rest — see Elevation for the shadow vocabulary that applies on hover/lift. -- **Border:** Hairline 1px in Paper Mist when a surface needs articulation without shadow. -- **Internal Padding:** 16–32px for typical cards; large editorial frames 48px+. Padding matches visual weight, not applied uniformly. +The before/after comparison is a proof object. It uses a dark grid field, a straight kinpaku seam, a gold handle, and readable labels. The "before" side can show AI slop colors, but the frame itself stays in the neo-kinpaku system. -### Inputs / Fields +### Command Rail -The site is primarily editorial, so inputs are minimal: +The first-viewport rail should map to workflow or high-value commands, not abstract design categories. Items use full-width dark bands, gold icons, and patina hover state. -- **Email / text field:** Radius 4–6px, hairline Paper Mist border, transparent background. Focus state shifts border to Editorial Magenta with a Magenta Whisper backdrop glow. -- **Combobox / select (filter controls):** Same stroke vocabulary, smaller padding, chevron glyph in Mid Ash. -- **No custom checkbox/radio styling** beyond what the live-mode command picker needs. +### Periodic Table -### Navigation +The command table uses dark category cells. Kinpaku covers Create, Refine, and Simplify variants; patina covers Evaluate and Harden; System is muted graphite. No light pastel category tints remain on the homepage. -- **Site Header:** 62px compact bar, left-aligned brand lockup (monochrome mark + wordmark), right-aligned link cluster. -- **Typography:** Body family, weight 500, 0.9–1rem. Normal case — the header is readable prose, not a set of signals. -- **Default State:** Deep Graphite on Warm Ash Cream. -- **Hover / Active:** Smooth color transition to Editorial Magenta, 200ms. No underline bar at rest; if an active indicator is needed, a thin accent-colored underline appears. -- **Mobile:** Collapses to an icon-triggered drawer when horizontal space is insufficient. +### DESIGN.md Panel -### Periodic Table of Commands (signature component) +The DESIGN.md visualization must show kinpaku as the primary color, patina as the secondary color, Alumni Sans Pinstripe and Albert Sans as the display and body families, and dark component samples. Magenta is not representative of the current system. -A distinctive custom element worth documenting: the 23 commands are laid out as a periodic-table grid of 56×64px tiles, each with a category tint background, category-colored border, atomic number in the top-left (mono family, 7px), a symbol in the center (display family, weight 500, 20px), and a command label in mono below. Hover lifts the tile 2px with a category-colored shadow. Tiles are the one place where the Category Tint vocabulary (see Colors) is used on a colored surface rather than as a text accent. +### Dividers and Material Accents -### Layout & Spacing (fold from spec-absent Layout section) +Dividers use real kinpaku, dust, or verdigris texture assets when they need to carry the brand. Simple CSS dividers are limited to straight hairlines. Avoid synthetic dot rails, fake dust strokes, or pseudo-circuit motifs that create rendering artifacts. -- **Max width:** Content blocks cap at 900px (`--width-content`); page-level containers at 1400px (`--width-max`). Prose further constrained to 65–75ch. -- **Spacing scale:** 8 / 16 / 24 / 32 / 48 / 80 / 120px (`--spacing-xs` through `--spacing-3xl`). The 4px step is deliberately omitted — this is an editorial scale, not an app-UI scale. -- **Rhythm:** 80–120px between top-level sections, 24–48px between content groups within a section, 6–16px inside tight clusters. -- **Grid:** No traditional column grid. Hero layouts are asymmetric two-column splits. Feature sections use `repeat(auto-fit, minmax(280px, 1fr))` rather than breakpoint-driven columns. -- **Motion:** 150ms for color/opacity, 300–400ms for transforms, 600–1200ms for orchestrated entrances. All use `--ease-out` (`cubic-bezier(0.16, 1, 0.3, 1)`) or `--ease-out-quint`. `prefers-reduced-motion` collapses every non-essential transition. +### Footer -## 6. Do's and Don'ts +The footer can carry the strongest oxidation accent. Use the gold seam plus patina edge as a final brand signature. -### Do: +### Live Mode Picker -- **Do** treat Warm Ash Cream (not Crisp Paper White) as the default page background. Warmth is load-bearing — see The Paper-Not-White Rule. -- **Do** use Editorial Magenta on ≤10% of any given screen. Scarcity is what makes it read as decisive rather than noisy — see The One Voice Rule. -- **Do** set all new colors in OKLCH. Hex is for the fenced Command Category Tints only. -- **Do** use italic display type as a voice, not as emphasis inside paragraphs. Body emphasis is carried by weight. -- **Do** use `clamp()` fluid sizing for headings; use fixed `rem` for body — see The Fluid-Headlines-Only Rule. -- **Do** keep the primary CTA sharp and squared. `border-radius: 0`, uppercase, letter-tracked. This is the editorial signature. -- **Do** use `--ease-out` (`cubic-bezier(0.16, 1, 0.3, 1)`) or `--ease-out-quint` on transitions. Expo-out only. -- **Do** leave surfaces flat at rest. Reach for shadows only on hover or for deliberate elevation — see The Flat-By-Default Rule. -- **Do** respect `prefers-reduced-motion` on every animation. -- **Do** cap body line length at 65–75ch via `max-width`. +The global bottom bar and the contextual bar (configure / cycling / accept) share one chrome treatment. Source of truth: `skill/scripts/live-browser.js` (`barPaletteForTheme`, `initGlobalBar`, `initBar`). Homepage and `/live-mode` demos mirror it via `.live-demo-gbar` and `.live-demo-ctx` in `site/styles/kinpaku-kit.css`. -### Don't: +- **Surface:** Lacquer Deep (`oklch(4% 0.004 95)`), always. Picker chrome does not adapt to the host page's light/dark theme. +- **Border:** 1.5px solid Kinpaku Gold (`oklch(84% 0.19 80.46)`). +- **Shadow:** `0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)` (kinpaku halo + drop shadow). +- **Brand mark:** Impeccable carved-tile icon (same SVG paths as `site/components/Header.astro` / `favicon.svg`), kinpaku fill on transparent ground. Not a "/" slash or rounded-square placeholder. +- **Default controls:** Champagne labels at rest (`oklch(84% 0.035 82)`), muted icons (`oklch(63% 0.024 82)`). +- **Active toggle:** Kinpaku-dim pill background (`oklch(78% 0.12 82 / 0.18)`) with kinpaku text. +- **Exit hover:** Vermilion (`oklch(58% 0.15 35)`), not a neutral gray lift. +- **Context bar internals:** Graphite-2 pills, gold hairline dividers, kinpaku Go/Accept CTAs with lacquer-deep text. +- **DESIGN.md toggle icon:** Four-quadrant swatch; bottom-right uses warm charcoal (`oklch(34% 0.014 82)`) so the tile reads against lacquer-deep, not void-black. -- **Don't** use pure black (#000) or pure white (#fff). Always the tinted neutrals (Deep Graphite / Warm Ash Cream / Crisp Paper White). -- **Don't** use `border-left` or `border-right` greater than 1px as a colored stripe on cards, list items, callouts, or alerts. Ever. This is the single most recognizable AI-dashboard tell. -- **Don't** use `background-clip: text` with a gradient. Gradient text is banned across the site. If you want emphasis, use weight or size, never gradient fill. -- **Don't** default to dark mode. The site is light mode because editorial reading is a light-mode activity. Dark mode with glowing accents is the AI-tool aesthetic Impeccable exists to replace. -- **Don't** use glassmorphism (blurred translucent cards, glass borders, glow backgrounds as decoration). It is on PRODUCT.md's explicit anti-reference list. -- **Don't** add a second accent color. If a layout "needs" a second emphasis point, use scale or weight, not hue. -- **Don't** use rounded rectangles with generic drop shadows. That's the "could be any AI output" fingerprint. -- **Don't** use bounce or elastic easing. Real objects decelerate smoothly — expo-out is the signature. -- **Don't** animate layout properties (`width`, `height`, `padding`, `margin`). Use `transform` and `opacity` only. -- **Don't** nest cards inside cards. Flatten the hierarchy. -- **Don't** use identical card grids (same-sized cards with icon + heading + text, repeated endlessly). -- **Don't** use the hero-metric layout template (big number + small label + supporting stats + gradient accent). SaaS cliché. -- **Don't** extend the Command Category Tints vocabulary. Those hex tints are scoped to the periodic-table viz. -- **Don't** hedge in UI copy. "Maybe consider" and "could be helpful" are banned in-product — match PRODUCT.md's expert-decisive voice. -- **Don't** introduce a new spacing token outside the 8/16/24/32/48/80/120 scale. If you need a specific pixel gap, use a literal value rather than polluting the token scale. +**The Picker Is Brand Rule.** Live mode UI is Impeccable product chrome, not host-page chrome. It always ships the full kinpaku border, lacquer-deep fill, and carved-tile mark. + +## 7. Do and Do Not + +### Do + +- Do use kinpaku gold as the primary brand color. +- Do use verdigris patina for secondary state and contrast. +- Do keep surfaces dark, warm, and mineral. +- Do use real texture assets for gold leaf and oxidation when the element is brand-bearing. +- Do use circuit/calibration geometry as structure, especially around product proof. +- Do keep cards compact, flat, and sharply bounded. +- Do preserve utility: demos, sliders, audit tables, command examples, docs, and pricing must remain understandable. +- Do keep live mode picker bars on lacquer-deep with kinpaku gold borders, regardless of host page theme. + +### Do Not + +- Do not use editorial magenta as a brand accent. +- Do not use italic serif display typography. +- Do not use purple gradients, neon cyan fields, glassmorphism, or generic AI-tool glow. +- Do not put gold texture under long text. +- Do not use beige, paper, or cream as the page ground. +- Do not add decorative calibration marks that do not align with real content. +- Do not use wide rounded cards or nested cards. +- Do not use pure black or pure white. +- Do not let the visual system hide the product proof. +- Do not theme-adapt live mode picker chrome to match arbitrary host pages. The picker is always neo-kinpaku. diff --git a/DEVELOP.md b/DEVELOP.md index 2bd922b0c..4d53b18bd 100644 --- a/DEVELOP.md +++ b/DEVELOP.md @@ -10,7 +10,7 @@ For detailed harness capabilities (which frontmatter fields each supports, place ## Source Format -### Skill (`skill/SKILL.md`) +### Skill (`skill/SKILL.src.md`) ```yaml --- @@ -127,10 +127,20 @@ scripts/ - `createTransformer(config)`: Factory that returns a transformer function from a provider config - `parseFrontmatter()`: Extracts YAML frontmatter and body from SKILL.md files -- `readSourceFiles()`: Reads `skill/SKILL.md` plus its `reference/` and `scripts/` siblings +- `readSourceFiles()`: Reads `skill/SKILL.src.md` plus its `reference/` and `scripts/` siblings - `replacePlaceholders()`: Substitutes `{{model}}`, `{{config_file}}`, etc. per provider - `generateYamlFrontmatter()`: Serializes objects to YAML frontmatter (auto-quotes values starting with `[` or `{`) +## Testing + +```bash +bun run test # Default suite — unit + static fixtures (no API keys needed) +bun run test:live-e2e # Opt-in — full-cycle live-mode E2E across framework fixtures (~2 min, needs `npx playwright install chromium` once) +bun run test:skill-behavior # Opt-in — LLM-backed checks that the SKILL.md Setup flow actually drives the agent (~5 min, costs cents, needs `.env`) +``` + +The skill-behavior suite runs three providers (claude-haiku-4-5, gpt-5.4-mini, gemini-3.1-flash-lite — the cheapest tier of each, every run) with the source `skill/SKILL.src.md` inlined as the system prompt and a workspace-scoped `bash`/`read`/`write`/`list` tool set. It then asserts on the tool-call trace, not on free-form output. Use it whenever you edit `skill/SKILL.src.md`'s Setup section, `skill/scripts/context.mjs`, or any Setup-touching reference (`teach.md`, `document.md`, `brand.md`, `product.md`, sub-command refs). Per-scenario assertions and the current baseline (21-22/24) live in `tests/skill-behavior/README.md`. Provider keys live in repo-root `.env` (gitignored); missing keys skip cleanly. + ## Best Practices ### Skill Writing @@ -140,7 +150,7 @@ scripts/ 3. **Clear instructions**: LLM should understand exactly what to do 4. **Include examples**: Where they clarify intent 5. **State constraints**: What NOT to do as clearly as what to do -6. **Test across providers**: Verify it works in multiple contexts +6. **Test across providers**: Verify it works in multiple contexts. For Setup-related edits to `skill/`, `bun run test:skill-behavior` automates this across three providers. ## Reference Documentation diff --git a/README.md b/README.md index c899efbf5..6691d96c1 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Impeccable adds: ### The Skill: impeccable -A comprehensive design skill with 7 domain-specific references ([view skill](skill/SKILL.md)): +A comprehensive design skill with 7 domain-specific references ([view skill](skill/SKILL.src.md)): | Reference | Covers | |-----------|--------| @@ -38,7 +38,7 @@ All commands are accessed through `/impeccable`: | Command | What it does | |---------|--------------| | `/impeccable craft` | Full shape-then-build flow with visual iteration | -| `/impeccable teach` | One-time setup: gather design context, write root PRODUCT.md and DESIGN.md | +| `/impeccable init` | One-time setup: gather design context, write PRODUCT.md and DESIGN.md, configure live mode, recommend next steps | | `/impeccable document` | Generate root DESIGN.md from existing project code | | `/impeccable extract` | Pull reusable components and tokens into the design system | | `/impeccable shape` | Plan UX/UI before writing code | @@ -93,11 +93,23 @@ Visit [impeccable.style](https://impeccable.style#casestudies) to see before/aft ## Installation -### Option 1: Download from Website (Recommended) +### Option 1: CLI installer (Recommended) + +From the root of your project, run: + +```bash +npx impeccable skills install +``` + +This auto-detects your harness and writes the build compiled for it to the right location (`.claude/skills/`, `.cursor/skills/`, etc.). Works with Cursor, Claude Code, Gemini CLI, Codex CLI, and every other supported tool. Reload your harness afterward. + +Claude Code users can alternatively install the plugin with `/plugin marketplace add pbakaus/impeccable`. The general-purpose `npx skills add pbakaus/impeccable` also works, though it installs one shared build for all harnesses rather than the one compiled for yours. + +### Option 2: Download from Website Visit [impeccable.style](https://impeccable.style), download the ZIP for your tool, and extract to your project. -### Option 2: Copy from Repository +### Option 3: Copy from Repository **Cursor:** ```bash @@ -195,22 +207,26 @@ cp -r dist/qoder/.qoder/skills/* ~/.qoder/skills/ ## Usage -Once installed, use commands in your AI harness: +Once installed, every command runs through the single `/impeccable` skill: ``` -/audit # Find issues -/normalize # Fix inconsistencies -/polish # Final cleanup -/distill # Remove complexity +/impeccable audit # Find issues +/impeccable polish # Final cleanup +/impeccable distill # Remove complexity +/impeccable critique # Full design review ``` +Type `/impeccable` alone to see the full command list. + Most commands accept an optional argument to focus on a specific area: ``` -/audit header -/polish checkout-form +/impeccable audit the header +/impeccable polish the checkout form ``` +If you reach for one command often, pin it with `/impeccable pin audit` to get `/audit` as a standalone shortcut. + **Note:** Codex uses skills here, not `/prompts:` commands. Open `/skills` or type `$impeccable`. Repo-local installs live in `.agents/skills/`; user-wide installs live in `~/.agents/skills/`. GitHub Copilot uses `.github/skills/`. Restart the tool if a newly installed skill does not appear. ## CLI diff --git a/astro.config.mjs b/astro.config.mjs index 5813ee4dc..8dd52a511 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -4,6 +4,9 @@ export default defineConfig({ srcDir: './site', publicDir: './site/public', output: 'static', + devToolbar: { + enabled: false, + }, build: { format: 'directory', }, diff --git a/bun.lock b/bun.lock index 6f89438fc..469c87f7a 100644 --- a/bun.lock +++ b/bun.lock @@ -13,6 +13,7 @@ }, "devDependencies": { "@ai-sdk/anthropic": "^3.0.71", + "@ai-sdk/google": "^3.0.75", "@ai-sdk/openai": "^3.0.53", "@anthropic-ai/claude-agent-sdk": "^0.2.119", "@anthropic-ai/sdk": "^0.91.1", @@ -21,8 +22,10 @@ "ai": "^6.0.168", "archiver": "^7.0.1", "astro": "^6.2.1", + "fontkit": "^2.0.4", "modern-screenshot": "^4.7.0", "motion": "^12.38.0", + "opentype.js": "^2.0.0", "playwright": "^1.59.1", "wrangler": "^4.85.0", "zod": "^4.3.6", @@ -37,6 +40,8 @@ "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.104", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA=="], + "@ai-sdk/google": ["@ai-sdk/google@3.0.75", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-XAm31ftiOrzlb8NjDzT7kw0xw+4lmgFdGFn1QKM73nXFFKyN1kWLESBV75UGNfjXP8X1YJ0YydnMVqO0jaPghw=="], + "@ai-sdk/openai": ["@ai-sdk/openai@3.0.53", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.23" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ=="], "@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="], @@ -335,6 +340,8 @@ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@swc/helpers": ["@swc/helpers@0.5.21", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg=="], + "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="], "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], @@ -429,6 +436,8 @@ "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "brotli": ["brotli@1.3.3", "", { "dependencies": { "base64-js": "^1.1.2" } }, "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg=="], + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], "buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="], @@ -459,6 +468,8 @@ "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -529,6 +540,8 @@ "devtools-protocol": ["devtools-protocol@0.0.1595872", "", {}, "sha512-kRfgp8vWVjBu/fbYCiVFiOqsCk3CrMKEo3WbgGT2NXK2dG7vawWPBljixajVgGK9II8rDO9G0oD0zLt3I1daRg=="], + "dfa": ["dfa@1.2.0", "", {}, "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="], + "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], @@ -637,6 +650,8 @@ "fontace": ["fontace@0.4.1", "", { "dependencies": { "fontkitten": "^1.0.2" } }, "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw=="], + "fontkit": ["fontkit@2.0.4", "", { "dependencies": { "@swc/helpers": "^0.5.12", "brotli": "^1.3.2", "clone": "^2.1.2", "dfa": "^1.2.0", "fast-deep-equal": "^3.1.3", "restructure": "^3.0.0", "tiny-inflate": "^1.0.3", "unicode-properties": "^1.4.0", "unicode-trie": "^2.0.0" } }, "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g=="], + "fontkitten": ["fontkitten@1.0.3", "", { "dependencies": { "tiny-inflate": "^1.0.3" } }, "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw=="], "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], @@ -955,6 +970,8 @@ "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], + "opentype.js": ["opentype.js@2.0.0", "", { "bin": { "ot": "bin/ot" } }, "sha512-kCyjv6xdDY1W/jLWZ/L3QhhTlKUqDZMQ5+Jdlw12b3dXkKNpYBqqlMMj0YDQPShWFTMwgZI1hG14kN3XUDSg/A=="], + "p-limit": ["p-limit@7.3.0", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw=="], "p-queue": ["p-queue@9.2.0", "", { "dependencies": { "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" } }, "sha512-dWgLE8AH0HjQ9fe74pUkKkvzzYT18Inp4zra3lKHnnwqGvcfcUBrvF2EAVX+envufDNBOzpPq/IBUONDbI7+3g=="], @@ -971,6 +988,8 @@ "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], + "pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="], + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], @@ -1073,6 +1092,8 @@ "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "restructure": ["restructure@3.0.2", "", {}, "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw=="], + "retext": ["retext@9.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "retext-latin": "^4.0.0", "retext-stringify": "^4.0.0", "unified": "^11.0.0" } }, "sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA=="], "retext-latin": ["retext-latin@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "parse-latin": "^7.0.0", "unified": "^11.0.0" } }, "sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA=="], @@ -1199,6 +1220,10 @@ "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], + "unicode-properties": ["unicode-properties@1.4.1", "", { "dependencies": { "base64-js": "^1.3.0", "unicode-trie": "^2.0.0" } }, "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg=="], + + "unicode-trie": ["unicode-trie@2.0.0", "", { "dependencies": { "pako": "^0.2.5", "tiny-inflate": "^1.0.0" } }, "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ=="], + "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], "unifont": ["unifont@0.7.4", "", { "dependencies": { "css-tree": "^3.1.0", "ofetch": "^1.5.1", "ohash": "^2.0.11" } }, "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg=="], @@ -1285,6 +1310,10 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@ai-sdk/google/@ai-sdk/provider": ["@ai-sdk/provider@3.0.10", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw=="], + + "@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.27", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw=="], + "@anthropic-ai/claude-agent-sdk/@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.81.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-D4K5PvEV6wPiRtVlVsJHIUhHAmOZ6IT/I9rKlTf84gR7GyyAurPJK7z9BOf/AZqC5d1DhYQGJNKRmV+q8dGhgw=="], "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], @@ -1329,6 +1358,8 @@ "yauzl/buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], + "@ai-sdk/google/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="], + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], diff --git a/cli/bin/commands/skills.mjs b/cli/bin/commands/skills.mjs index 4df17e98d..d234d93c1 100644 --- a/cli/bin/commands/skills.mjs +++ b/cli/bin/commands/skills.mjs @@ -3,7 +3,7 @@ * * Usage: * impeccable skills help Show all available skills and commands - * impeccable skills install Install skills via npx skills add + * impeccable skills install Install compiled skills from the universal bundle * impeccable skills update Update skills to latest version */ @@ -14,7 +14,7 @@ import { createInterface } from 'node:readline'; import { fileURLToPath } from 'node:url'; import { get } from 'node:https'; import { createHash } from 'node:crypto'; -import { tmpdir } from 'node:os'; +import { tmpdir, homedir } from 'node:os'; const __dirname = dirname(fileURLToPath(import.meta.url)); const API_BASE = 'https://impeccable.style'; @@ -22,6 +22,23 @@ const API_BASE = 'https://impeccable.style'; // Provider folder names in project roots const PROVIDER_DIRS = ['.claude', '.cursor', '.gemini', '.agents', '.github', '.kiro', '.opencode', '.pi', '.qoder', '.trae', '.trae-cn']; +// When a project has no harness folder yet, infer the target from globally +// installed harnesses (~/.claude, ~/.codex, ...). Codex reads skills from +// .agents/skills, so ~/.codex maps to the .agents bundle variant. +const GLOBAL_HARNESS_HINTS = [ + { home: '.claude', provider: '.claude' }, + { home: '.codex', provider: '.agents' }, + { home: '.cursor', provider: '.cursor' }, + { home: '.gemini', provider: '.gemini' }, + { home: '.kiro', provider: '.kiro' }, + { home: '.opencode', provider: '.opencode' }, + { home: '.qoder', provider: '.qoder' }, +]; + +// Last-resort default when nothing is detected: Claude Code + the universal +// (.agents, also Codex) folder, which covers the most common setups. +const DEFAULT_TARGETS = ['.claude', '.agents']; + function ask(question) { const rl = createInterface({ input: process.stdin, output: process.stdout }); return new Promise(r => rl.question(question, ans => { rl.close(); r(ans.trim().toLowerCase()); })); @@ -321,10 +338,71 @@ function renameSkillsWithPrefix(root, prefix) { return count; } +/** + * Decide which provider folders to install into. + * 1. An explicit --providers=.claude,.cursor list wins. + * 2. Otherwise, harness folders already present in the project. + * 3. Otherwise, infer from globally installed harnesses (~/.claude, ~/.codex). + * 4. Otherwise, a sensible default (.claude + .agents). + */ +function resolveInstallTargets(root, providersValue) { + if (providersValue) { + const wanted = providersValue + .split(',') + .map(s => s.trim()) + .filter(Boolean) + .map(s => (s.startsWith('.') ? s : `.${s}`)) + .filter(p => PROVIDER_DIRS.includes(p)); + return [...new Set(wanted)]; + } + + const inProject = PROVIDER_DIRS.filter(d => existsSync(join(root, d))); + if (inProject.length > 0) return inProject; + + const home = homedir(); + const inferred = []; + for (const { home: h, provider } of GLOBAL_HARNESS_HINTS) { + if (existsSync(join(home, h)) && !inferred.includes(provider)) inferred.push(provider); + } + if (inferred.length > 0) return inferred; + + return [...DEFAULT_TARGETS]; +} + +/** + * Copy each target provider's compiled skill variant from an extracted bundle + * into the project. Writes real directories (copy, never symlink) so every + * harness keeps the build that was compiled for it. Returns skills written. + */ +function copyProviderSkills(bundleDir, root, targets) { + let written = 0; + for (const provider of targets) { + const srcDir = join(bundleDir, provider, 'skills'); + if (!existsSync(srcDir)) continue; + const localSkillsDir = join(root, provider, 'skills'); + // A previous `npx skills` install may have left this provider's skills dir + // as a symlink to another provider's canonical copy. Drop the link so we + // write a real, provider-specific directory instead of writing through it. + try { + if (lstatSync(localSkillsDir).isSymbolicLink()) unlinkSync(localSkillsDir); + } catch {} + for (const skill of readdirSync(srcDir, { withFileTypes: true })) { + if (!skill.isDirectory()) continue; + const src = join(srcDir, skill.name); + const dest = join(localSkillsDir, skill.name); + rmSync(dest, { recursive: true, force: true }); + copyDirSync(src, dest); + written++; + } + } + return written; +} + async function install(flags) { const force = flags.includes('--force'); const yes = flags.includes('-y') || flags.includes('--yes'); const prefixFlag = flags.find(f => f.startsWith('--prefix=')); + const providersFlag = flags.find(f => f.startsWith('--providers=')); const root = findProjectRoot(); const existing = isAlreadyInstalled(root); @@ -334,18 +412,52 @@ async function install(flags) { process.exit(0); } - console.log('Installing impeccable skills via npx skills...\n'); - try { - // --copy forces npx skills to install each provider's variant separately - // instead of symlinking .claude/skills/ to .agents/skills/. The two - // directories have meaningfully different per-provider content (frontmatter, - // command prefix, paths), and the default symlink also fails silently when - // .claude/ doesn't exist yet or on Windows without elevated privileges (#140). - execSync(`npx skills add pbakaus/impeccable --copy${yes ? ' -y' : ''}`, { stdio: 'inherit' }); - } catch (e) { - process.exit(e.status ?? 1); + // Decide which harness folders to install into, then copy each harness's own + // compiled variant from the universal bundle. We deliberately do NOT shell out + // to `npx skills add`: its name-based discovery can install the uncompiled + // source, and its symlink default points every harness at one shared variant. + // Copying per-provider variants is the only correct install for this skill. + const targets = resolveInstallTargets(root, providersFlag ? providersFlag.split('=')[1] : null); + if (targets.length === 0) { + console.error('Could not determine a target harness folder.'); + console.error('Pass one explicitly, e.g. --providers=.claude,.cursor'); + process.exit(1); } + if (!yes) { + console.log(`Target harness folder(s): ${targets.join(', ')}`); + const ans = await ask(`Install impeccable skills into ${targets.length} folder(s)? (Y/n) `); + if (ans === 'n' || ans === 'no') { + console.log('Aborted. Re-run with --providers= to choose explicitly (e.g. --providers=.claude,.cursor).'); + process.exit(0); + } + } + + console.log('\nDownloading impeccable skills...'); + let bundleDir; + try { + bundleDir = await downloadAndExtractBundle(); + } catch (e) { + console.error(`Download failed: ${e.message}`); + process.exit(1); + } + + let written = 0; + try { + written = copyProviderSkills(bundleDir, root, targets); + } catch (e) { + rmSync(bundleDir, { recursive: true, force: true }); + console.error(`Install failed: ${e.message}`); + process.exit(1); + } + rmSync(bundleDir, { recursive: true, force: true }); + + if (written === 0) { + console.error(`Nothing was installed: the bundle had no variants for ${targets.join(', ')}.`); + process.exit(1); + } + console.log(`Installed impeccable into: ${targets.join(', ')}`); + // Ask about prefixing (skip in CI mode unless --prefix= is set) let prefix = ''; if (prefixFlag) { @@ -379,7 +491,7 @@ async function install(flags) { // Cleanup script not available -- skip } - console.log(`\nDone! Run /${prefix}impeccable teach in your AI harness to set up design context.\n`); + console.log(`\nDone! Run /${prefix}impeccable init in your AI harness to set up design context.\n`); } /** Detect prefix by looking for the 'impeccable' skill (or legacy 'teach-impeccable') */ diff --git a/cli/engine/browser/injected/index.mjs b/cli/engine/browser/injected/index.mjs index c03a69caf..ceff16737 100644 --- a/cli/engine/browser/injected/index.mjs +++ b/cli/engine/browser/injected/index.mjs @@ -9,8 +9,21 @@ if (IS_BROWSER) { const EXTENSION_MODE = (_myScript && _myScript.dataset.impeccableExtension === 'true') || document.documentElement.dataset.impeccableExtension === 'true'; - const BRAND_COLOR = 'oklch(55% 0.25 350)'; - const BRAND_COLOR_HOVER = 'oklch(45% 0.25 350)'; + // Kinpaku gold — pinned to the site's brand token (see + // site/styles/kinpaku-tokens.css --ks-kinpaku). Keep this in sync with + // the picker's C.brand in skill/scripts/live-browser.js and the kit's + // picker section in site/styles/kinpaku-kit.css. + // + // One color across both light and dark host pages. The outline is a + // 2px gesture pointing at an element + a labeled tag — it's a marker, + // not body text, so it doesn't need WCAG AA against the page. The + // label text inside the gold tag is dark (LABEL_INK) which has ~16:1 + // against the leaf gold, so reading the rule name is solid in both + // modes. Hover deepens the gold (preserves chroma — never drops it, + // dropping chroma washes the gold into a sand/olive tone). + const BRAND_COLOR = 'oklch(84% 0.19 80.46)'; + const BRAND_COLOR_HOVER = 'oklch(74% 0.18 80)'; + const LABEL_INK = 'oklch(4% 0.004 95)'; const LABEL_BG = BRAND_COLOR; const OUTLINE_COLOR = BRAND_COLOR; @@ -278,7 +291,7 @@ if (IS_BROWSER) { display: 'flex', alignItems: 'center', whiteSpace: 'nowrap', fontSize: '11px', fontWeight: '600', letterSpacing: '0.02em', - color: 'white', lineHeight: '14px', + color: LABEL_INK, lineHeight: '14px', background: LABEL_BG, fontFamily: 'system-ui, sans-serif', borderRadius: '4px 4px 0 0', @@ -398,7 +411,7 @@ if (IS_BROWSER) { banner.className = 'impeccable-overlay impeccable-banner'; Object.assign(banner.style, { position: 'fixed', top: '0', left: '0', right: '0', zIndex: '100000', - background: LABEL_BG, color: 'white', + background: LABEL_BG, color: LABEL_INK, fontFamily: 'system-ui, sans-serif', fontSize: '13px', display: 'flex', alignItems: 'center', pointerEvents: 'auto', height: '36px', overflow: 'hidden', maxWidth: '100vw', @@ -1223,12 +1236,12 @@ if (IS_BROWSER) { } console.group( `%c[impeccable] ${allFindings.length} anti-pattern${allFindings.length === 1 ? '' : 's'} found`, - 'color: oklch(60% 0.25 350); font-weight: bold' + 'color: oklch(84% 0.19 80.46); font-weight: bold' ); for (const { el, findings } of allFindings) { for (const f of findings) { console.log(`%c${f.type || f.id}%c ${f.detail || f.snippet}`, - 'color: oklch(55% 0.25 350); font-weight: bold', 'color: inherit', el); + 'color: oklch(84% 0.19 80.46); font-weight: bold', 'color: inherit', el); } } console.groupEnd(); @@ -1249,6 +1262,10 @@ if (IS_BROWSER) { const groupMap = new Map(); const _disabled = EXTENSION_MODE ? (window.__IMPECCABLE_CONFIG__?.disabledRules || []) : []; const _ruleOk = (id) => !_disabled.length || !_disabled.includes(id); + // Note: provider-gated rules (--gpt / --gemini) are NOT filtered here. In a + // real browser env (detector page, live overlay, extension) running every + // check is free, so we always surface them; the gating is purely a CLI + // output concern, applied in the Node engines' detect* return paths. for (const el of document.querySelectorAll('*')) { // Skip impeccable's own elements and any descendants (overlays, labels, banner, nav buttons) @@ -1270,11 +1287,23 @@ if (IS_BROWSER) { ...checkElementAIPaletteDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementIconTileDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementItalicSerifDOM(el).map(f => ({ type: f.id, detail: f.snippet })), - ...checkElementHeroEyebrowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ...checkElementQualityDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementOversizedH1DOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementClippedOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementGptBorderShadowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ...checkElementTextOverflowDOM(el).map(f => ({ type: f.id, detail: f.snippet })), ].filter(f => _ruleOk(f.type)); addBrowserFindings(groupMap, el, findings); + + // Hero eyebrow: the offending element is the eyebrow above the heading, + // not the heading itself — highlight the previous sibling instead. + const eyebrowFindings = checkElementHeroEyebrowDOM(el) + .map(f => ({ type: f.id, detail: f.snippet })) + .filter(f => _ruleOk(f.type)); + if (eyebrowFindings.length > 0 && el.previousElementSibling) { + addBrowserFindings(groupMap, el.previousElementSibling, eyebrowFindings); + } } const pageLevelFindings = []; @@ -1306,6 +1335,14 @@ if (IS_BROWSER) { addBrowserFindings(groupMap, document.body, qualityFindings); } + const creamFindings = checkCreamPalette(document) + .map(f => ({ type: f.id, detail: f.snippet })) + .filter(f => _ruleOk(f.type)); + if (creamFindings.length > 0) { + pageLevelFindings.push(...creamFindings); + addBrowserFindings(groupMap, document.body, creamFindings); + } + // Regex-on-HTML checks (shared with Node) // Clone the document and strip impeccable-live overlay nodes before the // regex scan, so the inspector's own inline styles (transitions on top/ diff --git a/cli/engine/cli/main.mjs b/cli/engine/cli/main.mjs index 76da09f0c..94283e01b 100644 --- a/cli/engine/cli/main.mjs +++ b/cli/engine/cli/main.mjs @@ -41,7 +41,7 @@ function formatFindings(findings, jsonMode) { // Stdin handling // --------------------------------------------------------------------------- -async function handleStdin() { +async function handleStdin(options = {}) { const chunks = []; for await (const chunk of process.stdin) chunks.push(chunk); const input = Buffer.concat(chunks).toString('utf-8'); @@ -50,10 +50,10 @@ async function handleStdin() { const fp = parsed?.tool_input?.file_path; if (fp && fs.existsSync(fp)) { return HTML_EXTENSIONS.has(path.extname(fp).toLowerCase()) - ? detectHtml(fp) : detectText(fs.readFileSync(fp, 'utf-8'), fp); + ? detectHtml(fp, options) : detectText(fs.readFileSync(fp, 'utf-8'), fp, options); } } catch { /* not JSON */ } - return detectText(input, ''); + return detectText(input, '', options); } @@ -81,6 +81,8 @@ Scan files or URLs for UI anti-patterns and design quality issues. Options: --fast Regex-only mode (skip static HTML/CSS analysis, faster but misses linked stylesheets) --json Output results as JSON + --gpt Also report GPT-specific provider tells (off by default) + --gemini Also report Gemini-specific provider tells (off by default) --help Show this help message Detection modes: @@ -106,6 +108,10 @@ async function detectCli() { const jsonMode = args.includes('--json'); const helpMode = args.includes('--help'); const fastMode = args.includes('--fast'); + const providers = []; + if (args.includes('--gpt')) providers.push('gpt'); + if (args.includes('--gemini')) providers.push('gemini'); + const scanOptions = { providers }; const targets = args.filter(a => !a.startsWith('--')); if (helpMode) { printUsage(); process.exit(0); } @@ -113,7 +119,7 @@ async function detectCli() { let allFindings = []; if (!process.stdin.isTTY && targets.length === 0) { - allFindings = await handleStdin(); + allFindings = await handleStdin(scanOptions); } else { const paths = targets.length > 0 ? targets : [process.cwd()]; const urlTargetCount = paths.filter(target => /^https?:\/\//i.test(target)).length; @@ -124,8 +130,8 @@ async function detectCli() { if (/^https?:\/\//i.test(target)) { try { const scanner = browserDetector - ? (url) => browserDetector.detectUrl(url) - : (url) => detectUrl(url); + ? (url) => browserDetector.detectUrl(url, scanOptions) + : (url) => detectUrl(url, scanOptions); allFindings.push(...await scanner(target)); } catch (e) { process.stderr.write(`Error: ${e.message}\n`); } continue; @@ -192,9 +198,9 @@ async function detectCli() { const ext = path.extname(file).toLowerCase(); let fileFindings; if (!fastMode && HTML_EXTENSIONS.has(ext)) { - fileFindings = await detectHtml(file); + fileFindings = await detectHtml(file, scanOptions); } else { - fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file); + fileFindings = detectText(fs.readFileSync(file, 'utf-8'), file, scanOptions); } // Annotate findings with import context const importers = importedByMap.get(file); @@ -209,9 +215,9 @@ async function detectCli() { } else if (stat.isFile()) { const ext = path.extname(resolved).toLowerCase(); if (!fastMode && HTML_EXTENSIONS.has(ext)) { - allFindings.push(...await detectHtml(resolved)); + allFindings.push(...await detectHtml(resolved, scanOptions)); } else { - allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved)); + allFindings.push(...detectText(fs.readFileSync(resolved, 'utf-8'), resolved, scanOptions)); } } } diff --git a/cli/engine/detect-antipatterns-browser.js b/cli/engine/detect-antipatterns-browser.js index 1afbe3f12..88439a5c3 100644 --- a/cli/engine/detect-antipatterns-browser.js +++ b/cli/engine/detect-antipatterns-browser.js @@ -38,7 +38,7 @@ const OVERUSED_FONTS = new Set([ // Older monoculture (still ubiquitous): 'inter', 'roboto', 'open sans', 'lato', 'montserrat', 'arial', 'helvetica', // Newer monoculture (the Anthropic-skill / Vercel / GitHub default wave): - 'fraunces', 'instrument sans', + 'fraunces', 'instrument sans', 'instrument serif', 'geist', 'geist sans', 'geist mono', 'mona sans', 'plus jakarta sans', 'space grotesk', 'recoleta', @@ -165,6 +165,15 @@ const ANTIPATTERNS = [ skillSection: 'Color & Contrast', skillGuideline: 'AI color palette', }, + { + id: 'cream-palette', + category: 'slop', + name: 'Cream / beige palette', + description: + 'A warm cream or beige page background has become the default "tasteful" AI surface, reached for by reflex. Choose a background that comes from a deliberate palette, not the safe warm off-white.', + skillSection: 'Color & Contrast', + skillGuideline: 'cream and beige as the default surface', + }, { id: 'nested-cards', category: 'slop', @@ -183,15 +192,6 @@ const ANTIPATTERNS = [ skillSection: 'Layout & Space', skillGuideline: 'same spacing everywhere', }, - { - id: 'everything-centered', - category: 'slop', - name: 'Everything centered', - description: - 'Every text element is center-aligned. Left-aligned text with asymmetric layouts feels more designed. Center only hero sections and CTAs.', - skillSection: 'Layout & Space', - skillGuideline: 'Center everything', - }, { id: 'bounce-easing', category: 'slop', @@ -247,17 +247,72 @@ const ANTIPATTERNS = [ skillSection: 'Typography', skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding', }, + { + id: 'numbered-section-markers', + category: 'slop', + severity: 'advisory', + name: 'Numbered section markers (01 / 02 / 03)', + description: + 'Numbered display markers as section labels (01, 02, 03) are the AI editorial scaffold one tier deeper than tracked eyebrow chips. If you find yourself reaching for them, choose a different section cadence.', + skillSection: 'Layout & Space', + skillGuideline: 'numbered section markers', + }, + { + id: 'em-dash-overuse', + category: 'slop', + name: 'Em-dash overuse', + description: + 'More than two em-dashes (— or --) in body copy is an AI cadence tell. Use commas, colons, periods, or parentheses instead.', + skillSection: 'Copy', + skillGuideline: 'no em dashes', + }, + { + id: 'marketing-buzzword', + category: 'slop', + name: 'Marketing buzzword', + description: + 'Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does.', + skillSection: 'Copy', + skillGuideline: 'marketing buzzwords', + }, + { + id: 'aphoristic-cadence', + category: 'slop', + name: 'Aphoristic-cadence copy', + description: + 'Three or more sections landing on a short rebuttal sentence ("X. No Y." / "X. Just Y.") or a manufactured-contrast aphorism ("Not a feature. A platform.") reads as AI cadence, not voice. Once is fine; the pattern is the tell.', + skillSection: 'Copy', + skillGuideline: 'aphoristic cadence', + }, + { + id: 'oversized-h1', + category: 'slop', + name: 'Oversized hero headline', + description: + 'A full-sentence headline set at display size ends up dominating the viewport, leaving no room for anything else above the fold. A punchy one- or two-word headline at that size is fine — the problem is a long headline blown up too large. Set long headlines smaller, or tighten the copy.', + skillSection: 'Typography', + skillGuideline: 'long headline set at display size', + }, + { + id: 'extreme-negative-tracking', + category: 'slop', + name: 'Crushed letter spacing', + description: + 'Letter-spacing pulled tighter than the point where characters keep their own shapes costs legibility. Tighten display type optically, not destructively.', + skillSection: 'Typography', + skillGuideline: 'letter spacing crushed past legibility', + }, + { + id: 'broken-image', + category: 'quality', + name: 'Broken or placeholder image', + description: + ' tags with empty src, missing src, or placeholder values ship as broken-image boxes. Use real images, generated assets, or remove the tag.', + skillSection: 'Imagery', + skillGuideline: 'broken image references', + }, // ── Quality: general design and accessibility issues ── - { - id: 'pure-black-white', - category: 'quality', - name: 'Pure black background', - description: - 'Pure #000000 as a background color looks harsh and unnatural. Tint it slightly toward your brand hue (e.g., oklch(12% 0.01 250)) for a more refined feel.', - skillSection: 'Color & Contrast', - skillGuideline: 'pure black (#000)', - }, { id: 'gray-on-color', category: 'quality', @@ -297,7 +352,9 @@ const ANTIPATTERNS = [ category: 'quality', name: 'Cramped padding', description: - 'Text is too close to the edge of its container. Add at least 8px (ideally 12-16px) of padding inside bordered or colored containers.', + 'Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.', + skillSection: 'Layout & Space', + skillGuideline: 'inside bordered or colored containers', }, { id: 'body-text-viewport-edge', @@ -350,6 +407,70 @@ const ANTIPATTERNS = [ description: 'Letter spacing above 0.05em on body text disrupts natural character groupings and slows reading. Reserve wide tracking for short uppercase labels only.', }, + { + id: 'text-overflow', + category: 'quality', + name: 'Content overflowing its container', + description: + 'Content renders wider than its container, spilling out or forcing a horizontal scrollbar. Let text wrap, constrain widths, or give the region a deliberate scroll affordance.', + skillSection: 'Layout & Space', + skillGuideline: 'content wider than its container', + }, + { + id: 'clipped-overflow-container', + category: 'quality', + name: 'Positioned child clipped by overflow container', + description: + 'A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.', + skillSection: 'Layout & Space', + skillGuideline: 'overflow container clipping positioned children', + }, + + // ── Provider tells: opt-in via --gpt / --gemini (gated off by default) ── + { + id: 'gpt-thin-border-wide-shadow', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Hairline border with wide shadow', + description: + 'A hairline border paired with a wide, diffuse shadow is a recurring generated-UI signature. Commit to one — a defined edge or a soft elevation — rather than both at once.', + skillSection: 'Visual Details', + skillGuideline: 'hairline border plus wide diffuse shadow', + }, + { + id: 'repeating-stripes-gradient', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Repeating-gradient stripes', + description: + 'Repeating-gradient stripes used as surface decoration are a recurring generated-UI signature. Reach for a deliberate texture or leave the surface plain.', + skillSection: 'Visual Details', + skillGuideline: 'repeating-gradient decorative stripes', + }, + { + id: 'theater-slop-phrase', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Theater framing copy', + description: + 'Dismissing something as "theater" is a recurring generated-copy tic. Say plainly what the thing does or does not do.', + skillSection: 'Copy', + skillGuideline: 'theater framing copy', + }, + { + id: 'image-hover-transform', + category: 'slop', + severity: 'advisory', + gated: 'gemini', + name: 'Image hover transform', + description: + 'Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction.', + skillSection: 'Motion', + skillGuideline: 'image scale or rotate on hover', + }, ]; // --- cli/engine/shared/color.mjs --- @@ -527,11 +648,6 @@ function checkColors(opts) { } const findings = []; - // Pure black background (only solid or near-solid, not semi-transparent overlays) - if (bgColor && bgColor.a >= 0.9 && bgColor.r === 0 && bgColor.g === 0 && bgColor.b === 0) { - findings.push({ id: 'pure-black-white', snippet: '#000000 background' }); - } - if (hasDirectText && textColor && !isEmojiOnly) { // Run background-dependent checks against either a solid bg or, if the // ancestor is a gradient, against every gradient stop (use the worst case). @@ -587,9 +703,6 @@ function checkColors(opts) { // Tailwind class checks if (classList) { const classStr = typeof classList === 'string' ? classList : Array.from(classList).join(' '); - if (/\bbg-black\b(?!\/)/.test(classStr)) { - findings.push({ id: 'pure-black-white', snippet: 'bg-black' }); - } const grayMatch = classStr.match(/\btext-(?:gray|slate|zinc|neutral|stone)-\d+\b/); const colorBgMatch = classStr.match(/\bbg-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d+\b/); @@ -905,12 +1018,6 @@ function checkHtmlPatterns(html) { // --- Color --- - // Pure black background - const pureBlackBgRe = /background(?:-color)?\s*:\s*(?:#000000|#000|rgb\(\s*0,\s*0,\s*0\s*\))\b/gi; - if (pureBlackBgRe.test(html)) { - findings.push({ id: 'pure-black-white', snippet: 'Pure #000 background' }); - } - // AI color palette: purple/violet const purpleHexRe = /#(?:7c3aed|8b5cf6|a855f7|9333ea|7e22ce|6d28d9|6366f1|764ba2|667eea)\b/gi; if (purpleHexRe.test(html)) { @@ -1027,6 +1134,39 @@ function checkHtmlPatterns(html) { } } + // --- Provider tells (gated): repeating-gradient stripes (GPT) --- + if (/repeating-(?:linear|radial|conic)-gradient\s*\(/i.test(html)) { + findings.push({ id: 'repeating-stripes-gradient', snippet: 'repeating-gradient decorative stripes' }); + } + + // --- Provider tells (gated): "X theater" framing copy (GPT) --- + // Lives here (regex-on-HTML) rather than in the text-content analyzers so it + // runs in the bundled browser path too, not just the CLI/static path. + { + const bodyText = html + .replace(/]*>[\s\S]*?<\/script>/gi, ' ') + .replace(/]*>[\s\S]*?<\/style>/gi, ' ') + .replace(/<[^>]+>/g, ' '); + const tm = /\b(\w+)\s+theater\b/i.exec(bodyText); + if (tm) findings.push({ id: 'theater-slop-phrase', snippet: `"${tm[0].trim()}"` }); + } + + // --- Provider tells (gated): image hover transform (Gemini) --- + // A CSS `img...:hover { transform: ... }` rule, or a Tailwind hover:scale / + // hover:rotate / hover:translate utility on an . Each distinct + // mechanism is its own finding. + const imgHoverCss = /\bimg\b[^,{}]*:hover\b[^{}]*\{[^}]*\btransform\s*:\s*(?:scale|rotate|translate|matrix|skew)/i; + if (imgHoverCss.test(html)) { + findings.push({ id: 'image-hover-transform', snippet: 'img:hover { transform } rule' }); + } + const imgTagRe = /]*\bclass\s*=\s*"([^"]*)"/gi; + let im; + while ((im = imgTagRe.exec(html)) !== null) { + if (/\bhover:(?:scale|rotate|translate|skew)-/.test(im[1])) { + findings.push({ id: 'image-hover-transform', snippet: 'Tailwind hover transform on ' }); + } + } + return findings; } @@ -1673,7 +1813,7 @@ function resolveLengthPx(value, fontSizePx) { // Both adapters resolve font-size, line-height and letter-spacing to pixels // before calling this so the pure function only deals with numbers. function checkQuality(opts) { - const { el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax = 80, viewportWidth = 0 } = opts; + const { el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax = 80, viewportWidth = 0, win = null } = opts; const findings = []; // Skip browser extension injected elements const elId = el.id || ''; @@ -1724,6 +1864,155 @@ function checkQuality(opts) { } } + // --- Flush against a visible boundary --- + // Fires when a container has a visible boundary (border, outline, OR a + // non-transparent background) AND near-zero padding on the bounded + // side(s) AND text-bearing children land flush against the boundary. + // + // Distinct from cramped-padding: that rule needs the element itself to + // have direct text (hasDirectText). This rule targets the OPPOSITE + // shape — a container with NO direct text, only children — which is + // exactly what cramped-padding misses (a section wrapping a label + + // list lands a free pass). + // + // The classic shape: agent writes `padding: 28px 0 0` shorthand on a + // section that also has a border, zeroing horizontal padding so the + // text-bearing children touch the side borders. Background and + // outline count too: a colored card with zero padding has the same + // visual failure mode. + { + const FLUSH_SKIP_TAGS = new Set(['HTML', 'BODY', 'MAIN', 'HEADER', 'FOOTER', 'NAV', 'ARTICLE', 'ASIDE', 'BUTTON', 'A', 'LABEL', 'SUMMARY', 'CODE', 'PRE', 'INPUT', 'TEXTAREA', 'SELECT', 'FORM', 'FIGURE', 'TABLE', 'TBODY', 'THEAD', 'TR', 'TD', 'TH']); + const upperTag = tag ? tag.toUpperCase() : ''; + const elPosition = style.position || ''; + if ( + !FLUSH_SKIP_TAGS.has(upperTag) && + !hasDirectText && + !['fixed', 'absolute'].includes(elPosition) && + el.children && el.children.length > 0 + ) { + const isTransparent = (c) => + !c || c === 'transparent' || c === 'rgba(0, 0, 0, 0)' || + /^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*0(?:\.0+)?\s*\)$/.test(c); + + const borderW = { + top: parseFloat(style.borderTopWidth) || 0, + right: parseFloat(style.borderRightWidth) || 0, + bottom: parseFloat(style.borderBottomWidth) || 0, + left: parseFloat(style.borderLeftWidth) || 0, + }; + const borderVisible = { + top: borderW.top > 0 && !isTransparent(style.borderTopColor), + right: borderW.right > 0 && !isTransparent(style.borderRightColor), + bottom: borderW.bottom > 0 && !isTransparent(style.borderBottomColor), + left: borderW.left > 0 && !isTransparent(style.borderLeftColor), + }; + // Outline detection. jsdom decomposes `border` shorthand into + // border{Top,…}Width/Color but does NOT decompose `outline` — + // the longhands come back empty when the value was set via the + // shorthand. Fall back to parsing `style.outline` ourselves. + let outlineW = parseFloat(style.outlineWidth) || 0; + let outlineStyleVal = style.outlineStyle || ''; + let outlineColorVal = style.outlineColor || ''; + if (!outlineW && style.outline) { + const wMatch = style.outline.match(/(\d+(?:\.\d+)?)\s*px/); + if (wMatch) outlineW = parseFloat(wMatch[1]) || 0; + if (!outlineStyleVal) { + outlineStyleVal = /\b(solid|dashed|dotted|double|groove|ridge|inset|outset)\b/.test(style.outline) ? 'solid' : ''; + } + if (!outlineColorVal) { + const cMatch = style.outline.match(/(rgba?\([^)]+\)|#[0-9a-fA-F]{3,8}|[a-zA-Z]+)\s*$/); + if (cMatch) outlineColorVal = cMatch[1]; + } + } + const outlineVisible = outlineW > 0 && !isTransparent(outlineColorVal) && outlineStyleVal && outlineStyleVal !== 'none'; + const bgVisible = !isTransparent(style.backgroundColor); + + const anyVisible = borderVisible.top || borderVisible.right || borderVisible.bottom || borderVisible.left || outlineVisible || bgVisible; + if (anyVisible) { + // Resolve padding to px (jsdom returns raw "1.5rem" etc., not the + // computed px value; parseFloat would strip the unit and treat + // 1.5rem as 1.5px, false-flagging legitimate insets). + const pad = { + top: resolveLengthPx(style.paddingTop, fontSize) ?? 0, + right: resolveLengthPx(style.paddingRight, fontSize) ?? 0, + bottom: resolveLengthPx(style.paddingBottom, fontSize) ?? 0, + left: resolveLengthPx(style.paddingLeft, fontSize) ?? 0, + }; + const PAD_THRESHOLD = 2; + // Children-insulate-this-side: a side is insulated if ANY direct + // child has its own padding ≥ 4px on that side. Rationale: in + // typical flow, only the first/last (or leftmost/rightmost) + // children actually sit at the parent's edges. If even one of + // them has its own padding, the visual flush is broken on that + // side. Classic example: a column-flow card frame where the + // top child (header) has padding-top:12 and the bottom child + // (footer) has padding-bottom:8 — the parent's padding:0 doesn't + // matter; nothing is actually flush. The `any-child-insulates` + // heuristic accepts some false negatives (a card with one heavily + // padded middle child won't flag) for far fewer false positives. + const CHILD_INSULATE_THRESHOLD = 4; + const childrenInsulate = { top: false, right: false, bottom: false, left: false }; + for (const child of el.children) { + let childStyle = null; + if (win && typeof win.getComputedStyle === 'function') { + try { childStyle = win.getComputedStyle(child); } catch {} + } + if (!childStyle && typeof getComputedStyle === 'function') { + try { childStyle = getComputedStyle(child); } catch {} + } + if (!childStyle) continue; + const childPad = { + top: resolveLengthPx(childStyle.paddingTop, fontSize) ?? 0, + right: resolveLengthPx(childStyle.paddingRight, fontSize) ?? 0, + bottom: resolveLengthPx(childStyle.paddingBottom, fontSize) ?? 0, + left: resolveLengthPx(childStyle.paddingLeft, fontSize) ?? 0, + }; + for (const s of ['top', 'right', 'bottom', 'left']) { + if (childPad[s] >= CHILD_INSULATE_THRESHOLD) childrenInsulate[s] = true; + } + } + + const flushSides = []; + for (const side of ['top', 'right', 'bottom', 'left']) { + const sideBounded = borderVisible[side] || outlineVisible || bgVisible; + if (sideBounded && pad[side] <= PAD_THRESHOLD && !childrenInsulate[side]) { + flushSides.push(side); + } + } + + if (flushSides.length > 0) { + // Confirm at least one direct child has substantial text content + // (> 4 chars). Without this, the flush is harmless: e.g. an + // image-only card. + let hasTextChild = false; + for (const child of el.children) { + const childText = (child.textContent || '').trim(); + if (childText.length > 4) { hasTextChild = true; break; } + } + if (hasTextChild) { + const cls = (typeof el.className === 'string' && el.className.trim()) + ? el.className.trim().split(/\s+/)[0] + : ''; + const boundaryParts = []; + const borderSidesVisible = ['top', 'right', 'bottom', 'left'].filter(s => borderVisible[s]); + if (borderSidesVisible.length === 4) boundaryParts.push('border'); + else if (borderSidesVisible.length > 0) boundaryParts.push(`border-${borderSidesVisible.join('/')}`); + if (outlineVisible) boundaryParts.push('outline'); + if (bgVisible) boundaryParts.push('bg'); + const sidesLabel = flushSides.length === 4 ? 'all sides' : flushSides.join('/'); + const ident = cls + ? `<${tag.toLowerCase()}> "${cls}"` + : `<${tag.toLowerCase()}>`; + findings.push({ + id: 'cramped-padding', + snippet: `${ident}: children flush against ${boundaryParts.join('+')} on ${sidesLabel} (no inset)`, + }); + } + } + } + } + } + // --- Body text touching viewport edge --- (browser-only: needs rect) // Catches the failure mode where the agent ships body paragraphs // with NO container providing horizontal padding — text bleeds @@ -1804,6 +2093,20 @@ function checkQuality(opts) { } } + // --- Crushed letter spacing (mirror of wide-tracking) --- + // Tracking pulled tighter than ~-0.05em crushes characters into each other. + // Optical tightening that display type legitimately wants (around -0.02em) + // stays well above this floor. + if (hasDirectText && textLen > 20 && fontSize > 0) { + if (letterSpacingPx != null && letterSpacingPx < 0) { + const trackingEm = letterSpacingPx / fontSize; + if (trackingEm <= -0.05) { + const excerpt = (el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 40); + findings.push({ id: 'extreme-negative-tracking', snippet: `letter-spacing: ${trackingEm.toFixed(2)}em — "${excerpt}"` }); + } + } + } + return findings; } @@ -1820,7 +2123,7 @@ function checkElementQualityDOM(el) { const rect = el.getBoundingClientRect(); const lineMax = (typeof window !== 'undefined' && window.__IMPECCABLE_CONFIG__?.lineLengthMax) || 80; const viewportWidth = (typeof window !== 'undefined' ? window.innerWidth : 0) || 0; - return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax, viewportWidth }); + return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect, lineMax, viewportWidth, win: typeof window !== 'undefined' ? window : null }); } // Pure page-level skipped-heading walk. Takes a Document so it works in both @@ -1862,7 +2165,7 @@ function checkElementQuality(el, style, tag, window) { const fontSize = resolveFontSizePx(el, window); const lineHeightPx = resolveLengthPx(style.lineHeight, fontSize); const letterSpacingPx = resolveLengthPx(style.letterSpacing, fontSize); - return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect: null }); + return checkQuality({ el, tag, style, hasDirectText, textLen, fontSize, lineHeightPx, letterSpacingPx, rect: null, win: window }); } function checkElementBorders(tag, style, overrides, resolvedRadius) { @@ -2303,40 +2606,213 @@ function checkPageLayout(doc, win) { } } - // Everything centered - const textEls = doc.querySelectorAll('h1, h2, h3, h4, h5, h6, p, li, div, button'); - let centeredCount = 0; - let totalText = 0; - for (const el of textEls) { - const hasDirectText = [...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim().length >= 3); - if (!hasDirectText) continue; - totalText++; - - let cur = el; - let isCentered = false; - while (cur && cur.nodeType === 1) { - const rawStyle = cur.getAttribute?.('style') || ''; - const cls = cur.getAttribute?.('class') || ''; - if (/text-align\s*:\s*center/i.test(rawStyle) || /\btext-center\b/.test(cls)) { - isCentered = true; - break; - } - if (cur.tagName === 'BODY') break; - cur = cur.parentElement; - } - if (isCentered) centeredCount++; - } - - if (totalText >= 5 && centeredCount / totalText > 0.7) { - findings.push({ - id: 'everything-centered', - snippet: `${centeredCount}/${totalText} text elements centered (${Math.round(centeredCount / totalText * 100)}%)`, - }); - } - return findings; } +// ─── Cream / beige palette (the default "tasteful" AI surface) ──────────────── +// A warm, lightly-tinted off-white page background — light, with R≥G≥B and a +// small warm tint (not white, not a strong color). The current reflex surface. +function isCreamColor(rgb) { + if (!rgb) return false; + const { r, g, b } = rgb; + if (Math.min(r, g, b) < 209) return false; // must be light + if (!(r >= g && g >= b)) return false; // warm ordering + const warmth = r - b; + return warmth >= 6 && warmth <= 48; // tinted, not white, not strong +} + +// Tailwind background utilities that render as a warm off-white surface. The +// static engine doesn't fetch Tailwind's CSS, so a `bg-amber-50` on +// resolves to nothing in computed style — catch it from the class list +// instead. Candidate tokens map to their actual Tailwind hex and are still +// filtered through isCreamColor, so neutral grays (stone) and over-saturated +// shades drop out on their own. +const TAILWIND_BG_HEX = { + 'bg-amber-50': '#fffbeb', 'bg-amber-100': '#fef3c7', + 'bg-orange-50': '#fff7ed', 'bg-orange-100': '#ffedd5', + 'bg-yellow-50': '#fefce8', + 'bg-stone-50': '#fafaf9', 'bg-stone-100': '#f5f5f4', 'bg-stone-200': '#e7e5e4', +}; + +function creamFromClassList(cls) { + if (!cls) return null; + // Arbitrary value: bg-[#f5f0e6] / bg-[rgb(245_240_230)] (underscores = spaces). + const arb = cls.match(/\bbg-\[([^\]]+)\]/); + if (arb && isCreamColor(parseAnyColor(arb[1].replace(/_/g, ' ')))) return `bg-[${arb[1]}]`; + // Named warm-light utilities. + for (const [tok, hex] of Object.entries(TAILWIND_BG_HEX)) { + if (new RegExp(`(^|\\s)${tok}($|\\s)`).test(cls) && isCreamColor(parseAnyColor(hex))) return tok; + } + return null; +} + +function checkCreamPalette(doc, win) { + const findings = []; + const body = doc.body || (doc.querySelector ? doc.querySelector('body') : null); + if (!body) return findings; + const html = doc.documentElement; + const getCS = (el) => (win ? win.getComputedStyle(el) : getComputedStyle(el)); + + // 1. Computed background — covers inline / close.', 'Prefix every preview selector with the matching [data-impeccable-variant="N"] selector.', 'Keep selectors anchored to the generated variant wrapper; do not rely on component CSS scoping for preview rules.', ], forbidden: [ 'Do not use @scope for this styleMode.', + 'Do not wrap style content in a JSX/TSX template literal ({` ... `}); that syntax is for .tsx/.jsx only.', + 'Do not put { immediately after the style opening tag; Astro parses { as expression syntax.', ], }; } @@ -629,4 +632,15 @@ if (_running?.endsWith('live-wrap.mjs') || _running?.endsWith('live-wrap.mjs/')) } // Test exports (used by tests/live-wrap.test.mjs) -export { buildSearchQueries, findElement, findClosingLine, detectCommentSyntax }; +export { + buildSearchQueries, + findElement, + findClosingLine, + detectCommentSyntax, + findAllElements, + filterByText, + findFileWithQuery, + detectStyleMode, + buildCssAuthoring, + buildCssSelectorPrefixExamples, +}; diff --git a/plugin/skills/impeccable/scripts/live.mjs b/plugin/skills/impeccable/scripts/live.mjs index cafb0eca9..8acd300ed 100644 --- a/plugin/skills/impeccable/scripts/live.mjs +++ b/plugin/skills/impeccable/scripts/live.mjs @@ -21,7 +21,7 @@ import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { loadContext } from './load-context.mjs'; +import { loadContext } from './context.mjs'; import { resolveFiles } from './live-inject.mjs'; import { readLiveServerInfo } from './impeccable-paths.mjs'; @@ -80,7 +80,7 @@ The agent should then: process.exit(1); } - // 4. Load PRODUCT.md + DESIGN.md context (auto-migrates legacy .impeccable.md) + // 4. Load PRODUCT.md + DESIGN.md context. const ctx = loadContext(process.cwd()); // 5. Compute drift-heal: compare resolved inject targets against the @@ -102,7 +102,6 @@ The agent should then: hasDesign: ctx.hasDesign, design: ctx.design, designPath: ctx.designPath, - migrated: ctx.migrated, }, null, 2)); } diff --git a/plugin/skills/impeccable/scripts/load-context.mjs b/plugin/skills/impeccable/scripts/load-context.mjs deleted file mode 100644 index dc340bf16..000000000 --- a/plugin/skills/impeccable/scripts/load-context.mjs +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Shared context loader for every impeccable command that needs to know - * "who is this for" and "what does this look like". - * - * Input: project root (process.cwd()). - * - * Output (JSON to stdout): - * { - * hasProduct: boolean, // PRODUCT.md found (or auto-migrated) - * product: string | null, // PRODUCT.md contents - * productPath: string | null, // relative path - * hasDesign: boolean, // DESIGN.md found - * design: string | null, // DESIGN.md contents - * designPath: string | null, - * migrated: boolean, // true if we auto-renamed .impeccable.md -> PRODUCT.md - * contextDir: string, // absolute path of the directory the files were found in - * } - * - * Filename matching is case-insensitive for PRODUCT.md and DESIGN.md. The - * Google DESIGN.md convention is uppercase at repo root; Kiro-style and - * lowercase variants are also matched so users don't get punished for case. - * - * Lookup directory resolution (first match wins): - * 1. process.env.IMPECCABLE_CONTEXT_DIR (absolute or relative to cwd) - * 2. cwd, if PRODUCT.md / DESIGN.md / .impeccable.md is there (back-compat) - * 3. Auto-fallback subdirectories of cwd: .agents/context/, then docs/ - * 4. cwd as a default "no context found" location - * - * Legacy `.impeccable.md` -> PRODUCT.md migration only fires at cwd root; - * fallback directories are read-only as far as auto-rename is concerned. - */ - -import fs from 'node:fs'; -import path from 'node:path'; - -const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; -const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; -const LEGACY_NAMES = ['.impeccable.md']; -const FALLBACK_DIRS = ['.agents/context', 'docs']; - -/** - * Resolve the directory that holds PRODUCT.md / DESIGN.md for - * this project. Exported so other scripts (e.g. live-server.mjs) can read the - * design files from the same location the loader uses. - */ -export function resolveContextDir(cwd = process.cwd()) { - // 1. Explicit override - const envDir = process.env.IMPECCABLE_CONTEXT_DIR; - if (envDir && envDir.trim()) { - const trimmed = envDir.trim(); - return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); - } - - // 2. cwd wins if any canonical or legacy file is there. We check legacy too - // so the auto-migration path in loadContext stays predictable. - if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES, ...LEGACY_NAMES])) { - return cwd; - } - - // 3. Auto-fallback subdirs. Match if PRODUCT.md or DESIGN.md is present; - // legacy `.impeccable.md` does not pull the lookup into a fallback dir. - for (const rel of FALLBACK_DIRS) { - const candidate = path.resolve(cwd, rel); - if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { - return candidate; - } - } - - // 4. Nothing found — keep the historical "default to cwd" behaviour so the - // caller's `hasProduct === false` branch still fires the same way. - return cwd; -} - -export function loadContext(cwd = process.cwd()) { - let migrated = false; - const contextDir = resolveContextDir(cwd); - - // 1. Look for PRODUCT.md (case-insensitive) in the resolved dir - let productPath = firstExisting(contextDir, PRODUCT_NAMES); - - // 2. Legacy: if no PRODUCT.md but .impeccable.md exists at cwd root, rename - // it in place. We only migrate at the root — fallback dirs are read-only - // so we don't surprise users by mutating files under docs/ or .agents/. - if (!productPath && contextDir === cwd) { - const legacyPath = firstExisting(cwd, LEGACY_NAMES); - if (legacyPath) { - const newPath = path.join(cwd, 'PRODUCT.md'); - try { - fs.renameSync(legacyPath, newPath); - productPath = newPath; - migrated = true; - } catch { - // Rename failed (permissions, etc.) — fall back to reading legacy in place - productPath = legacyPath; - } - } - } - - // 3. DESIGN.md (case-insensitive) - const designPath = firstExisting(contextDir, DESIGN_NAMES); - - const product = productPath ? safeRead(productPath) : null; - const design = designPath ? safeRead(designPath) : null; - - return { - hasProduct: !!product, - product, - productPath: productPath ? path.relative(cwd, productPath) : null, - hasDesign: !!design, - design, - designPath: designPath ? path.relative(cwd, designPath) : null, - migrated, - contextDir, - }; -} - -function firstExisting(dir, names) { - for (const name of names) { - const abs = path.join(dir, name); - if (fs.existsSync(abs)) return abs; - } - return null; -} - -function safeRead(p) { - try { return fs.readFileSync(p, 'utf-8'); } catch { return null; } -} - -// --------------------------------------------------------------------------- -// CLI mode — print the context as JSON -// --------------------------------------------------------------------------- - -function cli() { - const result = loadContext(process.cwd()); - console.log(JSON.stringify(result, null, 2)); -} - -const _running = process.argv[1]; -if (_running?.endsWith('load-context.mjs') || _running?.endsWith('load-context.mjs/')) { - cli(); -} diff --git a/plugin/skills/impeccable/scripts/palette.mjs b/plugin/skills/impeccable/scripts/palette.mjs new file mode 100644 index 000000000..4c3f1751a --- /dev/null +++ b/plugin/skills/impeccable/scripts/palette.mjs @@ -0,0 +1,633 @@ +#!/usr/bin/env node +/** + * Brand-seed picker. Returns one OKLCH seed color + the mood it most + * naturally evokes, and teaches the model how to compose a full palette + * around it. + * + * The seed is the brand's anchor color. The 5-role palette (bg, surface, + * ink, accent, muted) is composed by the caller at runtime using their + * judgment + the brief (PRODUCT.md / DESIGN.md / user prompt), NOT picked + * from a frozen 4-color preset. + * + * Why: 4-color frozen palettes drift toward safe defaults (warm-cream bg, + * complementary accent on near-white) regardless of brief. A single seed + + * the model's own composition lets the same seed produce a dark-mode jazz + * club or a light-mode hospitality brand depending on what the brief calls + * for. Tested empirically against curated 4-color palettes; seed approach + * wins on mood-fit in 3 of 5 cases and ties on the rest. + * + * Usage: + * node scripts/palette.mjs # pick at random + * node scripts/palette.mjs --id seed-021 # pick a specific seed + * node scripts/palette.mjs --from # hash to a seed (deterministic) + * + * Env vars: + * IMPECCABLE_PALETTE_SEED — same as --from; useful for the eval harness + * to make runs reproducible. + */ + +import crypto from 'node:crypto'; + +// Seeds are inlined (129 entries, hand-curated via a tinder review of +// ~400 candidates from ColorHunt + synthesis + Radix/brand/Pantone anchors). +// Each carries a mood + strategy the judging model produced — surfaced as +// hints, not commands; the brief still drives composition. +const SEEDS = [ + { id: "seed-200", oklch: [0.360, 0.137, 0.0], + mood: "Aesop apothecary shelf — oxblood bottle glass against linen, considered and unhurried", + strategy: "Seed is a deep desaturated red-brown that reads as brand ink itself; I push primary darker toward bottle-glass oxblood, pair with a pure white surface so the red does the work, and use a clear pale-blush accent that can carry dark text in pills." }, + { id: "seed-000", oklch: [0.400, 0.130, 0.0], + mood: "oxblood leather banquette in a 1940s steakhouse — low lamplight on dark wood and burgundy", + strategy: "Near-black bg with the faintest red undertone lets the oxblood primary glow like lamplit leather; warm cream ink and a brass accent complete the chophouse register." }, + { id: "seed-002", oklch: [0.450, 0.150, 0.0], + mood: "darkroom red light — analog photography, blood-warm safelight glow on chemical trays", + strategy: "Near-black surface with a deep oxblood primary lets the seed function like a safelight in a darkroom — the bg disappears so the red becomes the only emotional signal." }, + { id: "seed-003", oklch: [0.500, 0.194, 0.0], + mood: "darkroom safelight — the deep oxblood glow of analog photography, chemical and contemplative", + strategy: "Anchored the seed as primary against pure near-black so the red reads like a single illuminated bulb in a developing room, with cool desaturated ink to evoke silver gelatin print tones." }, + { id: "seed-004", oklch: [0.546, 0.204, 3.4], + mood: "midnight boudoir — velvet rose under low lamplight, perfumed and intimate", + strategy: "Near-black surface lets the rose seed glow like silk in shadow; a warm champagne accent provides the candle-flame counterpoint without breaking the hush." }, + { id: "seed-005", oklch: [0.550, 0.180, 0.0], + mood: "smoldering vermillion at dusk — the last red ember in a blacksmith's forge, iron-rich and quietly violent", + strategy: "Near-black gallery surround lets the seed read as glowing forged metal; ink stays warm-off-white, accent shifts to a hotter ember orange so the primary feels like cooling steel against a fresh strike." }, + { id: "seed-201", oklch: [0.647, 0.262, 0.3], + mood: "Figma plugin marketplace red — confident product-brand crimson, the kind a modern dev tool uses for a 'live' indicator or a primary CTA on a pristine docs page", + strategy: "Pure white surface lets a high-chroma crimson primary do all the brand work, paired with a hue-shifted warm coral accent for hierarchy without competing saturation" }, + { id: "seed-006", oklch: [0.650, 0.160, 0.0], + mood: "1960s Italian cinema — Technicolor lipstick red against a darkened theater", + strategy: "Pure near-black surface lets a saturated cinematic red and its warm peach accent perform like film light projected in a dark room — the brand colors carry the drama, the bg disappears." }, + { id: "seed-008", oklch: [0.520, 0.200, 10.4], + mood: "Negroni hour at a Milanese bar — bittersweet crimson, vermouth and amaro under low tungsten", + strategy: "Seed is a saturated red-crimson with cinematic weight, so I sit it on near-black to let the primary glow like backlit liquor, with a warmer amber accent acting as the citrus twist against the bitter red." }, + { id: "seed-010", oklch: [0.563, 0.223, 11.0], + mood: "Negroni hour on a Milan rooftop — bittersweet crimson, aperitivo light, polished restraint", + strategy: "Seed is a vivid carmine-red with strong chroma, so the surface gets out of the way (pure white) and lets the primary do the aperitivo work, with a cooled garnet accent for tension." }, + { id: "seed-202", oklch: [0.643, 0.247, 7.0], + mood: "Glossier brand pink — modern beauty editorial, confident and current", + strategy: "Pure white bg lets a saturated rose-red primary do all the brand work, paired with a deeper crimson accent for hierarchy — the Stripe/Glossier move where the color carries the mood." }, + { id: "seed-013", oklch: [0.400, 0.130, 20.0], + mood: "Tuscan cellar at dusk — aged terracotta, oxidized iron, the deep red of decanted Sangiovese", + strategy: "Black surface lets the oxblood seed and copper accent glow like firelight on cellar stone; brand colors carry all the warmth while the room recedes." }, + { id: "seed-014", oklch: [0.450, 0.150, 20.0], + mood: "smoldering tannery — oxblood leather, cured under low workshop light", + strategy: "Anchor the deep oxblood seed as primary against a near-black architectural ground, then lift with a single warm ember accent so the leather reads burnished rather than bloody." }, + { id: "seed-016", oklch: [0.550, 0.180, 20.0], + mood: "Negroni hour on a Roman terrace — bitter campari red, vermouth, late golden light spilling on white linen", + strategy: "Pure white surface lets the campari-red primary do all the emotional work, paired with a deeper oxblood accent for bittersweet depth — Italian aperitivo restraint, not warmth-washed." }, + { id: "seed-205", oklch: [0.634, 0.254, 17.6], + mood: "Aesop apothecary bottle — considered red-coral on a clinical white surface, the kind of brand restraint where one saturated object does all the work", + strategy: "Default A pure white surface lets a single coral-red primary carry the entire brand voice; accent shifts to a deeper oxblood for hierarchy without competing chroma." }, + { id: "seed-011", oklch: [0.639, 0.207, 13.5], + mood: "Aperitivo hour in Milan — Campari glow on a white marble bar, crisp and effervescent", + strategy: "Pure white gallery backdrop lets the Campari-red primary ring like a single bitter note; ink is near-black with a whisper of warmth, accent shifts to a deeper oxblood for hierarchy without competing hues." }, + { id: "seed-015", oklch: [0.527, 0.202, 22.7], + mood: "Negroni hour on a Milanese terrace — bittersweet vermillion, aperitivo glassware catching low sun", + strategy: "Seed becomes a saturated aperitivo-red primary against pure white so the color carries the bittersweet warmth alone, paired with a deep oxblood accent for typographic gravitas." }, + { id: "seed-023", oklch: [0.427, 0.175, 29.2], + mood: "blacksmith's forge at dusk — iron heated to ember red, the deep glow of oxidized metal and quenching oil", + strategy: "Pure black bg lets the seed's ember-red glow radiate like hot iron in a dark forge; accent shifts to a copper-amber to suggest scaling metal and sparks, while ink stays near-white for tool-precise legibility." }, + { id: "seed-206", oklch: [0.614, 0.234, 28.2], + mood: "Aesop apothecary bottle — considered red-orange on lab-white, calm utility with a single confident pigment", + strategy: "Pure white surface lets a saturated vermilion primary do all the brand work, paired with a deep oxblood accent for hierarchy without introducing a second hue family" }, + { id: "seed-029", oklch: [0.665, 0.222, 25.7], + mood: "Negroni hour at a Milanese bar — bittersweet orange-red liqueur catching late afternoon light on polished marble", + strategy: "Pure white surface lets the seed's vermilion read like Campari in a glass; a deeper oxblood accent provides the bitter depth, with neutral graphite ink keeping the editorial restraint of Italian design." }, + { id: "seed-022", oklch: [0.418, 0.155, 27.2], + mood: "Pompeiian red fresco — oxidized cinnabar on a museum wall, archaeological gravity", + strategy: "Pure black gallery surface lets the seed's iron-oxide red read as a lit artifact; accent shifts to an aged terracotta amber, so primary and accent form a fired-clay duet against neutral void." }, + { id: "seed-024", oklch: [0.464, 0.169, 26.9], + mood: "Mid-century darkroom under the safelight — developer trays, oxblood leather, the quiet patience of a print emerging", + strategy: "Seed becomes a deep oxblood primary; surface stays pure black so the red glows like a safelight, with a warmer ember accent for hierarchy" }, + { id: "seed-026", oklch: [0.489, 0.190, 28.3], + mood: "smoldering ember in a blacksmith's forge — iron-hot rust, soot, and controlled fire", + strategy: "Near-black soot background lets the seed's red-orange glow like heated metal; ink is bone-white, accent is a cooler tempered-steel orange that creates internal heat gradient with the primary." }, + { id: "seed-027", oklch: [0.568, 0.208, 27.1], + mood: "Sicilian blood orange at golden hour — citrus rind, terracotta, sun on stucco", + strategy: "Seed reads as vivid blood-orange — picked pure white surface so the citrus-red primary and a deep oxblood accent do all the emotional work, like a Loro Piana editorial spread." }, + { id: "seed-028", oklch: [0.591, 0.172, 24.0], + mood: "Sienna-fired ceramic studio at dusk — terracotta cooling on a wheel, hands still dusted with slip", + strategy: "Pure black stage lets the fired-clay primary glow like a kiln ember, with a deeper oxblood accent providing tonal weight rather than hue contrast — a monochrome warm-axis play." }, + { id: "seed-033", oklch: [0.544, 0.169, 31.3], + mood: "1960s Italian terracotta workshop — fired clay, espresso, late-afternoon Mediterranean dust", + strategy: "Pure black ground lets the seed's burnt-sienna primary glow like a lit kiln, with a deeper oxblood accent for restrained warmth tension — the brand carries the heat, the surface stays out." }, + { id: "seed-207", oklch: [0.564, 0.231, 29.1], + mood: "Aesop apothecary bottle — considered red oxide, the calm authority of a well-made object on a white shelf", + strategy: "Seed becomes the singular brand voice against pure white, with a deeper oxblood accent for hierarchy — the surface disappears so the red does all the speaking." }, + { id: "seed-035", oklch: [0.663, 0.153, 32.1], + mood: "Aesop apothecary bottle — clay-fired warmth, considered retail", + strategy: "Pure white surface lets the terracotta primary do the brand work, paired with a deep umber ink and a cooler clay accent for editorial tension." }, + { id: "seed-037", oklch: [0.590, 0.188, 35.8], + mood: "Aesop apothecary bottle — considered terracotta, herbalist restraint, the warmth comes from the glass not the room", + strategy: "Seed becomes a muted terracotta primary against pure white so the brand's warmth carries entirely through the color itself; accent shifts to a deeper umber for quiet hierarchy." }, + { id: "seed-038", oklch: [0.652, 0.229, 34.8], + mood: "blown-glass furnace at dusk — molten orange iron pulled from the kiln, a craftsman's signature heat", + strategy: "Pure black stage so the seed reads as live ember; primary holds the seed's heat, accent shifts to a brass-amber a hue-step away for a 1.7+ contrast pairing without leaving the fire." }, + { id: "seed-039", oklch: [0.653, 0.185, 33.5], + mood: "Aesop apothecary bottle — considered terracotta, quiet retail craft", + strategy: "Seed becomes a grounded clay primary against pure white, paired with a deeper umber accent so the warmth lives entirely in the brand marks, not the surface." }, + { id: "seed-167", oklch: [0.495, 0.134, 36.0], + mood: "Aesop apothecary shelf — burnished terracotta on clinical white, considered craft pharmacy", + strategy: "Treat the seed as a brand-carrying burnt-sienna against a pure paper-white surface so the warmth lives entirely in the primary, with a deep umber accent pulled along the same warm axis for typographic gravity." }, + { id: "seed-147", oklch: [0.500, 0.151, 40.0], + mood: "Aesop apothecary shelf — considered terracotta, pharmacy restraint, the brand color does the work against clinical white", + strategy: "Anchor the seed's burnt-sienna primary against a pure white surface so the rust speaks alone, with a deep umber ink and a cooler clay accent to give the palette product-brand discipline rather than environmental warmth." }, + { id: "seed-040", oklch: [0.660, 0.201, 40.0], + mood: "Aesop apothecary bottle — amber glass on a clean dispensary shelf, considered and clinical-warm", + strategy: "Seed becomes a burnt-amber primary against pure white so the bottle-glass color does the emotional work; accent shifts to a deep olive-bronze for the apothecary-label pairing." }, + { id: "seed-041", oklch: [0.673, 0.217, 38.6], + mood: "Aesop apothecary shelf — considered orange glass, clinical retail restraint", + strategy: "Pure white surface lets the burnt-orange primary do all the brand work, with a deep ink-brown for editorial gravity and a muted clay accent that reads as a sibling, not a contrast." }, + { id: "seed-042", oklch: [0.688, 0.133, 35.8], + mood: "Aesop apothecary shelf — terracotta glass, considered retail", + strategy: "Seed becomes a warm clay primary against pure white so the bottle-on-marble retail feel comes from the brand color alone; a deeper umber accent gives the label-print contrast." }, + { id: "seed-043", oklch: [0.781, 0.119, 38.1], + mood: "Aesop apothecary catalogue — considered terracotta, dermatological restraint, the warm color doing all the work against clinical white", + strategy: "Pure white surface lets the seed's warm clay tone read as the entire brand voice, paired with a deeper umber accent for hierarchy without competing with the primary's warmth." }, + { id: "seed-168", oklch: [0.400, 0.103, 50.0], + mood: "Aesop apothecary bottle — amber glass on a clinical white shelf, considered and pharmaceutical", + strategy: "Pure white surface lets the deep amber primary act like tinted glass against a clean shelf; accent is a muted clay that complements without competing, keeping the brand quiet and product-led." }, + { id: "seed-044", oklch: [0.568, 0.149, 45.9], + mood: "1970s desert highway at golden hour — sun-faded terracotta, denim dust, the warmth of a Polaroid pulled from a glovebox", + strategy: "Seed becomes a burnt-sienna primary against pure white so the terracotta does all the emotional work; a deep indigo accent acts as the denim shadow opposing the sun, creating the era's signature warm/cool tension without tinting the page." }, + { id: "seed-045", oklch: [0.607, 0.163, 47.7], + mood: "Aesop apothecary shelf — considered amber glass, clinical restraint, craft pharmacy", + strategy: "Pure white bg lets the burnt-amber primary do the apothecary work alone, paired with a deeper umber accent and graphite ink for editorial calm." }, + { id: "seed-046", oklch: [0.653, 0.175, 45.0], + mood: "Aesop apothecary shelf — considered amber glass, quiet luxury, restrained craft", + strategy: "Pure black backdrop lets the warm amber primary glow like backlit apothecary glass, with a deeper rust accent providing tonal depth in the same hue family — monochromatic warm against neutral void." }, + { id: "seed-047", oklch: [0.695, 0.205, 43.2], + mood: "Aesop apothecary label — sun-warmed amber glass on a clinical countertop, restrained botanical pharmacy", + strategy: "Pure white surface lets the burnt-amber primary and a deeper sienna accent do all the brand work, like an apothecary bottle photographed under daylight." }, + { id: "seed-051", oklch: [0.704, 0.189, 49.0], + mood: "blacksmith's forge at dusk — glowing iron, hammered copper, ember light against cooling steel", + strategy: "Pure near-black surface lets the seed's molten orange burn like heated metal; accent shifts to a deeper amber-red to suggest the cooling end of the same iron, while ink stays a clean off-white so type reads like chalk on slate." }, + { id: "seed-171", oklch: [0.550, 0.124, 60.0], + mood: "Klim Type Foundry specimen page — considered ochre on paper, design-school-honest", + strategy: "Seed becomes a muted ochre primary on pure white; accent is a deep ink-navy pulled across the wheel for editorial contrast without warmth-pooling in the bg" }, + { id: "seed-148", oklch: [0.650, 0.146, 60.0], + mood: "Klim-style editorial gold — late-afternoon paper light on a serif specimen sheet, considered and dry", + strategy: "Hold the seed's amber as primary on a pure white page so the gold reads as ink rather than atmosphere, and pair with a deep aubergine accent for typographic contrast." }, + { id: "seed-052", oklch: [0.700, 0.130, 60.0], + mood: "late-afternoon terracotta studio — sun-warmed clay, hands-on craft, the hour before dusk", + strategy: "Seed is a saturated amber-ochre with strong environmental association (ceramics, adobe, sunlit plaster), so I lean into Exception (a) with a faintly warm bone surface that reads as lime-washed wall, then deepen the seed slightly for primary and pair it with a fired-clay rust accent for hand-thrown warmth." }, + { id: "seed-053", oklch: [0.773, 0.157, 56.6], + mood: "late-summer apricot orchard at golden hour — sun-warmed fruit, considered Californian craft", + strategy: "Seed is a juicy mid-warm orange at daylight luminance — leaning optimistic/editorial, so pure white surface lets the apricot primary glow without muddying it; a deep wine accent provides the bite." }, + { id: "seed-149", oklch: [0.600, 0.124, 70.0], + mood: "1970s desert highway — late-afternoon amber light on chrome and asphalt", + strategy: "Anchor the amber seed as primary against pure black so the warm hue reads as headlight glow against night; a cooler dusk-mauve accent provides the complementary tension of horizon vs. sun." }, + { id: "seed-054", oklch: [0.740, 0.162, 68.1], + mood: "late-afternoon honey on terracotta — Mediterranean stucco at golden hour, sun-baked amber", + strategy: "Seed is a saturated honey-amber at high lightness; pairing it with pure black lets the warmth read as luminous gold against gravity, like lamplight in a dark room." }, + { id: "seed-055", oklch: [0.774, 0.174, 65.1], + mood: "late-summer honey hour — amber light slanting through a west-facing window, optimistic and golden", + strategy: "Anchor a saturated honey-amber primary on pure white so the warmth radiates from the brand itself, then pair with a deep teak accent for grounded contrast rather than tinting the canvas." }, + { id: "seed-056", oklch: [0.691, 0.146, 74.6], + mood: "Klim-style modern publishing house — late-afternoon paper warmth, considered editorial gold", + strategy: "Pure white surface so the amber seed becomes the brand voice; ink stays near-black neutral and accent shifts to a deep ink-blue to give the gold something structural to lean on." }, + { id: "seed-150", oklch: [0.750, 0.148, 80.0], + mood: "Klim Type Foundry specimen page — late-summer editorial gold, considered and grown-up", + strategy: "Pure white surface lets a single restrained ochre primary do all the brand work, paired with a deep ink-blue accent for typographic contrast in the Klim/Commercial Type tradition." }, + { id: "seed-058", oklch: [0.764, 0.120, 77.1], + mood: "Klim Type Foundry specimen page — late-afternoon ochre, considered editorial typography", + strategy: "Pure white surface lets the ochre primary do the brand work, paired with a deep ink-blue accent for editorial contrast — the type-foundry move where one warm hue carries the whole feeling against neutral paper." }, + { id: "seed-059", oklch: [0.784, 0.144, 79.8], + mood: "late afternoon in a Tuscan limonaia — sun-cured amber on whitewashed plaster", + strategy: "Pure white surface lets the saffron-amber primary and a deep olive accent carry the Mediterranean warmth, with split-complementary tension between gold and a quiet evergreen." }, + { id: "seed-061", oklch: [0.817, 0.161, 75.1], + mood: "late-afternoon honey on Tuscan limestone — golden hour, slow and luminous", + strategy: "Pure white surface lets the amber primary glow like sunlight on a wall, paired with a deep terracotta accent for warm tonal contrast within the same hue family." }, + { id: "seed-063", oklch: [0.842, 0.165, 91.3], + mood: "late-afternoon Tuscan sun on limestone — golden hour, considered, optimistic", + strategy: "Pure white surface lets the amber-gold primary radiate as the mood-carrier, with a deep aubergine accent providing the long shadow that golden light needs to feel three-dimensional." }, + { id: "seed-174", oklch: [0.350, 0.075, 110.0], + mood: "olive grove at late afternoon — sun-cured leaves, dust, and quiet Mediterranean weight", + strategy: "Pure white surface lets a deep, sun-cured olive primary do the emotional work, with a burnt-terracotta accent providing the warm-earth counterpoint olive groves are known for." }, + { id: "seed-117", oklch: [0.650, 0.100, 110.0], + mood: "Klim-style editorial sage — late-summer foundry catalogue, considered olive-yellow on paper", + strategy: "Seed sits at olive-chartreuse; treating it as a quiet typographic primary on pure paper, with a deeper bronze-olive accent for hierarchy — the color does the work, the page disappears." }, + { id: "seed-118", oklch: [0.750, 0.090, 110.0], + mood: "Klim Type Foundry specimen page — late-summer olive light on a working specimen, the honesty of a type designer showing their work", + strategy: "Pure white bg lets a desaturated olive-yellow primary do the editorial work, with a deeper olive-bronze accent providing typographic emphasis the way a specimen uses one heavy weight against the body roman." }, + { id: "seed-065", oklch: [0.797, 0.166, 113.1], + mood: "late-summer olive grove at noon — sun-bleached leaves, dry stone, Mediterranean glare", + strategy: "Hold the seed as a luminous chartreuse-olive primary against pure white so the color reads as sunlit foliage, pairing it with a deep umber accent for the dry-stone contrast." }, + { id: "seed-176", oklch: [0.300, 0.071, 120.0], + mood: "moss-darkened apothecary jar — herbal, shadowed, mid-19th-century botanical study", + strategy: "Seed is a deep desaturated olive-green that reads as preserved botanical pigment; I anchor it on pure white so the dim moss-green primary feels like ink on a herbarium page, with a warm ochre accent supplying the aged-paper counterpoint." }, + { id: "seed-155", oklch: [0.550, 0.142, 130.0], + mood: "moss-bed forest floor at noon — chlorophyll, lichen, sunlit fern", + strategy: "Seed is a confident mid-olive green with strong chroma; mood is daylight botanical, so I let the brand greens do the work on a pure paper-white bg and pair with a warm umber accent for fern-against-bark contrast." }, + { id: "seed-119", oklch: [0.600, 0.154, 130.0], + mood: "moss garden at Saihō-ji — damp stone, filtered green light through old cedar", + strategy: "Pure near-black bg lets the seed's mossy green glow like wet lichen under low light; accent shifts to a pale ochre-gold like sun catching through canopy." }, + { id: "seed-179", oklch: [0.300, 0.096, 140.0], + mood: "moss on wet stone — forest floor at dusk, deep botanical hush", + strategy: "Kept the seed's deep moss green as primary against a near-black surface so the green reads as living shadow, with a pale lichen accent providing the single point of light." }, + { id: "seed-180", oklch: [0.350, 0.110, 140.0], + mood: "moss-darkened apothecary — herbal tinctures in amber glass, pressed botanicals, the deep green of a conservatory at dusk", + strategy: "Near-black bg with a whisper of green undertone lets the seed's deep moss read as luminous foliage; a warm parchment accent provides the apothecary-label counterpoint without breaking the herbal register." }, + { id: "seed-120", oklch: [0.650, 0.100, 140.0], + mood: "moss on weathered stone — quiet botanical garden conservatory at midday", + strategy: "Pure white bg lets the muted sage-green primary read as a considered botanical mark, with a deeper terracotta accent providing earthen counterpoint without breaking the gallery-like restraint." }, + { id: "seed-121", oklch: [0.750, 0.090, 140.0], + mood: "moss garden at Saihō-ji — diffuse green light filtered through wet stone and lichen", + strategy: "Pure near-black bg lets the muted sage-green primary glow like lichen under low light; a warm pale-bone accent acts as the single ray of sun cutting through canopy." }, + { id: "seed-182", oklch: [0.400, 0.106, 150.0], + mood: "moss garden at Saiho-ji — deep cultivated green under wet stone shadow, contemplative and damp", + strategy: "Near-black bg with the faintest cool-green undertone evokes shaded stone; primary holds the seed's moss tone while accent shifts to a lichen-yellow for organic counterpoint without breaking the hush." }, + { id: "seed-157", oklch: [0.550, 0.145, 150.0], + mood: "moss garden at Saiho-ji — damp stone, filtered green light through cedar canopy", + strategy: "Near-black bg with a faint green undertone evokes deep forest shadow; primary holds the seed's verdant register while accent shifts to a pale lichen-cream to mimic light catching moss." }, + { id: "seed-122", oklch: [0.600, 0.158, 150.0], + mood: "forest floor at first light — moss, lichen, and clean morning air", + strategy: "Seed reads as a living, daylight green; surface stays pure white so the green carries the freshness, with a cool teal accent pulling it toward dew rather than earth." }, + { id: "seed-195", oklch: [0.650, 0.150, 145.0], + mood: "Considered horticulture brand — botanical research lab, the green of a healthy stem photographed in clean daylight", + strategy: "Pure white surface lets the seed's vegetal green carry the entire brand voice, paired with a deep forest ink and a warm clay accent for editorial contrast." }, + { id: "seed-183", oklch: [0.350, 0.077, 160.0], + mood: "moss-stained apothecary — deep forest glass, herbal tinctures shelved in low candlelight", + strategy: "Anchored the seed as primary and built a near-black dark surface with whisper-tinted green to evoke aged apothecary glass, letting the green glow rather than shout." }, + { id: "seed-184", oklch: [0.400, 0.087, 160.0], + mood: "deep forest apothecary — moss, bottle glass, and herbal tincture under afternoon light", + strategy: "Seed becomes a botanical-bottle-green primary on pure white, paired with a warm clove-amber accent to evoke herbal pharmacy contrast without tinting the surface." }, + { id: "seed-158", oklch: [0.550, 0.119, 160.0], + mood: "moss on wet stone — forest floor after rain, mineral and quiet", + strategy: "Pure white surface lets the deep mossy green carry the entire mood; accent shifts to a damp slate-teal to sit beside primary like lichen on stone without competing." }, + { id: "seed-159", oklch: [0.600, 0.130, 160.0], + mood: "moss-covered forest apothecary — herbal tinctures in amber glass, eucalyptus shadow", + strategy: "Anchored the green seed in a near-black backdrop so it reads like botanical glassware lit from within, with a warm amber accent pulled across the wheel to evoke tincture bottles against dark wood." }, + { id: "seed-185", oklch: [0.450, 0.086, 170.0], + mood: "weathered copper patina on a Pacific Northwest greenhouse — oxidized teal, glass light, botanical hush", + strategy: "Seed sits as a deep oxidized-teal primary against pure white so the patina reads as pigment, not atmosphere; a rust-copper accent completes the verdigris/oxidation story across the warm-cool axis." }, + { id: "seed-124", oklch: [0.750, 0.080, 170.0], + mood: "sea-glass on a foggy Pacific shoreline — weathered, mineral, quietly oxidized", + strategy: "Seed is a soft desaturated teal-green; pairing it on pure white lets the mineral primary read as patinated copper-glass, with a deeper kelp-toned primary and a rusted coral accent to spark the muted teal against its complement." }, + { id: "seed-160", oklch: [0.550, 0.095, 180.0], + mood: "weathered copper patina on a museum bronze — oxidized teal, conservatorial quiet", + strategy: "Pure near-black gallery surround lets the patina-teal primary glow like a lit artifact, with a warm verdigris-adjacent accent providing the oxidation contrast against the cool seed." }, + { id: "seed-161", oklch: [0.720, 0.100, 188.0], + mood: "climate-tech dashboard — calm verdigris on plain paper, the quiet confidence of an instrument that just works", + strategy: "Seed teal carries the entire mood as a single considered brand color on pure white, with a desaturated copper accent providing warm signal against the cool primary without competing for attention." }, + { id: "seed-186", oklch: [0.450, 0.074, 200.0], + mood: "deep hydrothermal vent — mineral teal under pressure, the cold blue-green of oxidized copper in submerged light", + strategy: "Near-black surface lets the mineral teal glow as if lit from within; accent shifts toward verdigris-copper to suggest patina on submerged metal, while ink stays cool-neutral to keep the register austere rather than aquatic-cute." }, + { id: "seed-125", oklch: [0.650, 0.100, 200.0], + mood: "climate-tech dashboard — calm operational teal, the color of clean water data and atmospheric sensors", + strategy: "Pure white surface lets a single muted-teal primary do all the brand work, with a deeper marine accent providing hierarchy without competing chroma." }, + { id: "seed-126", oklch: [0.750, 0.080, 200.0], + mood: "climate-tech product brand — quiet competence, dashboards for hard infrastructure problems", + strategy: "Hold the seed's muted teal as primary, pair with a sharper cyan-leaning accent for interactive lift, and let a pure white surface do the disappearing act so the brand reads as a tool, not an atmosphere." }, + { id: "seed-162", oklch: [0.550, 0.091, 210.0], + mood: "weathered nautical instrument — patinated brass on oxidized steel, the cool blue-grey of a ship's chronometer at dawn", + strategy: "Pure white surface lets the muted teal-steel primary read as a precise instrument mark, with a warm brass accent providing the single point of patina against clinical white." }, + { id: "seed-163", oklch: [0.450, 0.086, 230.0], + mood: "deep harbor at dusk — weathered nautical instruments, brass dials on oxidized steel", + strategy: "Near-black background with subtle cool tint evokes the marine dusk; primary holds the seed's teal-blue while a warm brass accent creates the instrument-on-steel tension." }, + { id: "seed-164", oklch: [0.550, 0.105, 230.0], + mood: "deep harbor at dawn — cold steel water, fog-muted light, the quiet before the boats leave", + strategy: "Pure near-black bg lets the seed's cold marine blue read as a luminous beacon, while a pale frost-cyan accent evokes diffused dawn light cutting through fog." }, + { id: "seed-127", oklch: [0.650, 0.100, 230.0], + mood: "climate-tech dashboard — atmospheric sensor blue, calm operational clarity", + strategy: "Anchor the seed as a confident mid-blue primary on pure white so the brand color carries all the atmospheric feeling, with a deep navy accent for hierarchy and a soft slate muted for body text." }, + { id: "seed-128", oklch: [0.750, 0.080, 230.0], + mood: "climate-tech dashboard — calm atmospheric data, considered sky-blue", + strategy: "Pure white surface lets the muted sky-blue primary carry the meteorological calm, with a deep-navy accent providing readable weight against the soft primary." }, + { id: "seed-187", oklch: [0.350, 0.078, 240.0], + mood: "deep harbor at blue hour — wet stone, cold steel, the quiet before night fully lands", + strategy: "Near-black architectural bg with a hint of marine chroma lets the seed read as ambient atmosphere rather than UI chrome; a cooler steel accent sits opposite the warmer-shifted primary for navigational clarity." }, + { id: "seed-077", oklch: [0.578, 0.130, 241.7], + mood: "pre-dawn signal tower — cold blue solitude, instruments glowing against the dark", + strategy: "Pure near-black bg lets the seed's cold tower-light blue glow as the sole emotional source, with a frost-cyan accent acting as a secondary indicator light." }, + { id: "seed-188", oklch: [0.400, 0.110, 250.0], + mood: "Linear's considered indigo — the calm authority of a well-built developer tool, blueprint ink on a clean page", + strategy: "Held the seed as a deep indigo primary against pure white so the brand color carries all the gravity; accent shifts to a cooler, brighter cyan-blue to create a crisp hierarchy pair without warming the surface." }, + { id: "seed-165", oklch: [0.450, 0.123, 250.0], + mood: "blueprint room at dusk — drafting table, graphite, civic-engineering blue", + strategy: "Seed is a mid-deep architectural blue with real chroma and no environmental cue, so I stay out of the way with a pure white surface and let the primary do all the talking, pairing it with a burnt-ochre accent for drafting-pencil contrast." }, + { id: "seed-079", oklch: [0.478, 0.136, 251.8], + mood: "twilight cartography — the blue of deep dusk over open water, precise and navigational", + strategy: "Pure white surface lets the seed's oceanic blue act as a single navigational anchor, with a warm amber accent struck across it like a lighthouse beam at dusk." }, + { id: "seed-080", oklch: [0.541, 0.122, 248.2], + mood: "Linear-style considered tool blue — the calm, exact register of a modern engineering app where every pixel is intentional", + strategy: "Pure white surface lets the considered indigo-blue primary carry the entire brand; a deeper navy accent provides hierarchy without warmth, keeping the palette in a single cool family for that focused-software feel" }, + { id: "seed-166", oklch: [0.550, 0.149, 250.0], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and quietly intense", + strategy: "Near-black bg with the faintest cool tint reads like a darkened cockpit; the seed becomes a luminous instrument-blue primary, paired with a warm amber accent that mimics avionics readouts for unmistakable signal contrast." }, + { id: "seed-081", oklch: [0.650, 0.160, 250.0], + mood: "deep-sea research vessel at dawn — instrument glow against cold steel light", + strategy: "Pure near-white bg keeps the palette technical and instrument-like; the seed blue holds as primary while a desaturated steel-cyan accent reads like signal readouts on glass." }, + { id: "seed-082", oklch: [0.742, 0.140, 247.4], + mood: "high-altitude flight deck at dawn — cold cabin instruments glowing against a sky still holding night", + strategy: "Near-black cockpit ground with a faint blue cast lets the seed read as an illuminated instrument; primary holds the seed, accent shifts to cyan for signal/indicator contrast." }, + { id: "seed-210", oklch: [0.360, 0.140, 260.0], + mood: "Linear-style considered tool indigo — late-night focused work, the deep blue of a code editor at 2am where everything else falls away", + strategy: "Pure black bg lets the indigo primary carry all the cognitive-focus weight, with a slightly brighter periwinkle accent for interactive lift — the surface disappears so the tool feels weightless." }, + { id: "seed-189", oklch: [0.400, 0.130, 260.0], + mood: "pre-dawn observatory — cold instrument blue, star-chart precision", + strategy: "Seed becomes the primary on pure black so the deep instrument-blue glows like a calibration light, with a faint cyan accent reading as starlight against the void." }, + { id: "seed-211", oklch: [0.420, 0.161, 260.0], + mood: "Linear's considered indigo — the tool-for-thought blue of focused product work, calm authority without coldness", + strategy: "Hold the seed as a deep indigo primary against pure white, then pair with a slightly warmer, lighter periwinkle accent to create gentle hue separation without breaking the disciplined tool-brand register." }, + { id: "seed-129", oklch: [0.450, 0.150, 260.0], + mood: "pre-dawn observatory — deep cobalt sky just before astronomical twilight, instruments cool to the touch", + strategy: "Near-black surface lets the cobalt seed read as luminous starlight; a single warm amber accent acts as the calibration lamp against the cold blue field." }, + { id: "seed-084", oklch: [0.476, 0.207, 261.2], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and awake", + strategy: "Default B black bg lets the cobalt primary read as a luminous instrument signal, with a cyan accent striking the analogous 'cockpit display' relationship." }, + { id: "seed-085", oklch: [0.681, 0.132, 258.4], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky", + strategy: "Anchored the seed as a luminous primary against a near-black architectural ground, with a warm amber accent acting as the single instrument light cutting through cold blue." }, + { id: "seed-086", oklch: [0.767, 0.106, 255.9], + mood: "Scandinavian winter morning — quiet light through frost, pale sky over snow", + strategy: "Anchored a pure white editorial stage so the seed's cool sky-blue reads as crisp polar light, with a deeper navy primary providing the only saturated weight — like a single dark pine against snow." }, + { id: "seed-083", oklch: [0.340, 0.159, 262.4], + mood: "deep cobalt twilight — the moment after sunset when the sky goes electric blue and city windows start to glow", + strategy: "Pure black stage lets the cobalt seed act as a luminous neon-window glow, with a warm amber accent across the wheel for the lit-window contrast." }, + { id: "seed-212", oklch: [0.360, 0.219, 270.0], + mood: "Linear-grade tooling indigo — considered software for people who care about craft", + strategy: "Anchored the deep indigo seed as primary on a pure white surface so the brand color carries all the weight, with a slightly cooler violet-blue accent for hierarchy without competing chroma." }, + { id: "seed-130", oklch: [0.400, 0.150, 270.0], + mood: "Linear-grade indigo — considered productivity tool, ink on paper, no theatrics", + strategy: "Pure white surface lets a deep cool indigo carry all the brand weight, paired with a slightly warmer violet-blue accent for hierarchy without acid." }, + { id: "seed-213", oklch: [0.411, 0.241, 267.9], + mood: "Linear-style indigo — considered tool surface, the kind of blue-violet that sits behind a developer's keyboard at 11pm without shouting", + strategy: "Pure black canvas lets a saturated indigo primary do all the brand work, with a cooler cyan-violet accent providing UI signal without competing." }, + { id: "seed-131", oklch: [0.450, 0.180, 270.0], + mood: "monastic indigo dusk — vespers light through stained glass, contemplative and severe", + strategy: "Seed becomes a deep indigo primary against pure near-black so the violet reads as luminous stained-glass against architectural shadow, with a cooler iris accent for tonal lift." }, + { id: "seed-088", oklch: [0.476, 0.158, 268.5], + mood: "pre-dawn astronomer's notebook — deep indigo sky just before the stars fade, ink and graphite", + strategy: "Near-black bg with the faintest cool tint to evoke night sky without theatrics; primary holds the seed's indigo, accent shifts to a paler periwinkle for stellar contrast, keeping the palette monochromatic-cool and observational." }, + { id: "seed-196", oklch: [0.530, 0.130, 268.0], + mood: "Linear-style considered tool indigo — the deep-focus blue-violet of a thoughtfully built productivity surface, the color of a well-typeset keyboard shortcut", + strategy: "Pure white bg lets the indigo seed do all the brand work as primary, with a slightly darker, more saturated violet-shifted accent for hierarchy and interactive states — the surface disappears so the brand color reads as the entire identity." }, + { id: "seed-132", oklch: [0.700, 0.120, 270.0], + mood: "Linear-style considered tool indigo — the quiet violet of a focused product workspace, late-afternoon thinking", + strategy: "Pure white surface lets a muted indigo-violet primary and a slightly cooler accent do all the brand work, keeping the register calm and software-like rather than theatrical." }, + { id: "seed-090", oklch: [0.445, 0.206, 279.1], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, not a nightclub", + strategy: "Anchor the seed as a confident product primary on pure white, with a cooler indigo-shift accent that reads as a sibling tool color, so the brand violet does all the emotional work." }, + { id: "seed-133", oklch: [0.500, 0.160, 280.0], + mood: "Linear-adjacent indigo — considered productivity tool, the violet of a thinking workspace", + strategy: "Seed becomes a measured indigo primary on pure white; accent shifts to a cooler blue-violet to create hierarchy without nightclub saturation, letting the brand color do all the emotional work." }, + { id: "seed-094", oklch: [0.533, 0.125, 294.3], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, calm authority for a creative workspace", + strategy: "Pure white canvas lets the indigo-violet primary carry the entire brand voice; accent shifts hue slightly toward blue for a cool, tool-like duotone rather than warm decorative pairing." }, + { id: "seed-137", oklch: [0.700, 0.120, 290.0], + mood: "Linear-adjacent indigo — the considered tool, late-evening focus mode, software made for people who care about craft", + strategy: "Pure black surface lets a single restrained indigo-violet carry the brand, with a cooler periwinkle accent providing UI hierarchy without competing — Vercel/Linear dark-mode discipline." }, + { id: "seed-100", oklch: [0.450, 0.150, 330.0], + mood: "velvet boudoir at last call — bruised orchid and lipstick traces under low lamplight", + strategy: "Pure near-black surface lets a deep magenta-rose primary smolder while a warm peach accent acts like skin-lit lamplight — drama lives in the brand pair, not the room." }, + { id: "seed-103", oklch: [0.650, 0.160, 330.0], + mood: "1980s Memphis boudoir — powder-pink neon humming against lacquered black, lipstick and lacquer", + strategy: "Near-black gallery surface lets the magenta-pink seed read as lit neon; accent shifts to warm coral to create cinematic dichromatic tension without competing chroma." }, + { id: "seed-228", oklch: [0.360, 0.147, 340.0], + mood: "Figma-era creative tool plum — considered productivity software for designers, the inky violet of a serif wordmark on a marketing site", + strategy: "Held the seed as a deep plum primary against pure white so the brand color does the emotional work; paired with a muted rose accent for warmth without breaking the productivity-tool restraint." }, + { id: "seed-107", oklch: [0.500, 0.200, 340.0], + mood: "Figma plum — creative-tool confidence, considered magenta for a modern design product", + strategy: "Pure white surface lets a saturated magenta-plum primary carry all the brand voice, paired with a cooler violet-leaning accent for hierarchy without competing." }, + { id: "seed-198", oklch: [0.600, 0.210, 340.0], + mood: "Figma-era creative tool plum — confident, considered, made for makers", + strategy: "Anchor a saturated plum primary against pure white so the brand color does all the emotional work, with a deeper magenta-rose accent for hierarchy." }, + { id: "seed-112", oklch: [0.754, 0.193, 343.4], + mood: "Figma-era creative tool — confident pink primary doing the brand work on a clean canvas, the way Linear uses indigo or Stripe uses violet", + strategy: "Anchor the seed pink as a saturated brand primary on pure white so the color carries all the personality; pair with a cooler plum accent to give the pink something to push against without competing." }, + { id: "seed-229", oklch: [0.420, 0.163, 350.0], + mood: "considered fintech rose — the deep magenta of a modern product brand (think Stripe-adjacent, but rotated toward berry), confident and current", + strategy: "pure white surface lets a single deep berry-rose primary do all the brand work, paired with a cooler indigo accent for the contrast move you see in modern product marketing" }, + { id: "seed-113", oklch: [0.470, 0.173, 354.8], + mood: "1960s velvet rope nightclub — crushed magenta, low light, cigarette smoke catching a spotlight", + strategy: "Pure black stage so the seed's smoky magenta reads as a single hot spotlight, paired with a cooler violet accent for the second light cue." }, + { id: "seed-114", oklch: [0.570, 0.158, 353.3], + mood: "fin-de-siècle Parisian rose — velvet curtain, theatre program, lipstick blotted on linen", + strategy: "Drop bg to true black so the dusty-rose primary reads as stage-lit silk; accent shifts to a warmer coral-mauve at higher lightness to create gentle hue rotation without breaking the romance." }, + { id: "seed-199", oklch: [0.650, 0.180, 350.0], + mood: "modern fintech rose — the considered pink of a Series B brand mark, confident and current without nostalgia", + strategy: "Pure white surface lets a saturated rose primary do the brand work, paired with a deep plum accent for hierarchy — the Stripe move applied to a pink hue." }, + { id: "seed-115", oklch: [0.636, 0.218, 355.3], + mood: "backstage at a cabaret — velvet rope, lipstick mark on a champagne glass", + strategy: "Seed reads as a saturated stage-light magenta-red; I push it into pure black so the primary glows like a neon sign and the accent (a cold pearl-pink) acts as the spotlight rim — the room is dark, the color does the singing." }, + { id: "seed-230", oklch: [0.650, 0.249, 354.5], + mood: "Modern fintech rose — the considered pink of a contemporary payments brand: confident, alive, and clear-headed", + strategy: "Pure white bg lets a saturated rose-magenta primary carry all the brand energy, paired with a cooler indigo accent for trustworthy contrast — the Stripe move applied to a pink hue." }, + { id: "seed-231", oklch: [0.682, 0.241, 353.2], + mood: "Figma-era creative tool — a confident pink-magenta product brand, the kind a modern design platform uses to feel alive without shouting", + strategy: "Default A pure white bg lets the saturated pink-magenta primary do all the brand work, with a near-complementary cool teal accent for tool-like clarity and a neutral ink for editorial calm" }, + { id: "seed-116", oklch: [0.734, 0.183, 356.8], + mood: "modern beauty brand DTC — Glossier-adjacent pink, confident and current without being saccharine", + 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." }, +]; + +function parseArgs(argv) { + const args = { id: null, from: null }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === '--id' && argv[i + 1]) { args.id = argv[++i]; } + else if (a === '--from' && argv[i + 1]) { args.from = argv[++i]; } + } + return args; +} + +// Hash a key into a stable float in [0, 1) for deterministic weighted picks. +function hashUnit(key) { + const h = crypto.createHash('sha256').update(key).digest(); + return h.readUInt32BE(0) / 0x100000000; +} + +// The curated library is hue-skewed (more reds/oranges than teals/magentas) +// because that's where the source material + taste landed. Left uniform, a +// random pick would land on red ~1/3 of the time. Inverse-frequency weighting +// gives each seed a weight of 1/(count in its 30° hue bucket), so each hue +// ZONE is roughly equally likely to be chosen regardless of how many seeds it +// holds — fair rainbow exposure across runs without pruning the library. +function buildWeights(seeds) { + const bucketCount = {}; + const bucketOf = (s) => Math.floor(((s.oklch[2] % 360) + 360) % 360 / 30); + for (const s of seeds) { const b = bucketOf(s); bucketCount[b] = (bucketCount[b] || 0) + 1; } + const weights = seeds.map((s) => 1 / bucketCount[bucketOf(s)]); + const total = weights.reduce((a, b) => a + b, 0); + return { weights, total }; +} + +function weightedPick(seeds, unit) { + const { weights, total } = buildWeights(seeds); + let target = unit * total; + for (let i = 0; i < seeds.length; i++) { + target -= weights[i]; + if (target < 0) return seeds[i]; + } + return seeds[seeds.length - 1]; +} + +function pickSeed(seeds, { id, from }) { + if (id) { + const found = seeds.find(s => s.id === id); + if (!found) { console.error(`no seed with id "${id}"`); process.exit(2); } + return found; + } + const envFrom = process.env.IMPECCABLE_PALETTE_SEED; + const key = from || envFrom; + const unit = key ? hashUnit(key) : Math.random(); + return weightedPick(seeds, unit); +} + +function fmtOklch([L, C, H]) { + return `oklch(${L.toFixed(3)} ${C.toFixed(3)} ${H.toFixed(1)})`; +} + +function hueWord(H) { + if (H < 15 || H >= 345) return 'pure red'; + if (H < 35) return 'warm red / crimson'; + if (H < 55) return 'warm coral / burnt orange'; + if (H < 80) return 'orange / honey'; + if (H < 105) return 'warm amber / honey-gold'; + if (H < 135) return 'yellow-green / olive'; + if (H < 170) return 'green'; + if (H < 200) return 'teal'; + if (H < 230) return 'sky blue'; + if (H < 265) return 'cobalt / indigo'; + if (H < 295) return 'violet / purple'; + if (H < 330) return 'magenta / pink'; + return 'deep pink / rose'; +} + +// --------------------------------------------------------------- + +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}` : ''; + +// --------------------------------------------------------------- +// Fat tool-exit response — what the model sees on stdout. +// --------------------------------------------------------------- + +process.stdout.write(`BRAND SEED · ${seed.id} + +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. + +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. + +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é. + + 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 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. + + 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. + +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): + + - 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. + +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. + +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. +`); diff --git a/plugin/skills/impeccable/scripts/pin.mjs b/plugin/skills/impeccable/scripts/pin.mjs index ba02783fb..320d98ce1 100644 --- a/plugin/skills/impeccable/scripts/pin.mjs +++ b/plugin/skills/impeccable/scripts/pin.mjs @@ -27,7 +27,7 @@ const HARNESS_DIRS = [ // Valid sub-command names const VALID_COMMANDS = [ - 'craft', 'teach', 'extract', 'document', 'shape', + 'craft', 'init', 'extract', 'document', 'shape', 'critique', 'audit', 'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live', 'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index 6734c07f2..000000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,5866 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - css-select: - specifier: ^5.2.2 - version: 5.2.2 - css-tree: - specifier: ^3.2.1 - version: 3.2.1 - domutils: - specifier: ^3.2.2 - version: 3.2.2 - htmlparser2: - specifier: ^10.0.0 - version: 10.1.0 - marked: - specifier: ^16.4.2 - version: 16.4.2 - devDependencies: - '@ai-sdk/anthropic': - specifier: ^3.0.71 - version: 3.0.71(zod@4.3.6) - '@ai-sdk/openai': - specifier: ^3.0.53 - version: 3.0.53(zod@4.3.6) - '@anthropic-ai/claude-agent-sdk': - specifier: ^0.2.119 - version: 0.2.122(zod@4.3.6) - '@anthropic-ai/sdk': - specifier: ^0.91.1 - version: 0.91.1(zod@4.3.6) - '@google/genai': - specifier: ^1.50.1 - version: 1.50.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6)) - '@paper-design/shaders': - specifier: ^0.0.76 - version: 0.0.76 - ai: - specifier: ^6.0.168 - version: 6.0.168(zod@4.3.6) - archiver: - specifier: ^7.0.1 - version: 7.0.1 - astro: - specifier: ^6.2.1 - version: 6.2.1(@types/node@25.6.0)(rollup@4.60.2) - modern-screenshot: - specifier: ^4.7.0 - version: 4.7.0 - motion: - specifier: ^12.38.0 - version: 12.38.0 - playwright: - specifier: ^1.59.1 - version: 1.59.1 - wrangler: - specifier: ^4.85.0 - version: 4.86.0 - zod: - specifier: ^4.3.6 - version: 4.3.6 - optionalDependencies: - puppeteer: - specifier: ^24.42.0 - version: 24.42.0 - -packages: - - '@ai-sdk/anthropic@3.0.71': - resolution: {integrity: sha512-bUWOzrzR0gJKJO/PLGMR4uH2dqEgqGhrsCV+sSpk4KtOEnUQlfjZI/F7BFlqSvVpFbjdgYRRLysAeEZpJ6S1lg==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4.1.8 - - '@ai-sdk/gateway@3.0.104': - resolution: {integrity: sha512-ZKX5n74io8VIRlhIMSLWVlvT3sXC8Z7cZ9GHuWBWZDVi96+62AIsWuLGvMfcBA1STYuSoDrp6rIziZmvrTq0TA==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4.1.8 - - '@ai-sdk/openai@3.0.53': - resolution: {integrity: sha512-Wld+Rbc05KaUn08uBt06eEuwcgalcIFtIl32Yp+GxuZXUQwOb6YeAuq+C6da4ch6BurFoqEaLemJVwjBb7x+PQ==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4.1.8 - - '@ai-sdk/provider-utils@4.0.23': - resolution: {integrity: sha512-z8GlDaCmRSDlqkMF2f4/RFgWxdarvIbyuk+m6WXT1LYgsnGiXRJGTD2Z1+SDl3LqtFuRtGX1aghYvQLoHL/9pg==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4.1.8 - - '@ai-sdk/provider@3.0.8': - resolution: {integrity: sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==} - engines: {node: '>=18'} - - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.2.122': - resolution: {integrity: sha512-qiU7yLgHkKcgHOmedaP6niuZ7Mkqb0Bdzkt7vkTSAcabD3t2JNln5SYgQL17WkmSrgfcLspRA0EIEeiXR5mrEg==} - cpu: [arm64] - os: [darwin] - - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.2.122': - resolution: {integrity: sha512-RheH4j4G133tyD3+m6M/sJfZI9UMxGo+dHVh09u5Y4ctDeLYSxTBVO5a1KJ/570TiedFeyWwuOGVs0YkTNLiLg==} - cpu: [x64] - os: [darwin] - - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.2.122': - resolution: {integrity: sha512-7EhskzBkeH5LOzuXNPoyTjNb7TwR9+mZcZ0s/e3AVEbTJTN9fddx3dcqZ6h+PivyF+JAGYjf7/OqUWTrawIgJw==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.2.122': - resolution: {integrity: sha512-tSOy0BrxQfPNG4mqrz7KC7T+7ysrcuzof1m2Cw+DRVqp1O4CZl1VvYu2gQJxnVYoVKO2B6KzC05oOqpV9EWn7A==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.2.122': - resolution: {integrity: sha512-SLPgQcz8tEsYA4YqHb8SrVfssTMPIXJ4C6NTrullOd3+IlutuJnyRw9wX0tLi0sozDwv6TIwAf78RV84RMUd/w==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@anthropic-ai/claude-agent-sdk-linux-x64@0.2.122': - resolution: {integrity: sha512-ClpyiD79YzjJ5o1w/yHJ/FugaZTCeS0IypQJg/SrAKKfn2oTyKFDg7P+Fu4+WSezj81qgdO/vT1TW2IPALue2w==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.2.122': - resolution: {integrity: sha512-h7aUAlm9PTuWyf2RNncpdFwweYD5yMsMqB1Y3DioCP8jZSPNRuiBPxJdtRVPimrAkPre7gY1hLiicKOaEDwFpw==} - cpu: [arm64] - os: [win32] - - '@anthropic-ai/claude-agent-sdk-win32-x64@0.2.122': - resolution: {integrity: sha512-GVBySbHu/CaoCdoMZ2mHM0ma8VzWahOkcpFObjFE0eTjvs8NqkWPQz+OyGkrySBwrto3VDn+lkd1KRW+Q2lhyA==} - cpu: [x64] - os: [win32] - - '@anthropic-ai/claude-agent-sdk@0.2.122': - resolution: {integrity: sha512-o/PO//rmivnHqUZkcNaEHlmXQjUUqAaXCrxUJHq/J+Jy2tCpGxJ5C5j33qKXfXAEOxeWnKnHqA0Oyh1SFx2SEA==} - engines: {node: '>=18.0.0'} - peerDependencies: - zod: ^4.0.0 - - '@anthropic-ai/sdk@0.81.0': - resolution: {integrity: sha512-D4K5PvEV6wPiRtVlVsJHIUhHAmOZ6IT/I9rKlTf84gR7GyyAurPJK7z9BOf/AZqC5d1DhYQGJNKRmV+q8dGhgw==} - hasBin: true - peerDependencies: - zod: ^3.25.0 || ^4.0.0 - peerDependenciesMeta: - zod: - optional: true - - '@anthropic-ai/sdk@0.91.1': - resolution: {integrity: sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==} - hasBin: true - peerDependencies: - zod: ^3.25.0 || ^4.0.0 - peerDependenciesMeta: - zod: - optional: true - - '@astrojs/compiler@4.0.0': - resolution: {integrity: sha512-eouss7G8ygdZqHuke033VMcVw5HTZUu+PXd/h06DGDUg/jt5btPYPqh66ENWw/mU78rBrf/oeC4oqoBwMtDMNA==} - - '@astrojs/internal-helpers@0.9.0': - resolution: {integrity: sha512-GdYkzR26re8izmyYlBqf4z2s7zNngmWLFuxw0UKiPNqHraZGS6GKWIwSHgS22RDlu2ePFJ8bzmpBcUszut/SDg==} - - '@astrojs/markdown-remark@7.1.1': - resolution: {integrity: sha512-C6e9BnLGlbdv6bV8MYGeHpHxsUHrCrB4OuRLqi5LI7oiBVcBcqfUN06zpwFQdHgV48QCCrMmLpyqBr7VqC+swA==} - - '@astrojs/prism@4.0.1': - resolution: {integrity: sha512-nksZQVjlferuWzhPsBpQ1JE5XuKAf1id1/9Hj4a9KG4+ofrlzxUUwX4YGQF/SuDiuiGKEnzopGOt38F3AnVWsQ==} - engines: {node: '>=22.12.0'} - - '@astrojs/telemetry@3.3.1': - resolution: {integrity: sha512-7fcIxXS9J4ls5tr8b3ww9rbAIz2+HrhNJYZdkAhhB4za/I5IZ/60g+Bs8q7zwG0tOIZfNB4JWhVJ1Qkl/OrNCw==} - engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} - - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.29.3': - resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/runtime@7.29.2': - resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.29.0': - resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} - engines: {node: '>=6.9.0'} - - '@capsizecss/unpack@4.0.0': - resolution: {integrity: sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==} - engines: {node: '>=18'} - - '@clack/core@1.3.0': - resolution: {integrity: sha512-xJPHpAmEQUBrXSLx0gF+q5K/IyihXpsHZcha+jB+tyahsKRK3Dxo4D0coZDewHo12NhiuzC3dTtMPbm53GEAAA==} - engines: {node: '>= 20.12.0'} - - '@clack/prompts@1.3.0': - resolution: {integrity: sha512-GgcWwRCs/xPtaqlMy8qRhPnZf9vlWcWZNHAitnVQ3yk7JmSralSiq5q07yaffYE8SogtDm7zFeKccx1QNVARpw==} - engines: {node: '>= 20.12.0'} - - '@cloudflare/kv-asset-handler@0.4.2': - resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} - engines: {node: '>=18.0.0'} - - '@cloudflare/unenv-preset@2.16.1': - resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==} - peerDependencies: - unenv: 2.0.0-rc.24 - workerd: '>1.20260305.0 <2.0.0-0' - peerDependenciesMeta: - workerd: - optional: true - - '@cloudflare/workerd-darwin-64@1.20260426.1': - resolution: {integrity: sha512-Ch7DqsmYzSQRTY87pZpsGsFVz9VVBnLPnCBOHxKt1HH25a7oMu1w1PbPWqVmE0VerCLsj/TScX7Ob3v6E14TZw==} - engines: {node: '>=16'} - cpu: [x64] - os: [darwin] - - '@cloudflare/workerd-darwin-arm64@1.20260426.1': - resolution: {integrity: sha512-0m0U8vaPRH25SpKjbSyRql6gmPe4rCsETRV2WW0qBnuMdKNr5Vh5/Uez80xVrfiCCRMTULGeg63Nqg2vg6CDOA==} - engines: {node: '>=16'} - cpu: [arm64] - os: [darwin] - - '@cloudflare/workerd-linux-64@1.20260426.1': - resolution: {integrity: sha512-C8LlC8uSYzg49y51n++75esxZmMp+Uz1OKHHA/4lkv6rjOTbcHQJuEwSLppjybVIXpv7A8MBhbu9iyCTvyv1mw==} - engines: {node: '>=16'} - cpu: [x64] - os: [linux] - - '@cloudflare/workerd-linux-arm64@1.20260426.1': - resolution: {integrity: sha512-ESVp/OIFMAqjQsa8BOP2BQQz5Vpfv6ncN6lNnIuNeOgsISQBdYk+LA60bwQHMud9tvmnSYtONp1zkZ8OQz+x6w==} - engines: {node: '>=16'} - cpu: [arm64] - os: [linux] - - '@cloudflare/workerd-windows-64@1.20260426.1': - resolution: {integrity: sha512-d3Xj/IjINRgNVwH+eKhpUn4xkkcEewbWXbOvBlapiirKWh5zl9m0Epi3qOqmjyRYK6MICqIGXg4qZBEt0lxudw==} - engines: {node: '>=16'} - cpu: [x64] - os: [win32] - - '@cspotcode/source-map-support@0.8.1': - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} - - '@emnapi/runtime@1.9.2': - resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} - - '@esbuild/aix-ppc64@0.27.3': - resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.27.3': - resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.27.3': - resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.27.3': - resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.27.3': - resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.27.3': - resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.27.3': - resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.27.3': - resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.27.3': - resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.27.3': - resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.27.3': - resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.27.3': - resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.27.3': - resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.27.3': - resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.27.3': - resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.27.3': - resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.27.3': - resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.27.3': - resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.27.3': - resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.27.3': - resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.27.3': - resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.27.3': - resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.27.3': - resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.27.3': - resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.27.3': - resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.27.3': - resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@google/genai@1.50.1': - resolution: {integrity: sha512-YbkX7H9+1Pt8wOt7DDREy8XSoiL6fRDzZQRyaVBarFf8MR3zHGqVdvM4cLbDXqPhxqvegZShgfxb8kw9C7YhAQ==} - engines: {node: '>=20.0.0'} - peerDependencies: - '@modelcontextprotocol/sdk': ^1.25.2 - peerDependenciesMeta: - '@modelcontextprotocol/sdk': - optional: true - - '@hono/node-server@1.19.14': - resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} - engines: {node: '>=18.14.1'} - peerDependencies: - hono: ^4 - - '@img/colour@1.1.0': - resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} - engines: {node: '>=18'} - - '@img/sharp-darwin-arm64@0.34.5': - resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [darwin] - - '@img/sharp-darwin-x64@0.34.5': - resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-darwin-arm64@1.2.4': - resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} - cpu: [arm64] - os: [darwin] - - '@img/sharp-libvips-darwin-x64@1.2.4': - resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-linux-arm64@1.2.4': - resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-arm@1.2.4': - resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-ppc64@1.2.4': - resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-riscv64@1.2.4': - resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-s390x@1.2.4': - resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-x64@1.2.4': - resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@img/sharp-linux-arm64@0.34.5': - resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-arm@0.34.5': - resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-ppc64@0.34.5': - resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-riscv64@0.34.5': - resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-s390x@0.34.5': - resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-x64@0.34.5': - resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@img/sharp-linuxmusl-arm64@0.34.5': - resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@img/sharp-linuxmusl-x64@0.34.5': - resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@img/sharp-wasm32@0.34.5': - resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [wasm32] - - '@img/sharp-win32-arm64@0.34.5': - resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [win32] - - '@img/sharp-win32-ia32@0.34.5': - resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ia32] - os: [win32] - - '@img/sharp-win32-x64@0.34.5': - resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [win32] - - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@jridgewell/trace-mapping@0.3.9': - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - - '@modelcontextprotocol/sdk@1.29.0': - resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} - engines: {node: '>=18'} - peerDependencies: - '@cfworker/json-schema': ^4.1.1 - zod: ^3.25 || ^4.0 - peerDependenciesMeta: - '@cfworker/json-schema': - optional: true - - '@opentelemetry/api@1.9.0': - resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} - engines: {node: '>=8.0.0'} - - '@oslojs/encoding@1.1.0': - resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} - - '@paper-design/shaders@0.0.76': - resolution: {integrity: sha512-AcNDY4J66YQHUfQYFInkCP7M9VOje0od7wLpOR7LtCmc532opJy6ll+h1W9zBovz8tt9U7OADUmJ/qKEXyOX/A==} - - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - - '@poppinss/colors@4.1.6': - resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} - - '@poppinss/dumper@0.6.5': - resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} - - '@poppinss/exception@1.2.3': - resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} - - '@protobufjs/aspromise@1.1.2': - resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} - - '@protobufjs/base64@1.1.2': - resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - - '@protobufjs/codegen@2.0.5': - resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} - - '@protobufjs/eventemitter@1.1.0': - resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} - - '@protobufjs/fetch@1.1.0': - resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} - - '@protobufjs/float@1.0.2': - resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} - - '@protobufjs/inquire@1.1.1': - resolution: {integrity: sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==} - - '@protobufjs/path@1.1.2': - resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} - - '@protobufjs/pool@1.1.0': - resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} - - '@protobufjs/utf8@1.1.1': - resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} - - '@puppeteer/browsers@2.13.0': - resolution: {integrity: sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==} - engines: {node: '>=18'} - hasBin: true - - '@rollup/pluginutils@5.3.0': - resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/rollup-android-arm-eabi@4.60.2': - resolution: {integrity: sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.60.2': - resolution: {integrity: sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.60.2': - resolution: {integrity: sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.60.2': - resolution: {integrity: sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.60.2': - resolution: {integrity: sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.60.2': - resolution: {integrity: sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.60.2': - resolution: {integrity: sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm-musleabihf@4.60.2': - resolution: {integrity: sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==} - cpu: [arm] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-arm64-gnu@4.60.2': - resolution: {integrity: sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm64-musl@4.60.2': - resolution: {integrity: sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-loong64-gnu@4.60.2': - resolution: {integrity: sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==} - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-loong64-musl@4.60.2': - resolution: {integrity: sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==} - cpu: [loong64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-ppc64-gnu@4.60.2': - resolution: {integrity: sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-ppc64-musl@4.60.2': - resolution: {integrity: sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==} - cpu: [ppc64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-gnu@4.60.2': - resolution: {integrity: sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-riscv64-musl@4.60.2': - resolution: {integrity: sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-s390x-gnu@4.60.2': - resolution: {integrity: sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-gnu@4.60.2': - resolution: {integrity: sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-musl@4.60.2': - resolution: {integrity: sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rollup/rollup-openbsd-x64@4.60.2': - resolution: {integrity: sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.60.2': - resolution: {integrity: sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==} - cpu: [arm64] - os: [openharmony] - - '@rollup/rollup-win32-arm64-msvc@4.60.2': - resolution: {integrity: sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.60.2': - resolution: {integrity: sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.60.2': - resolution: {integrity: sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.60.2': - resolution: {integrity: sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==} - cpu: [x64] - os: [win32] - - '@shikijs/core@4.0.2': - resolution: {integrity: sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==} - engines: {node: '>=20'} - - '@shikijs/engine-javascript@4.0.2': - resolution: {integrity: sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==} - engines: {node: '>=20'} - - '@shikijs/engine-oniguruma@4.0.2': - resolution: {integrity: sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==} - engines: {node: '>=20'} - - '@shikijs/langs@4.0.2': - resolution: {integrity: sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==} - engines: {node: '>=20'} - - '@shikijs/primitive@4.0.2': - resolution: {integrity: sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==} - engines: {node: '>=20'} - - '@shikijs/themes@4.0.2': - resolution: {integrity: sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==} - engines: {node: '>=20'} - - '@shikijs/types@4.0.2': - resolution: {integrity: sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==} - engines: {node: '>=20'} - - '@shikijs/vscode-textmate@10.0.2': - resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} - - '@sindresorhus/is@7.2.0': - resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} - engines: {node: '>=18'} - - '@speed-highlight/core@1.2.15': - resolution: {integrity: sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==} - - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - - '@tootallnate/quickjs-emscripten@0.23.0': - resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} - - '@types/debug@4.1.13': - resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} - - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - - '@types/hast@3.0.4': - resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} - - '@types/mdast@4.0.4': - resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} - - '@types/ms@2.1.0': - resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - - '@types/nlcst@2.0.3': - resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} - - '@types/node@25.6.0': - resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} - - '@types/retry@0.12.0': - resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} - - '@types/unist@3.0.3': - resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - - '@types/yauzl@2.10.3': - resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - - '@ungap/structured-clone@1.3.0': - resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - deprecated: Potential CWE-502 - Update to 1.3.1 or higher - - '@vercel/oidc@3.2.0': - resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==} - engines: {node: '>= 20'} - - abort-controller@3.0.0: - resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} - engines: {node: '>=6.5'} - - accepts@2.0.0: - resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} - engines: {node: '>= 0.6'} - - agent-base@7.1.4: - resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} - engines: {node: '>= 14'} - - ai@6.0.168: - resolution: {integrity: sha512-2HqCJuO+1V2aV7vfYs5LFEUfxbkGX+5oa54q/gCCTL7KLTdbxcCu5D7TdLA5kwsrs3Szgjah9q6D9tpjHM3hUQ==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4.1.8 - - ajv-formats@3.0.1: - resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - - ajv@8.20.0: - resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} - engines: {node: '>=12'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - ansi-styles@6.2.3: - resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} - engines: {node: '>=12'} - - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - archiver-utils@5.0.2: - resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} - engines: {node: '>= 14'} - - archiver@7.0.1: - resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} - engines: {node: '>= 14'} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - aria-query@5.3.2: - resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} - engines: {node: '>= 0.4'} - - array-iterate@2.0.1: - resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==} - - ast-types@0.13.4: - resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} - engines: {node: '>=4'} - - astro@6.2.1: - resolution: {integrity: sha512-3g1sYNly+QAkuO5ErNEQBYvsxorNDSCUNIeStBs+kcXGchvKQl1Q9EuDNOvSg010XLlHJFLVFZs9LV18Jjp4Hg==} - engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'} - hasBin: true - - async@3.2.6: - resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} - - axobject-query@4.1.0: - resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} - engines: {node: '>= 0.4'} - - b4a@1.8.0: - resolution: {integrity: sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==} - peerDependencies: - react-native-b4a: '*' - peerDependenciesMeta: - react-native-b4a: - optional: true - - bail@2.0.2: - resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - bare-events@2.8.2: - resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} - peerDependencies: - bare-abort-controller: '*' - peerDependenciesMeta: - bare-abort-controller: - optional: true - - bare-fs@4.7.0: - resolution: {integrity: sha512-xzqKsCFxAek9aezYhjJuJRXBIaYlg/0OGDTZp+T8eYmYMlm66cs6cYko02drIyjN2CBbi+I6L7YfXyqpqtKRXA==} - engines: {bare: '>=1.16.0'} - peerDependencies: - bare-buffer: '*' - peerDependenciesMeta: - bare-buffer: - optional: true - - bare-os@3.8.7: - resolution: {integrity: sha512-G4Gr1UsGeEy2qtDTZwL7JFLo2wapUarz7iTMcYcMFdS89AIQuBoyjgXZz0Utv7uHs3xA9LckhVbeBi8lEQrC+w==} - engines: {bare: '>=1.14.0'} - - bare-path@3.0.0: - resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} - - bare-stream@2.13.0: - resolution: {integrity: sha512-3zAJRZMDFGjdn+RVnNpF9kuELw+0Fl3lpndM4NcEOhb9zwtSo/deETfuIwMSE5BXanA0FrN1qVjffGwAg2Y7EA==} - peerDependencies: - bare-abort-controller: '*' - bare-buffer: '*' - bare-events: '*' - peerDependenciesMeta: - bare-abort-controller: - optional: true - bare-buffer: - optional: true - bare-events: - optional: true - - bare-url@2.4.0: - resolution: {integrity: sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==} - - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - - basic-ftp@5.2.2: - resolution: {integrity: sha512-1tDrzKsdCg70WGvbFss/ulVAxupNauGnOlgpyjKzeQxzyllBLS0CGLV7tjIXTK3ZQA9/FBEm9qyFFN1bciA6pw==} - engines: {node: '>=10.0.0'} - - bignumber.js@9.3.1: - resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} - - blake3-wasm@2.1.5: - resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} - - body-parser@2.2.2: - resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} - engines: {node: '>=18'} - - boolbase@1.0.0: - resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - - brace-expansion@2.1.0: - resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} - - buffer-crc32@0.2.13: - resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} - - buffer-crc32@1.0.0: - resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} - engines: {node: '>=8.0.0'} - - buffer-equal-constant-time@1.0.1: - resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} - - buffer@6.0.3: - resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - ccount@2.0.1: - resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - - character-entities-html4@2.1.0: - resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} - - character-entities-legacy@3.0.0: - resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} - - character-entities@2.0.2: - resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} - - chokidar@5.0.0: - resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} - engines: {node: '>= 20.19.0'} - - chromium-bidi@14.0.0: - resolution: {integrity: sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==} - peerDependencies: - devtools-protocol: '*' - - ci-info@4.4.0: - resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} - engines: {node: '>=8'} - - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - - clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} - engines: {node: '>=6'} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - comma-separated-tokens@2.0.3: - resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} - - commander@11.1.0: - resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} - engines: {node: '>=16'} - - common-ancestor-path@2.0.0: - resolution: {integrity: sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==} - engines: {node: '>= 18'} - - compress-commons@6.0.2: - resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} - engines: {node: '>= 14'} - - content-disposition@1.1.0: - resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} - engines: {node: '>=18'} - - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - - cookie-es@1.2.3: - resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==} - - cookie-signature@1.2.2: - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} - engines: {node: '>=6.6.0'} - - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - - cookie@1.1.1: - resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} - engines: {node: '>=18'} - - core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - - cors@2.8.6: - resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} - engines: {node: '>= 0.10'} - - cosmiconfig@9.0.1: - resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} - engines: {node: '>=14'} - peerDependencies: - typescript: '>=4.9.5' - peerDependenciesMeta: - typescript: - optional: true - - crc-32@1.2.2: - resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} - engines: {node: '>=0.8'} - hasBin: true - - crc32-stream@6.0.0: - resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} - engines: {node: '>= 14'} - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - crossws@0.3.5: - resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} - - css-select@5.2.2: - resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} - - css-tree@2.2.1: - resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} - engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} - - css-tree@3.2.1: - resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} - engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - - css-what@6.2.2: - resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} - engines: {node: '>= 6'} - - csso@5.0.5: - resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} - engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} - - data-uri-to-buffer@4.0.1: - resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} - engines: {node: '>= 12'} - - data-uri-to-buffer@6.0.2: - resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} - engines: {node: '>= 14'} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - decode-named-character-reference@1.3.0: - resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} - - defu@6.1.7: - resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} - - degenerator@5.0.1: - resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} - engines: {node: '>= 14'} - - depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - - dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - - destr@2.0.5: - resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} - - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - - devalue@5.8.0: - resolution: {integrity: sha512-2zA9pFEsnp7vWBZbXF5JAgAq0fsUIt/1XPbRiAmRV3lp/2C3upzH+sADiyy66aFCihoLEsrQHxNM5w1gIDfsBg==} - - devlop@1.1.0: - resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - - devtools-protocol@0.0.1595872: - resolution: {integrity: sha512-kRfgp8vWVjBu/fbYCiVFiOqsCk3CrMKEo3WbgGT2NXK2dG7vawWPBljixajVgGK9II8rDO9G0oD0zLt3I1daRg==} - - diff@8.0.4: - resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} - engines: {node: '>=0.3.1'} - - dlv@1.1.3: - resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - - dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} - - domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - - domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} - - domutils@3.2.2: - resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} - - dset@3.1.4: - resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} - engines: {node: '>=4'} - - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - - ecdsa-sig-formatter@1.0.11: - resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} - - ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - - encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - - end-of-stream@1.4.5: - resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - - entities@6.0.1: - resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} - engines: {node: '>=0.12'} - - entities@7.0.1: - resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} - engines: {node: '>=0.12'} - - env-paths@2.2.1: - resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} - engines: {node: '>=6'} - - error-ex@1.3.4: - resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - - error-stack-parser-es@1.0.5: - resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} - - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - - es-module-lexer@2.1.0: - resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} - - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} - - esbuild@0.27.3: - resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} - engines: {node: '>=18'} - hasBin: true - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - - escape-string-regexp@5.0.0: - resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} - engines: {node: '>=12'} - - escodegen@2.1.0: - resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} - engines: {node: '>=6.0'} - hasBin: true - - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - - estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - - etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - - event-target-shim@5.0.1: - resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} - engines: {node: '>=6'} - - eventemitter3@5.0.4: - resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} - - events-universal@1.0.1: - resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} - - events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} - - eventsource-parser@3.0.8: - resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} - engines: {node: '>=18.0.0'} - - eventsource@3.0.7: - resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} - engines: {node: '>=18.0.0'} - - express-rate-limit@8.4.1: - resolution: {integrity: sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==} - engines: {node: '>= 16'} - peerDependencies: - express: '>= 4.11' - - express@5.2.1: - resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} - engines: {node: '>= 18'} - - extend@3.0.2: - resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - - extract-zip@2.0.1: - resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} - engines: {node: '>= 10.17.0'} - hasBin: true - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-fifo@1.3.2: - resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} - - fast-string-truncated-width@3.0.3: - resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} - - fast-string-width@3.0.2: - resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} - - fast-uri@3.1.0: - resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - - fast-wrap-ansi@0.2.0: - resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==} - - fd-slicer@1.1.0: - resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - fetch-blob@3.2.0: - resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} - engines: {node: ^12.20 || >= 14.13} - - finalhandler@2.1.1: - resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} - engines: {node: '>= 18.0.0'} - - flattie@1.1.1: - resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} - engines: {node: '>=8'} - - fontace@0.4.1: - resolution: {integrity: sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==} - - fontkitten@1.0.3: - resolution: {integrity: sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==} - engines: {node: '>=20'} - - foreground-child@3.3.1: - resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} - engines: {node: '>=14'} - - formdata-polyfill@4.0.10: - resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} - engines: {node: '>=12.20.0'} - - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - - framer-motion@12.38.0: - resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==} - peerDependencies: - '@emotion/is-prop-valid': '*' - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@emotion/is-prop-valid': - optional: true - react: - optional: true - react-dom: - optional: true - - fresh@2.0.0: - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} - engines: {node: '>= 0.8'} - - fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - gaxios@7.1.4: - resolution: {integrity: sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==} - engines: {node: '>=18'} - - gcp-metadata@8.1.2: - resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} - engines: {node: '>=18'} - - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - - get-stream@5.2.0: - resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} - engines: {node: '>=8'} - - get-uri@6.0.5: - resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} - engines: {node: '>= 14'} - - github-slugger@2.0.0: - resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} - - glob@10.4.5: - resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - hasBin: true - - google-auth-library@10.6.2: - resolution: {integrity: sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==} - engines: {node: '>=18'} - - google-logging-utils@1.1.3: - resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} - engines: {node: '>=14'} - - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - h3@1.15.11: - resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==} - - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - hasown@2.0.3: - resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} - engines: {node: '>= 0.4'} - - hast-util-from-html@2.0.3: - resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} - - hast-util-from-parse5@8.0.3: - resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} - - hast-util-is-element@3.0.0: - resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} - - hast-util-parse-selector@4.0.0: - resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} - - hast-util-raw@9.1.0: - resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} - - hast-util-to-html@9.0.5: - resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} - - hast-util-to-parse5@8.0.1: - resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==} - - hast-util-to-text@4.0.2: - resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} - - hast-util-whitespace@3.0.0: - resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} - - hastscript@9.0.1: - resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} - - hono@4.12.15: - resolution: {integrity: sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==} - engines: {node: '>=16.9.0'} - - html-escaper@3.0.3: - resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} - - html-void-elements@3.0.0: - resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} - - htmlparser2@10.1.0: - resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} - - http-cache-semantics@4.2.0: - resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} - - http-errors@2.0.1: - resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} - engines: {node: '>= 0.8'} - - http-proxy-agent@7.0.2: - resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} - engines: {node: '>= 14'} - - https-proxy-agent@7.0.6: - resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} - engines: {node: '>= 14'} - - iconv-lite@0.7.2: - resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} - engines: {node: '>=0.10.0'} - - ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - ip-address@10.1.0: - resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} - engines: {node: '>= 12'} - - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - - iron-webcrypto@1.2.1: - resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} - - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - - is-docker@3.0.0: - resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - hasBin: true - - is-docker@4.0.0: - resolution: {integrity: sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==} - engines: {node: '>=20'} - hasBin: true - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - is-inside-container@1.0.0: - resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} - engines: {node: '>=14.16'} - hasBin: true - - is-plain-obj@4.1.0: - resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} - engines: {node: '>=12'} - - is-promise@4.0.0: - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - - is-wsl@3.1.1: - resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} - engines: {node: '>=16'} - - isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - - jose@6.2.3: - resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} - hasBin: true - - json-bigint@1.0.0: - resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} - - json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - - json-schema-to-ts@3.1.1: - resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} - engines: {node: '>=16'} - - json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - - json-schema-typed@8.0.2: - resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} - - json-schema@0.4.0: - resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} - - jwa@2.0.1: - resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} - - jws@4.0.1: - resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} - - kleur@4.1.5: - resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} - engines: {node: '>=6'} - - lazystream@1.0.1: - resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} - engines: {node: '>= 0.6.3'} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - - lodash@4.18.1: - resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} - - long@5.3.2: - resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} - - longest-streak@3.1.0: - resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} - - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - - lru-cache@11.3.3: - resolution: {integrity: sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==} - engines: {node: 20 || >=22} - - lru-cache@7.18.3: - resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} - engines: {node: '>=12'} - - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - - magicast@0.5.2: - resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==} - - markdown-table@3.0.4: - resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} - - marked@16.4.2: - resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} - engines: {node: '>= 20'} - hasBin: true - - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - - mdast-util-definitions@6.0.0: - resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==} - - mdast-util-find-and-replace@3.0.2: - resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} - - mdast-util-from-markdown@2.0.3: - resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} - - mdast-util-gfm-autolink-literal@2.0.1: - resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} - - mdast-util-gfm-footnote@2.1.0: - resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} - - mdast-util-gfm-strikethrough@2.0.0: - resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} - - mdast-util-gfm-table@2.0.0: - resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} - - mdast-util-gfm-task-list-item@2.0.0: - resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} - - mdast-util-gfm@3.1.0: - resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} - - mdast-util-phrasing@4.1.0: - resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} - - mdast-util-to-hast@13.2.1: - resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} - - mdast-util-to-markdown@2.1.2: - resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} - - mdast-util-to-string@4.0.0: - resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} - - mdn-data@2.0.28: - resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} - - mdn-data@2.27.1: - resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} - - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} - engines: {node: '>= 0.8'} - - merge-descriptors@2.0.0: - resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} - engines: {node: '>=18'} - - micromark-core-commonmark@2.0.3: - resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} - - micromark-extension-gfm-autolink-literal@2.1.0: - resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} - - micromark-extension-gfm-footnote@2.1.0: - resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} - - micromark-extension-gfm-strikethrough@2.1.0: - resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} - - micromark-extension-gfm-table@2.1.1: - resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} - - micromark-extension-gfm-tagfilter@2.0.0: - resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} - - micromark-extension-gfm-task-list-item@2.1.0: - resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} - - micromark-extension-gfm@3.0.0: - resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} - - micromark-factory-destination@2.0.1: - resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} - - micromark-factory-label@2.0.1: - resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} - - micromark-factory-space@2.0.1: - resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} - - micromark-factory-title@2.0.1: - resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} - - micromark-factory-whitespace@2.0.1: - resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} - - micromark-util-character@2.1.1: - resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} - - micromark-util-chunked@2.0.1: - resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} - - micromark-util-classify-character@2.0.1: - resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} - - micromark-util-combine-extensions@2.0.1: - resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} - - micromark-util-decode-numeric-character-reference@2.0.2: - resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} - - micromark-util-decode-string@2.0.1: - resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} - - micromark-util-encode@2.0.1: - resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} - - micromark-util-html-tag-name@2.0.1: - resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} - - micromark-util-normalize-identifier@2.0.1: - resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} - - micromark-util-resolve-all@2.0.1: - resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} - - micromark-util-sanitize-uri@2.0.1: - resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} - - micromark-util-subtokenize@2.1.0: - resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} - - micromark-util-symbol@2.0.1: - resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} - - micromark-util-types@2.0.2: - resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} - - micromark@4.0.2: - resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} - - mime-db@1.54.0: - resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} - engines: {node: '>= 0.6'} - - mime-types@3.0.2: - resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} - engines: {node: '>=18'} - - miniflare@4.20260426.0: - resolution: {integrity: sha512-KM+v76d04qT+NsPfVKVQEgnnuLNE3uzCCl2QKMTJ5OXor5JbBm1vpkQwQ+l7o5ELCrZ74RnyKhJKLiJyUA39Tw==} - engines: {node: '>=18.0.0'} - hasBin: true - - minimatch@5.1.9: - resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} - engines: {node: '>=10'} - - minimatch@9.0.9: - resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} - engines: {node: '>=16 || 14 >=14.17'} - - minipass@7.1.3: - resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} - engines: {node: '>=16 || 14 >=14.17'} - - mitt@3.0.1: - resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} - - modern-screenshot@4.7.0: - resolution: {integrity: sha512-9YxN+ddPSMMlhylOv25VHzXrl9u67QRxoh7+SEewGtgUw7t6hHTrjptSDJUSne9oG4Xk/h2cwG15nIt4Hc9ujg==} - - motion-dom@12.38.0: - resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==} - - motion-utils@12.36.0: - resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==} - - motion@12.38.0: - resolution: {integrity: sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==} - peerDependencies: - '@emotion/is-prop-valid': '*' - react: ^18.0.0 || ^19.0.0 - react-dom: ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@emotion/is-prop-valid': - optional: true - react: - optional: true - react-dom: - optional: true - - mrmime@2.0.1: - resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} - engines: {node: '>=10'} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - nanoid@3.3.12: - resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} - - neotraverse@0.6.18: - resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} - engines: {node: '>= 10'} - - netmask@2.1.1: - resolution: {integrity: sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==} - engines: {node: '>= 0.4.0'} - - nlcst-to-string@4.0.0: - resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==} - - node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} - engines: {node: '>=10.5.0'} - deprecated: Use your platform's native DOMException instead - - node-fetch-native@1.6.7: - resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} - - node-fetch@3.3.2: - resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - node-mock-http@1.0.4: - resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} - - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - nth-check@2.1.1: - resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - - obug@2.1.1: - resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - - ofetch@1.5.1: - resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} - - ohash@2.0.11: - resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} - - on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - oniguruma-parser@0.12.2: - resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} - - oniguruma-to-es@4.3.6: - resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} - - p-limit@7.3.0: - resolution: {integrity: sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==} - engines: {node: '>=20'} - - p-queue@9.2.0: - resolution: {integrity: sha512-dWgLE8AH0HjQ9fe74pUkKkvzzYT18Inp4zra3lKHnnwqGvcfcUBrvF2EAVX+envufDNBOzpPq/IBUONDbI7+3g==} - engines: {node: '>=20'} - - p-retry@4.6.2: - resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} - engines: {node: '>=8'} - - p-timeout@7.0.1: - resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==} - engines: {node: '>=20'} - - pac-proxy-agent@7.2.0: - resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} - engines: {node: '>= 14'} - - pac-resolver@7.0.1: - resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} - engines: {node: '>= 14'} - - package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - - package-manager-detector@1.6.0: - resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} - - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - - parse-latin@7.0.0: - resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} - - parse5@7.3.0: - resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} - - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} - - path-to-regexp@6.3.0: - resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} - - path-to-regexp@8.4.2: - resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} - - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - pend@1.2.0: - resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} - - piccolore@0.1.3: - resolution: {integrity: sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@2.3.2: - resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} - engines: {node: '>=8.6'} - - picomatch@4.0.4: - resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} - engines: {node: '>=12'} - - pkce-challenge@5.0.1: - resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} - engines: {node: '>=16.20.0'} - - playwright-core@1.59.1: - resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} - engines: {node: '>=18'} - hasBin: true - - playwright@1.59.1: - resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} - engines: {node: '>=18'} - hasBin: true - - postcss@8.5.13: - resolution: {integrity: sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==} - engines: {node: ^10 || ^12 || >=14} - - prismjs@1.30.0: - resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} - engines: {node: '>=6'} - - process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - - process@0.11.10: - resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} - engines: {node: '>= 0.6.0'} - - progress@2.0.3: - resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} - engines: {node: '>=0.4.0'} - - property-information@7.1.0: - resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} - - protobufjs@7.5.6: - resolution: {integrity: sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==} - engines: {node: '>=12.0.0'} - - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - - proxy-agent@6.5.0: - resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} - engines: {node: '>= 14'} - - proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - - pump@3.0.4: - resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} - - puppeteer-core@24.42.0: - resolution: {integrity: sha512-T4zXokk/izH01fYPhyyev1A4piWiOKrYq7CUFpdoYQxmOnXoV6YjUabmfIjCYkNspSoAXIxRid3Tw+Vg0fthYg==} - engines: {node: '>=18'} - - puppeteer@24.42.0: - resolution: {integrity: sha512-94MoPfFp2eY3eYIMdINkez4IOP5TMHntlZbVx06fHlQTtiQiYgaY0L2Zzfod8PVUkPqP7m3Qlre2v8YS8cudPA==} - engines: {node: '>=18'} - hasBin: true - - qs@6.15.1: - resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} - engines: {node: '>=0.6'} - - radix3@1.1.2: - resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} - - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} - - raw-body@3.0.2: - resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} - engines: {node: '>= 0.10'} - - readable-stream@2.3.8: - resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} - - readable-stream@4.7.0: - resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - readdir-glob@1.1.3: - resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} - - readdirp@5.0.0: - resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} - engines: {node: '>= 20.19.0'} - - regex-recursion@6.0.2: - resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} - - regex-utilities@2.3.0: - resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} - - regex@6.1.0: - resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} - - rehype-parse@9.0.1: - resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==} - - rehype-raw@7.0.0: - resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} - - rehype-stringify@10.0.1: - resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==} - - rehype@13.0.2: - resolution: {integrity: sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==} - - remark-gfm@4.0.1: - resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} - - remark-parse@11.0.0: - resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} - - remark-rehype@11.1.2: - resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} - - remark-smartypants@3.0.2: - resolution: {integrity: sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==} - engines: {node: '>=16.0.0'} - - remark-stringify@11.0.0: - resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} - - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - - retext-latin@4.0.0: - resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==} - - retext-smartypants@6.2.0: - resolution: {integrity: sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==} - - retext-stringify@4.0.0: - resolution: {integrity: sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==} - - retext@9.0.0: - resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==} - - retry@0.13.1: - resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} - engines: {node: '>= 4'} - - rollup@4.60.2: - resolution: {integrity: sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - router@2.2.0: - resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} - engines: {node: '>= 18'} - - safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - - sax@1.6.0: - resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} - engines: {node: '>=11.0.0'} - - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} - engines: {node: '>=10'} - hasBin: true - - send@1.2.1: - resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} - engines: {node: '>= 18'} - - serve-static@2.2.1: - resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} - engines: {node: '>= 18'} - - setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - - sharp@0.34.5: - resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - shiki@4.0.2: - resolution: {integrity: sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==} - engines: {node: '>=20'} - - side-channel-list@1.0.1: - resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} - engines: {node: '>= 0.4'} - - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} - - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - - sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - - smart-buffer@4.2.0: - resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} - engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - - smol-toml@1.6.1: - resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} - engines: {node: '>= 18'} - - socks-proxy-agent@8.0.5: - resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} - engines: {node: '>= 14'} - - socks@2.8.7: - resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} - engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - - space-separated-tokens@2.0.2: - resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - - statuses@2.0.2: - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} - engines: {node: '>= 0.8'} - - streamx@2.25.0: - resolution: {integrity: sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - - string_decoder@1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - - stringify-entities@4.0.4: - resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-ansi@7.2.0: - resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} - engines: {node: '>=12'} - - supports-color@10.2.2: - resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} - engines: {node: '>=18'} - - svgo@4.0.1: - resolution: {integrity: sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==} - engines: {node: '>=16'} - hasBin: true - - tar-fs@3.1.2: - resolution: {integrity: sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==} - - tar-stream@3.1.8: - resolution: {integrity: sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==} - - teex@1.0.1: - resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} - - text-decoder@1.2.7: - resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} - - tiny-inflate@1.0.3: - resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} - - tinyclip@0.1.12: - resolution: {integrity: sha512-Ae3OVUqifDw0wBriIBS7yVaW44Dp6eSHQcyq4Igc7eN2TJH/2YsicswaW+J/OuMvhpDPOKEgpAZCjkb4hpoyeA==} - engines: {node: ^16.14.0 || >= 17.3.0} - - tinyexec@1.1.2: - resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} - engines: {node: '>=18'} - - tinyglobby@0.2.16: - resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} - engines: {node: '>=12.0.0'} - - toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - - trim-lines@3.0.1: - resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} - - trough@2.2.0: - resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} - - ts-algebra@2.0.0: - resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} - - tsconfck@3.1.6: - resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} - engines: {node: ^18 || >=20} - hasBin: true - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - type-is@2.0.1: - resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} - engines: {node: '>= 0.6'} - - typed-query-selector@2.12.1: - resolution: {integrity: sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==} - - ufo@1.6.4: - resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} - - ultrahtml@1.6.0: - resolution: {integrity: sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==} - - uncrypto@0.1.3: - resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} - - undici-types@7.19.2: - resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} - - undici@7.24.8: - resolution: {integrity: sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==} - engines: {node: '>=20.18.1'} - - unenv@2.0.0-rc.24: - resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} - - unified@11.0.5: - resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} - - unifont@0.7.4: - resolution: {integrity: sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==} - - unist-util-find-after@5.0.0: - resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} - - unist-util-is@6.0.1: - resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} - - unist-util-modify-children@4.0.0: - resolution: {integrity: sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==} - - unist-util-position@5.0.0: - resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} - - unist-util-remove-position@5.0.0: - resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} - - unist-util-stringify-position@4.0.0: - resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} - - unist-util-visit-children@3.0.0: - resolution: {integrity: sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==} - - unist-util-visit-parents@6.0.2: - resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} - - unist-util-visit@5.1.0: - resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} - - unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - - unstorage@1.17.5: - resolution: {integrity: sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==} - peerDependencies: - '@azure/app-configuration': ^1.8.0 - '@azure/cosmos': ^4.2.0 - '@azure/data-tables': ^13.3.0 - '@azure/identity': ^4.6.0 - '@azure/keyvault-secrets': ^4.9.0 - '@azure/storage-blob': ^12.26.0 - '@capacitor/preferences': ^6 || ^7 || ^8 - '@deno/kv': '>=0.9.0' - '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 - '@planetscale/database': ^1.19.0 - '@upstash/redis': ^1.34.3 - '@vercel/blob': '>=0.27.1' - '@vercel/functions': ^2.2.12 || ^3.0.0 - '@vercel/kv': ^1 || ^2 || ^3 - aws4fetch: ^1.0.20 - db0: '>=0.2.1' - idb-keyval: ^6.2.1 - ioredis: ^5.4.2 - uploadthing: ^7.4.4 - peerDependenciesMeta: - '@azure/app-configuration': - optional: true - '@azure/cosmos': - optional: true - '@azure/data-tables': - optional: true - '@azure/identity': - optional: true - '@azure/keyvault-secrets': - optional: true - '@azure/storage-blob': - optional: true - '@capacitor/preferences': - optional: true - '@deno/kv': - optional: true - '@netlify/blobs': - optional: true - '@planetscale/database': - optional: true - '@upstash/redis': - optional: true - '@vercel/blob': - optional: true - '@vercel/functions': - optional: true - '@vercel/kv': - optional: true - aws4fetch: - optional: true - db0: - optional: true - idb-keyval: - optional: true - ioredis: - optional: true - uploadthing: - optional: true - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - - vfile-location@5.0.3: - resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} - - vfile-message@4.0.3: - resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} - - vfile@6.0.3: - resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - - vite@7.3.2: - resolution: {integrity: sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - jiti: '>=1.21.0' - less: ^4.0.0 - lightningcss: ^1.21.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vitefu@1.1.3: - resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} - peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - vite: - optional: true - - web-namespaces@2.0.1: - resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} - - web-streams-polyfill@3.3.3: - resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} - engines: {node: '>= 8'} - - webdriver-bidi-protocol@0.4.1: - resolution: {integrity: sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==} - - which-pm-runs@1.1.0: - resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} - engines: {node: '>=4'} - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - workerd@1.20260426.1: - resolution: {integrity: sha512-ELvGgN8c9oo+E6EPyecxk1TEf6/eAK4TxxQTW5mQ87C7jbjCzhMbg0P2ije49UBHV0dkBYPJcJvcklUltipl2A==} - engines: {node: '>=16'} - hasBin: true - - wrangler@4.86.0: - resolution: {integrity: sha512-9aa/gbF/HiUeeUEwyQpW5LDPBEzyt7iaE6xHwm0vk2Ly8A6J+jh03pzchqVnCCWR832mNyA28MD8oAYt0Kfvlw==} - engines: {node: '>=20.3.0'} - hasBin: true - peerDependencies: - '@cloudflare/workers-types': ^4.20260426.1 - peerDependenciesMeta: - '@cloudflare/workers-types': - optional: true - - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - ws@8.18.0: - resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - ws@8.20.0: - resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - xxhash-wasm@1.1.0: - resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} - - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - - yargs-parser@22.0.0: - resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} - engines: {node: ^20.19.0 || ^22.12.0 || >=23} - - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - - yauzl@2.10.0: - resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} - - yocto-queue@1.2.2: - resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} - engines: {node: '>=12.20'} - - youch-core@0.3.3: - resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} - - youch@4.1.0-beta.10: - resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} - - zip-stream@6.0.1: - resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} - engines: {node: '>= 14'} - - zod-to-json-schema@3.25.2: - resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} - peerDependencies: - zod: ^3.25.28 || ^4 - - zod@3.25.76: - resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - - zod@4.3.6: - resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} - - zwitch@2.0.4: - resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} - -snapshots: - - '@ai-sdk/anthropic@3.0.71(zod@4.3.6)': - dependencies: - '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.23(zod@4.3.6) - zod: 4.3.6 - - '@ai-sdk/gateway@3.0.104(zod@4.3.6)': - dependencies: - '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.23(zod@4.3.6) - '@vercel/oidc': 3.2.0 - zod: 4.3.6 - - '@ai-sdk/openai@3.0.53(zod@4.3.6)': - dependencies: - '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.23(zod@4.3.6) - zod: 4.3.6 - - '@ai-sdk/provider-utils@4.0.23(zod@4.3.6)': - dependencies: - '@ai-sdk/provider': 3.0.8 - '@standard-schema/spec': 1.1.0 - eventsource-parser: 3.0.8 - zod: 4.3.6 - - '@ai-sdk/provider@3.0.8': - dependencies: - json-schema: 0.4.0 - - '@anthropic-ai/claude-agent-sdk-darwin-arm64@0.2.122': - optional: true - - '@anthropic-ai/claude-agent-sdk-darwin-x64@0.2.122': - optional: true - - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.2.122': - optional: true - - '@anthropic-ai/claude-agent-sdk-linux-arm64@0.2.122': - optional: true - - '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.2.122': - optional: true - - '@anthropic-ai/claude-agent-sdk-linux-x64@0.2.122': - optional: true - - '@anthropic-ai/claude-agent-sdk-win32-arm64@0.2.122': - optional: true - - '@anthropic-ai/claude-agent-sdk-win32-x64@0.2.122': - optional: true - - '@anthropic-ai/claude-agent-sdk@0.2.122(zod@4.3.6)': - dependencies: - '@anthropic-ai/sdk': 0.81.0(zod@4.3.6) - '@modelcontextprotocol/sdk': 1.29.0(zod@4.3.6) - zod: 4.3.6 - optionalDependencies: - '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.2.122 - '@anthropic-ai/claude-agent-sdk-darwin-x64': 0.2.122 - '@anthropic-ai/claude-agent-sdk-linux-arm64': 0.2.122 - '@anthropic-ai/claude-agent-sdk-linux-arm64-musl': 0.2.122 - '@anthropic-ai/claude-agent-sdk-linux-x64': 0.2.122 - '@anthropic-ai/claude-agent-sdk-linux-x64-musl': 0.2.122 - '@anthropic-ai/claude-agent-sdk-win32-arm64': 0.2.122 - '@anthropic-ai/claude-agent-sdk-win32-x64': 0.2.122 - transitivePeerDependencies: - - '@cfworker/json-schema' - - supports-color - - '@anthropic-ai/sdk@0.81.0(zod@4.3.6)': - dependencies: - json-schema-to-ts: 3.1.1 - optionalDependencies: - zod: 4.3.6 - - '@anthropic-ai/sdk@0.91.1(zod@4.3.6)': - dependencies: - json-schema-to-ts: 3.1.1 - optionalDependencies: - zod: 4.3.6 - - '@astrojs/compiler@4.0.0': {} - - '@astrojs/internal-helpers@0.9.0': - dependencies: - picomatch: 4.0.4 - - '@astrojs/markdown-remark@7.1.1': - dependencies: - '@astrojs/internal-helpers': 0.9.0 - '@astrojs/prism': 4.0.1 - github-slugger: 2.0.0 - hast-util-from-html: 2.0.3 - hast-util-to-text: 4.0.2 - js-yaml: 4.1.1 - mdast-util-definitions: 6.0.0 - rehype-raw: 7.0.0 - rehype-stringify: 10.0.1 - remark-gfm: 4.0.1 - remark-parse: 11.0.0 - remark-rehype: 11.1.2 - remark-smartypants: 3.0.2 - retext-smartypants: 6.2.0 - shiki: 4.0.2 - smol-toml: 1.6.1 - unified: 11.0.5 - unist-util-remove-position: 5.0.0 - unist-util-visit: 5.1.0 - unist-util-visit-parents: 6.0.2 - vfile: 6.0.3 - transitivePeerDependencies: - - supports-color - - '@astrojs/prism@4.0.1': - dependencies: - prismjs: 1.30.0 - - '@astrojs/telemetry@3.3.1': - dependencies: - ci-info: 4.4.0 - dlv: 1.1.3 - dset: 3.1.4 - is-docker: 4.0.0 - is-wsl: 3.1.1 - which-pm-runs: 1.1.0 - - '@babel/code-frame@7.29.0': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 - optional: true - - '@babel/helper-string-parser@7.27.1': {} - - '@babel/helper-validator-identifier@7.28.5': {} - - '@babel/parser@7.29.3': - dependencies: - '@babel/types': 7.29.0 - - '@babel/runtime@7.29.2': {} - - '@babel/types@7.29.0': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 - - '@capsizecss/unpack@4.0.0': - dependencies: - fontkitten: 1.0.3 - - '@clack/core@1.3.0': - dependencies: - fast-wrap-ansi: 0.2.0 - sisteransi: 1.0.5 - - '@clack/prompts@1.3.0': - dependencies: - '@clack/core': 1.3.0 - fast-string-width: 3.0.2 - fast-wrap-ansi: 0.2.0 - sisteransi: 1.0.5 - - '@cloudflare/kv-asset-handler@0.4.2': {} - - '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260426.1)': - dependencies: - unenv: 2.0.0-rc.24 - optionalDependencies: - workerd: 1.20260426.1 - - '@cloudflare/workerd-darwin-64@1.20260426.1': - optional: true - - '@cloudflare/workerd-darwin-arm64@1.20260426.1': - optional: true - - '@cloudflare/workerd-linux-64@1.20260426.1': - optional: true - - '@cloudflare/workerd-linux-arm64@1.20260426.1': - optional: true - - '@cloudflare/workerd-windows-64@1.20260426.1': - optional: true - - '@cspotcode/source-map-support@0.8.1': - dependencies: - '@jridgewell/trace-mapping': 0.3.9 - - '@emnapi/runtime@1.9.2': - dependencies: - tslib: 2.8.1 - optional: true - - '@esbuild/aix-ppc64@0.27.3': - optional: true - - '@esbuild/android-arm64@0.27.3': - optional: true - - '@esbuild/android-arm@0.27.3': - optional: true - - '@esbuild/android-x64@0.27.3': - optional: true - - '@esbuild/darwin-arm64@0.27.3': - optional: true - - '@esbuild/darwin-x64@0.27.3': - optional: true - - '@esbuild/freebsd-arm64@0.27.3': - optional: true - - '@esbuild/freebsd-x64@0.27.3': - optional: true - - '@esbuild/linux-arm64@0.27.3': - optional: true - - '@esbuild/linux-arm@0.27.3': - optional: true - - '@esbuild/linux-ia32@0.27.3': - optional: true - - '@esbuild/linux-loong64@0.27.3': - optional: true - - '@esbuild/linux-mips64el@0.27.3': - optional: true - - '@esbuild/linux-ppc64@0.27.3': - optional: true - - '@esbuild/linux-riscv64@0.27.3': - optional: true - - '@esbuild/linux-s390x@0.27.3': - optional: true - - '@esbuild/linux-x64@0.27.3': - optional: true - - '@esbuild/netbsd-arm64@0.27.3': - optional: true - - '@esbuild/netbsd-x64@0.27.3': - optional: true - - '@esbuild/openbsd-arm64@0.27.3': - optional: true - - '@esbuild/openbsd-x64@0.27.3': - optional: true - - '@esbuild/openharmony-arm64@0.27.3': - optional: true - - '@esbuild/sunos-x64@0.27.3': - optional: true - - '@esbuild/win32-arm64@0.27.3': - optional: true - - '@esbuild/win32-ia32@0.27.3': - optional: true - - '@esbuild/win32-x64@0.27.3': - optional: true - - '@google/genai@1.50.1(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))': - dependencies: - google-auth-library: 10.6.2 - p-retry: 4.6.2 - protobufjs: 7.5.6 - ws: 8.20.0 - optionalDependencies: - '@modelcontextprotocol/sdk': 1.29.0(zod@4.3.6) - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - '@hono/node-server@1.19.14(hono@4.12.15)': - dependencies: - hono: 4.12.15 - - '@img/colour@1.1.0': {} - - '@img/sharp-darwin-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 - optional: true - - '@img/sharp-darwin-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 - optional: true - - '@img/sharp-libvips-darwin-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-darwin-x64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-arm@1.2.4': - optional: true - - '@img/sharp-libvips-linux-ppc64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-riscv64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-s390x@1.2.4': - optional: true - - '@img/sharp-libvips-linux-x64@1.2.4': - optional: true - - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - optional: true - - '@img/sharp-linux-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 - optional: true - - '@img/sharp-linux-arm@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 - optional: true - - '@img/sharp-linux-ppc64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 - optional: true - - '@img/sharp-linux-riscv64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 - optional: true - - '@img/sharp-linux-s390x@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 - optional: true - - '@img/sharp-linux-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 - optional: true - - '@img/sharp-linuxmusl-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - optional: true - - '@img/sharp-linuxmusl-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - optional: true - - '@img/sharp-wasm32@0.34.5': - dependencies: - '@emnapi/runtime': 1.9.2 - optional: true - - '@img/sharp-win32-arm64@0.34.5': - optional: true - - '@img/sharp-win32-ia32@0.34.5': - optional: true - - '@img/sharp-win32-x64@0.34.5': - optional: true - - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.2.0 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@jridgewell/trace-mapping@0.3.9': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@modelcontextprotocol/sdk@1.29.0(zod@4.3.6)': - dependencies: - '@hono/node-server': 1.19.14(hono@4.12.15) - ajv: 8.20.0 - ajv-formats: 3.0.1(ajv@8.20.0) - content-type: 1.0.5 - cors: 2.8.6 - cross-spawn: 7.0.6 - eventsource: 3.0.7 - eventsource-parser: 3.0.8 - express: 5.2.1 - express-rate-limit: 8.4.1(express@5.2.1) - hono: 4.12.15 - jose: 6.2.3 - json-schema-typed: 8.0.2 - pkce-challenge: 5.0.1 - raw-body: 3.0.2 - zod: 4.3.6 - zod-to-json-schema: 3.25.2(zod@4.3.6) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/api@1.9.0': {} - - '@oslojs/encoding@1.1.0': {} - - '@paper-design/shaders@0.0.76': {} - - '@pkgjs/parseargs@0.11.0': - optional: true - - '@poppinss/colors@4.1.6': - dependencies: - kleur: 4.1.5 - - '@poppinss/dumper@0.6.5': - dependencies: - '@poppinss/colors': 4.1.6 - '@sindresorhus/is': 7.2.0 - supports-color: 10.2.2 - - '@poppinss/exception@1.2.3': {} - - '@protobufjs/aspromise@1.1.2': {} - - '@protobufjs/base64@1.1.2': {} - - '@protobufjs/codegen@2.0.5': {} - - '@protobufjs/eventemitter@1.1.0': {} - - '@protobufjs/fetch@1.1.0': - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/inquire': 1.1.1 - - '@protobufjs/float@1.0.2': {} - - '@protobufjs/inquire@1.1.1': {} - - '@protobufjs/path@1.1.2': {} - - '@protobufjs/pool@1.1.0': {} - - '@protobufjs/utf8@1.1.1': {} - - '@puppeteer/browsers@2.13.0': - dependencies: - debug: 4.4.3 - extract-zip: 2.0.1 - progress: 2.0.3 - proxy-agent: 6.5.0 - semver: 7.7.4 - tar-fs: 3.1.2 - yargs: 17.7.2 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - react-native-b4a - - supports-color - optional: true - - '@rollup/pluginutils@5.3.0(rollup@4.60.2)': - dependencies: - '@types/estree': 1.0.8 - estree-walker: 2.0.2 - picomatch: 4.0.4 - optionalDependencies: - rollup: 4.60.2 - - '@rollup/rollup-android-arm-eabi@4.60.2': - optional: true - - '@rollup/rollup-android-arm64@4.60.2': - optional: true - - '@rollup/rollup-darwin-arm64@4.60.2': - optional: true - - '@rollup/rollup-darwin-x64@4.60.2': - optional: true - - '@rollup/rollup-freebsd-arm64@4.60.2': - optional: true - - '@rollup/rollup-freebsd-x64@4.60.2': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.60.2': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.60.2': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.60.2': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.60.2': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.60.2': - optional: true - - '@rollup/rollup-linux-loong64-musl@4.60.2': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.60.2': - optional: true - - '@rollup/rollup-linux-ppc64-musl@4.60.2': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.60.2': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.60.2': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.60.2': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.60.2': - optional: true - - '@rollup/rollup-linux-x64-musl@4.60.2': - optional: true - - '@rollup/rollup-openbsd-x64@4.60.2': - optional: true - - '@rollup/rollup-openharmony-arm64@4.60.2': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.60.2': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.60.2': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.60.2': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.60.2': - optional: true - - '@shikijs/core@4.0.2': - dependencies: - '@shikijs/primitive': 4.0.2 - '@shikijs/types': 4.0.2 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - hast-util-to-html: 9.0.5 - - '@shikijs/engine-javascript@4.0.2': - dependencies: - '@shikijs/types': 4.0.2 - '@shikijs/vscode-textmate': 10.0.2 - oniguruma-to-es: 4.3.6 - - '@shikijs/engine-oniguruma@4.0.2': - dependencies: - '@shikijs/types': 4.0.2 - '@shikijs/vscode-textmate': 10.0.2 - - '@shikijs/langs@4.0.2': - dependencies: - '@shikijs/types': 4.0.2 - - '@shikijs/primitive@4.0.2': - dependencies: - '@shikijs/types': 4.0.2 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - - '@shikijs/themes@4.0.2': - dependencies: - '@shikijs/types': 4.0.2 - - '@shikijs/types@4.0.2': - dependencies: - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - - '@shikijs/vscode-textmate@10.0.2': {} - - '@sindresorhus/is@7.2.0': {} - - '@speed-highlight/core@1.2.15': {} - - '@standard-schema/spec@1.1.0': {} - - '@tootallnate/quickjs-emscripten@0.23.0': - optional: true - - '@types/debug@4.1.13': - dependencies: - '@types/ms': 2.1.0 - - '@types/estree@1.0.8': {} - - '@types/hast@3.0.4': - dependencies: - '@types/unist': 3.0.3 - - '@types/mdast@4.0.4': - dependencies: - '@types/unist': 3.0.3 - - '@types/ms@2.1.0': {} - - '@types/nlcst@2.0.3': - dependencies: - '@types/unist': 3.0.3 - - '@types/node@25.6.0': - dependencies: - undici-types: 7.19.2 - - '@types/retry@0.12.0': {} - - '@types/unist@3.0.3': {} - - '@types/yauzl@2.10.3': - dependencies: - '@types/node': 25.6.0 - optional: true - - '@ungap/structured-clone@1.3.0': {} - - '@vercel/oidc@3.2.0': {} - - abort-controller@3.0.0: - dependencies: - event-target-shim: 5.0.1 - - accepts@2.0.0: - dependencies: - mime-types: 3.0.2 - negotiator: 1.0.0 - - agent-base@7.1.4: {} - - ai@6.0.168(zod@4.3.6): - dependencies: - '@ai-sdk/gateway': 3.0.104(zod@4.3.6) - '@ai-sdk/provider': 3.0.8 - '@ai-sdk/provider-utils': 4.0.23(zod@4.3.6) - '@opentelemetry/api': 1.9.0 - zod: 4.3.6 - - ajv-formats@3.0.1(ajv@8.20.0): - optionalDependencies: - ajv: 8.20.0 - - ajv@8.20.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.1.0 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - - ansi-regex@5.0.1: {} - - ansi-regex@6.2.2: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - ansi-styles@6.2.3: {} - - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.2 - - archiver-utils@5.0.2: - dependencies: - glob: 10.4.5 - graceful-fs: 4.2.11 - is-stream: 2.0.1 - lazystream: 1.0.1 - lodash: 4.18.1 - normalize-path: 3.0.0 - readable-stream: 4.7.0 - - archiver@7.0.1: - dependencies: - archiver-utils: 5.0.2 - async: 3.2.6 - buffer-crc32: 1.0.0 - readable-stream: 4.7.0 - readdir-glob: 1.1.3 - tar-stream: 3.1.8 - zip-stream: 6.0.1 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - react-native-b4a - - argparse@2.0.1: {} - - aria-query@5.3.2: {} - - array-iterate@2.0.1: {} - - ast-types@0.13.4: - dependencies: - tslib: 2.8.1 - optional: true - - astro@6.2.1(@types/node@25.6.0)(rollup@4.60.2): - dependencies: - '@astrojs/compiler': 4.0.0 - '@astrojs/internal-helpers': 0.9.0 - '@astrojs/markdown-remark': 7.1.1 - '@astrojs/telemetry': 3.3.1 - '@capsizecss/unpack': 4.0.0 - '@clack/prompts': 1.3.0 - '@oslojs/encoding': 1.1.0 - '@rollup/pluginutils': 5.3.0(rollup@4.60.2) - aria-query: 5.3.2 - axobject-query: 4.1.0 - ci-info: 4.4.0 - clsx: 2.1.1 - common-ancestor-path: 2.0.0 - cookie: 1.1.1 - devalue: 5.8.0 - diff: 8.0.4 - dset: 3.1.4 - es-module-lexer: 2.1.0 - esbuild: 0.27.3 - flattie: 1.1.1 - fontace: 0.4.1 - github-slugger: 2.0.0 - html-escaper: 3.0.3 - http-cache-semantics: 4.2.0 - js-yaml: 4.1.1 - magic-string: 0.30.21 - magicast: 0.5.2 - mrmime: 2.0.1 - neotraverse: 0.6.18 - obug: 2.1.1 - p-limit: 7.3.0 - p-queue: 9.2.0 - package-manager-detector: 1.6.0 - piccolore: 0.1.3 - picomatch: 4.0.4 - rehype: 13.0.2 - semver: 7.7.4 - shiki: 4.0.2 - smol-toml: 1.6.1 - svgo: 4.0.1 - tinyclip: 0.1.12 - tinyexec: 1.1.2 - tinyglobby: 0.2.16 - tsconfck: 3.1.6 - ultrahtml: 1.6.0 - unifont: 0.7.4 - unist-util-visit: 5.1.0 - unstorage: 1.17.5 - vfile: 6.0.3 - vite: 7.3.2(@types/node@25.6.0) - vitefu: 1.1.3(vite@7.3.2(@types/node@25.6.0)) - xxhash-wasm: 1.1.0 - yargs-parser: 22.0.0 - zod: 4.3.6 - optionalDependencies: - sharp: 0.34.5 - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@netlify/blobs' - - '@planetscale/database' - - '@types/node' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - aws4fetch - - db0 - - idb-keyval - - ioredis - - jiti - - less - - lightningcss - - rollup - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - typescript - - uploadthing - - yaml - - async@3.2.6: {} - - axobject-query@4.1.0: {} - - b4a@1.8.0: {} - - bail@2.0.2: {} - - balanced-match@1.0.2: {} - - bare-events@2.8.2: {} - - bare-fs@4.7.0: - dependencies: - bare-events: 2.8.2 - bare-path: 3.0.0 - bare-stream: 2.13.0(bare-events@2.8.2) - bare-url: 2.4.0 - fast-fifo: 1.3.2 - transitivePeerDependencies: - - bare-abort-controller - - react-native-b4a - - bare-os@3.8.7: {} - - bare-path@3.0.0: - dependencies: - bare-os: 3.8.7 - - bare-stream@2.13.0(bare-events@2.8.2): - dependencies: - streamx: 2.25.0 - teex: 1.0.1 - optionalDependencies: - bare-events: 2.8.2 - transitivePeerDependencies: - - react-native-b4a - - bare-url@2.4.0: - dependencies: - bare-path: 3.0.0 - - base64-js@1.5.1: {} - - basic-ftp@5.2.2: - optional: true - - bignumber.js@9.3.1: {} - - blake3-wasm@2.1.5: {} - - body-parser@2.2.2: - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 4.4.3 - http-errors: 2.0.1 - iconv-lite: 0.7.2 - on-finished: 2.4.1 - qs: 6.15.1 - raw-body: 3.0.2 - type-is: 2.0.1 - transitivePeerDependencies: - - supports-color - - boolbase@1.0.0: {} - - brace-expansion@2.1.0: - dependencies: - balanced-match: 1.0.2 - - buffer-crc32@0.2.13: - optional: true - - buffer-crc32@1.0.0: {} - - buffer-equal-constant-time@1.0.1: {} - - buffer@6.0.3: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - - bytes@3.1.2: {} - - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - - callsites@3.1.0: - optional: true - - ccount@2.0.1: {} - - character-entities-html4@2.1.0: {} - - character-entities-legacy@3.0.0: {} - - character-entities@2.0.2: {} - - chokidar@5.0.0: - dependencies: - readdirp: 5.0.0 - - chromium-bidi@14.0.0(devtools-protocol@0.0.1595872): - dependencies: - devtools-protocol: 0.0.1595872 - mitt: 3.0.1 - zod: 3.25.76 - optional: true - - ci-info@4.4.0: {} - - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - optional: true - - clsx@2.1.1: {} - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - comma-separated-tokens@2.0.3: {} - - commander@11.1.0: {} - - common-ancestor-path@2.0.0: {} - - compress-commons@6.0.2: - dependencies: - crc-32: 1.2.2 - crc32-stream: 6.0.0 - is-stream: 2.0.1 - normalize-path: 3.0.0 - readable-stream: 4.7.0 - - content-disposition@1.1.0: {} - - content-type@1.0.5: {} - - cookie-es@1.2.3: {} - - cookie-signature@1.2.2: {} - - cookie@0.7.2: {} - - cookie@1.1.1: {} - - core-util-is@1.0.3: {} - - cors@2.8.6: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - - cosmiconfig@9.0.1: - dependencies: - env-paths: 2.2.1 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - parse-json: 5.2.0 - optional: true - - crc-32@1.2.2: {} - - crc32-stream@6.0.0: - dependencies: - crc-32: 1.2.2 - readable-stream: 4.7.0 - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - crossws@0.3.5: - dependencies: - uncrypto: 0.1.3 - - css-select@5.2.2: - dependencies: - boolbase: 1.0.0 - css-what: 6.2.2 - domhandler: 5.0.3 - domutils: 3.2.2 - nth-check: 2.1.1 - - css-tree@2.2.1: - dependencies: - mdn-data: 2.0.28 - source-map-js: 1.2.1 - - css-tree@3.2.1: - dependencies: - mdn-data: 2.27.1 - source-map-js: 1.2.1 - - css-what@6.2.2: {} - - csso@5.0.5: - dependencies: - css-tree: 2.2.1 - - data-uri-to-buffer@4.0.1: {} - - data-uri-to-buffer@6.0.2: - optional: true - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - decode-named-character-reference@1.3.0: - dependencies: - character-entities: 2.0.2 - - defu@6.1.7: {} - - degenerator@5.0.1: - dependencies: - ast-types: 0.13.4 - escodegen: 2.1.0 - esprima: 4.0.1 - optional: true - - depd@2.0.0: {} - - dequal@2.0.3: {} - - destr@2.0.5: {} - - detect-libc@2.1.2: {} - - devalue@5.8.0: {} - - devlop@1.1.0: - dependencies: - dequal: 2.0.3 - - devtools-protocol@0.0.1595872: - optional: true - - diff@8.0.4: {} - - dlv@1.1.3: {} - - dom-serializer@2.0.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.5.0 - - domelementtype@2.3.0: {} - - domhandler@5.0.3: - dependencies: - domelementtype: 2.3.0 - - domutils@3.2.2: - dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - - dset@3.1.4: {} - - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - - eastasianwidth@0.2.0: {} - - ecdsa-sig-formatter@1.0.11: - dependencies: - safe-buffer: 5.2.1 - - ee-first@1.1.1: {} - - emoji-regex@8.0.0: {} - - emoji-regex@9.2.2: {} - - encodeurl@2.0.0: {} - - end-of-stream@1.4.5: - dependencies: - once: 1.4.0 - optional: true - - entities@4.5.0: {} - - entities@6.0.1: {} - - entities@7.0.1: {} - - env-paths@2.2.1: - optional: true - - error-ex@1.3.4: - dependencies: - is-arrayish: 0.2.1 - optional: true - - error-stack-parser-es@1.0.5: {} - - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - - es-module-lexer@2.1.0: {} - - es-object-atoms@1.1.1: - dependencies: - es-errors: 1.3.0 - - esbuild@0.27.3: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.3 - '@esbuild/android-arm': 0.27.3 - '@esbuild/android-arm64': 0.27.3 - '@esbuild/android-x64': 0.27.3 - '@esbuild/darwin-arm64': 0.27.3 - '@esbuild/darwin-x64': 0.27.3 - '@esbuild/freebsd-arm64': 0.27.3 - '@esbuild/freebsd-x64': 0.27.3 - '@esbuild/linux-arm': 0.27.3 - '@esbuild/linux-arm64': 0.27.3 - '@esbuild/linux-ia32': 0.27.3 - '@esbuild/linux-loong64': 0.27.3 - '@esbuild/linux-mips64el': 0.27.3 - '@esbuild/linux-ppc64': 0.27.3 - '@esbuild/linux-riscv64': 0.27.3 - '@esbuild/linux-s390x': 0.27.3 - '@esbuild/linux-x64': 0.27.3 - '@esbuild/netbsd-arm64': 0.27.3 - '@esbuild/netbsd-x64': 0.27.3 - '@esbuild/openbsd-arm64': 0.27.3 - '@esbuild/openbsd-x64': 0.27.3 - '@esbuild/openharmony-arm64': 0.27.3 - '@esbuild/sunos-x64': 0.27.3 - '@esbuild/win32-arm64': 0.27.3 - '@esbuild/win32-ia32': 0.27.3 - '@esbuild/win32-x64': 0.27.3 - - escalade@3.2.0: - optional: true - - escape-html@1.0.3: {} - - escape-string-regexp@5.0.0: {} - - escodegen@2.1.0: - dependencies: - esprima: 4.0.1 - estraverse: 5.3.0 - esutils: 2.0.3 - optionalDependencies: - source-map: 0.6.1 - optional: true - - esprima@4.0.1: - optional: true - - estraverse@5.3.0: - optional: true - - estree-walker@2.0.2: {} - - esutils@2.0.3: - optional: true - - etag@1.8.1: {} - - event-target-shim@5.0.1: {} - - eventemitter3@5.0.4: {} - - events-universal@1.0.1: - dependencies: - bare-events: 2.8.2 - transitivePeerDependencies: - - bare-abort-controller - - events@3.3.0: {} - - eventsource-parser@3.0.8: {} - - eventsource@3.0.7: - dependencies: - eventsource-parser: 3.0.8 - - express-rate-limit@8.4.1(express@5.2.1): - dependencies: - express: 5.2.1 - ip-address: 10.1.0 - - express@5.2.1: - dependencies: - accepts: 2.0.0 - body-parser: 2.2.2 - content-disposition: 1.1.0 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.2.2 - debug: 4.4.3 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 2.1.1 - fresh: 2.0.0 - http-errors: 2.0.1 - merge-descriptors: 2.0.0 - mime-types: 3.0.2 - on-finished: 2.4.1 - once: 1.4.0 - parseurl: 1.3.3 - proxy-addr: 2.0.7 - qs: 6.15.1 - range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.1 - serve-static: 2.2.1 - statuses: 2.0.2 - type-is: 2.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - - extend@3.0.2: {} - - extract-zip@2.0.1: - dependencies: - debug: 4.4.3 - get-stream: 5.2.0 - yauzl: 2.10.0 - optionalDependencies: - '@types/yauzl': 2.10.3 - transitivePeerDependencies: - - supports-color - optional: true - - fast-deep-equal@3.1.3: {} - - fast-fifo@1.3.2: {} - - fast-string-truncated-width@3.0.3: {} - - fast-string-width@3.0.2: - dependencies: - fast-string-truncated-width: 3.0.3 - - fast-uri@3.1.0: {} - - fast-wrap-ansi@0.2.0: - dependencies: - fast-string-width: 3.0.2 - - fd-slicer@1.1.0: - dependencies: - pend: 1.2.0 - optional: true - - fdir@6.5.0(picomatch@4.0.4): - optionalDependencies: - picomatch: 4.0.4 - - fetch-blob@3.2.0: - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 3.3.3 - - finalhandler@2.1.1: - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - - flattie@1.1.1: {} - - fontace@0.4.1: - dependencies: - fontkitten: 1.0.3 - - fontkitten@1.0.3: - dependencies: - tiny-inflate: 1.0.3 - - foreground-child@3.3.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - - formdata-polyfill@4.0.10: - dependencies: - fetch-blob: 3.2.0 - - forwarded@0.2.0: {} - - framer-motion@12.38.0: - dependencies: - motion-dom: 12.38.0 - motion-utils: 12.36.0 - tslib: 2.8.1 - - fresh@2.0.0: {} - - fsevents@2.3.2: - optional: true - - fsevents@2.3.3: - optional: true - - function-bind@1.1.2: {} - - gaxios@7.1.4: - dependencies: - extend: 3.0.2 - https-proxy-agent: 7.0.6 - node-fetch: 3.3.2 - transitivePeerDependencies: - - supports-color - - gcp-metadata@8.1.2: - dependencies: - gaxios: 7.1.4 - google-logging-utils: 1.1.3 - json-bigint: 1.0.0 - transitivePeerDependencies: - - supports-color - - get-caller-file@2.0.5: - optional: true - - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.3 - math-intrinsics: 1.1.0 - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - - get-stream@5.2.0: - dependencies: - pump: 3.0.4 - optional: true - - get-uri@6.0.5: - dependencies: - basic-ftp: 5.2.2 - data-uri-to-buffer: 6.0.2 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - optional: true - - github-slugger@2.0.0: {} - - glob@10.4.5: - dependencies: - foreground-child: 3.3.1 - jackspeak: 3.4.3 - minimatch: 9.0.9 - minipass: 7.1.3 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 - - google-auth-library@10.6.2: - dependencies: - base64-js: 1.5.1 - ecdsa-sig-formatter: 1.0.11 - gaxios: 7.1.4 - gcp-metadata: 8.1.2 - google-logging-utils: 1.1.3 - jws: 4.0.1 - transitivePeerDependencies: - - supports-color - - google-logging-utils@1.1.3: {} - - gopd@1.2.0: {} - - graceful-fs@4.2.11: {} - - h3@1.15.11: - dependencies: - cookie-es: 1.2.3 - crossws: 0.3.5 - defu: 6.1.7 - destr: 2.0.5 - iron-webcrypto: 1.2.1 - node-mock-http: 1.0.4 - radix3: 1.1.2 - ufo: 1.6.4 - uncrypto: 0.1.3 - - has-symbols@1.1.0: {} - - hasown@2.0.3: - dependencies: - function-bind: 1.1.2 - - hast-util-from-html@2.0.3: - dependencies: - '@types/hast': 3.0.4 - devlop: 1.1.0 - hast-util-from-parse5: 8.0.3 - parse5: 7.3.0 - vfile: 6.0.3 - vfile-message: 4.0.3 - - hast-util-from-parse5@8.0.3: - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.3 - devlop: 1.1.0 - hastscript: 9.0.1 - property-information: 7.1.0 - vfile: 6.0.3 - vfile-location: 5.0.3 - web-namespaces: 2.0.1 - - hast-util-is-element@3.0.0: - dependencies: - '@types/hast': 3.0.4 - - hast-util-parse-selector@4.0.0: - dependencies: - '@types/hast': 3.0.4 - - hast-util-raw@9.1.0: - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.3 - '@ungap/structured-clone': 1.3.0 - hast-util-from-parse5: 8.0.3 - hast-util-to-parse5: 8.0.1 - html-void-elements: 3.0.0 - mdast-util-to-hast: 13.2.1 - parse5: 7.3.0 - unist-util-position: 5.0.0 - unist-util-visit: 5.1.0 - vfile: 6.0.3 - web-namespaces: 2.0.1 - zwitch: 2.0.4 - - hast-util-to-html@9.0.5: - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.3 - ccount: 2.0.1 - comma-separated-tokens: 2.0.3 - hast-util-whitespace: 3.0.0 - html-void-elements: 3.0.0 - mdast-util-to-hast: 13.2.1 - property-information: 7.1.0 - space-separated-tokens: 2.0.2 - stringify-entities: 4.0.4 - zwitch: 2.0.4 - - hast-util-to-parse5@8.0.1: - dependencies: - '@types/hast': 3.0.4 - comma-separated-tokens: 2.0.3 - devlop: 1.1.0 - property-information: 7.1.0 - space-separated-tokens: 2.0.2 - web-namespaces: 2.0.1 - zwitch: 2.0.4 - - hast-util-to-text@4.0.2: - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.3 - hast-util-is-element: 3.0.0 - unist-util-find-after: 5.0.0 - - hast-util-whitespace@3.0.0: - dependencies: - '@types/hast': 3.0.4 - - hastscript@9.0.1: - dependencies: - '@types/hast': 3.0.4 - comma-separated-tokens: 2.0.3 - hast-util-parse-selector: 4.0.0 - property-information: 7.1.0 - space-separated-tokens: 2.0.2 - - hono@4.12.15: {} - - html-escaper@3.0.3: {} - - html-void-elements@3.0.0: {} - - htmlparser2@10.1.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.2.2 - entities: 7.0.1 - - http-cache-semantics@4.2.0: {} - - http-errors@2.0.1: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.2 - toidentifier: 1.0.1 - - http-proxy-agent@7.0.2: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - optional: true - - https-proxy-agent@7.0.6: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - iconv-lite@0.7.2: - dependencies: - safer-buffer: 2.1.2 - - ieee754@1.2.1: {} - - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - optional: true - - inherits@2.0.4: {} - - ip-address@10.1.0: {} - - ipaddr.js@1.9.1: {} - - iron-webcrypto@1.2.1: {} - - is-arrayish@0.2.1: - optional: true - - is-docker@3.0.0: {} - - is-docker@4.0.0: {} - - is-fullwidth-code-point@3.0.0: {} - - is-inside-container@1.0.0: - dependencies: - is-docker: 3.0.0 - - is-plain-obj@4.1.0: {} - - is-promise@4.0.0: {} - - is-stream@2.0.1: {} - - is-wsl@3.1.1: - dependencies: - is-inside-container: 1.0.0 - - isarray@1.0.0: {} - - isexe@2.0.0: {} - - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - - jose@6.2.3: {} - - js-tokens@4.0.0: - optional: true - - js-yaml@4.1.1: - dependencies: - argparse: 2.0.1 - - json-bigint@1.0.0: - dependencies: - bignumber.js: 9.3.1 - - json-parse-even-better-errors@2.3.1: - optional: true - - json-schema-to-ts@3.1.1: - dependencies: - '@babel/runtime': 7.29.2 - ts-algebra: 2.0.0 - - json-schema-traverse@1.0.0: {} - - json-schema-typed@8.0.2: {} - - json-schema@0.4.0: {} - - jwa@2.0.1: - dependencies: - buffer-equal-constant-time: 1.0.1 - ecdsa-sig-formatter: 1.0.11 - safe-buffer: 5.2.1 - - jws@4.0.1: - dependencies: - jwa: 2.0.1 - safe-buffer: 5.2.1 - - kleur@4.1.5: {} - - lazystream@1.0.1: - dependencies: - readable-stream: 2.3.8 - - lines-and-columns@1.2.4: - optional: true - - lodash@4.18.1: {} - - long@5.3.2: {} - - longest-streak@3.1.0: {} - - lru-cache@10.4.3: {} - - lru-cache@11.3.3: {} - - lru-cache@7.18.3: - optional: true - - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - magicast@0.5.2: - dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 - source-map-js: 1.2.1 - - markdown-table@3.0.4: {} - - marked@16.4.2: {} - - math-intrinsics@1.1.0: {} - - mdast-util-definitions@6.0.0: - dependencies: - '@types/mdast': 4.0.4 - '@types/unist': 3.0.3 - unist-util-visit: 5.1.0 - - mdast-util-find-and-replace@3.0.2: - dependencies: - '@types/mdast': 4.0.4 - escape-string-regexp: 5.0.0 - unist-util-is: 6.0.1 - unist-util-visit-parents: 6.0.2 - - mdast-util-from-markdown@2.0.3: - dependencies: - '@types/mdast': 4.0.4 - '@types/unist': 3.0.3 - decode-named-character-reference: 1.3.0 - devlop: 1.1.0 - mdast-util-to-string: 4.0.0 - micromark: 4.0.2 - micromark-util-decode-numeric-character-reference: 2.0.2 - micromark-util-decode-string: 2.0.1 - micromark-util-normalize-identifier: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - unist-util-stringify-position: 4.0.0 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-autolink-literal@2.0.1: - dependencies: - '@types/mdast': 4.0.4 - ccount: 2.0.1 - devlop: 1.1.0 - mdast-util-find-and-replace: 3.0.2 - micromark-util-character: 2.1.1 - - mdast-util-gfm-footnote@2.1.0: - dependencies: - '@types/mdast': 4.0.4 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - micromark-util-normalize-identifier: 2.0.1 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-strikethrough@2.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-table@2.0.0: - dependencies: - '@types/mdast': 4.0.4 - devlop: 1.1.0 - markdown-table: 3.0.4 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-task-list-item@2.0.0: - dependencies: - '@types/mdast': 4.0.4 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm@3.1.0: - dependencies: - mdast-util-from-markdown: 2.0.3 - mdast-util-gfm-autolink-literal: 2.0.1 - mdast-util-gfm-footnote: 2.1.0 - mdast-util-gfm-strikethrough: 2.0.0 - mdast-util-gfm-table: 2.0.0 - mdast-util-gfm-task-list-item: 2.0.0 - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-phrasing@4.1.0: - dependencies: - '@types/mdast': 4.0.4 - unist-util-is: 6.0.1 - - mdast-util-to-hast@13.2.1: - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - '@ungap/structured-clone': 1.3.0 - devlop: 1.1.0 - micromark-util-sanitize-uri: 2.0.1 - trim-lines: 3.0.1 - unist-util-position: 5.0.0 - unist-util-visit: 5.1.0 - vfile: 6.0.3 - - mdast-util-to-markdown@2.1.2: - dependencies: - '@types/mdast': 4.0.4 - '@types/unist': 3.0.3 - longest-streak: 3.1.0 - mdast-util-phrasing: 4.1.0 - mdast-util-to-string: 4.0.0 - micromark-util-classify-character: 2.0.1 - micromark-util-decode-string: 2.0.1 - unist-util-visit: 5.1.0 - zwitch: 2.0.4 - - mdast-util-to-string@4.0.0: - dependencies: - '@types/mdast': 4.0.4 - - mdn-data@2.0.28: {} - - mdn-data@2.27.1: {} - - media-typer@1.1.0: {} - - merge-descriptors@2.0.0: {} - - micromark-core-commonmark@2.0.3: - dependencies: - decode-named-character-reference: 1.3.0 - devlop: 1.1.0 - micromark-factory-destination: 2.0.1 - micromark-factory-label: 2.0.1 - micromark-factory-space: 2.0.1 - micromark-factory-title: 2.0.1 - micromark-factory-whitespace: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-chunked: 2.0.1 - micromark-util-classify-character: 2.0.1 - micromark-util-html-tag-name: 2.0.1 - micromark-util-normalize-identifier: 2.0.1 - micromark-util-resolve-all: 2.0.1 - micromark-util-subtokenize: 2.1.0 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm-autolink-literal@2.1.0: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-sanitize-uri: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm-footnote@2.1.0: - dependencies: - devlop: 1.1.0 - micromark-core-commonmark: 2.0.3 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-normalize-identifier: 2.0.1 - micromark-util-sanitize-uri: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm-strikethrough@2.1.0: - dependencies: - devlop: 1.1.0 - micromark-util-chunked: 2.0.1 - micromark-util-classify-character: 2.0.1 - micromark-util-resolve-all: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm-table@2.1.1: - dependencies: - devlop: 1.1.0 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm-tagfilter@2.0.0: - dependencies: - micromark-util-types: 2.0.2 - - micromark-extension-gfm-task-list-item@2.1.0: - dependencies: - devlop: 1.1.0 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm@3.0.0: - dependencies: - micromark-extension-gfm-autolink-literal: 2.1.0 - micromark-extension-gfm-footnote: 2.1.0 - micromark-extension-gfm-strikethrough: 2.1.0 - micromark-extension-gfm-table: 2.1.1 - micromark-extension-gfm-tagfilter: 2.0.0 - micromark-extension-gfm-task-list-item: 2.1.0 - micromark-util-combine-extensions: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-destination@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-label@2.0.1: - dependencies: - devlop: 1.1.0 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-space@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-types: 2.0.2 - - micromark-factory-title@2.0.1: - dependencies: - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-factory-whitespace@2.0.1: - dependencies: - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-character@2.1.1: - dependencies: - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-chunked@2.0.1: - dependencies: - micromark-util-symbol: 2.0.1 - - micromark-util-classify-character@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-combine-extensions@2.0.1: - dependencies: - micromark-util-chunked: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-decode-numeric-character-reference@2.0.2: - dependencies: - micromark-util-symbol: 2.0.1 - - micromark-util-decode-string@2.0.1: - dependencies: - decode-named-character-reference: 1.3.0 - micromark-util-character: 2.1.1 - micromark-util-decode-numeric-character-reference: 2.0.2 - micromark-util-symbol: 2.0.1 - - micromark-util-encode@2.0.1: {} - - micromark-util-html-tag-name@2.0.1: {} - - micromark-util-normalize-identifier@2.0.1: - dependencies: - micromark-util-symbol: 2.0.1 - - micromark-util-resolve-all@2.0.1: - dependencies: - micromark-util-types: 2.0.2 - - micromark-util-sanitize-uri@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-encode: 2.0.1 - micromark-util-symbol: 2.0.1 - - micromark-util-subtokenize@2.1.0: - dependencies: - devlop: 1.1.0 - micromark-util-chunked: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-symbol@2.0.1: {} - - micromark-util-types@2.0.2: {} - - micromark@4.0.2: - dependencies: - '@types/debug': 4.1.13 - debug: 4.4.3 - decode-named-character-reference: 1.3.0 - devlop: 1.1.0 - micromark-core-commonmark: 2.0.3 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-chunked: 2.0.1 - micromark-util-combine-extensions: 2.0.1 - micromark-util-decode-numeric-character-reference: 2.0.2 - micromark-util-encode: 2.0.1 - micromark-util-normalize-identifier: 2.0.1 - micromark-util-resolve-all: 2.0.1 - micromark-util-sanitize-uri: 2.0.1 - micromark-util-subtokenize: 2.1.0 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - transitivePeerDependencies: - - supports-color - - mime-db@1.54.0: {} - - mime-types@3.0.2: - dependencies: - mime-db: 1.54.0 - - miniflare@4.20260426.0: - dependencies: - '@cspotcode/source-map-support': 0.8.1 - sharp: 0.34.5 - undici: 7.24.8 - workerd: 1.20260426.1 - ws: 8.18.0 - youch: 4.1.0-beta.10 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - minimatch@5.1.9: - dependencies: - brace-expansion: 2.1.0 - - minimatch@9.0.9: - dependencies: - brace-expansion: 2.1.0 - - minipass@7.1.3: {} - - mitt@3.0.1: - optional: true - - modern-screenshot@4.7.0: {} - - motion-dom@12.38.0: - dependencies: - motion-utils: 12.36.0 - - motion-utils@12.36.0: {} - - motion@12.38.0: - dependencies: - framer-motion: 12.38.0 - tslib: 2.8.1 - - mrmime@2.0.1: {} - - ms@2.1.3: {} - - nanoid@3.3.12: {} - - negotiator@1.0.0: {} - - neotraverse@0.6.18: {} - - netmask@2.1.1: - optional: true - - nlcst-to-string@4.0.0: - dependencies: - '@types/nlcst': 2.0.3 - - node-domexception@1.0.0: {} - - node-fetch-native@1.6.7: {} - - node-fetch@3.3.2: - dependencies: - data-uri-to-buffer: 4.0.1 - fetch-blob: 3.2.0 - formdata-polyfill: 4.0.10 - - node-mock-http@1.0.4: {} - - normalize-path@3.0.0: {} - - nth-check@2.1.1: - dependencies: - boolbase: 1.0.0 - - object-assign@4.1.1: {} - - object-inspect@1.13.4: {} - - obug@2.1.1: {} - - ofetch@1.5.1: - dependencies: - destr: 2.0.5 - node-fetch-native: 1.6.7 - ufo: 1.6.4 - - ohash@2.0.11: {} - - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - oniguruma-parser@0.12.2: {} - - oniguruma-to-es@4.3.6: - dependencies: - oniguruma-parser: 0.12.2 - regex: 6.1.0 - regex-recursion: 6.0.2 - - p-limit@7.3.0: - dependencies: - yocto-queue: 1.2.2 - - p-queue@9.2.0: - dependencies: - eventemitter3: 5.0.4 - p-timeout: 7.0.1 - - p-retry@4.6.2: - dependencies: - '@types/retry': 0.12.0 - retry: 0.13.1 - - p-timeout@7.0.1: {} - - pac-proxy-agent@7.2.0: - dependencies: - '@tootallnate/quickjs-emscripten': 0.23.0 - agent-base: 7.1.4 - debug: 4.4.3 - get-uri: 6.0.5 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 - pac-resolver: 7.0.1 - socks-proxy-agent: 8.0.5 - transitivePeerDependencies: - - supports-color - optional: true - - pac-resolver@7.0.1: - dependencies: - degenerator: 5.0.1 - netmask: 2.1.1 - optional: true - - package-json-from-dist@1.0.1: {} - - package-manager-detector@1.6.0: {} - - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - optional: true - - parse-json@5.2.0: - dependencies: - '@babel/code-frame': 7.29.0 - error-ex: 1.3.4 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - optional: true - - parse-latin@7.0.0: - dependencies: - '@types/nlcst': 2.0.3 - '@types/unist': 3.0.3 - nlcst-to-string: 4.0.0 - unist-util-modify-children: 4.0.0 - unist-util-visit-children: 3.0.0 - vfile: 6.0.3 - - parse5@7.3.0: - dependencies: - entities: 6.0.1 - - parseurl@1.3.3: {} - - path-key@3.1.1: {} - - path-scurry@1.11.1: - dependencies: - lru-cache: 10.4.3 - minipass: 7.1.3 - - path-to-regexp@6.3.0: {} - - path-to-regexp@8.4.2: {} - - pathe@2.0.3: {} - - pend@1.2.0: - optional: true - - piccolore@0.1.3: {} - - picocolors@1.1.1: {} - - picomatch@2.3.2: {} - - picomatch@4.0.4: {} - - pkce-challenge@5.0.1: {} - - playwright-core@1.59.1: {} - - playwright@1.59.1: - dependencies: - playwright-core: 1.59.1 - optionalDependencies: - fsevents: 2.3.2 - - postcss@8.5.13: - dependencies: - nanoid: 3.3.12 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - prismjs@1.30.0: {} - - process-nextick-args@2.0.1: {} - - process@0.11.10: {} - - progress@2.0.3: - optional: true - - property-information@7.1.0: {} - - protobufjs@7.5.6: - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/base64': 1.1.2 - '@protobufjs/codegen': 2.0.5 - '@protobufjs/eventemitter': 1.1.0 - '@protobufjs/fetch': 1.1.0 - '@protobufjs/float': 1.0.2 - '@protobufjs/inquire': 1.1.1 - '@protobufjs/path': 1.1.2 - '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.1 - '@types/node': 25.6.0 - long: 5.3.2 - - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - - proxy-agent@6.5.0: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 - lru-cache: 7.18.3 - pac-proxy-agent: 7.2.0 - proxy-from-env: 1.1.0 - socks-proxy-agent: 8.0.5 - transitivePeerDependencies: - - supports-color - optional: true - - proxy-from-env@1.1.0: - optional: true - - pump@3.0.4: - dependencies: - end-of-stream: 1.4.5 - once: 1.4.0 - optional: true - - puppeteer-core@24.42.0: - dependencies: - '@puppeteer/browsers': 2.13.0 - chromium-bidi: 14.0.0(devtools-protocol@0.0.1595872) - debug: 4.4.3 - devtools-protocol: 0.0.1595872 - typed-query-selector: 2.12.1 - webdriver-bidi-protocol: 0.4.1 - ws: 8.20.0 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - bufferutil - - react-native-b4a - - supports-color - - utf-8-validate - optional: true - - puppeteer@24.42.0: - dependencies: - '@puppeteer/browsers': 2.13.0 - chromium-bidi: 14.0.0(devtools-protocol@0.0.1595872) - cosmiconfig: 9.0.1 - devtools-protocol: 0.0.1595872 - puppeteer-core: 24.42.0 - typed-query-selector: 2.12.1 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - bufferutil - - react-native-b4a - - supports-color - - typescript - - utf-8-validate - optional: true - - qs@6.15.1: - dependencies: - side-channel: 1.1.0 - - radix3@1.1.2: {} - - range-parser@1.2.1: {} - - raw-body@3.0.2: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.7.2 - unpipe: 1.0.0 - - readable-stream@2.3.8: - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 1.0.0 - process-nextick-args: 2.0.1 - safe-buffer: 5.1.2 - string_decoder: 1.1.1 - util-deprecate: 1.0.2 - - readable-stream@4.7.0: - dependencies: - abort-controller: 3.0.0 - buffer: 6.0.3 - events: 3.3.0 - process: 0.11.10 - string_decoder: 1.3.0 - - readdir-glob@1.1.3: - dependencies: - minimatch: 5.1.9 - - readdirp@5.0.0: {} - - regex-recursion@6.0.2: - dependencies: - regex-utilities: 2.3.0 - - regex-utilities@2.3.0: {} - - regex@6.1.0: - dependencies: - regex-utilities: 2.3.0 - - rehype-parse@9.0.1: - dependencies: - '@types/hast': 3.0.4 - hast-util-from-html: 2.0.3 - unified: 11.0.5 - - rehype-raw@7.0.0: - dependencies: - '@types/hast': 3.0.4 - hast-util-raw: 9.1.0 - vfile: 6.0.3 - - rehype-stringify@10.0.1: - dependencies: - '@types/hast': 3.0.4 - hast-util-to-html: 9.0.5 - unified: 11.0.5 - - rehype@13.0.2: - dependencies: - '@types/hast': 3.0.4 - rehype-parse: 9.0.1 - rehype-stringify: 10.0.1 - unified: 11.0.5 - - remark-gfm@4.0.1: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-gfm: 3.1.0 - micromark-extension-gfm: 3.0.0 - remark-parse: 11.0.0 - remark-stringify: 11.0.0 - unified: 11.0.5 - transitivePeerDependencies: - - supports-color - - remark-parse@11.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.3 - micromark-util-types: 2.0.2 - unified: 11.0.5 - transitivePeerDependencies: - - supports-color - - remark-rehype@11.1.2: - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - mdast-util-to-hast: 13.2.1 - unified: 11.0.5 - vfile: 6.0.3 - - remark-smartypants@3.0.2: - dependencies: - retext: 9.0.0 - retext-smartypants: 6.2.0 - unified: 11.0.5 - unist-util-visit: 5.1.0 - - remark-stringify@11.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-to-markdown: 2.1.2 - unified: 11.0.5 - - require-directory@2.1.1: - optional: true - - require-from-string@2.0.2: {} - - resolve-from@4.0.0: - optional: true - - retext-latin@4.0.0: - dependencies: - '@types/nlcst': 2.0.3 - parse-latin: 7.0.0 - unified: 11.0.5 - - retext-smartypants@6.2.0: - dependencies: - '@types/nlcst': 2.0.3 - nlcst-to-string: 4.0.0 - unist-util-visit: 5.1.0 - - retext-stringify@4.0.0: - dependencies: - '@types/nlcst': 2.0.3 - nlcst-to-string: 4.0.0 - unified: 11.0.5 - - retext@9.0.0: - dependencies: - '@types/nlcst': 2.0.3 - retext-latin: 4.0.0 - retext-stringify: 4.0.0 - unified: 11.0.5 - - retry@0.13.1: {} - - rollup@4.60.2: - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.60.2 - '@rollup/rollup-android-arm64': 4.60.2 - '@rollup/rollup-darwin-arm64': 4.60.2 - '@rollup/rollup-darwin-x64': 4.60.2 - '@rollup/rollup-freebsd-arm64': 4.60.2 - '@rollup/rollup-freebsd-x64': 4.60.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.60.2 - '@rollup/rollup-linux-arm-musleabihf': 4.60.2 - '@rollup/rollup-linux-arm64-gnu': 4.60.2 - '@rollup/rollup-linux-arm64-musl': 4.60.2 - '@rollup/rollup-linux-loong64-gnu': 4.60.2 - '@rollup/rollup-linux-loong64-musl': 4.60.2 - '@rollup/rollup-linux-ppc64-gnu': 4.60.2 - '@rollup/rollup-linux-ppc64-musl': 4.60.2 - '@rollup/rollup-linux-riscv64-gnu': 4.60.2 - '@rollup/rollup-linux-riscv64-musl': 4.60.2 - '@rollup/rollup-linux-s390x-gnu': 4.60.2 - '@rollup/rollup-linux-x64-gnu': 4.60.2 - '@rollup/rollup-linux-x64-musl': 4.60.2 - '@rollup/rollup-openbsd-x64': 4.60.2 - '@rollup/rollup-openharmony-arm64': 4.60.2 - '@rollup/rollup-win32-arm64-msvc': 4.60.2 - '@rollup/rollup-win32-ia32-msvc': 4.60.2 - '@rollup/rollup-win32-x64-gnu': 4.60.2 - '@rollup/rollup-win32-x64-msvc': 4.60.2 - fsevents: 2.3.3 - - router@2.2.0: - dependencies: - debug: 4.4.3 - depd: 2.0.0 - is-promise: 4.0.0 - parseurl: 1.3.3 - path-to-regexp: 8.4.2 - transitivePeerDependencies: - - supports-color - - safe-buffer@5.1.2: {} - - safe-buffer@5.2.1: {} - - safer-buffer@2.1.2: {} - - sax@1.6.0: {} - - semver@7.7.4: {} - - send@1.2.1: - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 2.0.0 - http-errors: 2.0.1 - mime-types: 3.0.2 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - - serve-static@2.2.1: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 1.2.1 - transitivePeerDependencies: - - supports-color - - setprototypeof@1.2.0: {} - - sharp@0.34.5: - dependencies: - '@img/colour': 1.1.0 - detect-libc: 2.1.2 - semver: 7.7.4 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.5 - '@img/sharp-darwin-x64': 0.34.5 - '@img/sharp-libvips-darwin-arm64': 1.2.4 - '@img/sharp-libvips-darwin-x64': 1.2.4 - '@img/sharp-libvips-linux-arm': 1.2.4 - '@img/sharp-libvips-linux-arm64': 1.2.4 - '@img/sharp-libvips-linux-ppc64': 1.2.4 - '@img/sharp-libvips-linux-riscv64': 1.2.4 - '@img/sharp-libvips-linux-s390x': 1.2.4 - '@img/sharp-libvips-linux-x64': 1.2.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - '@img/sharp-linux-arm': 0.34.5 - '@img/sharp-linux-arm64': 0.34.5 - '@img/sharp-linux-ppc64': 0.34.5 - '@img/sharp-linux-riscv64': 0.34.5 - '@img/sharp-linux-s390x': 0.34.5 - '@img/sharp-linux-x64': 0.34.5 - '@img/sharp-linuxmusl-arm64': 0.34.5 - '@img/sharp-linuxmusl-x64': 0.34.5 - '@img/sharp-wasm32': 0.34.5 - '@img/sharp-win32-arm64': 0.34.5 - '@img/sharp-win32-ia32': 0.34.5 - '@img/sharp-win32-x64': 0.34.5 - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - shiki@4.0.2: - dependencies: - '@shikijs/core': 4.0.2 - '@shikijs/engine-javascript': 4.0.2 - '@shikijs/engine-oniguruma': 4.0.2 - '@shikijs/langs': 4.0.2 - '@shikijs/themes': 4.0.2 - '@shikijs/types': 4.0.2 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - - side-channel-list@1.0.1: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - - side-channel@1.1.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.1 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - - signal-exit@4.1.0: {} - - sisteransi@1.0.5: {} - - smart-buffer@4.2.0: - optional: true - - smol-toml@1.6.1: {} - - socks-proxy-agent@8.0.5: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - socks: 2.8.7 - transitivePeerDependencies: - - supports-color - optional: true - - socks@2.8.7: - dependencies: - ip-address: 10.1.0 - smart-buffer: 4.2.0 - optional: true - - source-map-js@1.2.1: {} - - source-map@0.6.1: - optional: true - - space-separated-tokens@2.0.2: {} - - statuses@2.0.2: {} - - streamx@2.25.0: - dependencies: - events-universal: 1.0.1 - fast-fifo: 1.3.2 - text-decoder: 1.2.7 - transitivePeerDependencies: - - bare-abort-controller - - react-native-b4a - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.2.0 - - string_decoder@1.1.1: - dependencies: - safe-buffer: 5.1.2 - - string_decoder@1.3.0: - dependencies: - safe-buffer: 5.2.1 - - stringify-entities@4.0.4: - dependencies: - character-entities-html4: 2.1.0 - character-entities-legacy: 3.0.0 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-ansi@7.2.0: - dependencies: - ansi-regex: 6.2.2 - - supports-color@10.2.2: {} - - svgo@4.0.1: - dependencies: - commander: 11.1.0 - css-select: 5.2.2 - css-tree: 3.2.1 - css-what: 6.2.2 - csso: 5.0.5 - picocolors: 1.1.1 - sax: 1.6.0 - - tar-fs@3.1.2: - dependencies: - pump: 3.0.4 - tar-stream: 3.1.8 - optionalDependencies: - bare-fs: 4.7.0 - bare-path: 3.0.0 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - react-native-b4a - optional: true - - tar-stream@3.1.8: - dependencies: - b4a: 1.8.0 - bare-fs: 4.7.0 - fast-fifo: 1.3.2 - streamx: 2.25.0 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - react-native-b4a - - teex@1.0.1: - dependencies: - streamx: 2.25.0 - transitivePeerDependencies: - - bare-abort-controller - - react-native-b4a - - text-decoder@1.2.7: - dependencies: - b4a: 1.8.0 - transitivePeerDependencies: - - react-native-b4a - - tiny-inflate@1.0.3: {} - - tinyclip@0.1.12: {} - - tinyexec@1.1.2: {} - - tinyglobby@0.2.16: - dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - - toidentifier@1.0.1: {} - - trim-lines@3.0.1: {} - - trough@2.2.0: {} - - ts-algebra@2.0.0: {} - - tsconfck@3.1.6: {} - - tslib@2.8.1: {} - - type-is@2.0.1: - dependencies: - content-type: 1.0.5 - media-typer: 1.1.0 - mime-types: 3.0.2 - - typed-query-selector@2.12.1: - optional: true - - ufo@1.6.4: {} - - ultrahtml@1.6.0: {} - - uncrypto@0.1.3: {} - - undici-types@7.19.2: {} - - undici@7.24.8: {} - - unenv@2.0.0-rc.24: - dependencies: - pathe: 2.0.3 - - unified@11.0.5: - dependencies: - '@types/unist': 3.0.3 - bail: 2.0.2 - devlop: 1.1.0 - extend: 3.0.2 - is-plain-obj: 4.1.0 - trough: 2.2.0 - vfile: 6.0.3 - - unifont@0.7.4: - dependencies: - css-tree: 3.2.1 - ofetch: 1.5.1 - ohash: 2.0.11 - - unist-util-find-after@5.0.0: - dependencies: - '@types/unist': 3.0.3 - unist-util-is: 6.0.1 - - unist-util-is@6.0.1: - dependencies: - '@types/unist': 3.0.3 - - unist-util-modify-children@4.0.0: - dependencies: - '@types/unist': 3.0.3 - array-iterate: 2.0.1 - - unist-util-position@5.0.0: - dependencies: - '@types/unist': 3.0.3 - - unist-util-remove-position@5.0.0: - dependencies: - '@types/unist': 3.0.3 - unist-util-visit: 5.1.0 - - unist-util-stringify-position@4.0.0: - dependencies: - '@types/unist': 3.0.3 - - unist-util-visit-children@3.0.0: - dependencies: - '@types/unist': 3.0.3 - - unist-util-visit-parents@6.0.2: - dependencies: - '@types/unist': 3.0.3 - unist-util-is: 6.0.1 - - unist-util-visit@5.1.0: - dependencies: - '@types/unist': 3.0.3 - unist-util-is: 6.0.1 - unist-util-visit-parents: 6.0.2 - - unpipe@1.0.0: {} - - unstorage@1.17.5: - dependencies: - anymatch: 3.1.3 - chokidar: 5.0.0 - destr: 2.0.5 - h3: 1.15.11 - lru-cache: 11.3.3 - node-fetch-native: 1.6.7 - ofetch: 1.5.1 - ufo: 1.6.4 - - util-deprecate@1.0.2: {} - - vary@1.1.2: {} - - vfile-location@5.0.3: - dependencies: - '@types/unist': 3.0.3 - vfile: 6.0.3 - - vfile-message@4.0.3: - dependencies: - '@types/unist': 3.0.3 - unist-util-stringify-position: 4.0.0 - - vfile@6.0.3: - dependencies: - '@types/unist': 3.0.3 - vfile-message: 4.0.3 - - vite@7.3.2(@types/node@25.6.0): - dependencies: - esbuild: 0.27.3 - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - postcss: 8.5.13 - rollup: 4.60.2 - tinyglobby: 0.2.16 - optionalDependencies: - '@types/node': 25.6.0 - fsevents: 2.3.3 - - vitefu@1.1.3(vite@7.3.2(@types/node@25.6.0)): - optionalDependencies: - vite: 7.3.2(@types/node@25.6.0) - - web-namespaces@2.0.1: {} - - web-streams-polyfill@3.3.3: {} - - webdriver-bidi-protocol@0.4.1: - optional: true - - which-pm-runs@1.1.0: {} - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - workerd@1.20260426.1: - optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20260426.1 - '@cloudflare/workerd-darwin-arm64': 1.20260426.1 - '@cloudflare/workerd-linux-64': 1.20260426.1 - '@cloudflare/workerd-linux-arm64': 1.20260426.1 - '@cloudflare/workerd-windows-64': 1.20260426.1 - - wrangler@4.86.0: - dependencies: - '@cloudflare/kv-asset-handler': 0.4.2 - '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260426.1) - blake3-wasm: 2.1.5 - esbuild: 0.27.3 - miniflare: 4.20260426.0 - path-to-regexp: 6.3.0 - unenv: 2.0.0-rc.24 - workerd: 1.20260426.1 - optionalDependencies: - fsevents: 2.3.3 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.3 - string-width: 5.1.2 - strip-ansi: 7.2.0 - - wrappy@1.0.2: {} - - ws@8.18.0: {} - - ws@8.20.0: {} - - xxhash-wasm@1.1.0: {} - - y18n@5.0.8: - optional: true - - yargs-parser@21.1.1: - optional: true - - yargs-parser@22.0.0: {} - - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - optional: true - - yauzl@2.10.0: - dependencies: - buffer-crc32: 0.2.13 - fd-slicer: 1.1.0 - optional: true - - yocto-queue@1.2.2: {} - - youch-core@0.3.3: - dependencies: - '@poppinss/exception': 1.2.3 - error-stack-parser-es: 1.0.5 - - youch@4.1.0-beta.10: - dependencies: - '@poppinss/colors': 4.1.6 - '@poppinss/dumper': 0.6.5 - '@speed-highlight/core': 1.2.15 - cookie: 1.1.1 - youch-core: 0.3.3 - - zip-stream@6.0.1: - dependencies: - archiver-utils: 5.0.2 - compress-commons: 6.0.2 - readable-stream: 4.7.0 - - zod-to-json-schema@3.25.2(zod@4.3.6): - dependencies: - zod: 4.3.6 - - zod@3.25.76: - optional: true - - zod@4.3.6: {} - - zwitch@2.0.4: {} diff --git a/scripts/build.js b/scripts/build.js index 4934ed03d..e1686b720 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -149,6 +149,10 @@ function validateProse(rootDir) { 'README.npm.md', ]; const extensions = new Set(['.html', '.md', '.js', '.mjs', '.css', '.astro']); + // The slop catalog documents every antipattern by example, so it must + // contain em dashes, buzzwords, and the rest as specimens. Exempt it from + // the prose gate: its job is to show the slop, not to avoid it. + const excludedPrefixes = ['site/pages/slop']; const emDashPatterns = [/—/g, /—/gi, /—/gi, /—/gi]; // Phrase rules: { re, rationale }. Add to STYLE.md when adding here. const phraseRules = [ @@ -202,6 +206,7 @@ function validateProse(rootDir) { }; const scan = (absPath, rel) => { + if (excludedPrefixes.some(p => rel === p || rel.startsWith(p + '/'))) return; const stat = fs.statSync(absPath); if (stat.isDirectory()) { for (const entry of fs.readdirSync(absPath)) { @@ -331,6 +336,56 @@ function validateSiteHeader(_rootDir) { return 0; } +/** + * Guard the kinpaku default. Kinpaku is the site-wide default theme: the legacy + * --color-* names in tokens.css now carry dark-lacquer / gold-accent values, and + * the per-page kinpaku styling assumes that. If someone reintroduces the retired + * light-mode palette (white --color-paper, magenta --color-accent) the whole site + * silently regresses to light. Fail the build instead. Scoped to the token source + * — that's where the default lives; page CSS may still use light values locally + * for the deliberate AI-slop demonstrations. + */ +function validateTheme(rootDir) { + const tokensPath = path.join(rootDir, 'site', 'styles', 'tokens.css'); + if (!fs.existsSync(tokensPath)) { + console.log('✓ Theme guard skipped (tokens.css not found)'); + return 0; + } + const css = fs.readFileSync(tokensPath, 'utf8'); + let errors = 0; + + // Surfaces must be dark lacquer: either a --ks-* reference or a dark oklch + // (lightness < 35%). A high-lightness oklch means the light palette is back. + for (const name of ['color-paper', 'color-cream', 'color-bg']) { + const m = css.match(new RegExp(`--${name}:\\s*([^;]+);`)); + if (!m) continue; // token removed entirely is fine + const val = m[1].trim(); + if (val.includes('var(--ks-')) continue; + const light = val.match(/oklch\(\s*([\d.]+)%/); + if (light && Number(light[1]) >= 35) { + console.error(` ❌ tokens.css: --${name} is light (${val}). Kinpaku is the default; surfaces must be dark lacquer (var(--ks-lacquer*) or oklch < 35%).`); + errors++; + } + } + + // Accent must be kinpaku gold, not the retired magenta (hue ~350). + const accent = css.match(/--color-accent:\s*([^;]+);/); + if (accent && !accent[1].includes('var(--ks-')) { + const hue = accent[1].match(/oklch\(\s*[\d.]+%?\s+[\d.]+\s+([\d.]+)/); + if (hue && Number(hue[1]) >= 300 && Number(hue[1]) <= 360) { + console.error(` ❌ tokens.css: --color-accent is magenta (${accent[1].trim()}). The accent is kinpaku gold — use var(--ks-kinpaku).`); + errors++; + } + } + + if (errors > 0) { + console.error(`\n❌ ${errors} theme regression(s): light-mode defaults reintroduced in tokens.css.`); + } else { + console.log('✓ Theme defaults are kinpaku (dark surfaces, gold accent)'); + } + return errors; +} + /** * Copy directory recursively */ @@ -445,7 +500,7 @@ function generateApiData(buildDir, skills, patterns) { } const impeccable = skills.find(s => s.name === 'impeccable'); if (!impeccable) { - throw new Error('impeccable skill not found at skill/SKILL.md. The build system expects exactly one skill at that path.'); + throw new Error('impeccable skill not found at skill/SKILL.src.md. The build system expects exactly one skill at that path.'); } const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8')); @@ -470,6 +525,15 @@ function generateApiData(buildDir, skills, patterns) { // patterns.json fs.writeFileSync(path.join(apiDir, 'patterns.json'), JSON.stringify(patterns)); + // version.json - a tiny endpoint the installed skill polls on boot + // (skill/scripts/context.mjs) to nudge users toward `npx impeccable skills + // update`. Kept deliberately small so the boot-time check is cheap, unlike + // the full bundle download `skills check` performs. The skills version is + // the canonical one in the Claude plugin manifest. + const pluginManifestPath = path.join(ROOT_DIR, '.claude-plugin/plugin.json'); + const skillsVersion = JSON.parse(fs.readFileSync(pluginManifestPath, 'utf-8')).version; + fs.writeFileSync(path.join(apiDir, 'version.json'), JSON.stringify({ skills: skillsVersion })); + // command-source/{id}.json (one per skill) const cmdSourceDir = path.join(apiDir, 'command-source'); fs.mkdirSync(cmdSourceDir, { recursive: true }); @@ -556,12 +620,15 @@ function generateCFConfig(buildDir) { // Plus permanent redirects for legacy URLs. const redirects = `/api/skills /_data/api/skills.json 200 /api/commands /_data/api/commands.json 200 +/api/version /_data/api/version.json 200 /api/patterns /_data/api/patterns.json 200 /api/command-source/:id /_data/api/command-source/:id.json 200 /gallery /slop#try-it-live 301 /cheatsheet /docs 301 /skills /docs 301 +/skills/teach /docs/init 301 /skills/:id /docs/:id 301 +/docs/teach /docs/init 301 /anti-patterns /slop#catalog 301 /visual-mode /slop#see-it 301 /neon-mirai /neo-mirai/ 301 @@ -747,6 +814,9 @@ async function build() { // Verify every hand-authored HTML page carries the shared site header const headerErrors = validateSiteHeader(ROOT_DIR); + // Guard the kinpaku default: fail if light-mode token values are reintroduced + const themeErrors = validateTheme(ROOT_DIR); + // Scan user-facing copy for AI tells (em dashes, marketing fluff, denylisted phrases) const proseErrors = validateProse(ROOT_DIR); @@ -754,7 +824,7 @@ async function build() { // that has no technical reading. Hardening repetition is intentionally allowed. const skillProseErrors = validateSkillProse(ROOT_DIR); - if (countErrors > 0 || headerErrors > 0 || proseErrors > 0 || skillProseErrors > 0) { + if (countErrors > 0 || headerErrors > 0 || themeErrors > 0 || proseErrors > 0 || skillProseErrors > 0) { process.exit(1); } diff --git a/scripts/lib/sub-pages-data.js b/scripts/lib/sub-pages-data.js index f61ac59d0..b6c548fdb 100644 --- a/scripts/lib/sub-pages-data.js +++ b/scripts/lib/sub-pages-data.js @@ -3,7 +3,7 @@ * generators. * * Single source of truth: - * - skill/SKILL.md → skill frontmatter + body + * - skill/SKILL.src.md → skill frontmatter + body * - skill/reference/*.md → skill reference files * - cli/engine/registry/antipatterns.mjs → ANTIPATTERNS registry * - site/content/skills/{id}.md → optional editorial wrapper @@ -34,7 +34,7 @@ export { */ const EXCLUDED_SKILLS = new Set([ 'frontend-design', // deprecated, renamed to impeccable - 'teach-impeccable', // deprecated, folded into /impeccable teach + 'teach-impeccable', // deprecated, folded into /impeccable init 'arrange', // renamed to layout 'normalize', // merged into /polish ]); @@ -71,7 +71,7 @@ export const SKILL_CATEGORIES = { harden: 'harden', onboard: 'harden', // SYSTEM - setup and tooling - teach: 'system', + init: 'system', document: 'system', extract: 'system', live: 'system', @@ -131,8 +131,8 @@ export const COMMAND_RELATIONSHIPS = { harden: { combinesWith: ['optimize'] }, onboard: { combinesWith: ['clarify', 'delight'] }, // System - teach: { combinesWith: ['document'] }, - document: { combinesWith: ['teach', 'extract'] }, + init: { combinesWith: ['document'] }, + document: { combinesWith: ['init', 'extract'] }, extract: { combinesWith: ['document'] }, live: {}, }; diff --git a/scripts/lib/transformers/factory.js b/scripts/lib/transformers/factory.js index 787ca6084..8dbbe5c78 100644 --- a/scripts/lib/transformers/factory.js +++ b/scripts/lib/transformers/factory.js @@ -7,6 +7,7 @@ import { generateYamlDocument, replacePlaceholders, compileProviderBlocks, + stripRuleMarkers, } from '../utils.js'; import { SKILL_CATEGORIES, CATEGORY_ORDER } from '../sub-pages-data.js'; @@ -220,6 +221,7 @@ export function createTransformer(config) { // Build body let skillBody = compileProviderBlocks(skill.body, providerTags); skillBody = replacePlaceholders(skillBody, placeholderKey, commandNames, allSkillNames); + skillBody = stripRuleMarkers(skillBody); // Replace {{scripts_path}} with provider-aware path to skill's scripts directory const scriptsPath = `${configDir}/skills/${skillName}/scripts`; @@ -241,6 +243,7 @@ export function createTransformer(config) { for (const ref of skill.references) { let refContent = compileProviderBlocks(ref.content, providerTags); refContent = replacePlaceholders(refContent, placeholderKey, [], allSkillNames); + refContent = stripRuleMarkers(refContent); refContent = refContent.replace(/\{\{scripts_path\}\}/g, scriptsPath); writeFile(path.join(refDir, `${ref.name}.md`), refContent); refCount++; diff --git a/scripts/lib/utils.js b/scripts/lib/utils.js index 50e323a1c..234e32ea5 100644 --- a/scripts/lib/utils.js +++ b/scripts/lib/utils.js @@ -184,12 +184,19 @@ export function readFilesRecursive(dir, fileList = []) { * Read and parse the impeccable skill source. * After v3.0 the repo holds exactly one user-invocable skill, flat at skill/. * Returns { skills: [oneEntry] } so downstream array-shaped consumers stay happy. + * + * The source manifest is `SKILL.src.md`, NOT `SKILL.md`, on purpose: the + * `vercel-labs/skills` CLI discovers a skill by finding a literal `SKILL.md` + * and copies that directory verbatim. If `skill/SKILL.md` existed, `npx skills` + * would install the UNCOMPILED source (unresolved `{{placeholders}}`, no vendored + * detector). Naming it `SKILL.src.md` hides it from discovery so the CLI falls + * through to a compiled harness dir (`.agents/skills/impeccable`) instead. */ export function readSourceFiles(rootDir) { const skillDir = path.join(rootDir, 'skill'); const skills = []; - const skillMdPath = path.join(skillDir, 'SKILL.md'); + const skillMdPath = path.join(skillDir, 'SKILL.src.md'); if (!fs.existsSync(skillMdPath)) { return { skills }; } @@ -419,7 +426,7 @@ export function readPatterns(_rootDir, _relativePath) { // Previous SKILL.md parser retained below but disabled; kept as a // reference for how prefix-style extraction used to work. -function _legacyReadPatterns(rootDir, relativePath = 'skill/SKILL.md') { +function _legacyReadPatterns(rootDir, relativePath = 'skill/SKILL.src.md') { const skillPath = path.join(rootDir, relativePath); if (!fs.existsSync(skillPath)) { @@ -623,6 +630,24 @@ export function compileProviderBlocks(content, activeTags = []) { return didCompileBlock ? compiled.replace(/(?:\r?\n){3,}/g, '\n\n') : compiled; } +/** + * Strip `` markers from skill markdown. + * + * The impeccable-evals registry at `tools/instruction-rules.ts` pins + * each instruction line to a stable ID. Markers in the source keep + * that mapping verifiable in lock-step with the file. The model that + * loads the staged SKILL.md should not see them, so this strip runs + * during the per-provider staging in factory.js. + * + * Removes the marker plus any leading whitespace on the same line, so + * `something. ` becomes `something.` and a standalone + * marker line collapses to an empty line that the existing + * blank-line normalization in compileProviderBlocks reaps. + */ +export function stripRuleMarkers(content) { + return content.replace(/[ \t]*/g, ''); +} + /** * Replace all {{placeholder}} tokens with provider-specific values */ diff --git a/scripts/release.mjs b/scripts/release.mjs index 485540459..e98ca0482 100755 --- a/scripts/release.mjs +++ b/scripts/release.mjs @@ -149,18 +149,20 @@ if (remoteTags.split('\n').some((line) => line.endsWith(`refs/tags/${tag}`))) { ok('tag is free'); step(`Extracting changelog entry for "${cfg.changelogLabel}${version}"`); -const changelogSource = path.join(repoRoot, 'site/pages/index.astro'); -const indexHtml = readFileSync(changelogSource, 'utf8'); -const expectedHeader = `${cfg.changelogLabel}${version}`; -const headerIdx = indexHtml.indexOf(expectedHeader); +const changelogSource = path.join(repoRoot, 'site/pages/changelog.astro'); +const changelogHtml = readFileSync(changelogSource, 'utf8'); +const expectedHeader = `${cfg.changelogLabel}${version}`; +const headerIdx = changelogHtml.indexOf(expectedHeader); if (headerIdx === -1) { - fail(`No changelog entry found for "${cfg.changelogLabel}${version}" in site/pages/index.astro. Add one before releasing.`); + fail(`No changelog entry found for "${cfg.changelogLabel}${version}" in site/pages/changelog.astro. Add one before releasing.`); } -const entryStart = indexHtml.lastIndexOf('
', headerIdx); -if (entryStart === -1 || ulEnd === -1) fail('Changelog entry markup is malformed.'); -const entryEnd = indexHtml.indexOf('
', ulEnd) + ''.length; -const entryHtml = indexHtml.slice(entryStart, entryEnd); +// Notes are the entry's bullet list. Scoping to
    +// skips the optional lead paragraph, before/after figure, and stat row +// that the headline release (v3.5.0) carries, so release notes stay clean. +const listStart = changelogHtml.indexOf('
      ', headerIdx); +const listEnd = changelogHtml.indexOf('
    ', listStart); +if (listStart === -1 || listEnd === -1) fail('Changelog entry markup is malformed.'); +const entryHtml = changelogHtml.slice(listStart, listEnd + '
'.length); const notes = htmlToMarkdown(entryHtml); ok('extracted'); diff --git a/scripts/screenshot-antipatterns.js b/scripts/screenshot-antipatterns.js index 9e1b07673..854e38f3d 100644 --- a/scripts/screenshot-antipatterns.js +++ b/scripts/screenshot-antipatterns.js @@ -4,7 +4,7 @@ * Screenshot Anti-Pattern Examples * * Takes 1080x1080 screenshots of each anti-pattern example for LinkedIn sharing. - * Requires the dev server to be running on localhost:3000 + * Requires the dev server to be running on localhost:4321 */ import { chromium } from 'playwright'; @@ -43,7 +43,7 @@ async function screenshotAntipatterns() { for (const file of files) { const name = path.basename(file, '.html'); - const url = `http://localhost:3000/antipattern-examples/${file}`; + const url = `http://localhost:4321/antipattern-examples/${file}`; const outputPath = path.join(OUTPUT_DIR, `${name}.png`); console.log(` ${name}...`); @@ -82,7 +82,7 @@ async function screenshotAntipatterns() { // Check if dev server is running async function checkServer() { try { - const response = await fetch('http://localhost:3000'); + const response = await fetch('http://localhost:4321'); return response.ok; } catch { return false; diff --git a/site/components/DocsSidebar.astro b/site/components/DocsSidebar.astro new file mode 100644 index 000000000..db3c9bcbb --- /dev/null +++ b/site/components/DocsSidebar.astro @@ -0,0 +1,74 @@ +--- +// Shared sidebar for the /docs section. +// Renders Tutorials + Commands, highlighting the current entry. +// Used by: +// - site/pages/docs/index.astro (no active entry) +// - site/layouts/Doc.astro (activeCommand) +// - site/pages/tutorials/[...slug].astro (activeTutorial) +import { getCollection } from 'astro:content'; +import { + SKILL_CATEGORIES, + CATEGORY_ORDER, + CATEGORY_LABELS, +} from '../data/sub-pages-data'; + +interface Props { + activeCommand?: string; + activeTutorial?: string; +} +const { activeCommand, activeTutorial } = Astro.props; + +const tutorials = (await getCollection('tutorials')) + .sort((a, b) => a.data.order - b.data.order); + +const skills = await getCollection('skills'); +const sidebarGroups: Record = {}; +for (const cat of CATEGORY_ORDER) { + sidebarGroups[cat] = skills + .filter(e => (SKILL_CATEGORIES[e.id] || 'system') === cat) + .sort((a, b) => a.id.localeCompare(b.id)) + .map(e => ({ slug: e.id })); +} +--- + + diff --git a/site/components/Footer.astro b/site/components/Footer.astro index 1d2309478..d2dccd6f0 100644 --- a/site/components/Footer.astro +++ b/site/components/Footer.astro @@ -5,10 +5,8 @@ diff --git a/site/pages/tutorials/index.astro b/site/pages/tutorials/index.astro index 52a250ed4..63e3f73ab 100644 --- a/site/pages/tutorials/index.astro +++ b/site/pages/tutorials/index.astro @@ -12,7 +12,7 @@ const tutorials = (await getCollection('tutorials')) description="Step-by-step guides for getting started with Impeccable, from first install to live browser iteration." activeNav="docs" canonicalPath="/tutorials" - bodyClass="sub-page" + bodyClass="sub-page kinpaku-chrome kinpaku-surface" >
@@ -45,7 +45,7 @@ const tutorials = (await getCollection('tutorials')) } .tutorial-card:hover { border-color: var(--color-accent); } .tutorial-card-num { font-family: var(--font-mono); font-size: 0.8125rem; color: var(--color-ash); flex-shrink: 0; padding-top: 0.125rem; } - .tutorial-card-title { font-size: 1.125rem; font-weight: 600; margin: 0 0 0.25rem; } + .tutorial-card-title { font-family: var(--font-body); font-style: normal; font-size: 1.125rem; font-weight: 600; margin: 0 0 0.25rem; color: var(--color-ink); } .tutorial-card-desc { font-size: 0.875rem; color: var(--color-ash); margin: 0; line-height: 1.5; } diff --git a/site/public/apple-touch-icon.png b/site/public/apple-touch-icon.png index 4424834f0..4008baacc 100644 Binary files a/site/public/apple-touch-icon.png and b/site/public/apple-touch-icon.png differ diff --git a/site/public/assets/brand/impeccable-logo-on-dark.png b/site/public/assets/brand/impeccable-logo-on-dark.png new file mode 100644 index 000000000..e5b63f797 Binary files /dev/null and b/site/public/assets/brand/impeccable-logo-on-dark.png differ diff --git a/site/public/assets/brand/impeccable-logo-on-dark.svg b/site/public/assets/brand/impeccable-logo-on-dark.svg new file mode 100644 index 000000000..a6079f9d5 --- /dev/null +++ b/site/public/assets/brand/impeccable-logo-on-dark.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/site/public/assets/brand/impeccable-logo-on-light.png b/site/public/assets/brand/impeccable-logo-on-light.png new file mode 100644 index 000000000..8aaf5f5a7 Binary files /dev/null and b/site/public/assets/brand/impeccable-logo-on-light.png differ diff --git a/site/public/assets/brand/impeccable-logo-on-light.svg b/site/public/assets/brand/impeccable-logo-on-light.svg new file mode 100644 index 000000000..ade4ba353 --- /dev/null +++ b/site/public/assets/brand/impeccable-logo-on-light.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/site/public/assets/changelog/luxury-hotel-gpt55-after.jpg b/site/public/assets/changelog/luxury-hotel-gpt55-after.jpg new file mode 100644 index 000000000..c7a43b58f Binary files /dev/null and b/site/public/assets/changelog/luxury-hotel-gpt55-after.jpg differ diff --git a/site/public/assets/changelog/luxury-hotel-gpt55-before.jpg b/site/public/assets/changelog/luxury-hotel-gpt55-before.jpg new file mode 100644 index 000000000..5ecd37bb6 Binary files /dev/null and b/site/public/assets/changelog/luxury-hotel-gpt55-before.jpg differ diff --git a/site/public/assets/neo-kinpaku/before-after-seam.png b/site/public/assets/neo-kinpaku/before-after-seam.png new file mode 100644 index 000000000..9bb7c36a3 Binary files /dev/null and b/site/public/assets/neo-kinpaku/before-after-seam.png differ diff --git a/site/public/assets/neo-kinpaku/candidates/finalists/m-01-v2-01.png b/site/public/assets/neo-kinpaku/candidates/finalists/m-01-v2-01.png new file mode 100644 index 000000000..04852fa54 Binary files /dev/null and b/site/public/assets/neo-kinpaku/candidates/finalists/m-01-v2-01.png differ diff --git a/site/public/assets/neo-kinpaku/gold-dust-rule.png b/site/public/assets/neo-kinpaku/gold-dust-rule.png new file mode 100644 index 000000000..69e5f9d82 Binary files /dev/null and b/site/public/assets/neo-kinpaku/gold-dust-rule.png differ diff --git a/site/public/assets/neo-kinpaku/hero-seam-field-v2.png b/site/public/assets/neo-kinpaku/hero-seam-field-v2.png new file mode 100644 index 000000000..72d2181a8 Binary files /dev/null and b/site/public/assets/neo-kinpaku/hero-seam-field-v2.png differ diff --git a/site/public/assets/neo-kinpaku/hero-seam-field.png b/site/public/assets/neo-kinpaku/hero-seam-field.png new file mode 100644 index 000000000..35a0a7ba8 Binary files /dev/null and b/site/public/assets/neo-kinpaku/hero-seam-field.png differ diff --git a/site/public/assets/neo-kinpaku/kinpaku-gold-leaf.png b/site/public/assets/neo-kinpaku/kinpaku-gold-leaf.png new file mode 100644 index 000000000..9fc214c61 Binary files /dev/null and b/site/public/assets/neo-kinpaku/kinpaku-gold-leaf.png differ diff --git a/site/public/assets/neo-kinpaku/lacquer-grain.png b/site/public/assets/neo-kinpaku/lacquer-grain.png new file mode 100644 index 000000000..77d31e02d Binary files /dev/null and b/site/public/assets/neo-kinpaku/lacquer-grain.png differ diff --git a/site/public/assets/neo-kinpaku/oxidation-edge-accent.png b/site/public/assets/neo-kinpaku/oxidation-edge-accent.png new file mode 100644 index 000000000..ff1515701 Binary files /dev/null and b/site/public/assets/neo-kinpaku/oxidation-edge-accent.png differ diff --git a/site/public/assets/neo-kinpaku/verdigris-patina.png b/site/public/assets/neo-kinpaku/verdigris-patina.png new file mode 100644 index 000000000..c6d3d6161 Binary files /dev/null and b/site/public/assets/neo-kinpaku/verdigris-patina.png differ diff --git a/site/public/assets/testimonials/BowTiedGroundHo.jpg b/site/public/assets/testimonials/BowTiedGroundHo.jpg new file mode 100644 index 000000000..108a9081c Binary files /dev/null and b/site/public/assets/testimonials/BowTiedGroundHo.jpg differ diff --git a/site/public/assets/testimonials/EmreCoklar.jpg b/site/public/assets/testimonials/EmreCoklar.jpg new file mode 100644 index 000000000..e15d04184 Binary files /dev/null and b/site/public/assets/testimonials/EmreCoklar.jpg differ diff --git a/site/public/assets/testimonials/HeyZohaib.jpg b/site/public/assets/testimonials/HeyZohaib.jpg new file mode 100644 index 000000000..be196a7a8 Binary files /dev/null and b/site/public/assets/testimonials/HeyZohaib.jpg differ diff --git a/site/public/assets/testimonials/Hicker_Moledao.jpg b/site/public/assets/testimonials/Hicker_Moledao.jpg new file mode 100644 index 000000000..b062288f4 Binary files /dev/null and b/site/public/assets/testimonials/Hicker_Moledao.jpg differ diff --git a/site/public/assets/testimonials/IanAndrewsDC.jpg b/site/public/assets/testimonials/IanAndrewsDC.jpg new file mode 100644 index 000000000..667c0cb3e Binary files /dev/null and b/site/public/assets/testimonials/IanAndrewsDC.jpg differ diff --git a/site/public/assets/testimonials/IceCreamChai.jpg b/site/public/assets/testimonials/IceCreamChai.jpg new file mode 100644 index 000000000..9f0049e55 Binary files /dev/null and b/site/public/assets/testimonials/IceCreamChai.jpg differ diff --git a/site/public/assets/testimonials/JASHANGUPTA15.jpg b/site/public/assets/testimonials/JASHANGUPTA15.jpg new file mode 100644 index 000000000..5330d3446 Binary files /dev/null and b/site/public/assets/testimonials/JASHANGUPTA15.jpg differ diff --git a/site/public/assets/testimonials/MertcanDYuzer.jpg b/site/public/assets/testimonials/MertcanDYuzer.jpg new file mode 100644 index 000000000..5401cbf48 Binary files /dev/null and b/site/public/assets/testimonials/MertcanDYuzer.jpg differ diff --git a/site/public/assets/testimonials/MikitaHQ.jpg b/site/public/assets/testimonials/MikitaHQ.jpg new file mode 100644 index 000000000..07b1cfe9c Binary files /dev/null and b/site/public/assets/testimonials/MikitaHQ.jpg differ diff --git a/site/public/assets/testimonials/Paul_Kinlan.jpg b/site/public/assets/testimonials/Paul_Kinlan.jpg new file mode 100644 index 000000000..754d55452 Binary files /dev/null and b/site/public/assets/testimonials/Paul_Kinlan.jpg differ diff --git a/site/public/assets/testimonials/adrien_ninet.jpg b/site/public/assets/testimonials/adrien_ninet.jpg new file mode 100644 index 000000000..bef2f165d Binary files /dev/null and b/site/public/assets/testimonials/adrien_ninet.jpg differ diff --git a/site/public/assets/testimonials/alejandroreyes.jpg b/site/public/assets/testimonials/alejandroreyes.jpg new file mode 100644 index 000000000..d20fc935e Binary files /dev/null and b/site/public/assets/testimonials/alejandroreyes.jpg differ diff --git a/site/public/assets/testimonials/billzh.jpg b/site/public/assets/testimonials/billzh.jpg new file mode 100644 index 000000000..c1a34a228 Binary files /dev/null and b/site/public/assets/testimonials/billzh.jpg differ diff --git a/site/public/assets/testimonials/carlrannaberg.jpg b/site/public/assets/testimonials/carlrannaberg.jpg new file mode 100644 index 000000000..2e9cb396d Binary files /dev/null and b/site/public/assets/testimonials/carlrannaberg.jpg differ diff --git a/site/public/assets/testimonials/cathrynlavery-1.jpg b/site/public/assets/testimonials/cathrynlavery-1.jpg new file mode 100644 index 000000000..b78ff7989 Binary files /dev/null and b/site/public/assets/testimonials/cathrynlavery-1.jpg differ diff --git a/site/public/assets/testimonials/devsome_sh.jpg b/site/public/assets/testimonials/devsome_sh.jpg new file mode 100644 index 000000000..c01d01714 Binary files /dev/null and b/site/public/assets/testimonials/devsome_sh.jpg differ diff --git a/site/public/assets/testimonials/faizan10114.jpg b/site/public/assets/testimonials/faizan10114.jpg new file mode 100644 index 000000000..097047a5b Binary files /dev/null and b/site/public/assets/testimonials/faizan10114.jpg differ diff --git a/site/public/assets/testimonials/illscience.jpg b/site/public/assets/testimonials/illscience.jpg new file mode 100644 index 000000000..488d0f195 Binary files /dev/null and b/site/public/assets/testimonials/illscience.jpg differ diff --git a/site/public/assets/testimonials/ivanleomk.jpg b/site/public/assets/testimonials/ivanleomk.jpg new file mode 100644 index 000000000..70d7c89b0 Binary files /dev/null and b/site/public/assets/testimonials/ivanleomk.jpg differ diff --git a/site/public/assets/testimonials/johnennis.jpg b/site/public/assets/testimonials/johnennis.jpg new file mode 100644 index 000000000..a9733faa6 Binary files /dev/null and b/site/public/assets/testimonials/johnennis.jpg differ diff --git a/site/public/assets/testimonials/karatzas_thomas.jpg b/site/public/assets/testimonials/karatzas_thomas.jpg new file mode 100644 index 000000000..4768e8aca Binary files /dev/null and b/site/public/assets/testimonials/karatzas_thomas.jpg differ diff --git a/site/public/assets/testimonials/largePrawn.jpg b/site/public/assets/testimonials/largePrawn.jpg new file mode 100644 index 000000000..d6ed4678f Binary files /dev/null and b/site/public/assets/testimonials/largePrawn.jpg differ diff --git a/site/public/assets/testimonials/littlemartta.jpg b/site/public/assets/testimonials/littlemartta.jpg new file mode 100644 index 000000000..fb56db27a Binary files /dev/null and b/site/public/assets/testimonials/littlemartta.jpg differ diff --git a/site/public/assets/testimonials/michaelhedgpeth.jpg b/site/public/assets/testimonials/michaelhedgpeth.jpg new file mode 100644 index 000000000..340f220ba Binary files /dev/null and b/site/public/assets/testimonials/michaelhedgpeth.jpg differ diff --git a/site/public/assets/testimonials/nik_ska.jpg b/site/public/assets/testimonials/nik_ska.jpg new file mode 100644 index 000000000..218273c26 Binary files /dev/null and b/site/public/assets/testimonials/nik_ska.jpg differ diff --git a/site/public/assets/testimonials/sid_hori.jpg b/site/public/assets/testimonials/sid_hori.jpg new file mode 100644 index 000000000..5bc27d537 Binary files /dev/null and b/site/public/assets/testimonials/sid_hori.jpg differ diff --git a/site/public/assets/testimonials/vandotorres.jpg b/site/public/assets/testimonials/vandotorres.jpg new file mode 100644 index 000000000..d0568de80 Binary files /dev/null and b/site/public/assets/testimonials/vandotorres.jpg differ diff --git a/site/public/assets/testimonials/wanikwai.jpg b/site/public/assets/testimonials/wanikwai.jpg new file mode 100644 index 000000000..76fe9b328 Binary files /dev/null and b/site/public/assets/testimonials/wanikwai.jpg differ diff --git a/site/public/favicon.svg b/site/public/favicon.svg index 57a8e0eaa..e81e28b4c 100644 --- a/site/public/favicon.svg +++ b/site/public/favicon.svg @@ -1,4 +1,4 @@ - - - / + + + diff --git a/site/scripts/app.js b/site/scripts/app.js index 7d5c1ca21..0b77e8f3d 100644 --- a/site/scripts/app.js +++ b/site/scripts/app.js @@ -2,13 +2,12 @@ import { initGlassTerminal, renderTerminalLayout, } from "./components/glass-terminal.js"; -import { initLensEffect } from "./components/lens.js"; import { initFrameworkViz } from "./components/framework-viz.js"; import { initScrollReveal } from "./utils/reveal.js"; import { initAnchorScroll, initHashTracking } from "./utils/scroll.js"; import { initSectionNav } from "./components/section-nav.js"; import { initFoundationGrid } from "./components/foundation-grid.js"; -import { initLiveDemo } from "./components/live-demo.js"; +import { initLiveDemo, initGbarPageChat } from "./components/live-demo.js"; // ============================================ // STATE @@ -236,10 +235,33 @@ document.addEventListener("click", (e) => { // STARTUP // ============================================ +// Fade the header's glass background in px-by-px as the user scrolls off the +// hero, by writing scroll progress (0 → 1 over RANGE px) to a --hp custom +// property the CSS interpolates against. +function initHeaderScroll() { + const header = document.querySelector("[data-site-header]"); + if (!header) return; + const RANGE = 200; + let ticking = false; + const apply = () => { + const p = Math.min(1, window.scrollY / RANGE); + header.style.setProperty("--hp", p.toFixed(4)); + ticking = false; + }; + const onScroll = () => { + if (!ticking) { + ticking = true; + requestAnimationFrame(apply); + } + }; + apply(); + window.addEventListener("scroll", onScroll, { passive: true }); +} + function init() { initAnchorScroll(); initHashTracking(); - initLensEffect(); + initHeaderScroll(); initScrollReveal(); initGlassTerminal(); initFrameworkViz(); @@ -248,6 +270,7 @@ function init() { initWhyTabs(); initLanguageTabs(); initLiveDemo(); + initGbarPageChat(); loadContent(); document.body.classList.add("loaded"); diff --git a/site/scripts/components/foundation-grid.js b/site/scripts/components/foundation-grid.js index a650f146a..a6f03947c 100644 --- a/site/scripts/components/foundation-grid.js +++ b/site/scripts/components/foundation-grid.js @@ -1,26 +1,35 @@ -import { skillFocusAreas, dimensionGuidelineCounts } from '../data.js'; +import { skillFocusAreas, slopFocusAreas, dimensionGuidelineCounts } from '../data.js'; import { foundationAnimations } from './foundation-animations.js'; +// Renders the seven-discipline grid. The container picks its data source via +// `data-source="foundation"` (the original "loaded on every command" copy) or +// `data-source="slop"` (the slop-tells-we-prevent copy used on /catch-the-slop). +// Both share the icon + plinth + card chrome so we get the elegant magazine +// rail twice without two parallel components. export function initFoundationGrid() { - const container = document.querySelector('.foundation-grid'); - if (!container) return; + const containers = document.querySelectorAll('.foundation-grid'); + if (!containers.length) return; - const dimensions = skillFocusAreas['impeccable']; - if (!dimensions) return; + containers.forEach((container) => { + const source = container.dataset.source || 'foundation'; + const data = source === 'slop' ? slopFocusAreas['impeccable'] : skillFocusAreas['impeccable']; + if (!data) return; + const showCount = source !== 'slop'; - container.innerHTML = dimensions.map((dim, i) => ` -
-
-
- ${foundationAnimations[dim.area] || ''} + container.innerHTML = data.map((dim, i) => ` +
+
+
+ ${foundationAnimations[dim.area] || ''} +
+
+ ${dim.area} + ${showCount ? `${dimensionGuidelineCounts[dim.area] || ''}` : ''} +
+

${dim.detail}

-
- ${dim.area} - ${dimensionGuidelineCounts[dim.area] || ''} -
-

${dim.detail}

+
-
-
- `).join(''); + `).join(''); + }); } diff --git a/site/scripts/components/framework-viz.js b/site/scripts/components/framework-viz.js index bde86bac2..2d82d1fba 100644 --- a/site/scripts/components/framework-viz.js +++ b/site/scripts/components/framework-viz.js @@ -45,7 +45,7 @@ const commandSymbols = { 'optimize': 'Op', 'harden': 'Ha', 'onboard': 'On', - 'teach': 'Te', + 'init': 'In', 'document': 'Dc', 'extract': 'Ex', 'live': 'Li' @@ -58,7 +58,7 @@ const commandNumbers = { 'delight': 10, 'bolder': 11, 'quieter': 12, 'overdrive': 13, 'distill': 14, 'clarify': 15, 'adapt': 16, 'polish': 17, 'optimize': 18, 'harden': 19, 'onboard': 20, - 'teach': 21, 'document': 22, 'extract': 23, 'live': 24 + 'init': 21, 'document': 22, 'extract': 23, 'live': 24 }; // After the v3.0 consolidation, all commands except the root "impeccable" are @@ -100,6 +100,7 @@ export class PeriodicTable { const categoryOrder = ['create', 'evaluate', 'refine', 'simplify', 'harden', 'system']; const grid = document.createElement('div'); + grid.className = 'ptable-grid'; grid.style.cssText = ` display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); @@ -191,9 +192,11 @@ export class PeriodicTable { const colors = categoryColors[category]; const group = document.createElement('div'); + group.className = `ptable-group ptable-group--${category}`; group.style.cssText = `display: flex; flex-direction: column; gap: 6px;`; const label = document.createElement('div'); + label.className = 'ptable-group-label'; label.style.cssText = ` font-family: var(--font-body); font-size: 10px; @@ -207,6 +210,7 @@ export class PeriodicTable { group.appendChild(label); const row = document.createElement('div'); + row.className = 'ptable-row'; row.style.cssText = `display: flex; flex-wrap: wrap; gap: 6px;`; commands.forEach(cmd => { @@ -223,6 +227,8 @@ export class PeriodicTable { const display = commandDisplay[cmd]; const el = document.createElement('button'); + el.className = `ptable-element ptable-element--${category}`; + el.dataset.category = category; el.type = 'button'; // Build accessible label with the full invocation const invocation = cmd === 'impeccable' @@ -250,6 +256,7 @@ export class PeriodicTable { // Atomic number const number = document.createElement('div'); + number.className = 'ptable-number'; number.style.cssText = ` position: absolute; top: 3px; @@ -264,6 +271,7 @@ export class PeriodicTable { // Symbol const symbol = document.createElement('div'); + symbol.className = 'ptable-symbol'; symbol.style.cssText = ` font-family: var(--font-display); font-size: 20px; @@ -274,10 +282,10 @@ export class PeriodicTable { symbol.textContent = commandSymbols[cmd]; el.appendChild(symbol); - // Command name. The root "impeccable" is shown with its slash as the - // entry point. All other commands are sub-commands and show their - // bare name (the invocation is /impeccable ). + // Command name. The palette lists names; invocation examples elsewhere + // include the slash syntax. const name = document.createElement('div'); + name.className = 'ptable-name'; name.style.cssText = ` font-family: var(--font-mono); font-size: 8px; @@ -290,7 +298,7 @@ export class PeriodicTable { white-space: nowrap; `; if (cmd === 'impeccable') { - name.textContent = '/impeccable'; + name.textContent = 'impeccable'; } else if (display) { name.textContent = display.label; } else { @@ -301,6 +309,7 @@ export class PeriodicTable { // Alpha badge if (alphaCommands.includes(cmd)) { const badge = document.createElement('div'); + badge.className = 'ptable-alpha'; badge.style.cssText = ` position: absolute; top: 2px; diff --git a/site/scripts/components/glass-terminal.js b/site/scripts/components/glass-terminal.js index 71f79f357..a8616a4ba 100644 --- a/site/scripts/components/glass-terminal.js +++ b/site/scripts/components/glass-terminal.js @@ -89,7 +89,7 @@ function renderDesktopLayout(container, commands) { 'refine': ['typeset', 'layout', 'colorize', 'animate', 'delight', 'bolder', 'quieter', 'overdrive'], 'simplify': ['distill', 'clarify', 'adapt'], 'harden': ['polish', 'optimize', 'harden'], - 'system': ['teach', 'extract'] + 'system': ['init', 'extract'] }; const grouped = {}; filteredCommands.forEach(cmd => { @@ -135,12 +135,8 @@ function renderDesktopLayout(container, commands) { const fisheyeHTML = filteredCommands.map((cmd, i) => { const cat = commandCategories[cmd.id] || 'other'; const isAlpha = alphaCommands.includes(cmd.id); - // The root skill is shown as "/impeccable", everything else is a sub-command - // displayed without a slash (invocation is /impeccable ) - const isRoot = cmd.id === 'impeccable'; - const label = isRoot - ? `/impeccable` - : cmd.id; + // The command palette lists command names, not raw invocations. + const label = cmd.id; return ``; }).join(''); @@ -201,12 +197,12 @@ function renderSpread(cmd, index, isActive) { } } - // The root skill is rendered as /impeccable; sub-commands are rendered as + // The root skill is rendered as impeccable; sub-commands are rendered as // /impeccable on a smaller line above the command name, so the command name // stays the visual anchor at full display size. const isRoot = cmd.id === 'impeccable'; const nameHTML = isRoot - ? `/impeccable` + ? 'impeccable' : `/impeccable${cmd.id}`; return ` @@ -251,7 +247,8 @@ function initSpreadDemo(index) { const splitComparison = demoArea.querySelector('.demo-split-comparison'); if (splitComparison) { currentSplitInstance = initSplitCompare(splitComparison, { - defaultPosition: 50 + defaultPosition: 50, + skewAngle: 0 }); } initCommandDemo(cmd.id, demoArea); @@ -547,6 +544,7 @@ function setupMobileInteractions(commands) { if (initialSplit) { currentSplitInstance = initSplitCompare(initialSplit, { defaultPosition: 50, + skewAngle: 0, minPosition: 10, maxPosition: 90 }); @@ -587,7 +585,8 @@ function setupMobileInteractions(commands) { const splitComparison = demoArea.querySelector('.demo-split-comparison'); if (splitComparison) { currentSplitInstance = initSplitCompare(splitComparison, { - defaultPosition: 50 + defaultPosition: 50, + skewAngle: 0 }); } initCommandDemo(cmdId, demoArea); @@ -664,4 +663,3 @@ async function updateSourceContent(cmdId) { contentEl.innerHTML = 'Source not available'; } } - diff --git a/site/scripts/components/lens.js b/site/scripts/components/lens.js index 1d6e37c6c..142f80b40 100644 --- a/site/scripts/components/lens.js +++ b/site/scripts/components/lens.js @@ -5,9 +5,9 @@ export function initLensEffect() { if (!container) return; initSplitCompare(container, { - defaultPosition: 50 + defaultPosition: 50, + skewAngle: 0 }); } - diff --git a/site/scripts/components/live-demo.js b/site/scripts/components/live-demo.js index 2961c26ec..11315a549 100644 --- a/site/scripts/components/live-demo.js +++ b/site/scripts/components/live-demo.js @@ -16,12 +16,14 @@ const PHASE = { const TIMELINE = [ { dt: 400, action: 'cursor-show' }, { dt: 400, action: 'cursor-to-target' }, - { dt: 900, action: 'outline-show', caption: 'Hover to pick.' }, - { dt: 500, action: 'cursor-click' }, - { dt: 200, action: 'open-ctx', caption: 'Picked. Contextual bar appears.' }, - { dt: 700, action: 'cursor-to-input' }, - { dt: 300, action: 'type', text: 'more playful', caption: 'Type a refinement, or skip.' }, - { dt: 1200, action: 'draw-stroke', caption: 'Annotate on the page, if you want.' }, + { dt: 800, action: 'outline-show', caption: 'Pick any element on your live page.' }, + { dt: 450, action: 'cursor-click' }, + { dt: 200, action: 'open-ctx' }, + { dt: 560, action: 'cursor-to-button' }, + { dt: 250, action: 'draw-circle', caption: 'Circle what’s off-brand…' }, + { dt: 700, action: 'drop-pin' }, + { dt: 300, action: 'type-note', text: 'match the suites below', caption: '…and say what you want.' }, + { dt: 1800, action: 'hold', caption: 'Mark + note are sent to your agent.' }, { dt: 900, action: 'cursor-to-go' }, { dt: 300, action: 'click-go', caption: 'Generating three variants…' }, { dt: 1600, action: 'show-variant', n: 1, caption: 'Variant 1 of 3.' }, @@ -29,7 +31,7 @@ const TIMELINE = [ { dt: 1400, action: 'show-variant', n: 3, caption: 'Variant 3 of 3.' }, { dt: 900, action: 'cursor-to-accept' }, { dt: 300, action: 'click-accept', caption: 'Accepted. Written to source.' }, - { dt: 1800, action: 'reset', caption: 'Hover to pick.' }, + { dt: 1800, action: 'reset', caption: 'Pick any element on your live page.' }, ]; export function initLiveDemo() { @@ -43,6 +45,7 @@ export function initLiveDemo() { const cursor = root.querySelector('[data-demo-cursor]'); const ctx = root.querySelector('[data-demo-ctx]'); const inputText = root.querySelector('[data-demo-input-text]'); + const noteText = root.querySelector('[data-demo-note-text]'); const counter = root.querySelector('[data-demo-counter]'); const captionLabel = root.querySelector('[data-demo-caption-label]'); const variants = Array.from(root.querySelectorAll('.live-demo-variant')); @@ -61,6 +64,18 @@ export function initLiveDemo() { outline.style.height = (targetRect.height + 8) + 'px'; }; + // Overlay the annotation layer (circle + comment pin) exactly on the target, + // so the marks sit on the picked card rather than the stage center. + const positionAnnotations = () => { + if (!annotations) return; + const stageRect = stage.getBoundingClientRect(); + const targetRect = target.getBoundingClientRect(); + annotations.style.left = (targetRect.left - stageRect.left) + 'px'; + annotations.style.top = (targetRect.top - stageRect.top) + 'px'; + annotations.style.width = targetRect.width + 'px'; + annotations.style.height = targetRect.height + 'px'; + }; + // Position the contextual bar below the target (or above if below would // collide with the global bar). Mirrors positionBar() in live-browser.js. const positionCtx = () => { @@ -68,7 +83,9 @@ export function initLiveDemo() { const targetRect = target.getBoundingClientRect(); const ctxRect = ctx.getBoundingClientRect(); const GAP = 10; - const BAR_RESERVE = 60; + // Clearance kept below the bar inside the stage. The global bar lives + // outside the stage, so this only needs to be a small visual margin. + const BAR_RESERVE = 24; const belowTop = targetRect.bottom - stageRect.top + GAP; const aboveTop = targetRect.top - stageRect.top - ctxRect.height - GAP; let top; @@ -80,6 +97,13 @@ export function initLiveDemo() { top = stage.clientHeight - ctxRect.height - BAR_RESERVE; } ctx.style.top = top + 'px'; + + // Center the bar on the target horizontally (the target is no longer the + // stage centre), clamped to the stage edges. + const rawLeft = targetRect.left - stageRect.left + targetRect.width / 2 - ctxRect.width / 2; + const maxLeft = stage.clientWidth - ctxRect.width - 8; + ctx.style.left = Math.max(8, Math.min(rawLeft, maxLeft)) + 'px'; + ctx.style.transform = 'none'; }; const moveCursor = (selector, offsetX = 0, offsetY = 0) => { @@ -113,22 +137,27 @@ export function initLiveDemo() { setCtxPhase(PHASE.HIDDEN); cursor.classList.remove('is-visible', 'is-click'); outline.classList.remove('is-visible'); - annotations.classList.remove('is-visible', 'is-comment-visible'); - inputText.textContent = ''; + annotations.classList.remove('is-visible', 'is-pin-visible', 'is-note-visible'); + if (noteText) noteText.textContent = ''; + if (inputText) inputText.textContent = ''; showVariant(0); }; + const clearAnnotations = () => + annotations.classList.remove('is-visible', 'is-pin-visible', 'is-note-visible'); + const setCaption = (text) => { if (text && captionLabel) captionLabel.textContent = text; }; - const typeText = (text, duration) => new Promise((resolve) => { - inputText.textContent = ''; + const typeInto = (el, text, duration) => new Promise((resolve) => { + if (!el) return resolve(); + el.textContent = ''; const per = Math.max(30, Math.floor(duration / text.length)); let i = 0; const tick = () => { if (i >= text.length) return resolve(); - inputText.textContent += text[i++]; + el.textContent += text[i++]; setTimeout(tick, per); }; tick(); @@ -154,15 +183,26 @@ export function initLiveDemo() { case 'open-ctx': setCtxPhase(PHASE.CONFIGURING); break; - case 'cursor-to-input': - moveCursor(root.querySelector('[data-demo-input]')); + case 'cursor-to-button': + // Over the purple "Book Now" button (lower-left). + moveCursor(target, -150, 26); break; - case 'type': - await typeText(s.text, 700); - break; - case 'draw-stroke': + case 'draw-circle': + positionAnnotations(); annotations.classList.add('is-visible'); - setTimeout(() => annotations.classList.add('is-comment-visible'), 600); + break; + case 'drop-pin': + // Near the pin's CSS position (left 46%, top 78%). + moveCursor(target, -16, 52); + cursor.classList.add('is-click'); + setTimeout(() => cursor.classList.remove('is-click'), 220); + annotations.classList.add('is-pin-visible'); + break; + case 'type-note': + annotations.classList.add('is-note-visible'); + await typeInto(noteText, s.text, 800); + break; + case 'hold': break; case 'cursor-to-go': moveCursor(root.querySelector('[data-demo-go]')); @@ -170,7 +210,7 @@ export function initLiveDemo() { case 'click-go': cursor.classList.add('is-click'); setTimeout(() => cursor.classList.remove('is-click'), 260); - annotations.classList.remove('is-visible', 'is-comment-visible'); + clearAnnotations(); setCtxPhase(PHASE.GENERATING); break; case 'show-variant': @@ -237,6 +277,119 @@ export function initLiveDemo() { window.addEventListener('resize', () => requestAnimationFrame(() => { positionOutline(); + positionAnnotations(); positionCtx(); })); } + +/** Collapsed page-chat pill on marketing gbars — mirrors live-browser.js expand UX. */ +export function initGbarPageChat() { + const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; + document.querySelectorAll('[data-demo-gbar-chat]').forEach((chat) => { + const input = chat.querySelector('.live-demo-gbar-chat-input'); + const hint = chat.querySelector('.live-demo-gbar-chat-hint'); + const voice = chat.querySelector('.live-demo-gbar-chat-voice'); + if (!input) return; + + const expand = () => { + chat.classList.add('is-expanded'); + chat.dataset.expanded = 'true'; + if (hint) hint.hidden = true; + input.style.pointerEvents = ''; + requestAnimationFrame(() => input.focus()); + }; + + const collapse = () => { + chat.classList.remove('is-expanded'); + chat.dataset.expanded = 'false'; + if (hint) hint.hidden = false; + input.style.pointerEvents = 'none'; + input.blur(); + }; + + const syncVisual = () => { + if (input.value.length > 0 && !chat.classList.contains('is-expanded')) expand(); + else if (!input.value.length && chat.classList.contains('is-expanded')) { + chat.classList.remove('is-expanded'); + chat.dataset.expanded = 'false'; + if (hint) hint.hidden = false; + } + }; + + chat.style.transition = `width 0.28s ${EASE}, border-color 0.15s ease`; + + input.addEventListener('input', syncVisual); + + chat.addEventListener('click', (e) => { + if (voice?.contains(e.target)) return; + expand(); + }); + + let demoRec = null; + + voice?.addEventListener('click', (e) => { + e.stopPropagation(); + expand(); + const Ctor = window.SpeechRecognition || window.webkitSpeechRecognition; + if (!Ctor) return; + if (voice.dataset.listening === 'true') { + if (demoRec) { + try { demoRec.stop(); } catch { /* ignore */ } + } + return; + } + const demoBase = input.value.trim() ? input.value.trim() + ' ' : ''; + const rec = new Ctor(); + rec.continuous = false; + rec.interimResults = true; + rec.lang = document.documentElement.lang || navigator.language || 'en-US'; + rec.onstart = () => { + voice.classList.add('is-active'); + voice.dataset.listening = 'true'; + voice.setAttribute('aria-pressed', 'true'); + }; + rec.onresult = (event) => { + let transcript = ''; + for (let i = 0; i < event.results.length; i++) { + transcript += event.results[i][0]?.transcript || ''; + } + input.value = (demoBase + transcript).trim(); + syncVisual(); + }; + rec.onend = () => { + voice.classList.remove('is-active'); + voice.dataset.listening = 'false'; + voice.setAttribute('aria-pressed', 'false'); + demoRec = null; + }; + rec.onerror = () => { + voice.classList.remove('is-active'); + voice.dataset.listening = 'false'; + voice.setAttribute('aria-pressed', 'false'); + demoRec = null; + }; + demoRec = rec; + try { rec.start(); } catch { /* ignore */ } + }); + + input.addEventListener('keydown', (e) => { + e.stopPropagation(); + if (e.key === 'Escape') { + e.preventDefault(); + if (input.value.trim()) { + input.value = ''; + syncVisual(); + } else { + collapse(); + } + } + }); + + input.addEventListener('blur', () => { + window.setTimeout(() => { + if (chat.contains(document.activeElement)) return; + if (!input.value.trim()) collapse(); + }, 120); + }); + }); +} diff --git a/site/scripts/data.js b/site/scripts/data.js index 16a0ab53f..df3802bd4 100644 --- a/site/scripts/data.js +++ b/site/scripts/data.js @@ -30,6 +30,20 @@ export const skillFocusAreas = { ] }; +// Slop tells we prevent in each discipline. Same seven categories, but the +// detail line communicates what bad-default we catch — not what we teach. +export const slopFocusAreas = { + 'impeccable': [ + { area: 'Typography', detail: 'No Inter monoculture. No flat hierarchy. No all-caps body.' }, + { area: 'Color & Contrast', detail: 'No purple gradients. No sub-WCAG text. No washed pastels.' }, + { area: 'Spatial Design', detail: 'No cards in cards. No identical grids. No template layouts.' }, + { area: 'Responsive', detail: 'No stiff breakpoints. No mobile-as-afterthought.' }, + { area: 'Interaction', detail: 'No modal abuse. No generic CTAs. No gradient text.' }, + { area: 'Motion', detail: 'No bouncy easing. No decorative fades. No spring overshoot.' }, + { area: 'UX Writing', detail: 'No "Welcome to our platform." No em dashes. No "let\'s dive in".' } + ] +}; + // Guideline counts per dimension (verified from reference files) export const dimensionGuidelineCounts = { 'Typography': 33, @@ -73,7 +87,7 @@ export const commandProcessSteps = { 'optimize': ['Profile', 'Identify', 'Improve', 'Measure'], 'harden': ['Assess', 'Implement', 'Test', 'Verify'], 'onboard': ['Identify', 'Design', 'Guide', 'Measure'], - 'teach': ['Explore', 'Interview', 'Synthesize', 'Save'], + 'init': ['Explore', 'Interview', 'Configure', 'Recommend'], 'document': ['Scan', 'Extract', 'Describe', 'Write'], 'extract': ['Identify', 'Abstract', 'Migrate', 'Document'], 'live': ['Start', 'Select', 'Generate', 'Accept'] @@ -106,7 +120,7 @@ export const commandCategories = { 'harden': 'harden', 'onboard': 'harden', // SYSTEM - setup and tooling - 'teach': 'system', + 'init': 'system', 'document': 'system', 'extract': 'system', 'live': 'system' @@ -142,7 +156,7 @@ export const commandRelationships = { 'optimize': { flow: 'Harden: Performance improvements' }, 'harden': { combinesWith: ['optimize'], flow: 'Harden: Edge cases, error handling, and i18n' }, 'onboard': { combinesWith: ['clarify', 'delight'], flow: 'Harden: First-run experiences and empty states' }, - 'teach': { flow: 'System: One-time project design context setup' }, + 'init': { flow: 'System: One-time project setup. Context, live config, next steps' }, 'extract': { flow: 'System: Extract design system components and tokens' }, 'live': { flow: 'System: Visual variant mode in the browser' } }; diff --git a/site/scripts/demo-renderer.js b/site/scripts/demo-renderer.js index bd7061118..a831c7200 100644 --- a/site/scripts/demo-renderer.js +++ b/site/scripts/demo-renderer.js @@ -42,10 +42,10 @@ export function renderCommandDemo(commandId) {
- /impeccable teach + /impeccable init one-time setup
- Scans your codebase, interviews you about brand and audience, then saves a Design Context that all other commands use automatically. + Scans your codebase, interviews you about brand and audience, saves a Design Context all other commands use, configures live mode, and points you at what to run next.
@@ -55,7 +55,7 @@ export function renderCommandDemo(commandId) { Runs /shape to plan UX first, loads the right references, then builds and iterates visually until the result delights.
-
Start with /impeccable teach once per project. Then use the other modes as needed.
+
Start with /impeccable init once per project. Then use the other modes as needed.
@@ -100,8 +100,8 @@ export function renderCommandDemo(commandId) { `; } - // teach sets up the project's design context, show the flow - if (commandId === 'teach') { + // init sets up the project's design context, show the flow + if (commandId === 'init') { return `
@@ -122,9 +122,15 @@ export function renderCommandDemo(commandId) {
- 3. Save + 3. Configure
- Writes a PRODUCT.md file with users, brand, aesthetic direction, and design principles. Every future command reads it automatically. + Writes PRODUCT.md with users, brand, and design principles, optionally a DESIGN.md visual spec, and sets up live mode so it just works. +
+
+
+ 4. Recommend +
+ Points you at the best commands to run next, picked from what the scan found. No more guessing where to start.
Run once per project. Then forget it exists.
@@ -169,7 +175,7 @@ export function renderCommandDemo(commandId) { return `
-
+
Visual demo for /${commandId} coming soon
@@ -281,4 +287,3 @@ export function setupDemoTabs() { } - diff --git a/site/scripts/utils/scroll.js b/site/scripts/utils/scroll.js index 2e149e86f..b19eb1849 100644 --- a/site/scripts/utils/scroll.js +++ b/site/scripts/utils/scroll.js @@ -2,23 +2,35 @@ // `behavior: 'instant'` explicitly overrides any CSS `scroll-behavior: smooth` // from a stylesheet we don't own; `behavior: 'auto'` would defer to CSS. export function initAnchorScroll() { + const getAnchorOffset = () => { + const header = document.querySelector('[data-site-header]'); + return (header?.getBoundingClientRect().height || 0) + 28; + }; + document.querySelectorAll('a[href^="#"]').forEach((anchor) => { anchor.addEventListener("click", (e) => { e.preventDefault(); const target = document.querySelector(anchor.getAttribute("href")); if (target) { - const offset = 40; + const offset = getAnchorOffset(); const targetPosition = target.getBoundingClientRect().top + window.scrollY - offset; window.scrollTo({ top: targetPosition, behavior: 'instant' }); } }); }); + + return getAnchorOffset; } export function initHashTracking() { const sections = document.querySelectorAll('section[id]'); if (!sections.length) return; + const getAnchorOffset = () => { + const header = document.querySelector('[data-site-header]'); + return (header?.getBoundingClientRect().height || 0) + 28; + }; + let currentHash = window.location.hash.slice(1) || ''; let ticking = false; @@ -88,7 +100,7 @@ export function initHashTracking() { currentHash = hash; let clicked = false; const jump = () => { - const offset = 40; + const offset = getAnchorOffset(); const targetPosition = target.getBoundingClientRect().top + window.scrollY - offset; window.scrollTo({ top: targetPosition, behavior: 'instant' }); if (!clicked && hash.startsWith('cmd-') && target.classList.contains('manual-entry')) { @@ -104,4 +116,3 @@ export function initHashTracking() { // No hash — don't set one on initial load } } - diff --git a/site/styles/changelog-faq-kinpaku.css b/site/styles/changelog-faq-kinpaku.css new file mode 100644 index 000000000..29dce48fa --- /dev/null +++ b/site/styles/changelog-faq-kinpaku.css @@ -0,0 +1,303 @@ +/* ============================================================================ + /changelog and /faq — Neo Kinpaku redesign + + Two self-contained support pages on a kinpaku surface. Both share the same + container + header treatment; only the content differs. + ============================================================================ */ + +@import "./kinpaku-tokens.css"; + +/* Page scope — kinpaku-surface (set on body via Base) provides the dark bg; + this class wraps the article content for the centered measure. */ +.cf-page { + max-width: 760px; + margin: 0 auto; + padding: clamp(48px, 6vw, 96px) clamp(20px, 4vw, 48px) clamp(80px, 10vw, 140px); +} + +.cf-eyebrow { + display: block; + font-family: var(--ks-mono); + font-size: var(--ks-type-eyebrow-size); + letter-spacing: var(--ks-type-eyebrow-track); + text-transform: uppercase; + color: var(--ks-kinpaku); + margin: 0 0 18px; +} + +.cf-title { + font-family: var(--ks-font-display); + font-weight: var(--ks-type-display-weight); + font-size: var(--ks-type-display-size); + line-height: var(--ks-type-display-line); + letter-spacing: var(--ks-type-display-track); + color: var(--ks-champagne); + margin: 0 0 20px; + text-wrap: balance; +} + +.cf-lede { + font-size: 1.05rem; + line-height: 1.65; + color: var(--ks-text); + margin: 0 0 clamp(40px, 5vw, 64px); + max-width: 60ch; +} + +/* ============================================================================ + Changelog + ============================================================================ */ + +.cf-items { + padding-left: 1.1rem; + margin: 0; +} +.cf-items li { + color: var(--ks-text); + line-height: 1.7; + margin: 0 0 12px; +} +.cf-items li:last-child { margin-bottom: 0; } +.cf-items strong { color: var(--ks-champagne); font-weight: 500; } +.cf-items code { + font-family: var(--ks-mono); + color: var(--ks-kinpaku); + background: transparent; + padding: 0; +} +.cf-items a { + color: var(--ks-patina); + text-decoration: underline; + text-underline-offset: 3px; +} +.cf-items a:hover { color: var(--ks-patina-pale); } + +.cf-entry { + margin: 0 0 22px; + padding: 20px 24px; + border: 1px solid var(--ks-rule); + border-radius: 4px; + background: + linear-gradient(135deg, oklch(78% 0.12 82 / 0.04), transparent 44%), + oklch(8% 0.006 95 / 0.84); +} +.cf-entry-head { + display: flex; + align-items: baseline; + gap: 14px; + margin: 0 0 12px; + font-family: var(--ks-mono); + font-size: 0.86rem; +} +.cf-version { color: var(--ks-kinpaku); } +.cf-date { color: var(--ks-text-muted); } + +/* Current release — brighter kinpaku border, a gold wash, and a glow so the + newest version reads as the one you're on at a glance. */ +.cf-entry--current { + border-color: oklch(78% 0.12 82 / 0.5); + background: + linear-gradient(135deg, oklch(78% 0.12 82 / 0.10), transparent 52%), + oklch(11% 0.01 82 / 0.92); + box-shadow: 0 0 30px oklch(78% 0.12 82 / 0.14); +} +.cf-current-badge { + margin-left: auto; + align-self: center; + padding: 3px 11px; + border: 1px solid oklch(78% 0.12 82 / 0.5); + border-radius: 999px; + background: oklch(78% 0.12 82 / 0.12); + color: var(--ks-kinpaku); + font-size: 0.64rem; + letter-spacing: 0.16em; + text-transform: uppercase; +} + +/* One-line summary that sits above the bullets on a big release, so the + gist reads before the detail. */ +.cf-entry-lead { + margin: 0 0 20px; + color: var(--ks-champagne); + font-size: 1.05rem; + line-height: 1.5; +} + +/* Before/after proof pair — two real eval screenshots side by side. */ +.cf-ba { + margin: 0 0 22px; +} +.cf-ba-pair { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} +@media (max-width: 560px) { + .cf-ba-pair { grid-template-columns: 1fr; } +} +.cf-ba-shot { + position: relative; + border: 1px solid var(--ks-rule); + border-radius: 5px; + overflow: hidden; + background: oklch(8% 0.006 95); +} +.cf-ba-shot img { + display: block; + width: 100%; + height: auto; +} +.cf-ba-tag { + position: absolute; + top: 10px; + left: 10px; + padding: 3px 10px; + border-radius: 999px; + font-family: var(--ks-mono); + font-size: 0.64rem; + letter-spacing: 0.16em; + text-transform: uppercase; + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); +} +.cf-ba-tag--before { + color: var(--ks-text-muted); + background: oklch(8% 0.006 95 / 0.7); + border: 1px solid var(--ks-rule); +} +.cf-ba-tag--after { + color: var(--ks-kinpaku); + background: oklch(78% 0.12 82 / 0.14); + border: 1px solid oklch(78% 0.12 82 / 0.4); +} +.cf-ba figcaption { + margin-top: 12px; + color: var(--ks-text-muted); + font-size: 0.86rem; + line-height: 1.55; +} + +/* Stat row — three big numbers from the bias-mining sweep. */ +.cf-stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; + margin: 0 0 30px; + padding: 22px 24px; + border: 1px solid var(--ks-rule); + border-radius: 5px; + background: + linear-gradient(135deg, oklch(78% 0.12 82 / 0.04), transparent 50%), + oklch(8% 0.006 95 / 0.84); +} +@media (max-width: 560px) { + .cf-stats { grid-template-columns: 1fr; gap: 18px; } +} +.cf-stat { + display: flex; + flex-direction: column; + gap: 6px; +} +.cf-stat-num { + font-family: var(--ks-font-display); + font-weight: var(--ks-type-display-weight); + font-size: clamp(2.2rem, 5vw, 3rem); + line-height: 1; + color: var(--ks-kinpaku); +} +.cf-stat-label { + color: var(--ks-text); + font-size: 0.84rem; + line-height: 1.45; +} +.cf-stats-note { + grid-column: 1 / -1; + margin: 4px 0 0; + padding-top: 14px; + border-top: 1px solid var(--ks-rule); + color: var(--ks-text-muted); + font-size: 0.78rem; + line-height: 1.5; +} + +/* ============================================================================ + FAQ + ============================================================================ */ + +.cf-faq-list { + display: grid; + gap: 8px; +} + +.cf-faq-item { + border: 1px solid var(--ks-rule); + border-radius: 4px; + background: + linear-gradient(135deg, oklch(78% 0.12 82 / 0.045), transparent 44%), + oklch(8% 0.006 95 / 0.84); +} + +.cf-faq-question { + position: relative; + padding: 22px 64px 22px 26px; + font-family: var(--ks-font); + font-size: 1.02rem; + font-weight: 500; + color: var(--ks-kinpaku); + cursor: pointer; + list-style: none; +} +.cf-faq-question::-webkit-details-marker { display: none; } +.cf-faq-question::after { + content: "+"; + position: absolute; + right: 24px; + top: 50%; + transform: translateY(-50%); + color: var(--ks-patina); + font-size: 1.4rem; + line-height: 1; + transition: transform 160ms ease; +} +.cf-faq-item[open] > .cf-faq-question::after { + transform: translateY(-50%) rotate(45deg); +} +.cf-faq-question:hover { + color: var(--ks-patina); +} +.cf-faq-question code { + font-family: var(--ks-mono); + font-size: 0.92em; + color: inherit; +} + +.cf-faq-answer { + padding: 0 26px 24px; + color: var(--ks-text); +} +.cf-faq-answer p { + margin: 0 0 12px; + line-height: 1.7; +} +.cf-faq-answer p:last-child { margin-bottom: 0; } +.cf-faq-answer ul { + margin: 8px 0 12px; + padding-left: 1.1rem; +} +.cf-faq-answer li { + margin: 0 0 6px; + line-height: 1.6; +} +.cf-faq-answer strong { color: var(--ks-champagne); font-weight: 500; } +.cf-faq-answer code { + font-family: var(--ks-mono); + color: var(--ks-kinpaku); + background: transparent; + padding: 0; +} +.cf-faq-answer a { + color: var(--ks-patina); + text-decoration: underline; + text-underline-offset: 3px; +} +.cf-faq-answer a:hover { color: var(--ks-patina-pale); } diff --git a/site/styles/design-system.css b/site/styles/design-system.css new file mode 100644 index 000000000..ba81a4d07 --- /dev/null +++ b/site/styles/design-system.css @@ -0,0 +1,1671 @@ +/* ============================================================================ + Impeccable — Neo Kinpaku Design System + Restrained dark surface. Kinpaku gold and verdigris patina carry the brand. + No nested cards, no decorative chrome, no washed-out textures. + + Brand-defining tokens (kinpaku, pale, rich, patina, champagne, text, font, + ease, vermilion) live in kinpaku-tokens.css and are inherited from :root. + Only page-specific neutrals (ink/surface shades, rule alphas, muted) stay + scoped here. + ============================================================================ */ + +@import "./kinpaku-tokens.css"; +/* Divider geometry, tone modifiers, and texture-fill modifiers — shared with + the homepage. Must stay at the top of the file because CSS @import is only + valid before any other rules. */ +@import "./divider-patterns.css"; + +.kinpaku-system-page { + /* Legacy alias layer. All real values live in kinpaku-tokens.css at :root; + this page's selectors still reference the old short names (--ks-ink, + --ks-surface, --ks-muted, --ks-mute-deep), so we forward them to the + forward names. Phase 4 of the design-system rework will rename the + selectors and delete this block. */ + --ks-ink: var(--ks-lacquer); + --ks-ink-deep: var(--ks-lacquer-deep); + --ks-surface: var(--ks-lacquer-raised); + --ks-muted: var(--ks-text-muted); + --ks-mute-deep: var(--ks-text-mute-deep); + + color: var(--ks-text); + background: var(--ks-lacquer); + font-family: var(--ks-font); + font-weight: 300; + margin: 0; +} + +.kinpaku-system-page *, +.kinpaku-system-page *::before, +.kinpaku-system-page *::after { + box-sizing: border-box; +} + +.kinpaku-system-page img, +.kinpaku-system-page svg { + display: block; + max-width: 100%; +} + +.kinpaku-system-page a { + color: inherit; + text-decoration: none; +} + +.kinpaku-system-page button, +.kinpaku-system-page input, +.kinpaku-system-page select { + font: inherit; +} + +.kinpaku-system { + position: relative; + isolation: isolate; +} + +/* Skip link */ +.kinpaku-system-page .skip-link { + position: fixed; + top: -96px; + left: 50%; + z-index: 1000; + transform: translateX(-50%); + padding: 10px 16px; + border-radius: 2px; + color: var(--ks-ink-deep); + background: var(--ks-kinpaku); + font-weight: 500; + text-decoration: none; + transition: top 180ms var(--ks-ease); +} +.kinpaku-system-page .skip-link:focus { top: 16px; } +.kinpaku-system-page .skip-link:focus:not(:focus-visible) { top: -96px; } + +/* ============================================================================ + Hero + ============================================================================ */ + +.ks-hero { + position: relative; + min-height: 100svh; + display: grid; + grid-template-rows: auto 1fr auto; + padding: 36px 56px 0; + overflow: hidden; +} + +.ks-hero-art { + position: absolute; + inset: 0; + z-index: 0; + background: + linear-gradient(90deg, var(--ks-ink) 0%, oklch(4% 0.004 95 / 0.65) 26%, transparent 48%, transparent 88%, var(--ks-ink) 100%), + linear-gradient(180deg, oklch(4% 0.004 95 / 0.35) 0%, transparent 35%, var(--ks-ink) 100%), + url("/assets/neo-kinpaku/candidates/finalists/m-01-v2-01.png") center / cover no-repeat; + filter: saturate(1.22) contrast(1.1); +} + +/* Header */ +.ks-topbar { + position: relative; + z-index: 2; + display: flex; + align-items: center; + justify-content: space-between; + gap: 32px; +} + +/* Brand lockup (.ks-brand, .ks-mark, .ks-wordmark) is in kinpaku-kit.css. */ + +.ks-nav { + display: flex; + align-items: center; + gap: 38px; +} + +.ks-nav a { + color: var(--ks-champagne); + font-size: 0.92rem; + text-decoration: none; + letter-spacing: 0.01em; + transition: color 180ms var(--ks-ease); +} + +.ks-nav a:hover { color: var(--ks-kinpaku); } + +.ks-nav a[aria-current="page"] { + color: var(--ks-kinpaku); + position: relative; +} +.ks-nav a[aria-current="page"]::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: -8px; + height: 1px; + background: var(--ks-kinpaku); +} + +/* Hero body */ +.ks-hero-body { + position: relative; + z-index: 1; + display: grid; + align-content: center; + padding: 80px 0 96px; + max-width: 940px; +} + +.ks-hero-eyebrow { + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-size: 0.7rem; + letter-spacing: 0.32em; + text-transform: uppercase; + margin: 0 0 28px; +} + +.ks-hero-wordmark { + display: block; + color: var(--ks-kinpaku); + font-family: var(--ks-font-display); + font-weight: 300; + font-size: clamp(3.2rem, 7.2vw, 5.6rem); + line-height: 1; + letter-spacing: 0.22em; + margin: 0; + padding-right: 0.22em; +} + +.ks-hero-line { + margin: 28px 0 0; + max-width: 540px; + color: var(--ks-champagne); + font-size: clamp(1.05rem, 1.4vw, 1.25rem); + font-weight: 300; + line-height: 1.55; +} + +.ks-hero-actions { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-top: 36px; +} + +.ks-hero-note { + margin-top: 56px; + display: grid; + grid-template-columns: 56px minmax(0, 1fr); + gap: 18px; + align-items: start; + max-width: 480px; + color: var(--ks-muted); + font-size: 0.9rem; + line-height: 1.6; +} + +.ks-hero-note-pin { + height: 40px; + border-top: 1px solid var(--ks-kinpaku); + border-left: 1px solid var(--ks-kinpaku); + position: relative; +} +.ks-hero-note-pin::before { + content: ""; + position: absolute; + top: -3px; + left: -3px; + width: 5px; + height: 5px; + border-radius: 999px; + background: var(--ks-kinpaku); +} + +/* Category rail */ +.ks-category-rail { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: repeat(5, 1fr); + border-top: 1px solid var(--ks-rule); + margin: 0 -56px; + background: linear-gradient(180deg, transparent, oklch(4% 0.004 95 / 0.8)); +} + +.ks-category-rail a { + display: flex; + align-items: center; + justify-content: center; + gap: 14px; + padding: 26px 16px; + color: var(--ks-champagne); + text-decoration: none; + font-size: 0.96rem; + font-weight: 300; + letter-spacing: 0.02em; + border-right: 1px solid var(--ks-rule); + transition: color 180ms var(--ks-ease); +} + +.ks-category-rail a:last-child { border-right: 0; } +.ks-category-rail a:hover { color: var(--ks-kinpaku); } + +.ks-category-rail svg { + width: 18px; + height: 18px; + flex: none; + color: var(--ks-kinpaku); +} + +/* ============================================================================ + Sections + ============================================================================ */ + +/* Section scaffolding (.ks-section, .ks-section-head, .ks-section-eyebrow, + .ks-subsection, .ks-subsection-label, .ks-section-sub) is in + kinpaku-kit.css. */ + +/* Hero candidate gallery */ +.ks-candidate-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 18px; +} + +.ks-candidate { + position: relative; + margin: 0; + border: 1px solid var(--ks-rule); + border-radius: 2px; + overflow: hidden; + background: var(--ks-ink-deep); +} + +.ks-candidate img { + display: block; + width: 100%; + height: auto; + aspect-ratio: 3 / 2; + object-fit: cover; +} + +.ks-icon-candidate img { + aspect-ratio: 1; +} + +.ks-icon-family-label { + margin: 32px 0 14px; + font-family: var(--ks-mono); + font-size: 0.72rem; + letter-spacing: 0.22em; + text-transform: uppercase; + color: var(--ks-kinpaku); + font-weight: 400; +} + +.ks-icon-family-label:first-of-type { + margin-top: 4px; +} + +/* Hand-drawn icon-mark concept tiles. Each tile shows the candidate inside the + actual 38px icon box so it's evaluated at the size it'd ship at. */ +.ks-icon-concepts { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 18px; +} + +.ks-icon-concept { + margin: 0; + display: grid; + gap: 18px; + justify-items: center; + padding: 36px 18px 18px; + border: 1px solid var(--ks-rule); + border-radius: 2px; + background: var(--ks-ink-deep); +} + +.ks-icon-concept-box { + width: 38px; + height: 38px; + display: grid; + place-items: center; + border: 1px solid var(--ks-kinpaku); + color: var(--ks-kinpaku); + border-radius: 2px; +} + +.ks-icon-concept-box svg { + width: 14px; + height: 22px; + display: block; +} + +.ks-icon-concept figcaption { + font-family: var(--ks-mono); + font-size: 0.66rem; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--ks-muted); + text-align: center; +} + +/* Canonical mark, shown at a range of sizes. No container box — the tile is + the mark. */ +.ks-mark-scale { + display: flex; + align-items: flex-end; + gap: 40px; +} +.ks-mark-scale-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 14px; + margin: 0; +} +.ks-mark-glyph { + display: inline-grid; + place-items: center; + color: var(--ks-kinpaku); +} +.ks-mark-glyph svg { width: 100%; height: 100%; display: block; } +.ks-mark-scale-item figcaption { + font-family: var(--ks-mono); + font-size: 0.62rem; + letter-spacing: 0.14em; + color: var(--ks-muted); +} + +.ks-candidate figcaption { + position: absolute; + left: 10px; + top: 10px; + padding: 2px 8px; + background: oklch(4% 0.004 95 / 0.74); + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-size: 0.66rem; + letter-spacing: 0.18em; + text-transform: uppercase; + border: 1px solid var(--ks-rule-strong); + border-radius: 2px; + pointer-events: none; +} + +.ks-candidate.is-kept figcaption { + color: var(--ks-patina); + border-color: var(--ks-patina); +} + +/* Keyed (transparent) candidates: split background to reveal alpha */ +.ks-candidate-keyed-stack { + position: relative; + aspect-ratio: 3 / 2; +} + +.ks-candidate-keyed-stack img { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + z-index: 2; +} + +.ks-candidate-keyed-checker { + position: absolute; + inset: 0; + z-index: 1; + background: + linear-gradient(90deg, var(--ks-ink-deep) 50%, transparent 50%), + linear-gradient(90deg, transparent 50%, var(--ks-graphite) 50%); + background-size: 100% 100%, 100% 100%; +} + +@media (max-width: 1080px) { + .ks-candidate-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} +@media (max-width: 640px) { + .ks-candidate-grid { grid-template-columns: 1fr; } +} + +/* ============================================================================ + Buttons + ============================================================================ */ + +/* Buttons (.ks-button, .ks-button-primary, .ks-button-secondary, + .ks-button-ghost, .ks-button-disabled, .ks-button-arrow, .ks-button-row, + focus-visible) are in kinpaku-kit.css. */ + +/* ============================================================================ + Foundations: Color swatches (flat, no card chrome) + ============================================================================ */ + +.ks-swatch-grid { + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + gap: 28px; +} + +.ks-swatch { + display: grid; + gap: 12px; +} + +.ks-swatch-chip { + width: 100%; + aspect-ratio: 5 / 4; + border: 1px solid var(--ks-rule); + border-radius: 2px; +} + +.ks-swatch-chip.is-kinpaku { + border-color: transparent; + background: url("/assets/neo-kinpaku/kinpaku-gold-leaf.png") 50% 50% / cover no-repeat; + filter: saturate(1.18) contrast(1.08); +} + +.ks-swatch-chip.is-patina { + border-color: transparent; + background: url("/assets/neo-kinpaku/verdigris-patina.png") 50% 50% / cover no-repeat; + filter: saturate(1.2) contrast(1.06); +} + +.ks-swatch-chip.is-ink { background: var(--ks-ink); border-color: oklch(20% 0.006 95); } +.ks-swatch-chip.is-surface { background: var(--ks-surface); } +.ks-swatch-chip.is-champagne { background: var(--ks-champagne); border-color: transparent; } +.ks-swatch-chip.is-vermilion { background: var(--ks-vermilion); border-color: transparent; } + +.ks-swatch-name { + display: block; + color: var(--ks-champagne); + font-size: 0.92rem; + font-weight: 400; +} + +.ks-swatch-role { + display: block; + color: var(--ks-muted); + font-size: 0.78rem; + margin-top: 2px; +} + +.ks-swatch-value { + display: block; + color: var(--ks-mute-deep); + font-family: var(--ks-mono); + font-size: 0.7rem; + margin-top: 6px; +} + +/* ============================================================================ + Foundations: Type specs (no card) + ============================================================================ */ + +.ks-type-stack { + display: grid; + gap: 28px; +} + +.ks-type-row { + display: grid; + grid-template-columns: 120px 1fr; + gap: 32px; + align-items: baseline; + padding-bottom: 28px; + border-bottom: 1px solid var(--ks-rule); +} + +.ks-type-row:last-child { + padding-bottom: 0; + border-bottom: 0; +} + +.ks-type-label { + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-size: 0.66rem; + letter-spacing: 0.24em; + text-transform: uppercase; +} + +.ks-type-spec-line { + color: var(--ks-muted); + font-family: var(--ks-mono); + font-size: 0.7rem; + letter-spacing: 0.06em; + margin-top: 8px; +} + +/* Type samples — render the production scale, full strength. The samples + read directly from --ks-type-* tokens so the design-system page literally + shows what the homepage ships. data-scale on each sample selects which + token family applies. */ + +.ks-type-sample { + margin: 0; + font-family: var(--ks-font); + color: var(--ks-text); +} + +.ks-type-sample[data-scale="wordmark"] { + color: var(--ks-kinpaku); + font-family: var(--ks-font-display); + font-weight: var(--ks-type-display-weight); + font-size: var(--ks-type-wordmark-size); + letter-spacing: var(--ks-type-wordmark-track); + text-transform: uppercase; + line-height: 1; +} + +.ks-type-sample[data-scale="display"] { + color: var(--ks-champagne); + font-family: var(--ks-font-display); + font-weight: var(--ks-type-display-weight); + font-size: var(--ks-type-display-size); + line-height: var(--ks-type-display-line); + letter-spacing: var(--ks-type-display-track); + text-wrap: balance; +} + +.ks-type-sample[data-scale="headline"] { + color: var(--ks-champagne); + font-family: var(--ks-font-display); + font-weight: var(--ks-type-headline-weight); + font-size: var(--ks-type-headline-size); + line-height: var(--ks-type-headline-line); + text-wrap: balance; +} + +.ks-type-sample[data-scale="body"] { + color: var(--ks-text); + font-size: var(--ks-type-body-size); + line-height: var(--ks-type-body-line); + max-width: 65ch; +} + +.ks-type-sample[data-scale="mono"] { + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-size: var(--ks-type-mono-size); + letter-spacing: var(--ks-type-mono-track); + text-transform: uppercase; +} + +/* ============================================================================ + Foundations: Paired material + solid swatches + The two brand anchors (kinpaku, verdigris) each get a side-by-side display + of their material texture and their solid color token. Reinforces that + "material" and "color" are two sides of the same anchor, not separate + categories. + ============================================================================ */ + +.ks-pair-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 28px; +} + +.ks-pair { + display: grid; + grid-template-columns: 1fr 1fr; + column-gap: 4px; + row-gap: 12px; +} + +.ks-pair-chip { + width: 100%; + aspect-ratio: 5 / 4; + border-radius: 2px; + grid-row: 1; + border: 1px solid transparent; +} + +.ks-pair-chip.is-kinpaku-leaf { + background: url("/assets/neo-kinpaku/kinpaku-gold-leaf.png") 50% 50% / cover no-repeat; + filter: saturate(1.18) contrast(1.08); +} + +.ks-pair-chip.is-kinpaku-solid { + background: var(--ks-kinpaku); +} + +.ks-pair-chip.is-patina-leaf { + background: url("/assets/neo-kinpaku/verdigris-patina.png") 50% 50% / cover no-repeat; + filter: saturate(1.2) contrast(1.06); +} + +.ks-pair-chip.is-patina-solid { + background: var(--ks-patina); +} + +.ks-pair-name { + grid-column: 1 / -1; + color: var(--ks-champagne); + font-size: 0.92rem; + font-weight: 400; +} + +.ks-pair-role { + grid-column: 1 / -1; + color: var(--ks-muted); + font-size: 0.78rem; + line-height: 1.5; + margin-top: -6px; +} + +.ks-pair-value { + grid-column: 1 / -1; + color: var(--ks-mute-deep); + font-family: var(--ks-mono); + font-size: 0.7rem; + margin-top: -2px; +} + +@media (max-width: 700px) { + .ks-pair-grid { grid-template-columns: 1fr; } +} + +/* ============================================================================ + Inline mono code chip (motion section, in-prose references to tokens) + ============================================================================ */ + +.ks-mono-inline { + font-family: var(--ks-mono); + font-size: 0.8em; + color: var(--ks-kinpaku); + padding: 0.05em 0.4em; + border: 1px solid oklch(78% 0.12 82 / 0.18); + border-radius: 2px; +} + +/* ============================================================================ + Container patterns: Canonical bento showcase + Wraps a live .ks-bento at reduced height with a "Used in production" + annotation. The frame keeps the bento from breaking out of the section + because the doc's canonical sample is illustrative, not full-bleed. + ============================================================================ */ + +.ks-canonical { + display: grid; + gap: 18px; +} + +.ks-canonical-frame { + border: 1px solid var(--ks-rule); + border-radius: 2px; + padding: 16px; + background: oklch(0.10 0.005 95); + overflow: hidden; +} + +/* Override the .ks-bento's top/bottom border (which is brand-bearing 8px + plinth) for the in-doc sample so the frame's border carries the contain. */ +.ks-canonical-bento { + border-top-width: 0; + border-bottom-width: 0; +} + +.ks-canonical-bento .ks-bento-tile { + padding: 24px; + gap: 8px; +} + +.ks-canonical-foot { + display: grid; + gap: 6px; + color: var(--ks-muted); + font-size: 0.88rem; + line-height: 1.55; + max-width: 78ch; +} + +.ks-canonical-foot strong { + color: var(--ks-champagne); + font-weight: 500; +} + +.ks-canonical-meta { + font-family: var(--ks-mono); + font-size: 0.72rem; + letter-spacing: 0.04em; + color: var(--ks-mute-deep); + margin: 0; +} + +.ks-canonical-used { + color: var(--ks-kinpaku); + text-transform: uppercase; + letter-spacing: 0.18em; +} + +.ks-canonical-meta a { + color: var(--ks-champagne); + border-bottom: 1px solid var(--ks-rule); +} + +.ks-canonical-meta a:hover { + color: var(--ks-kinpaku); + border-bottom-color: var(--ks-kinpaku); +} + +/* ============================================================================ + Foundations: Logo download tiles + ============================================================================ */ + +.ks-logo-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18px; +} + +.ks-logo-tile { + margin: 0; + display: grid; + gap: 0; + border: 1px solid var(--ks-rule); + border-radius: 2px; + overflow: hidden; +} + +.ks-logo-tile img { + display: block; + width: auto; + max-width: 78%; + height: auto; + margin: 56px auto; +} + +.ks-logo-tile.is-dark { + background: oklch(7% 0.006 95); +} + +.ks-logo-tile.is-light { + background: oklch(94% 0.012 82); +} + +.ks-logo-tile figcaption { + display: flex; + align-items: baseline; + justify-content: space-between; + padding: 14px 18px; + border-top: 1px solid var(--ks-rule); + background: oklch(9% 0.006 95); + color: var(--ks-muted); +} + +.ks-logo-tile-label { + font-family: var(--ks-mono); + font-size: 0.66rem; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--ks-kinpaku); +} + +.ks-logo-tile-links { + display: flex; + gap: 14px; + font-family: var(--ks-mono); + font-size: 0.72rem; +} + +.ks-logo-tile-links a { + color: var(--ks-text); + text-decoration: none; + border-bottom: 1px solid oklch(40% 0.015 95); + padding-bottom: 1px; + transition: color 160ms ease, border-color 160ms ease; +} + +.ks-logo-tile-links a:hover { + color: var(--ks-kinpaku); + border-bottom-color: var(--ks-kinpaku); +} + +.ks-logo-note { + margin: 18px 0 0; + color: var(--ks-muted); + font-size: 0.88rem; + line-height: 1.55; + max-width: 62ch; +} + +@media (max-width: 720px) { + .ks-logo-grid { grid-template-columns: 1fr; } +} + +/* ============================================================================ + Container prototypes + Six framing approaches that group content without nesting card-in-card. + The .ksp-tile is the prototype's outer frame (only there so the user can + compare techniques side-by-side). The .ksp-stage is the dark surface each + prototype "lives on" — that's the real container under test. + ============================================================================ */ + +.ks-container-prototypes { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 28px; +} + +.ksp-tile { + margin: 0; + display: grid; + gap: 0; + border: 1px solid var(--ks-rule); + border-radius: 2px; + background: oklch(9% 0.006 95); + overflow: hidden; +} + +.ksp-tile-head { + display: grid; + grid-template-columns: auto auto 1fr; + align-items: baseline; + gap: 14px; + padding: 14px 22px; + border-bottom: 1px solid var(--ks-rule); + background: oklch(11% 0.006 95); + font-family: var(--ks-mono); +} + +.ksp-tile-id { + color: var(--ks-kinpaku); + font-size: 0.7rem; + letter-spacing: 0.18em; + text-transform: uppercase; +} + +.ksp-tile-name { + color: var(--ks-champagne); + font-family: var(--ks-font); + font-size: 0.95rem; +} + +.ksp-tile-desc { + color: var(--ks-muted); + font-size: 0.78rem; + text-align: right; +} + +/* The "stage" is the lacquer surface each prototype sits on. Generous padding + so the framing technique has air to breathe; the real test is whether the + content feels grouped without sitting inside a competing card. */ +.ksp-stage { + position: relative; + padding: 56px 48px; + min-height: 240px; + display: flex; + align-items: center; +} + +.ksp-stage--dark { + background: var(--ks-ink); +} + +/* Sample content typography — shared across all six prototypes so framing + is the only visual variable. */ +.ksp-eyebrow { + margin: 0 0 12px; + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-size: 0.7rem; + letter-spacing: 0.18em; + text-transform: uppercase; +} + +.ksp-title { + margin: 0 0 12px; + color: var(--ks-champagne); + font-family: var(--ks-font); + font-weight: 300; + font-size: 1.6rem; + line-height: 1.15; + letter-spacing: -0.005em; +} + +.ksp-body { + margin: 0; + color: var(--ks-text); + font-size: 0.95rem; + line-height: 1.55; + max-width: 46ch; +} + +/* B · Numbered marginalia — large outlined number in the gutter. */ +.ksp-style-b { + display: grid; + grid-template-columns: auto 1fr; + gap: 28px; + align-items: start; +} + +.ksp-marginal-num { + color: var(--ks-kinpaku); + font-family: var(--ks-font); + font-weight: 300; + font-size: 4.4rem; + line-height: 0.9; + letter-spacing: -0.02em; +} + +/* F · Kinpaku slab anchor — tiny full-vibrancy gold-leaf chip behind the + eyebrow. The texture is brand-bearing, the rest is clean lacquer. */ +.ksp-style-f .ksp-eyebrow--chip { + display: inline-block; + padding: 6px 14px; + margin-bottom: 16px; + color: var(--ks-ink-deep); + background: url("/assets/neo-kinpaku/kinpaku-gold-leaf.png") 50% 50% / cover no-repeat; + filter: saturate(1.15) contrast(1.08); + font-weight: 600; + letter-spacing: 0.16em; +} + +/* G · Tinted plinth — flat neutral lift. No border, no shadow, no inner + shape. Pure background-color step from the lacquer (7%) up to 17% neutral + gray. Zero chroma (the "0 0" in oklch) keeps it neutral so the gold accents + in the content carry the warmth on their own. The dark-mode equivalent of + the light-mode "raise content by 4% lightness" trick. */ +.ksp-style-g { + background: oklch(0.17 0 0); + padding: 28px 32px; + border-radius: 2px; +} + +@media (max-width: 900px) { + .ks-container-prototypes { grid-template-columns: 1fr; } +} + +/* ============================================================================ + Divider patterns (CSS-only, no image assets) + Each pattern uses currentColor or var(--ks-kinpaku) so the gold tone follows + the brand token. Use these on full-width section transitions or as accent + rules above eyebrow labels. + ============================================================================ */ + +.ks-divider-stack { + display: grid; + gap: 36px; +} + +.ksd-row { + margin: 0; +} + +.ksd-row figcaption { + font-family: var(--ks-mono); + font-size: 0.66rem; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--ks-muted); + margin-bottom: 14px; +} + +/* Tone comparison row inside a divider figure: small label + the actual + divider, repeated for each of kinpaku / patina / plinth / textures. */ +.ksd-tones { + display: grid; + gap: 14px; +} +.ksd-tone { + display: grid; + grid-template-columns: 72px 1fr; + gap: 18px; + align-items: center; +} +.ksd-tone-label { + font-family: var(--ks-mono); + font-size: 0.6rem; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--ks-muted); +} + +/* ============================================================================ + Foundations: Accent textures (full vibrancy, three only) + ============================================================================ */ + +.ks-accent-stack { + display: grid; + gap: 28px; +} + +.ks-accent-row { + display: grid; + gap: 14px; +} + +.ks-accent-row-label { + display: flex; + align-items: baseline; + justify-content: space-between; + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-size: 0.66rem; + letter-spacing: 0.24em; + text-transform: uppercase; +} + +.ks-accent-row-label small { + color: var(--ks-muted); + font-size: 0.66rem; + letter-spacing: 0.18em; + text-transform: uppercase; +} + +.ks-accent-band { + position: relative; + height: 96px; + overflow: hidden; + border-radius: 2px; +} + +.ks-accent-band.is-leaf { + background: url("/assets/neo-kinpaku/kinpaku-gold-leaf.png") 50% 50% / cover no-repeat; + filter: saturate(1.15) contrast(1.08); +} + +.ks-accent-band.is-patina { + background: url("/assets/neo-kinpaku/verdigris-patina.png") 50% 50% / cover no-repeat; + filter: saturate(1.18) contrast(1.06); +} + +.ks-accent-band.is-dust { + background: url("/assets/neo-kinpaku/gold-dust-rule.png") center / 100% 100% no-repeat; + filter: saturate(1.22) contrast(1.15) brightness(1.12); +} + +/* ============================================================================ + Navigation sample + ============================================================================ */ + +.ks-nav-sample { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + min-height: 68px; + padding: 0 24px; + border-top: 1px solid var(--ks-rule); + border-bottom: 1px solid var(--ks-rule); +} + +.ks-nav-sample .ks-brand { gap: 14px; } +.ks-nav-sample .ks-mark { width: 32px; height: 32px; } +.ks-nav-sample .ks-mark svg { width: 27px; height: 27px; } +.ks-nav-sample .ks-wordmark { font-size: 0.78rem; } + +.ks-nav-sample-mobile { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + min-height: 60px; + padding: 0 24px; + border-top: 1px solid var(--ks-rule); + border-bottom: 1px solid var(--ks-rule); + margin-top: 28px; + max-width: 420px; +} + +.ks-menu-button { + width: 36px; + height: 36px; + display: grid; + place-content: center; + gap: 6px; + background: transparent; + border: 0; + color: var(--ks-kinpaku); + cursor: pointer; +} + +.ks-menu-button span { + width: 22px; + height: 1px; + background: currentColor; +} + +/* ============================================================================ + Actions: button row, states, form, tabs + ============================================================================ */ + +/* .ks-button-row is in kinpaku-kit.css. */ + +.ks-state-grid { + display: grid; + grid-template-columns: minmax(120px, auto) 1fr; + gap: 16px 24px; + align-items: center; + max-width: 420px; +} + +.ks-state-label { + color: var(--ks-muted); + font-family: var(--ks-mono); + font-size: 0.7rem; + letter-spacing: 0.18em; + text-transform: uppercase; +} + +/* Force pseudo-states for state showcase */ +.ks-state-grid .ks-button-primary[data-state="hover"] { + background: var(--ks-kinpaku-pale); + border-color: var(--ks-kinpaku-pale); +} +.ks-state-grid .ks-button-primary[data-state="pressed"] { + background: oklch(72% 0.11 82); + border-color: oklch(72% 0.11 82); +} + +/* Form controls (.ks-form-sample, .ks-toggle, .ks-checkbox, .ks-select) and + tabs (.ks-tabs, .ks-tab-list, .ks-tab-panel) are in kinpaku-kit.css. */ + +/* ============================================================================ + Components: Audit Result Row, badges, tags, integration badges, pagination + ============================================================================ */ + +.ks-audit-result { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 48px; + padding: 32px 36px 36px; + border-top: 1px solid var(--ks-rule); + margin-bottom: 64px; +} + +.ks-audit-stat { + display: grid; + gap: 6px; +} + +.ks-audit-stat-label { + color: var(--ks-muted); + font-family: var(--ks-mono); + font-size: 0.66rem; + letter-spacing: 0.24em; + text-transform: uppercase; +} + +.ks-audit-stat-value { + font-weight: 300; + font-size: clamp(2.4rem, 4vw, 3.2rem); + line-height: 1; + letter-spacing: -0.01em; +} +.ks-audit-stat-value.is-warning { color: var(--ks-vermilion); } +.ks-audit-stat-value.is-improved { color: var(--ks-patina); } +.ks-audit-stat-value.is-ready { color: var(--ks-kinpaku); } + +.ks-audit-stat-meta { + color: var(--ks-muted); + font-size: 0.84rem; +} + +/* Badges (.ks-badge, .ks-badge-row) and tags (.ks-tag, .ks-tag-row) are in + kinpaku-kit.css. */ + +/* Integration badges */ +.ks-integration-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 18px; +} + +.ks-integration-badge { + width: 44px; + height: 44px; + display: grid; + place-items: center; + border: 1px solid var(--ks-rule); + border-radius: 2px; + color: var(--ks-champagne); + transition: border-color 180ms var(--ks-ease), color 180ms var(--ks-ease); +} +.ks-integration-badge:hover { + border-color: var(--ks-kinpaku); + color: var(--ks-kinpaku); +} +.ks-integration-badge svg { width: 20px; height: 20px; } + +/* Pagination, toast/alert, icon button + tooltip, modal, empty state, + changelog row, skeleton (+ ks-shimmer keyframes) are all in kinpaku-kit.css. + Only the tooltip *trigger* wrapper (.ks-tooltip-sample) stays here as + page-specific doc presentation. */ + +.ks-tooltip-sample { + position: relative; + display: inline-flex; + align-items: center; + gap: 0; + padding: 22px 0; +} + +.ks-tooltip-sample:hover .ks-tooltip { + opacity: 1; +} + +/* ============================================================================ + Proof: Comparison + audit detail rows + ============================================================================ */ + +.ks-proof-grid { + display: grid; + grid-template-columns: minmax(0, 1.4fr) minmax(280px, 0.6fr); + gap: 48px; +} + +.ks-comparison { + position: relative; +} + +.ks-comparison-head { + display: flex; + align-items: baseline; + justify-content: space-between; + margin-bottom: 18px; +} + +.ks-comparison-head h3 { + margin: 0; + color: var(--ks-champagne); + font-size: 0.96rem; + font-weight: 400; +} + +.ks-comparison-head span { + color: var(--ks-muted); + font-family: var(--ks-mono); + font-size: 0.7rem; + letter-spacing: 0.18em; + text-transform: uppercase; +} + +.ks-comparison-stage { + position: relative; + aspect-ratio: 16 / 10; + overflow: hidden; + border: 1px solid var(--ks-rule-strong); + border-radius: 2px; + background: + linear-gradient(180deg, oklch(8% 0.006 95), oklch(5% 0.004 95)); +} + +.ks-comparison-side { + position: absolute; + inset: 0; + padding: 28px; + display: grid; + align-items: center; + justify-items: center; +} + +.ks-comparison-before { + background: + radial-gradient(circle at 50% 50%, oklch(60% 0.18 305 / 0.12), transparent 18rem), + linear-gradient(180deg, oklch(8% 0.006 95), oklch(5% 0.004 95)); +} + +.ks-comparison-after { + background: + radial-gradient(circle at 50% 50%, oklch(69% 0.105 190 / 0.1), transparent 18rem), + linear-gradient(180deg, oklch(8% 0.006 95), oklch(5% 0.004 95)); +} + +.ks-mini-ui { + width: min(360px, 88%); + padding: 18px; + border: 1px solid var(--ks-rule); + border-radius: 4px; + background: var(--ks-surface); +} + +.ks-mini-ui-before .ks-mini-orb { + width: 40px; + height: 40px; + border-radius: 999px; + background: linear-gradient(135deg, oklch(65% 0.22 285), oklch(54% 0.25 310)); + margin-bottom: 14px; +} +.ks-mini-ui-before strong { + color: var(--ks-champagne); + font-size: 0.98rem; + font-weight: 500; + display: block; +} +.ks-mini-ui-before p { + margin: 6px 0 14px; + color: var(--ks-muted); + font-size: 0.82rem; +} +.ks-mini-ui-before .ks-mini-cta { + width: 100%; + min-height: 38px; + border: 0; + border-radius: 4px; + background: linear-gradient(90deg, oklch(62% 0.24 285), oklch(48% 0.22 315)); + color: oklch(98% 0.01 285); + font-size: 0.86rem; + cursor: pointer; +} + +.ks-mini-ui-after { + border-color: var(--ks-rule-strong); +} +.ks-mini-ui-after .ks-mini-rule { + width: 64px; + height: 1px; + background: var(--ks-kinpaku); + margin-bottom: 14px; +} +.ks-mini-ui-after strong { + color: var(--ks-champagne); + font-size: 0.98rem; + font-weight: 400; + display: block; +} +.ks-mini-ui-after p { + margin: 6px 0 0; + color: var(--ks-muted); + font-size: 0.82rem; +} + +.ks-comparison-divider { + position: absolute; + top: -6%; + left: 50%; + width: 140px; + height: 112%; + transform: translateX(-50%); + pointer-events: none; + z-index: 4; + background-image: url("/assets/neo-kinpaku/before-after-seam.png"); + background-repeat: no-repeat; + background-position: center; + background-size: auto 100%; + filter: drop-shadow(0 0 16px oklch(77% 0.14 82 / 0.42)) saturate(1.22); +} + +.ks-comparison-range { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + opacity: 0; + cursor: ew-resize; + margin: 0; +} + +.ks-audit-rows { + display: grid; + gap: 0; +} + +.ks-audit-row { + display: grid; + grid-template-columns: 70px 1fr auto; + gap: 18px; + align-items: center; + padding: 18px 0; + border-bottom: 1px solid var(--ks-rule); +} +.ks-audit-row:first-child { padding-top: 0; } +.ks-audit-row:last-child { border-bottom: 0; } + +.ks-severity { + display: inline-flex; + align-items: center; + gap: 6px; + font-family: var(--ks-mono); + font-size: 0.7rem; + letter-spacing: 0.16em; + text-transform: uppercase; +} + +.ks-severity::before { + content: ""; + width: 6px; + height: 6px; + border-radius: 999px; +} + +.ks-severity.is-high { color: var(--ks-vermilion); } +.ks-severity.is-high::before { background: var(--ks-vermilion); } +.ks-severity.is-medium { color: var(--ks-kinpaku); } +.ks-severity.is-medium::before { background: var(--ks-kinpaku); } +.ks-severity.is-low { color: var(--ks-muted); } +.ks-severity.is-low::before { background: var(--ks-muted); } + +.ks-audit-row-title { + color: var(--ks-champagne); + font-size: 0.96rem; + font-weight: 400; + display: block; +} + +.ks-audit-row-meta { + display: block; + margin-top: 2px; + color: var(--ks-muted); + font-size: 0.82rem; +} + +.ks-audit-status { + color: var(--ks-muted); + font-family: var(--ks-mono); + font-size: 0.7rem; + letter-spacing: 0.16em; + text-transform: uppercase; +} +.ks-audit-status.is-improved { color: var(--ks-patina); } +.ks-audit-status.is-ready { color: var(--ks-kinpaku); } + +/* ============================================================================ + Component grids + ============================================================================ */ + +.ks-component-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 56px; + align-items: start; +} + +.ks-component-grid-3 { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 48px; + align-items: start; +} + +.ks-component { + display: grid; + gap: 16px; + align-self: start; +} + +.ks-component-label { + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-size: 0.66rem; + letter-spacing: 0.24em; + text-transform: uppercase; +} + +/* ============================================================================ + Footer (re-enabled) + ============================================================================ */ + +.ks-footer { + position: relative; + border-top: 1px solid var(--ks-rule); + padding: 80px 56px 56px; + margin-top: 80px; + overflow: hidden; +} + +.ks-footer::after { + content: ""; + position: absolute; + right: -2%; + top: -10%; + width: 220px; + height: 140%; + background: url("/assets/neo-kinpaku/oxidation-edge-accent.png") right center / contain no-repeat; + opacity: 0.85; + pointer-events: none; + filter: saturate(1.1); +} + +.ks-footer-inner { + position: relative; + z-index: 1; + max-width: 1320px; + margin: 0 auto; + display: grid; + grid-template-columns: 1.3fr 2fr; + gap: 64px; +} + +.ks-footer-brand .ks-brand { margin-bottom: 22px; } + +.ks-footer-tagline { + color: var(--ks-muted); + max-width: 280px; + font-size: 0.92rem; + line-height: 1.55; +} + +.ks-footer-links { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 32px; +} + +.ks-footer-col h4 { + margin: 0 0 14px; + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-size: 0.66rem; + letter-spacing: 0.24em; + text-transform: uppercase; + font-weight: 500; +} + +.ks-footer-col ul { + list-style: none; + margin: 0; + padding: 0; + display: grid; + gap: 10px; +} + +.ks-footer-col a { + color: var(--ks-champagne); + font-size: 0.92rem; + text-decoration: none; + transition: color 180ms var(--ks-ease); +} + +.ks-footer-col a:hover { color: var(--ks-kinpaku); } + +.ks-footer-meta { + max-width: 1320px; + margin: 56px auto 0; + padding-top: 22px; + border-top: 1px solid var(--ks-rule); + display: flex; + flex-wrap: wrap; + gap: 18px; + justify-content: space-between; + align-items: center; + position: relative; + z-index: 1; +} + +.ks-footer-meta small { + color: var(--ks-mute-deep); + font-size: 0.78rem; +} + +.ks-footer-social { + display: flex; + gap: 14px; +} +.ks-footer-social a { + width: 32px; + height: 32px; + display: grid; + place-items: center; + color: var(--ks-muted); + border: 1px solid var(--ks-rule); + border-radius: 2px; + transition: color 180ms var(--ks-ease), border-color 180ms var(--ks-ease); +} +.ks-footer-social a:hover { + color: var(--ks-kinpaku); + border-color: var(--ks-kinpaku); +} +.ks-footer-social svg { width: 14px; height: 14px; } + +/* ============================================================================ + Focus states + ============================================================================ */ + +.kinpaku-system-page button:focus-visible, +.kinpaku-system-page a:focus-visible, +.kinpaku-system-page input:focus-visible, +.kinpaku-system-page select:focus-visible { + outline: 2px solid var(--ks-patina); + outline-offset: 3px; +} + +/* ============================================================================ + Responsive + ============================================================================ */ + +@media (max-width: 1080px) { + .ks-section { padding: 80px 36px; } + .ks-footer { padding: 64px 36px 40px; } + .ks-hero { padding: 28px 36px 0; } + .ks-category-rail { margin: 0 -36px; grid-template-columns: repeat(3, 1fr); } + .ks-category-rail a:nth-child(4), + .ks-category-rail a:nth-child(5) { display: none; } + + .ks-swatch-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 22px; } + .ks-proof-grid { grid-template-columns: 1fr; } + .ks-component-grid { grid-template-columns: 1fr; gap: 36px; } + .ks-component-grid-3 { grid-template-columns: repeat(2, 1fr); gap: 32px; } + .ks-footer-inner { grid-template-columns: 1fr; gap: 40px; } + .ks-audit-result { padding: 24px; gap: 24px; } +} + +@media (max-width: 720px) { + .ks-section { padding: 64px 24px; } + .ks-hero { padding: 20px 24px 0; } + .ks-category-rail { margin: 0 -24px; grid-template-columns: repeat(2, 1fr); } + .ks-category-rail a:nth-child(3) { display: none; } + .ks-nav { display: none; } + .ks-topbar > .ks-nav { display: none; } + .ks-hero-actions { flex-direction: column; align-items: stretch; } + .ks-hero-actions .ks-button { width: 100%; } + .ks-swatch-grid { grid-template-columns: repeat(2, 1fr); } + .ks-component-grid-3 { grid-template-columns: 1fr; } + .ks-footer-links { grid-template-columns: 1fr 1fr; } + .ks-audit-result { grid-template-columns: 1fr; gap: 24px; padding: 24px 0; } + .ks-type-row { grid-template-columns: 1fr; gap: 12px; } +} + +@media (prefers-reduced-motion: reduce) { + .kinpaku-system-page *, + .kinpaku-system-page *::before, + .kinpaku-system-page *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} diff --git a/site/styles/designing-kinpaku.css b/site/styles/designing-kinpaku.css new file mode 100644 index 000000000..8a0c482e8 --- /dev/null +++ b/site/styles/designing-kinpaku.css @@ -0,0 +1,1448 @@ +/* ============================================================================ + /designing — Neo Kinpaku redesign + + Overrides sub-pages.css + docs-visuals.css for the long-form "Designing + with Impeccable" walkthrough. The existing markup is unchanged; this file + restyles every block to the kinpaku brand: dark lacquer surface, gold + + verdigris accents, Alumni Sans Pinstripe display, Albert Sans body. + + Structure of this file mirrors the page itself: + 1. Page scope + base + 2. Site header / footer override on dark surface + 3. Hero + loop wheel + 4. Phase scaffolding (eyebrow / title / sub / command chips) + 5. Start phase (file mock + 3-plate visualize spread) + 6. Iterate phase (terminal + live mock + reach-for-which table) + 7. Polish phase (band + 3-col) + 8. Maintain phase (extract + DESIGN.md tiles) + 9. Brand vs Product lanes + 10. Avoid list + 11. CTA cards + ============================================================================ */ + +@import "./kinpaku-tokens.css"; + +/* ============================================================================ + 1. PAGE SCOPE + BASE + ============================================================================ */ + +.designing-kinpaku { + /* Surfaces, text, accent, and fonts now default to kinpaku at :root + (tokens.css remaps the legacy --color-* / --font-* names), so the + docs-visuals.css rules this page reuses inherit the dark theme with no + per-page remap. Only the --ks-muted alias (still read by name here) and + the page shell remain. */ + --ks-muted: var(--ks-text-muted); + + background: + linear-gradient(180deg, oklch(7% 0.006 95), oklch(4% 0.004 95)); + color: var(--ks-text); + font-family: var(--ks-font); + min-height: 100vh; +} + +.designing-kinpaku a { + color: inherit; +} + +.designing-kinpaku code { + font-family: var(--ks-mono); + color: var(--ks-kinpaku); + background: transparent; + border: 0; + padding: 0; +} + +/* ============================================================================ + 2. SITE HEADER / FOOTER on dark surface + ============================================================================ */ + +/* Site header styling is shared with the homepage via kinpaku-kit.css — + one selector list covers every kinpaku-themed page so the header is + visually identical across /, /designing, and any future page. The only + designing-specific header rule is the hover/aria-current accent below. */ + +.designing-kinpaku .site-header-nav a:hover, +.designing-kinpaku .site-header-nav a[aria-current="page"] { + color: var(--ks-kinpaku); +} + +/* Footer chrome moved to kinpaku-kit.css (.kinpaku-chrome .site-footer). */ + +/* ============================================================================ + 3. HERO + COMPASS LOOP WHEEL + + The original /designing arranges 4 phase nodes in a compass around a + central SVG wheel (display: grid 3x3, ring + ticks + clockwise orbiting + dot animation, arrow glyphs at 45° points, italic wordmark center). + Restore that structure for kinpaku — kinpaku ring, patina ticks, kinpaku + dot. Phase nodes are text-only labels at the compass points, no card + chrome (so the wheel is the visual artifact and the labels read as + directional annotations). Header section title sits centered above the + compass. + ============================================================================ */ + +/* docs-visuals.css caps .designing-page at 920px for the editorial measure + the old design used. The kinpaku redesign uses .ks-section's 1320px cap + so the hero pair (title + compass) and the bento sections have room to + breathe. Override the legacy cap here. */ +.designing-kinpaku .designing-page { + max-width: 1320px; + padding: 64px 0 96px; +} + +/* No italic display anywhere — DESIGN.md No-Italic-Display rule applies to + every heading on the page. docs-visuals.css sets font-style: italic on + .designing-page-title / .designing-phase-title / .designing-iterate-name + / .designing-polish-name / .designing-maintain-name / .designing-loop-name + / .designing-loop-wheel-center-mark, and there are explicit italics + in markup. Reset them all here. */ +.designing-kinpaku .designing-page-title, +.designing-kinpaku .designing-page-title em, +.designing-kinpaku .designing-phase-title, +.designing-kinpaku .designing-iterate-name, +.designing-kinpaku .designing-polish-name, +.designing-kinpaku .designing-maintain-name, +.designing-kinpaku .designing-loop-name, +.designing-kinpaku .designing-loop-wheel-center-mark, +.designing-kinpaku .designing-lane-mock-title, +.designing-kinpaku .designing-cta-card-title, +.designing-kinpaku .designing-cta-card-title em, +.designing-kinpaku .docs-viz-live-title em { + font-style: normal; +} + +/* Hero — side-by-side at desktop (header left, compass right), stacked + centered at narrow viewports. The editorial 2-col composition lets the + title and lede sit in a tighter measure while the compass holds the + right column as the artifact. */ +/* Hero pairs the title and the compass loop side by side, centered in the + viewport. The compass loop has a fixed 3-column internal grid + (label / ring / label) that needs ~560px to render without overflowing; + the title column takes whatever's left. Composition stays tight because + max-width caps the outer width and margin: 0 auto centers the pair. */ +.designing-kinpaku .designing-hero { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + column-gap: clamp(2rem, 5vw, 4rem); + row-gap: 56px; + max-width: 1100px; + margin: 0 auto; + padding: 80px 24px 96px; +} +@media (max-width: 960px) { + .designing-kinpaku .designing-hero { + /* minmax(0, 1fr) instead of 1fr so the column shrinks to fit the + container — bare 1fr lets the column grow to the intrinsic min-content + width of "Impeccable" (~472px), blowing past a 360px viewport. */ + grid-template-columns: minmax(0, 1fr); + padding: 56px 24px 64px; + align-items: start; + } +} + +.designing-kinpaku .designing-page-header { + max-width: 46ch; + margin: 0; + text-align: left; +} +@media (max-width: 960px) { + .designing-kinpaku .designing-page-header { + max-width: 56ch; + margin: 0 auto; + text-align: center; + } +} + +/* No section eyebrows or numbered markers. Detector flagged 8 of these as + repeated-section-kicker AI scaffolding and SKILL.md bans them. Hide every + eyebrow/number-marker class. The h2/h3 carries the section identity. */ +.designing-kinpaku .designing-page-eyebrow, +.designing-kinpaku .designing-phase-num, +.designing-kinpaku .designing-iterate-kind, +.designing-kinpaku .designing-lane-kind, +.designing-kinpaku .designing-cta-card-kind, +.designing-kinpaku .designing-start-step-label, +.designing-kinpaku .designing-loop-num { + display: none; +} + +/* H1 reads via the kit's display tokens — same scale and weight as the + homepage hero so the two pages share one display voice. The page's + earlier letter-spacing: 0.02em was slightly looser than the production + hero (-0.01em); unifying via --ks-type-display-track fixes the drift. */ +.designing-kinpaku .designing-page-title { + font-family: var(--ks-font-display); + font-weight: var(--ks-type-display-weight); + font-size: var(--ks-type-display-size); + line-height: var(--ks-type-display-line); + letter-spacing: var(--ks-type-display-track); + color: var(--ks-champagne); + margin: 0 0 24px; + text-wrap: balance; + max-width: none; + /* "Impeccable" at the display token's 3.4rem minimum is 270px+ in the + narrow Pinstripe face and overflows a 360px viewport. Allow break-word + as a safety net so the title never blows out of its container. */ + word-break: break-word; + overflow-wrap: anywhere; +} +@media (max-width: 480px) { + .designing-kinpaku .designing-page-title { + /* Bring the display size down for narrow viewports — the token's 3.4rem + minimum is sized for hero impact at desktop and tablet, too big when + the line has to wrap into a 320-400px column. */ + font-size: clamp(2.2rem, 11vw, 3rem); + } +} + +.designing-kinpaku .designing-page-title em { + color: var(--ks-kinpaku); +} + +.designing-kinpaku .designing-page-lede { + font-family: var(--ks-font); + font-size: 1.12rem; + line-height: 1.7; + color: var(--ks-text); + max-width: 46ch; + margin: 0; + text-wrap: balance; +} +@media (max-width: 960px) { + .designing-kinpaku .designing-page-lede { + max-width: 56ch; + margin: 0 auto; + } +} + +/* Compass loop -------------------------------------------------------------- + Restore the 3x3 grid: ring SVG in center cell, 4 phase nodes at N/E/S/W, + text-only nodes (no card chrome). */ + +.designing-kinpaku .designing-loop-wrap { + padding: 0; + margin: 0; + background: transparent; + border: 0; + position: static; +} +@media (max-width: 960px) { + .designing-kinpaku .designing-loop-wrap { + margin-top: 24px; + } +} + +.designing-kinpaku .designing-loop-wrap-eyebrow { + display: none; +} + +/* Don't override the existing 3x3 grid from docs-visuals; just give it + breathing room appropriate to the larger kinpaku scale. Max-width caps + the loop's preferred size so the hero's auto-sized right column doesn't + grow into the title column. */ +.designing-kinpaku .designing-loop { + grid-template-columns: minmax(110px, 1fr) clamp(220px, 28vw, 320px) minmax(110px, 1fr); + grid-template-rows: auto clamp(220px, 28vw, 320px) auto; + column-gap: clamp(1rem, 2.4vw, 1.75rem); + row-gap: clamp(1rem, 2.4vw, 1.75rem); + max-width: 560px; +} + +/* Compass labels — text only, no card chrome. Each variant keeps its + compass position (grid-column/row) from docs-visuals and just gets its + text alignment set toward the wheel center. */ +.designing-kinpaku .designing-loop-node { + display: flex; + flex-direction: column; + gap: 6px; + padding: 0; + background: transparent; + border: 0; + border-radius: 0; + text-decoration: none; + color: var(--ks-text); + max-width: none; + transition: color 180ms var(--ks-ease); +} +.designing-kinpaku .designing-loop-node:hover .designing-loop-name { + color: var(--ks-kinpaku); +} + +.designing-kinpaku .designing-loop-name { + color: var(--ks-champagne); + font-family: var(--ks-font-display); + font-weight: 300; + font-size: clamp(1.5rem, 2.2vw, 2rem); + line-height: 1.05; + letter-spacing: 0.01em; + transition: color 180ms var(--ks-ease); +} + +.designing-kinpaku .designing-loop-hint { + display: block; + color: var(--ks-muted); + font-size: 0.82rem; + line-height: 1.5; + max-width: 22ch; +} + +.designing-kinpaku .designing-loop-node--start { text-align: center; align-items: center; } +.designing-kinpaku .designing-loop-node--iterate { text-align: left; align-items: flex-start; } +.designing-kinpaku .designing-loop-node--polish { text-align: center; align-items: center; } +.designing-kinpaku .designing-loop-node--maintain { text-align: right; align-items: flex-end; } + +/* Restore the SVG wheel. Kinpaku ring at full visibility, patina ticks, + kinpaku gold dot on the orbit path. Center label stays hidden — the wheel + itself is the artifact, no need for an italic wordmark inside it. */ +.designing-kinpaku .designing-loop-wheel { + display: block; +} +/* The wheel is a decorative SVG flourish that sits beside the hero. + On narrow viewports it overflows the page (~22px past the right edge); + hide it on mobile — the hero copy carries the page without it. */ +@media (max-width: 600px) { + .designing-kinpaku .designing-loop-wheel { + display: none; + } +} +.designing-kinpaku .designing-loop-wheel-ring { + fill: none; + stroke: var(--ks-kinpaku); + stroke-width: 0.4; + opacity: 0.65; +} +.designing-kinpaku .designing-loop-wheel-tick { + stroke: var(--ks-patina); + stroke-width: 0.6; + opacity: 0.55; +} +.designing-kinpaku .designing-loop-wheel-tick--cardinal { + stroke: var(--ks-kinpaku); + stroke-width: 1; + opacity: 1; +} +.designing-kinpaku .designing-loop-wheel-dot { + fill: var(--ks-kinpaku); +} + +/* Arrow glyphs at 45° points — quiet, kinpaku-tinted. */ +.designing-kinpaku .designing-loop-wheel-arrow { + background: var(--ks-lacquer); + color: var(--ks-kinpaku); + font-size: 14px; + width: 22px; + height: 22px; +} + +/* Center label: hide the italic wordmark mark (DESIGN.md No-Italic-Display + rule). Keep the small mono label as a quiet centerpiece. */ +.designing-kinpaku .designing-loop-wheel-center-mark { + display: none; +} +.designing-kinpaku .designing-loop-wheel-center-label { + font-family: var(--ks-mono); + font-size: 0.62rem; + letter-spacing: 0.28em; + text-transform: uppercase; + color: var(--ks-muted); +} + +/* ============================================================================ + 3b. LOOP TRACK + STICKY PHASE NAV + + The four phase sections (#start/#iterate/#polish/#maintain) are wrapped in + .designing-loop-track. A slim phase nav pins under the 95px sticky site + header for the duration of the track, scroll-spying the active phase. This + gives the compass-driven loop a persistent wayfinder without hiding any + content behind tabs (the page stays a scrollable editorial walkthrough). + ============================================================================ */ + +.designing-kinpaku .designing-loop-track { + position: relative; +} + +.designing-kinpaku .designing-phasenav { + position: sticky; + top: 95px; /* sits directly under the sticky .site-header (95px, z 100) */ + z-index: 40; /* above page content, below the header */ + display: flex; + justify-content: center; + gap: clamp(1.25rem, 5vw, 3.5rem); + padding: 15px 24px; + /* Opaque lacquer, not glass — DESIGN.md No-Glass rule. */ + background: oklch(6% 0.006 95); + border-top: 1px solid var(--ks-rule); + border-bottom: 1px solid var(--ks-rule); +} + +.designing-kinpaku .designing-phasenav-link { + position: relative; + font-family: var(--ks-mono); + font-size: 0.72rem; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--ks-muted); + text-decoration: none; + padding: 2px 0; + transition: color 180ms var(--ks-ease); +} +.designing-kinpaku .designing-phasenav-link:hover { + color: var(--ks-champagne); +} +.designing-kinpaku .designing-phasenav-link.is-active { + color: var(--ks-kinpaku); +} +.designing-kinpaku .designing-phasenav-link.is-active::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: -16px; /* reaches the nav's bottom hairline (15px pad + 1px border) */ + height: 2px; + background: var(--ks-kinpaku); +} + +@media (max-width: 600px) { + .designing-kinpaku .designing-phasenav { + gap: 0.7rem; + padding: 12px 14px; + } + .designing-kinpaku .designing-phasenav-link { + font-size: 0.62rem; + letter-spacing: 0.12em; + } +} + +/* The nav's bottom hairline is the divider into #start; drop the section's + own top border so the seam isn't doubled. */ +.designing-kinpaku .designing-phasenav + .designing-phase { + border-top: 0; +} + +/* Anchor jumps (compass nodes + phase nav) must clear the sticky header + + nav. The base .designing-phase scroll-margin (80px) is too short here. */ +.designing-kinpaku .designing-loop-track .designing-phase { + scroll-margin-top: 152px; +} + +/* One deliberate, consistent break between the loop spine and the context + tail (replaces the old maintain->appendix adjacency that fired unevenly). */ +.designing-kinpaku .designing-loop-track + .designing-phase--appendix { + margin-top: 80px; +} + +/* ============================================================================ + 4. PHASE SCAFFOLDING — command chips only + + Section container, header, title, and subtitle now come from + kinpaku-kit.css (.ks-section, .ks-section-head, .ks-section-head h2, + .ks-section-sub). Sections in markup carry both class names — + .ks-section for kit styling, .designing-phase for legacy hooks where + inner phase-specific layouts still target the descendant selector. + ============================================================================ */ + +.designing-kinpaku .designing-phase-sub code { + color: var(--ks-kinpaku); +} + +.designing-kinpaku .designing-phase-commands { + display: flex; + flex-wrap: wrap; + gap: 10px; + justify-content: flex-start; + margin-top: 28px; +} + +.designing-kinpaku .designing-phase-cmd { + display: inline-flex; + align-items: center; + padding: 8px 16px; + background: oklch(11% 0.006 95); + border: 1px solid var(--ks-rule); + border-radius: 2px; + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-size: 0.84rem; + text-decoration: none; + transition: border-color 180ms var(--ks-ease), background 180ms var(--ks-ease); +} +.designing-kinpaku .designing-phase-cmd:hover { + border-color: var(--ks-kinpaku); + background: oklch(78% 0.12 82 / 0.06); +} + +/* ============================================================================ + 5. START PHASE (file mock + visualize spread) + ============================================================================ */ + +.designing-kinpaku .designing-start-step { + margin-bottom: 40px; +} +.designing-kinpaku .designing-start-step:last-child { margin-bottom: 0; } + +/* .designing-start-step-label is hidden via the eyebrow-kill rule above; the + step name is integrated into the start-step structure now. */ +.designing-kinpaku .designing-start-step-label-disabled-by-eyebrow-kill { + display: none; + margin-bottom: 18px; + color: var(--ks-patina); + font-family: var(--ks-mono); + font-size: 0.62rem; + letter-spacing: 0.28em; + text-transform: uppercase; +} + +.designing-kinpaku .designing-start-step-note { + color: var(--ks-text); + font-size: 1rem; + line-height: 1.65; + max-width: 70ch; + margin: 0 0 36px; + text-wrap: pretty; +} + +.designing-kinpaku .designing-start-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 48px; + align-items: start; +} +@media (max-width: 820px) { + .designing-kinpaku .designing-start-grid { grid-template-columns: 1fr; } +} + +.designing-kinpaku .designing-start-grid-prose p { + color: var(--ks-text); + font-size: 1rem; + line-height: 1.7; + text-wrap: pretty; +} + +/* File mock (PRODUCT.md preview) — bring it onto the kinpaku surface. */ +.designing-kinpaku .docs-viz-file { + background: var(--ks-lacquer-raised); + border: 1px solid var(--ks-rule); + border-radius: 4px; + overflow: hidden; +} +.designing-kinpaku .docs-viz-file-header { + padding: 14px 18px; + border-bottom: 1px solid var(--ks-rule); + background: oklch(13% 0.006 95); + display: flex; + align-items: center; + justify-content: space-between; +} +.designing-kinpaku .docs-viz-file-name { + color: var(--ks-champagne); + font-family: var(--ks-mono); + font-size: 0.82rem; +} +.designing-kinpaku .docs-viz-file-status { + color: var(--ks-patina); + font-family: var(--ks-mono); + font-size: 0.62rem; + letter-spacing: 0.14em; + text-transform: uppercase; +} +.designing-kinpaku .docs-viz-file-body { + padding: 18px; + display: grid; + gap: 12px; +} +.designing-kinpaku .docs-viz-file-row { + display: grid; + grid-template-columns: 132px 1fr; + gap: 16px; + align-items: baseline; + padding-bottom: 10px; + border-bottom: 1px solid var(--ks-rule); +} +.designing-kinpaku .docs-viz-file-row:last-child { border-bottom: 0; } +.designing-kinpaku .docs-viz-file-k { + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-size: 0.72rem; + letter-spacing: 0.14em; + text-transform: uppercase; +} +.designing-kinpaku .docs-viz-file-v { + color: var(--ks-text); + font-size: 0.92rem; + line-height: 1.5; +} + +/* Visualize spread — 3-plate Shape/Visualize/Ship gallery */ +.designing-kinpaku .designing-visualize-spread { + display: grid; + grid-template-columns: 0.88fr 0.68fr 1.18fr; + gap: clamp(14px, 1.6vw, 22px); + align-items: stretch; +} +@media (max-width: 900px) { + .designing-kinpaku .designing-visualize-spread { grid-template-columns: 1fr; } +} + +.designing-kinpaku .designing-visualize-plate { + margin: 0; + display: flex; + flex-direction: column; + gap: 14px; + min-width: 0; +} +.designing-kinpaku .designing-visualize-plate-frame { + display: block; + background: var(--ks-lacquer-raised); + border: 1px solid var(--ks-rule); + border-radius: 4px; + overflow: hidden; + height: 188px; + transition: border-color 200ms var(--ks-ease), transform 200ms var(--ks-ease); +} +.designing-kinpaku .designing-visualize-plate:hover .designing-visualize-plate-frame { + border-color: var(--ks-kinpaku); + transform: translateY(-2px); +} +.designing-kinpaku .designing-visualize-plate-frame img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; + object-position: top center; +} +.designing-kinpaku .designing-visualize-plate--live .designing-visualize-plate-frame img { + object-position: top left; +} +.designing-kinpaku .designing-visualize-plate-cap { + display: flex; + flex-direction: column; + gap: 6px; +} +.designing-kinpaku .designing-visualize-plate-kind { + color: var(--ks-patina); + font-family: var(--ks-mono); + font-size: 0.62rem; + font-weight: 500; + letter-spacing: 0.22em; + text-transform: uppercase; +} +.designing-kinpaku .designing-visualize-plate-note { + color: var(--ks-text); + font-size: 0.92rem; + line-height: 1.6; + margin: 0; + text-wrap: pretty; +} +.designing-kinpaku .designing-visualize-plate-note a { + color: var(--ks-kinpaku); + text-decoration: underline; + text-decoration-color: var(--ks-kinpaku); + text-underline-offset: 3px; +} +.designing-kinpaku .designing-visualize-foot { + margin: 28px 0 0; + color: var(--ks-muted); + font-size: 0.88rem; + max-width: 64ch; + line-height: 1.65; +} +.designing-kinpaku .designing-visualize-foot strong { + color: var(--ks-champagne); + font-weight: 500; +} + +/* ============================================================================ + 6. ITERATE PHASE (terminal + live + table) + ============================================================================ */ + +/* Three shared row tracks (name / paragraph / box) so the two columns align + via subgrid: both mock boxes start on the same line regardless of how many + lines each intro paragraph wraps to. Without this, a taller paragraph pushes + one box down and the pair looks broken. */ +.designing-kinpaku .designing-iterate-split { + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: auto auto auto; + column-gap: 40px; + row-gap: 18px; + margin-bottom: 56px; +} +@media (max-width: 900px) { + .designing-kinpaku .designing-iterate-split { + grid-template-columns: 1fr; + grid-template-rows: none; + } +} + +/* No card chrome on the iterate col — the inner terminal / browser mock IS + the card. Two cards stacked would be a nesting violation. */ +.designing-kinpaku .designing-iterate-col { + display: grid; + grid-row: span 3; + grid-template-rows: subgrid; + row-gap: 18px; + padding: 0; + background: transparent; + border: 0; + border-radius: 0; +} +@media (max-width: 900px) { + .designing-kinpaku .designing-iterate-col { + grid-row: auto; + grid-template-rows: none; + gap: 16px; + } +} + +.designing-kinpaku .designing-iterate-kind { + color: var(--ks-patina); + font-family: var(--ks-mono); + font-size: 0.62rem; + font-weight: 500; + letter-spacing: 0.24em; + text-transform: uppercase; +} +.designing-kinpaku .designing-iterate-col:first-child .designing-iterate-kind { + color: var(--ks-kinpaku); +} + +/* Subsection h3s use Albert Sans, not the Pinstripe display face. DESIGN.md + reserves the display face for the hero h1 and section h2s; at h3 sizes the + thin Pinstripe loses its identity and reads as a broken render. */ +.designing-kinpaku .designing-iterate-name { + color: var(--ks-champagne); + font-family: var(--ks-font); + font-weight: 600; + font-size: 1.2rem; + line-height: 1.3; + letter-spacing: -0.01em; + margin: 0; + text-wrap: balance; +} + +.designing-kinpaku .designing-iterate-when { + color: var(--ks-text); + font-size: 0.96rem; + line-height: 1.6; + margin: 0; + /* Base docs-visuals caps this at 36ch (~348px), which crams the paragraph + into many short lines inside a 480px+ column. Let it breathe. */ + max-width: 52ch; +} + +/* Terminal mock. The base rule (docs-visuals.css) is a flex column with + justify-content: center for vertical centering; switching to grid here made + that declaration center the lines horizontally. justify-items: start pins + the lines flush-left like a real terminal; align-content: center keeps the + block vertically centered in the column when it stretches to match the live + frame opposite it. */ +.designing-kinpaku .designing-iterate-terminal { + padding: 20px 22px; + background: oklch(13% 0.006 95); + border: 1px solid var(--ks-rule); + border-radius: 4px; + font-family: var(--ks-mono); + font-size: 0.86rem; + line-height: 1.7; + color: var(--ks-champagne); + display: grid; + gap: 10px; + justify-content: start; + justify-items: start; + align-content: center; +} +.designing-kinpaku .designing-iterate-terminal-line { + display: flex; + gap: 12px; +} +.designing-kinpaku .designing-iterate-terminal-prompt { + color: var(--ks-kinpaku); +} + +/* Live frame mock — restyle docs-viz-live-* on dark surface */ +.designing-kinpaku .docs-viz-live-frame { + background: oklch(13% 0.006 95); + border: 1px solid var(--ks-rule); + border-radius: 4px; + overflow: hidden; +} +.designing-kinpaku .docs-viz-live-chrome { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 14px; + border-bottom: 1px solid var(--ks-rule); + background: oklch(11% 0.006 95); +} +.designing-kinpaku .docs-viz-live-dot { + width: 9px; + height: 9px; + border-radius: 50%; + background: oklch(40% 0.02 82); +} +.designing-kinpaku .docs-viz-live-url { + margin-left: 8px; + color: var(--ks-muted); + font-family: var(--ks-mono); + font-size: 0.74rem; +} +/* The stage reserves an 88px band at the bottom (padding-bottom) for the two + stacked picker bars. Absolutely-positioned children offset from the padding + box, so a larger padding-bottom pushes the target up out of the bar band + while the bars (positioned from the bottom) float clear of it. */ +.designing-kinpaku .docs-viz-live-stage { + position: relative; + padding: 24px 24px 108px; + min-height: 292px; + background: + linear-gradient(90deg, oklch(78% 0.12 82 / 0.035) 1px, transparent 1px), + linear-gradient(180deg, oklch(78% 0.12 82 / 0.028) 1px, transparent 1px), + oklch(8% 0.006 95); + background-size: 28px 28px; +} +.designing-kinpaku .docs-viz-live-target { + width: 100%; + background: oklch(11% 0.006 95); + border: 1px solid var(--ks-rule); + border-radius: 4px; + padding: 20px; + display: grid; + gap: 10px; +} +.designing-kinpaku .docs-viz-live-kicker { + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-size: 0.66rem; + letter-spacing: 0.18em; + text-transform: uppercase; +} +.designing-kinpaku .docs-viz-live-title { + color: var(--ks-champagne); + font-family: var(--ks-font); + font-style: normal; + font-weight: 400; + font-size: 1.1rem; + margin: 0; +} +.designing-kinpaku .docs-viz-live-title em { + color: var(--ks-patina); +} +.designing-kinpaku .docs-viz-live-btn { + align-self: start; + padding: 8px 14px; + background: var(--ks-kinpaku-pale); + color: var(--ks-lacquer-deep); + border: 0; + border-radius: 2px; + font-family: var(--ks-font); + font-size: 0.88rem; + cursor: pointer; +} +/* Outline hugs the target region (above the 88px bar band), with a faint + kinpaku halo so it reads as the picker's selection highlight. */ +.designing-kinpaku .docs-viz-live-outline { + position: absolute; + inset: 14px 14px 108px; + border: 1.5px solid var(--ks-kinpaku); + border-radius: 4px; + box-shadow: 0 0 0 4px oklch(78% 0.12 82 / 0.1); + pointer-events: none; +} +/* top: auto is required — the base/legacy rule sets top: 16px, and leaving it + set alongside bottom stretches the bar into a tall box over the target. */ +.designing-kinpaku .docs-viz-live-ctx { + position: absolute; + top: auto; + bottom: 62px; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + gap: 6px; + padding: 6px; + background: var(--ks-lacquer-deep); + border: 1.5px solid var(--ks-kinpaku); + border-radius: 6px; + box-shadow: + 0 0 0 1px oklch(78% 0.12 82 / 0.18), + 0 6px 18px oklch(0% 0 0 / 0.4); +} +.designing-kinpaku .docs-viz-live-ctx-nav, +.designing-kinpaku .docs-viz-live-ctx-discard, +.designing-kinpaku .docs-viz-live-ctx-accept { + padding: 4px 10px; + background: transparent; + border: 0; + color: var(--ks-text); + font-family: var(--ks-font); + font-size: 0.78rem; + cursor: pointer; +} +.designing-kinpaku .docs-viz-live-ctx-discard { + color: var(--ks-muted); + padding: 4px 6px; +} +.designing-kinpaku .docs-viz-live-ctx-accept { + background: var(--ks-kinpaku-pale); + color: var(--ks-lacquer-deep); + border-radius: 3px; + font-weight: 500; +} +.designing-kinpaku .docs-viz-live-ctx-counter { + color: var(--ks-muted); + font-family: var(--ks-mono); + font-size: 0.74rem; + padding: 0 6px; +} +.designing-kinpaku .docs-viz-live-ctx-divider { + width: 1px; + height: 14px; + background: var(--ks-rule); + margin: 0 4px; +} +/* Global picker bar — mirrors the real Impeccable live bar: carved-tile mark, + Pick/Insert/Detect/DESIGN.md controls, exit. Lacquer-deep fill + 1.5px gold + border + halo per DESIGN.md's "Picker Is Brand" rule. */ +.designing-kinpaku .docs-viz-live-gbar { + position: absolute; + bottom: 16px; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + gap: 2px; + padding: 5px 7px; + max-width: calc(100% - 24px); + background: var(--ks-lacquer-deep); + border: 1.5px solid var(--ks-kinpaku); + border-radius: 9px; + box-shadow: + 0 0 0 1px oklch(78% 0.12 82 / 0.18), + 0 10px 28px oklch(0% 0 0 / 0.28); +} +.designing-kinpaku .docs-viz-live-gbar-mark { + display: inline-flex; + color: var(--ks-kinpaku); + padding: 0 5px; +} +.designing-kinpaku .docs-viz-live-gbar-btn { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 5px 8px; + border-radius: 6px; + color: var(--ks-text-muted); + font-family: var(--ks-font); + font-size: 0.74rem; + white-space: nowrap; +} +.designing-kinpaku .docs-viz-live-gbar-btn svg { + width: 14px; + height: 14px; + flex-shrink: 0; +} +.designing-kinpaku .docs-viz-live-gbar-btn.is-active { + background: oklch(78% 0.12 82 / 0.18); + color: var(--ks-kinpaku); +} +.designing-kinpaku .docs-viz-live-gbar-btn-mono { + font-family: var(--ks-mono); + font-size: 0.66rem; + letter-spacing: 0.03em; +} +.designing-kinpaku .docs-viz-live-gbar-divider { + width: 1px; + height: 16px; + background: var(--ks-rule); + margin: 0 4px; +} +.designing-kinpaku .docs-viz-live-gbar-x { + display: inline-flex; + align-items: center; + color: var(--ks-text-muted); + padding: 5px 6px; +} +.designing-kinpaku .docs-viz-live-gbar-x svg { + width: 13px; + height: 13px; +} + +/* When-to-reach-for-which table */ +.designing-kinpaku .designing-iterate-table { + width: 100%; + border-collapse: collapse; + border-top: 1px solid var(--ks-rule); + border-bottom: 1px solid var(--ks-rule); +} +.designing-kinpaku .designing-iterate-table caption { + text-align: left; + caption-side: top; + padding-bottom: 18px; + color: var(--ks-patina); + font-family: var(--ks-mono); + font-size: 0.66rem; + letter-spacing: 0.24em; + text-transform: uppercase; +} +.designing-kinpaku .designing-iterate-table tbody tr { + border-top: 1px solid var(--ks-rule); +} +.designing-kinpaku .designing-iterate-table tbody tr:first-child { border-top: 0; } +.designing-kinpaku .designing-iterate-table th, +.designing-kinpaku .designing-iterate-table td { + padding: 18px 16px; + font-weight: 400; + text-align: left; + font-size: 0.96rem; + line-height: 1.5; +} +.designing-kinpaku .designing-iterate-table th { + color: var(--ks-text); + width: 58%; +} +.designing-kinpaku .designing-iterate-table td { + color: var(--ks-muted); +} +.designing-kinpaku .designing-iterate-table td a { + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-size: 0.9rem; + text-decoration: underline; + text-decoration-color: var(--ks-kinpaku); + text-underline-offset: 3px; +} + +/* ============================================================================ + 7. POLISH PHASE (band + 3-col) + ============================================================================ */ + +.designing-kinpaku .designing-polish { + display: flex; + flex-direction: column; + gap: 0; +} + +/* Polish band — a horizontal header row introducing audit/clarify/harden. + Stripped to a top + bottom hairline so it reads as a section header for + the 3-col grid below, not as a card competing with them. */ +.designing-kinpaku .designing-polish-band { + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + gap: 20px; + padding: 18px 0; + background: transparent; + border: 0; + border-top: 1px solid var(--ks-rule); + border-bottom: 1px solid var(--ks-rule); + border-radius: 0; + margin-bottom: 36px; +} +.designing-kinpaku .designing-polish-band-label { + color: var(--ks-patina); + font-family: var(--ks-mono); + font-size: 0.66rem; + letter-spacing: 0.28em; + text-transform: uppercase; +} +.designing-kinpaku .designing-polish-band-cmds { + display: flex; + gap: 14px; + flex-wrap: wrap; +} +.designing-kinpaku .designing-polish-band-cmds a { + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-size: 1rem; + text-decoration: underline; + text-decoration-color: var(--ks-kinpaku); + text-underline-offset: 4px; +} +.designing-kinpaku .designing-polish-band-meta { + color: var(--ks-muted); + font-family: var(--ks-mono); + font-size: 0.7rem; + letter-spacing: 0.18em; +} + +.designing-kinpaku .designing-polish-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 24px; +} +@media (max-width: 900px) { + .designing-kinpaku .designing-polish-grid { grid-template-columns: 1fr; } +} + +.designing-kinpaku .designing-polish-col { + padding: 28px; + background: var(--ks-lacquer-raised); + border: 1px solid var(--ks-rule); + border-radius: 2px; + display: flex; + flex-direction: column; + gap: 10px; +} +.designing-kinpaku .designing-polish-name { + color: var(--ks-champagne); + font-family: var(--ks-font); + font-weight: 600; + font-size: 1.2rem; + line-height: 1.3; + letter-spacing: -0.01em; + margin: 0; + text-wrap: balance; +} +.designing-kinpaku .designing-polish-desc { + color: var(--ks-text); + font-size: 0.94rem; + line-height: 1.6; + margin: 0; + text-wrap: pretty; +} + +/* ============================================================================ + 8. MAINTAIN PHASE (extract + DESIGN.md preview) + ============================================================================ */ + +/* The .designing-maintain / .designing-lanes / .designing-cta classes share + markup with .ks-bento now, but docs-visuals.css still asserts a 1fr 1fr grid + on them at equal specificity to the kit. That declaration loads after the + kit and wins. Reassert the kit's 12-column grid via a chained selector + (specificity 0,2,0) so the .ks-bento-tile--span-* sizing actually works. + Same trick for the legacy maintain-tile / cta-card flex chrome that + conflicts with the kit tile's vertical-stack flex column. */ +.designing-kinpaku .ks-bento.designing-maintain, +.designing-kinpaku .ks-bento.designing-lanes, +.designing-kinpaku .ks-bento.designing-surfaces, +.designing-kinpaku .ks-bento.designing-cta { + grid-template-columns: repeat(12, minmax(0, 1fr)); + gap: 8px; +} + +/* Surfaces grounding (skill / CLI / extension). Each tile carries a small + mono identifier line: a terminal command for the CLI, a browser address + for the extension. */ +.designing-kinpaku .designing-surface-cmd { + display: flex; + align-items: center; + gap: 10px; + margin: 2px 0; + padding: 11px 14px; + background: oklch(13% 0.006 95); + border: 1px solid var(--ks-rule); + border-radius: 4px; + font-family: var(--ks-mono); + font-size: 0.84rem; + color: var(--ks-champagne); +} +.designing-kinpaku .designing-surface-cmd-prompt { + color: var(--ks-kinpaku); +} +.designing-kinpaku .designing-surface-cmd--chrome { + gap: 7px; +} +.designing-kinpaku .designing-surface-cmd-url { + margin-left: 6px; + color: var(--ks-muted); +} + +.designing-kinpaku .designing-maintain-stage { + background: var(--ks-lacquer-raised); + border: 1px solid var(--ks-rule); + border-radius: 4px; + padding: 28px; + min-height: 180px; + display: grid; + place-items: center; +} + +.designing-kinpaku .designing-maintain-caption { + display: flex; + flex-direction: column; + gap: 8px; +} +.designing-kinpaku .designing-maintain-label { + color: var(--ks-patina); + font-family: var(--ks-mono); + font-size: 0.62rem; + letter-spacing: 0.24em; + text-transform: uppercase; +} +.designing-kinpaku .designing-maintain-label a { + color: var(--ks-kinpaku); + text-decoration: underline; + text-decoration-color: var(--ks-kinpaku); + text-underline-offset: 3px; +} +.designing-kinpaku .designing-maintain-name { + color: var(--ks-champagne); + font-family: var(--ks-font); + font-weight: 600; + font-size: 1.2rem; + line-height: 1.3; + letter-spacing: -0.01em; + margin: 0; +} +.designing-kinpaku .designing-maintain-desc { + color: var(--ks-text); + font-size: 0.94rem; + line-height: 1.6; + margin: 0; + text-wrap: pretty; +} + +/* Extract viz — buttons getting consolidated into "Button" primitive */ +.designing-kinpaku .designing-extract-viz { + display: grid; + grid-template-columns: 1fr auto 1fr; + gap: 20px; + align-items: center; + width: 100%; +} +.designing-kinpaku .designing-extract-before { + display: flex; + flex-wrap: wrap; + gap: 6px; +} +.designing-kinpaku .designing-extract-btn { + display: inline-flex; + align-items: center; + padding: 5px 11px; + background: oklch(13% 0.006 95); + border: 1px solid var(--ks-rule); + border-radius: 3px; + color: var(--ks-muted); + font-family: var(--ks-mono); + font-size: 0.74rem; +} +.designing-kinpaku .designing-extract-arrow { + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-size: 1.2rem; +} +.designing-kinpaku .designing-extract-after { + justify-self: end; + padding: 8px 18px; + background: var(--ks-kinpaku-pale); + color: var(--ks-lacquer-deep); + border-radius: 2px; + font-family: var(--ks-font); + font-size: 0.9rem; +} + +/* DESIGN.md preview lines */ +.designing-kinpaku .designing-designmd-preview { + display: grid; + gap: 8px; + width: 100%; +} +.designing-kinpaku .designing-designmd-preview-line { + display: grid; + grid-template-columns: 32px 1fr; + gap: 14px; + align-items: baseline; + padding: 6px 0; + border-bottom: 1px solid var(--ks-rule); + color: var(--ks-text); + font-size: 0.92rem; +} +.designing-kinpaku .designing-designmd-preview-line:last-child { border-bottom: 0; } +.designing-kinpaku .designing-designmd-preview-num { + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-size: 0.7rem; + letter-spacing: 0.16em; +} + +/* ============================================================================ + 9. BRAND vs PRODUCT LANES + ============================================================================ */ + +/* .designing-lanes 2-column grid + .designing-lane tile chrome now come + from .ks-bento + .ks-bento-tile. Only lane content stays. The trailing + margin below the lanes is preserved here so the "Read the tutorial" link + has breathing room. */ +.designing-kinpaku .designing-lanes { + margin-bottom: 24px; +} + +.designing-kinpaku .designing-lane-kind { + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-size: 0.66rem; + letter-spacing: 0.28em; + text-transform: uppercase; +} +.designing-kinpaku .designing-lane:nth-child(2) .designing-lane-kind { + color: var(--ks-patina); +} + +.designing-kinpaku .designing-lane-rule { + color: var(--ks-text); + font-size: 1rem; + line-height: 1.6; + margin: 0; + text-wrap: pretty; +} + +.designing-kinpaku .designing-lane-mock { + margin-top: 6px; + padding: 24px; + background: oklch(11% 0.006 95); + border: 1px solid var(--ks-rule); + border-radius: 4px; + min-height: 110px; + display: flex; + flex-direction: column; + gap: 10px; +} +.designing-kinpaku .designing-lane-mock--brand { + align-items: flex-start; +} +.designing-kinpaku .designing-lane-mock--product { + align-items: flex-start; +} +.designing-kinpaku .designing-lane-mock-label { + color: var(--ks-patina); + font-family: var(--ks-mono); + font-size: 0.66rem; + letter-spacing: 0.2em; + text-transform: uppercase; +} +.designing-kinpaku .designing-lane-mock--brand .designing-lane-mock-title { + color: var(--ks-champagne); + font-family: var(--ks-font-display); + font-weight: 300; + font-size: 1.6rem; +} +.designing-kinpaku .designing-lane-mock--product .designing-lane-mock-title { + color: var(--ks-champagne); + font-family: var(--ks-font); + font-size: 1rem; + font-weight: 500; +} + +.designing-kinpaku .designing-lane-link { + display: inline-block; + margin-top: 4px; + color: var(--ks-kinpaku); + font-size: 0.9rem; + text-decoration: underline; + text-decoration-color: var(--ks-kinpaku); + text-underline-offset: 4px; +} + +/* ============================================================================ + 10. AVOID LIST + ============================================================================ */ + +.designing-kinpaku .designing-avoid { + list-style: none; + margin: 0; + padding: 0; + display: grid; + gap: 18px; +} +.designing-kinpaku .designing-avoid li { + display: grid; + grid-template-columns: 28px 1fr; + gap: 14px; + padding: 22px 24px; + background: var(--ks-lacquer-raised); + border: 1px solid var(--ks-rule); + border-radius: 2px; +} +.designing-kinpaku .designing-avoid-x { + color: var(--ks-vermilion); + font-family: var(--ks-mono); + font-size: 1.2rem; + line-height: 1.4; + text-align: center; +} +.designing-kinpaku .designing-avoid-title { + display: block; + color: var(--ks-champagne); + font-family: var(--ks-font); + font-weight: 500; + font-size: 1rem; + margin-bottom: 6px; +} +.designing-kinpaku .designing-avoid-title code { color: var(--ks-kinpaku); } +.designing-kinpaku .designing-avoid-desc { + color: var(--ks-text); + font-size: 0.94rem; + line-height: 1.65; + margin: 0; + text-wrap: pretty; +} + +/* ============================================================================ + 11. CTA CARDS + ============================================================================ */ + +/* Two standalone destination cards — no bento plinth (the plinth-on-tile + nesting read as a box-in-a-box). Raised lacquer panels with a gold hairline + that snaps to kinpaku on hover, matching the card vocabulary used elsewhere + on the page: small radius, flat, sharply bounded. */ +.designing-kinpaku .designing-cta { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + margin-top: 72px; +} +@media (max-width: 720px) { + .designing-kinpaku .designing-cta { + grid-template-columns: 1fr; + } +} + +.designing-kinpaku .designing-cta-card { + display: flex; + flex-direction: column; + gap: 14px; + padding: 40px; + background: var(--ks-lacquer-raised); + border: 1px solid var(--ks-rule); + border-radius: 4px; + text-decoration: none; + transition: border-color 200ms var(--ks-ease), transform 200ms var(--ks-ease); +} +.designing-kinpaku .designing-cta-card:hover { + border-color: var(--ks-kinpaku); + transform: translateY(-2px); +} + +.designing-kinpaku .designing-cta-card-kind { + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-size: 0.66rem; + letter-spacing: 0.28em; + text-transform: uppercase; +} +.designing-kinpaku .designing-cta-card:nth-child(2) .designing-cta-card-kind { + color: var(--ks-patina); +} + +.designing-kinpaku .designing-cta-card-title { + color: var(--ks-champagne); + font-family: var(--ks-font-display); + font-weight: 300; + font-size: clamp(1.8rem, 3vw, 2.4rem); + line-height: 1.1; + margin: 0; + letter-spacing: 0.01em; +} +.designing-kinpaku .designing-cta-card-title em { + color: var(--ks-kinpaku); +} + +.designing-kinpaku .designing-cta-card-desc { + color: var(--ks-text); + font-size: 0.96rem; + line-height: 1.65; + margin: 0; + text-wrap: pretty; +} diff --git a/site/styles/detector-lab.css b/site/styles/detector-lab.css index a8232569c..d1c11b528 100644 --- a/site/styles/detector-lab.css +++ b/site/styles/detector-lab.css @@ -1,5 +1,17 @@ +/* + * Detector Lab — internal fixture browser + timing dashboard. + * + * Neo-kinpaku tool shell. This is a genuinely page-specific app layout + * (fixed sidebar + workspace + iframe + metrics), so it keeps its bespoke + * structure rather than forcing the marketing kit's .ks-section grid onto it. + * Every color, rule, and type value reads from kinpaku-tokens.css (--ks-*). + * The fixture iframe renders untouched test HTML — only the lab chrome is themed. + */ + .detector-lab-page { - background: var(--color-cream); + background: var(--ks-lacquer); + color: var(--ks-text); + font-family: var(--ks-font); overflow: hidden; } @@ -12,7 +24,7 @@ --detector-sidebar-width: 320px; height: 100vh; min-height: 0; - background: var(--color-cream); + background: var(--ks-lacquer); } .detector-fixture-nav { @@ -22,72 +34,58 @@ display: flex; flex-direction: column; width: var(--detector-sidebar-width); - border-right: 1px solid var(--color-mist); - background: var(--color-paper); + border-right: 1px solid var(--ks-rule); + background: var(--ks-lacquer-deep); } .detector-nav-head { flex-shrink: 0; padding: var(--spacing-md); - border-bottom: 1px solid var(--color-mist); + border-bottom: 1px solid var(--ks-rule); } .detector-home-link { - display: inline-flex; - align-items: center; - gap: 9px; - color: var(--color-ink); + color: var(--ks-kinpaku); text-decoration: none; } -.detector-home-link span { - display: grid; - place-items: center; - width: 24px; - height: 24px; - border-radius: 6px; - background: var(--color-ink); - color: var(--color-paper); - font-family: var(--font-body); - font-weight: 700; - line-height: 1; -} +.detector-home-link .ks-mark { width: 30px; height: 30px; } +.detector-home-link .ks-mark svg { width: 26px; height: 26px; } -.detector-home-link strong { - color: var(--color-ink); - font-family: var(--font-display); - font-size: 1.125rem; - font-weight: 600; +.detector-home-link .ks-wordmark { + font-size: 1.05rem; + letter-spacing: 0.16em; } .detector-nav-head h1 { margin-top: var(--spacing-md); - font-family: var(--font-body); + color: var(--ks-champagne); + font-family: var(--ks-font); font-size: 1.25rem; - font-weight: 800; + font-weight: 600; line-height: 1.15; letter-spacing: 0; } .detector-nav-head > p { margin-top: 6px; - color: var(--color-charcoal); + color: var(--ks-text-muted); font-size: 0.875rem; - line-height: 1.45; + line-height: 1.5; } .detector-summary-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); margin-top: var(--spacing-md); - border: 1px solid var(--color-mist); + border: 1px solid var(--ks-rule); } .detector-summary-grid div { min-width: 0; padding: 10px; - border-right: 1px solid var(--color-mist); - border-bottom: 1px solid var(--color-mist); + border-right: 1px solid var(--ks-rule); + border-bottom: 1px solid var(--ks-rule); } .detector-summary-grid div:nth-child(2n) { @@ -102,20 +100,20 @@ .detector-panel-label, .detector-nav-group h2, .detector-live-stats dt { - font-family: var(--font-mono); + font-family: var(--ks-mono); font-size: 0.6875rem; font-weight: 500; - letter-spacing: 0.08em; + letter-spacing: 0.16em; text-transform: uppercase; - color: var(--color-ash); + color: var(--ks-text-muted); } .detector-summary-grid dd { margin-top: 4px; overflow-wrap: anywhere; - color: var(--color-ink); + color: var(--ks-champagne); font-size: 1rem; - font-weight: 700; + font-weight: 600; line-height: 1.15; } @@ -127,13 +125,18 @@ .detector-nav-group { padding: var(--spacing-sm); - border-bottom: 1px solid var(--color-mist); + border-bottom: 1px solid var(--ks-rule); } .detector-nav-group:last-child { border-bottom: 0; } +.detector-nav-group h2 { + letter-spacing: 0.2em; + color: var(--ks-kinpaku-deep); +} + .detector-nav-list { display: grid; gap: 4px; @@ -149,43 +152,44 @@ min-height: 38px; padding: 8px 10px; border: 1px solid transparent; - border-radius: 6px; + border-radius: 4px; background: transparent; - color: var(--color-charcoal); + color: var(--ks-text-muted); + font-family: var(--ks-font); text-align: left; cursor: pointer; transition: - background var(--duration-fast) var(--ease-out), - border-color var(--duration-fast) var(--ease-out), - color var(--duration-fast) var(--ease-out); + background var(--duration-fast, 140ms) var(--ks-ease), + border-color var(--duration-fast, 140ms) var(--ks-ease), + color var(--duration-fast, 140ms) var(--ks-ease); } .detector-fixture-button:hover { - background: var(--color-cream); - color: var(--color-ink); + background: var(--ks-graphite); + color: var(--ks-champagne); } .detector-fixture-button:focus-visible { - outline: 2px solid var(--color-accent); + outline: 2px solid var(--ks-rule-strong); outline-offset: 2px; } .detector-fixture-button[aria-pressed="true"] { - background: var(--color-ink); - border-color: var(--color-ink); - color: var(--color-paper); + background: var(--ks-kinpaku); + border-color: var(--ks-kinpaku); + color: var(--ks-lacquer-deep); } .detector-fixture-button span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - font-weight: 700; + font-weight: 500; } .detector-fixture-button span:last-child { min-width: 2ch; - font-family: var(--font-mono); + font-family: var(--ks-mono); font-size: 0.75rem; color: currentColor; text-align: right; @@ -212,27 +216,27 @@ align-items: center; min-height: 72px; padding: 12px var(--spacing-md); - border-bottom: 1px solid var(--color-mist); - background: var(--color-paper); + border-bottom: 1px solid var(--ks-rule); + background: var(--ks-lacquer-deep); } .detector-preview-head h2, .detector-panel-head h2, .detector-table-panel h2 { - font-family: var(--font-body); + font-family: var(--ks-font); font-size: 1rem; - font-weight: 800; + font-weight: 600; line-height: 1.2; letter-spacing: 0; - color: var(--color-ink); + color: var(--ks-champagne); } .detector-preview-head p:not(.detector-panel-label) { max-width: 78ch; margin-top: 4px; - color: var(--color-charcoal); + color: var(--ks-text-muted); font-size: 0.875rem; - line-height: 1.45; + line-height: 1.5; } .detector-preview-actions { @@ -247,26 +251,47 @@ display: inline-flex; align-items: center; justify-content: center; - min-height: 34px; - padding: 0 12px; - border: 1px solid var(--color-mist); - border-radius: 6px; - background: var(--color-paper); - color: var(--color-ink); + min-height: 36px; + padding: 0 16px; + border: 1px solid var(--ks-rule-strong); + border-radius: 2px; + background: transparent; + color: var(--ks-kinpaku); + font-family: var(--ks-font); font-size: 0.8125rem; - font-weight: 700; + font-weight: 500; text-decoration: none; white-space: nowrap; cursor: pointer; transition: - border-color var(--duration-fast) var(--ease-out), - color var(--duration-fast) var(--ease-out); + background var(--duration-fast, 140ms) var(--ks-ease), + border-color var(--duration-fast, 140ms) var(--ks-ease), + color var(--duration-fast, 140ms) var(--ks-ease), + transform var(--duration-fast, 140ms) var(--ks-ease); } .detector-preview-actions a:hover, .detector-preview-actions button:hover { - border-color: var(--color-accent); - color: var(--color-accent); + background: oklch(77% 0.14 82 / 0.1); + transform: translateY(-1px); +} + +/* Run browser scan is the primary action: gold fill, dark text. */ +.detector-preview-actions button[data-rerun-browser] { + background: var(--ks-kinpaku); + border-color: var(--ks-kinpaku); + color: var(--ks-lacquer-deep); +} + +.detector-preview-actions button[data-rerun-browser]:hover { + background: var(--ks-kinpaku-pale); + border-color: var(--ks-kinpaku-pale); +} + +.detector-preview-actions a:focus-visible, +.detector-preview-actions button:focus-visible { + outline: 2px solid var(--ks-patina); + outline-offset: 3px; } .detector-content-grid { @@ -281,12 +306,12 @@ .detector-metric-panel, .detector-table-panel { min-width: 0; - border: 1px solid var(--color-mist); - background: var(--color-paper); + border: 1px solid var(--ks-rule); + background: var(--ks-lacquer-raised); } .detector-frame-wrap { - background: oklch(95% 0.004 350); + background: var(--ks-lacquer-deep); } .detector-frame-wrap iframe { @@ -295,7 +320,7 @@ height: max(360px, calc(100vh - 156px)); min-height: 0; border: 0; - background: var(--color-paper); + background: var(--ks-lacquer-deep); } .detector-workspace-scroll { @@ -319,7 +344,7 @@ .detector-panel-head { padding: var(--spacing-sm); - border-bottom: 1px solid var(--color-mist); + border-bottom: 1px solid var(--ks-rule); } .detector-live-stats { @@ -329,8 +354,8 @@ .detector-live-stats div { padding: var(--spacing-sm); - border-right: 1px solid var(--color-mist); - border-bottom: 1px solid var(--color-mist); + border-right: 1px solid var(--ks-rule); + border-bottom: 1px solid var(--ks-rule); } .detector-live-stats div:nth-child(2n) { @@ -340,8 +365,8 @@ .detector-live-stats dd { margin-top: 4px; font-size: 1.125rem; - font-weight: 800; - color: var(--color-ink); + font-weight: 600; + color: var(--ks-champagne); } .detector-rule-pills { @@ -357,11 +382,11 @@ align-items: center; gap: 6px; max-width: 100%; - padding: 5px 7px; - border: 1px solid var(--color-mist); - border-radius: 6px; - background: var(--color-cream); - color: var(--color-charcoal); + padding: 5px 8px; + border: 1px solid var(--ks-rule); + border-radius: 2px; + background: var(--ks-graphite); + color: var(--ks-text); font-size: 0.75rem; line-height: 1.2; } @@ -371,18 +396,19 @@ padding: 0; overflow: hidden; background: transparent; - color: var(--color-ink); + color: var(--ks-champagne); + font-family: var(--ks-mono); text-overflow: ellipsis; white-space: nowrap; } .detector-rule-pill span { - font-family: var(--font-mono); - color: var(--color-accent); + font-family: var(--ks-mono); + color: var(--ks-patina); } .detector-empty-pill { - color: var(--color-ash); + color: var(--ks-text-faint); } .detector-tables { @@ -397,14 +423,14 @@ justify-content: space-between; gap: var(--spacing-md); padding: var(--spacing-sm); - border-bottom: 1px solid var(--color-mist); + border-bottom: 1px solid var(--ks-rule); } .detector-table-panel header p:not(.detector-panel-label) { max-width: 28ch; - color: var(--color-ash); + color: var(--ks-text-faint); font-size: 0.8125rem; - line-height: 1.45; + line-height: 1.5; text-align: right; } @@ -423,8 +449,8 @@ .detector-table-panel th, .detector-table-panel td { padding: 8px 10px; - border-bottom: 1px solid var(--color-mist); - color: var(--color-charcoal); + border-bottom: 1px solid var(--ks-rule); + color: var(--ks-text); text-align: left; vertical-align: top; } @@ -433,19 +459,20 @@ position: sticky; top: 0; z-index: 1; - background: var(--color-paper); - font-family: var(--font-mono); + background: var(--ks-lacquer-raised); + font-family: var(--ks-mono); font-size: 0.6875rem; font-weight: 500; - letter-spacing: 0.04em; + letter-spacing: 0.12em; text-transform: uppercase; - color: var(--color-ash); + color: var(--ks-text-muted); } .detector-table-panel td code { padding: 0; background: transparent; - color: var(--color-ink); + color: var(--ks-champagne); + font-family: var(--ks-mono); font-size: 0.8125rem; } @@ -483,7 +510,7 @@ position: static; width: auto; border-right: 0; - border-bottom: 1px solid var(--color-mist); + border-bottom: 1px solid var(--ks-rule); } .detector-nav-scroll { @@ -546,6 +573,6 @@ } .detector-summary-grid div:nth-last-child(2) { - border-bottom: 1px solid var(--color-mist); + border-bottom: 1px solid var(--ks-rule); } } diff --git a/site/styles/divider-patterns.css b/site/styles/divider-patterns.css new file mode 100644 index 000000000..72abf8cfb --- /dev/null +++ b/site/styles/divider-patterns.css @@ -0,0 +1,153 @@ +/* ============================================================================ + Divider patterns — shared between /design-system (where they live as + foundation elements) and / (homepage, where they're applied between + sections via a dev toggle until we pick). + + Four CSS-only geometries: D1 double-notch, D2 ticks, D3 dot grid, + D5 circle anchor. Each composes with: + • Tone modifier — --patina, --plinth (defaults to kinpaku via currentColor) + • Texture modifier — --tex-leaf, --tex-patina, --tex-dust (uses the brand + accent images as the actual fill, with the pattern as a mask) + + Depends on the brand tokens in kinpaku-tokens.css (--ks-kinpaku, --ks-patina). + ============================================================================ */ + +.ksd-divider { + width: 100%; + color: var(--ks-kinpaku); +} + +/* Tone modifiers. Override currentColor so any pattern can be re-tinted + without touching its geometry. Patina reads as oxidized copper alongside + the gold; plinth is a low-chroma warm dark that sits one step above the + lacquer bg and reads as inscribed into the surface. */ +.ksd-divider--patina { color: var(--ks-patina); } +.ksd-divider--plinth { color: oklch(0.42 0.014 82); } + +/* Texture-fill modifiers. The pattern's geometry becomes a mask; the brand + accent texture becomes the visible fill. Filters match the .ks-accent-band + tuning so the inline divider reads as the same material as the foundation + textures. */ +.ksd-divider--tex-leaf { + --ksd-fill: url("/assets/neo-kinpaku/kinpaku-gold-leaf.png"); + --ksd-fill-filter: saturate(1.15) contrast(1.08); +} +.ksd-divider--tex-patina { + --ksd-fill: url("/assets/neo-kinpaku/verdigris-patina.png"); + --ksd-fill-filter: saturate(1.18) contrast(1.06); +} +.ksd-divider--tex-dust { + --ksd-fill: url("/assets/neo-kinpaku/gold-dust-rule.png"); + --ksd-fill-filter: saturate(1.22) contrast(1.15) brightness(1.12); +} + +/* ----- Geometry: D1 double-notch ----- */ +.ksd-divider--double-notch { + position: relative; + height: 8px; +} +.ksd-divider--double-notch::before, +.ksd-divider--double-notch::after { + content: ""; + position: absolute; + left: 0; + right: 0; + height: 1.5px; + background: currentColor; +} +.ksd-divider--double-notch::before { + top: 0; + background: linear-gradient(90deg, currentColor 0 9%, transparent 9% 11.5%, currentColor 11.5%); +} +.ksd-divider--double-notch::after { + bottom: 0; +} + +/* ----- Geometry: D2 ticks ----- */ +.ksd-divider--ticks { + height: 18px; + background: + repeating-linear-gradient( + -65deg, + currentColor 0 1.4px, + transparent 1.4px 12px + ); +} + +/* ----- Geometry: D3 dots ----- */ +.ksd-divider--dots { + height: 18px; + background-image: radial-gradient(circle, currentColor 0.9px, transparent 1.4px); + background-size: 7px 6px; + background-position: 0 1px; + background-repeat: repeat; +} + +/* ----- Geometry: D5 anchor ----- */ +.ksd-divider--anchor { + position: relative; + height: 14px; + display: flex; + align-items: center; + gap: 0; +} +.ksd-divider--anchor::before { + content: ""; + width: 11px; + height: 11px; + border: 1.5px solid currentColor; + border-radius: 50%; + flex: none; +} +.ksd-divider--anchor::after { + content: ""; + flex: 1; + height: 1.5px; + background: currentColor; +} + +/* ----- Texture-masked overrides per pattern ----- */ +.ksd-divider--ticks:is(.ksd-divider--tex-leaf, .ksd-divider--tex-patina, .ksd-divider--tex-dust) { + background: var(--ksd-fill) center / cover no-repeat; + filter: var(--ksd-fill-filter); + -webkit-mask-image: repeating-linear-gradient(-65deg, black 0 1.4px, transparent 1.4px 12px); + mask-image: repeating-linear-gradient(-65deg, black 0 1.4px, transparent 1.4px 12px); +} + +.ksd-divider--dots:is(.ksd-divider--tex-leaf, .ksd-divider--tex-patina, .ksd-divider--tex-dust) { + background-image: var(--ksd-fill); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + filter: var(--ksd-fill-filter); + -webkit-mask-image: radial-gradient(circle, black 0.9px, transparent 1.4px); + mask-image: radial-gradient(circle, black 0.9px, transparent 1.4px); + -webkit-mask-size: 7px 6px; + mask-size: 7px 6px; + -webkit-mask-repeat: repeat; + mask-repeat: repeat; + -webkit-mask-position: 0 1px; + mask-position: 0 1px; +} + +.ksd-divider--double-notch:is(.ksd-divider--tex-leaf, .ksd-divider--tex-patina, .ksd-divider--tex-dust)::before, +.ksd-divider--double-notch:is(.ksd-divider--tex-leaf, .ksd-divider--tex-patina, .ksd-divider--tex-dust)::after { + background: var(--ksd-fill) center / cover no-repeat; + filter: var(--ksd-fill-filter); +} +.ksd-divider--double-notch:is(.ksd-divider--tex-leaf, .ksd-divider--tex-patina, .ksd-divider--tex-dust)::before { + -webkit-mask-image: linear-gradient(90deg, black 0 9%, transparent 9% 11.5%, black 11.5%); + mask-image: linear-gradient(90deg, black 0 9%, transparent 9% 11.5%, black 11.5%); +} + +.ksd-divider--anchor:is(.ksd-divider--tex-leaf, .ksd-divider--tex-patina, .ksd-divider--tex-dust)::before { + border: 0; + background: var(--ksd-fill) center / cover no-repeat; + filter: var(--ksd-fill-filter); + -webkit-mask-image: radial-gradient(circle, transparent 0 4px, black 4px 5.5px, transparent 5.5px); + mask-image: radial-gradient(circle, transparent 0 4px, black 4px 5.5px, transparent 5.5px); +} +.ksd-divider--anchor:is(.ksd-divider--tex-leaf, .ksd-divider--tex-patina, .ksd-divider--tex-dust)::after { + background: var(--ksd-fill) center / cover no-repeat; + filter: var(--ksd-fill-filter); +} diff --git a/site/styles/docs-kinpaku.css b/site/styles/docs-kinpaku.css new file mode 100644 index 000000000..4fcdb7827 --- /dev/null +++ b/site/styles/docs-kinpaku.css @@ -0,0 +1,1353 @@ +/* ============================================================================ + /docs — Neo Kinpaku redesign + + Re-skins the docs index (/docs) and per-command detail (/docs/:slug) on the + dark lacquer kinpaku surface. Mirrors the /designing approach: remap the + generic light tokens (--color-paper, --color-cream, etc.) at the body class + so the inline visualizations from docs-visuals.css (.docs-viz-*) re-skin + into the dark theme without per-rule overrides. + + Structure of this file mirrors the page structure: + 1. Page scope + base + 2. Sidebar (commands rail) + 3. /docs index — categories + command tiles + 4. /docs/:slug — breadcrumb, hero, related-commands footer + 5. Prose body — markdown rendering on lacquer + 6. Footer + ============================================================================ */ + +@import "./kinpaku-tokens.css"; + + +/* ============================================================================ + 1. PAGE SCOPE + BASE + ============================================================================ */ + +.docs-kinpaku { + /* Surface/text/accent/font tokens default to kinpaku at :root now + (tokens.css), so the embedded docs-viz visualizations inherit dark + surfaces without a per-page remap. Only the --ks-muted alias (read by + name in this file) and the page shell remain. */ + --ks-muted: var(--ks-text-muted); + + background: + linear-gradient(180deg, oklch(7% 0.006 95), oklch(4% 0.004 95)); + color: var(--ks-text); + font-family: var(--ks-font); + min-height: 100vh; +} + +.docs-kinpaku a { + color: inherit; +} + +/* Page reset: the docs layout is its own full-bleed grid, no Base.astro + measure cap. */ +.docs-kinpaku.skills-layout-page main#main { + max-width: none; + margin: 0; + padding: 0; +} + +.docs-kinpaku .skills-layout { + display: grid; + grid-template-columns: 240px minmax(0, 1fr); + gap: clamp(40px, 4vw, 72px); + padding: 0 clamp(22px, 4vw, 56px); + align-items: start; + max-width: 1500px; + margin: 0 auto; +} + +@media (max-width: 960px) { + .docs-kinpaku .skills-layout { + grid-template-columns: 1fr; + gap: 18px; + } +} + + +/* ============================================================================ + 2. SIDEBAR — commands rail + + Sticky dark rail. Mono command names in champagne; the active command + gets a kinpaku 2px border-left accent. Category labels are mono caps in + kinpaku, mirroring the .ks-section-eyebrow voice. + ============================================================================ */ + +.docs-kinpaku .skills-sidebar { + position: sticky; + top: 86px; + align-self: start; + max-height: calc(100vh - 86px); + overflow-y: auto; + padding: 48px 0 64px; + border-right: 1px solid var(--ks-rule); + scrollbar-width: thin; + scrollbar-color: var(--ks-rule) transparent; +} + +@media (min-width: 961px) { + .docs-kinpaku .skills-sidebar { + min-height: calc(100vh - 86px); + } +} + +.docs-kinpaku .skills-sidebar::-webkit-scrollbar { + width: 6px; +} + +.docs-kinpaku .skills-sidebar::-webkit-scrollbar-thumb { + background: var(--ks-rule); + border-radius: 3px; +} + +.docs-kinpaku .skills-sidebar-inner { + padding-right: 22px; +} + +.docs-kinpaku .skills-sidebar-toggle { + display: none; +} + +.docs-kinpaku .skills-sidebar-label { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} + +.docs-kinpaku .skills-sidebar-group { + margin-bottom: 26px; +} + +.docs-kinpaku .skills-sidebar-group:last-child { + margin-bottom: 0; +} + +.docs-kinpaku .skills-sidebar-category { + display: block; + font-family: var(--ks-mono); + font-size: 0.66rem; + letter-spacing: 0.24em; + text-transform: uppercase; + color: var(--ks-kinpaku); + margin-bottom: 10px; + padding: 0 0 0 14px; +} + +.docs-kinpaku .skills-sidebar-list { + list-style: none; + padding: 0; + margin: 0; +} + +.docs-kinpaku .skills-sidebar-list li { + margin: 0; +} + +.docs-kinpaku .skills-sidebar-list a { + display: block; + padding: 5px 0 5px 12px; + border-left: 2px solid transparent; + font-family: var(--ks-mono); + font-size: 0.86rem; + font-weight: 400; + line-height: 1.5; + color: var(--ks-text); + text-decoration: none; + transition: color 160ms var(--ks-ease), + border-color 160ms var(--ks-ease); +} + +.docs-kinpaku .skills-sidebar-list a:hover { + color: var(--ks-kinpaku); +} + +.docs-kinpaku .skills-sidebar-list a[aria-current="page"] { + color: var(--ks-kinpaku); + font-weight: 500; + border-left-color: var(--ks-kinpaku); +} + + +/* Mobile: collapse the sidebar behind a toggle. */ +@media (max-width: 960px) { + .docs-kinpaku .skills-sidebar { + position: static; + max-height: none; + min-height: 0; + overflow: visible; + padding: 18px 0 0; + border-right: 0; + border-bottom: 1px solid var(--ks-rule); + } + + .docs-kinpaku .skills-sidebar-toggle { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 14px 0; + background: transparent; + border: 0; + color: var(--ks-champagne); + font-family: var(--ks-mono); + font-size: 0.72rem; + letter-spacing: 0.22em; + text-transform: uppercase; + cursor: pointer; + } + + .docs-kinpaku .skills-sidebar-toggle-chevron { + transition: transform 220ms var(--ks-ease); + color: var(--ks-kinpaku); + flex-shrink: 0; + } + + .docs-kinpaku .skills-sidebar-toggle[aria-expanded="true"] .skills-sidebar-toggle-chevron { + transform: rotate(180deg); + } + + .docs-kinpaku .skills-sidebar-inner { + display: none; + padding: 14px 0 24px; + } + + .docs-kinpaku .skills-sidebar-toggle[aria-expanded="true"] + .skills-sidebar-inner { + display: block; + } +} + + +/* ============================================================================ + 3. /docs INDEX + + Sections, in render order: + a. Hero + b. Live mode callout (the headliner capability) + c. A typical session (linear workflow of 4 commands) + d. Pairs and chains (relational schema) + e. Full reference (cheatsheet by lifecycle phase) + ============================================================================ */ + +.docs-kinpaku .docs-index { + padding: clamp(64px, 8vw, 110px) 0 clamp(80px, 10vw, 140px); +} + + +/* a. Hero -------------------------------------------------------------- */ + +.docs-kinpaku .docs-index-header { + margin-bottom: clamp(72px, 9vw, 110px); + max-width: 760px; +} + +.docs-kinpaku .docs-index-eyebrow { + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-size: var(--ks-type-eyebrow-size); + letter-spacing: var(--ks-type-eyebrow-track); + text-transform: uppercase; + margin: 0 0 18px; +} + +.docs-kinpaku .docs-index-title { + font-family: var(--ks-font-display); + font-style: normal; + font-weight: var(--ks-type-display-weight); + font-size: var(--ks-type-display-size); + line-height: var(--ks-type-display-line); + letter-spacing: var(--ks-type-display-track); + color: var(--ks-champagne); + margin: 0 0 22px; + text-wrap: balance; +} + +.docs-kinpaku .docs-index-mark { + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-weight: 400; + letter-spacing: 0; + font-size: 0.82em; + vertical-align: 0.04em; +} + +.docs-kinpaku .docs-index-lede { + font-size: 1.1rem; + line-height: 1.65; + color: var(--ks-text); + margin: 0; + max-width: 58ch; +} + + +/* Shared section scaffolding for the four index sections ---------------- */ + +.docs-kinpaku .docs-live, +.docs-kinpaku .docs-flow-section, +.docs-kinpaku .docs-chains-section, +.docs-kinpaku .docs-reference-section { + margin-bottom: clamp(64px, 8vw, 110px); +} + +.docs-kinpaku .docs-reference-section:last-child { + margin-bottom: 0; +} + +.docs-kinpaku .docs-live-eyebrow, +.docs-kinpaku .docs-flow-eyebrow, +.docs-kinpaku .docs-chains-eyebrow, +.docs-kinpaku .docs-reference-eyebrow { + font-family: var(--ks-mono); + font-size: 0.7rem; + letter-spacing: 0.24em; + text-transform: uppercase; + color: var(--ks-kinpaku); + margin: 0 0 14px; +} + +.docs-kinpaku .docs-live-title, +.docs-kinpaku .docs-flow-title, +.docs-kinpaku .docs-chains-title, +.docs-kinpaku .docs-reference-title { + font-family: var(--ks-font-display); + font-style: normal; + font-weight: var(--ks-type-headline-weight); + font-size: var(--ks-type-headline-size); + line-height: var(--ks-type-headline-line); + letter-spacing: -0.005em; + color: var(--ks-champagne); + margin: 0 0 14px; + text-wrap: balance; +} + +.docs-kinpaku .docs-live-lede, +.docs-kinpaku .docs-flow-sub, +.docs-kinpaku .docs-chains-sub { + font-size: 1rem; + line-height: 1.65; + color: var(--ks-text); + margin: 0; + max-width: 52ch; +} + + +/* b. Live mode callout ------------------------------------------------- */ + +.docs-kinpaku .docs-live { + display: grid; + grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); + gap: clamp(36px, 5vw, 64px); + align-items: center; + padding: 36px 0 0; + border-top: 1px solid var(--ks-rule); +} + +@media (max-width: 880px) { + .docs-kinpaku .docs-live { + grid-template-columns: 1fr; + gap: 32px; + } +} + +.docs-kinpaku .docs-live-cta { + margin: 26px 0 0; +} + +/* Static browser frame mock. Lacquer-deep ground, kinpaku active outline + around the targeted element, mock action bar underneath. No animation; + this is a teaching diagram, not a demo. */ +.docs-kinpaku .docs-live-figure { + margin: 0; +} + +.docs-kinpaku .docs-live-frame { + position: relative; + background: var(--ks-lacquer-deep); + border: 1px solid var(--ks-rule); + border-radius: 4px; + overflow: hidden; + box-shadow: 0 24px 60px oklch(2% 0.004 95 / 0.55); +} + +.docs-kinpaku .docs-live-chrome { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 14px; + background: var(--ks-graphite); + border-bottom: 1px solid var(--ks-rule); +} + +.docs-kinpaku .docs-live-dot { + width: 9px; + height: 9px; + border-radius: 50%; + background: oklch(28% 0.01 95); +} + +.docs-kinpaku .docs-live-url { + margin-left: 12px; + font-family: var(--ks-mono); + font-size: 0.72rem; + color: var(--ks-text-muted); + letter-spacing: 0.04em; +} + +.docs-kinpaku .docs-live-stage { + position: relative; + padding: 42px 28px 28px; + min-height: 220px; +} + +.docs-kinpaku .docs-live-target { + position: relative; + padding: 22px 24px; + border: 1px solid var(--ks-kinpaku); + border-radius: 2px; + background: oklch(11% 0.012 82); + /* Picker outline corner ticks (top-left & bottom-right) */ + box-shadow: + inset 0 0 0 1px transparent, + -2px -2px 0 -1px var(--ks-kinpaku), + 2px 2px 0 -1px var(--ks-kinpaku); +} + +.docs-kinpaku .docs-live-target-title { + font-family: var(--ks-font-display); + font-style: normal; + font-weight: 600; + font-size: 1.4rem; + line-height: 1.15; + color: var(--ks-champagne); +} + +.docs-kinpaku .docs-live-target-sub { + margin-top: 8px; + font-family: var(--ks-mono); + font-size: 0.7rem; + letter-spacing: 0.22em; + text-transform: uppercase; + color: var(--ks-kinpaku); +} + +.docs-kinpaku .docs-live-bar { + margin-top: 22px; + display: flex; + align-items: center; + gap: 14px; + padding: 10px 14px; + background: oklch(8% 0.006 95); + border: 1px solid var(--ks-rule); + border-radius: 2px; + font-family: var(--ks-mono); + font-size: 0.74rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--ks-text-muted); +} + +.docs-kinpaku .docs-live-bar-counter { + color: var(--ks-champagne); +} + +.docs-kinpaku .docs-live-bar-accept { + margin-left: auto; + color: var(--ks-kinpaku); +} + +.docs-kinpaku .docs-live-bar-divider { + width: 1px; + align-self: stretch; + background: var(--ks-rule); + margin-left: auto; +} + + +/* c. A typical session (linear flow of commands) ----------------------- */ + +.docs-kinpaku .docs-flow-head { + margin-bottom: 32px; +} + +.docs-kinpaku .docs-flow { + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: 1fr auto 1fr auto 1fr auto 1fr; + align-items: stretch; + gap: 0; +} + +@media (max-width: 880px) { + .docs-kinpaku .docs-flow { + grid-template-columns: 1fr; + gap: 8px; + } +} + +.docs-kinpaku .docs-flow-step { + display: contents; +} + +.docs-kinpaku .docs-flow-link { + display: grid; + align-content: start; + gap: 8px; + padding: 22px 22px 24px; + background: var(--ks-lacquer-raised); + border: 1px solid var(--ks-rule); + border-radius: 2px; + text-decoration: none; + color: inherit; + transition: border-color 180ms var(--ks-ease), + background 180ms var(--ks-ease); + min-height: 100%; +} + +.docs-kinpaku .docs-flow-link:hover { + border-color: var(--ks-kinpaku); + background: oklch(11% 0.012 82); +} + +.docs-kinpaku .docs-flow-verb { + font-family: var(--ks-mono); + font-size: 0.66rem; + letter-spacing: 0.24em; + text-transform: uppercase; + color: var(--ks-kinpaku); +} + +.docs-kinpaku .docs-flow-cmd { + font-family: var(--ks-mono); + font-size: 0.94rem; + color: var(--ks-champagne); +} + +.docs-kinpaku .docs-flow-tag { + font-size: 0.86rem; + line-height: 1.45; + color: var(--ks-text-muted); +} + +.docs-kinpaku .docs-flow-arrow { + display: grid; + place-items: center; + width: 36px; + color: var(--ks-rule-strong); +} + +.docs-kinpaku .docs-flow-arrow svg { + width: 24px; + height: 8px; +} + +@media (max-width: 880px) { + .docs-kinpaku .docs-flow-arrow { + width: 100%; + height: 18px; + transform: rotate(90deg); + } +} + +.docs-kinpaku .docs-flow-alt { + margin: 22px 0 0; + font-family: var(--ks-font); + font-size: 0.95rem; + line-height: 1.6; + color: var(--ks-text-muted); + max-width: 60ch; +} + +.docs-kinpaku .docs-flow-alt a { + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-size: 0.88em; + text-decoration: none; + border-bottom: 1px solid oklch(78% 0.12 82 / 0.4); + transition: border-color 160ms var(--ks-ease); +} + +.docs-kinpaku .docs-flow-alt a:hover { + border-bottom-color: var(--ks-kinpaku); +} + + +/* d. Pairs and chains -------------------------------------------------- */ + +.docs-kinpaku .docs-chains-head { + margin-bottom: 32px; +} + +.docs-kinpaku .docs-chains-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +@media (max-width: 700px) { + .docs-kinpaku .docs-chains-grid { + grid-template-columns: 1fr; + } +} + +.docs-kinpaku .docs-chain { + display: grid; + gap: 10px; + padding: 28px 28px 30px; + background: var(--ks-lacquer-raised); + border: 1px solid var(--ks-rule); + border-radius: 2px; +} + +.docs-kinpaku .docs-chain-formula { + display: inline-flex; + align-items: baseline; + gap: 14px; + margin: 0; + font-family: var(--ks-mono); + font-size: 1.06rem; + line-height: 1.2; +} + +.docs-kinpaku .docs-chain-cmd { + color: var(--ks-kinpaku); + text-decoration: none; + border-bottom: 1px solid transparent; + transition: border-color 160ms var(--ks-ease); +} + +.docs-kinpaku .docs-chain-cmd:hover { + border-bottom-color: var(--ks-kinpaku); +} + +.docs-kinpaku .docs-chain-arrow { + color: var(--ks-rule-strong); + font-size: 1.1em; +} + +.docs-kinpaku .docs-chain-line { + margin: 0; + font-size: 0.94rem; + line-height: 1.5; + color: var(--ks-text); +} + + +/* e. Full reference (cheatsheet) --------------------------------------- */ + +.docs-kinpaku .docs-reference-head { + margin-bottom: 32px; + padding-top: 36px; + border-top: 1px solid var(--ks-rule); +} + +.docs-kinpaku .docs-reference { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 36px 48px; +} + +@media (max-width: 760px) { + .docs-kinpaku .docs-reference { + grid-template-columns: 1fr; + gap: 32px; + } +} + +.docs-kinpaku .docs-reference-group { + display: grid; + gap: 10px; +} + +.docs-kinpaku .docs-reference-cat { + display: flex; + align-items: baseline; + gap: 10px; + margin: 0 0 6px; + padding-bottom: 8px; + border-bottom: 1px solid var(--ks-rule); + font-family: var(--ks-mono); + font-size: 0.72rem; + letter-spacing: 0.22em; + text-transform: uppercase; + color: var(--ks-champagne); +} + +.docs-kinpaku .docs-reference-count { + color: var(--ks-text-faint); + font-size: 0.86em; +} + +.docs-kinpaku .docs-reference-list { + list-style: none; + padding: 0; + margin: 0; + display: grid; + gap: 4px; +} + +.docs-kinpaku .docs-reference-row { + display: grid; + grid-template-columns: 96px minmax(0, 1fr); + align-items: baseline; + gap: 14px; + padding: 4px 0; + text-decoration: none; + color: inherit; + transition: color 160ms var(--ks-ease); +} + +.docs-kinpaku .docs-reference-row:hover .docs-reference-name { + color: var(--ks-kinpaku); +} + +.docs-kinpaku .docs-reference-row:hover .docs-reference-line { + color: var(--ks-champagne); +} + +.docs-kinpaku .docs-reference-name { + font-family: var(--ks-mono); + font-size: 0.92rem; + color: var(--ks-text); + transition: color 160ms var(--ks-ease); +} + +.docs-kinpaku .docs-reference-line { + font-size: 0.9rem; + line-height: 1.45; + color: var(--ks-text-muted); + transition: color 160ms var(--ks-ease); +} + + +/* ============================================================================ + 4. /docs/:slug — breadcrumb, hero, related-commands footer + ============================================================================ */ + +.docs-kinpaku .skills-main { + min-width: 0; + padding: 48px 0 clamp(80px, 10vw, 140px); +} + +.docs-kinpaku .skills-detail { + max-width: 900px; +} + +.docs-kinpaku .skills-breadcrumb { + display: flex; + align-items: center; + gap: 10px; + font-family: var(--ks-mono); + font-size: 0.7rem; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--ks-text-muted); + margin: 0 0 36px; +} + +.docs-kinpaku .skills-breadcrumb a { + color: var(--ks-kinpaku); + text-decoration: none; + transition: color 160ms var(--ks-ease); +} + +.docs-kinpaku .skills-breadcrumb a:hover { + color: var(--ks-champagne); +} + +.docs-kinpaku .skills-breadcrumb span[aria-hidden] { + color: var(--ks-text-faint); +} + + +/* Hero — eyebrow (category), title (/impeccable name), lede. */ +.docs-kinpaku .sub-page-header { + margin-bottom: clamp(40px, 5vw, 64px); +} + +.docs-kinpaku .sub-page-eyebrow { + display: block; + font-family: var(--ks-mono); + font-size: var(--ks-type-eyebrow-size); + letter-spacing: var(--ks-type-eyebrow-track); + text-transform: uppercase; + color: var(--ks-kinpaku); + margin: 0 0 16px; +} + +/* Small `/impeccable` prefix above the command name — reads like a + kinpaku eyebrow so the command verb (the h1) is the real subject. + This replaces the earlier prefix-as-inline-text approach where + `/impeccable` was at display scale and competed with the verb. */ +.docs-kinpaku .sub-page-prefix { + display: block; + font-family: var(--ks-mono); + font-size: 0.86rem; + letter-spacing: 0.02em; + color: var(--ks-text-muted); + margin: 0 0 8px; +} + +/* Command name (h1) — display scale, kinpaku gold so it carries the + page identity. The prefix above provides the `/impeccable …` context. */ +.docs-kinpaku .sub-page-title { + font-family: var(--ks-font-display); + font-weight: var(--ks-type-display-weight); + font-size: var(--ks-type-display-size); + line-height: var(--ks-type-display-line); + letter-spacing: var(--ks-type-display-track); + color: var(--ks-kinpaku); + margin: 0 0 20px; + text-wrap: balance; +} + +.docs-kinpaku .sub-page-lede { + font-size: 1.08rem; + line-height: 1.65; + color: var(--ks-text); + margin: 0; + max-width: 56ch; +} + + +/* Related commands footer — chips with relation label + command name. */ +.docs-kinpaku .skills-relationships { + margin-top: clamp(56px, 7vw, 88px); + padding-top: 32px; + border-top: 1px solid var(--ks-rule); +} + +.docs-kinpaku .skills-relationships-title { + font-family: var(--ks-mono); + font-size: 0.7rem; + letter-spacing: 0.24em; + text-transform: uppercase; + color: var(--ks-kinpaku); + font-weight: 400; + margin: 0 0 18px; +} + +.docs-kinpaku .skills-relationships-list { + display: flex; + flex-wrap: wrap; + gap: 12px; +} + +.docs-kinpaku .skills-relationship-chip { + display: inline-grid; + grid-auto-flow: column; + align-items: baseline; + gap: 10px; + padding: 10px 14px; + border: 1px solid var(--ks-rule); + border-radius: 2px; + background: var(--ks-lacquer-raised); + text-decoration: none; + transition: border-color 180ms var(--ks-ease), + background 180ms var(--ks-ease); +} + +.docs-kinpaku .skills-relationship-chip:hover { + border-color: var(--ks-kinpaku); + background: oklch(11% 0.012 82); +} + +.docs-kinpaku .skills-relationship-label { + font-family: var(--ks-mono); + font-size: 0.64rem; + letter-spacing: 0.22em; + text-transform: uppercase; + color: var(--ks-text-faint); +} + +.docs-kinpaku .skills-relationship-name { + font-family: var(--ks-mono); + font-size: 0.88rem; + color: var(--ks-kinpaku); +} + + +/* ============================================================================ + 5. PROSE BODY — markdown rendering on lacquer + ============================================================================ */ + +/* Vertical rhythm. + + One base unit: 1em (≈ 16-17px at the prose font-size). Everything is a + multiple of that. + + - paragraph-to-paragraph (sibling) = 1em + - paragraph-to-list = 1em (sibling rule) + - list-item-to-list-item = 0.25em (members of one thought) + - list-to-paragraph = 1em (sibling rule) + - block (pre/blockquote/hr) to neighbor = 1.4em (breathes more) + - h2 above body = 1.6em (anchor, not chasm) + - h3 above body = 1.2em + - heading to its body = 0.4em (kept tight) +*/ + +.docs-kinpaku .prose { + font-family: var(--ks-font); + font-size: 1.02rem; + line-height: 1.7; + color: var(--ks-text); + max-width: 76ch; +} + +/* Single source of truth for margins: + - `.prose > *` zeros every direct child's margin (universal selector, + beats browser default but loses to element-selector rules). + - `.prose > * + *` adds the sibling gap. + - Element-specific overrides (h2, h3, h4) inherit from the same family. + Element-selector rules elsewhere in this file MUST NOT set `margin` + shorthand or `margin-top` — they'd outrank the sibling rule and starve + blocks of their spacing (this happened with the list + p combo). */ +.docs-kinpaku .prose > * { + margin: 0; +} + +.docs-kinpaku .prose > * + * { + margin-top: 1em; +} + +/* Blocks (pre, blockquote, hr) get extra breath above and below. */ +.docs-kinpaku .prose > pre + *, +.docs-kinpaku .prose > * + pre, +.docs-kinpaku .prose > blockquote + *, +.docs-kinpaku .prose > * + blockquote, +.docs-kinpaku .prose > hr + *, +.docs-kinpaku .prose > * + hr { + margin-top: 1.4em; +} + +.docs-kinpaku .prose h1, +.docs-kinpaku .prose h2, +.docs-kinpaku .prose h3, +.docs-kinpaku .prose h4 { + font-family: var(--ks-font-display); + color: var(--ks-champagne); + font-style: normal; + line-height: 1.2; + scroll-margin-top: 96px; + /* Intentionally no margin — see `.prose > *` rule above. */ +} + +.docs-kinpaku .prose > * + h2 { + margin-top: 1.6em; +} + +.docs-kinpaku .prose > * + h3 { + margin-top: 1.2em; +} + +.docs-kinpaku .prose > * + h4 { + margin-top: 1em; +} + +.docs-kinpaku .prose > h2 + *, +.docs-kinpaku .prose > h3 + *, +.docs-kinpaku .prose > h4 + * { + margin-top: 0.4em; +} + +/* h2/h3 follow the DESIGN.md type scale exactly: + - h2 (Headline): Alumni Sans Pinstripe, clamp 2.6-3.4rem, weight 600 + - h3 (Title): Albert Sans, 1.18rem, weight 500 + - h4: Albert Sans, 1rem, weight 600 (utility) +*/ +.docs-kinpaku .prose h2 { + font-family: var(--ks-font-display); + font-size: var(--ks-type-headline-size); + font-weight: var(--ks-type-headline-weight); + line-height: var(--ks-type-headline-line); + letter-spacing: -0.005em; + padding-bottom: 14px; + border-bottom: 1px solid var(--ks-rule); + text-wrap: balance; +} + +.docs-kinpaku .prose h3 { + font-family: var(--ks-font); + font-size: var(--ks-type-title-size); + font-weight: var(--ks-type-title-weight); + line-height: var(--ks-type-title-line); + color: var(--ks-kinpaku); +} + +.docs-kinpaku .prose h4 { + font-family: var(--ks-font); + font-size: 1rem; + font-weight: 600; + letter-spacing: 0.02em; + color: var(--ks-champagne); +} + +/* Lists. Margins are handled by the universal `> *` reset + sibling rule + above; only set list-specific padding and line-height here. */ +.docs-kinpaku .prose ul, +.docs-kinpaku .prose ol { + padding-left: 1.4rem; + line-height: 1.6; +} + +.docs-kinpaku .prose li + li { + margin-top: 0.5em; +} + +.docs-kinpaku .prose li::marker { + color: var(--ks-kinpaku); +} + +/* When a list item wraps multi-block content, hug its internals. */ +.docs-kinpaku .prose li > p { + margin: 0; +} +.docs-kinpaku .prose li > * + * { + margin-top: 0.35em; +} + +.docs-kinpaku .prose a { + color: var(--ks-kinpaku); + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 4px; + text-decoration-color: oklch(78% 0.12 82 / 0.4); + transition: text-decoration-color 160ms var(--ks-ease); +} + +.docs-kinpaku .prose a:hover { + text-decoration-color: var(--ks-kinpaku); +} + +.docs-kinpaku .prose strong { + color: var(--ks-champagne); + font-weight: 600; +} + +.docs-kinpaku .prose em { + font-style: italic; + color: var(--ks-champagne); +} + +/* Inline code — champagne mono on a graphite chip. The chip is the + affordance; color stays neutral so a paragraph full of inline + references doesn't vibrate. Kinpaku is reserved for links, h3s, + eyebrows, and CTAs. `--ks-graphite-2` (≈19% lightness) reads as the + neutral gray the chip wants without picking up cool tint. */ +.docs-kinpaku .prose code { + font-family: var(--ks-mono); + font-size: 0.84em; + color: var(--ks-champagne); + background: var(--ks-graphite-2); + border: 0; + padding: 4px 7px; + border-radius: 3px; +} + +/* Inline code that is itself a link — get the kinpaku cue back, since + the link semantic is the meaningful one. */ +.docs-kinpaku .prose a code { + color: var(--ks-kinpaku); +} + +/* Fenced code blocks — deep lacquer terminal. Shiki injects inline + `background-color` and `color` on `pre.astro-code` (default theme is + `github-dark`, which reads cool-gray against the warm kinpaku ground), + so we override with !important to bake the theme into the kinpaku + palette. Syntax token spans inside keep their own colors. */ +.docs-kinpaku .prose pre, +.docs-kinpaku .prose .code-block, +.docs-kinpaku .prose pre.astro-code { + margin: 0; + padding: 22px 24px; + background: var(--ks-lacquer-deep) !important; + color: oklch(86% 0.025 82) !important; + border: 1px solid var(--ks-rule); + border-radius: 2px; + overflow-x: auto; + font-family: var(--ks-mono); + font-size: 0.84rem; + line-height: 1.6; + max-width: 100%; +} + +.docs-kinpaku .prose pre code, +.docs-kinpaku .prose .code-block code { + background: transparent; + border: 0; + padding: 0; + color: inherit; + font-size: inherit; + border-radius: 0; +} + +.docs-kinpaku .prose .code-block-wrap { + position: relative; + margin: 0; +} + +/* Copy button on dark — match the kinpaku button voice. */ +.docs-kinpaku .code-block-copy { + position: absolute; + top: 10px; + right: 10px; + padding: 4px 10px; + background: var(--ks-lacquer-raised); + border: 1px solid var(--ks-rule); + color: var(--ks-text-muted); + border-radius: 2px; + font-family: var(--ks-mono); + font-size: 0.62rem; + letter-spacing: 0.18em; + text-transform: uppercase; + cursor: pointer; + opacity: 0; + transition: opacity 180ms var(--ks-ease), + border-color 180ms var(--ks-ease), + color 180ms var(--ks-ease); +} + +.docs-kinpaku .code-block-wrap:hover .code-block-copy, +.docs-kinpaku .code-block-copy:focus-visible { + opacity: 1; +} + +.docs-kinpaku .code-block-copy::before { + content: "Copy"; +} + +.docs-kinpaku .code-block-copy:hover { + border-color: var(--ks-kinpaku); + color: var(--ks-kinpaku); +} + +.docs-kinpaku .code-block-copy.is-copied { + opacity: 1; + border-color: var(--ks-kinpaku); + color: var(--ks-lacquer-deep); + background: var(--ks-kinpaku); +} + +.docs-kinpaku .code-block-copy.is-copied::before { + content: "Copied"; +} + +.docs-kinpaku .prose blockquote { + margin: 0; + padding: 6px 0 6px 22px; + border-left: 2px solid var(--ks-kinpaku); + color: var(--ks-text); + font-style: italic; +} + +.docs-kinpaku .prose hr { + border: 0; + height: 1px; + background: var(--ks-rule); + margin: 32px 0; +} + +.docs-kinpaku .prose table { + width: 100%; + border-collapse: collapse; + font-size: 0.92rem; +} + +.docs-kinpaku .prose th, +.docs-kinpaku .prose td { + padding: 10px 14px; + text-align: left; + border-bottom: 1px solid var(--ks-rule); + vertical-align: top; +} + +.docs-kinpaku .prose th { + font-family: var(--ks-mono); + font-size: 0.7rem; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--ks-kinpaku); + font-weight: 500; + border-bottom-color: var(--ks-kinpaku); +} + + +/* Two-column editorial hero (when the command has a registered demo). + Demo on the left, h1 + lede on the right. Mirrors the old pre-Astro + layout: a slightly wider demo column (1.15fr) against a comfortable + text measure on the right (1fr). Collapses to single-column below + the breakpoint where the demo would be too cramped. + + When there's no demo, the page renders the existing single-column + sub-page-header — no docs-hero wrapper, no grid. */ +.docs-kinpaku .docs-hero--with-demo { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr); + gap: clamp(28px, 4vw, 48px); + align-items: center; + margin: 0 0 clamp(40px, 5vw, 64px); +} + +@media (max-width: 880px) { + .docs-kinpaku .docs-hero--with-demo { + grid-template-columns: 1fr; + gap: 32px; + } +} + +/* In the two-column hero, the h1 + lede sits in the right column and + doesn't need its own bottom margin (the grid gap handles spacing). */ +.docs-kinpaku .docs-hero--with-demo .sub-page-header { + margin: 0; +} + +/* Command demo (rendered client-side by Doc.astro from + site/scripts/demos/commands/.js). Same demos the homepage's + command carousel uses — one source of truth in the demo JS, two + surfaces that render from it. + + Matches the homepage's dark dual-tone treatment: lacquer-raised + container with a subtle kinpaku-grid backing, then `before` and + `after` sides each picking up a different radial glow. The demo + content's `--color-*` tokens stay remapped to kinpaku (no local + light override) — shapes render light-on-dark and the kinpaku + accent shines. */ +.docs-kinpaku .docs-command-demo { + margin: 0; + border: 1px solid var(--ks-rule); + border-radius: 4px; + overflow: hidden; + background: var(--ks-lacquer-raised); +} + +.docs-kinpaku .docs-command-demo .split-comparison { + max-width: none; + margin: 0; + padding: 0; +} + +.docs-kinpaku .docs-command-demo .split-container { + height: 300px; + max-width: none; + border-radius: 0; + border: 0; + border-bottom: 1px solid var(--ks-rule); + background: + linear-gradient(90deg, oklch(78% 0.12 82 / 0.04) 1px, transparent 1px), + linear-gradient(180deg, oklch(78% 0.12 82 / 0.035) 1px, transparent 1px), + var(--ks-lacquer-raised); + background-size: 24px 24px; +} + +.docs-kinpaku .docs-command-demo .split-before { + background: + linear-gradient(90deg, oklch(10% 0.006 95 / 0.92), oklch(12% 0.012 60 / 0.72)), + radial-gradient(circle at 28% 25%, oklch(60% 0.05 80 / 0.18), transparent 11rem); +} + +.docs-kinpaku .docs-command-demo .split-after { + background: + linear-gradient(90deg, oklch(8% 0.006 95 / 0.92), oklch(10% 0.006 95 / 0.96)), + radial-gradient(circle at 82% 26%, oklch(78% 0.12 82 / 0.14), transparent 12rem); +} + +.docs-kinpaku .docs-command-demo .split-divider { + background: var(--ks-kinpaku); + box-shadow: 0 0 20px oklch(78% 0.12 82 / 0.4); +} + +.docs-kinpaku .docs-command-demo .demo-caption { + padding: 10px 16px; + font-family: var(--ks-mono); + font-size: 0.72rem; + letter-spacing: 0.04em; + color: var(--ks-text-muted); + background: var(--ks-lacquer-deep); + text-align: center; +} + + +/* Visualization wrappers inside the markdown body. + + Default: .docs-viz-hero is a transparent layout container — no card + chrome, no border. The inner mock owns its own surface treatment, so + we don't get card-in-card. + + When the wrapper contains a multi-tile group (.docs-viz-flow), apply + the tinted-plinth pattern from .ks-bento on the homepage: + - the wrapper becomes a graphite mat + - tiles sit on top in flat lacquer with no borders + - the gap between tiles is the plinth showing through as inset gutters + - depth contrast carries the edges; no double borders required */ + +.docs-kinpaku .prose .docs-viz-hero { + margin: 0; + padding: 0; + background: transparent; + border: 0; + border-radius: 0; +} + +.docs-kinpaku .prose .docs-viz-hero:has(.docs-viz-flow), +.docs-kinpaku .prose .docs-viz-hero:has(.docs-viz-variants) { + padding: 24px; + background: var(--ks-graphite-2); + border-radius: 2px; +} + +.docs-kinpaku .prose .docs-viz-flow { + gap: 8px; +} + +.docs-kinpaku .prose .docs-viz-flow-step, +.docs-kinpaku .prose .docs-viz-variant { + background: var(--ks-lacquer); + border: 0; + border-radius: 2px; +} + +.docs-kinpaku .prose .docs-viz-flow-step + .docs-viz-flow-step { + margin-left: 0; +} + +.docs-kinpaku .prose .docs-viz-flow-step + .docs-viz-flow-step::before { + left: -8px; + width: 8px; +} + +/* Accent / active states get a kinpaku-tinted fill instead of the + bordered/glow treatment from docs-visuals.css. */ +.docs-kinpaku .prose .docs-viz-flow-step--accent, +.docs-kinpaku .prose .docs-viz-variant.is-active { + background: oklch(78% 0.12 82 / 0.16); + box-shadow: none; +} + +/* Per-variant theming in docs-visuals.css uses paper/cream/light-magenta + on the v1/v3 backgrounds — both off-brand here. Re-skin to lacquer + ramps so the variants still differ but stay kinpaku. */ +.docs-kinpaku .prose .docs-viz-variant--v1 { + background: var(--ks-lacquer); +} +.docs-kinpaku .prose .docs-viz-variant--v3 { + background: var(--ks-lacquer-deep); +} + +.docs-kinpaku .prose .docs-viz-caption { + margin-top: 16px; + color: var(--ks-text-muted); +} + + +/* ============================================================================ + 6. FOOTER on dark surface + ============================================================================ */ + +/* Footer chrome moved to kinpaku-kit.css (.kinpaku-chrome .site-footer). */ diff --git a/site/styles/docs-visuals.css b/site/styles/docs-visuals.css index 8ea08eb6f..4c7a4d02d 100644 --- a/site/styles/docs-visuals.css +++ b/site/styles/docs-visuals.css @@ -3064,15 +3064,25 @@ } .neon-case-page { - background: - radial-gradient(circle at 84% 8%, var(--color-accent-dim), transparent 22rem), - var(--color-cream); + background: var(--ks-lacquer); +} + +/* Kinpaku eyebrow/labels override the color from the shared craft+neon rule + above (which keeps its legacy accent for the craft-case component). */ +.neon-case-eyebrow, +.neon-case-section-label, +.neon-case-strip figure > span { + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + letter-spacing: 0.2em; } .neon-case { - width: min(100% - 32px, 1360px); + width: min(100%, 1392px); margin: 0 auto; - padding: clamp(4rem, 8vw, 6.5rem) 0 clamp(4rem, 8vw, 7rem); + /* Horizontal gutters mirror the site header's padding so the page edge + aligns with the header, instead of bleeding to the viewport. */ + padding: clamp(4rem, 8vw, 6.5rem) clamp(22px, 4vw, 56px) clamp(4rem, 8vw, 7rem); } .neon-case-hero { @@ -3085,37 +3095,38 @@ .neon-case-back { display: inline-block; margin-bottom: clamp(2rem, 5vw, 4rem); - font-family: var(--font-body); + font-family: var(--ks-font); font-size: 0.875rem; - font-weight: 600; - color: var(--color-ash); + font-weight: 500; + color: var(--ks-text-muted); text-decoration: none; + transition: color var(--duration-fast) var(--ks-ease); } .neon-case-back:hover { - color: var(--color-accent); + color: var(--ks-kinpaku); } .neon-case-hero h1 { margin: 0.8rem 0 1.5rem; - font-family: var(--font-display); - font-size: clamp(2.8rem, 5vw, 4.9rem); - font-style: italic; - font-weight: 400; - letter-spacing: -0.045em; - line-height: 0.96; - color: var(--color-ink); + font-family: var(--ks-font-display); + font-size: var(--ks-type-display-size); + font-weight: var(--ks-type-display-weight); + letter-spacing: var(--ks-type-display-track); + line-height: var(--ks-type-display-line); + color: var(--ks-champagne); max-width: 15ch; + text-wrap: balance; } .neon-case-hero p, .neon-case-command p { max-width: 58ch; margin: 0; - font-family: var(--font-body); - font-size: clamp(1rem, 1.5vw, 1.125rem); - line-height: 1.65; - color: var(--color-charcoal); + font-family: var(--ks-font); + font-size: clamp(1rem, 1.5vw, 1.1rem); + line-height: 1.75; + color: var(--ks-text); } .neon-case-actions { @@ -3129,23 +3140,26 @@ .neon-case-secondary { display: inline-flex; align-items: center; - min-height: 44px; - padding: 0.85rem 1.15rem; - font-family: var(--font-body); - font-size: 0.875rem; - font-weight: 700; + min-height: 48px; + padding: 0 1.4rem; + border: 1px solid transparent; + border-radius: 2px; + font-family: var(--ks-font); + font-size: 0.9rem; + font-weight: 500; text-decoration: none; - transition: transform var(--duration-base) var(--ease-out), background var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out); + transition: transform var(--duration-base) var(--ks-ease), background var(--duration-fast) var(--ks-ease), color var(--duration-fast) var(--ks-ease), border-color var(--duration-fast) var(--ks-ease); } .neon-case-primary { - color: var(--color-paper); - background: var(--color-ink); + color: var(--ks-lacquer-deep); + background: var(--ks-kinpaku); + border-color: var(--ks-kinpaku); } .neon-case-secondary { - color: var(--color-ink); - border: 1px solid var(--color-mist); + color: var(--ks-kinpaku); + border-color: var(--ks-rule-strong); } .neon-case-primary:hover, @@ -3154,26 +3168,27 @@ } .neon-case-primary:hover { - background: var(--color-accent); + background: var(--ks-kinpaku-pale); + border-color: var(--ks-kinpaku-pale); } .neon-case-secondary:hover { - color: var(--color-accent); - border-color: var(--color-accent); + background: oklch(77% 0.14 82 / 0.1); + border-color: var(--ks-kinpaku); } .neon-case-hero-shot { display: block; margin: 0; - border: 1px solid var(--color-mist); - background: var(--color-paper); + border: 1px solid var(--ks-rule); + background: var(--ks-lacquer-raised); overflow: hidden; - transition: transform var(--duration-base) var(--ease-out), border-color var(--duration-fast) var(--ease-out); + transition: transform var(--duration-base) var(--ks-ease), border-color var(--duration-fast) var(--ks-ease); } .neon-case-hero-shot:hover { transform: translateY(-3px); - border-color: var(--color-accent); + border-color: var(--ks-rule-strong); } .neon-case-hero-shot img { @@ -3199,8 +3214,8 @@ .neon-case-details img { display: block; width: 100%; - border: 1px solid var(--color-mist); - background: var(--color-paper); + border: 1px solid var(--ks-rule); + background: var(--ks-lacquer-raised); } .neon-case-strip a, @@ -3225,10 +3240,10 @@ .neon-case-strip figcaption, .neon-case-details figcaption { margin-top: 0.9rem; - font-family: var(--font-body); + font-family: var(--ks-font); font-size: 0.875rem; - line-height: 1.6; - color: var(--color-charcoal); + line-height: 1.65; + color: var(--ks-text-muted); } .neon-case-body { @@ -3236,47 +3251,48 @@ grid-template-columns: minmax(260px, 0.7fr) minmax(0, 1.3fr); gap: clamp(2rem, 6vw, 5rem); padding: clamp(3rem, 6vw, 5rem) 0; - border-top: 1px solid var(--color-mist); - border-bottom: 1px solid var(--color-mist); + border-top: 1px solid var(--ks-rule); + border-bottom: 1px solid var(--ks-rule); } .neon-case-body h2, .neon-case-command h2 { margin: 0.8rem 0 0; - font-family: var(--font-display); - font-size: clamp(2.25rem, 5vw, 4.6rem); - font-style: italic; - font-weight: 400; - letter-spacing: -0.035em; - line-height: 0.98; - color: var(--color-ink); + font-family: var(--ks-font-display); + font-size: clamp(2.2rem, 4vw, 3.4rem); + font-weight: var(--ks-type-headline-weight); + letter-spacing: -0.005em; + line-height: var(--ks-type-headline-line); + color: var(--ks-champagne); + text-wrap: balance; } .neon-case-notes { display: grid; gap: 1px; - background: var(--color-mist); - border: 1px solid var(--color-mist); + background: var(--ks-rule); + border: 1px solid var(--ks-rule); } .neon-case-notes article { padding: clamp(1.25rem, 3vw, 2rem); - background: var(--color-cream); + background: var(--ks-lacquer-raised); } .neon-case-notes h3 { margin: 0 0 0.6rem; - font-family: var(--font-body); - font-size: 1rem; - color: var(--color-ink); + font-family: var(--ks-font); + font-size: 1.05rem; + font-weight: 600; + color: var(--ks-champagne); } .neon-case-notes p { margin: 0; - font-family: var(--font-body); - font-size: 0.9375rem; - line-height: 1.65; - color: var(--color-charcoal); + font-family: var(--ks-font); + font-size: 0.95rem; + line-height: 1.7; + color: var(--ks-text); } .neon-case-details { @@ -3296,8 +3312,8 @@ .neon-case-details figcaption span { display: block; margin-bottom: 0.25rem; - font-weight: 700; - color: var(--color-ink); + font-weight: 600; + color: var(--ks-champagne); } .neon-case-command { @@ -3306,19 +3322,18 @@ gap: clamp(2rem, 5vw, 4rem); align-items: end; padding: clamp(2rem, 5vw, 4rem); - color: var(--color-paper); - background: var(--color-ink); + background: var(--ks-lacquer-deep); + border: 1px solid var(--ks-rule-strong); + border-radius: 2px; } -.neon-case-command h2, -.neon-case-command p, -.neon-case-command .neon-case-section-label { - color: inherit; +.neon-case-command h2 { + color: var(--ks-champagne); } .neon-case-command p { margin-top: 1.1rem; - color: oklch(90% 0.005 350); + color: var(--ks-text-muted); } .neon-case-command code { @@ -3329,6 +3344,33 @@ margin: 0; } +.neon-case-command .code-block { + margin: 0; + padding: var(--spacing-md); + background: var(--ks-lacquer); + color: var(--ks-champagne); + border: 1px solid var(--ks-rule); + border-radius: 2px; + overflow-x: auto; + font-family: var(--ks-mono); + font-size: 0.8125rem; + line-height: 1.55; +} + +.neon-case-command .code-block code { + background: transparent; + border: 0; + padding: 0; + color: inherit; + font-size: inherit; +} + +.neon-case-command .code-block-copy.is-copied { + background: var(--ks-kinpaku); + color: var(--ks-lacquer-deep); + border-color: var(--ks-kinpaku); +} + @media (max-width: 980px) { .designing-visualize-spread, .neon-case-hero, @@ -3350,10 +3392,6 @@ } @media (max-width: 640px) { - .neon-case { - width: min(100% - 24px, 1360px); - } - .neon-case-actions { flex-direction: column; } diff --git a/site/styles/footer.css b/site/styles/footer.css index 81afd16cf..6de849d13 100644 --- a/site/styles/footer.css +++ b/site/styles/footer.css @@ -92,4 +92,4 @@ } } -/* ============================================ +/* ============================================ */ diff --git a/site/styles/home-kinpaku.css b/site/styles/home-kinpaku.css new file mode 100644 index 000000000..ca5768ea7 --- /dev/null +++ b/site/styles/home-kinpaku.css @@ -0,0 +1,3038 @@ +/* All brand-bearing tokens (kinpaku, patina, lacquer, graphite, champagne, + text, rules, type scale, motion) come from kinpaku-tokens.css. The block + below only keeps the home page's two intentional divergences and the + legacy --color-* aliasing layer that older shared CSS still reads from. */ + +@import "./kinpaku-tokens.css"; + +.home-kinpaku { + /* Intentional overrides: the homepage uses brighter rule alphas because + bento tile borders need to read against busy backgrounds. The default + --ks-rule from kinpaku-tokens.css is the quieter doctrinal value used + on /design-system and elsewhere. */ + --ks-rule: oklch(58% 0.065 82 / 0.48); + --ks-rule-strong: oklch(74% 0.09 82 / 0.68); + + /* Slightly lighter muted text on the homepage's busier surfaces. The + default --ks-text-muted (63% L) is used everywhere else; here we lift + to 65% so captions stay readable against bento tile fills. Kept under + the old --ks-muted name for code-churn reasons. */ + --ks-muted: oklch(65% 0.024 82); + + /* Legacy alias — homepage code still reads --ks-faint and --ks-oxide. The + forward names live in kinpaku-tokens.css as --ks-text-faint and + --ks-patina-deep respectively. */ + --ks-faint: var(--ks-text-faint); + --ks-oxide: var(--ks-patina-deep); + + /* Legacy --color-* aliasing layer. Older shared CSS (main.css, + sub-pages.css) still reads from these names; the homepage maps them + onto the kinpaku tokens so the cascade stays consistent. */ + --font-display: var(--ks-font); + --font-body: var(--ks-font); + --font-mono: var(--ks-mono); + --color-ink: var(--ks-champagne); + --color-text: var(--ks-text); + --color-paper: var(--ks-lacquer); + --color-cream: var(--ks-lacquer-raised); + --color-charcoal: var(--ks-text); + --color-ash: var(--ks-muted); + --color-mist: var(--ks-rule); + --color-bg: var(--ks-lacquer-raised); + --color-accent: var(--ks-kinpaku); + --color-accent-hover: var(--ks-patina); + --color-accent-dim: oklch(78% 0.12 82 / 0.16); + --color-accent-soft: oklch(78% 0.12 82 / 0.26); + + --cat-create-bg: linear-gradient(155deg, oklch(78% 0.12 82 / 0.13), transparent 56%), oklch(10% 0.009 95); + --cat-create-border: oklch(78% 0.12 82 / 0.7); + --cat-create-text: oklch(86% 0.075 82); + --cat-evaluate-bg: linear-gradient(155deg, oklch(70% 0.105 190 / 0.15), transparent 58%), oklch(9% 0.009 95); + --cat-evaluate-border: oklch(70% 0.105 190 / 0.74); + --cat-evaluate-text: oklch(76% 0.088 190); + --cat-refine-bg: linear-gradient(155deg, oklch(89% 0.055 84 / 0.11), transparent 58%), oklch(10% 0.008 95); + --cat-refine-border: oklch(84% 0.055 84 / 0.58); + --cat-refine-text: oklch(88% 0.047 84); + --cat-simplify-bg: linear-gradient(155deg, oklch(61% 0.085 78 / 0.14), transparent 58%), oklch(9% 0.009 95); + --cat-simplify-border: oklch(70% 0.096 72 / 0.72); + --cat-simplify-text: oklch(82% 0.095 76); + --cat-harden-bg: linear-gradient(155deg, oklch(49% 0.08 188 / 0.16), transparent 58%), oklch(8% 0.01 95); + --cat-harden-border: oklch(58% 0.085 185 / 0.74); + --cat-harden-text: oklch(71% 0.082 184); + --cat-system-bg: linear-gradient(155deg, oklch(84% 0.035 82 / 0.08), transparent 58%), oklch(10% 0.007 95); + --cat-system-border: oklch(58% 0.028 82 / 0.5); + --cat-system-text: oklch(71% 0.028 82); + + color: var(--ks-text); + /* Pure lacquer black. Previously had lacquer-grain.png on top of a 180deg + gradient + two faded gold/patina radial washes; with the washes removed + the grain texture itself read as scratched stone/asphalt, not lacquer. + The brand lives off the contrast of clean black against gold, not on + low-contrast surface texture. */ + background: linear-gradient(180deg, var(--ks-lacquer), var(--ks-lacquer-deep)); + font-family: var(--ks-font); +} + +.home-kinpaku *, +.home-kinpaku *::before, +.home-kinpaku *::after { + box-sizing: border-box; +} + +.home-kinpaku body { + background: var(--ks-lacquer); +} + +.home-kinpaku a { + color: inherit; +} + +.home-kinpaku strong { + color: var(--ks-champagne); +} + +/* Inline code in body text. Three things to balance against the surrounding + prose: (1) mono glyphs are wider than sans, so font-size needs to step + down ~12% to read as equal weight, (2) the brand kinpaku (0.19 chroma) + reads as pumpkin on small text — drop to 0.11 for actual code-gold, + (3) keep the chip treatment subtle so it doesn't drag attention off the + prose. */ +.home-kinpaku code { + color: oklch(82% 0.11 82); + background: oklch(78% 0.12 82 / 0.08); + border: 1px solid oklch(78% 0.12 82 / 0.14); + border-radius: 3px; + font-size: 0.88em; + padding: 0.05em 0.4em; +} + +.home-kinpaku ::selection { + color: var(--ks-lacquer); + background: var(--ks-patina); +} + +.home-kinpaku .grain-overlay { + opacity: 0.055; + mix-blend-mode: soft-light; +} + +.home-kinpaku .skip-link { + background: var(--ks-patina); + color: var(--ks-lacquer-deep); + border: 1px solid var(--ks-patina); + border-radius: 4px; +} + +.home-kinpaku .site-content { + max-width: 1500px; + padding: 0 clamp(22px, 4vw, 56px); +} + +.home-kinpaku #foundation, +.home-kinpaku #language, +.home-kinpaku #why, +.home-kinpaku #live-mode, +.home-kinpaku #downloads, +.home-kinpaku #changelog, +.home-kinpaku #faq, +.home-kinpaku #consulting { + scroll-margin-top: 128px; +} + +/* Site header styling (.site-header, .site-header-brand, .site-header-brand-logo, + .site-header-brand-name, .footer-logo, .site-header-nav) lives in + kinpaku-kit.css under a selector list that covers every kinpaku-themed + page. Update the kit, not this file. */ + +.home-kinpaku .site-header-nav a { + padding: 8px 0; +} + +.home-kinpaku .site-header-nav a:hover, +.home-kinpaku .site-header-nav a[aria-current="page"] { + color: var(--ks-kinpaku); +} + +.home-kinpaku .site-header-nav a[aria-current="page"]::after { + bottom: -7px; + height: 1px; + background: var(--ks-kinpaku); + border-radius: 0; +} + +/* GitHub star pill + star-gold now live in kinpaku-kit.css under + .kinpaku-chrome so every kinpaku page matches. Don't re-declare here. */ + +.home-kinpaku h1, +.home-kinpaku h2, +.home-kinpaku h3, +.home-kinpaku h4, +.home-kinpaku h5, +.home-kinpaku h6 { + color: var(--ks-champagne); + font-family: var(--ks-font); + font-style: normal; + font-weight: 300; + letter-spacing: 0; +} + +.home-kinpaku p { + color: var(--ks-text); +} + +.home-kinpaku .section-header { + display: grid; + gap: 12px; + margin-bottom: 34px; +} + +.home-kinpaku .section-number, +.home-kinpaku .section-eyebrow, +.home-kinpaku .hero-included-title, +.home-kinpaku .spread-category-label, +.home-kinpaku .gallery-stack-title, +.home-kinpaku .install-alts-label, +.home-kinpaku .install-tool-preview-caption, +.home-kinpaku .changelog-highlights-label, +.home-kinpaku .live-demo-support-k { + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-size: 0.7rem; + font-weight: 500; + letter-spacing: 0.18em; + text-transform: uppercase; +} + +.home-kinpaku .section-title { + font-family: var(--ks-font-display); + font-weight: 600; + font-size: clamp(2.6rem, 4vw, 3.4rem); + line-height: 1.04; + color: var(--ks-champagne); + text-wrap: balance; +} + +.home-kinpaku .section-lead, +.home-kinpaku .section-subtitle { + max-width: 68ch; + color: var(--ks-text); + font-size: 1.02rem; + line-height: 1.8; + text-wrap: pretty; +} + +/* Balance line lengths on shorter copy that often ends up with a single + word stranded on the last line. text-wrap: balance is browser-limited to + ≤10 lines, which is exactly where the awkward wraps happen. Longer body + paragraphs use text-wrap: pretty (smarter end-of-paragraph orphan handling + without the cost of full balancing). */ +.home-kinpaku .hero-rebuild-title, +.home-kinpaku .hero-rebuild-meta, +.home-kinpaku .slop-teaser-title, +.home-kinpaku .downloads-rebuild-title, +.home-kinpaku .downloads-rebuild-note, +.home-kinpaku .why-panel-title, +.home-kinpaku .live-demo-support-v, +.home-kinpaku .downloads-rebuild-examples span, +.home-kinpaku .downloads-rebuild-extras strong, +.home-kinpaku .downloads-rebuild-extras span { + text-wrap: balance; +} + +.home-kinpaku .hero-rebuild-body, +.home-kinpaku .slop-teaser-body, +.home-kinpaku .why-panel-body { + text-wrap: pretty; +} + +.home-kinpaku .cheatsheet-link, +.home-kinpaku .why-panel-meta a, +.home-kinpaku .install-howto-link, +.home-kinpaku .gallery-stack-link, +.home-kinpaku .install-cmd-note a, +.home-kinpaku .faq-answer a, +.home-kinpaku .consulting-desc a { + color: var(--ks-kinpaku); + text-decoration: none; + border-bottom: 1px solid oklch(78% 0.12 82 / 0.42); +} + +.home-kinpaku .cheatsheet-link:hover, +.home-kinpaku .why-panel-meta a:hover, +.home-kinpaku .install-howto-link:hover, +.home-kinpaku .gallery-stack-link:hover, +.home-kinpaku .install-cmd-note a:hover, +.home-kinpaku .faq-answer a:hover, +.home-kinpaku .consulting-desc a:hover { + color: var(--ks-patina); + border-bottom-color: var(--ks-patina); +} + +.home-kinpaku .hero-combined { + min-height: calc(100svh - 101px); + display: grid; + grid-template-rows: minmax(0, 1fr) auto; + padding: clamp(44px, 6vh, 84px) 0 0; + overflow: clip; + background: + linear-gradient(90deg, oklch(5% 0.004 95 / 0.96) 0%, oklch(5% 0.004 95 / 0.86) 48%, oklch(5% 0.004 95 / 0.72) 100%), + radial-gradient(circle at 76% 34%, oklch(78% 0.12 82 / 0.055), transparent 30rem), + url("/assets/neo-kinpaku/lacquer-grain.png") center / 1700px auto, + var(--ks-lacquer); + isolation: isolate; +} + +.home-kinpaku .hero-combined::before { + content: ""; + position: absolute; + inset: -10% -4% 92px 44%; + z-index: -2; + background: + linear-gradient(90deg, oklch(5% 0.004 95 / 0.98) 0%, transparent 46%, transparent 100%), + linear-gradient(180deg, transparent 0%, oklch(5% 0.004 95 / 0.52) 74%, oklch(5% 0.004 95 / 0.94) 100%), + url("/assets/neo-kinpaku/hero-seam-field-v2.png") center right / cover no-repeat; + filter: contrast(1.1) saturate(1.16) brightness(0.98); + opacity: 0.72; +} + +.home-kinpaku .hero-combined::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 91px; + height: 20px; + background: url("/assets/neo-kinpaku/gold-dust-rule.png") center / cover no-repeat; + opacity: 0.56; + mask-image: linear-gradient(90deg, transparent, black 10%, black 90%, transparent); + pointer-events: none; +} + +.home-hero-art { + position: absolute; + inset: 0; + z-index: -1; + pointer-events: none; + overflow: hidden; +} + +.home-circuit-field { + position: absolute; + right: -14%; + top: 2%; + width: min(760px, 48vw); + height: min(560px, 64vh); + opacity: 0.18; + mix-blend-mode: screen; +} + +.home-circuit-lines path { + fill: none; + stroke: var(--ks-kinpaku-deep); + stroke-width: 1.1; + stroke-linecap: round; + stroke-linejoin: round; + vector-effect: non-scaling-stroke; +} + +.home-circuit-nodes circle { + fill: oklch(5% 0.004 95 / 0.78); + stroke: var(--ks-kinpaku); + stroke-width: 1.2; + vector-effect: non-scaling-stroke; +} + +.home-calibration { + position: absolute; + border: 1px solid var(--ks-rule); + opacity: 0.56; +} + +.home-calibration::before, +.home-calibration::after { + content: ""; + position: absolute; + background: var(--ks-kinpaku-deep); +} + +.home-calibration-one { + right: -2px; + top: 19%; + width: 340px; + height: 240px; + border-left: 0; +} + +.home-calibration-one::before { + top: 54%; + left: -120px; + width: 220px; + height: 1px; +} + +.home-calibration-one::after { + top: -24px; + left: 42%; + width: 1px; + height: 286px; +} + +.home-patina-dot { + position: absolute; + width: 7px; + height: 7px; + border-radius: 999px; + background: var(--ks-patina); + box-shadow: 0 0 22px var(--ks-patina); + opacity: 0.85; +} + +.home-patina-dot-one { + right: 6.5%; + top: 47%; +} + +.home-kinpaku .hero-combined-container { + position: relative; + z-index: 1; + align-self: center; + max-width: 1500px; + grid-template-columns: minmax(0, 0.95fr) minmax(430px, 0.82fr); + gap: clamp(36px, 5vw, 78px); + align-items: center; + padding: 0 clamp(22px, 4vw, 56px) clamp(34px, 5vh, 70px); +} + +.home-kinpaku .hero-combined-left { + gap: 22px; +} + +.home-kinpaku .hero-title-combined { + max-width: 760px; + color: var(--ks-champagne); + font-size: clamp(3.4rem, 5.5vw, 5.2rem); + font-weight: 300; + line-height: 1.02; + letter-spacing: 0; + text-transform: none; +} + +.home-kinpaku .hero-tagline-combined { + color: var(--ks-kinpaku); + font-size: 1.12rem; + font-style: normal; + font-weight: 300; + letter-spacing: 0.08em; + line-height: 1.65; +} + +.home-kinpaku .hero-tagline-combined::before { + content: ""; + display: block; + width: 172px; + height: 1px; + margin: 0 0 24px; + background: linear-gradient(90deg, var(--ks-kinpaku), var(--ks-kinpaku-deep)); + box-shadow: 0 0 14px oklch(78% 0.12 82 / 0.3); +} + +.home-kinpaku .hero-hook-text { + max-width: 54ch; + color: var(--ks-text); + font-size: 1.02rem; + line-height: 1.78; +} + +.home-kinpaku .hero-included-box { + max-width: 560px; + padding: 16px 18px; + border: 1px solid var(--ks-rule); + background: + linear-gradient(90deg, oklch(78% 0.12 82 / 0.055), transparent), + oklch(8% 0.005 95 / 0.82); +} + +.home-kinpaku .hero-included-items { + color: var(--ks-text); + font-size: 0.88rem; +} + +.home-kinpaku .hero-included-items em { + color: var(--ks-kinpaku-pale); + font-family: var(--ks-mono); +} + +.home-kinpaku .hero-included-sep { + color: var(--ks-kinpaku-deep); +} + +.home-kinpaku .hero-cta-group { + gap: 28px; + flex-wrap: wrap; +} + +.home-kinpaku .hero-cta-combined, +.home-kinpaku .btn-primary, +.home-kinpaku .install-tool-cta { + position: relative; + isolation: isolate; + overflow: hidden; + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 58px; + padding: 0 38px; + border: 1px solid transparent; + border-radius: 2px; + color: var(--ks-lacquer-deep); + background: + linear-gradient(90deg, oklch(95% 0.04 84 / 0.36), oklch(78% 0.12 82 / 0.72)), + url("/assets/neo-kinpaku/kinpaku-gold-leaf.png") center / 160% auto; + background-size: auto, 220% auto; + animation: home-kinpaku-button-pan 24s ease-in-out infinite alternate; + box-shadow: + inset 0 0 18px oklch(100% 0 0 / 0.12), + 0 18px 48px oklch(2% 0.004 95 / 0.4); + font-family: var(--ks-font); + font-size: 1rem; + font-weight: 500; + letter-spacing: 0; + text-transform: none; +} + +.home-kinpaku .hero-cta-combined::after, +.home-kinpaku .btn-primary::after, +.home-kinpaku .install-tool-cta::after { + content: ""; + position: absolute; + inset: -12px -32%; + z-index: -1; + background: + linear-gradient( + 105deg, + transparent 0%, + transparent 28%, + oklch(98% 0.04 84 / 0.56) 48%, + oklch(94% 0.07 82 / 0.32) 53%, + transparent 72%, + transparent 100% + ); + mix-blend-mode: screen; + filter: blur(1.2px); + animation: home-kinpaku-sheen 6.8s linear infinite; + pointer-events: none; +} + +.home-kinpaku .hero-cta-combined:hover, +.home-kinpaku .btn-primary:hover, +.home-kinpaku .install-tool-cta:hover { + color: var(--ks-lacquer-deep); + background: + linear-gradient(90deg, oklch(98% 0.035 84 / 0.38), oklch(78% 0.12 82 / 0.58)), + url("/assets/neo-kinpaku/kinpaku-gold-leaf.png") 60% 46% / 140% auto; + transform: translateY(-2px); +} + +.home-kinpaku .hero-logos-inline { + gap: 8px; +} + +@keyframes home-kinpaku-button-pan { + 0% { background-position: center, 0% 50%; } + 100% { background-position: center, 100% 60%; } +} + +@keyframes home-kinpaku-sheen { + 0% { transform: translateX(-58%); } + 100% { transform: translateX(58%); } +} + +.home-kinpaku .hero-logos-label { + color: var(--ks-muted); + font-size: 0.72rem; +} + +.home-kinpaku .hero-logo-icon { + display: inline-grid; + place-items: center; + width: 24px; + height: 24px; + border: 0; + border-radius: 0; + background: transparent; + opacity: 0.68; + transition: + opacity 180ms cubic-bezier(0.16, 1, 0.3, 1), + transform 180ms cubic-bezier(0.16, 1, 0.3, 1); +} + +.home-kinpaku .hero-logos-row img { + opacity: 0.74; + filter: grayscale(1) saturate(0) invert(1) brightness(1.18) contrast(0.72); + transition: + opacity 180ms cubic-bezier(0.16, 1, 0.3, 1), + filter 180ms cubic-bezier(0.16, 1, 0.3, 1); +} + +.home-kinpaku .hero-logo-icon:hover, +.home-kinpaku .hero-logo-icon:focus-visible { + opacity: 1; + transform: translateY(-1px); +} + +.home-kinpaku .hero-logo-icon:hover img, +.home-kinpaku .hero-logo-icon:focus-visible img { + opacity: 1; + filter: none; +} + +.home-kinpaku .hero-version-link { + margin: 4px 0 0; + color: var(--ks-muted); + font-size: 0.85rem; +} + +.home-kinpaku .hero-version-link a { + color: inherit; + text-decoration: none; +} + +.home-kinpaku .hero-version-link a:hover { + color: var(--ks-patina); +} + +.home-hero-rail { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 0; + position: relative; + z-index: 1; + width: 100%; + min-height: 92px; + margin-top: 0; + border-top: 1px solid oklch(78% 0.12 82 / 0.2); + background: + linear-gradient(180deg, oklch(5% 0.004 95 / 0.76), oklch(4% 0.004 95 / 0.94)), + url("/assets/neo-kinpaku/lacquer-grain.png") center bottom / 1500px auto; +} + +.home-hero-rail a { + min-height: 92px; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 16px; + padding: 0 clamp(16px, 3.2vw, 46px); + color: var(--ks-champagne); + text-decoration: none; + border-right: 1px solid oklch(78% 0.12 82 / 0.18); + font-size: 0.94rem; + transition: + color 180ms cubic-bezier(0.16, 1, 0.3, 1), + background 180ms cubic-bezier(0.16, 1, 0.3, 1); +} + +.home-hero-rail a:last-child { + border-right: 0; +} + +.home-hero-rail a:hover { + color: var(--ks-patina); + background: oklch(70% 0.105 190 / 0.045); +} + +.home-hero-rail strong, +.home-hero-rail small { + display: block; +} + +.home-hero-rail strong { + color: inherit; + font-size: 1rem; + font-weight: 400; +} + +.home-hero-rail small { + margin-top: 4px; + color: var(--ks-muted); + font-size: 0.66rem; + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.home-rail-index { + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-size: 0.72rem; + letter-spacing: 0.08em; +} + +.home-kinpaku .hero-combined-right { + position: relative; + isolation: isolate; + justify-content: flex-end; +} + +.home-kinpaku .hero-combined-right::before { + content: ""; + position: absolute; + inset: -88px -126px -124px -116px; + z-index: -1; + background: + radial-gradient(ellipse at center, oklch(5% 0.004 95 / 0.98) 0%, oklch(5% 0.004 95 / 0.94) 52%, oklch(5% 0.004 95 / 0.54) 68%, transparent 84%); + filter: blur(8px); + pointer-events: none; +} + +.home-kinpaku .hero-combined-right .split-comparison { + max-width: 468px; + width: 100%; + padding: 0; + margin: 0; +} + +.home-kinpaku .split-container { + height: 352px; + max-width: 448px; + border-radius: 4px; + border: 1px solid var(--ks-rule); + background: + linear-gradient(90deg, oklch(78% 0.12 82 / 0.04) 1px, transparent 1px), + linear-gradient(180deg, oklch(78% 0.12 82 / 0.035) 1px, transparent 1px), + var(--ks-lacquer-raised); + background-size: 24px 24px; + box-shadow: + 0 28px 80px oklch(2% 0.004 95 / 0.44), + inset 0 0 0 1px oklch(78% 0.12 82 / 0.05); +} + +.home-kinpaku .split-before { + background: + linear-gradient(90deg, oklch(10% 0.006 95 / 0.92), oklch(12% 0.012 330 / 0.72)), + radial-gradient(circle at 28% 25%, oklch(60% 0.25 350 / 0.22), transparent 11rem); + justify-content: center; + padding-left: 0; +} + +.home-kinpaku .split-after { + background: + linear-gradient(90deg, oklch(8% 0.006 95 / 0.92), oklch(10% 0.006 95 / 0.96)), + radial-gradient(circle at 82% 26%, oklch(78% 0.12 82 / 0.12), transparent 12rem); + justify-content: center; + padding-right: 0; +} + +.home-kinpaku .split-divider { + top: -9%; + bottom: auto; + width: 112px; + height: 118%; + background: url("/assets/neo-kinpaku/before-after-seam.png") center / auto 100% no-repeat; + filter: drop-shadow(0 0 11px oklch(78% 0.12 82 / 0.28)); + box-shadow: none; + transform: translateX(-50%); +} + +.home-kinpaku .split-divider::before { + content: none; +} + +.home-kinpaku .split-badge { + position: absolute; + top: 16px; + z-index: 4; + padding: 7px 10px; + border: 1px solid var(--ks-rule); + color: var(--ks-kinpaku); + background: oklch(6% 0.004 95 / 0.84); + font-family: var(--ks-mono); + font-size: 0.64rem; + letter-spacing: 0.16em; + text-transform: uppercase; +} + +.home-kinpaku .split-badge--before { + left: 16px; +} + +.home-kinpaku .split-badge--after { + right: 16px; + color: var(--ks-patina); + border-color: oklch(70% 0.105 190 / 0.42); +} + +.home-kinpaku .slop-card, +.home-kinpaku .impeccable-card { + width: 244px; + min-height: 252px; + border-radius: 6px; +} + +.home-kinpaku .slop-card { + background: + linear-gradient(135deg, oklch(96% 0.04 303), oklch(86% 0.12 300) 52%, oklch(78% 0.16 283)); + color: oklch(19% 0.02 285); + box-shadow: 0 20px 50px oklch(2% 0.004 95 / 0.4); +} + +.home-kinpaku .slop-callout { + color: var(--ks-vermilion); + background: oklch(8% 0.006 95 / 0.9); + border-color: var(--ks-vermilion); +} + +.home-kinpaku .impeccable-card { + border: 1px solid var(--ks-rule-strong); + background: + linear-gradient(135deg, oklch(78% 0.12 82 / 0.11), transparent 42%), + linear-gradient(180deg, oklch(14% 0.008 95 / 0.98), oklch(9% 0.006 95 / 0.98)); + box-shadow: + inset 0 0 0 1px oklch(78% 0.12 82 / 0.08), + 0 18px 42px oklch(2% 0.004 95 / 0.32); +} + +.home-kinpaku .impeccable-eyebrow { + color: var(--ks-patina); + font-family: var(--ks-mono); + letter-spacing: 0.18em; +} + +.home-kinpaku .impeccable-title { + color: var(--ks-kinpaku-pale); + font-family: var(--ks-font); + font-size: 1.7rem; + font-style: normal; + font-weight: 400; +} + +.home-kinpaku .impeccable-body { + color: var(--ks-text); +} + +.home-kinpaku .impeccable-button { + background: var(--ks-kinpaku); + color: var(--ks-lacquer-deep); +} + +.home-kinpaku .split-labels { + justify-content: space-between; + gap: 18px; + max-width: 448px; + margin: 16px auto 0; + padding: 0 2px; +} + +.home-kinpaku .split-label-item { + color: var(--ks-champagne); + font-family: var(--ks-mono); + font-size: 0.78rem; + font-weight: 600; + letter-spacing: 0.12em; + text-transform: uppercase; + text-shadow: + 0 1px 1px var(--ks-lacquer-deep), + 0 0 16px oklch(5% 0.004 95 / 0.9); +} + +.home-kinpaku .split-label-dot { + background: var(--ks-faint); +} + +.home-kinpaku .split-label-dot--accent { + background: var(--ks-patina); +} + +.home-kinpaku .section-nav { + background: oklch(7% 0.006 95 / 0.86); + border: 1px solid var(--ks-rule); + border-radius: 999px; + box-shadow: 0 16px 50px oklch(2% 0.004 95 / 0.52); +} + +.home-kinpaku .section-nav-item:hover { + background: oklch(78% 0.12 82 / 0.09); +} + +.home-kinpaku .section-nav-item.is-active { + background: var(--ks-kinpaku); +} + +.home-kinpaku .section-nav-num, +.home-kinpaku .section-nav-label { + color: var(--ks-muted); +} + +.home-kinpaku .section-nav-item.is-active .section-nav-num, +.home-kinpaku .section-nav-item.is-active .section-nav-label { + color: var(--ks-lacquer-deep); +} + +/* Sections sit on clean lacquer. The earlier faded radial gold wash + (0.075 alpha) was sub-perceptual yet contributed visual noise; the muddy + gold-dust streak at the top (opacity 0.48 with mask) read as a dirty + horizontal smear, not a brand accent. Removed. Where we want a section + divider, use a full-vibrancy treatment (see kinpaku accent strip below). */ +.home-kinpaku .foundation-section, +.home-kinpaku .language-section, +.home-kinpaku .why-section, +.home-kinpaku .live-section, +.home-kinpaku .platforms-section, +.home-kinpaku .consulting-section { + position: relative; + isolation: isolate; + padding: 112px 0; +} + + +/* Ready-to-drop-in section — kill the global .why-section hairline (set by + workflow.css for the generic light treatment). No radial spotlight: the + soft glowing wash that style is associated with generic SaaS heroes, and + it reads as off-brand against kinpaku's material/editorial aesthetic. */ +.home-kinpaku .why-section { + border-top: 0; +} + +.home-kinpaku :is(.foundation-section, .language-section, .why-section, .live-section, .platforms-section, .consulting-section) > * { + position: relative; + z-index: 1; +} + +.home-kinpaku .foundation-grid { + gap: 14px; + margin-top: -34px; +} + +.home-kinpaku .foundation-card { + min-height: 284px; + padding: 24px; + border: 1px solid var(--ks-rule); + border-radius: 4px; + background: oklch(9% 0.006 95 / 0.92); +} + +.home-kinpaku .foundation-column:hover .foundation-card { + border-color: var(--ks-patina); + box-shadow: + inset 0 0 0 1px oklch(70% 0.105 190 / 0.08), + 0 24px 58px oklch(2% 0.004 95 / 0.34); +} + +.home-kinpaku .foundation-card-viz { + color: var(--ks-kinpaku); +} + +.home-kinpaku .foundation-card-label { + color: var(--ks-champagne); + font-family: var(--ks-font); + font-size: 1.18rem; + font-weight: 400; +} + +.home-kinpaku .foundation-card-count { + color: var(--ks-patina); + font-family: var(--ks-mono); +} + +.home-kinpaku .foundation-card-detail, +.home-kinpaku .foundation-cta-text { + color: var(--ks-muted); +} + +.home-kinpaku .foundation-plinth { + border-color: var(--ks-rule); + background: + repeating-linear-gradient(45deg, oklch(78% 0.12 82 / 0.12), oklch(78% 0.12 82 / 0.12) 1px, transparent 1px, transparent 9px), + oklch(10% 0.006 95 / 0.42); + /* Soften the bottom so the plinth fades into the section bg like a real + shadow instead of terminating in a hard horizontal line. Mask fades the + bottom 45% of the element, taking the diagonal stripes + side borders + down with it. */ + -webkit-mask-image: linear-gradient(to bottom, black 0%, black 55%, transparent 100%); + mask-image: linear-gradient(to bottom, black 0%, black 55%, transparent 100%); +} + +.home-kinpaku .language-intro-row { + align-items: end; +} + +.home-kinpaku .language-view-toggle { + border: 1px solid var(--ks-rule); + border-radius: 999px; + background: oklch(8% 0.006 95 / 0.78); +} + +.home-kinpaku .language-view-tab { + color: var(--ks-muted); + border-radius: 999px; +} + +.home-kinpaku .language-view-tab.is-active { + background: var(--ks-kinpaku); + color: var(--ks-lacquer-deep); +} + +.home-kinpaku .magazine-spread[data-category="create"] { --spread-accent: var(--ks-kinpaku); } +.home-kinpaku .magazine-spread[data-category="evaluate"] { --spread-accent: var(--ks-patina); } +.home-kinpaku .magazine-spread[data-category="refine"] { --spread-accent: var(--ks-kinpaku-pale); } +.home-kinpaku .magazine-spread[data-category="simplify"] { --spread-accent: var(--ks-kinpaku-deep); } +.home-kinpaku .magazine-spread[data-category="harden"] { --spread-accent: var(--ks-oxide); } +.home-kinpaku .magazine-spread[data-category="system"] { --spread-accent: var(--ks-muted); } + +/* Card-swap to G pattern: the OUTER spread carries the single tinted-plinth + surface (flat oklch(0.17 0 0), no border, no shadow). All inner demo + surfaces sit transparently on top — no plinth-in-plinth, no nested rings. */ +.home-kinpaku .magazine-container { + border: 0; + border-radius: 2px; + background: oklch(0.17 0 0); + box-shadow: none; +} + +.home-kinpaku .fisheye-item { + color: var(--ks-muted); + font-family: var(--ks-font); + font-size: 1.18rem; +} + +.home-kinpaku .fisheye-item.is-active { + color: var(--ks-kinpaku); +} + +.home-kinpaku .fisheye-slash, +.home-kinpaku .fisheye-item.is-active .fisheye-slash { + color: var(--ks-patina); +} + +.home-kinpaku .spread-command-name { + color: var(--ks-kinpaku); + font-family: var(--ks-font-display); + font-size: 4.2rem; + font-weight: 300; + letter-spacing: 0; +} + +.home-kinpaku .spread-command-name .spread-namespace, +.home-kinpaku .spread-description, +.home-kinpaku .spread-flow-label { + color: var(--ks-muted); +} + +.home-kinpaku .spread-flow-cmd { + color: var(--ks-kinpaku); + background: oklch(78% 0.12 82 / 0.08); + border: 1px solid oklch(78% 0.12 82 / 0.18); +} + +/* Inner demo surfaces are transparent — the plinth lives on the outer + .magazine-container; these sit on top with no chrome of their own. */ +.home-kinpaku .spread-demo-area .demo-split-comparison .split-container, +.home-kinpaku .terminal-preview .demo-split-comparison .split-container, +.home-kinpaku .demo-container, +.home-kinpaku .demo-viewport, +.home-kinpaku .solution-visual-interactive { + border: 0; + border-radius: 0; + background: transparent; +} + +.home-kinpaku .spread-demo-area > .demo-container { + border: 0; + border-radius: 0; + background: transparent; +} + +.home-kinpaku .spread-demo-area .demo-container .demo-viewport { + width: 100%; + max-width: none; + min-height: 100%; + background: transparent; +} + +.home-kinpaku .demo-placeholder { + padding: clamp(24px, 4vw, 48px); + color: var(--ks-text); + font-family: var(--ks-font); + font-size: clamp(1rem, 1.5vw, 1.3rem); + font-style: normal; + font-weight: 300; + letter-spacing: 0.02em; + text-align: center; +} + +/* Periodic table container — G plinth. Previously had six layered backgrounds + (grid lines, two radial washes, lacquer-grain, dark fill, plus the dust- + accent pseudo and the oxidation-edge accent), which read as a stone-tile + background instead of a clean canvas. Stripped to the same tonal-lift + surface we use on the magazine spread: flat oklch(0.17 0 0), no border, + no box-shadow, no inner imagery. */ +.home-kinpaku .language-view--periodic .solution-visual-interactive { + min-height: clamp(520px, 44vw, 680px); + position: relative; + isolation: isolate; + overflow: visible; + border: 0; + background: oklch(0.17 0 0); + box-shadow: none; +} + +.home-kinpaku .language-view--periodic .solution-visual-interactive::before, +.home-kinpaku .language-view--periodic .solution-visual-interactive::after { + content: none; +} + +.home-kinpaku #framework-viz-container .ptable-grid { + position: relative; + z-index: 1; + grid-template-columns: repeat(5, minmax(128px, 1fr)) !important; + gap: clamp(22px, 4vw, 58px) !important; + align-items: start; + align-content: start; +} + +.home-kinpaku #framework-viz-container .ptable-group { + gap: 12px !important; +} + +.home-kinpaku #framework-viz-container .ptable-group--system { + grid-column: 1 / span 2; + margin-top: clamp(42px, 5vw, 78px); +} + +.home-kinpaku #framework-viz-container .ptable-group-label { + padding-left: 0 !important; + font-family: var(--ks-mono) !important; + font-size: 0.66rem !important; + font-weight: 500 !important; + letter-spacing: 0.2em !important; + opacity: 0.95; +} + +.home-kinpaku #framework-viz-container .ptable-row { + gap: 10px !important; +} + +.home-kinpaku #framework-viz-container .ptable-element { + --ptable-accent: var(--ks-kinpaku); + width: clamp(64px, 5vw, 76px) !important; + height: clamp(76px, 5.8vw, 90px) !important; + border-width: 1px !important; + border-radius: 4px !important; + overflow: hidden; + isolation: isolate; + box-shadow: + inset 0 0 0 1px oklch(100% 0 0 / 0.035), + inset 0 -18px 34px oklch(2% 0.004 95 / 0.28), + 0 18px 42px oklch(2% 0.004 95 / 0.22) !important; +} + +.home-kinpaku #framework-viz-container .ptable-element--create { + --ptable-accent: var(--cat-create-text); +} + +.home-kinpaku #framework-viz-container .ptable-element--evaluate { + --ptable-accent: var(--cat-evaluate-text); +} + +.home-kinpaku #framework-viz-container .ptable-element--refine { + --ptable-accent: var(--cat-refine-text); +} + +.home-kinpaku #framework-viz-container .ptable-element--simplify { + --ptable-accent: var(--cat-simplify-text); +} + +.home-kinpaku #framework-viz-container .ptable-element--harden { + --ptable-accent: var(--cat-harden-text); +} + +.home-kinpaku #framework-viz-container .ptable-element--system { + --ptable-accent: var(--cat-system-text); +} + +.home-kinpaku #framework-viz-container .ptable-element::before { + content: ""; + position: absolute; + inset: 0; + z-index: -1; + background: + radial-gradient(circle at 74% 12%, oklch(100% 0 0 / 0.09), transparent 32%), + url("/assets/neo-kinpaku/kinpaku-gold-leaf.png") center / 190px auto; + mix-blend-mode: screen; + opacity: 0.055; + pointer-events: none; +} + +.home-kinpaku #framework-viz-container .ptable-element::after { + content: ""; + position: absolute; + left: 12px; + right: 12px; + bottom: 9px; + z-index: 0; + height: 1px; + background: linear-gradient(90deg, transparent, var(--ptable-accent), transparent); + opacity: 0.24; + pointer-events: none; +} + +.home-kinpaku #framework-viz-container .ptable-element:is(:hover, :focus-visible) { + transform: translateY(-3px) !important; + border-color: var(--ptable-accent) !important; + box-shadow: + inset 0 0 0 1px oklch(100% 0 0 / 0.05), + 0 0 0 1px oklch(78% 0.12 82 / 0.12), + 0 22px 48px oklch(2% 0.004 95 / 0.36), + 0 0 26px oklch(78% 0.12 82 / 0.16) !important; +} + +.home-kinpaku #framework-viz-container .ptable-number, +.home-kinpaku #framework-viz-container .ptable-symbol, +.home-kinpaku #framework-viz-container .ptable-name, +.home-kinpaku #framework-viz-container .ptable-alpha { + z-index: 1; +} + +.home-kinpaku #framework-viz-container .ptable-number { + top: 6px !important; + left: 7px !important; + font-size: 0.48rem !important; + opacity: 0.62 !important; +} + +.home-kinpaku #framework-viz-container .ptable-symbol { + font-size: clamp(1.45rem, 1.8vw, 1.8rem) !important; + font-weight: 300 !important; + letter-spacing: 0.01em; +} + +.home-kinpaku #framework-viz-container .ptable-name { + max-width: 64px !important; + color: var(--ptable-accent) !important; + font-size: 0.54rem !important; + letter-spacing: 0.04em; + opacity: 0.72 !important; +} + +.home-kinpaku #framework-viz-container .ptable-alpha { + top: 5px !important; + right: 6px !important; + font-size: 0.48rem !important; + opacity: 0.54 !important; +} + +.home-kinpaku .spread-demo-area .demo-split-comparison .split-before, +.home-kinpaku .terminal-preview .demo-split-comparison .split-before { + background: oklch(10% 0.006 95); +} + +.home-kinpaku .spread-demo-area .demo-split-comparison .split-after, +.home-kinpaku .terminal-preview .demo-split-comparison .split-after { + background: oklch(8% 0.006 95); +} + +.home-kinpaku .demo-caption, +.home-kinpaku .demo-toggle-label, +.home-kinpaku .terminal-line, +.home-kinpaku .terminal-output { + color: var(--ks-muted); +} + +.home-kinpaku .demo-toggle-switch { + background: oklch(10% 0.006 95); + border-color: var(--ks-rule); +} + +.home-kinpaku .demo-toggle-switch.active { + background: var(--ks-kinpaku); +} + +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison { + --demo-panel: oklch(13% 0.008 95); + --demo-panel-raised: oklch(17% 0.01 95); + --demo-panel-soft: oklch(20% 0.012 95); + --demo-border: oklch(58% 0.052 82 / 0.34); + --demo-text: oklch(82% 0.026 82); + --demo-muted: oklch(61% 0.019 82); + --demo-dim: oklch(48% 0.016 82); + --demo-warning-panel: oklch(25% 0.046 72); + --demo-danger-panel: oklch(22% 0.045 35); + --demo-info-panel: oklch(22% 0.055 205); + --demo-purple: oklch(55% 0.16 286); + --demo-purple-soft: oklch(24% 0.06 286); +} + +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison .split-container { + background: + linear-gradient(90deg, oklch(78% 0.12 82 / 0.028) 1px, transparent 1px), + linear-gradient(180deg, oklch(78% 0.12 82 / 0.022) 1px, transparent 1px), + radial-gradient(circle at 74% 48%, oklch(70% 0.105 190 / 0.055), transparent 18rem), + oklch(7% 0.005 95); + background-size: 36px 36px, 36px 36px, auto, auto; +} + +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison .split-before, +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison .split-after { + color: var(--demo-text); +} + +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison .split-after { + clip-path: polygon(50% 0%, 100% 0%, 100% 100%, 50% 100%); +} + +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison .split-divider { + transform: translateX(-50%) !important; +} + +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="background: #fafafa"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="background: #f8f8f8"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="background: #f5f5f5"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="background: #f0f0f0"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="background: white"] { + color: var(--demo-text) !important; + background: + linear-gradient(135deg, oklch(78% 0.12 82 / 0.055), transparent 48%), + var(--demo-panel) !important; + border-color: var(--demo-border) !important; + box-shadow: + inset 0 0 0 1px oklch(78% 0.12 82 / 0.035), + 0 18px 44px oklch(2% 0.004 95 / 0.24) !important; +} + +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="background: #fef3c7"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="background: #fff8e1"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="background: #fff3e0"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="background: #fff8f8"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="background: #fff0f0"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="background: #fee2e2"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="background: #e3f2fd"] { + color: var(--demo-text) !important; + background: var(--demo-panel-soft) !important; + border-color: var(--demo-border) !important; + box-shadow: inset 0 0 0 1px oklch(78% 0.12 82 / 0.035) !important; +} + +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="background: #fef3c7"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="background: #fff8e1"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="background: #fff3e0"] { + background: var(--demo-warning-panel) !important; +} + +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="background: #fff8f8"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="background: #fff0f0"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="background: #fee2e2"] { + background: var(--demo-danger-panel) !important; +} + +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="background: #e3f2fd"] { + background: var(--demo-info-panel) !important; +} + +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="background: #ddd"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="background: #eee"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="background: #e0e0e0"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="background: #ccc"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="background: #333"] { + color: var(--demo-text) !important; + background: var(--demo-panel-raised) !important; + border-color: var(--demo-border) !important; +} + +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison button[style*="#4F46E5"] { + background: linear-gradient(180deg, var(--demo-purple), var(--demo-purple-soft)) !important; + color: oklch(92% 0.018 286) !important; +} + +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="border: 1px solid #ddd"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="border: 2px solid #ddd"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="border: 1px solid #e0e0e0"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="border: 1px solid #e5e5e5"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="border: 1px solid #fca5a5"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="border: 1px solid #ffcccc"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="border: 1px solid #ffcc80"] { + border-color: var(--demo-border) !important; +} + +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="color: #333"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="color: #444"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="color: #666"] { + color: var(--demo-text) !important; +} + +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="color: #888"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="color: #999"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="color: #aaa"] { + color: var(--demo-muted) !important; +} + +/* Amber warning-card text colors hardcoded in the clarify demo (and others) + render as brown-on-brown when the underlying #fff8e1 / #fef3c7 panel gets + swapped for the dark --demo-warning-panel. Map them to readable amber tones + that survive the dark plinth. */ +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="color: #92400e"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="color: #854d0e"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="color: #78350f"] { + color: oklch(82% 0.08 75) !important; +} + +/* Primary CTA buttons that use background: var(--color-ink) — on home-kinpaku + that token maps to champagne (cream) which paints the button cream-on-dark. + The demo wants a dark primary button, so override back to a dark ink and a + light label color. */ +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison button[style*="background: var(--color-ink)"], +.home-kinpaku :is(.spread-demo-area, .terminal-preview, .mobile-demo-area) .demo-split-comparison [style*="background: var(--color-ink)"] { + background: oklch(13% 0.008 95) !important; + color: var(--ks-champagne) !important; + border-color: var(--demo-border) !important; +} + +.home-kinpaku .ptable-tooltip { + background: var(--ks-lacquer-raised) !important; + border-color: var(--ks-rule) !important; +} + +.home-kinpaku .why-layout { + grid-template-columns: minmax(250px, 330px) minmax(0, 1fr); + gap: 34px; +} + +.home-kinpaku .why-tabs { + border: 1px solid var(--ks-rule); + background: oklch(8% 0.006 95 / 0.82); +} + +.home-kinpaku .why-tabs li + li { + border-top-color: var(--ks-rule); +} + +.home-kinpaku .why-tab { + color: var(--ks-muted); + background: transparent; +} + +.home-kinpaku .why-tab:hover { + color: var(--ks-champagne); + background: oklch(78% 0.12 82 / 0.06); +} + +/* Active tab: subtle patina-tinted dark wash. Single flat color so the + `transition: background 180ms` on .why-tab can interpolate from + transparent to here — multi-layer gradients can't be transitioned, which + was breaking the activation animation. Teal-tinted bg + warm gold tab-num + gives the kinpaku-meets-verdigris pairing the section needed. */ +.home-kinpaku .why-tab.is-active { + color: var(--ks-champagne); + background: oklch(22% 0.04 188 / 0.42); +} + +.home-kinpaku .why-tab-num { + color: var(--ks-kinpaku); +} + +/* Progress bar runs in verdigris instead of kinpaku — the gold accent + already lives in the tab number, so the running bar reads as a second + tonal voice rather than a duplicate. */ +.home-kinpaku .why-tab-progress { + background: var(--ks-patina); +} + +/* Bento layout — full-bleed, no card chrome. Six tiles on a 12-col grid + with mixed spans (8/4, 4/8, 6/6) for editorial rhythm. Hairlines between + cells come from the grid: gap: 1px reveals the bento background through + as 1px rules, top + bottom borders close the outer frame. */ +/* The .ks-bento base (grid + dark-plinth bg + top/bottom borders) and the + .ks-bento-tile primitives live in kinpaku-kit.css. The homepage layers two + page-specific behaviors on top via the .why-bento companion class: + 1. Full-bleed breakout from .site-content's max-width + padding. + 2. Edge-tile padding alignment to .site-content's clamp gutter so outer + tile content visually aligns with the section title above. */ +.home-kinpaku .why-bento { + margin-top: 36px; + width: 100vw; + position: relative; + left: 50%; + margin-left: -50vw; + margin-right: -50vw; +} + +/* Outer-edge tiles align with .site-content's content edge so tile content + sits at the same x as the section title above. Two-part calc: + 1. clamp(22px, 4vw, 56px) — matches .site-content's inner padding when the + viewport is below max-width. + 2. max(0px, (100vw - 1500px) / 2) — the extra gutter that opens up once + the viewport exceeds .site-content's 1500px max-width, while the bento + itself stays full-bleed via 100vw breakout. + Pattern is odd = left-edge, even = right-edge across all three rows + (8/4, 4/8, 6/6). */ +.home-kinpaku .why-bento .ks-bento-tile:nth-child(odd) { + padding-left: calc(max(0px, (100vw - 1500px) / 2) + clamp(22px, 4vw, 56px)); +} +.home-kinpaku .why-bento .ks-bento-tile:nth-child(even) { + padding-right: calc(max(0px, (100vw - 1500px) / 2) + clamp(22px, 4vw, 56px)); +} + +/* Narrow span-4 tiles host visuals that were originally designed for wider + columns (PRODUCT.md commands aside, DESIGN.md 3-col grid). Scope the + responsive collapse to those tiles regardless of viewport so the inner + content doesn't get crushed. */ +.home-kinpaku .ks-bento-tile--span-4 .why-visual--productmd { + grid-template-columns: 1fr; +} +.home-kinpaku .ks-bento-tile--span-4 .why-dm-grid { + grid-template-columns: 1fr 1fr; +} +.home-kinpaku .ks-bento-tile--span-4 .why-dm-tile--color { + grid-row: 1 / 3; +} + +/* Inner mockups (the actual visual content of each tile — terminal, + PRODUCT.md file, browser, etc.) get a flat raised-dark surface so they + read as distinct UI elements raised above the tile bg. Previous treatment + had a kinpaku diagonal gradient that produced a smudgy glow on top-left + — dropped, the bento structure carries the brand voice now. */ +.home-kinpaku .why-terminal, +.home-kinpaku .why-productmd-file, +.home-kinpaku .why-register, +.home-kinpaku .why-ci-window, +.home-kinpaku .why-browser, +.home-kinpaku .why-dm-grid, +.home-kinpaku .patterns-tabbed, +.home-kinpaku .gallery-stack-container { + border-color: var(--ks-rule); + background: var(--ks-lacquer-raised); + border-radius: 4px; +} + +.home-kinpaku .why-panel-title { + color: var(--ks-champagne); + font-size: 2rem; + font-weight: 300; +} + +.home-kinpaku .why-panel-body, +.home-kinpaku .why-panel-meta, +.home-kinpaku .why-terminal-line, +.home-kinpaku .why-productmd-v, +.home-kinpaku .why-productmd-status, +.home-kinpaku .why-dm-foot { + color: var(--ks-text); +} + +.home-kinpaku .why-productmd-k, +.home-kinpaku .why-terminal-ok, +.home-kinpaku .why-terminal-path, +.home-kinpaku .why-dm-badge, +.home-kinpaku .why-ci-status { + color: var(--ks-patina); +} + +.home-kinpaku .why-visual--ci { + padding: 0; + overflow: hidden; + background: + linear-gradient(90deg, oklch(78% 0.12 82 / 0.018) 1px, transparent 1px), + linear-gradient(180deg, oklch(78% 0.12 82 / 0.015) 1px, transparent 1px), + radial-gradient(circle at 92% 12%, oklch(70% 0.105 190 / 0.055), transparent 18rem), + oklch(5% 0.004 95 / 0.92); + background-size: 34px 34px, 34px 34px, auto, auto; +} + +.home-kinpaku .why-ci-window { + min-height: 270px; + border: 1px solid oklch(78% 0.12 82 / 0.26); + border-radius: 5px; + overflow: hidden; + background: + linear-gradient(135deg, oklch(100% 0 0 / 0.025), transparent 38%), + oklch(4% 0.004 95 / 0.94); + box-shadow: + inset 0 0 0 1px oklch(100% 0 0 / 0.02), + 0 24px 70px oklch(2% 0.004 95 / 0.34); +} + +.home-kinpaku .why-ci-header { + position: relative; + min-height: 54px; + padding: 14px 18px 14px 72px; + border-bottom: 1px solid var(--ks-rule); + background: oklch(13% 0.006 95); +} + +.home-kinpaku .why-ci-header::before { + content: ""; + position: absolute; + left: 22px; + top: 50%; + width: 34px; + height: 9px; + transform: translateY(-50%); + background: + radial-gradient(circle at 4px 50%, var(--ks-kinpaku) 0 3px, transparent 3.5px), + radial-gradient(circle at 17px 50%, var(--ks-patina) 0 3px, transparent 3.5px), + radial-gradient(circle at 30px 50%, var(--ks-kinpaku-deep) 0 3px, transparent 3.5px); + opacity: 0.88; +} + +.home-kinpaku .why-ci-branch { + color: var(--ks-champagne); + font-size: 0.78rem; + letter-spacing: 0.03em; +} + +.home-kinpaku .why-ci-status { + padding: 5px 13px; + border: 1px solid oklch(58% 0.15 35 / 0.3); + color: var(--ks-patina); + background: + linear-gradient(90deg, oklch(58% 0.15 35 / 0.22), oklch(58% 0.15 35 / 0.1)); + box-shadow: inset 0 0 0 1px oklch(100% 0 0 / 0.02); +} + +.home-kinpaku .why-ci-body { + padding: 24px 28px; + color: var(--ks-text); + font-size: 0.88rem; + line-height: 1.92; +} + +.home-kinpaku .why-ci-line { + color: var(--ks-champagne); +} + +.home-kinpaku .why-ci-cmd { + color: var(--ks-kinpaku); + margin-right: 10px; +} + +.home-kinpaku .why-ci-issue { + gap: 12px; + padding-left: 6px; + color: var(--ks-text); +} + +.home-kinpaku .why-ci-issue code { + display: inline-flex; + align-items: center; + min-height: 26px; + padding: 2px 10px 3px; + color: var(--ks-kinpaku); + background: oklch(78% 0.12 82 / 0.08); + border: 1px solid oklch(78% 0.12 82 / 0.25); + border-radius: 4px; + line-height: 1; +} + +.home-kinpaku .why-ci-x { + color: oklch(68% 0.19 30); +} + +.home-kinpaku .why-ci-summary { + margin-top: 14px; + padding-top: 14px; + border-top-color: oklch(78% 0.12 82 / 0.18); + color: var(--ks-champagne); +} + +/* ============================================================ + T7 Live Mode visual — condensed picker + steer bar mock. + The full demo lives on /live-mode; this tile shows the + essential beat (pick + comment + steer bar) in one glance. + ============================================================ */ + +.home-kinpaku .why-panel-badge { + display: inline-block; + margin-left: 8px; + padding: 3px 8px; + border: 1px solid oklch(78% 0.12 82 / 0.4); + border-radius: 3px; + font-family: var(--ks-mono); + font-size: 0.62rem; + letter-spacing: 0.18em; + color: var(--ks-kinpaku); + vertical-align: middle; + transform: translateY(-3px); +} + +.home-kinpaku .why-visual--live { + padding: 0; + overflow: hidden; + min-height: 240px; + background: + linear-gradient(90deg, oklch(78% 0.12 82 / 0.018) 1px, transparent 1px), + linear-gradient(180deg, oklch(78% 0.12 82 / 0.015) 1px, transparent 1px), + radial-gradient(circle at 18% 100%, oklch(78% 0.12 82 / 0.07), transparent 16rem), + oklch(5% 0.004 95 / 0.92); + background-size: 34px 34px, 34px 34px, auto, auto; +} + +.home-kinpaku .why-live-frame { + position: relative; + margin: 24px clamp(20px, 3vw, 32px) 0; + border: 1px solid oklch(78% 0.12 82 / 0.22); + border-radius: 6px; + background: + linear-gradient(135deg, oklch(100% 0 0 / 0.025), transparent 38%), + oklch(4% 0.004 95 / 0.94); + box-shadow: + inset 0 0 0 1px oklch(100% 0 0 / 0.02), + 0 18px 50px oklch(2% 0.004 95 / 0.32); + overflow: hidden; +} + +.home-kinpaku .why-live-chrome { + display: flex; + align-items: center; + gap: 5px; + padding: 9px 14px; + border-bottom: 1px solid var(--ks-rule); + background: oklch(13% 0.006 95); +} +.home-kinpaku .why-live-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: oklch(58% 0.018 82); + opacity: 0.55; +} +.home-kinpaku .why-live-dot:nth-child(1) { background: var(--ks-kinpaku-deep); } +.home-kinpaku .why-live-dot:nth-child(2) { background: var(--ks-patina-deep); } +.home-kinpaku .why-live-url { + margin-left: 12px; + padding: 3px 10px; + background: oklch(8% 0.006 95); + border: 1px solid oklch(78% 0.12 82 / 0.14); + border-radius: 3px; + color: var(--ks-text-muted); + font-family: var(--ks-mono); + font-size: 0.72rem; +} + +.home-kinpaku .why-live-stage { + position: relative; + min-height: 130px; + padding: 24px; + background: + repeating-linear-gradient(0deg, transparent 0 9px, oklch(100% 0 0 / 0.012) 9px 10px), + oklch(8% 0.006 95); +} + +.home-kinpaku .why-live-pick { + position: relative; + width: max-content; + max-width: calc(100% - 8px); + padding: 18px 22px; + margin: 12px auto 4px; + border: 2px solid var(--ks-kinpaku); + border-radius: 3px; + background: + linear-gradient(135deg, oklch(78% 0.12 82 / 0.05), transparent 60%), + oklch(10% 0.006 95 / 0.7); + box-shadow: + 0 0 0 1px oklch(78% 0.12 82 / 0.18), + 0 0 24px oklch(78% 0.12 82 / 0.18); +} +.home-kinpaku .why-live-pick-tag { + display: block; + font-family: var(--ks-mono); + font-size: 0.7rem; + letter-spacing: 0.06em; + color: var(--ks-kinpaku); +} +.home-kinpaku .why-live-pick-note { + position: absolute; + top: -14px; + right: -18px; + padding: 5px 10px; + background: var(--ks-kinpaku); + border-radius: 2px; + color: oklch(8% 0.006 95); + font-family: var(--ks-mono); + font-size: 0.7rem; + letter-spacing: 0.02em; + white-space: nowrap; + box-shadow: 0 4px 14px oklch(78% 0.12 82 / 0.25); +} +.home-kinpaku .why-live-pick-caret { + display: inline-block; + width: 1.5px; + height: 0.85em; + margin-left: 3px; + background: oklch(8% 0.006 95); + vertical-align: middle; + animation: why-live-caret 1s steps(2, end) infinite; +} +@keyframes why-live-caret { + 50% { opacity: 0; } +} + +.home-kinpaku .why-live-gbar { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + border-top: 1px solid var(--ks-rule); + background: oklch(13% 0.006 95); + font-family: var(--ks-mono); + font-size: 0.74rem; +} +.home-kinpaku .why-live-gbar-brand { + width: 18px; + height: 18px; + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--ks-kinpaku); + color: oklch(8% 0.006 95); + border-radius: 3px; + font-weight: 600; +} +.home-kinpaku .why-live-gbar-cmd { + padding: 3px 10px; + border: 1px solid oklch(78% 0.12 82 / 0.3); + border-radius: 3px; + color: var(--ks-kinpaku); +} +.home-kinpaku .why-live-gbar-count { + padding: 3px 8px; + border: 1px solid var(--ks-rule); + border-radius: 3px; + color: var(--ks-text-muted); +} +.home-kinpaku .why-live-gbar-go { + margin-left: auto; + padding: 4px 12px; + background: var(--ks-kinpaku); + color: oklch(8% 0.006 95); + border-radius: 3px; + font-weight: 500; +} + +/* ============================================================ + T8 Writes-to-source visual — file write + small diff. + The "Accept" beat made visible: not a mock, a real edit. + ============================================================ */ + +.home-kinpaku .why-visual--writes { + padding: 0; + overflow: hidden; + background: + radial-gradient(circle at 80% 14%, oklch(78% 0.12 82 / 0.05), transparent 14rem), + oklch(5% 0.004 95 / 0.92); +} + +.home-kinpaku .why-writes-card { + margin: 24px; + border: 1px solid oklch(78% 0.12 82 / 0.22); + border-radius: 5px; + overflow: hidden; + background: oklch(4% 0.004 95 / 0.94); + box-shadow: 0 14px 40px oklch(2% 0.004 95 / 0.32); +} + +.home-kinpaku .why-writes-head { + display: flex; + align-items: center; + gap: 8px; + padding: 11px 14px; + background: oklch(13% 0.006 95); + border-bottom: 1px solid var(--ks-rule); + font-family: var(--ks-mono); + font-size: 0.78rem; +} +.home-kinpaku .why-writes-check { + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + background: var(--ks-kinpaku); + color: oklch(8% 0.006 95); + border-radius: 50%; + font-size: 0.78rem; + font-weight: 700; +} +.home-kinpaku .why-writes-label { + color: var(--ks-text-muted); + letter-spacing: 0.04em; +} +.home-kinpaku .why-writes-file { + color: var(--ks-champagne); + letter-spacing: 0.01em; +} + +.home-kinpaku .why-writes-diff { + padding: 14px 14px 16px; + font-family: var(--ks-mono); + font-size: 0.74rem; + line-height: 1.75; +} +.home-kinpaku .why-writes-line { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.home-kinpaku .why-writes-line--del { + color: oklch(62% 0.13 30); + background: oklch(62% 0.13 30 / 0.06); +} +.home-kinpaku .why-writes-line--add { + color: var(--ks-kinpaku); + background: oklch(78% 0.12 82 / 0.07); +} + +.home-kinpaku .why-visual--extension { + min-height: 332px; + padding: 0; + overflow: hidden; + background: + radial-gradient(circle at 82% 18%, oklch(70% 0.105 190 / 0.05), transparent 18rem), + linear-gradient(135deg, oklch(78% 0.12 82 / 0.04), transparent 48%), + oklch(5% 0.004 95 / 0.94); +} + +.home-kinpaku .why-visual--extension .why-browser { + min-height: 332px; + border: 1px solid oklch(78% 0.12 82 / 0.26); + border-radius: 5px; + background: oklch(4% 0.004 95 / 0.96); + box-shadow: + inset 0 0 0 1px oklch(100% 0 0 / 0.02), + 0 24px 70px oklch(2% 0.004 95 / 0.34); +} + +.home-kinpaku .why-visual--extension .why-browser-chrome { + min-height: 56px; + padding: 14px 18px; + gap: 10px; + border-bottom: 1px solid oklch(78% 0.12 82 / 0.28); + background: + linear-gradient(90deg, oklch(78% 0.12 82 / 0.105), transparent 34%), + linear-gradient(180deg, oklch(13% 0.006 95), oklch(8% 0.005 95)); +} + +.home-kinpaku .why-visual--extension .why-browser-dot { + width: 8px; + height: 8px; + background: var(--ks-kinpaku-deep); +} + +.home-kinpaku .why-visual--extension .why-browser-dot:nth-child(2) { + background: var(--ks-patina); +} + +.home-kinpaku .why-visual--extension .why-browser-dot:nth-child(3) { + background: var(--ks-kinpaku); +} + +.home-kinpaku .why-visual--extension .why-browser-url { + min-height: 30px; + display: flex; + align-items: center; + margin-left: 10px; + padding: 0 14px; + color: var(--ks-text); + border: 1px solid oklch(78% 0.12 82 / 0.16); + border-radius: 4px; + background: oklch(4% 0.004 95 / 0.9); +} + +.home-kinpaku .why-visual--extension .why-browser-body { + min-height: 276px; + padding: 24px; + overflow: hidden; + background: + linear-gradient(90deg, oklch(78% 0.12 82 / 0.026) 1px, transparent 1px), + linear-gradient(180deg, oklch(78% 0.12 82 / 0.02) 1px, transparent 1px), + radial-gradient(circle at 72% 52%, oklch(70% 0.105 190 / 0.05), transparent 13rem), + oklch(6% 0.005 95); + background-size: 42px 42px, 42px 42px, auto, auto; +} + +.home-kinpaku .why-visual--extension .why-browser-body::before, +.home-kinpaku .why-visual--extension .why-browser-body::after { + content: ""; + position: absolute; + left: 36px; + border: 1px solid oklch(84% 0.035 82 / 0.18); + border-radius: 4px; + background: + linear-gradient(135deg, oklch(78% 0.12 82 / 0.05), transparent 46%), + oklch(9% 0.006 95 / 0.72); + pointer-events: none; +} + +.home-kinpaku .why-visual--extension .why-browser-body::before { + top: 38px; + width: min(420px, 48%); + height: 92px; +} + +.home-kinpaku .why-visual--extension .why-browser-body::after { + bottom: 36px; + width: min(320px, 36%); + height: 50px; +} + +.home-kinpaku .why-visual--extension .why-ext-popup { + top: 34px; + right: 28px; + width: min(300px, 42%); + border: 1px solid oklch(78% 0.12 82 / 0.38); + border-radius: 5px; + background: + linear-gradient(135deg, oklch(78% 0.12 82 / 0.055), transparent 48%), + oklch(5% 0.004 95 / 0.96); + box-shadow: + inset 0 0 0 1px oklch(100% 0 0 / 0.025), + 0 24px 60px oklch(2% 0.004 95 / 0.48); +} + +.home-kinpaku .why-visual--extension .why-ext-popup-header { + color: var(--ks-champagne); + border-bottom: 1px solid oklch(78% 0.12 82 / 0.22); + background: oklch(9% 0.006 95 / 0.86); +} + +.home-kinpaku .why-visual--extension .why-ext-popup-row { + color: var(--ks-text); + border-bottom-color: oklch(78% 0.12 82 / 0.12); +} + +.home-kinpaku .why-visual--extension .why-ext-sev { + color: var(--ks-vermilion); +} + +.home-kinpaku .why-visual--extension .why-ext-overlay-a, +.home-kinpaku .why-visual--extension .why-ext-overlay-b { + z-index: 1; + border-color: var(--ks-kinpaku); + box-shadow: + 0 0 0 1px oklch(4% 0.004 95 / 0.64), + 0 0 24px oklch(78% 0.12 82 / 0.18); +} + +.home-kinpaku .why-visual--extension .why-ext-overlay-a { + left: 36px; + top: 38px; + width: min(420px, 48%); + height: 92px; +} + +.home-kinpaku .why-visual--extension .why-ext-overlay-b { + left: 36px; + bottom: 36px; + width: min(320px, 36%); + height: 50px; +} + +.home-kinpaku .why-visual--designmd-v2 { + gap: 16px; + padding: clamp(18px, 2vw, 26px); + overflow: hidden; + background: + radial-gradient(circle at 13% 24%, oklch(78% 0.12 82 / 0.055), transparent 18rem), + radial-gradient(circle at 88% 18%, oklch(70% 0.105 190 / 0.06), transparent 18rem), + linear-gradient(135deg, oklch(78% 0.12 82 / 0.035), transparent 46%), + oklch(6% 0.005 95 / 0.92); +} + +.home-kinpaku .why-dm-header { + position: relative; + z-index: 1; +} + +.home-kinpaku .why-dm-filename { + color: var(--ks-champagne); + font-family: var(--ks-mono); + font-size: 0.88rem; + letter-spacing: 0.06em; +} + +.home-kinpaku .why-dm-badge { + border-color: oklch(70% 0.105 190 / 0.42); + background: oklch(70% 0.105 190 / 0.08); + box-shadow: 0 0 24px oklch(70% 0.105 190 / 0.08); +} + +.home-kinpaku .why-dm-grid { + border: 0; + background: transparent; +} + +.home-kinpaku .why-dm-tile { + border-color: var(--ks-rule); + background: + linear-gradient(135deg, oklch(100% 0 0 / 0.024), transparent 38%), + oklch(5% 0.004 95 / 0.78); + box-shadow: inset 0 0 0 1px oklch(78% 0.12 82 / 0.026); +} + +.home-kinpaku .why-dm-tile-meta { + color: var(--ks-muted); +} + +.home-kinpaku .why-dm-tile-meta span:first-child { + color: var(--ks-kinpaku-pale); +} + +.home-kinpaku .why-dm-color-hero { + position: relative; + overflow: hidden; + border: 1px solid oklch(100% 0 0 / 0.08); + background: + linear-gradient(180deg, oklch(96% 0.035 84 / 0.14), oklch(62% 0.08 78 / 0.22)), + url("/assets/neo-kinpaku/kinpaku-gold-leaf.png") center / cover, + var(--c); + box-shadow: inset 0 0 28px oklch(100% 0 0 / 0.06); +} + +.home-kinpaku .why-dm-color-hero::after { + content: ""; + position: absolute; + inset: auto -8% -14% 42%; + height: 52%; + background: url("/assets/neo-kinpaku/verdigris-patina.png") center / cover no-repeat; + opacity: 0.42; + mix-blend-mode: screen; + transform: rotate(-4deg); +} + +.home-kinpaku .why-dm-color-ramp span { + border: 1px solid oklch(100% 0 0 / 0.05); +} + +.home-kinpaku .why-dm-type-specimen { + color: var(--ks-champagne); + font-family: var(--ks-font); + font-style: normal; + font-weight: 300; + letter-spacing: 0.08em; +} + +.home-kinpaku .why-dm-type-family { + color: var(--ks-text); +} + +.home-kinpaku .why-dm-comp-chip { + color: var(--ks-champagne); + border-color: var(--ks-rule); + background: transparent; +} + +.home-kinpaku .why-dm-comp-chip--primary { + color: var(--ks-lacquer-deep); + border-color: var(--ks-kinpaku); + background: + linear-gradient(90deg, oklch(95% 0.04 84 / 0.34), oklch(78% 0.12 82 / 0.72)), + url("/assets/neo-kinpaku/kinpaku-gold-leaf.png") center / 160% auto; +} + +.home-kinpaku .why-dm-comp-input { + color: var(--ks-muted); + border-color: var(--ks-rule); + background: oklch(5% 0.004 95 / 0.74); +} + +.home-kinpaku .why-visual--v2 { + min-height: 284px; + overflow: hidden; + background: + linear-gradient(90deg, oklch(78% 0.12 82 / 0.018) 1px, transparent 1px), + linear-gradient(180deg, oklch(78% 0.12 82 / 0.015) 1px, transparent 1px), + radial-gradient(circle at 78% 20%, oklch(70% 0.105 190 / 0.05), transparent 18rem), + oklch(6% 0.005 95 / 0.94); + background-size: 34px 34px, 34px 34px, auto, auto; +} + +.home-kinpaku .why-visual--v2 .v2-side { + position: relative; + padding: clamp(18px, 2vw, 28px); + gap: 16px; +} + +.home-kinpaku .why-visual--v2 .v2-side--old { + background: + linear-gradient(135deg, oklch(84% 0.035 82 / 0.04), transparent 58%), + oklch(8% 0.005 95 / 0.88); +} + +.home-kinpaku .why-visual--v2 .v2-side--old::after { + background: + repeating-linear-gradient(0deg, transparent 0 32px, oklch(78% 0.12 82 / 0.055) 32px 33px), + linear-gradient(90deg, transparent, oklch(2% 0.004 95 / 0.52)); +} + +.home-kinpaku .why-visual--v2 .v2-side--new { + border-left: 1px solid var(--ks-rule); + background: + linear-gradient(135deg, oklch(78% 0.12 82 / 0.07), transparent 44%), + radial-gradient(circle at 86% 18%, oklch(70% 0.105 190 / 0.065), transparent 12rem), + oklch(9% 0.006 95 / 0.9); +} + +.home-kinpaku .why-visual--v2 .v2-side--new::before { + content: ""; + position: absolute; + top: 16px; + bottom: 16px; + left: -1px; + width: 1px; + background: url("/assets/neo-kinpaku/before-after-seam.png") center / 18px 100% no-repeat; + opacity: 0.7; + pointer-events: none; +} + +.home-kinpaku .why-visual--v2 .v2-label { + color: var(--ks-text); + font-family: var(--ks-mono); + font-size: 0.68rem; + font-weight: 500; + letter-spacing: 0.22em; +} + +.home-kinpaku .why-visual--v2 .v2-side--new .v2-label { + color: var(--ks-patina); +} + +.home-kinpaku .why-visual--v2 .v2-canvas-chrome { + gap: 8px; + padding-bottom: 12px; + border-bottom: 1px solid oklch(78% 0.12 82 / 0.18); +} + +.home-kinpaku .why-visual--v2 .v2-canvas-tool { + color: var(--ks-muted); + border: 1px solid oklch(84% 0.035 82 / 0.16); + border-radius: 3px; + background: + linear-gradient(180deg, oklch(100% 0 0 / 0.025), transparent), + oklch(4% 0.004 95 / 0.92); + box-shadow: inset 0 -10px 16px oklch(2% 0.004 95 / 0.42); +} + +.home-kinpaku .why-visual--v2 .v2-canvas-art { + gap: 10px; +} + +.home-kinpaku .why-visual--v2 .v2-canvas-file { + max-width: 100%; + color: var(--ks-text); + font-size: clamp(0.62rem, 0.82vw, 0.72rem); + letter-spacing: 0.04em; + text-align: center; + white-space: nowrap; +} + +.home-kinpaku .why-visual--v2 .v2-canvas-mock { + position: relative; + width: min(100%, 188px); + max-width: none; + height: 78px; + overflow: hidden; + border: 1px solid oklch(84% 0.035 82 / 0.2); + border-radius: 4px; + background: + radial-gradient(circle at 82% 18%, oklch(78% 0.12 82 / 0.08), transparent 44%), + oklch(4% 0.004 95 / 0.96); + box-shadow: + inset 0 0 0 1px oklch(100% 0 0 / 0.02), + 0 18px 48px oklch(2% 0.004 95 / 0.34); +} + +.home-kinpaku .why-visual--v2 .v2-canvas-mock::before, +.home-kinpaku .why-visual--v2 .v2-canvas-mock::after { + content: ""; + position: absolute; + border: 1px solid oklch(84% 0.035 82 / 0.16); + border-radius: 2px; +} + +.home-kinpaku .why-visual--v2 .v2-canvas-mock::before { + left: 18px; + top: 18px; + width: 58px; + height: 40px; + background: oklch(10% 0.006 95); +} + +.home-kinpaku .why-visual--v2 .v2-canvas-mock::after { + right: 18px; + top: 18px; + width: 72px; + height: 40px; + background: + repeating-linear-gradient(180deg, oklch(84% 0.035 82 / 0.32) 0 2px, transparent 2px 9px), + transparent; +} + +.home-kinpaku .why-visual--v2 .v2-canvas-stamp { + color: var(--ks-muted); + font-family: var(--ks-font); + font-size: 0.72rem; + font-style: normal; + letter-spacing: 0.04em; +} + +.home-kinpaku .why-visual--v2 .v2-new-surface { + gap: 12px; +} + +.home-kinpaku .why-visual--v2 .v2-new-line { + color: var(--ks-kinpaku-pale); + border: 1px solid var(--ks-rule); + border-radius: 4px; + background: + linear-gradient(90deg, oklch(70% 0.105 190 / 0.07), transparent), + oklch(5% 0.004 95 / 0.88); + box-shadow: inset 0 0 0 1px oklch(78% 0.12 82 / 0.035); +} + +.home-kinpaku .why-visual--v2 .v2-new-line::before { + color: var(--ks-patina); +} + +.home-kinpaku .why-visual--v2 .v2-new-browser { + min-height: 102px; + border-color: var(--ks-rule); + border-radius: 4px; + background: oklch(5% 0.004 95 / 0.92); + box-shadow: 0 18px 44px oklch(2% 0.004 95 / 0.26); +} + +.home-kinpaku .why-visual--v2 .v2-new-browser-dots { + border-bottom: 1px solid var(--ks-rule); + background: + linear-gradient(90deg, oklch(78% 0.12 82 / 0.08), transparent), + oklch(8% 0.006 95); +} + +.home-kinpaku .why-visual--v2 .v2-new-browser-dot { + background: var(--ks-kinpaku-deep); +} + +.home-kinpaku .why-visual--v2 .v2-new-browser-dot:nth-child(2) { + background: var(--ks-patina); +} + +.home-kinpaku .why-visual--v2 .v2-new-browser-frame { + background: + linear-gradient(90deg, oklch(78% 0.12 82 / 0.035) 1px, transparent 1px), + linear-gradient(180deg, oklch(78% 0.12 82 / 0.026) 1px, transparent 1px), + radial-gradient(circle at 72% 58%, oklch(70% 0.105 190 / 0.06), transparent 7rem), + oklch(7% 0.005 95); + background-size: 26px 26px, 26px 26px, auto, auto; +} + +.home-kinpaku .why-visual--v2 .v2-new-browser-frame::after { + left: 14px; + top: 14px; + width: 48%; + height: 12px; + border-color: var(--ks-patina); + border-radius: 2px; + box-shadow: + 0 20px 0 -4px oklch(78% 0.12 82 / 0.46), + 86px 0 0 -4px oklch(78% 0.12 82 / 0.34), + 86px 20px 0 -4px oklch(78% 0.12 82 / 0.24); +} + +.home-kinpaku .why-visual--v2 .v2-new-bar { + color: var(--ks-lacquer-deep); + border-radius: 999px; + background: var(--ks-kinpaku); + box-shadow: 0 12px 28px oklch(78% 0.12 82 / 0.16); +} + +.home-kinpaku .patterns-tab { + color: var(--ks-muted); + border-color: var(--ks-rule); + background: transparent; +} + +.home-kinpaku .patterns-tab:hover, +.home-kinpaku .patterns-tab.is-active { + color: var(--ks-lacquer-deep); + background: var(--ks-kinpaku); + border-color: var(--ks-kinpaku); +} + +.home-kinpaku .patterns-col li { + color: var(--ks-text); + border-color: oklch(78% 0.12 82 / 0.12); +} + +.home-kinpaku .patterns-col--do li::before { + color: var(--ks-patina); +} + +.home-kinpaku .patterns-col--dont li::before { + color: var(--ks-vermilion); +} + +.home-kinpaku .gallery-stack-btn { + color: var(--ks-kinpaku); + border-color: var(--ks-rule); + background: oklch(9% 0.006 95); +} + +.home-kinpaku .gallery-stack-card { + border: 1px solid var(--ks-rule); + border-radius: 4px; + background: var(--ks-lacquer-raised); +} + +.home-kinpaku .gallery-stack-label { + color: var(--ks-champagne); + background: oklch(5% 0.004 95 / 0.82); +} + +.home-kinpaku .live-demo-frame { + border: 1px solid var(--ks-rule); + border-radius: 4px; + background: var(--ks-lacquer-raised); + box-shadow: 0 30px 84px oklch(2% 0.004 95 / 0.42); +} + +/* Picker chrome (chrome bar, gbar, ctx, outline, comment, stroke, etc.) + shares one source of truth in kinpaku-kit.css under the "Live picker + (kinpaku theme)" section, so this page and /live-mode stay in lockstep. + Only the page-specific bits below remain: the stylized gold-grid stage + and the simulated card content. */ + +.home-kinpaku .live-demo-stage { + background: + linear-gradient(90deg, oklch(78% 0.12 82 / 0.035) 1px, transparent 1px), + linear-gradient(180deg, oklch(78% 0.12 82 / 0.028) 1px, transparent 1px), + oklch(8% 0.006 95); + background-size: 28px 28px; +} + +.home-kinpaku .live-demo-card, +.home-kinpaku .live-demo-support-cell { + color: var(--ks-text); + border-color: var(--ks-rule); + background: oklch(10% 0.006 95 / 0.92); +} + +.home-kinpaku .live-demo-card h3 { + color: var(--ks-champagne); + font-family: var(--ks-font); + font-style: normal; +} + +.home-kinpaku .live-demo-card p, +.home-kinpaku .live-demo-support-v { + color: var(--ks-muted); +} + +.home-kinpaku .live-demo-card button { + background: var(--ks-kinpaku); + color: var(--ks-lacquer-deep); +} + +.home-kinpaku .live-demo-card--v2, +.home-kinpaku .live-demo-card--v3 { + border-color: var(--ks-patina); + background: oklch(9% 0.006 95 / 0.95); +} + +.home-kinpaku .platforms-section { + overflow: hidden; +} + +.home-kinpaku .install-row-primary, +.home-kinpaku .install-accordion, +.home-kinpaku .changelog-faq-row, +.home-kinpaku .consulting-content { + gap: 28px; +} + +.home-kinpaku .install-row-primary { + padding: 0; + background: transparent; + border-radius: 0; +} + +.home-kinpaku .install-primary-main, +.home-kinpaku .install-primary-howto, +.home-kinpaku .install-step, +.home-kinpaku .install-cmd-block, +.home-kinpaku .install-tool-preview, +.home-kinpaku .install-updated-x, +.home-kinpaku .changelog-highlights, +.home-kinpaku .changelog-entry, +.home-kinpaku .faq-item, +.home-kinpaku .consulting-content, +.home-kinpaku .load-error { + border: 1px solid var(--ks-rule); + border-radius: 4px; + background: + linear-gradient(135deg, oklch(78% 0.12 82 / 0.045), transparent 44%), + oklch(8% 0.006 95 / 0.84); + box-shadow: inset 0 0 0 1px oklch(78% 0.12 82 / 0.04); +} + +.home-kinpaku .install-primary-main, +.home-kinpaku .install-primary-howto { + padding: 32px; +} + +.home-kinpaku .consulting-content { + align-items: center; + gap: clamp(32px, 6vw, 96px); + padding: clamp(28px, 4vw, 52px); +} + +.home-kinpaku .consulting-text { + max-width: 54ch; +} + +.home-kinpaku .consulting-title { + margin-bottom: 18px; +} + +.home-kinpaku .consulting-desc { + max-width: 52ch; + line-height: 1.75; +} + +.home-kinpaku .consulting-actions { + margin-left: auto; +} + +.home-kinpaku .consulting-actions .btn { + min-width: 220px; +} + +.home-kinpaku .install-cmd-block { + gap: 16px; + padding: 18px; + margin-bottom: 0; +} + +.home-kinpaku .install-cmd-line { + padding: 14px 18px; +} + +.home-kinpaku .install-cmd-note { + display: block; + max-width: 76ch; + padding: 0 4px 2px; + line-height: 1.75; +} + +.home-kinpaku .install-primary-howto { + border-left: 1px solid var(--ks-rule); + background: + linear-gradient(135deg, oklch(70% 0.105 190 / 0.05), transparent 44%), + oklch(8% 0.006 95 / 0.84); +} + +.home-kinpaku .install-path-title, +.home-kinpaku .consulting-title, +.home-kinpaku .load-error-title { + color: var(--ks-champagne); + font-family: var(--ks-font); + font-size: 1.45rem; + font-weight: 400; +} + +.home-kinpaku .install-path-step { + color: var(--ks-lacquer-deep); + background: var(--ks-kinpaku); + border: 0; +} + +.home-kinpaku .install-path-desc, +.home-kinpaku .install-howto-row dd, +.home-kinpaku .install-cmd-note, +.home-kinpaku .install-terminal-note, +.home-kinpaku .install-alt-note, +.home-kinpaku .consulting-desc, +.home-kinpaku .load-error-text { + color: var(--ks-text); +} + +.home-kinpaku .glass-terminal, +.home-kinpaku .terminal-window, +.home-kinpaku .install-terminal-cmd, +.home-kinpaku .install-cmd-line { + border-color: var(--ks-rule); + background: oklch(5% 0.004 95 / 0.9); +} + +.home-kinpaku .terminal-header { + border-color: var(--ks-rule); + background: oklch(8% 0.006 95 / 0.95); +} + +.home-kinpaku .terminal-prompt, +.home-kinpaku .install-path-slash { + color: var(--ks-patina); +} + +.home-kinpaku .copy-btn, +.home-kinpaku .install-zip-link, +.home-kinpaku .install-alts-summary, +.home-kinpaku .install-step-summary, +.home-kinpaku .changelog-older-toggle, +.home-kinpaku .faq-question, +.home-kinpaku .btn-secondary { + color: var(--ks-kinpaku); + border-color: var(--ks-rule); + background: transparent; +} + +.home-kinpaku .copy-btn:hover, +.home-kinpaku .copy-btn.copied, +.home-kinpaku .install-zip-link:hover, +.home-kinpaku .install-alts-summary:hover, +.home-kinpaku .install-step-summary:hover, +.home-kinpaku .changelog-older-toggle:hover, +.home-kinpaku .faq-question:hover, +.home-kinpaku .btn-secondary:hover { + color: var(--ks-patina); + border-color: var(--ks-patina); + background: oklch(70% 0.105 190 / 0.07); +} + +.home-kinpaku .install-howto-row { + border-color: var(--ks-rule); +} + +.home-kinpaku .install-howto-row dt code, +.home-kinpaku .install-terminal-cmd code, +.home-kinpaku .install-cmd-line code { + color: var(--ks-kinpaku-pale); + background: transparent; + border: 0; +} + +.home-kinpaku .install-path-badge { + border-color: oklch(70% 0.105 190 / 0.42); + color: var(--ks-patina); + background: oklch(70% 0.105 190 / 0.08); +} + +.home-kinpaku .install-updated-substack { + border-color: var(--ks-rule); + border-radius: 4px; + filter: sepia(0.24) saturate(0.74) brightness(0.82); +} + +.home-kinpaku .changelog-faq-row { + border-top: 1px solid var(--ks-rule); + padding: 112px 0; +} + +.home-kinpaku .changelog-section, +.home-kinpaku .faq-section { + border-top: 0; + padding: 0; +} + +@media (min-width: 1024px) { + .home-kinpaku .changelog-faq-row { + column-gap: 0; + } + + .home-kinpaku .changelog-faq-row > .changelog-section { + padding-right: clamp(40px, 4vw, 72px); + } + + .home-kinpaku .changelog-faq-row > .faq-section { + margin-left: 0; + padding-left: clamp(40px, 4vw, 72px); + border-left-color: var(--ks-rule); + } +} + +.home-kinpaku .changelog-highlights { + padding: 24px 28px; + overflow: hidden; +} + +.home-kinpaku .changelog-highlights-label { + margin-bottom: 20px; +} + +.home-kinpaku .changelog-items { + padding-left: 1.1rem; +} + +.home-kinpaku .faq-list { + gap: 8px; +} + +.home-kinpaku .faq-question { + padding: 24px 64px 24px 28px; +} + +.home-kinpaku .faq-question::after { + right: 28px; +} + +.home-kinpaku .faq-answer { + padding: 0 28px 24px; +} + +.home-kinpaku .changelog-version { + color: var(--ks-kinpaku); +} + +.home-kinpaku .changelog-date, +.home-kinpaku .changelog-items li, +.home-kinpaku .faq-answer, +.home-kinpaku .faq-answer p, +.home-kinpaku .faq-answer li { + color: var(--ks-text); +} + +.home-kinpaku .faq-question::after, +.home-kinpaku .changelog-older-toggle::before { + color: var(--ks-patina); +} + +/* The home footer layers a textured verdigris divider over the shared + kinpaku footer. `body.` prefix raises specificity above + .kinpaku-chrome .site-footer so this wins regardless of import order. */ +body.home-kinpaku .site-footer { + position: relative; + border-top: 0; + background: + linear-gradient(180deg, oklch(5% 0.004 95 / 0.92), oklch(3% 0.004 95)), + url("/assets/neo-kinpaku/lacquer-grain.png") center bottom / 1500px auto; +} + +/* Oxidation edge accent moved to the Get Started card — see + .downloads-rebuild-card::before in home-rebuild.css. */ + +/* D3 dot grid masked over the verdigris-patina texture — the same tex-patina + variant from /design-system. Each dot is a tiny window of the patina image + so the row reads as oxidized copper rather than flat green-blue. */ +body.home-kinpaku .site-footer::after { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 18px; + background-image: url("/assets/neo-kinpaku/verdigris-patina.png"); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + filter: saturate(1.18) contrast(1.06); + -webkit-mask-image: radial-gradient(circle, black 0.9px, transparent 1.4px); + mask-image: radial-gradient(circle, black 0.9px, transparent 1.4px); + -webkit-mask-size: 7px 6px; + mask-size: 7px 6px; + -webkit-mask-repeat: repeat; + mask-repeat: repeat; + -webkit-mask-position: 0 1px; + mask-position: 0 1px; + pointer-events: none; + z-index: 1; +} + +.home-kinpaku .footer-row { + position: relative; + z-index: 1; +} + +body.home-kinpaku .footer-links a, +body.home-kinpaku .footer-credit, +body.home-kinpaku .footer-credit a, +body.home-kinpaku .footer-social-link { + color: var(--ks-muted); +} + +body.home-kinpaku .footer-links a:hover, +body.home-kinpaku .footer-credit a:hover, +body.home-kinpaku .footer-social-link:hover { + color: var(--ks-patina); +} + +.home-kinpaku .btn:focus-visible, +.home-kinpaku a:focus-visible, +.home-kinpaku button:focus-visible, +.home-kinpaku summary:focus-visible { + outline: 2px solid var(--ks-patina); + outline-offset: 4px; +} + +@media (max-width: 1100px) { + .home-kinpaku .hero-combined-container { + grid-template-columns: 1fr; + text-align: left; + } + + .home-kinpaku .hero-combined-left { + align-items: flex-start; + } + + .home-kinpaku .hero-combined-right { + justify-content: flex-start; + order: 0; + } + + .home-circuit-field, + .home-calibration { + opacity: 0.32; + } + + .home-kinpaku .hero-title-combined { + font-size: 4.35rem; + } + + .home-kinpaku .why-layout { + grid-template-columns: 1fr; + } +} + +@media (max-width: 820px) { + .home-kinpaku .site-header { + padding: 16px 18px; + } + + .home-kinpaku .site-header-brand { + gap: 4px; + } + + .home-kinpaku .site-header-brand-logo { + width: 34px; + height: 34px; + } + + .home-kinpaku .site-header-brand-name { + letter-spacing: 0.08em; + } + + .home-kinpaku .site-header-nav { + gap: 18px; + } + + .home-kinpaku .hero-combined { + padding-top: 40px; + } + + .home-kinpaku .hero-combined::before { + inset: -8% -28% 0 20%; + opacity: 0.36; + filter: contrast(1.02) saturate(0.96) brightness(0.72); + } + + .home-kinpaku .hero-combined-container { + padding-bottom: 60px; + } + + .home-kinpaku .hero-title-combined { + font-size: 3.1rem; + letter-spacing: 0; + } + + .home-kinpaku .hero-tagline-combined { + font-size: 1.08rem; + letter-spacing: 0.08em; + } + + .home-hero-rail { + grid-template-columns: repeat(2, 1fr); + } + + .home-hero-rail a { + border-bottom: 1px solid oklch(78% 0.12 82 / 0.12); + } + + .home-kinpaku .split-container { + height: 356px; + } + + .home-kinpaku .split-before { + padding-left: 0; + } + + .home-kinpaku .split-after { + padding-right: 0; + } + + .home-kinpaku .slop-card, + .home-kinpaku .impeccable-card { + width: 240px; + } + + .home-circuit-field, + .home-calibration, + .home-patina-dot { + display: none; + } + + .home-kinpaku .foundation-section, + .home-kinpaku .language-section, + .home-kinpaku .why-section, + .home-kinpaku .live-section, + .home-kinpaku .platforms-section, + .home-kinpaku .consulting-section, + .home-kinpaku .changelog-faq-row { + padding: 72px 0; + } + + .home-kinpaku .section-title { + font-size: 2rem; + } + + .home-kinpaku .spread-command-name { + font-size: 3rem; + } + + .home-kinpaku .language-view--periodic .solution-visual-interactive { + overflow: hidden; + } + + .home-kinpaku .language-view--periodic .solution-visual-interactive::before { + display: none; + } + + .home-kinpaku #framework-viz-container .ptable-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)) !important; + gap: 24px !important; + } + + .home-kinpaku #framework-viz-container .ptable-group--system { + grid-column: auto; + margin-top: 0; + } + + .home-kinpaku #framework-viz-container .ptable-element { + width: 64px !important; + height: 76px !important; + } + + .home-kinpaku .why-visual--v2 { + grid-template-columns: 1fr; + } + + .home-kinpaku .why-visual--v2 .v2-side--new { + border-top: 1px solid var(--ks-rule); + border-left: 0; + } + + .home-kinpaku .why-visual--v2 .v2-side--new::before { + top: -1px; + bottom: auto; + left: 16px; + right: 16px; + width: auto; + height: 1px; + background: url("/assets/neo-kinpaku/gold-dust-rule.png") center / cover no-repeat; + } + + .home-kinpaku .why-visual--v2 .v2-canvas-file { + white-space: normal; + } + + .home-kinpaku .why-visual--extension, + .home-kinpaku .why-visual--extension .why-browser { + min-height: 420px; + } + + .home-kinpaku .why-visual--extension .why-browser-body { + min-height: 364px; + } + + .home-kinpaku .why-visual--extension .why-ext-popup { + top: 154px; + right: 22px; + left: 22px; + width: auto; + } + + .home-kinpaku .why-visual--extension .why-browser-body::before, + .home-kinpaku .why-visual--extension .why-ext-overlay-a { + top: 28px; + left: 22px; + width: calc(100% - 44px); + height: 82px; + } + + .home-kinpaku .why-visual--extension .why-browser-body::after, + .home-kinpaku .why-visual--extension .why-ext-overlay-b { + bottom: 24px; + left: 22px; + width: calc(100% - 44px); + height: 44px; + } +} + +@media (max-width: 560px) { + /* (The hamburger drawer in Header.astro now handles the mobile nav. + The old display:none hacks for github / designing / live were removed + because they hid items the drawer is meant to show.) */ + + .home-kinpaku .hero-title-combined { + font-size: 2.4rem; + letter-spacing: 0; + } + + .home-kinpaku .hero-cta-group { + align-items: stretch; + width: 100%; + } + + .home-kinpaku .hero-included-box, + .home-kinpaku .hero-logos-inline, + .home-kinpaku .hero-version-link, + .home-hero-rail { + display: none; + } + + .home-kinpaku .hero-cta-combined { + width: 100%; + } + + .home-kinpaku .hero-combined-right { + width: 100%; + margin-top: 4px; + } + + .home-kinpaku .split-container { + height: 300px; + } + + .home-kinpaku .split-before { + padding-left: 0; + } + + .home-kinpaku .split-after { + padding-right: 0; + } + + .home-kinpaku .slop-card, + .home-kinpaku .impeccable-card { + width: 214px; + min-height: 250px; + padding: 18px; + } + + .home-kinpaku .slop-callout { + display: none; + } + + .home-hero-rail { + grid-template-columns: 1fr; + } + + .home-hero-rail a { + justify-content: flex-start; + padding: 0 18px; + border-right: 0; + } +} + +@media (prefers-reduced-motion: reduce) { + .home-kinpaku *, + .home-kinpaku *::before, + .home-kinpaku *::after { + animation-duration: 1ms !important; + transition-duration: 1ms !important; + scroll-behavior: auto !important; + } +} diff --git a/site/styles/home-rebuild.css b/site/styles/home-rebuild.css new file mode 100644 index 000000000..85a42f1ad --- /dev/null +++ b/site/styles/home-rebuild.css @@ -0,0 +1,1790 @@ +/* ============================================================================ + Homepage rebuild: hero + slop teaser + Built to match the /design-system aesthetic (neo kinpaku). + Loaded after home-kinpaku.css so these rules win on the rebuilt sections. + + All brand tokens (--ks-kinpaku, --ks-kinpaku-pale, --ks-patina, etc.) come + from kinpaku-tokens.css via home-kinpaku.css's import — no duplicate + declarations here. + ============================================================================ */ + + +/* ============================================================================ + HERO REBUILD + ============================================================================ */ + +/* The header overlays the hero (see .home-kinpaku .site-header below), so the + hero fills the full viewport and its art runs to the very top, behind the + transparent nav — the /design-system topbar treatment. */ +.hero-rebuild { + position: relative; + /* Leave ~115px of the testimonials section visible above the fold + (dotted divider + first row's top edge). Without this the hero + fills the entire viewport and the testimonials read as out of sight. */ + min-height: calc(100svh - 115px); + display: grid; + align-items: center; + /* Top padding clears the fixed (overlay) header (97px). On short viewports the + hero grows to fit its content and pins it at this padding-top, so the demo + never tucks under the nav; on tall viewports the content still centers. */ + padding: 113px 0 96px; + overflow: hidden; +} + +/* Header sits on top of the hero image like the /design-system topbar: + transparent over the hero, then glassing in as you scroll. Pinned (fixed) so + it overlays the hero without taking flow space. Overrides the sticky light + bar from sub-pages.css. + + --hp is the scroll progress (0 at the top → 1 once past the hero top), set + px-by-px from app.js, so the glass interpolates continuously rather than + snapping. */ +.home-kinpaku .site-header { + --hp: 0; + position: fixed; + top: 0; + left: 0; + right: 0; + background: oklch(8% 0.006 95 / calc(0.72 * var(--hp))); + border-bottom: 1px solid oklch(78% 0.12 82 / calc(0.18 * var(--hp))); + -webkit-backdrop-filter: saturate(calc(1 + 0.3 * var(--hp))) blur(calc(16px * var(--hp))); + backdrop-filter: saturate(calc(1 + 0.3 * var(--hp))) blur(calc(16px * var(--hp))); +} + +.hero-rebuild-art { + position: absolute; + inset: 0; + z-index: 0; + /* Top scrim darkens the band behind the overlaid nav so the links stay + legible over the bright kintsugi gold, fading out before the hero copy. */ + background: + linear-gradient(180deg, oklch(7% 0.006 95 / 0.9) 0%, oklch(7% 0.006 95 / 0.5) 8%, transparent 18%), + url("/assets/neo-kinpaku/candidates/finalists/m-01-v2-01.png") center / cover no-repeat; + filter: saturate(1.2) contrast(1.08); +} + +/* Mirror .site-content's container math so the hero text aligns horizontally + with every other section's content edge. */ +.hero-rebuild-container { + position: relative; + z-index: 1; + max-width: 1500px; + margin: 0 auto; + padding: 0 56px; + width: 100%; + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 0.95fr); + gap: 72px; + align-items: center; +} + +.hero-rebuild-left { + display: grid; + gap: 28px; + align-content: center; + max-width: 620px; +} + +.home-kinpaku .hero-rebuild-title { + margin: 0; + color: var(--ks-champagne); + font-family: var(--ks-font-display); + font-weight: 300; + font-size: clamp(3.4rem, 6.5vw, 5.6rem); + line-height: 1.02; + letter-spacing: -0.01em; +} + +.hero-rebuild-body { + margin: 0; + color: var(--ks-text); + font-size: 1.05rem; + line-height: 1.6; + font-weight: 300; + max-width: 56ch; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; +} + +.hero-rebuild-actions { + display: flex; + flex-wrap: wrap; + gap: 14px; + margin-top: 4px; +} + +/* Hero CTAs migrated to .ks-button.ks-button-primary / .ks-button-secondary + (see kinpaku-kit.css). The .hero-rebuild-arrow class is gone too — the + arrow now lives in .ks-button-arrow inside the button. */ + +.hero-rebuild-meta { + margin: 24px 0 0; + color: var(--ks-muted); + font-size: 0.86rem; + line-height: 1.5; + max-width: 52ch; +} + +.hero-rebuild-logos { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 14px; + margin-top: 4px; +} + +.home-kinpaku .hero-rebuild-logo { + display: inline-grid; + place-items: center; + width: 24px; + height: 24px; + border: 0; + border-radius: 0; + background: transparent; + opacity: 0.72; + transition: opacity 180ms var(--ks-ease), transform 180ms var(--ks-ease); +} + +.home-kinpaku .hero-rebuild-logo img { + width: 22px; + height: 22px; + object-fit: contain; + opacity: 0.78; + filter: grayscale(1) saturate(0) invert(1) brightness(1.18) contrast(0.72); + transition: opacity 180ms var(--ks-ease), filter 180ms var(--ks-ease); +} + +/* Pi's source SVG is already white-on-transparent, so the row's default + invert(1) filter would turn it black (invisible against the lacquer + hero). Skip the filter entirely; the asset is already on-brand. */ +.home-kinpaku .hero-rebuild-logo[data-tooltip="Pi"] img { + filter: none; +} + +.home-kinpaku .hero-rebuild-logo:hover, +.home-kinpaku .hero-rebuild-logo:focus-visible { + opacity: 1; + transform: translateY(-1px); +} + +.home-kinpaku .hero-rebuild-logo:hover img, +.home-kinpaku .hero-rebuild-logo:focus-visible img { + opacity: 1; + filter: none; +} + +/* VS Code Copilot and Codex CLI logos are dark monochrome glyphs. The row's + default invert filter makes them readable; removing it on hover would put + them back to dark-on-dark, which is invisible. Keep them inverted on + hover — they don't have a brand color to restore. */ +.home-kinpaku .hero-rebuild-logo[data-tooltip="VS Code Copilot"]:hover img, +.home-kinpaku .hero-rebuild-logo[data-tooltip="VS Code Copilot"]:focus-visible img, +.home-kinpaku .hero-rebuild-logo[data-tooltip="Codex CLI"]:hover img, +.home-kinpaku .hero-rebuild-logo[data-tooltip="Codex CLI"]:focus-visible img { + filter: grayscale(1) saturate(0) invert(1) brightness(1.18); +} + +/* Right column hosts the existing split comparison */ +.hero-rebuild-right { + position: relative; + width: 100%; + max-width: 720px; + justify-self: end; +} + +/* The .home-kinpaku .split-container rule caps width/height for the magazine + spread. Inside the hero rebuild we want the demo to fill the right column, + so override both caps here. Higher specificity wins. */ +.home-kinpaku .hero-rebuild-right .split-comparison { + max-width: 100%; +} +.home-kinpaku .hero-rebuild-right .split-container { + max-width: 100%; + height: 440px; +} + +/* problem-section.css applies @keyframes splitEntry to every .split-divider + with a baked-in skewX(-10deg) in the from/to transforms. That keyframe + transform overrides our flat translateX(-50%) for the 0.9s animation + window, producing a visible "skewed-then-corrects" effect on first paint. + Disable the animation here — the home kinpaku divider doesn't need an + entry effect. */ +.home-kinpaku .hero-rebuild-right .split-divider { + animation: none; +} + +/* The category labels under the demo ("Generic AI output" / "With Impeccable") + need to stay readable on the vibrant kinpaku-leaf hero. Localized frosted + pills behind each label only — backdrop stays full-color elsewhere. */ +.home-kinpaku .hero-rebuild-right .split-labels { + margin-top: 18px; +} + +.home-kinpaku .hero-rebuild-right .split-label-item { + display: inline-flex; + align-items: center; + gap: 9px; + padding: 7px 14px; + background: oklch(4% 0.004 95 / 0.72); + backdrop-filter: blur(8px) saturate(1.4); + -webkit-backdrop-filter: blur(8px) saturate(1.4); + border: 1px solid oklch(58% 0.065 82 / 0.32); + border-radius: 2px; + font-size: 0.72rem; + letter-spacing: 0.14em; + font-weight: 500; + color: var(--ks-champagne); +} + +.home-kinpaku .hero-rebuild-right .split-label-item[data-point="after"] { + color: var(--ks-kinpaku); + border-color: oklch(78% 0.10 82 / 0.45); +} + +@media (max-width: 1080px) { + .hero-rebuild { padding: 108px 36px 80px; } /* keep top clear of the fixed header */ + .hero-rebuild-container { grid-template-columns: 1fr; gap: 48px; } + .hero-rebuild-right { justify-self: stretch; max-width: 100%; } + .home-kinpaku .hero-rebuild-right .split-container { height: 380px; } +} + +@media (max-width: 720px) { + .hero-rebuild { padding: 92px 24px 64px; } /* clear the compact mobile header */ + .hero-rebuild-actions { flex-direction: column; align-items: stretch; } + .home-kinpaku .hero-rebuild-right .split-container { height: 320px; } +} + +/* ============================================================================ + CATCH THE SLOP TEASER + ============================================================================ */ + +.slop-teaser-section { + padding: 120px 0; +} + +.slop-teaser-head { + display: grid; + gap: 18px; + max-width: 760px; + margin-bottom: 64px; +} + +.slop-teaser-eyebrow { + margin: 0; + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-size: 0.72rem; + letter-spacing: 0.28em; + text-transform: uppercase; +} + +.home-kinpaku .slop-teaser-title { + margin: 0; + color: var(--ks-champagne); + font-family: var(--ks-font-display); + font-weight: 600; + font-size: clamp(2.6rem, 4vw, 3.4rem); + line-height: 1.04; + letter-spacing: -0.005em; +} + +.slop-teaser-body { + margin: 0; + color: var(--ks-text); + font-size: 1.02rem; + line-height: 1.6; + /* Weight 400 + explicit smoothing for the same reason .hero-rebuild-body + uses them: Avenir Next Light (weight 300) isn't installed on every system, + so the browser synthesizes it from the regular face and the anti-aliasing + goes splotchy. Weight 400 with explicit smoothing renders cleanly. */ + font-weight: 400; + max-width: 60ch; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; +} + +.slop-teaser-numerics { + display: inline-block; + margin-left: 8px; + color: var(--ks-muted); + font-family: var(--ks-mono); + font-size: 0.82rem; + letter-spacing: 0.04em; +} + +/* Gallery: horizontal scroller of antipattern thumbnails */ +.slop-teaser-gallery { + position: relative; + margin: 0 0 56px; +} + +.slop-teaser-track { + display: flex; + gap: 18px; + overflow-x: auto; + scroll-snap-type: x mandatory; + scrollbar-width: none; + padding: 4px 4px 18px; + margin: 0 -4px; +} + +.slop-teaser-track::-webkit-scrollbar { display: none; } + +.home-kinpaku .slop-teaser-card { + flex: 0 0 320px; + scroll-snap-align: start; + display: grid; + grid-template-rows: auto auto; + text-decoration: none; + background: var(--ks-lacquer-raised); + border: 1px solid var(--ks-rule); + border-radius: 2px; + overflow: hidden; + transition: border-color 180ms var(--ks-ease), transform 220ms var(--ks-ease); + position: relative; +} + +.home-kinpaku .slop-teaser-card:hover { + border-color: var(--ks-rule-strong); + transform: translateY(-2px); +} + +.slop-teaser-thumb { + position: relative; + aspect-ratio: 16 / 10; + overflow: hidden; + background: oklch(13% 0.008 290); +} + +.slop-teaser-thumb img { + width: 100%; + height: 100%; + object-fit: cover; + object-position: top left; + display: block; + transition: transform 360ms var(--ks-ease); +} + +.home-kinpaku .slop-teaser-card:hover .slop-teaser-thumb img { + transform: scale(1.03); +} + +/* Marker scribble across the thumbnail, evoking "caught" */ +.slop-teaser-thumb::after { + content: ""; + position: absolute; + inset: 0; + background: + repeating-linear-gradient(135deg, transparent 0 22px, oklch(58% 0.15 35 / 0.04) 22px 23px), + linear-gradient(180deg, transparent 60%, oklch(4% 0.004 95 / 0.55)); + pointer-events: none; +} + +.slop-teaser-meta { + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + gap: 12px; + padding: 14px 16px; + border-top: 1px solid var(--ks-rule); +} + +.slop-teaser-cat { + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-size: 0.62rem; + letter-spacing: 0.22em; + text-transform: uppercase; +} + +.home-kinpaku .slop-teaser-card strong { + color: var(--ks-champagne); + font-family: var(--ks-font); + font-weight: 400; + font-size: 1rem; + line-height: 1.2; +} + +.slop-teaser-flag { + color: oklch(58% 0.15 35); + font-family: var(--ks-mono); + font-size: 0.62rem; + letter-spacing: 0.22em; + border: 1px solid oklch(58% 0.15 35); + padding: 2px 8px; + border-radius: 2px; +} + +/* Gallery nav buttons */ +.slop-teaser-arrow { + position: absolute; + top: 38%; + width: 40px; + height: 40px; + display: grid; + place-items: center; + border: 1px solid var(--ks-rule); + background: oklch(8% 0.006 95 / 0.92); + color: var(--ks-kinpaku); + border-radius: 999px; + cursor: pointer; + transition: border-color 180ms var(--ks-ease), color 180ms var(--ks-ease); +} + +.slop-teaser-arrow:hover { border-color: var(--ks-kinpaku); } +.slop-teaser-arrow svg { width: 12px; height: 20px; } +.slop-teaser-arrow--prev { left: -20px; } +.slop-teaser-arrow--next { right: -20px; } + +@media (max-width: 720px) { + .slop-teaser-card { flex: 0 0 80%; } + .slop-teaser-arrow { display: none; } +} + +.slop-teaser-actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 24px; + margin-top: 14px; +} + +.home-kinpaku .slop-teaser-cta-primary { + display: inline-flex; + align-items: center; + gap: 14px; + color: var(--ks-kinpaku); + font-size: 1rem; + font-weight: 400; + text-decoration: none; + padding-bottom: 4px; + border-bottom: 1px solid var(--ks-kinpaku); + transition: gap 180ms var(--ks-ease); +} + +.home-kinpaku .slop-teaser-cta-primary:hover { + gap: 18px; + color: var(--ks-kinpaku); +} + +.slop-teaser-cta-primary svg { + width: 16px; + height: 8px; + flex: none; +} + +/* Scoped against the global .home-kinpaku code color override using the + chained selector (.home-kinpaku code.slop-teaser-cli has higher specificity + than .home-kinpaku code alone). Keeps the CLI block in the softer code-gold + so the line reads as a terminal command, not a pumpkin headline. */ +.home-kinpaku code.slop-teaser-cli { + color: oklch(82% 0.11 82); + font-family: var(--ks-mono); + font-size: 0.86rem; + background: var(--ks-lacquer-raised); + padding: 10px 16px; + border: 1px solid var(--ks-rule); + border-radius: 2px; + user-select: all; +} + +@media (max-width: 1080px) { + .slop-teaser-section { padding: 80px 0; } + .slop-teaser-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} + +@media (max-width: 720px) { + .slop-teaser-section { padding: 64px 0; } + .slop-teaser-grid { grid-template-columns: 1fr; } + .slop-teaser-actions { flex-direction: column; align-items: stretch; } + .slop-teaser-cli { text-align: center; } +} + +/* ============================================================================ + LIVE SECTION — right-aligned intro + The section header and lead paragraph sit flush right so the heavy demo + block below reads as left-anchored to the page, with the intro language + right-anchored above it. (Language section keeps its default left-align; + Get Started below is centered.) + ============================================================================ */ + +.home-kinpaku .live-section .section-header { + text-align: right; + justify-items: end; +} +.home-kinpaku .live-section .section-lead { + margin-left: auto; + text-align: right; +} + +/* ============================================================================ + GET STARTED REBUILD + ============================================================================ */ + +.downloads-rebuild { + padding: 120px 0; +} + +/* The card wrapper holds everything: header, install command, examples, + extras. Plinth surface. The --card-* tokens are kept so inner rules + reference them once and we can re-theme the whole card in one place if + needed later. */ +.downloads-rebuild-card { + --card-fg: var(--ks-text); + --card-fg-strong: var(--ks-champagne); + --card-fg-muted: var(--ks-muted); + --card-accent: var(--ks-kinpaku); + --card-cmd-bg: oklch(0.11 0 0); + --card-cmd-fg: var(--ks-champagne); + --card-copy-border: var(--ks-rule); + --card-copy-fg: var(--ks-kinpaku); + --card-copy-hover-bg: oklch(77% 0.14 82 / 0.08); + --card-link-color: var(--ks-kinpaku); + --card-link-underline: var(--ks-kinpaku); + --card-link-secondary: var(--ks-muted); + + /* Width of the accent's reserved right column. Content padding-right + matches this so the accent never overlaps text. */ + --card-accent-w: 280px; + + position: relative; + max-width: 1080px; + margin: 0 auto; + padding: 88px var(--card-accent-w) 88px 56px; + border-radius: 2px; + background: oklch(0.17 0 0); + color: var(--card-fg); + text-align: center; + overflow: hidden; +} + +/* Oxidation edge accent — moved here from the footer. Full-bleed on the + right, full opacity (much more glorious than the 0.6 it carried in the + footer). Content gets dedicated padding-right via --card-accent-w above, + so the accent and the content never overlap. */ +.downloads-rebuild-card::before { + content: ""; + position: absolute; + right: 0; + top: 0; + width: var(--card-accent-w); + height: 100%; + /* `cover` instead of `contain` so the image fills the card's full height — + the source PNG is 1:2 but the card aspect is more like 1:5, so `contain` + bottom-anchored a half-height render. Right-anchored, vertically centered + overflow goes off the left edge (the gold streak lives on the right side + of the image, so cropping there is fine). */ + background: url("/assets/neo-kinpaku/oxidation-edge-accent.png") right center / cover no-repeat; + opacity: 1; + pointer-events: none; + z-index: 0; +} + +/* Make sure the inner content stacks above the accent. */ +.downloads-rebuild-card > * { + position: relative; + z-index: 1; +} + +/* Narrow viewports — the accent's reserved column would crush content. + Drop the accent below the lg breakpoint and restore symmetric padding. */ +@media (max-width: 980px) { + .downloads-rebuild-card { + padding: 64px 40px; + } + .downloads-rebuild-card::before { display: none; } +} + +.downloads-rebuild-head { + display: grid; + gap: 18px; + max-width: 760px; + margin: 0 auto 48px; + text-align: center; + justify-items: center; +} + +.downloads-rebuild-eyebrow { + margin: 0; + color: var(--ks-kinpaku); + font-family: var(--ks-mono); + font-size: 0.72rem; + letter-spacing: 0.28em; + text-transform: uppercase; +} + +.home-kinpaku .downloads-rebuild-title { + margin: 0; + color: var(--card-fg-strong); + font-family: var(--ks-font-display); + font-weight: 600; + font-size: clamp(2.6rem, 4vw, 3.4rem); + line-height: 1.04; +} + +.downloads-rebuild-install { + display: grid; + gap: 12px; + margin: 0 auto 56px; + max-width: 720px; +} + +/* Install command is THE primary CTA of the Get Started section. Strong + plinth bg + bigger font + more padding make it the hero. Copy button is + nested inside on the right. No side-stripe border (brand-ban). */ +.home-kinpaku .downloads-rebuild-cmd { + display: flex; + align-items: center; + gap: 16px; + padding: 14px 14px 14px 28px; + background: var(--card-cmd-bg); + border: 0; + border-radius: 2px; +} + +.home-kinpaku .downloads-rebuild-cmd > code { + flex: 1 1 auto; + min-width: 0; + overflow-x: auto; + white-space: nowrap; + color: var(--card-cmd-fg); + font-family: var(--ks-mono); + font-size: 1.25rem; + letter-spacing: 0; + background: transparent; + border: 0; + padding: 0; + user-select: all; +} + +/* Compact icon-only copy button nested inside the cmd box, on the right. */ +.home-kinpaku .downloads-rebuild-copy { + flex: none; + display: inline-grid; + place-items: center; + width: 40px; + height: 40px; + border: 1px solid var(--card-copy-border); + background: transparent; + color: var(--card-copy-fg); + border-radius: 2px; + cursor: pointer; + transition: background 180ms var(--ks-ease), border-color 180ms var(--ks-ease); +} + +.home-kinpaku .downloads-rebuild-copy span { + /* Hide the "Copy" label — icon carries the meaning. Kept in markup for a11y. */ + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; +} + +.home-kinpaku .downloads-rebuild-copy:hover { + background: var(--card-copy-hover-bg); + border-color: var(--card-copy-fg); +} + +/* "Other install methods" — a quiet disclosure under the primary command. + Native
so it works without JS and stays accessible. */ +.downloads-rebuild-alts { + grid-column: 1 / -1; + margin: 2px 0 0; +} +.downloads-rebuild-alts-summary { + display: inline-flex; + align-items: center; + gap: 7px; + cursor: pointer; + list-style: none; + font-size: 0.84rem; + color: var(--ks-text-muted); + transition: color 160ms var(--ks-ease); +} +.downloads-rebuild-alts-summary::-webkit-details-marker { display: none; } +.downloads-rebuild-alts-summary:hover { color: var(--ks-patina); } +.downloads-rebuild-alts[open] > .downloads-rebuild-alts-summary { + color: var(--card-fg-strong); + margin-bottom: 18px; +} +.downloads-rebuild-alts-arrow { + flex: none; + transition: transform 180ms var(--ks-ease); +} +.downloads-rebuild-alts[open] > .downloads-rebuild-alts-summary .downloads-rebuild-alts-arrow { + transform: rotate(90deg); +} + +.downloads-rebuild-alt + .downloads-rebuild-alt { margin-top: 18px; } +.downloads-rebuild-alt-label { + display: block; + margin-bottom: 7px; + font-size: 0.82rem; + font-weight: 600; + color: var(--card-fg-strong); +} + +/* Smaller, quieter command box for the alternatives. */ +.home-kinpaku .downloads-rebuild-cmd--alt { + padding: 9px 9px 9px 18px; + gap: 12px; +} +.home-kinpaku .downloads-rebuild-cmd--alt > code { font-size: 0.98rem; } +.home-kinpaku .downloads-rebuild-cmd--alt .downloads-rebuild-copy { + width: 34px; + height: 34px; +} + +.downloads-rebuild-alt-note { + display: block; + margin-top: 7px; + font-size: 0.82rem; + line-height: 1.5; + color: var(--ks-text-muted); +} +.downloads-rebuild-alt-note code { + font-family: var(--ks-mono); + font-size: 0.9em; + color: var(--card-fg-muted); +} + +.downloads-rebuild-note { + grid-column: 1 / -1; + margin: 0; + color: var(--card-fg-muted); + font-size: 0.88rem; +} + +/* Quieter than the note — the changelog + FAQ hints sit just under the + primary "works with" line. Patina links so they read as related but + distinct from the kinpaku primary CTA above. */ +.downloads-rebuild-secondary { + grid-column: 1 / -1; + margin: 6px 0 0; + font-size: 0.84rem; + color: var(--ks-text-muted); +} +.downloads-rebuild-secondary a { + color: var(--ks-patina); + text-decoration: none; + border-bottom: 1px solid oklch(70% 0.12 188 / 0.35); +} +.downloads-rebuild-secondary a:hover { + color: var(--ks-patina-pale); + border-bottom-color: var(--ks-patina-pale); +} + +/* Closing "go deeper" links under the usage examples — point to the full + workflow page and the command docs. */ +.downloads-rebuild-more { + margin: 30px auto 0; + max-width: 720px; + text-align: center; + font-size: 0.9rem; + color: var(--ks-text-muted); +} +.downloads-rebuild-more a { + color: var(--ks-patina); + text-decoration: none; + border-bottom: 1px solid oklch(70% 0.12 188 / 0.35); +} +.downloads-rebuild-more a:hover { + color: var(--ks-patina-pale); + border-bottom-color: var(--ks-patina-pale); +} + +/* Center-aligned examples: each row is a vertical stack — the command line + on top, the description below, both centered horizontally. */ +/* Usage examples now sit after the extras (the logical next step once the + skill, extension, CLI, and subscriptions are set up), so the separating + gap moves to the top. */ +.downloads-rebuild-examples { + list-style: none; + margin: 48px auto 0; + padding: 48px 0 0; + /* Subtle hairline marking the "now use it" step after the setup block. */ + border-top: 1px solid var(--ks-rule); + display: grid; + gap: 22px; + max-width: 720px; +} + +.downloads-rebuild-examples li { + display: grid; + grid-template-columns: 1fr; + gap: 6px; + justify-items: center; + text-align: center; +} + +/* Strip the global .home-kinpaku code chrome for these example rows. */ +.home-kinpaku .downloads-rebuild-examples > li > code { + font-family: var(--ks-mono); + color: var(--card-accent); + font-size: 1rem; + background: transparent; + border: 0; + border-radius: 0; + padding: 0; + letter-spacing: 0.005em; +} + +.downloads-rebuild-slash { + opacity: 0.55; +} + +.downloads-rebuild-examples span { + color: var(--card-fg); + font-size: 0.94rem; + line-height: 1.5; +} + +.home-kinpaku .downloads-rebuild-examples span code { + color: var(--card-fg-strong); + background: transparent; + border: 0; + border-radius: 0; + padding: 0; + font-family: var(--ks-mono); + font-size: 0.86rem; +} + +.downloads-rebuild-extras { + list-style: none; + margin: 0 auto; + padding: 0; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 36px; + max-width: 900px; +} + +/* Each cell is a flex column so primary links can be bottom-anchored. + Text is left-aligned within the cell — wrapping paragraphs read better + that way than centered. The grid as a whole stays centered on the page. */ +.downloads-rebuild-extras li { + display: flex; + flex-direction: column; + gap: 10px; + text-align: left; + align-items: stretch; +} + +.downloads-rebuild-extras strong { + color: var(--card-fg-strong); + font-family: var(--ks-font); + font-weight: 400; + font-size: 1.05rem; +} + +.downloads-rebuild-extras span { + color: var(--card-fg-muted); + font-size: 0.88rem; + line-height: 1.5; +} + +.home-kinpaku .downloads-rebuild-extras span code { + background: transparent; + border: 0; + border-radius: 0; + padding: 0; + font-family: var(--ks-mono); + font-size: 0.84rem; + color: var(--card-fg-strong); +} + +.home-kinpaku .downloads-rebuild-extras a { + color: var(--card-link-color); + font-size: 0.88rem; + text-decoration: underline; + text-decoration-color: var(--card-link-underline); + text-underline-offset: 4px; + text-decoration-thickness: 1px; + width: fit-content; + transition: color 180ms var(--ks-ease); +} + +/* Push the primary link to the bottom of each cell so all three primary + links sit at the same Y. The secondary link follows naturally below. */ +.home-kinpaku .downloads-rebuild-extras a:not(.downloads-rebuild-link-secondary) { + margin-top: auto; +} + +.home-kinpaku .downloads-rebuild-extras a.downloads-rebuild-link-secondary { + color: var(--card-link-secondary); + text-decoration-color: var(--card-link-secondary); + margin-top: 4px; +} + +.home-kinpaku .downloads-rebuild-extras a.downloads-rebuild-link-secondary:hover { + color: var(--card-fg); + text-decoration-color: var(--card-fg); +} + +@media (max-width: 1080px) { + .downloads-rebuild { padding: 80px 0; } + .downloads-rebuild-extras { grid-template-columns: 1fr; } +} + +@media (max-width: 720px) { + .downloads-rebuild { padding: 64px 0; } + .downloads-rebuild-install { grid-template-columns: 1fr; } + .downloads-rebuild-examples li { grid-template-columns: 1fr; gap: 8px; } +} + +/* ============================================================================ + FOUNDATION CARD VISUALIZATIONS (varied per discipline) + Foundation cards use the original animated SVG icons (foundation-animations.js + restored). No extra chrome here. + ============================================================================ */ + +@media (prefers-reduced-motion: reduce) { + .home-kinpaku .slop-teaser-cta-primary { + transition: none; + } +} + +/* ============================================================================ + Section dividers — varied patterns from /design-system#dividers, so each + transition has its own personality without being noisy. All drawn as a + ::before pseudo on the section so no markup change required. + ============================================================================ */ + +.home-kinpaku .slop-teaser-section, +.home-kinpaku .live-section, +.home-kinpaku .language-section, +.home-kinpaku .downloads-rebuild { + position: relative; + /* Override stray border-tops applied by main.css → live-mode.css and any + other transitive imports — section dividers are drawn via ::before. */ + border-top: 0; +} + +/* The Hero → Slop dot-grid divider previously lived here as a ::before on + .slop-teaser-section. After the testimonials section was inserted between + hero and slop, that divider moved onto .testimonials-section in + testimonials.css. The slop section no longer needs a top divider — its + own padding and the testimonials section above carry the transition. */ + +/* Slop → Live, Live → Language, Language → Get Started: intentionally no + dividers. The plinth dot grid at the top of Testimonials and the verdigris + dot grid at the top of the footer bracket the section stack — interior + transitions read on layout alone. */ + + +/* ============================================================================ + HERO LIVE-MODE DEMO + The Live Mode loop (live-demo.js, #live-demo) now lives in the hero. Override + the section-03 frame proportions — the base frame is 16:9, too short for the + portrait card in the narrow hero column — and give all variants a single + card footprint so the pick-outline hugs them. + ============================================================================ */ + +.hero-live-demo { + display: block; /* override the .live-demo 2-col grid */ + width: 100%; +} + +.hero-live-demo .live-demo-frame { + aspect-ratio: auto; + /* Height follows content — fixed min was leaving ivory gap under clipped suites. */ + height: auto; + max-height: min(540px, calc(100svh - 140px)); + max-width: 100%; +} + +/* The stage holds a full (fake) website now, not a centered card. */ +.hero-live-demo .live-demo-stage { + display: block; + padding: 0; + height: auto; + min-height: 0; + overflow: hidden; +} + +/* Default .hotel-site is position:absolute; inset:0, which stretches to the tall + stage and paints ivory in the gap below clipped suites. */ +.hero-live-demo .hotel-site { + position: relative; + inset: auto; + height: auto; +} + +/* The picked element is the hero — a single, full-width, one-of-a-kind block. + Contained with side margins so the pick-outline hugs it inside the frame. */ +.hero-live-demo .live-demo-target.hotel-hero-pick { + align-self: stretch; + width: auto; + height: 214px; + min-height: 0; + margin: 16px 18px 0; +} +.hero-live-demo .live-demo-variant { + display: grid; + place-items: stretch; +} +.hero-live-demo .live-demo-variant > .hotel-hero { + width: 100%; + height: 100%; + box-sizing: border-box; +} +/* Suite cards run to the frame bottom; the gbar overlaps their lower edge. */ +.hero-live-demo .hotel-suites { + flex: none; + max-height: 132px; + padding: 10px 0 0 22px; + overflow: hidden; +} +.hero-live-demo .hotel-suites-row > .hotel-card { + width: 198px; + height: 226px; + box-sizing: border-box; + flex: none; +} + +/* The contextual bar's optional free-text input reads as a placeholder (in the + real picker you type here OR annotate; here the annotation is the input). */ +.hero-live-demo [data-demo-input-text]:empty::before { + content: "refine further…"; + opacity: 0.5; +} + +@media (max-width: 1080px) { + .hero-live-demo { max-width: 460px; margin-inline: auto; } +} + + +/* ---------------------------------------------------------------------------- + The page under the picker: AURELIA, a warm serif boutique-hotel site. + Deliberately the opposite of Impeccable's dark kinpaku aesthetic. + ---------------------------------------------------------------------------- */ +.hotel-site { + --h-ivory: oklch(96.5% 0.012 84); + --h-sand: oklch(92% 0.02 80); + --h-ink: oklch(31% 0.025 55); + --h-muted: oklch(50% 0.025 55); + --h-green: oklch(43% 0.055 150); + --h-line: oklch(82% 0.02 80); + --h-serif: "Cormorant Garamond", Georgia, serif; + --h-sans: var(--ks-font, system-ui, sans-serif); + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + background: var(--h-ivory); + color: var(--h-ink); + overflow: hidden; +} + +.hotel-nav { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 22px; + height: 44px; + flex: none; + border-bottom: 1px solid var(--h-line); +} +.hotel-logo { + font-family: var(--h-serif); + font-weight: 600; + font-size: 18px; + letter-spacing: 0.22em; + color: var(--h-ink); +} +.hotel-nav-links { + display: flex; + gap: 18px; + font-family: var(--h-sans); + font-size: 11px; + letter-spacing: 0.06em; + color: var(--h-muted); +} +.hotel-nav-book { + font-family: var(--h-sans); + font-size: 11px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--h-green); + border: 1px solid var(--h-green); + border-radius: 999px; + padding: 5px 13px; +} + +/* ---------------------------------------------------------------------------- + The hero = the picked element. One shared structure; the slop original is + generic + sans, the three elegant variants are the on-brand fixes. + ---------------------------------------------------------------------------- */ +.hotel-hero { + position: relative; + display: flex; + align-items: flex-end; + overflow: hidden; + border-radius: 12px; + box-sizing: border-box; +} +.hotel-hero-photo { + position: absolute; + inset: 0; + background: + linear-gradient(to top, oklch(24% 0.03 40 / 0.82), oklch(38% 0.04 40 / 0.12) 58%, transparent), + linear-gradient(150deg, oklch(66% 0.08 60), oklch(48% 0.08 34) 52%, oklch(36% 0.05 255)); +} +.hotel-hero-inner { + position: relative; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 6px; + padding: 22px 24px; +} +.hotel-hero-eyebrow { + margin: 0; + font-family: var(--h-sans); + font-size: 10px; + letter-spacing: 0.26em; + text-transform: uppercase; + color: oklch(91% 0.05 85); +} +.hotel-hero-title { + margin: 0; + font-family: var(--h-serif); + font-weight: 500; + font-size: 31px; + line-height: 1.05; + letter-spacing: -0.01em; + color: oklch(98% 0.01 85); +} +.hotel-hero-sub { + margin: 0; + font-family: var(--h-sans); + font-size: 12px; + color: oklch(89% 0.02 85); +} +.hotel-hero-cta { + margin-top: 5px; + font-family: var(--h-sans); + font-weight: 600; + font-size: 12px; + letter-spacing: 0.04em; + padding: 9px 18px; + border: 0; + border-radius: 6px; + background: var(--h-ivory); + color: var(--h-ink); + cursor: pointer; +} + +/* original — AI slop: flat, generic, sans, purple CTA. Clashes with the brand. */ +.hotel-hero--slop { + align-items: center; + background: oklch(95% 0.004 270); +} +.hotel-hero--slop .hotel-hero-photo { display: none; } +.hotel-hero--slop .hotel-hero-title { + font-family: "Inter", "Albert Sans", system-ui, sans-serif; + font-weight: 800; + font-size: 27px; + letter-spacing: -0.02em; + color: oklch(32% 0.02 285); +} +.hotel-hero--slop .hotel-hero-sub { color: oklch(56% 0.015 285); } +.hotel-hero--slop .hotel-hero-cta { + background: linear-gradient(135deg, oklch(62% 0.2 300), oklch(56% 0.21 268)); + color: oklch(98% 0.01 300); + border-radius: 8px; +} + +/* v1 — warm dusk, text bottom-left (uses base photo) */ +.hotel-hero--1 .hotel-hero-cta { background: oklch(98% 0.01 85); color: oklch(30% 0.03 40); } + +/* v2 — sea-toned, centered, outline CTA. justify-content centers the inner + block horizontally (the base hero is a flex row defaulting to flex-start). */ +.hotel-hero--2 { align-items: center; justify-content: center; text-align: center; } +.hotel-hero--2 .hotel-hero-photo { + background: + linear-gradient(to top, oklch(24% 0.03 220 / 0.82), oklch(38% 0.04 220 / 0.12) 58%, transparent), + linear-gradient(150deg, oklch(64% 0.08 205), oklch(46% 0.07 230) 55%, oklch(34% 0.05 255)); +} +.hotel-hero--2 .hotel-hero-inner { align-items: center; } +.hotel-hero--2 .hotel-hero-title { font-style: italic; } +.hotel-hero--2 .hotel-hero-cta { + background: transparent; + color: oklch(97% 0.01 85); + border: 1px solid oklch(97% 0.01 85 / 0.7); +} + +/* v3 — golden hour, gold CTA */ +.hotel-hero--3 .hotel-hero-photo { + background: + linear-gradient(to top, oklch(26% 0.04 50 / 0.78), oklch(42% 0.05 55 / 0.12) 58%, transparent), + linear-gradient(150deg, oklch(78% 0.1 75), oklch(60% 0.1 50) 52%, oklch(44% 0.07 38)); +} +.hotel-hero--3 .hotel-hero-cta { + background: var(--ks-kinpaku, oklch(84% 0.19 80)); + color: oklch(24% 0.03 60); +} + +.hotel-suites { + flex: 1; + padding: 16px 0 0 22px; /* no right padding so the row runs off the edge */ + overflow: hidden; +} +.hotel-suites-label { + margin: 0 0 11px; + font-family: var(--h-sans); + font-size: 10px; + letter-spacing: 0.24em; + text-transform: uppercase; + color: var(--h-muted); +} +/* Left-aligned row; the later suites run off the right edge (clipped by the + site), so the page reads as scrollable rather than a centered modal. */ +.hotel-suites-row { + display: flex; + gap: 16px; +} + +/* ---------------------------------------------------------------------------- + The room/rate card — one shared structure, four distinct treatments. + ---------------------------------------------------------------------------- */ +.hotel-card { + --c-surface: oklch(99% 0.008 84); + --c-ink: var(--h-ink); + --c-accent: var(--h-green); + display: flex; + flex-direction: column; + overflow: hidden; + border-radius: 10px; + background: var(--c-surface); + border: 1px solid var(--h-line); + box-shadow: 0 3px 10px oklch(40% 0.04 60 / 0.08); /* flat in layout, not a modal */ + font-family: var(--h-sans); + text-align: left; +} +.hotel-card-photo--b { + background: + radial-gradient(120% 90% at 30% 10%, oklch(88% 0.05 250 / 0.6), transparent 60%), + linear-gradient(150deg, oklch(70% 0.06 235), oklch(85% 0.05 90)); +} +.hotel-card-photo--c { + background: + radial-gradient(120% 90% at 60% 0%, oklch(90% 0.06 70 / 0.7), transparent 60%), + linear-gradient(150deg, oklch(76% 0.07 50), oklch(86% 0.05 110)); +} +.hotel-card-photo { + flex: none; + height: 104px; + background: + radial-gradient(120% 90% at 70% 10%, oklch(88% 0.06 95 / 0.7), transparent 60%), + linear-gradient(150deg, oklch(74% 0.05 160), oklch(86% 0.05 78)); +} +.hotel-card-body { + display: flex; + flex-direction: column; + gap: 4px; + flex: 1; + padding: 13px 15px 14px; +} +.hotel-card-kicker { + margin: 0; + font-size: 9px; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--c-accent); +} +.hotel-card-name { + margin: 0; + font-family: var(--h-serif); + font-weight: 600; + font-size: 23px; + line-height: 1.05; + color: var(--c-ink); +} +.hotel-card-meta { + margin: 0; + font-size: 11px; + color: var(--h-muted); +} +.hotel-card-foot { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-top: auto; + padding-top: 10px; +} +.hotel-card-price { + font-family: var(--h-serif); + font-weight: 600; + font-size: 20px; + color: var(--c-ink); +} +.hotel-card-price i { + font-style: normal; + font-family: var(--h-sans); + font-size: 10px; + color: var(--h-muted); +} +.hotel-card-btn { + flex: none; + font-family: var(--h-sans); + font-weight: 500; + font-size: 11px; + letter-spacing: 0.06em; + padding: 7px 14px; + border: 0; + border-radius: 6px; + background: var(--c-ink); + color: var(--h-ivory); + cursor: pointer; +} + +/* original — the AI-slop card: generic sans, purple gradient, overdone radius + + shadow, weak copy. Clashes on purpose with the serif siblings. */ +.hotel-card--slop { + font-family: "Inter", "Albert Sans", system-ui, sans-serif; + border-radius: 16px; + border-color: transparent; + box-shadow: 0 12px 30px oklch(52% 0.2 295 / 0.4); +} +.hotel-card--slop .hotel-card-photo { + background: linear-gradient(135deg, oklch(68% 0.2 300), oklch(60% 0.21 268)); +} +.hotel-card--slop .hotel-card-kicker { + font-size: 10px; + letter-spacing: 0.02em; + text-transform: none; + color: oklch(58% 0.2 290); +} +.hotel-card--slop .hotel-card-name { + font-family: inherit; + font-weight: 700; + font-size: 17px; + letter-spacing: -0.01em; + color: oklch(32% 0.02 290); +} +.hotel-card--slop .hotel-card-meta { color: oklch(62% 0.01 290); } +.hotel-card--slop .hotel-card-price { + font-family: inherit; + font-weight: 700; + font-size: 16px; + color: oklch(32% 0.02 290); +} +.hotel-card--slop .hotel-card-price i { font-family: inherit; } +.hotel-card--slop .hotel-card-btn { + background: linear-gradient(135deg, oklch(62% 0.2 300), oklch(56% 0.21 268)); + border-radius: 8px; + text-transform: none; + letter-spacing: 0; +} + +/* v1 — full-bleed dusk photo, body overlaid on a scrim */ +.hotel-card--1 { + position: relative; + border-color: transparent; +} +.hotel-card--1 .hotel-card-photo { + position: absolute; + inset: 0; + height: auto; + background: + linear-gradient(to top, oklch(22% 0.03 40 / 0.92), oklch(30% 0.04 40 / 0.15) 55%, transparent), + linear-gradient(155deg, oklch(64% 0.08 55), oklch(40% 0.06 28) 55%, oklch(32% 0.05 320)); +} +.hotel-card--1 .hotel-card-body { position: relative; justify-content: flex-end; } +.hotel-card--1 .hotel-card-kicker { color: oklch(88% 0.06 85); } +.hotel-card--1 .hotel-card-name, +.hotel-card--1 .hotel-card-price { color: oklch(97% 0.01 85); } +.hotel-card--1 .hotel-card-meta { color: oklch(86% 0.02 85); } +.hotel-card--1 .hotel-card-price i { color: oklch(86% 0.02 85); } +.hotel-card--1 .hotel-card-btn { + background: oklch(97% 0.01 85); + color: oklch(28% 0.03 40); +} + +/* v2 — editorial, no photo, type-forward */ +.hotel-card--2 { + background: var(--h-sand); + justify-content: center; + text-align: center; +} +.hotel-card--2 .hotel-card-photo { display: none; } +.hotel-card--2 .hotel-card-body { gap: 7px; padding: 22px 18px; align-items: center; } +.hotel-card--2 .hotel-card-name { font-size: 30px; font-style: italic; } +.hotel-card--2 .hotel-card-foot { + flex-direction: column; + gap: 10px; + border-top: 1px solid var(--h-line); + margin-top: 12px; +} +.hotel-card--2 .hotel-card-price { font-size: 26px; } +.hotel-card--2 .hotel-card-btn { + background: transparent; + color: var(--c-accent); + border-bottom: 1px solid currentColor; + border-radius: 0; + padding: 2px 0; +} + +/* v3 — framed, terracotta accent */ +.hotel-card--3 { + --c-accent: oklch(56% 0.13 42); + background: oklch(98% 0.012 70); + padding: 7px; + border-color: oklch(56% 0.13 42 / 0.4); +} +.hotel-card--3 .hotel-card-photo { + height: 88px; + border-radius: 5px; + background: + linear-gradient(150deg, oklch(72% 0.09 45), oklch(84% 0.06 70)); +} +.hotel-card--3 .hotel-card-name { color: oklch(34% 0.04 42); } +.hotel-card--3 .hotel-card-price { color: var(--c-accent); } +.hotel-card--3 .hotel-card-btn { background: var(--c-accent); } + + +/* ---------------------------------------------------------------------------- + Annotation layer: freehand circle + comment pin, drawn on the picked card. + The layer is sized/positioned onto the card by live-demo.js (positionAnnotations); + marks sit in element-local space, mirroring the real picker. + ---------------------------------------------------------------------------- */ +.hero-live-demo .live-demo-annotations { + position: absolute; + z-index: 4; /* above the card + outline, below the cursor (5) */ + pointer-events: none; + color: var(--ks-kinpaku); + transform: none; /* JS sets exact left/top/width/height onto the card */ + opacity: 0; + transition: opacity 200ms var(--ks-ease, ease); +} +.hero-live-demo .live-demo-annotations.is-visible { opacity: 1; } + +/* Circle stroke fills the card-sized layer (viewBox 0 0 100 100). The base rule + centers + offsets the stroke for the old layout — null that out here. */ +.hero-live-demo .live-demo-stroke { + position: absolute; + inset: 0; + left: 0; + top: 0; + width: 100%; + height: 100%; + transform: none; + overflow: visible; + color: var(--ks-kinpaku); +} +.hero-live-demo .live-demo-stroke path { + stroke-dasharray: 1; + stroke-dashoffset: 1; + filter: drop-shadow(0 1px 2px oklch(0% 0 0 / 0.35)); +} +/* Single circle around the off-brand button, drawn when the layer reveals. */ +.hero-live-demo .live-demo-annotations.is-visible .live-demo-stroke path { + animation: liveDemoStroke 620ms var(--ks-ease, ease) forwards; +} + +/* Comment pin: gold dot + note bubble, dropped near the button / suites edge. */ +.live-demo-pin { + position: absolute; + left: 46%; + top: 78%; + opacity: 0; + transform: scale(0.5); + transform-origin: center; + transition: opacity 160ms ease, transform 220ms cubic-bezier(0.22, 1, 0.36, 1); +} +.hero-live-demo .live-demo-annotations.is-pin-visible .live-demo-pin { + opacity: 1; + transform: scale(1); +} +.live-demo-pin-dot { + display: block; + width: 14px; + height: 14px; + border-radius: 50%; + background: var(--ks-kinpaku); + border: 2px solid oklch(98% 0.005 95); + box-shadow: 0 2px 7px oklch(0% 0 0 / 0.45); +} +.live-demo-pin-note { + position: absolute; + left: -3px; + bottom: 24px; + display: inline-flex; + align-items: center; + white-space: nowrap; + padding: 6px 10px; + border-radius: 7px; + background: oklch(19% 0.008 95); + border: 1px solid var(--ks-gold-hairline-strong, oklch(74% 0.09 82 / 0.6)); + color: oklch(94% 0.02 85); + font-family: var(--ks-font, system-ui, sans-serif); + font-size: 11.5px; + line-height: 1; + box-shadow: 0 8px 22px oklch(0% 0 0 / 0.4); + opacity: 0; + transform: translateY(3px); + transition: opacity 160ms ease, transform 160ms ease; +} +.hero-live-demo .live-demo-annotations.is-note-visible .live-demo-pin-note { + opacity: 1; + transform: none; +} +.live-demo-pin-caret { + display: inline-block; + width: 1px; + height: 12px; + margin-left: 1px; + background: var(--ks-kinpaku); + animation: liveDemoCaret 1s steps(1) infinite; +} +@keyframes liveDemoCaret { 50% { opacity: 0; } } + +/* CLEAR chip, top-right of the annotation overlay (authentic to the picker). */ +.live-demo-annot-clear { + position: absolute; + top: 6px; + right: 6px; + font-family: var(--ks-mono, monospace); + font-size: 8.5px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--ks-kinpaku); + background: oklch(8% 0.006 95 / 0.72); + border: 1px solid var(--ks-gold-hairline, oklch(58% 0.065 82 / 0.32)); + border-radius: 4px; + padding: 3px 7px; + opacity: 0; + transition: opacity 200ms ease; +} +.hero-live-demo .live-demo-annotations.is-visible .live-demo-annot-clear { opacity: 0.85; } + + +/* ============================================================================ + SECTION 03 (Live Mode) — how-it-works flow + audience tiles + The animated demo lives in the hero; this section explains how Live Mode wires + itself into your codebase, then sells it to each evaluator. Left-aligned blocks + below the (right-aligned) section header. + ============================================================================ */ + +/* --- How it works: 3 steps, each with a minimal glyph -------------------- */ +.live-how { + list-style: none; + margin: 48px 0 0; + padding: 0; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 18px; + text-align: left; +} +/* No outer card — the mini-screen below is the only framed element, so the + step reads as one clean unit instead of a box-in-a-box. */ +.live-how-step { + display: grid; + align-content: start; + gap: 13px; + padding: 18px; + border: 1px solid var(--ks-rule); + border-radius: 6px; + background: oklch(10% 0.006 95 / 0.45); +} +.live-how-step .live-how-n { margin-top: 4px; } +.live-how-n { + font-family: var(--ks-mono); + font-size: 0.72rem; + letter-spacing: 0.18em; + color: var(--ks-kinpaku); +} +.live-how-title { + margin: 0; + font-family: var(--ks-font-display); + font-weight: 600; + font-size: 1.18rem; + color: var(--ks-champagne); +} +.live-how-copy { + margin: 0; + font-size: 0.92rem; + line-height: 1.55; + color: var(--ks-muted); +} +.live-how-copy code { + font-family: var(--ks-mono); + font-size: 0.85em; + color: var(--ks-kinpaku); +} + +/* Each step's visual is a clean mini browser screen (chrome bar + screen), + echoing the picker still — not an abstract panel. */ +.live-how-glyph { + border-radius: 5px; + overflow: hidden; + background: oklch(6% 0.005 95); +} +.live-how-screen { + position: relative; + min-height: 104px; + padding: 16px; + display: grid; + align-content: center; + justify-items: start; + gap: 9px; +} +/* Step 1: terminal line + framework chips */ +.live-how-term { + font-family: var(--ks-mono); + font-size: 0.72rem; + color: var(--ks-champagne); +} +.live-how-term-prompt { color: var(--ks-kinpaku); } +.live-how-chips { + display: flex; + flex-wrap: wrap; + gap: 5px; +} +.live-how-chips span { + font-family: var(--ks-mono); + font-size: 0.6rem; + letter-spacing: 0.04em; + color: var(--ks-muted); + padding: 2px 7px; + border: 1px solid var(--ks-rule); + border-radius: 999px; +} +.live-how-chips span:first-child { + color: var(--ks-kinpaku); + border-color: oklch(74% 0.09 82 / 0.55); +} +/* Step 2: an element with a pick outline + an attached comment, echoing the + on-canvas picker from the hero loop. */ +.live-how-screen--pick { justify-items: center; align-content: start; } +.live-how-pick { + position: relative; + width: 122px; + height: 40px; + border-radius: 4px; + background: oklch(14% 0.006 95); + border: 2px solid var(--ks-kinpaku); + box-shadow: 0 0 0 4px oklch(78% 0.12 82 / 0.12); + display: grid; + place-items: center; +} +.live-how-pick-tag { + font-family: var(--ks-mono); + font-size: 0.62rem; + color: var(--ks-muted); +} +.live-how-pick-dot { + position: absolute; + right: -5px; + top: -5px; + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--ks-kinpaku); + border: 2px solid oklch(98% 0.005 95); +} +.live-how-note { + position: absolute; + top: calc(100% + 9px); + right: -14px; + white-space: nowrap; + font-family: var(--ks-mono); + font-size: 0.6rem; + color: var(--ks-champagne); + background: oklch(13% 0.006 95); + border: 1px solid var(--ks-rule); + border-left: 2px solid var(--ks-kinpaku); + border-radius: 3px; + padding: 4px 8px; +} +.live-how-note-caret { + position: absolute; + top: -4px; + left: 13px; + width: 7px; + height: 7px; + background: oklch(13% 0.006 95); + border-left: 1px solid var(--ks-rule); + border-top: 1px solid var(--ks-rule); + transform: rotate(45deg); +} +/* Step 3: the floating contextual bar at accept, plus the source write. */ +.live-how-screen--accept { justify-items: center; gap: 12px; } +.live-how-ctxbar { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 5px 6px 5px 10px; + border: 1px solid var(--ks-rule); + border-radius: 999px; + background: oklch(12% 0.006 95); + box-shadow: 0 6px 18px oklch(2% 0.004 95 / 0.45); + font-family: var(--ks-mono); + font-size: 0.62rem; +} +.live-how-ctx-nav { color: var(--ks-muted); } +.live-how-ctx-count { color: var(--ks-champagne); } +.live-how-ctx-div { width: 1px; height: 13px; background: var(--ks-rule); } +.live-how-ctx-accept { + padding: 3px 10px; + border-radius: 999px; + background: var(--ks-kinpaku); + color: oklch(16% 0.02 90); + font-weight: 600; +} +.live-how-wrote { + display: inline-flex; + align-items: center; + gap: 6px; + font-family: var(--ks-mono); + font-size: 0.62rem; + color: var(--ks-patina); +} +.live-how-wrote-check { font-size: 0.7rem; } + +/* --- Who it's for: three role tiles -------------------------------------- */ +.live-audience-eyebrow { + margin: 52px 0 22px; + font-family: var(--ks-mono); + font-size: 0.62rem; + letter-spacing: 0.26em; + text-transform: uppercase; + color: var(--ks-patina); +} +.live-audience { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 30px; + text-align: left; +} +.live-audience-tile { + padding: 16px 0 0; + border-top: 2px solid var(--ks-patina); +} +.live-audience-role { + display: block; + margin-bottom: 11px; + font-family: var(--ks-mono); + font-size: 0.66rem; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--ks-patina); +} +.live-audience-tile p { + margin: 0; + font-size: 0.92rem; + line-height: 1.55; + color: var(--ks-muted); +} + +@media (max-width: 920px) { + .live-how { grid-template-columns: 1fr; } + .live-audience { grid-template-columns: 1fr; } +} + diff --git a/site/styles/kinpaku-kit.css b/site/styles/kinpaku-kit.css new file mode 100644 index 000000000..a8c255974 --- /dev/null +++ b/site/styles/kinpaku-kit.css @@ -0,0 +1,1399 @@ +/* ============================================================================ + 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: 500; + 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: 600; + 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; + flex-direction: column; + align-items: stretch; + gap: 0; + margin: 12px calc(-1 * clamp(16px, 4vw, 24px)) -16px; + padding: 4px 0 6px; + background: oklch(4% 0.004 95); + border-top: 1px solid oklch(78% 0.12 82 / 0.18); + } + [data-site-header][data-nav-open="true"] .site-header-right { + display: flex; + } + + .kinpaku-chrome .site-header-nav { + 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 .site-header-github { + align-self: flex-start; + margin: 12px clamp(16px, 4vw, 24px) 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: 500; + 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 .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 var(--ks-rule); + 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: oklch(58% 0.065 82 / 0.22); + --color-charcoal: oklch(72% 0.025 82); + --color-ash: oklch(60% 0.018 82); + --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 Pinstripe) reads wrong 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). */ +.home-kinpaku .live-demo-chrome, +.live-mode-kinpaku .live-demo-chrome { + background: var(--ks-graphite); + border-bottom: 1px solid var(--ks-rule); +} + +.home-kinpaku .live-demo-dot, +.live-mode-kinpaku .live-demo-dot { + background: oklch(28% 0.01 95); +} + +.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 var(--ks-rule); +} + +/* 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); + border: 1.5px solid var(--ks-kinpaku); + box-shadow: + 0 0 0 1px oklch(78% 0.12 82 / 0.18), + 0 10px 28px oklch(0% 0 0 / 0.28); + 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 — soft kinpaku-dim pill with kinpaku text. The outer + gbar border carries the loud signal; the active button is the + quieter "currently selected" pill inside. */ +.home-kinpaku .live-demo-gbar-btn.is-active, +.live-mode-kinpaku .live-demo-gbar-btn.is-active { + background: oklch(78% 0.12 82 / 0.18); + color: var(--ks-kinpaku); +} + +/* 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: 88px; + margin: 0 4px 0 6px; + border-radius: 7px; + background: var(--ks-graphite); + border: 1px solid var(--ks-rule); + overflow: hidden; + cursor: pointer; + flex-shrink: 0; + transition: + width 0.28s cubic-bezier(0.22, 1, 0.36, 1), + 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, 42vw); + cursor: text; + border-color: oklch(78% 0.12 82 / 0.55); +} + +.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; + transition: opacity 0.15s ease; +} + +.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-pill, +.live-mode-kinpaku .live-demo-ctx-pill { + background: var(--ks-graphite-2); + border: 1px solid var(--ks-rule); + color: var(--ks-champagne); +} + +.home-kinpaku .live-demo-ctx-pill-caret, +.live-mode-kinpaku .live-demo-ctx-pill-caret { + color: var(--ks-text-muted); +} + +.home-kinpaku .live-demo-ctx-input, +.live-mode-kinpaku .live-demo-ctx-input { + background: var(--ks-graphite); + border: 1px solid var(--ks-rule); + color: var(--ks-champagne); +} + +.home-kinpaku .live-demo-ctx-caret, +.live-mode-kinpaku .live-demo-ctx-caret { + background: var(--ks-kinpaku); +} + +.home-kinpaku .live-demo-ctx-count, +.live-mode-kinpaku .live-demo-ctx-count { + background: var(--ks-graphite-2); + border: 1px solid var(--ks-rule); + color: var(--ks-text-muted); +} + +.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; +} + +.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); + color: var(--ks-kinpaku); + box-shadow: 0 0 0 4px oklch(78% 0.12 82 / 0.16); +} + +.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/site/styles/kinpaku-tokens.css b/site/styles/kinpaku-tokens.css new file mode 100644 index 000000000..609370d77 --- /dev/null +++ b/site/styles/kinpaku-tokens.css @@ -0,0 +1,126 @@ +/* + * 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 { + /* ============================================================ + 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. Warm champagne family. Body text reads at ≥0.92rem on + lacquer; smaller than that loses to the warm noise floor. + ============================================================ */ + --ks-champagne: oklch(84% 0.035 82); /* headlines, */ + --ks-text: oklch(81% 0.03 82); /* body */ + --ks-text-muted: oklch(63% 0.024 82); /* captions, meta */ + --ks-text-faint: oklch(52% 0.018 82); /* subdued */ + --ks-text-mute-deep: oklch(48% 0.018 82); /* disabled */ + + /* ============================================================ + Rules. Gold hairlines. Default is quiet; "strong" appears on + focus, active state, and structural anchors. + ============================================================ */ + --ks-rule: oklch(58% 0.065 82 / 0.32); + --ks-rule-strong: oklch(74% 0.09 82 / 0.6); + + /* ============================================================ + Typography — three faces. + - --ks-font-display: Alumni Sans Pinstripe. Pinstripe horizontal + strikes feel futuristic and editorial at display size; used for + the hero h1 and section h2s. Single-weight — font-weight is inert. + - --ks-font-wordmark: solid Alumni Sans, the weightable sibling of + the pinstripe. Same letterforms, but carries real weight in the + brand lockup where the hairline pinstripe reads too thin. + - --ks-font: Albert Sans. Quiet geometric humanist, used for body + and UI text. Pairs cleanly with the pinstripe display without + competing. + All loaded via Base.astro's Google Fonts link. + ============================================================ */ + --ks-font-display: "Alumni Sans Pinstripe", "Albert Sans", Arial, sans-serif; + --ks-font-wordmark: "Alumni Sans", "Alumni Sans Pinstripe", "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 (300) for elegance; section h2s read heavier (600) 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: 300; + --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: 600; + --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); +} diff --git a/site/styles/live-mode-kinpaku.css b/site/styles/live-mode-kinpaku.css new file mode 100644 index 000000000..76eb30849 --- /dev/null +++ b/site/styles/live-mode-kinpaku.css @@ -0,0 +1,645 @@ +/* ============================================================================ + /live-mode — Neo Kinpaku redesign + + Same page-scope pattern as docs-kinpaku.css and slop-kinpaku.css. The + interactive demo at the top of the page simulates a real dev server with + Impeccable's picker overlaid. The simulated user-app surface inside the + browser mock stays light (it represents any app, not Impeccable itself). + The Impeccable-branded picker bar at the bottom of the mock — which IS + Impeccable's UI — gets the kinpaku treatment. + + Sections of this file: + 1. Page scope + base + 2. Hero + 3. Demo frame chrome + Impeccable picker bar re-skin + 4. Stages (3-up "What happens") + 5. Pathways (3-up "Where next") + 6. Frameworks list + 7. Footer + ============================================================================ */ + +@import "./kinpaku-tokens.css"; + + +/* ============================================================================ + 1. PAGE SCOPE + BASE + ============================================================================ */ + +.live-mode-kinpaku { + /* Surface/text/accent/font tokens default to kinpaku at :root now; only + the --ks-muted alias and the page shell remain here. */ + --ks-muted: var(--ks-text-muted); + + background: + linear-gradient(180deg, oklch(7% 0.006 95), oklch(4% 0.004 95)); + color: var(--ks-text); + font-family: var(--ks-font); + min-height: 100vh; +} + +.live-mode-kinpaku a { + color: inherit; +} + +.live-mode-kinpaku .live-mode-page { + max-width: 1180px; + margin: 0 auto; + /* Tighter top so the auto-playing demo lands closer to the fold — the + demo is the page's main asset; preamble shouldn't push it away. */ + padding: clamp(32px, 4vw, 64px) clamp(22px, 4vw, 56px) clamp(80px, 10vw, 140px); +} + + +/* ============================================================================ + 2. HERO + ============================================================================ */ + +.live-mode-kinpaku .live-mode-page-header { + max-width: 760px; + /* Pulled in from clamp(64px, 8vw, 96px). The demo viewport carries its + own margin-bottom; we don't need a big gap on both sides. */ + margin: 0 0 clamp(32px, 4vw, 48px); +} + +.live-mode-kinpaku .live-mode-page-eyebrow { + display: flex; + align-items: center; + gap: 10px; + font-family: var(--ks-mono); + font-size: var(--ks-type-eyebrow-size); + letter-spacing: var(--ks-type-eyebrow-track); + text-transform: uppercase; + color: var(--ks-kinpaku); + margin: 0 0 18px; +} + +.live-mode-kinpaku .live-mode-page-eyebrow-badge { + display: inline-flex; + align-items: center; + font-family: var(--ks-mono); + font-size: 0.62rem; + letter-spacing: 0.18em; + padding: 2px 8px; + border: 1px solid oklch(70% 0.12 188 / 0.4); + border-radius: 2px; + color: var(--ks-patina); + background: oklch(70% 0.12 188 / 0.10); + text-transform: uppercase; +} + +.live-mode-kinpaku .live-mode-page-title { + font-family: var(--ks-font-display); + font-style: normal; + font-weight: var(--ks-type-display-weight); + font-size: var(--ks-type-display-size); + line-height: var(--ks-type-display-line); + letter-spacing: var(--ks-type-display-track); + color: var(--ks-champagne); + margin: 0 0 22px; + text-wrap: balance; +} + +.live-mode-kinpaku .live-mode-page-lede { + font-size: 1.1rem; + line-height: 1.65; + color: var(--ks-text); + margin: 0 0 22px; + max-width: 56ch; +} + +/* Alpha note — quiet inline footnote below the command pill, not a card. + Previously a bordered callout, but it ate vertical space above the + demo without earning it. Compact form keeps the disclaimer honest + without blocking the demo. */ +.live-mode-kinpaku .live-mode-page-alpha-note { + margin: 14px 0 0; + padding: 0; + background: transparent; + border: 0; + border-radius: 0; + font-size: 0.82rem; + line-height: 1.5; + color: var(--ks-text-muted); + max-width: 60ch; +} + +.live-mode-kinpaku .live-mode-page-alpha-note strong { + color: var(--ks-patina); + font-weight: 500; +} + +/* Start command pill — terminal-style, kinpaku-tinted. */ +.live-mode-kinpaku .live-mode-start { + display: inline-flex; + align-items: center; + gap: 14px; + padding: 12px 18px; + background: var(--ks-lacquer-deep); + border: 1px solid var(--ks-rule); + border-radius: 2px; + font-family: var(--ks-mono); +} + +.live-mode-kinpaku .live-mode-start-prompt { + color: var(--ks-kinpaku); + font-size: 0.92rem; +} + +.live-mode-kinpaku .live-mode-start-cmd { + color: var(--ks-champagne); + font-size: 0.92rem; + background: transparent; + border: 0; + padding: 0; +} + +.live-mode-kinpaku .live-mode-start-copy { + appearance: none; + background: transparent; + border: 0; + color: var(--ks-text-muted); + cursor: pointer; + display: grid; + place-items: center; + padding: 4px; + border-radius: 2px; + transition: color 160ms var(--ks-ease); +} + +.live-mode-kinpaku .live-mode-start-copy:hover { + color: var(--ks-kinpaku); +} + +.live-mode-kinpaku .live-mode-start-copy.is-copied { + color: var(--ks-kinpaku); +} + + +/* ============================================================================ + 3. INTERACTIVE DEMO — frame chrome + Impeccable picker bar re-skin + + The mock represents: a real user dev server (light "app" inside) with + Impeccable's picker bar (kinpaku) layered on top. Only the Impeccable + chrome flips dark; the simulated app stays cream so it reads as a + generic dev preview, not as Impeccable-themed. + ============================================================================ */ + +.live-mode-kinpaku .live-mode-demo-wrap { + margin-bottom: clamp(72px, 9vw, 110px); +} + +.live-mode-kinpaku .live-demo-frame { + background: var(--ks-lacquer-deep); + border: 1px solid var(--ks-rule); + border-radius: 4px; + overflow: hidden; + box-shadow: 0 32px 80px oklch(2% 0.004 95 / 0.55); +} + +/* All .live-demo-* picker chrome (chrome bar, gbar, ctx, outline, + annotations, comment, stroke) lives in kinpaku-kit.css under the + "Live picker (kinpaku theme)" section so the homepage and this page + share one source of truth. Only the page-specific bits below remain: + the demo frame surface and the simulated user-app stage. */ + +/* The simulated user-app stage in the middle of the mock previously + re-established light token values to read as "a generic light dev + preview", but on the dark page chrome that white slab between two + dark surfaces hurts the eye. Pull it into the kinpaku dark palette + so the demo reads as one coherent surface. The cost is the metaphor + shifts from "any user's app" to "a dark-themed user's app"; the + payoff is the page no longer flashes white at scroll. */ +.live-mode-kinpaku .live-demo-stage { + background: var(--ks-lacquer); + color: var(--ks-text); +} + +/* The three card variants in the cycling demo were authored for the old + magenta editorial brand: v1 italic-serif column, v2 brutalist ticket + on `--color-ink`, v3 pastel-pink card. None of those survive the + kinpaku remap (champagne reads as sand, pink reads off-brand, italic + display is banned by DESIGN.md). Re-skin each variant to feel like a + distinct design direction inside the kinpaku world. + + - v1: editorial — lacquer-raised with a kinpaku top rule, display + headline at weight 600. + - v2: brutalist ticket — deeper lacquer with a chunky kinpaku + side-bar (rendered via ::before, not a CSS side-stripe accent on a + card), all-caps body face, kinpaku CTA. + - v3: soft — slightly tinted lacquer with a hairline kinpaku border, + body-face heading. + + The cycling animation also temporarily applies these as `.is-active` + variants; the kinpaku styling holds in both static and animated states. */ + +.live-mode-kinpaku .live-demo-card--v1, +.live-mode-kinpaku .live-demo-card--v2, +.live-mode-kinpaku .live-demo-card--v3 { + color: var(--ks-text); +} + +.live-mode-kinpaku .live-demo-card--v1 { + background: var(--ks-lacquer-raised); + border: 0; + border-top: 3px solid var(--ks-kinpaku); + border-radius: 0; +} + +.live-mode-kinpaku .live-demo-card--v1 .live-demo-card-kicker { + color: var(--ks-kinpaku); +} + +.live-mode-kinpaku .live-demo-card--v1 h3 { + font-family: var(--ks-font-display); + font-style: normal; + font-weight: 600; + color: var(--ks-champagne); +} + +.live-mode-kinpaku .live-demo-card--v1 h3 em { + color: var(--ks-kinpaku); + font-style: normal; +} + +.live-mode-kinpaku .live-demo-card--v1 p { + font-family: var(--ks-font); + font-style: normal; + color: var(--ks-text-muted); +} + +.live-mode-kinpaku .live-demo-card--v1 button { + background: transparent; + color: var(--ks-kinpaku); + border-bottom: 1.5px solid var(--ks-kinpaku); + border-radius: 0; +} + +.live-mode-kinpaku .live-demo-card--v2 { + background: var(--ks-lacquer-deep); + color: var(--ks-champagne); + border: 0; + border-radius: 0; +} + +.live-mode-kinpaku .live-demo-card--v2::before { + background: var(--ks-kinpaku); +} + +.live-mode-kinpaku .live-demo-card--v2 .live-demo-card-kicker, +.live-mode-kinpaku .live-demo-card--v2 .live-demo-card-stamp { + color: var(--ks-kinpaku); +} + +.live-mode-kinpaku .live-demo-card--v2 h3 { + font-family: var(--ks-font); + font-style: normal; + font-weight: 600; + color: var(--ks-champagne); +} + +.live-mode-kinpaku .live-demo-card--v2 button { + background: var(--ks-kinpaku); + color: var(--ks-lacquer-deep); +} + +.live-mode-kinpaku .live-demo-card--v3 { + background: oklch(11% 0.012 82); + border: 1px solid var(--ks-rule); + color: var(--ks-text); +} + +.live-mode-kinpaku .live-demo-card--v3 .live-demo-card-kicker { + color: var(--ks-kinpaku); +} + +.live-mode-kinpaku .live-demo-card--v3 h3 { + font-family: var(--ks-font); + font-style: normal; + font-weight: 600; + color: var(--ks-champagne); +} + +.live-mode-kinpaku .live-demo-card--v3 h3 em { + color: var(--ks-kinpaku); + font-style: normal; +} + +.live-mode-kinpaku .live-demo-card--v3 button { + background: transparent; + color: var(--ks-kinpaku); + border: 1px solid var(--ks-kinpaku); + border-radius: 2px; +} + +.live-mode-kinpaku .live-mode-demo-caption { + margin: 18px 0 0; + font-size: 0.92rem; + line-height: 1.55; + color: var(--ks-text-muted); + font-style: normal; +} + +.live-mode-kinpaku .live-mode-demo-caption code { + font-family: var(--ks-mono); + font-size: 0.86em; + color: var(--ks-champagne); + background: var(--ks-graphite-2); + border: 1px solid var(--ks-rule); + padding: 1px 6px; + border-radius: 2px; +} + + +/* ============================================================================ + 4. STAGES — 3-up "What happens, in three moves" + ============================================================================ */ + +.live-mode-kinpaku .live-mode-stages { + margin-bottom: clamp(72px, 9vw, 110px); +} + +.live-mode-kinpaku .live-mode-stages-title, +.live-mode-kinpaku .live-mode-pathways-title { + font-family: var(--ks-font-display); + font-style: normal; + font-weight: var(--ks-type-headline-weight); + font-size: var(--ks-type-headline-size); + line-height: var(--ks-type-headline-line); + letter-spacing: -0.005em; + color: var(--ks-champagne); + margin: 0 0 28px; + text-wrap: balance; +} + +.live-mode-kinpaku .live-mode-stages-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 16px; +} + +@media (max-width: 880px) { + .live-mode-kinpaku .live-mode-stages-grid { + grid-template-columns: 1fr; + } +} + +/* Flex column so the viz band can stick to the bottom (margin-top: auto) + and all three cards align their viz bands at the same baseline, + regardless of how many lines of body copy each one has. */ +.live-mode-kinpaku .live-mode-stage { + display: flex; + flex-direction: column; + gap: 12px; + padding: 24px 24px 0; + background: var(--ks-lacquer-raised); + border: 1px solid var(--ks-rule); + border-radius: 2px; + overflow: hidden; +} + +/* Numbered markers are allowed here — one deliberate sequence on a single + page is not the saturated "every section has 01/02/03/04 numbers" tell. + See the relaxed wording in DESIGN.md §7. Restyle as a mono caps + eyebrow in kinpaku, paired with the kinpaku flow shape. */ +.live-mode-kinpaku .live-mode-stage-num { + display: block; + font-family: var(--ks-mono); + font-size: 0.7rem; + letter-spacing: 0.22em; + text-transform: uppercase; + color: var(--ks-kinpaku); + margin: 0; +} + +.live-mode-kinpaku .live-mode-stage-name { + font-family: var(--ks-font); + font-style: normal; + font-size: 1.1rem; + font-weight: 600; + line-height: 1.25; + color: var(--ks-champagne); + margin: 0; +} + +.live-mode-kinpaku .live-mode-stage-desc { + font-size: 0.92rem; + line-height: 1.55; + color: var(--ks-text); + margin: 0; +} + +/* Stage viz — a dark inset band that anchors to the bottom of the card. + `margin-top: auto` (with the parent flex column) pushes it down so all + three cards align their viz at the same baseline. Fixed height so the + bands are visually equal regardless of mock content size. */ +.live-mode-kinpaku .live-mode-stage-viz { + margin: 14px -24px 0; + margin-top: auto; + padding: 20px 24px; + background: var(--ks-lacquer-deep); + border-top: 1px solid var(--ks-rule); + height: 132px; + display: flex; + align-items: center; + justify-content: center; + /* No border-radius — the card's overflow:hidden + 2px corner radius is + enough; the band should look flush against the card sides. */ +} + +/* Re-skin the docs-viz-* mini-mocks inside the viz band for the dark + kinpaku surface. docs-visuals.css gives them light-theme backgrounds + and borders that don't read on lacquer. Match the patterns used in + /docs and /slop so the same mock vocabulary feels consistent. + + Picker-target pin (PICK card) — kinpaku outline on lacquer. */ +.live-mode-kinpaku .live-mode-stage-viz .docs-viz-picker-row { + background: transparent; + border: 0; + padding: 0; +} + +.live-mode-kinpaku .live-mode-stage-viz .docs-viz-picker-target { + background: var(--ks-lacquer-raised); + border: 1.5px solid var(--ks-kinpaku); + color: var(--ks-champagne); + /* Pinstripe display font loses legibility below ~1.2rem (Two-Face Rule). + The mock renders at 12px, so force the body face here. */ + font-family: var(--ks-font); + font-style: normal; + font-weight: 500; + box-shadow: 0 0 0 4px oklch(78% 0.12 82 / 0.16); +} + +.live-mode-kinpaku .live-mode-stage-viz .docs-viz-picker-pin { + background: var(--ks-kinpaku); + color: var(--ks-lacquer-deep); + box-shadow: none; +} + +/* Three-up variants strip (GENERATE card) — lacquer tiles, active gets + the kinpaku-tinted bg. */ +.live-mode-kinpaku .live-mode-stage-viz .docs-viz-variant { + background: var(--ks-lacquer-raised); + border: 1px solid var(--ks-rule); + color: var(--ks-text-muted); + box-shadow: none; +} + +.live-mode-kinpaku .live-mode-stage-viz .docs-viz-variant.is-active { + background: oklch(78% 0.12 82 / 0.16); + border-color: var(--ks-kinpaku); + box-shadow: none; +} + +.live-mode-kinpaku .live-mode-stage-viz .docs-viz-variant-kicker { + color: var(--ks-text-muted); +} + +.live-mode-kinpaku .live-mode-stage-viz .docs-viz-variant.is-active .docs-viz-variant-kicker { + color: var(--ks-kinpaku); +} + +/* Accept pill (ACCEPT card) — kinpaku gold ink on a subtle kinpaku wash. */ +.live-mode-kinpaku .live-mode-stage-viz .docs-viz-accept-pill { + background: oklch(78% 0.12 82 / 0.16); + border: 1px solid var(--ks-kinpaku); + color: var(--ks-kinpaku); +} + + +/* ============================================================================ + 5. PATHWAYS — 3-up "Where next" + ============================================================================ */ + +.live-mode-kinpaku .live-mode-pathways { + margin-bottom: clamp(56px, 7vw, 88px); +} + +.live-mode-kinpaku .live-mode-pathways-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 16px; +} + +@media (max-width: 880px) { + .live-mode-kinpaku .live-mode-pathways-grid { + grid-template-columns: 1fr; + } +} + +.live-mode-kinpaku .live-mode-pathway { + display: grid; + align-content: start; + gap: 10px; + padding: 28px 28px 32px; + background: var(--ks-lacquer-raised); + border: 1px solid var(--ks-rule); + border-radius: 2px; + text-decoration: none; + color: inherit; + transition: border-color 180ms var(--ks-ease), + background 180ms var(--ks-ease); +} + +.live-mode-kinpaku .live-mode-pathway:hover { + border-color: var(--ks-kinpaku); + background: oklch(13% 0.012 82); +} + +.live-mode-kinpaku .live-mode-pathway-kind { + font-family: var(--ks-mono); + font-size: 0.7rem; + letter-spacing: 0.22em; + text-transform: uppercase; + color: var(--ks-kinpaku); + margin: 0; +} + +.live-mode-kinpaku .live-mode-pathway-title { + font-family: var(--ks-font); + font-style: normal; + font-size: 1.1rem; + font-weight: 600; + line-height: 1.25; + color: var(--ks-champagne); + margin: 0; +} + +.live-mode-kinpaku .live-mode-pathway-desc { + font-size: 0.92rem; + line-height: 1.55; + color: var(--ks-text-muted); + margin: 0; +} + +.live-mode-kinpaku .live-mode-pathway-desc code { + font-family: var(--ks-mono); + font-size: 0.86em; + color: var(--ks-champagne); + background: var(--ks-graphite-2); + border: 1px solid var(--ks-rule); + padding: 1px 6px; + border-radius: 2px; +} + +.live-mode-kinpaku .live-mode-pathway-cta { + margin-top: 4px; + font-family: var(--ks-mono); + font-size: 0.78rem; + color: var(--ks-kinpaku); +} + + +/* ============================================================================ + 6. FRAMEWORKS + ============================================================================ */ + +/* docs-visuals.css sets `background: cream; border: 1px solid mist; + border-radius: 8px; padding: 18px 20px` here. My previous override only + touched border-top + zeroed padding, leaving the inherited box border + in place with no inset — text and pills landed flush against the + border. Strip the entire box: background, all sides of the border, + and the radius. Keep just a top hairline rule as a separator. */ +.live-mode-kinpaku .live-mode-frameworks { + padding: 28px 0 0; + background: transparent; + border: 0; + border-top: 1px solid var(--ks-rule); + border-radius: 0; +} + +.live-mode-kinpaku .live-mode-frameworks-label { + display: block; + font-family: var(--ks-mono); + font-size: 0.7rem; + letter-spacing: 0.22em; + text-transform: uppercase; + color: var(--ks-kinpaku); + margin: 0 0 14px; +} + +.live-mode-kinpaku .live-mode-frameworks-list { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.live-mode-kinpaku .live-mode-frameworks-list li { + font-family: var(--ks-mono); + font-size: 0.82rem; + color: var(--ks-champagne); + padding: 6px 12px; + background: var(--ks-graphite-2); + border: 1px solid var(--ks-rule); + border-radius: 2px; +} + + +/* ============================================================================ + 7. FOOTER on dark surface + ============================================================================ */ + +/* Footer chrome moved to kinpaku-kit.css (.kinpaku-chrome .site-footer). */ diff --git a/site/styles/live-mode.css b/site/styles/live-mode.css index c15c484d2..60157bb4c 100644 --- a/site/styles/live-mode.css +++ b/site/styles/live-mode.css @@ -251,43 +251,91 @@ 40% { transform: scale(0.78); } } -/* Global bar — the persistent dark pill at the bottom of every session. */ +/* Global bar — mirrors live-browser.js initGlobalBar (brand + inner toggles). */ .live-demo-gbar { position: absolute; bottom: 14px; left: 50%; transform: translateX(-50%); + display: flex; + align-items: stretch; + gap: 0; + width: max-content; + max-width: calc(100% - 24px); + overflow: hidden; + box-sizing: border-box; min-height: 36px; - padding: 0 4px; + padding: 0; background: oklch(14% 0 0); color: oklch(92% 0 0); border: 1px solid oklch(22% 0 0); border-radius: 10px; - display: flex; - align-items: center; - gap: 2px; font-family: var(--font-body); font-size: 12px; + line-height: 1; box-shadow: 0 8px 24px oklch(0% 0 0 / 0.2); z-index: 4; } .live-demo-gbar-brand { - font-family: var(--font-display); - font-size: 16px; - color: var(--color-accent); - padding: 0 10px; -} -.live-demo-gbar-btn { display: inline-flex; align-items: center; - gap: 6px; - padding: 7px 10px; + justify-content: center; + align-self: stretch; + flex-shrink: 0; + padding: 0 6px 0 14px; + color: var(--color-accent); +} +.live-demo-gbar-brand-mark { + display: block; +} +.live-demo-gbar-inner { + display: flex; + align-items: center; + gap: 2px; + flex-shrink: 0; + padding: 4px 6px 4px 2px; +} +.live-demo-gbar-btn { + position: relative; + display: inline-flex; + align-items: center; + flex-shrink: 0; + padding: 6px 8px; background: transparent; border: 0; border-radius: 7px; color: oklch(75% 0 0); font: inherit; + font-size: 11.5px; + font-weight: 500; + line-height: 1; + white-space: nowrap; + overflow: hidden; cursor: pointer; + transition: background 0.15s ease, color 0.15s ease; +} +.live-demo-gbar-btn svg { + flex-shrink: 0; +} +.live-demo-gbar-btn-label { + display: inline-block; + max-width: 0; + opacity: 0; + margin-left: 0; + overflow: hidden; + transition: + max-width 0.25s cubic-bezier(0.22, 1, 0.36, 1), + opacity 0.2s ease, + margin-left 0.25s cubic-bezier(0.22, 1, 0.36, 1); +} +.live-demo-gbar-btn-label--mono { + font-family: var(--font-mono); +} +.live-demo-gbar:hover .live-demo-gbar-btn:not(.is-active) .live-demo-gbar-btn-label, +.live-demo-gbar-btn.is-active .live-demo-gbar-btn-label { + max-width: 120px; + opacity: 1; + margin-left: 6px; } .live-demo-gbar-btn.is-active { background: var(--color-accent-dim); @@ -295,30 +343,40 @@ } .live-demo-gbar-dmd { display: inline-grid; - grid-template: repeat(2, 1fr) / repeat(2, 1fr); - width: 12px; - height: 12px; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr; + width: 14px; + height: 14px; + flex-shrink: 0; border-radius: 3px; overflow: hidden; + box-shadow: inset 0 0 0 1px oklch(58% 0.065 82 / 0.55); } -.live-demo-gbar-dmd span:nth-child(1) { background: oklch(60% 0.25 350); } -.live-demo-gbar-dmd span:nth-child(2) { background: oklch(60% 0.15 45); } -.live-demo-gbar-dmd span:nth-child(3) { background: oklch(55% 0.12 250); } -.live-demo-gbar-dmd span:nth-child(4) { background: oklch(30% 0 0); } -.live-demo-gbar-divider { - width: 1px; - height: 18px; - background: oklch(28% 0 0); - margin: 0 4px; -} +.live-demo-gbar-dmd span:nth-child(1) { background: oklch(84% 0.19 80.46); } +.live-demo-gbar-dmd span:nth-child(2) { background: oklch(70% 0.12 188); } +.live-demo-gbar-dmd span:nth-child(3) { background: oklch(84% 0.035 82); } +.live-demo-gbar-dmd span:nth-child(4) { background: oklch(34% 0.014 82); } .live-demo-gbar-x { - padding: 7px 10px; + display: inline-flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + flex-shrink: 0; + align-self: center; + width: 24px; + height: 24px; + padding: 0; background: transparent; border: 0; - border-radius: 7px; + border-radius: 6px; color: oklch(60% 0 0); - font-size: 13px; + font-size: 0; + line-height: 0; cursor: pointer; + transition: color 0.12s ease, background 0.12s ease; +} +.live-demo-gbar-x:hover { + color: oklch(58% 0.15 35); } /* Contextual bar — LIGHT paper-backed pill that floats near the picked diff --git a/site/styles/main.css b/site/styles/main.css index 581e8fc90..2e0c8d4c5 100644 --- a/site/styles/main.css +++ b/site/styles/main.css @@ -392,7 +392,6 @@ code { font-family: var(--font-display); font-size: clamp(2.5rem, 7vw, 4.5rem); font-weight: 300; - font-style: italic; line-height: 1; letter-spacing: -0.02em; margin: 0; @@ -403,7 +402,6 @@ code { font-family: var(--font-display); font-size: clamp(1.125rem, 2.5vw, 1.75rem); font-weight: 400; - font-style: italic; line-height: 1.3; margin: 0; color: var(--color-charcoal); @@ -807,7 +805,6 @@ code { font-family: var(--font-display); font-size: 1.75rem; font-weight: 300; - font-style: italic; color: var(--color-ink); margin-bottom: var(--spacing-sm); line-height: 1.1; @@ -1552,7 +1549,6 @@ code { padding: var(--spacing-xl); text-align: center; color: var(--color-ash); - font-style: italic; } /* ============================================ @@ -2828,7 +2824,6 @@ code { .install-primary-howto .install-path-desc em { font-family: var(--font-display); - font-style: italic; font-weight: 500; color: var(--color-accent); } @@ -2905,7 +2900,6 @@ code { font-family: var(--font-display); font-size: 1.75rem; font-weight: 400; - font-style: italic; color: var(--color-accent); line-height: 1; letter-spacing: -0.02em; @@ -3320,7 +3314,6 @@ code { font-size: 0.8125rem; color: var(--color-ash); margin: 0 0 var(--spacing-sm); - font-style: italic; } .install-path-title { @@ -4130,7 +4123,6 @@ a.install-updated-ref:hover { .consulting-title { font-size: clamp(1.5rem, 4vw, 2rem); font-weight: 300; - font-style: italic; margin: 0 0 var(--spacing-sm) 0; } diff --git a/site/styles/slop-kinpaku.css b/site/styles/slop-kinpaku.css new file mode 100644 index 000000000..e3571b2af --- /dev/null +++ b/site/styles/slop-kinpaku.css @@ -0,0 +1,965 @@ +/* ============================================================================ + /slop — Neo Kinpaku redesign + + Re-skins the /slop page (anti-pattern catalog + detection overlay demo) on + the dark lacquer kinpaku surface. Same scoping pattern as docs-kinpaku.css: + a body class (.slop-kinpaku) remaps the generic light tokens so embedded + visualizations inherit dark surfaces, then per-component styles use the + kinpaku tokens directly. + + The page shows AI antipattern specimens (purple gradients, etc.) — those + previews must stay light/colorful because that IS the demonstration. Only + the page chrome around them flips to lacquer. + + Sections of this file mirror the page: + 1. Page scope + base + 2. Sidebar + 3. Hero + 4. Shared section scaffolding (eyebrows, h2s) + 5. See it (era toggle + iframe demo) + 6. Try it live (specimen gallery) + 7. The catalog (37 rules in 8 categories, plinth-of-tiles per category) + 8. Run it yourself (3-up methods) + 9. Footer + ============================================================================ */ + +@import "./kinpaku-tokens.css"; + + +/* ============================================================================ + 1. PAGE SCOPE + BASE + ============================================================================ */ + +.slop-kinpaku { + /* Surface/text/accent/font tokens default to kinpaku at :root now; only + the --ks-muted alias and the page shell remain here. The AI-slop + specimens keep their own explicit light/colorful styling — that's the + demonstration, and it's unaffected by the token default. */ + --ks-muted: var(--ks-text-muted); + + background: + linear-gradient(180deg, oklch(7% 0.006 95), oklch(4% 0.004 95)); + color: var(--ks-text); + font-family: var(--ks-font); + min-height: 100vh; +} + +.slop-kinpaku a { + color: inherit; +} + +/* The skills-layout container is shared with /docs. Re-declare the kinpaku + layout to match: 240px sidebar + flexible main, max-width 1500, centered. */ +.slop-kinpaku.skills-layout-page main#main { + max-width: none; + margin: 0; + padding: 0; +} + +.slop-kinpaku .skills-layout { + display: grid; + grid-template-columns: 240px minmax(0, 1fr); + gap: clamp(40px, 4vw, 72px); + padding: 0 clamp(22px, 4vw, 56px); + align-items: start; + max-width: 1500px; + margin: 0 auto; +} + +@media (max-width: 960px) { + .slop-kinpaku .skills-layout { + grid-template-columns: 1fr; + gap: 18px; + } +} + +.slop-kinpaku .skills-main { + min-width: 0; + padding: 48px 0 clamp(80px, 10vw, 140px); +} + +.slop-kinpaku .anti-patterns-content { + max-width: none; +} + + +/* ============================================================================ + 2. SIDEBAR + ============================================================================ */ + +.slop-kinpaku .skills-sidebar { + position: sticky; + top: 86px; + align-self: start; + max-height: calc(100vh - 86px); + overflow-y: auto; + padding: 48px 0 64px; + border-right: 1px solid var(--ks-rule); + scrollbar-width: thin; + scrollbar-color: var(--ks-rule) transparent; +} + +@media (min-width: 961px) { + .slop-kinpaku .skills-sidebar { + min-height: calc(100vh - 86px); + } +} + +.slop-kinpaku .skills-sidebar::-webkit-scrollbar { + width: 6px; +} + +.slop-kinpaku .skills-sidebar::-webkit-scrollbar-thumb { + background: var(--ks-rule); + border-radius: 3px; +} + +.slop-kinpaku .skills-sidebar-inner { + padding-right: 22px; +} + +.slop-kinpaku .skills-sidebar-toggle { + display: none; +} + +.slop-kinpaku .skills-sidebar-label { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} + +.slop-kinpaku .skills-sidebar-list { + list-style: none; + padding: 0; + margin: 0; +} + +.slop-kinpaku .skills-sidebar-list a { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 8px; + padding: 5px 0 5px 12px; + border-left: 2px solid transparent; + font-family: var(--ks-mono); + font-size: 0.86rem; + font-weight: 400; + line-height: 1.5; + color: var(--ks-text); + text-decoration: none; + transition: color 160ms var(--ks-ease), + border-color 160ms var(--ks-ease); +} + +.slop-kinpaku .skills-sidebar-list a:hover { + color: var(--ks-kinpaku); +} + +.slop-kinpaku .skills-sidebar-list a[aria-current="true"] { + color: var(--ks-kinpaku); + border-left-color: var(--ks-kinpaku); +} + +.slop-kinpaku .anti-patterns-sidebar-count { + font-family: var(--ks-mono); + font-size: 0.72rem; + color: var(--ks-text-faint); +} + +.slop-kinpaku .slop-sidebar-sublist { + list-style: none; + padding: 4px 0 0 14px; + margin: 0; + display: grid; + gap: 2px; +} + +.slop-kinpaku .slop-sidebar-sublist a { + font-size: 0.8rem; + padding-left: 10px; + color: var(--ks-text-muted); +} + +@media (max-width: 960px) { + .slop-kinpaku .skills-sidebar { + position: static; + max-height: none; + min-height: 0; + overflow: visible; + padding: 18px 0 0; + border-right: 0; + border-bottom: 1px solid var(--ks-rule); + } + + .slop-kinpaku .skills-sidebar-toggle { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 14px 0; + background: transparent; + border: 0; + color: var(--ks-champagne); + font-family: var(--ks-mono); + font-size: 0.72rem; + letter-spacing: 0.22em; + text-transform: uppercase; + cursor: pointer; + } + + .slop-kinpaku .skills-sidebar-toggle-chevron { + transition: transform 220ms var(--ks-ease); + color: var(--ks-kinpaku); + flex-shrink: 0; + } + + .slop-kinpaku .skills-sidebar-toggle[aria-expanded="true"] .skills-sidebar-toggle-chevron { + transform: rotate(180deg); + } + + .slop-kinpaku .skills-sidebar-inner { + display: none; + padding: 14px 0 24px; + } + + .slop-kinpaku .skills-sidebar-toggle[aria-expanded="true"] + .skills-sidebar-inner { + display: block; + } +} + + +/* ============================================================================ + 3. HERO + ============================================================================ */ + +.slop-kinpaku .slop-header { + margin-bottom: clamp(72px, 9vw, 110px); + max-width: 760px; +} + +.slop-kinpaku .sub-page-eyebrow { + display: block; + font-family: var(--ks-mono); + font-size: var(--ks-type-eyebrow-size); + letter-spacing: var(--ks-type-eyebrow-track); + text-transform: uppercase; + color: var(--ks-kinpaku); + margin: 0 0 18px; +} + +/* Hero h1 follows the Weight-Inversion Rule: display scale at weight 300. */ +.slop-kinpaku .sub-page-title { + font-family: var(--ks-font-display); + font-style: normal; + font-weight: var(--ks-type-display-weight); + font-size: var(--ks-type-display-size); + line-height: var(--ks-type-display-line); + letter-spacing: var(--ks-type-display-track); + color: var(--ks-champagne); + margin: 0 0 22px; + text-wrap: balance; +} + +.slop-kinpaku .sub-page-lede { + font-size: 1.1rem; + line-height: 1.65; + color: var(--ks-text); + margin: 0; + max-width: 60ch; +} + +.slop-kinpaku .sub-page-lede code { + font-family: var(--ks-mono); + font-size: 0.86em; + color: var(--ks-champagne); + background: var(--ks-graphite-2); + border: 1px solid var(--ks-rule); + padding: 1px 7px; + border-radius: 2px; +} + +.slop-kinpaku .sub-page-lede a { + color: var(--ks-kinpaku); + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 4px; + text-decoration-color: oklch(78% 0.12 82 / 0.4); + transition: text-decoration-color 160ms var(--ks-ease); +} + +.slop-kinpaku .sub-page-lede a:hover { + text-decoration-color: var(--ks-kinpaku); +} + + +/* ============================================================================ + 4. SHARED SECTION SCAFFOLDING + ============================================================================ */ + +.slop-kinpaku .slop-section { + margin-bottom: clamp(72px, 9vw, 120px); +} + +.slop-kinpaku .slop-section:last-child { + margin-bottom: 0; +} + +.slop-kinpaku .slop-section-heading { + font-family: var(--ks-font-display); + font-style: normal; + font-weight: var(--ks-type-headline-weight); + font-size: var(--ks-type-headline-size); + line-height: var(--ks-type-headline-line); + letter-spacing: -0.005em; + color: var(--ks-champagne); + margin: 0 0 18px; + padding-bottom: 14px; + border-bottom: 1px solid var(--ks-rule); + text-wrap: balance; +} + + +/* ============================================================================ + 5. SEE IT — era toggle + iframe demo + ============================================================================ */ + +.slop-kinpaku .slop-then-now-intro { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: clamp(24px, 4vw, 56px); + margin: 0 0 22px; +} + +@media (max-width: 720px) { + .slop-kinpaku .slop-then-now-intro { + flex-direction: column; + gap: 18px; + } +} + +.slop-kinpaku .slop-then-now-lede { + font-size: 1rem; + line-height: 1.6; + color: var(--ks-text); + margin: 0; + max-width: 52ch; +} + +/* Era toggle — segmented control. Kinpaku-active, graphite-rest. */ +.slop-kinpaku .slop-era-toggle { + display: inline-flex; + padding: 4px; + background: var(--ks-graphite-2); + border: 1px solid var(--ks-rule); + border-radius: 2px; + flex-shrink: 0; +} + +.slop-kinpaku .slop-era-tab { + appearance: none; + border: 0; + background: transparent; + color: var(--ks-text-muted); + font-family: var(--ks-mono); + font-size: 0.78rem; + letter-spacing: 0.16em; + text-transform: uppercase; + padding: 8px 18px; + cursor: pointer; + border-radius: 2px; + transition: background 160ms var(--ks-ease), + color 160ms var(--ks-ease); +} + +.slop-kinpaku .slop-era-tab:hover { + color: var(--ks-champagne); +} + +.slop-kinpaku .slop-era-tab.is-active { + background: var(--ks-kinpaku); + color: var(--ks-lacquer-deep); +} + +/* Browser frame mock around the iframe. Lacquer-deep shell with kinpaku + chrome bar, matching the docs-kinpaku live-mode mock vocabulary. */ +.slop-kinpaku .visual-mode-preview { + background: var(--ks-lacquer-deep); + border: 1px solid var(--ks-rule); + border-radius: 4px; + overflow: hidden; + box-shadow: 0 24px 60px oklch(2% 0.004 95 / 0.55); +} + +.slop-kinpaku .visual-mode-preview-header { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 14px; + background: var(--ks-graphite); + border-bottom: 1px solid var(--ks-rule); +} + +.slop-kinpaku .visual-mode-preview-dot { + width: 9px; + height: 9px; + border-radius: 50%; + background: oklch(28% 0.01 95); +} + +.slop-kinpaku .visual-mode-preview-title { + margin-left: auto; + font-family: var(--ks-mono); + font-size: 0.72rem; + color: var(--ks-text-muted); + letter-spacing: 0.04em; +} + +.slop-kinpaku .visual-mode-frame { + width: 100%; + height: 720px; + border: 0; + display: block; + background: var(--ks-lacquer-deep); +} + +.slop-kinpaku .visual-mode-demo-caption { + margin: 14px 0 0; + font-size: 0.88rem; + line-height: 1.55; + color: var(--ks-text-muted); +} + + +/* ============================================================================ + 6. TRY IT LIVE — 11 specimen gallery on a tinted plinth + ============================================================================ */ + +.slop-kinpaku .visual-mode-gallery-header { + margin-bottom: 22px; +} + +.slop-kinpaku .visual-mode-gallery-lede { + font-size: 1rem; + line-height: 1.6; + color: var(--ks-text); + margin: 0; + max-width: 60ch; +} + +/* Gallery — auto-fill grid of standalone specimen cards. No wrapping + plinth: the specimens are independent items, not a unified composition, + so each card carries its own edge via a hairline border on a lifted + lacquer-raised surface. */ +.slop-kinpaku .gallery-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 16px; +} + +.slop-kinpaku .gallery-card { + display: grid; + background: var(--ks-lacquer-raised); + border: 1px solid var(--ks-rule); + border-radius: 2px; + overflow: hidden; + text-decoration: none; + color: inherit; + transition: border-color 180ms var(--ks-ease), + background 180ms var(--ks-ease); +} + +.slop-kinpaku .gallery-card:hover { + border-color: var(--ks-kinpaku); + background: oklch(13% 0.012 82); +} + +/* Specimen images are 2560×1600 (16:10) screenshots of landing-page + slop. The thumb was 1:1, which forced object-fit: cover to chop half + the screenshot AND made every card stupidly tall. Match the source + aspect so the full screenshot fits without cropping and the card + becomes a sensible thumbnail height. */ +.slop-kinpaku .gallery-card-thumb { + aspect-ratio: 16 / 10; + overflow: hidden; + background: oklch(4% 0.004 95); +} + +.slop-kinpaku .gallery-card-thumb img { + width: 100%; + height: 100%; + object-fit: cover; + object-position: top; + display: block; + filter: saturate(1.05); + transition: transform 320ms var(--ks-ease); +} + +.slop-kinpaku .gallery-card:hover .gallery-card-thumb img { + transform: scale(1.04); +} + +.slop-kinpaku .gallery-card-body { + display: grid; + gap: 8px; + padding: 18px 20px 22px; +} + +.slop-kinpaku .gallery-card-title { + font-family: var(--ks-font); + font-style: normal; + font-size: 1.02rem; + font-weight: 600; + line-height: 1.25; + color: var(--ks-champagne); + margin: 0; +} + +.slop-kinpaku .gallery-card-desc { + font-size: 0.88rem; + line-height: 1.5; + color: var(--ks-text-muted); + margin: 0; +} + + +/* ============================================================================ + 7. THE CATALOG — 8 categories, each a plinth of rule tiles + ============================================================================ */ + +.slop-kinpaku .slop-catalog-header { + margin-bottom: 22px; +} + +.slop-kinpaku .slop-catalog-lede { + font-size: 1rem; + line-height: 1.6; + color: var(--ks-text); + margin: 0; + max-width: 64ch; +} + +.slop-kinpaku .slop-catalog-lede strong { + color: var(--ks-champagne); + font-weight: 600; +} + +.slop-kinpaku .slop-catalog-lede a { + color: var(--ks-kinpaku); + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 4px; + text-decoration-color: oklch(78% 0.12 82 / 0.4); +} + +.slop-kinpaku .slop-catalog-lede a:hover { + text-decoration-color: var(--ks-kinpaku); +} + +/* Legend — collapsible. Champagne body, kinpaku layer chips. */ +.slop-kinpaku .anti-patterns-legend { + margin: 22px 0 32px; + background: var(--ks-lacquer-raised); + border: 1px solid var(--ks-rule); + border-radius: 2px; +} + +.slop-kinpaku .anti-patterns-legend-summary { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 18px; + cursor: pointer; + list-style: none; +} + +.slop-kinpaku .anti-patterns-legend-summary::-webkit-details-marker { + display: none; +} + +.slop-kinpaku .anti-patterns-legend-title { + font-family: var(--ks-mono); + font-size: 0.72rem; + letter-spacing: 0.22em; + text-transform: uppercase; + color: var(--ks-kinpaku); +} + +.slop-kinpaku .anti-patterns-legend-chevron { + transition: transform 220ms var(--ks-ease); + color: var(--ks-kinpaku); +} + +.slop-kinpaku .anti-patterns-legend[open] .anti-patterns-legend-chevron { + transform: rotate(180deg); +} + +.slop-kinpaku .anti-patterns-legend-body { + padding: 0 18px 18px; + border-top: 1px solid var(--ks-rule); +} + +.slop-kinpaku .anti-patterns-legend-body p { + margin: 16px 0; + color: var(--ks-text); + font-size: 0.94rem; +} + +.slop-kinpaku .anti-patterns-legend-body a { + color: var(--ks-kinpaku); + text-decoration: underline; + text-underline-offset: 3px; + text-decoration-color: oklch(78% 0.12 82 / 0.4); +} + +.slop-kinpaku .anti-patterns-legend-body a:hover { + text-decoration-color: var(--ks-kinpaku); +} + +.slop-kinpaku .anti-patterns-legend-body code { + font-family: var(--ks-mono); + font-size: 0.86em; + color: var(--ks-champagne); + background: var(--ks-graphite-2); + border: 1px solid var(--ks-rule); + padding: 1px 6px; + border-radius: 2px; +} + +.slop-kinpaku .anti-patterns-legend-layers { + display: grid; + gap: 12px; + margin: 0; +} + +.slop-kinpaku .anti-patterns-legend-layers > div { + display: grid; + grid-template-columns: 110px 1fr; + align-items: baseline; + gap: 16px; +} + +.slop-kinpaku .anti-patterns-legend-layers dt { + margin: 0; +} + +.slop-kinpaku .anti-patterns-legend-layers dd { + margin: 0; + color: var(--ks-text); + font-size: 0.9rem; + line-height: 1.5; +} + +/* Per-category section. Header + plinth of rule tiles. */ +.slop-kinpaku .anti-patterns-sections { + display: grid; + gap: clamp(36px, 4vw, 56px); +} + +.slop-kinpaku .anti-patterns-section-header { + display: flex; + align-items: baseline; + gap: 12px; + margin-bottom: 14px; + padding-bottom: 10px; + border-bottom: 1px solid var(--ks-rule); +} + +.slop-kinpaku .anti-patterns-section-title { + font-family: var(--ks-font); + font-size: 1.04rem; + font-weight: 600; + letter-spacing: 0.04em; + color: var(--ks-champagne); + margin: 0; +} + +.slop-kinpaku .anti-patterns-section-count { + font-family: var(--ks-mono); + font-size: 0.72rem; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--ks-text-faint); + margin: 0; +} + +/* Rule cards — independent specimens per category. Each card is + self-contained; no wrapping plinth. The category section header above + provides all the grouping context the cards need. */ +.slop-kinpaku .rule-card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 16px; +} + +.slop-kinpaku .rule-card { + display: grid; + background: var(--ks-lacquer-raised); + border: 1px solid var(--ks-rule); + border-radius: 2px; + overflow: hidden; + transition: border-color 180ms var(--ks-ease), + background 180ms var(--ks-ease); +} + +.slop-kinpaku .rule-card:hover { + border-color: var(--ks-kinpaku); + background: oklch(13% 0.012 82); +} + +/* Visual preview — the actual bad-pattern demo. The inline styles in the + markdown were authored against a light cream ground (#fff cards, #0a0b14 + dark-mode demos, #f5f3ef paper, gray text on cream, etc.); putting them + on lacquer-deep would erase the dark-on-dark specimens. So this band + keeps a light cream surface — the kinpaku card chrome around it stays + dark. Reads as a "specimen viewer" framed by the dark card. */ +.slop-kinpaku .rule-card-visual { + height: 160px; + background: oklch(96% 0.003 255); + border-bottom: 1px solid var(--ks-rule); + position: relative; + overflow: hidden; +} + +.slop-kinpaku .rule-card-visual-inner { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 0; +} + +/* For full-bleed inline previews (width:100% in the markdown) — kill + their inline border-radius so the preview meets the card edges + sharply, instead of leaving a dark sliver around rounded corners. + Fixed-width inline previews are unaffected (they sit centered in the + visual band and intentionally show the lacquer frame around them). */ +.slop-kinpaku .rule-card-visual-inner > div[style*="100%"] { + border-radius: 0 !important; +} + +.slop-kinpaku .rule-card-body { + display: grid; + gap: 8px; + padding: 16px 18px 20px; +} + +.slop-kinpaku .rule-card-head { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + margin-bottom: 4px; +} + +/* Chips. Both .rule-card-category and .rule-card-layer share the same + shape so they line up cleanly in the meta row. sub-pages.css sets + per-variant background-color and border-color via element selectors + (specificity 0,2,1) — match that level here so the kinpaku theme wins + on the cascade for both bg and border, not just color. */ +.slop-kinpaku .rule-card-category, +.slop-kinpaku .rule-card-layer { + display: inline-flex; + align-items: center; + font-family: var(--ks-mono); + font-size: 0.62rem; + letter-spacing: 0.18em; + text-transform: uppercase; + padding: 3px 8px; + border-radius: 2px; + line-height: 1.4; + border: 1px solid transparent; + background: transparent; +} + +.slop-kinpaku .rule-card-category[data-category="slop"] { + color: var(--ks-vermilion); + background: oklch(58% 0.15 35 / 0.14); + border-color: oklch(58% 0.15 35 / 0.32); +} + +.slop-kinpaku .rule-card-category[data-category="quality"] { + color: var(--ks-patina); + background: oklch(70% 0.12 188 / 0.12); + border-color: oklch(70% 0.12 188 / 0.30); +} + +.slop-kinpaku .rule-card-layer[data-layer="cli"] { + color: var(--ks-kinpaku); + background: oklch(78% 0.12 82 / 0.10); + border-color: oklch(78% 0.12 82 / 0.34); +} + +.slop-kinpaku .rule-card-layer[data-layer="browser"] { + color: var(--ks-patina); + background: oklch(70% 0.12 188 / 0.10); + border-color: oklch(70% 0.12 188 / 0.30); +} + +.slop-kinpaku .rule-card-layer[data-layer="llm"] { + color: var(--ks-text-muted); + background: var(--ks-graphite); + border-color: var(--ks-rule); +} + +/* Opt-in provider tells: deterministic but off by default (--gpt / --gemini). */ +.slop-kinpaku .rule-card-layer[data-layer="optin"] { + color: var(--ks-vermilion); + background: oklch(58% 0.15 35 / 0.10); + border-color: oklch(58% 0.15 35 / 0.30); + text-transform: none; + letter-spacing: 0.04em; +} + +.slop-kinpaku .rule-card-name { + font-family: var(--ks-font); + font-style: normal; + font-size: 0.98rem; + font-weight: 600; + line-height: 1.25; + color: var(--ks-champagne); + margin: 0; +} + +.slop-kinpaku .rule-card-desc { + font-size: 0.86rem; + line-height: 1.5; + color: var(--ks-text-muted); + margin: 0; +} + +/* "See in /impeccable" link. The parent .rule-card-body is display: grid, + so a block-level would stretch the full grid-cell width and the + border-bottom would draw across the whole card. justify-self: start + constrains the link to its content width on the inline axis. */ +.slop-kinpaku .rule-card-skill-link { + margin-top: 4px; + justify-self: start; + font-family: var(--ks-mono); + font-size: 0.74rem; + color: var(--ks-kinpaku); + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 4px; + text-decoration-color: oklch(78% 0.12 82 / 0.32); + transition: text-decoration-color 160ms var(--ks-ease); +} + +.slop-kinpaku .rule-card-skill-link:hover { + text-decoration-color: var(--ks-kinpaku); +} + +.slop-kinpaku .rule-card-skill-link::after { + content: " →"; + text-decoration: none; +} + + +/* ============================================================================ + 8. RUN IT YOURSELF — 3-up methods on a plinth + ============================================================================ */ + +/* Run-it methods — three independent install paths. Standalone cards, + not a unified bento. */ +.slop-kinpaku .visual-mode-methods-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 16px; +} + +@media (max-width: 880px) { + .slop-kinpaku .visual-mode-methods-grid { + grid-template-columns: 1fr; + } +} + +.slop-kinpaku .visual-mode-method { + display: grid; + align-content: start; + gap: 12px; + padding: 28px 28px 32px; + background: var(--ks-lacquer-raised); + border: 1px solid var(--ks-rule); + border-radius: 2px; +} + +.slop-kinpaku .visual-mode-method-label { + font-family: var(--ks-mono); + font-size: 0.7rem; + letter-spacing: 0.22em; + text-transform: uppercase; + color: var(--ks-kinpaku); + margin: 0; +} + +.slop-kinpaku .visual-mode-method-name { + font-family: var(--ks-font); + font-style: normal; + font-size: 1.1rem; + font-weight: 600; + line-height: 1.25; + color: var(--ks-champagne); + margin: 0; +} + +.slop-kinpaku .visual-mode-method-name a { + color: var(--ks-kinpaku); + text-decoration: none; + border-bottom: 1px solid oklch(78% 0.12 82 / 0.32); + transition: border-color 160ms var(--ks-ease); +} + +.slop-kinpaku .visual-mode-method-name a:hover { + border-bottom-color: var(--ks-kinpaku); +} + +.slop-kinpaku .visual-mode-method-name code { + font-family: var(--ks-mono); + font-size: 0.92em; + color: var(--ks-champagne); + background: var(--ks-graphite-2); + border: 1px solid var(--ks-rule); + padding: 1px 8px; + border-radius: 2px; +} + +.slop-kinpaku .visual-mode-method-desc { + font-size: 0.92rem; + line-height: 1.55; + color: var(--ks-text); + margin: 0; +} + +.slop-kinpaku .visual-mode-method-desc code { + font-family: var(--ks-mono); + font-size: 0.86em; + color: var(--ks-champagne); + background: var(--ks-graphite-2); + border: 1px solid var(--ks-rule); + padding: 1px 6px; + border-radius: 2px; +} + +.slop-kinpaku .visual-mode-method-desc a { + color: var(--ks-kinpaku); + text-decoration: underline; + text-underline-offset: 3px; + text-decoration-color: oklch(78% 0.12 82 / 0.4); +} + +.slop-kinpaku .visual-mode-method-desc a:hover { + text-decoration-color: var(--ks-kinpaku); +} + + +/* ============================================================================ + 9. FOOTER on dark surface + ============================================================================ */ + +/* Footer chrome moved to kinpaku-kit.css (.kinpaku-chrome .site-footer). */ diff --git a/site/styles/sub-pages.css b/site/styles/sub-pages.css index a28c87f7a..331638834 100644 --- a/site/styles/sub-pages.css +++ b/site/styles/sub-pages.css @@ -102,8 +102,14 @@ a { .site-header-brand-logo { width: 26px; height: 26px; - border-radius: 6px; flex-shrink: 0; + color: var(--color-ink); +} + +.site-header-brand-logo svg { + width: 100%; + height: 100%; + display: block; } .site-header-brand-name { @@ -535,7 +541,6 @@ main#main { font-family: var(--font-display); font-size: clamp(2rem, 4vw, 2.75rem); font-weight: 500; - font-style: italic; color: var(--color-ink); letter-spacing: -0.02em; line-height: 1; @@ -640,7 +645,6 @@ main#main { font-family: var(--font-display); font-size: clamp(1.5rem, 3vw, 2rem); font-weight: 500; - font-style: italic; color: var(--color-ink); letter-spacing: -0.01em; line-height: 1.1; @@ -1341,7 +1345,6 @@ main#main { font-size: 0.8125rem; color: var(--color-ash); margin-top: var(--spacing-sm); - font-style: italic; max-width: 60ch; } @@ -1870,7 +1873,6 @@ main#main { text-transform: none; letter-spacing: 0; color: var(--color-ash); - font-style: italic; text-align: center; justify-self: center; /* Allow wrapping inside the middle column if the text is long. */ @@ -2109,7 +2111,6 @@ main#main { .skill-source-card-subtitle { font-size: 0.8125rem; color: var(--color-ash); - font-style: italic; } .skill-source-card-body { @@ -2133,7 +2134,6 @@ main#main { .skill-references-heading { font-family: var(--font-display); font-size: 1.5rem; - font-style: italic; font-weight: 500; color: var(--color-ink); margin-bottom: var(--spacing-md); @@ -2193,7 +2193,6 @@ main#main { .skill-reference-title { font-family: var(--font-display); font-size: 1.125rem; - font-style: italic; color: var(--color-ink); } @@ -2225,8 +2224,8 @@ main#main { margin-bottom: 0.6em; } -.prose h1 { font-family: var(--font-display); font-size: 1.875rem; font-weight: 500; font-style: italic; } -.prose h2 { font-family: var(--font-display); font-size: 1.5rem; font-weight: 500; font-style: italic; margin-top: 2.2em; } +.prose h1 { font-family: var(--font-display); font-size: 1.875rem; font-weight: 500; } +.prose h2 { font-family: var(--font-display); font-size: 1.5rem; font-weight: 500; margin-top: 2.2em; } .prose h3 { font-size: 1.125rem; margin-top: 1.8em; } .prose h4 { font-size: 1rem; } diff --git a/site/styles/testimonials.css b/site/styles/testimonials.css new file mode 100644 index 000000000..b742074a2 --- /dev/null +++ b/site/styles/testimonials.css @@ -0,0 +1,200 @@ +/* ============================================================================ + Testimonials — two-row marquee on a tinted graphite plinth. + Single variant, no switcher. Spotlight card (Ben Davis) gets a kinpaku + gold gradient so it commands attention even mid-row. + ============================================================================ */ + +.home-kinpaku .testimonials-section { + position: relative; + /* Escape main.site-content's max-width + side padding so the section + spans the full viewport. */ + width: 100vw; + margin-left: calc(50% - 50vw); + padding: 20px 0 clamp(56px, 7vw, 96px); + background: var(--ks-lacquer); + color: var(--ks-text); + border-top: 0; + overflow: hidden; +} + +/* Flat dotted top divider (matches the hero → testimonials boundary). */ +.home-kinpaku .testimonials-section::before { + content: ""; + position: absolute; + top: 0; + left: 50%; + width: 100vw; + height: 18px; + transform: translateX(-50%); + background-image: radial-gradient(circle, oklch(0.42 0.014 82) 0.9px, transparent 1.4px); + background-size: 7px 6px; + background-position: 0 1px; + background-repeat: repeat; + pointer-events: none; +} + +/* --------------------------------------------------------------------------- + Plinth — the tinted graphite shelf the marquee rides on + --------------------------------------------------------------------------- */ + +.home-kinpaku .t-plinth { + position: relative; + width: 100vw; + padding: 20px 0; + background: + linear-gradient(180deg, oklch(20% 0.01 78) 0%, oklch(17% 0.008 78) 100%); + border-top: 1px solid oklch(78% 0.12 82 / 0.12); + border-bottom: 1px solid oklch(78% 0.12 82 / 0.10); +} +.home-kinpaku .t-plinth--shelf::before, +.home-kinpaku .t-plinth--shelf::after { + content: ""; + position: absolute; + left: 0; right: 0; + height: 8px; + pointer-events: none; + z-index: 1; +} +.home-kinpaku .t-plinth--shelf::before { + top: 0; + background: linear-gradient(180deg, oklch(0% 0 0 / 0.18), transparent); +} +.home-kinpaku .t-plinth--shelf::after { + bottom: 0; + background: linear-gradient(0deg, oklch(0% 0 0 / 0.22), transparent); +} + +/* --------------------------------------------------------------------------- + Marquee engine + --------------------------------------------------------------------------- */ + +.home-kinpaku .t-marquee { + display: grid; + gap: 14px; + width: 100%; +} +.home-kinpaku .t-marquee-row { overflow: hidden; } + +.home-kinpaku .t-marquee-track { + display: flex; + gap: 14px; + width: max-content; + /* Both rows share the same duration so on-screen speeds match. */ + animation: t-marquee-fwd 110s linear infinite; +} +.home-kinpaku .t-marquee-track--reverse { + animation: t-marquee-rev 110s linear infinite; +} +.home-kinpaku .t-marquee:hover .t-marquee-track { + animation-play-state: paused; +} +@keyframes t-marquee-fwd { + from { transform: translateX(0); } + to { transform: translateX(-50%); } +} +@keyframes t-marquee-rev { + from { transform: translateX(-50%); } + to { transform: translateX(0); } +} +@media (prefers-reduced-motion: reduce) { + .home-kinpaku .t-marquee-track { animation: none; } +} + +/* --------------------------------------------------------------------------- + Card + --------------------------------------------------------------------------- */ + +.home-kinpaku .testimonials-section .t-card { + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 14px; + width: 320px; + height: 168px; + flex: none; + padding: 18px 18px 14px; + border-radius: 10px; + color: var(--ks-text); + text-decoration: none; + transition: transform 200ms ease, background 200ms ease, box-shadow 200ms ease, border-color 200ms ease; +} + +/* Plinth-resident card — borderless, slightly raised tone */ +.home-kinpaku .t-card--plinth { + background: oklch(13% 0.008 95); + border: 1px solid transparent; + box-shadow: 0 1px 0 0 oklch(78% 0.12 82 / 0.06) inset, 0 4px 12px -8px oklch(0% 0 0 / 0.6); +} +.home-kinpaku .t-card--plinth:hover { + background: oklch(15% 0.012 78); + transform: translateY(-1px); +} + +/* Quote text */ +.home-kinpaku .testimonials-section .t-card-quote { + font-family: var(--ks-font); + font-size: 0.94rem; + line-height: 1.5; + color: var(--ks-text); + margin: 0; + text-wrap: pretty; + display: -webkit-box; + -webkit-line-clamp: 4; + -webkit-box-orient: vertical; + overflow: hidden; +} + +/* Meta row (avatar + name + handle) */ +.home-kinpaku .testimonials-section .t-card-meta { + display: flex; + align-items: center; + gap: 10px; + font-family: var(--ks-mono); +} +.home-kinpaku .testimonials-section .t-name-block { + display: flex; + flex-direction: column; + line-height: 1.2; + min-width: 0; +} +.home-kinpaku .testimonials-section .t-name { + color: var(--ks-text); + font-size: 0.82rem; + letter-spacing: 0.01em; + font-family: var(--ks-mono); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 180px; +} +.home-kinpaku .testimonials-section .t-handle { + color: var(--ks-text-muted); + font-size: 0.74rem; + letter-spacing: 0.02em; +} + +/* Avatar + coin fallback */ +.home-kinpaku .testimonials-section .t-avatar { + width: 28px; + height: 28px; + border-radius: 50%; + object-fit: cover; + flex: none; + background: var(--ks-graphite-2); + box-shadow: inset 0 0 0 1px oklch(78% 0.12 82 / 0.22); +} +.home-kinpaku .testimonials-section .t-coin { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 50%; + background: var(--ks-graphite-2); + border: 1px solid oklch(78% 0.12 82 / 0.32); + font-family: var(--ks-font-display); + font-weight: 400; + font-size: 0.95rem; + color: var(--ks-kinpaku); + flex: none; +} diff --git a/site/styles/tokens.css b/site/styles/tokens.css index e5517adac..77d98a5e0 100644 --- a/site/styles/tokens.css +++ b/site/styles/tokens.css @@ -31,10 +31,13 @@ button, input, textarea, select { ============================================ */ :root { - /* Typography */ - --font-display: 'Cormorant Garamond', Georgia, serif; - --font-body: 'Instrument Sans', system-ui, sans-serif; - --font-mono: 'Space Grotesk', monospace; + /* Typography — kinpaku is the default. The legacy token names now point at + the --ks-* faces (defined in kinpaku-tokens.css, loaded globally via + Base.astro) so any rule still reading --font-* renders in the brand + faces. The old Cormorant/Instrument/Space Grotesk trio is retired. */ + --font-display: var(--ks-font-display); + --font-body: var(--ks-font); + --font-mono: var(--ks-mono); /* Spacing Scale */ --spacing-xs: 8px; @@ -59,21 +62,24 @@ button, input, textarea, select { --duration-slower: 0.8s; --duration-slowest: 1.2s; - /* Core Colors - Light Mode */ - --color-ink: oklch(10% 0 0); - --color-text: oklch(10% 0 0); - --color-paper: oklch(98% 0 0); - --color-cream: oklch(96% 0.005 350); - --color-charcoal: oklch(25% 0 0); - --color-ash: oklch(55% 0 0); - --color-mist: oklch(92% 0 0); - --color-bg: oklch(96% 0.005 350); + /* Core colors — kinpaku is the default surface. The legacy token names now + carry dark-lacquer values (mirroring the per-page kinpaku remaps), so any + rule still reading --color-* renders on the kinpaku surface. Surfaces are + literal oklch; ink/text/accent point at the --ks-* brand tokens. */ + --color-ink: var(--ks-champagne); + --color-text: var(--ks-text); + --color-paper: oklch(7% 0.006 95); + --color-cream: oklch(9% 0.006 95); + --color-charcoal: oklch(72% 0.025 82); + --color-ash: oklch(60% 0.018 82); + --color-mist: oklch(58% 0.065 82 / 0.22); + --color-bg: oklch(11% 0.006 95); - /* Accent - Vibrant Magenta/Rose (original) */ - --color-accent: oklch(60% 0.25 350); - --color-accent-hover: oklch(52% 0.25 350); - --color-accent-dim: oklch(60% 0.25 350 / 0.15); - --color-accent-soft: oklch(60% 0.25 350 / 0.25); + /* Accent — kinpaku gold (was magenta). */ + --color-accent: var(--ks-kinpaku); + --color-accent-hover: var(--ks-kinpaku-pale); + --color-accent-dim: oklch(78% 0.12 82 / 0.18); + --color-accent-soft: oklch(78% 0.12 82 / 0.32); /* Framework category colors - Light mode */ --cat-create-bg: #fdf2f8; diff --git a/site/styles/workflow.css b/site/styles/workflow.css index d2cf720eb..baba1d317 100644 --- a/site/styles/workflow.css +++ b/site/styles/workflow.css @@ -365,6 +365,13 @@ min-height: auto; } +.demo-placeholder { + padding: var(--spacing-lg); + color: var(--color-ash); + font-style: italic; + text-align: center; +} + /* (nav moved to fisheye list) */ /* ============================================ diff --git a/skill/SKILL.md b/skill/SKILL.md deleted file mode 100644 index 1d611efc5..000000000 --- a/skill/SKILL.md +++ /dev/null @@ -1,168 +0,0 @@ ---- -name: impeccable -description: "Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks." -argument-hint: "[{{command_hint}}] [target]" -user-invocable: true -allowed-tools: - - Bash(npx impeccable *) -license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution. ---- - -Designs and iterates production-grade frontend interfaces. Real working code, committed design choices, exceptional craft. - -## Setup - -Before any design work or file edits: - -1. Load context (PRODUCT.md / DESIGN.md) via the loader script. -2. Identify the register and load the matching register reference (brand.md or product.md). -3. **If the user invoked a sub-command (e.g. `craft`, `shape`, `audit`), load its reference file too.** This is non-negotiable: `craft` without `craft.md` loaded means you'll skip the shape-and-confirm step the user expects. - -Skipping these produces generic output that ignores the project. - -### 1. Context gathering - -Two files, case-insensitive. The loader looks at the project root by default and falls back to `.agents/context/` and `docs/` if the root is clean. Override with `IMPECCABLE_CONTEXT_DIR=path/to/dir` (absolute or relative to cwd). - -- **PRODUCT.md**: required. Users, brand, tone, anti-references, strategic principles. -- **DESIGN.md**: optional, strongly recommended. Colors, typography, elevation, components. - -Load both in one call: - -```bash -node {{scripts_path}}/load-context.mjs -``` - -Consume the full JSON output. Never pipe through `head`, `tail`, `grep`, or `jq`. The output's `contextDir` field tells you where the files were resolved from. - -If the output is already in this session's conversation history, don't re-run. Exceptions requiring a fresh load: you just ran `{{command_prefix}}impeccable teach` or `{{command_prefix}}impeccable document` (they rewrite the files), or the user manually edited one. - -`{{command_prefix}}impeccable live` already warms context via `live.mjs`. If you've run `live.mjs`, don't also run `load-context.mjs` this session. - -If PRODUCT.md is missing, empty, or placeholder (`[TODO]` markers, <200 chars): run `{{command_prefix}}impeccable teach`, then resume the user's original task with the fresh context. If the original task was `{{command_prefix}}impeccable craft`, resume into `{{command_prefix}}impeccable shape` before any implementation work. - -If DESIGN.md is missing: nudge once per session (*"Run `{{command_prefix}}impeccable document` for more on-brand output"*), then proceed. - -### 2. Register - -Every design task is **brand** (marketing, landing, campaign, long-form content, portfolio: design IS the product) or **product** (app UI, admin, dashboard, tool: design SERVES the product). - -Identify before designing. Priority: (1) cue in the task itself ("landing page" vs "dashboard"); (2) the surface in focus (the page, file, or route being worked on); (3) `register` field in PRODUCT.md. First match wins. - -If PRODUCT.md lacks the `register` field (legacy), infer it once from its "Users" and "Product Purpose" sections, then cache the inferred value for the session. Suggest the user run `{{command_prefix}}impeccable teach` to add the field explicitly. - -Load the matching reference: [reference/brand.md](reference/brand.md) or [reference/product.md](reference/product.md). The shared design laws below apply to both. - -## Shared design laws - -Apply to every design, both registers. Match implementation complexity to the aesthetic vision: maximalism needs elaborate code, minimalism needs precision. Interpret creatively. Vary across projects; never converge on the same choices. {{model}} is capable of extraordinary work. Don't hold back. - -### Color - -- Use OKLCH. Reduce chroma as lightness approaches 0 or 100; high chroma at extremes looks garish. -- Never use `#000` or `#fff`. Tint every neutral toward the brand hue (chroma 0.005–0.01 is enough). -- Pick a **color strategy** before picking colors. Four steps on the commitment axis: - - **Restrained**: tinted neutrals + one accent ≤10%. Product default; brand minimalism. - - **Committed**: one saturated color carries 30–60% of the surface. Brand default for identity-driven pages. - - **Full palette**: 3–4 named roles, each used deliberately. Brand campaigns; product data viz. - - **Drenched**: the surface IS the color. Brand heroes, campaign pages. -- The "one accent ≤10%" rule is Restrained only. Committed / Full palette / Drenched exceed it on purpose. Don't collapse every design to Restrained by reflex. - -### Theme - -Dark vs. light is never a default. Not dark "because tools look cool dark." Not light "to be safe." - -Before choosing, write one sentence of physical scene: who uses this, where, under what ambient light, in what mood. If the sentence doesn't force the answer, it's not concrete enough. Add detail until it does. - -"Observability dashboard" does not force an answer. "SRE glancing at incident severity on a 27-inch monitor at 2am in a dim room" does. Run the sentence, not the category. - -### Typography - -- Cap body line length at 65–75ch. -- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales. - -### Layout - -- Vary spacing for rhythm. Same padding everywhere is monotony. -- Cards are the lazy answer. Use them only when they're truly the best affordance. Nested cards are always wrong. -- Don't wrap everything in a container. Most things don't need one. - -### Motion - -- Don't animate CSS layout properties. -- Ease out with exponential curves (ease-out-quart / quint / expo). No bounce, no elastic. - -### Absolute bans - -Match-and-refuse. If you're about to write any of these, rewrite the element with different structure. - -- **Side-stripe borders.** `border-left` or `border-right` greater than 1px as a colored accent on cards, list items, callouts, or alerts. Never intentional. Rewrite with full borders, background tints, leading numbers/icons, or nothing. -- **Gradient text.** `background-clip: text` combined with a gradient background. Decorative, never meaningful. Use a single solid color. Emphasis via weight or size. -- **Glassmorphism as default.** Blurs and glass cards used decoratively. Rare and purposeful, or nothing. -- **The hero-metric template.** Big number, small label, supporting stats, gradient accent. SaaS cliché. -- **Identical card grids.** Same-sized cards with icon + heading + text, repeated endlessly. -- **Modal as first thought.** Modals are usually laziness. Exhaust inline / progressive alternatives first. - -### Copy - -- Every word earns its place. No restated headings, no intros that repeat the title. -- **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`. - -### The AI slop test - -If someone could look at this interface and say "AI made that" without doubt, it's failed. Cross-register failures are the absolute bans above. Register-specific failures live in each reference. - -**Category-reflex check.** Run at two altitudes; the second one catches what the first one misses. - -- **First-order:** if someone could guess the theme + palette from the category alone ("observability → dark blue", "healthcare → white + teal", "finance → navy + gold", "crypto → neon on black"), it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain. -- **Second-order:** if someone could guess the aesthetic family from category-plus-anti-references ("AI workflow tool that's not SaaS-cream → editorial-typographic", "fintech that's not navy-and-gold → terminal-native dark mode"), it's the trap one tier deeper. The first reflex was avoided; the second wasn't. Rework until both answers are not obvious. The brand register's [reflex-reject aesthetic lanes](reference/brand.md) list catches the currently-saturated families. - -## Commands - -| Command | Category | Description | Reference | -|---|---|---|---| -| `craft [feature]` | Build | Shape, then build a feature end-to-end | [reference/craft.md](reference/craft.md) | -| `shape [feature]` | Build | Plan UX/UI before writing code | [reference/shape.md](reference/shape.md) | -| `teach` | Build | Set up PRODUCT.md and DESIGN.md context | [reference/teach.md](reference/teach.md) | -| `document` | Build | Generate DESIGN.md from existing project code | [reference/document.md](reference/document.md) | -| `extract [target]` | Build | Pull reusable tokens and components into design system | [reference/extract.md](reference/extract.md) | -| `critique [target]` | Evaluate | UX design review with heuristic scoring | [reference/critique.md](reference/critique.md) | -| `audit [target]` | Evaluate | Technical quality checks (a11y, perf, responsive) | [reference/audit.md](reference/audit.md) | -| `polish [target]` | Refine | Final quality pass before shipping | [reference/polish.md](reference/polish.md) | -| `bolder [target]` | Refine | Amplify safe or bland designs | [reference/bolder.md](reference/bolder.md) | -| `quieter [target]` | Refine | Tone down aggressive or overstimulating designs | [reference/quieter.md](reference/quieter.md) | -| `distill [target]` | Refine | Strip to essence, remove complexity | [reference/distill.md](reference/distill.md) | -| `harden [target]` | Refine | Production-ready: errors, i18n, edge cases | [reference/harden.md](reference/harden.md) | -| `onboard [target]` | Refine | Design first-run flows, empty states, activation | [reference/onboard.md](reference/onboard.md) | -| `animate [target]` | Enhance | Add purposeful animations and motion | [reference/animate.md](reference/animate.md) | -| `colorize [target]` | Enhance | Add strategic color to monochromatic UIs | [reference/colorize.md](reference/colorize.md) | -| `typeset [target]` | Enhance | Improve typography hierarchy and fonts | [reference/typeset.md](reference/typeset.md) | -| `layout [target]` | Enhance | Fix spacing, rhythm, and visual hierarchy | [reference/layout.md](reference/layout.md) | -| `delight [target]` | Enhance | Add personality and memorable touches | [reference/delight.md](reference/delight.md) | -| `overdrive [target]` | Enhance | Push past conventional limits | [reference/overdrive.md](reference/overdrive.md) | -| `clarify [target]` | Fix | Improve UX copy, labels, and error messages | [reference/clarify.md](reference/clarify.md) | -| `adapt [target]` | Fix | Adapt for different devices and screen sizes | [reference/adapt.md](reference/adapt.md) | -| `optimize [target]` | Fix | Diagnose and fix UI performance | [reference/optimize.md](reference/optimize.md) | -| `live` | Iterate | Visual variant mode: pick elements in the browser, generate alternatives | [reference/live.md](reference/live.md) | - -Plus two management commands: `pin ` and `unpin `, detailed below. - -### Routing rules - -1. **No argument**: render the table above as the user-facing command menu, grouped by category. Ask what they'd like to do. -2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. -3. **First word doesn't match**: general design invocation. Apply the setup steps, shared design laws, and the loaded register reference, using the full argument as context. - -Setup (context gathering, register) is already loaded by then; sub-commands don't re-invoke `{{command_prefix}}impeccable`. - -If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `teach` as a blocker, finish teach, refresh context, then resume the original command and target. - -## Pin / Unpin - -**Pin** creates a standalone shortcut so `{{command_prefix}}` invokes `{{command_prefix}}impeccable ` directly. **Unpin** removes it. The script writes to every harness directory present in the project. - -```bash -node {{scripts_path}}/pin.mjs -``` - -Valid `` is any command from the table above. Report the script's result concisely. Confirm the new shortcut on success, relay stderr verbatim on error. diff --git a/skill/SKILL.src.md b/skill/SKILL.src.md new file mode 100644 index 000000000..019382b48 --- /dev/null +++ b/skill/SKILL.src.md @@ -0,0 +1,183 @@ +--- +name: impeccable +description: "Use when the user wants to design, redesign, shape, critique, audit, polish, clarify, distill, harden, optimize, adapt, animate, colorize, extract, or otherwise improve a frontend interface. Covers websites, landing pages, dashboards, product UI, app shells, components, forms, settings, onboarding, and empty states. Handles UX review, visual hierarchy, information architecture, cognitive load, accessibility, performance, responsive behavior, theming, anti-patterns, typography, fonts, spacing, layout, alignment, color, motion, micro-interactions, UX copy, error states, edge cases, i18n, and reusable design systems or tokens. Also use for bland designs that need to become bolder or more delightful, loud designs that should become quieter, live browser iteration on UI elements, or ambitious visual effects that should feel technically extraordinary. Not for backend-only or non-UI tasks." +argument-hint: "[{{command_hint}}] [target]" +user-invocable: true +allowed-tools: + - Bash(npx impeccable *) +license: Apache 2.0 +--- + +Designs and iterates production-grade frontend interfaces. Real working code, committed design choices, exceptional craft. + +## Setup + +You MUST do these steps before proceeding: + +1. Run `node {{scripts_path}}/context.mjs` once per session. If you've already seen its output in this conversation, do not re-run it. The script either prints the project's PRODUCT.md (and DESIGN.md when present) as a markdown block, or tells you it's missing. Follow whatever it prints. **If it reports `NO_PRODUCT_MD`, stop and follow `reference/init.md` before doing anything else.** If the output ends with an `UPDATE_AVAILABLE` directive, follow it (ask the user once about updating, then continue). It never blocks the current task. +2. If the user invoked a sub-command (`craft`, `shape`, `audit`, `polish`, ...), you MUST read `reference/.md` next. Non-optional. The reference defines the command's flow; without it you will skip steps the user expects. +3. Familiarize yourself with any existing design system, conventions, and components in the code. Read at least one project file (CSS / tokens / theme / a representative component or page). **Required even when you've loaded a sub-command reference in step 2.** Don't reinvent the wheel; use what's there when it works, branch out when the UX wins. +4. Read the matching register reference. **This is non-optional; skipping it produces generic output.** If the project is marketing, a landing page, a campaign, long-form content, or a portfolio (design IS the product), read `reference/brand.md`. If it is app UI, admin, a dashboard, or a tool (design SERVES the product), read `reference/product.md`. Pick by first match: (1) task cue ("landing page" vs "dashboard"); (2) surface in focus (the page, file, or route being worked on); (3) `register` field in PRODUCT.md. +5. **If the project is brand-new (no existing CSS tokens / theme / committed brand colors found in step 3)**, run `node {{scripts_path}}/palette.mjs` to receive a brand seed color and composition guidance. This is the anchor for your primary brand color. Compose the rest of the palette (bg, surface, ink, accent, muted) around it per the script's instructions. Use OKLCH throughout. **Skip this step only if step 3 found committed brand colors in existing tokens; in that case identity-preservation wins.** + +## Design guidance + +Produce ready-to-ship, production-grade code, not prototypes or starting points. Take no shortcuts unless the user asks for them (when in doubt, ask). Don't stop until arriving at a complete implementation (beautiful, responsive, fast, precise, bug-free, on brand). You take attention to detail seriously: every page, section or component crafted is battle tested using the tools available to you (browser screenshotting, computer use, etc). {{model}} is capable of extraordinary work. Don't hold back. + +### General rules + +#### Color + +- **Verify contrast.** Body text must hit ≥4.5:1 against its background; large text (≥18px or bold ≥14px) needs ≥3:1. Placeholder text needs the same 4.5:1, not the muted-gray default. The most common failure: muted gray body text on a tinted near-white. If the contrast is even close, bump the body color toward the ink end of the ramp; light gray "for elegance" is the single biggest reason AI designs feel hard to read. +- Gray text on a colored background looks washed out. Use a darker shade of the background's own hue, or a transparency of the text color. + +#### Typography + +- Cap body line length at 65–75ch. +- Hierarchy through scale + weight contrast (≥1.25 ratio between steps). Avoid flat scales. +- Cap font-family count at 3 (display + body + optional mono). More than 3 reads as indecision, not richness. One well-tuned family with weight contrast usually beats three competing typefaces. +- Don't pair fonts that are similar but not identical (two geometric sans-serifs, two humanist sans-serifs). Pair on a contrast axis (serif + sans, geometric + humanist) or use one family in multiple weights. +- No all-caps body copy. Reserve uppercase for short labels (≤4 words), section eyebrows (used sparingly per the Absolute bans), and badges. Sentences in ALL CAPS are unreadable at body sizes. +- Hero / display heading ceiling: clamp() max ≤ 6rem (~96px). Above that the page is shouting, not designing. +- Display heading letter-spacing floor: ≥ -0.04em. Anything tighter and letters touch; cramped, not "designed". +- Use `text-wrap: balance` on h1–h3 for even line lengths; `text-wrap: pretty` on long prose to reduce orphans. + + +Two hard typographic ceilings you currently miss: +- Hero clamp() max ≤ 6rem. 8–11rem (128–176px) reads as comically loud, not bold. +- Display letter-spacing ≥ -0.04em. Your default of -0.05 to -0.085em on display H1s makes the letters touch and reads as cramped. -0.02 to -0.03em is plenty for tight grotesque display; -0.04em is the floor. + + +#### Layout + +- Vary spacing for rhythm. +- Cards are the lazy answer. Use them only when they're truly the best affordance. Nested cards are always wrong. +- Flexbox for 1D, Grid for 2D. Don't default to Grid when `flex-wrap` would be simpler. +- For responsive grids without breakpoints: `repeat(auto-fit, minmax(280px, 1fr))`. +- Build a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip). Never arbitrary values like 999 or 9999. + +#### Motion +- Motion should be intentional, and not be an afterthought. consider it as part of the build. +- Don't animate CSS layout properties unless truly needed. +- Ease out with exponential curves (ease-out-quart / quint / expo). No bounce, no elastic. +- Use libraries for more advanced motion needs (e.g. motion, gsap, anime.js, lenis etc) +- Reduced motion is not optional. Every animation needs a `@media (prefers-reduced-motion: reduce)` alternative: typically a crossfade or instant transition. +- Staggering the items within one list is legitimate. The tell is the uniform reflex (one identical entrance applied to every section), not motion itself; each reveal should fit what it reveals. Suppressing the reflex is never a reason to ship a page with no motion at all. +- Reveal animations must enhance an already-visible default. Don't gate content visibility on a class-triggered transition; transitions pause on hidden tabs and headless renderers, so the reveal never fires and the section ships blank. +- Premium motion materials are not just transform/opacity. Blur, backdrop-filter, clip-path, mask, and shadow/glow are part of the palette when they materially improve the effect and stay smooth. + +#### Interaction + +- Dropdowns rendered with `position: absolute` inside an `overflow: hidden` or `overflow: auto` container will be clipped. Use the native `` / popover API, `position: fixed`, or a portal to escape the stacking context. + + +**Gemini-specific defect: hard ban.** Never animate `` elements on hover. This includes any `transform` on `:hover` of an image, AND `.group:hover .group-hover\:scale` / `.group:hover .group-hover\:rotate` / `.group:hover .group-hover\:translate` patterns from Tailwind that animate a child image via a parent hover. This is your single most common motion tell; it adds no information (the image isn't an action target) and reads as "AI animated this because it could". If a card needs hover feedback, animate the card's background, border, or shadow. Never the image, never via the image's parent. + + +### Copy + +- Every word earns its place. No restated headings, no intros that repeat the title. +- **No em dashes.** Use commas, colons, semicolons, periods, or parentheses. Also not `--`. +- **No aphoristic-cadence body copy as a default voice.** Don't fall into the rhythm of "serious statement, then punchy short negation" as the page's recurring voice. If three or more section copy blocks on the page land on a short rebuttal-shaped sentence, rewrite. Specific, not aphoristic. +- **No marketing buzzwords.** The streamline / empower / supercharge / leverage / unleash / transform / seamless / world-class / enterprise-grade / next-generation / cutting-edge / game-changer / mission-critical family of phrases. Pick a specific noun and a verb that describes what the product literally does. +- Button labels: verb + object. "Save changes" beats "OK"; "Delete project" beats "Yes". The label should say what will happen. +- Link text needs standalone meaning. "View pricing plans" beats "Click here"; screen readers announce links out of context. + +### New projects only (when no prior work exists) + +#### Color & Theme + +- Use OKLCH. +- **The cream / sand / beige body bg is the saturated AI default of 2026.** The whole warm-neutral band (OKLCH L 0.84-0.97, C < 0.06, hue 40-100) reads as cream/sand/paper/parchment regardless of what you call it. Token names like `--paper`, `--cream`, `--sand`, `--bone`, `--flour`, `--linen`, `--parchment`, `--wheat`, `--biscuit`, `--ivory` are tells in themselves. If the brief is "warm, traditional, family-coastal-Italian" or "magazine-warm" or "editorial-restraint", DO NOT translate that into a near-white warm-tinted bg; that's the AI move. Pick: (a) a saturated brand color as the body (terracotta, oxblood, deep ochre, near-black), (b) a true off-white at chroma 0 (or chroma toward the brand's own hue, not toward warmth-by-default), or (c) a darker mid-tone tinted neutral that's clearly the brand's own. "Warmth" in the brand is carried by accent + typography + imagery, not by body bg. +- Tinted neutrals: add 0.005–0.015 chroma toward the brand's hue. Don't default-tint toward warm or cool "because the brand feels that way"; that's the cross-project monoculture move. +- When picking a theme: Dark vs. light is never a default. Not dark "because tools look cool dark." Not light "to be safe.".Before choosing, write one sentence of physical scene: who uses this, where, under what ambient light, in what mood. If the sentence doesn't force the answer, it's not concrete enough. Add detail until it does. +- Pick a **color strategy** before picking colors. Four steps on the commitment axis: + - **Restrained**: tinted neutrals + one accent ≤10%. Product default; brand minimalism. + - **Committed**: one saturated color carries 30–60% of the surface. Brand default for identity-driven pages. + - **Full palette**: 3–4 named roles, each used deliberately. Brand campaigns; product data viz. + - **Drenched**: the surface IS the color. Brand heroes, campaign pages. + +### Absolute bans + +Match-and-refuse. If you're about to write any of these, rewrite the element with different structure. + +- **Side-stripe borders.** `border-left` or `border-right` greater than 1px as a colored accent on cards, list items, callouts, or alerts. Never intentional. Rewrite with full borders, background tints, leading numbers/icons, or nothing. +- **Gradient text.** `background-clip: text` combined with a gradient background. Decorative, never meaningful. Use a single solid color. Emphasis via weight or size. +- **Glassmorphism as default.** Blurs and glass cards used decoratively. Rare and purposeful, or nothing. +- **The hero-metric template.** Big number, small label, supporting stats, gradient accent. SaaS cliché. +- **Identical card grids.** Same-sized cards with icon + heading + text, repeated endlessly. +- **Tiny uppercase tracked eyebrow above every section.** The 2023-era kicker (small all-caps text with wide tracking, "ABOUT" "PROCESS" "PRICING" above each heading) is now the saturated AI scaffold; it appears on 55-95% of generations regardless of brief, which is the definition of a tell. One named kicker as a deliberate brand system is voice; an eyebrow on every section is AI grammar. Choose a different cadence. +- **Numbered section markers as default scaffolding (01 / 02 / 03).** Putting `01 · About / 02 · Process / 03 · Pricing` above every section is the eyebrow trope one tier deeper: reach for it because "landing pages do this" and you're scaffolding by reflex. Numbers earn their place when the section actually IS a sequence (a real 3-step process, an ordered flow, a typed timeline) and the order carries information the reader needs. One deliberate numbered sequence on one page is voice; numbered eyebrows on every section across the site is AI grammar. +- **Text that overflows its container.** Long heading words plus large clamp scales plus narrow grids cause headline overflow on tablet/mobile. Test the heading copy at every breakpoint; if it overflows, reduce the clamp max or rewrite the copy. The viewport is part of the design. + + +**Codex-specific defects** (your most-frequent giveaways; refuse-and-rewrite): + +- **`border: 1px solid X` + `box-shadow: 0 Npx Mpx ...` with M ≥ 16px** on the same element. The "ghost-card" pattern: 1px border plus soft wide drop shadow on buttons and cards. Don't pair them. Pick one (a single solid border at the brand color, OR a defined shadow at no more than 8px blur), never both as decoration. +- **`border-radius: 32px+` on cards / sections / inputs.** You over-round. Cards top out at 12–16px; full-pill is fine for tags/buttons. Picking 24/28/32/40px on a card is the codex tell; no brand wants "insanely rounded". +- **Hand-drawn / sketchy SVG illustrations.** Class names like `loose-sketch`, `*-sketch`, `doodle`, `wavy`; `feTurbulence` / `feDisplacementMap` "paper grain" filters; 5-to-30 path crude scenes meant to depict a tangible subject (an otter, a table-and-fork, an album cover). All of these read as amateurish, not whimsical. If you can't render the scene with real assets, ship no illustration. Don't attempt sketchy SVG as a fallback. +- **`repeating-linear-gradient(...)` stripe backgrounds.** Diagonal stripes in `body:before` or section backgrounds are pure codex decoration. Don't. +- **"X theater" / "actually X" / "not just X, it's Y" copy.** "Productivity theater", "engagement theater", "growth theater": instant AI slop. Choose a specific noun, not a meta-criticism phrase. + + +### The AI slop test + +If someone could look at this interface and say "AI made that" without doubt, it's failed. Cross-register failures are the absolute bans above. Register-specific failures live in each reference. + +**Category-reflex check.** Run at two altitudes; the second one catches what the first one misses. + +- **First-order:** if someone could guess the theme + palette from the category alone, it's the first training-data reflex. Rework the scene sentence and color strategy until the answer isn't obvious from the domain. +- **Second-order:** if someone could guess the aesthetic family from category-plus-anti-references ("AI workflow tool that's not SaaS-cream → editorial-typographic", "fintech that's not navy-and-gold → terminal-native dark mode"), it's the trap one tier deeper. The first reflex was avoided; the second wasn't. Rework until both answers are not obvious. The brand register's [reflex-reject aesthetic lanes](reference/brand.md) list catches the currently-saturated families. + +## Commands + +| Command | Category | Description | Reference | +|---|---|---|---| +| `craft [feature]` | Build | Shape, then build a feature end-to-end | [reference/craft.md](reference/craft.md) | +| `shape [feature]` | Build | Plan UX/UI before writing code | [reference/shape.md](reference/shape.md) | +| `init` | Build | Set up project context: PRODUCT.md, DESIGN.md, live config, next steps | [reference/init.md](reference/init.md) | +| `document` | Build | Generate DESIGN.md from existing project code | [reference/document.md](reference/document.md) | +| `extract [target]` | Build | Pull reusable tokens and components into design system | [reference/extract.md](reference/extract.md) | +| `critique [target]` | Evaluate | UX design review with heuristic scoring | [reference/critique.md](reference/critique.md) | +| `audit [target]` | Evaluate | Technical quality checks (a11y, perf, responsive) | [reference/audit.md](reference/audit.md) | +| `polish [target]` | Refine | Final quality pass before shipping | [reference/polish.md](reference/polish.md) | +| `bolder [target]` | Refine | Amplify safe or bland designs | [reference/bolder.md](reference/bolder.md) | +| `quieter [target]` | Refine | Tone down aggressive or overstimulating designs | [reference/quieter.md](reference/quieter.md) | +| `distill [target]` | Refine | Strip to essence, remove complexity | [reference/distill.md](reference/distill.md) | +| `harden [target]` | Refine | Production-ready: errors, i18n, edge cases | [reference/harden.md](reference/harden.md) | +| `onboard [target]` | Refine | Design first-run flows, empty states, activation | [reference/onboard.md](reference/onboard.md) | +| `animate [target]` | Enhance | Add purposeful animations and motion | [reference/animate.md](reference/animate.md) | +| `colorize [target]` | Enhance | Add strategic color to monochromatic UIs | [reference/colorize.md](reference/colorize.md) | +| `typeset [target]` | Enhance | Improve typography hierarchy and fonts | [reference/typeset.md](reference/typeset.md) | +| `layout [target]` | Enhance | Fix spacing, rhythm, and visual hierarchy | [reference/layout.md](reference/layout.md) | +| `delight [target]` | Enhance | Add personality and memorable touches | [reference/delight.md](reference/delight.md) | +| `overdrive [target]` | Enhance | Push past conventional limits | [reference/overdrive.md](reference/overdrive.md) | +| `clarify [target]` | Fix | Improve UX copy, labels, and error messages | [reference/clarify.md](reference/clarify.md) | +| `adapt [target]` | Fix | Adapt for different devices and screen sizes | [reference/adapt.md](reference/adapt.md) | +| `optimize [target]` | Fix | Diagnose and fix UI performance | [reference/optimize.md](reference/optimize.md) | +| `live` | Iterate | Visual variant mode: pick elements in the browser, generate alternatives | [reference/live.md](reference/live.md) | + +Plus two management commands: `pin ` and `unpin `, detailed below. + +### Routing rules + +1. **No argument**: render the table above as the user-facing command menu, grouped by category. Ask what they'd like to do. +2. **First word matches a command**: load its reference file and follow its instructions. Everything after the command name is the target. +3. **First word doesn't match, but the intent clearly maps to one command** (e.g. "fix the spacing" → `layout`, "rewrite this error message" → `clarify`, "the colors feel flat" → `colorize`): load that command's reference and proceed as if invoked. If two commands could fit, ask once which. +4. **No clear command match**: general design invocation. Apply the setup steps, the General rules, and the loaded register reference, using the full argument as context. + +Setup (context gathering, register) is already loaded by then; sub-commands don't re-invoke `{{command_prefix}}impeccable`. + +If the first word is `craft`, setup still runs first, but [reference/craft.md](reference/craft.md) owns the rest of the flow. If setup invokes `init` as a blocker, finish init, refresh context, then resume the original command and target. + +`teach` is a deprecated alias for `init`: if the user types it, load [reference/init.md](reference/init.md) and proceed as if they ran `init`. + +## Pin / Unpin + +**Pin** creates a standalone shortcut so `{{command_prefix}}` invokes `{{command_prefix}}impeccable ` directly. **Unpin** removes it. The script writes to every harness directory present in the project. + +```bash +node {{scripts_path}}/pin.mjs +``` + +Valid `` is any command from the table above. Report the script's result concisely. Confirm the new shortcut on success, relay stderr verbatim on error. diff --git a/skill/reference/adapt.md b/skill/reference/adapt.md index 6ac6a798f..1d053061c 100644 --- a/skill/reference/adapt.md +++ b/skill/reference/adapt.md @@ -188,3 +188,124 @@ Test thoroughly across contexts: - **Slow connections**: Test on throttled network When the adaptation feels native to each context, hand off to `{{command_prefix}}impeccable polish` for the final pass. + +--- + +## Reference Material + +The sections below were previously `responsive-design.md` and live inline now so the adapt flow has its deep responsive reference in one place. + +### Responsive Design + +#### Mobile-First: Write It Right + +Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first. + +#### Breakpoints: Content-Driven + +Don't chase device sizes; let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints. + +#### Detect Input Method, Not Just Screen Size + +**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard. Use pointer and hover queries: + +```css +/* Fine pointer (mouse, trackpad) */ +@media (pointer: fine) { + .button { padding: 8px 16px; } +} + +/* Coarse pointer (touch, stylus) */ +@media (pointer: coarse) { + .button { padding: 12px 20px; } /* Larger touch target */ +} + +/* Device supports hover */ +@media (hover: hover) { + .card:hover { transform: translateY(-2px); } +} + +/* Device doesn't support hover (touch) */ +@media (hover: none) { + .card { /* No hover state - use active instead */ } +} +``` + +**Critical**: Don't rely on hover for functionality. Touch users can't hover. + +#### Safe Areas: Handle the Notch + +Modern phones have notches, rounded corners, and home indicators. Use `env()`: + +```css +body { + padding-top: env(safe-area-inset-top); + padding-bottom: env(safe-area-inset-bottom); + padding-left: env(safe-area-inset-left); + padding-right: env(safe-area-inset-right); +} + +/* With fallback */ +.footer { + padding-bottom: max(1rem, env(safe-area-inset-bottom)); +} +``` + +**Enable viewport-fit** in your meta tag: +```html + +``` + +#### Responsive Images: Get It Right + +##### srcset with Width Descriptors + +```html +Hero image +``` + +**How it works**: +- `srcset` lists available images with their actual widths (`w` descriptors) +- `sizes` tells the browser how wide the image will display +- Browser picks the best file based on viewport width AND device pixel ratio + +##### Picture Element for Art Direction + +When you need different crops/compositions (not just resolutions): + +```html + + + + ... + +``` + +#### Layout Adaptation Patterns + +**Navigation**: Three stages: hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `
/` for content that can collapse on mobile. + +#### Testing: Don't Trust DevTools Alone + +DevTools device emulation is useful for layout but misses: + +- Actual touch interactions +- Real CPU/memory constraints +- Network latency patterns +- Font rendering differences +- Browser chrome/keyboard appearances + +**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators. + +--- + +**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful. diff --git a/skill/reference/animate.md b/skill/reference/animate.md index a73450aad..fcb330cf6 100644 --- a/skill/reference/animate.md +++ b/skill/reference/animate.md @@ -6,7 +6,7 @@ Add motion that conveys state, gives feedback, and clarifies hierarchy. Cut moti ## Register -Brand: orchestrated page-load sequences, staggered reveals, scroll-driven animation. Motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions. +Brand: motion is part of the voice; one well-rehearsed entrance beats scattered micro-interactions. The saturated AI default is fade-and-rise reveals on every scrolled section; that's a tell, not a choreography. Reserve scroll-triggered motion for moments that earn it. Product: 150–250 ms on most transitions. Motion conveys state: feedback, reveal, loading, transitions between views. No page-load choreography; users are in a task and won't wait for it. @@ -49,10 +49,11 @@ Create a purposeful animation plan: Add motion systematically across these categories: ### Entrance Animations -- **Page load choreography**: Stagger element reveals (100-150ms delays), fade + slide combinations - **Hero section**: Dramatic entrance for primary content (scale, parallax, or creative effects) -- **Content reveals**: Scroll-triggered animations using intersection observer - **Modal/drawer entry**: Smooth slide + fade, backdrop fade, focus management +- **List rhythm**: Sibling stagger is legitimate for cards-in-a-grid or list-items-appearing. Whole-section fade-on-scroll is not a list and is not legitimate. Cap total stagger time: 10 items at 50ms each = 500ms total. For more items, reduce per-item delay or cap the staggered count. + + Use CSS custom properties for clean stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"`, `style="--i: 1"`, etc. on each item. ### Micro-interactions - **Button feedback**: @@ -97,11 +98,14 @@ Use appropriate techniques for each animation: ### Timing & Easing -**Durations by purpose:** -- **100-150ms**: Instant feedback (button press, toggle) -- **200-300ms**: State changes (hover, menu open) -- **300-500ms**: Layout changes (accordion, modal) -- **500-800ms**: Entrance animations (page load) +**Duration: the 100/300/500 rule.** Timing matters more than easing for "feels right": + +| Duration | Use Case | Examples | +|----------|----------|----------| +| **100–150ms** | Instant feedback | Button press, toggle, color change | +| **200–300ms** | State changes | Menu open, tooltip, hover state | +| **300–500ms** | Layout changes | Accordion, modal, drawer | +| **500–800ms** | Entrance animations | Page load, hero reveal | **Easing curves (use these, not CSS defaults):** ```css @@ -134,13 +138,35 @@ Use appropriate techniques for each animation: - GSAP for complex sequences ``` +### Motion Materials + +Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties. Match material to effect: + +- **Transform / opacity**: movement, press feedback, simple reveals, list choreography +- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances +- **Clip-path / masks**: wipes, reveals, editorial cropping, product-like transitions +- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state +- **Grid-template-rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly + +The hard rule isn't "transform and opacity only." It's: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify smoothness in-browser on target viewports. + ### Performance -- **Motion materials**: Use transform/opacity for reliable movement, but use blur, filters, masks, shadows, and color shifts when they materially improve the effect - **Layout safety**: Avoid casual animation of layout-driving properties (`width`, `height`, `top`, `left`, margins) -- **will-change**: Add sparingly for known expensive animations +- **will-change**: Add sparingly for known expensive animations only (e.g. on `:hover` or an `.animating` class), never preemptively across the whole page +- **Scroll triggers**: Use Intersection Observer instead of scroll event listeners; unobserve after the animation fires once - **Bound expensive effects**: Keep blur/filter/shadow areas small or isolated, use `contain` where appropriate - **Monitor FPS**: Ensure 60fps on target devices +### Perceived Performance + +Nobody cares how fast your site *is*, only how fast it feels. The 80ms threshold: anything under ~80ms feels instant because our brains buffer sensory input for that long to synchronize perception. Target this for micro-interactions. + +- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening. +- **Early completion**: Show content progressively, don't wait for everything (progressive images, streaming HTML, skeleton fade-ins). +- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Use for low-stakes actions (likes, follows). Avoid for payments or destructive operations. +- **Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances. +- **Caution**: Too-fast responses can decrease perceived value for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening. + ### Accessibility ```css @media (prefers-reduced-motion: reduce) { diff --git a/skill/reference/bolder.md b/skill/reference/bolder.md index 7c6b756ba..cc1e0a8c1 100644 --- a/skill/reference/bolder.md +++ b/skill/reference/bolder.md @@ -50,7 +50,7 @@ Create a strategy to increase impact while maintaining coherence: Systematically increase impact across these dimensions: ### Typography Amplification -- **Replace generic fonts**: Swap system fonts for distinctive choices (see the parent skill's typography guidelines and [typography.md](typography.md) for inspiration) +- **Replace generic fonts**: Swap system fonts for distinctive choices (see the parent skill's typography guidelines and the [Reference Material section of typeset.md](typeset.md#reference-material) for inspiration) - **Extreme scale**: Create dramatic size jumps (3x-5x differences, not 1.5x) - **Weight contrast**: Pair 900 weights with 200 weights, not 600 with 400 - **Unexpected choices**: Variable fonts, display fonts for headlines, condensed/extended widths, monospace as intentional accent (not as lazy "dev tool" default) @@ -78,10 +78,10 @@ Systematically increase impact across these dimensions: - **Custom elements**: Illustrative elements, custom icons, decorative details that reinforce brand ### Motion & Animation -- **Entrance choreography**: Staggered, dramatic page load animations with 50-100ms delays -- **Scroll effects**: Parallax, reveal animations, scroll-triggered sequences -- **Micro-interactions**: Satisfying hover effects, click feedback, state changes -- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic, which cheapen the effect) +- **Hero moment**: One signature entrance, once. Not on every visit and not on every section. +- **Micro-interactions**: Satisfying hover effects, click feedback, state changes. +- **Transitions**: Smooth, noticeable transitions using ease-out-quart/quint/expo (not bounce or elastic, which cheapen the effect). +- **Bolder ≠ scroll-fade-rise on every section.** That's the saturated AI default, the opposite of bold. ### Composition Boldness - **Hero moments**: Create clear focal points with dramatic treatment diff --git a/skill/reference/brand.md b/skill/reference/brand.md index 3d83a1cdc..ba8c2b34c 100644 --- a/skill/reference/brand.md +++ b/skill/reference/brand.md @@ -18,7 +18,7 @@ Then the inverse test: in one sentence, describe what you're about to build the ### Font selection procedure -Every project. Never skip. +Every project. Never skip. 1. Read the brief. Write three concrete brand-voice words. Not "modern" or "elegant," but "warm and mechanical and opinionated" or "calm and clinical and careful." Physical-object words. 2. List the three fonts you'd reach for by reflex. If any appear in the reflex-reject list below, reject them; they are training-data defaults and they create monoculture. @@ -27,13 +27,13 @@ Every project. Never skip. ### Reflex-reject list -Training-data defaults. Ban list. Look further: +Training-data defaults. Ban list. Look further: Fraunces · Newsreader · Lora · Crimson · Crimson Pro · Crimson Text · Playfair Display · Cormorant · Cormorant Garamond · Syne · IBM Plex Mono · IBM Plex Sans · IBM Plex Serif · Space Mono · Space Grotesk · Inter · DM Sans · DM Serif Display · DM Serif Text · Outfit · Plus Jakarta Sans · Instrument Sans · Instrument Serif ### Reflex-reject aesthetic lanes -Parallel to the font list. Currently saturated aesthetic families that have flooded brand surfaces. If a brief lands in one of these lanes without a register reason that *requires* it (a literal magazine, a literal terminal, a literal industrial signage system), it's the second-order training reflex: the trap one tier deeper than picking a Fraunces font. Look further. +Parallel to the font list. Currently saturated aesthetic families that have flooded brand surfaces. If a brief lands in one of these lanes without a register reason that *requires* it (a literal magazine, a literal terminal, a literal industrial signage system), it's the second-order training reflex: the trap one tier deeper than picking a Fraunces font. Look further. - **Editorial-typographic.** Display serif (often italic) + small mono labels + ruled separators + monochromatic restraint. Klim-influenced, magazine-cover affectation. By 2026, every Stripe-adjacent and Notion-adjacent brand has landed here. The fingerprint: three rule-separated columns, an italic Fraunces / Recoleta / Newsreader headline, lowercase track-spaced metadata, no imagery. @@ -43,76 +43,66 @@ The reflex-reject lists apply to **new design choices**. When the existing brand ### Pairing and voice -Distinctive + refined is the goal. The specific shape depends on the brand: - -- **Editorial / long-form / luxury**: display serif + sans body (a magazine shape). -- **Tech / dev tools / fintech**: one committed sans, usually; custom-tight tracking, strong weight contrast inside a single family. -- **Consumer / food / travel**: warmer pairings, often a humanist sans plus a script or display serif. -- **Creative studios / agencies**: rule-breaking welcome. Mono-only, or display-only, or custom-drawn type as voice. +Distinctive + refined is the goal. The specific shape depends on the brand, not on the brand's category. A category ("restaurant", "dev tool", "magazine", "fintech") is not a recipe; treating it as one is the first-order reflex SKILL.md warns against. Two families minimum is the rule *only* when the voice needs it. A single well-chosen family with committed weight/size contrast is stronger than a timid display+body pair. -Vary across projects. If the last brief was a serif-display landing page, this one isn't. - ### Scale -Modular scale, fluid `clamp()` for headings, ≥1.25 ratio between steps. Flat scales (1.1× apart) read as uncommitted. +Modular scale, fluid `clamp()` for headings, ≥1.25 ratio between steps. Flat scales (1.1× apart) read as uncommitted. -Light text on dark backgrounds: add 0.05–0.1 to line-height. Light type reads as lighter weight and needs more breathing room. +Light text on dark backgrounds: add 0.05–0.1 to line-height. Light type reads as lighter weight and needs more breathing room. ## Color -Brand surfaces have permission for Committed, Full palette, and Drenched strategies. Use them. A single saturated color spread across a hero is not excess; it's voice. A beige-and-muted-slate landing page ignores the register. +Brand surfaces have permission for Committed, Full palette, and Drenched strategies. Use them. A single saturated color spread across a hero is not excess; it's voice. A beige-and-muted-slate landing page ignores the register. -- Name a real reference before picking a strategy. "Klim Type Foundry #ff4500 orange drench", "Stripe purple-on-white restraint", "Liquid Death acid-green full palette", "Mailchimp yellow full palette", "Condé Nast Traveler muted navy restraint", "Vercel pure black monochrome". Unnamed ambition becomes beige. -- Palette IS voice. A calm brand and a restless brand should not share palette mechanics. -- When the strategy is Committed or Drenched, color carries the brand. Don't hedge with neutrals around the edges. Commit. -- Don't converge across projects. If the last brand surface was restrained-on-cream, this one is not. -- When a cultural-symbol palette is the obvious pull, reach past it. Let the cultural reading come from typography, imagery, and copy, not the palette. +- Name a real reference before picking a strategy. "Klim Type Foundry #ff4500 orange drench", "Stripe purple-on-white restraint", "Liquid Death acid-green full palette", "Mailchimp yellow full palette", "Condé Nast Traveler muted navy restraint", "Vercel pure black monochrome". Unnamed ambition becomes beige. +- Palette IS voice. A calm brand and a restless brand should not share palette mechanics. +- When the strategy is Committed or Drenched, color carries the brand. Don't hedge with neutrals around the edges. Commit. +- Don't converge across projects. If the last brand surface was restrained-on-cream, this one is not. +- When a cultural-symbol palette is the obvious pull, reach past it. Let the cultural reading come from typography, imagery, and copy, not the palette. ## Layout -- Asymmetric compositions are one option. Break the grid intentionally for emphasis. -- Fluid spacing with `clamp()` that breathes on larger viewports. Vary for rhythm: generous separations, tight groupings. -- Alternative: a strict, visible grid as the voice (brutalist / Swiss / tech-spec aesthetics). Either asymmetric or rigorously-gridded can be "designed"; the failure mode is splitting the difference into a generic centered stack. -- Don't default to centering everything. Left-aligned with asymmetric layouts feels more designed; a strict grid reads as confident structure. A centered-stack hero with icon-title-subtitle cards reads as template. -- When cards ARE the right affordance, use `grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` for breakpoint-free responsiveness. +- Asymmetric compositions are one option. Break the grid intentionally for emphasis. +- Fluid spacing with `clamp()` that breathes on larger viewports. Vary for rhythm: generous separations, tight groupings. +- For image-led briefs (hotels, restaurants, magazines, photography), full-bleed hero imagery with overlaid menu and centered headline is a canonical move; let the photograph be the design. +- When cards ARE the right affordance, use `grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` for breakpoint-free responsiveness. ## Imagery Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landing page without any imagery reads as incomplete, not as restrained. A solid-color rectangle where a hero image should go is worse than a representative stock photo. -**When the brief implies imagery (restaurants, hotels, magazines, photography, hobbyist communities, food, travel, fashion, product), you must ship imagery.** Zero images is a bug, not a design choice. "Restraint" is not an excuse. If the approved comp or brief is image-led, ship real project assets, generated raster assets, or a credible canvas/SVG/WebGL scene. Do not replace photographic, architectural, product, or place imagery with generic CSS panels, decorative diagrams, cards, bullets, or copy. +**When the brief implies imagery (restaurants, hotels, magazines, photography, hobbyist communities, food, travel, fashion, product), you must ship imagery.** Zero images is a bug, not a design choice. "Restraint" is not an excuse. If the approved comp or brief is image-led, ship real project assets, generated raster assets, or a credible canvas/SVG/WebGL scene. Do not replace photographic, architectural, product, or place imagery with generic CSS panels, decorative diagrams, cards, bullets, or copy. -- **For greenfield work without local assets, use stock imagery.** Unsplash is the default. The URL shape is `https://images.unsplash.com/photo-{id}?auto=format&fit=crop&w=1600&q=80`. **Verify the URLs before referencing them.** If you have an image-search MCP, web-fetch tool, or browser access, use it to find real photo IDs and confirm they resolve. Guessed IDs (even ones that look real) often 404 and ship as broken-image placeholders. Without a verification path, pick fewer photos you're confident exist over more that you guessed; never substitute colored `
` placeholders. -- **Search for the brand's physical object**, not the generic category: "handmade pasta on a scratched wooden table" beats "Italian food"; "cypress trees above a limestone hotel facade at dusk" beats "luxury hotel". -- **One decisive photo beats five mediocre ones.** Hero imagery should commit to a mood; padding with more stock doesn't rescue an indecisive one. -- **Alt text is part of the voice.** "Coastal fettuccine, hand-cut, served on the terrace" beats "pasta dish". +- **For greenfield work without local assets, use stock imagery.** Unsplash is the default. The URL shape is `https://images.unsplash.com/photo-{id}?auto=format&fit=crop&w=1600&q=80`. **Verify the URLs before referencing them.** If you have an image-search MCP, web-fetch tool, or browser access, use it to find real photo IDs and confirm they resolve. Guessed IDs (even ones that look real) often 404 and ship as broken-image placeholders. Without a verification path, pick fewer photos you're confident exist over more that you guessed; never substitute colored `
` placeholders. +- **Search for the brand's physical object**, not the generic category: "handmade pasta on a scratched wooden table" beats "Italian food"; "cypress trees above a limestone hotel facade at dusk" beats "luxury hotel". +- **One decisive photo beats five mediocre ones.** Hero imagery should commit to a mood; padding with more stock doesn't rescue an indecisive one. +- **Alt text is part of the voice.** "Coastal fettuccine, hand-cut, served on the terrace" beats "pasta dish". "Imagery" here is broader than stock photography: product screenshots, custom data visualizations, generated SVG, and canvas/WebGL scenes are all imagery. Text-only pages where typography alone carries the entire visual weight are the failure mode. ## Motion -- One well-orchestrated page-load with staggered reveals beats scattered micro-interactions, when the brand invites it. Tech-minimal brands often skip entrance motion entirely; the restraint is the voice. -- For collapsing/expanding sections, transition `grid-template-rows` rather than `height`. +- One well-orchestrated page-load beats scattered micro-interactions, when the brand invites it. Some brands skip entrance motion entirely; the restraint is the voice. ## Brand bans (on top of the shared absolute bans) -- Monospace as lazy shorthand for "technical / developer." If the brand isn't technical, mono reads as costume. -- Large rounded-corner icons above every heading. Screams template. -- Single-family pages that picked the family by reflex, not voice. (A single family chosen deliberately is fine.) -- All-caps body copy. Reserve caps for short labels and headings. -- Timid palettes and average layouts. Safe = invisible. -- Zero imagery on a brief that implies imagery (restaurant, hotel, food, travel, fashion, photography, hobbyist). Colored blocks where a hero photo belongs. -- Defaulting to editorial-magazine aesthetics (display serif + italic + drop caps + broadsheet grid) on briefs that aren't magazine-shaped. Editorial is ONE aesthetic lane, not the default brand aesthetic. -- Repeated tiny uppercase tracked labels above every section heading. A single strong kicker can be voice; repeating it as section grammar is AI scaffolding unless it's a deliberate, named brand system. +- Monospace as lazy shorthand for "technical / developer." If the brand isn't technical, mono reads as costume. +- Large rounded-corner icons above every heading. Screams template. +- Single-family pages that picked the family by reflex, not voice. (A single family chosen deliberately is fine.) +- All-caps body copy. Reserve caps for short labels and headings. +- Timid palettes and average layouts. Safe = invisible. +- Zero imagery on a brief that implies imagery (restaurant, hotel, food, travel, fashion, photography, hobbyist). Colored blocks where a hero photo belongs. +- Defaulting to editorial-magazine aesthetics (display serif + italic + drop caps + broadsheet grid) on briefs that aren't magazine-shaped. Editorial is ONE aesthetic lane, not the default brand aesthetic. +- Repeated tiny uppercase tracked labels above every section heading. A single strong kicker can be voice; repeating it as section grammar is AI scaffolding unless it's a deliberate, named brand system. ## Brand permissions Brand can afford things product can't. Take them. -- Ambitious first-load motion. Reveals, scroll-triggered transitions, typographic choreography. -- Single-purpose viewports. One dominant idea per fold, long scroll, deliberate pacing. -- Typographic risk. Enormous display type, unexpected italic cuts, mixed cases, hand-drawn headlines, a single oversize word as a hero. -- Unexpected color strategies. Palette IS voice; a calm brand and a restless brand should not share palette mechanics. -- Art direction per section. Different sections can have different visual worlds if the narrative demands it. Consistency of voice beats consistency of treatment. +- Ambitious first-load motion. Reveals and typographic choreography that earn their place; not fade-on-scroll for every section. +- Single-purpose viewports. One dominant idea per fold, long scroll, deliberate pacing. +- Unexpected color strategies. Palette IS voice; a calm brand and a restless brand should not share palette mechanics. +- Art direction per section. Different sections can have different visual worlds if the narrative demands it. Consistency of voice beats consistency of treatment. diff --git a/skill/reference/clarify.md b/skill/reference/clarify.md index f48861647..a4895a5d4 100644 --- a/skill/reference/clarify.md +++ b/skill/reference/clarify.md @@ -172,3 +172,117 @@ Test that copy improvements work: - **Tone**: Is it appropriate for the situation? When the copy reads cleanly, hand off to `{{command_prefix}}impeccable polish` for the final pass. + +--- + +## Reference Material + +The sections below were previously `ux-writing.md` and live inline now so the clarify flow has its deep UX-writing reference in one place. + +### UX Writing + +#### The Button Label Problem + +**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns: + +| Bad | Good | Why | +|-----|------|-----| +| OK | Save changes | Says what will happen | +| Submit | Create account | Outcome-focused | +| Yes | Delete message | Confirms the action | +| Cancel | Keep editing | Clarifies what "cancel" means | +| Click here | Download PDF | Describes the destination | + +**For destructive actions**, name the destruction: +- "Delete" not "Remove" (delete is permanent, remove implies recoverable) +- "Delete 5 items" not "Delete selected" (show the count) + +#### Error Messages: The Formula + +Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input". + +##### Error Message Templates + +| Situation | Template | +|-----------|----------| +| **Format error** | "[Field] needs to be [format]. Example: [example]" | +| **Missing required** | "Please enter [what's missing]" | +| **Permission denied** | "You don't have access to [thing]. [What to do instead]" | +| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." | +| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" | + +##### Don't Blame the User + +Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date". + +#### Empty States Are Opportunities + +Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items". + +#### Voice vs Tone + +**Voice** is your brand's personality, consistent everywhere. +**Tone** adapts to the moment. + +| Moment | Tone Shift | +|--------|------------| +| Success | Celebratory, brief: "Done! Your changes are live." | +| Error | Empathetic, helpful: "That didn't work. Here's what to try..." | +| Loading | Reassuring: "Saving your work..." | +| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | + +**Never use humor for errors.** Users are already frustrated. Be helpful, not cute. + +#### Writing for Accessibility + +**Link text** must have standalone meaning: "View pricing plans" not "Click here". **Alt text** describes information, not the image: "Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context. + +#### Writing for Translation + +##### Plan for Expansion + +German text is ~30% longer than English. Allocate space: + +| Language | Expansion | +|----------|-----------| +| German | +30% | +| French | +20% | +| Finnish | +30-40% | +| Chinese | -30% (fewer chars, but same width) | + +##### Translation-Friendly Patterns + +Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear. + +#### Consistency: The Terminology Problem + +Pick one term and stick with it: + +| Inconsistent | Consistent | +|--------------|------------| +| Delete / Remove / Trash | Delete | +| Settings / Preferences / Options | Settings | +| Sign in / Log in / Enter | Sign in | +| Create / Add / New | Create | + +Build a terminology glossary and enforce it. Variety creates confusion. + +#### Avoid Redundant Copy + +If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well. + +#### Loading States + +Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress. + +#### Confirmation Dialogs: Use Sparingly + +Most confirmation dialogs are design failures; consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No"). + +#### Form Instructions + +Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking. + +--- + +**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors. diff --git a/skill/reference/codex.md b/skill/reference/codex.md index 0901e64f4..923bb899b 100644 --- a/skill/reference/codex.md +++ b/skill/reference/codex.md @@ -23,8 +23,8 @@ Before generating anything, run a brief direction conversation grounded in the s Ask **2-3 targeted questions** about visual lane, color strategy, atmosphere, and named anchor references. Don't enumerate generic menus; tie each question to the shape brief's answers. Example shape-grounded questions: -- "Brief says 'editorial restraint, Klim-adjacent.' Are we closer to a quiet specimen page or a magazine-spread feel with hero imagery?" -- "Palette strategy from shape was 'Committed.' Want it warm-grounded (deep oxblood + cream) or cool-grounded (slate + paper white)?" +- "Brief says 'specimen-page restraint.' Are we closer to a quiet typographic page or a wider editorial spread with hero imagery?" +- "Palette strategy from shape was 'Committed.' Which one color carries the surface (a brand-driven pick rather than a default warm-or-cool framing)? (And no, the answer isn't a cream/sand body bg; that's the saturated AI default.)" **STOP and wait for answers.** These pin the palette before any pixel gets generated. Do not proceed to Step B until the user has responded. diff --git a/skill/reference/cognitive-load.md b/skill/reference/cognitive-load.md deleted file mode 100644 index 48f8ad58b..000000000 --- a/skill/reference/cognitive-load.md +++ /dev/null @@ -1,106 +0,0 @@ -# Cognitive Load Assessment - -Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload. - ---- - -## Three Types of Cognitive Load - -### Intrinsic Load: The Task Itself -Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it. - -**Manage it by**: -- Breaking complex tasks into discrete steps -- Providing scaffolding (templates, defaults, examples) -- Progressive disclosure: show what's needed now, hide the rest -- Grouping related decisions together - -### Extraneous Load: Bad Design -Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste. - -**Common sources**: -- Confusing navigation that requires mental mapping -- Unclear labels that force users to guess meaning -- Visual clutter competing for attention -- Inconsistent patterns that prevent learning -- Unnecessary steps between user intent and result - -### Germane Load: Learning Effort -Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery. - -**Support it by**: -- Progressive disclosure that reveals complexity gradually -- Consistent patterns that reward learning -- Feedback that confirms correct understanding -- Onboarding that teaches through action, not walls of text - ---- - -## Cognitive Load Checklist - -Evaluate the interface against these 8 items: - -- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements? -- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)? -- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)? -- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen? -- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next? -- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)? -- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one? -- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it? - -**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed). - ---- - -## The Working Memory Rule - -**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001). - -At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider: -- **≤4 items**: Within working memory limits, manageable -- **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure -- **8+ items**: Overloaded; users will skip, misclick, or abandon - -**Practical applications**: -- Navigation menus: ≤5 top-level items (group the rest under clear categories) -- Form sections: ≤4 fields visible per group before a visual break -- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu -- Dashboard widgets: ≤4 key metrics visible without scrolling -- Pricing tiers: ≤3 options (more causes analysis paralysis) - ---- - -## Common Cognitive Load Violations - -### 1. The Wall of Options -**Problem**: Presenting 10+ choices at once with no hierarchy. -**Fix**: Group into categories, highlight recommended, use progressive disclosure. - -### 2. The Memory Bridge -**Problem**: User must remember info from step 1 to complete step 3. -**Fix**: Keep relevant context visible, or repeat it where it's needed. - -### 3. The Hidden Navigation -**Problem**: User must build a mental map of where things are. -**Fix**: Always show current location (breadcrumbs, active states, progress indicators). - -### 4. The Jargon Barrier -**Problem**: Technical or domain language forces translation effort. -**Fix**: Use plain language. If domain terms are unavoidable, define them inline. - -### 5. The Visual Noise Floor -**Problem**: Every element has the same visual weight; nothing stands out. -**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted. - -### 6. The Inconsistent Pattern -**Problem**: Similar actions work differently in different places. -**Fix**: Standardize interaction patterns. Same type of action = same type of UI. - -### 7. The Multi-Task Demand -**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating). -**Fix**: Sequence the steps. Let the user do one thing at a time. - -### 8. The Context Switch -**Problem**: User must jump between screens/tabs/modals to gather info for a single decision. -**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth. diff --git a/skill/reference/color-and-contrast.md b/skill/reference/color-and-contrast.md deleted file mode 100644 index 110c2ee47..000000000 --- a/skill/reference/color-and-contrast.md +++ /dev/null @@ -1,105 +0,0 @@ -# Color & Contrast - -## Color Spaces: Use OKLCH - -**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal, unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark. - -The OKLCH function takes three components: `oklch(lightness chroma hue)` where lightness is 0-100%, chroma is roughly 0-0.4, and hue is 0-360. To build a primary color and its lighter / darker variants, hold the chroma+hue roughly constant and vary the lightness, but **reduce chroma as you approach white or black**, because high chroma at extreme lightness looks garish. - -The hue you pick is a brand decision and should not come from a default. Do not reach for blue (hue 250) or warm orange (hue 60) by reflex; those are the dominant AI-design defaults, not the right answer for any specific brand. - -## Building Functional Palettes - -### Tinted Neutrals - -**Pure gray is dead.** A neutral with zero chroma feels lifeless next to a colored brand. Add a tiny chroma value (0.005-0.015) to all your neutrals, hued toward whatever your brand color is. The chroma is small enough not to read as "tinted" consciously, but it creates subconscious cohesion between brand color and UI surfaces. - -The hue you tint toward should come from THIS project's brand, not from a "warm = friendly, cool = tech" formula. If your brand color is teal, your neutrals lean toward teal. If your brand color is amber, they lean toward amber. The point is cohesion with the SPECIFIC brand, not a stock palette. - -**Avoid** the trap of always tinting toward warm orange or always tinting toward cool blue. Those are the two laziest defaults and they create their own monoculture across projects. - -### Palette Structure - -A complete system needs: - -| Role | Purpose | Example | -|------|---------|---------| -| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades | -| **Neutral** | Text, backgrounds, borders | 9-11 shade scale | -| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each | -| **Surface** | Cards, modals, overlays | 2-3 elevation levels | - -**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise. - -### The 60-30-10 Rule (Applied Correctly) - -This rule is about **visual weight**, not pixel count: - -- **60%**: Neutral backgrounds, white space, base surfaces -- **30%**: Secondary colors: text, borders, inactive states -- **10%**: Accent: CTAs, highlights, focus states - -The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power. - -## Contrast & Accessibility - -### WCAG Requirements - -| Content Type | AA Minimum | AAA Target | -|--------------|------------|------------| -| Body text | 4.5:1 | 7:1 | -| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | -| UI components, icons | 3:1 | 4.5:1 | -| Non-essential decorations | None | None | - -**The gotcha**: Placeholder text still needs 4.5:1. That light gray placeholder you see everywhere? Usually fails WCAG. - -### Dangerous Color Combinations - -These commonly fail contrast or cause readability issues: - -- Light gray text on white (the #1 accessibility fail) -- **Gray text on any colored background**: gray looks washed out and dead on color. Use a darker shade of the background color, or transparency -- Red text on green background (or vice versa): 8% of men can't distinguish these -- Blue text on red background (vibrates visually) -- Yellow text on white (almost always fails) -- Thin light text on images (unpredictable contrast) - -### Never Use Pure Gray or Pure Black - -Pure gray (`oklch(50% 0 0)`) and pure black (`#000`) don't exist in nature; real shadows and surfaces always have a color cast. Even a chroma of 0.005-0.01 is enough to feel natural without being obviously tinted. (See tinted neutrals example above.) - -### Testing - -Don't trust your eyes. Use tools: - -- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) -- Browser DevTools → Rendering → Emulate vision deficiencies -- [Polypane](https://polypane.app/) for real-time testing - -## Theming: Light & Dark Mode - -### Dark Mode Is Not Inverted Light Mode - -You can't just swap colors. Dark mode requires different design decisions: - -| Light Mode | Dark Mode | -|------------|-----------| -| Shadows for depth | Lighter surfaces for depth (no shadows) | -| Dark text on light | Light text on dark (reduce font weight) | -| Vibrant accents | Desaturate accents slightly | -| White backgrounds | Never pure black; use dark gray (oklch 12-18%) | - -In dark mode, depth comes from surface lightness, not shadow. Build a 3-step surface scale where higher elevations are lighter (e.g. 15% / 20% / 25% lightness). Use the SAME hue and chroma as your brand color (whatever it is for THIS project; do not reach for blue) and only vary the lightness. Reduce body text weight slightly (e.g. 350 instead of 400) because light text on dark reads as heavier than dark text on light. - -### Token Hierarchy - -Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer; primitives stay the same. - -## Alpha Is A Design Smell - -Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed. - ---- - -**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Using pure black (#000) for large areas. Skipping color blindness testing (8% of men affected). diff --git a/skill/reference/colorize.md b/skill/reference/colorize.md index 59c40bc88..46e5cabc4 100644 --- a/skill/reference/colorize.md +++ b/skill/reference/colorize.md @@ -68,10 +68,10 @@ Add color systematically across these dimensions: - **Hover states**: Introduce color on interaction ### Background & Surfaces -- **Tinted backgrounds**: Replace pure gray (`#f5f5f5`) with warm neutrals (`oklch(97% 0.01 60)`) or cool tints (`oklch(97% 0.01 250)`) +- **Tinted backgrounds**: If you replace pure gray, tint toward the brand hue, not toward a generic-warm-or-cool pair. The default-warm-tint (`oklch(97% 0.01 60)` and its neighbors) is now the AI cream/sand giveaway. Be specific to the brand or stay neutral. - **Colored sections**: Use subtle background colors to separate areas - **Gradient backgrounds**: Add depth with subtle, intentional gradients (not generic purple-blue) -- **Cards & surfaces**: Tint cards or surfaces slightly for warmth +- **Cards & surfaces**: Tint cards or surfaces toward the brand, not "for warmth" by reflex **Use OKLCH for color**: It's perceptually uniform, meaning equal steps in lightness *look* equal. Great for generating harmonious scales. @@ -124,8 +124,6 @@ Ensure color addition improves rather than overwhelms: - Use every color in the rainbow (choose 2-4 colors beyond neutrals) - Apply color randomly without semantic meaning - Put gray text on colored backgrounds. It looks washed out; use a darker shade of the background color or transparency instead -- Use pure gray for neutrals. Add subtle color tint (warm or cool) for depth -- Use pure black (`#000`) or pure white (`#fff`) for large areas - Violate WCAG contrast requirements - Use color as the only indicator (accessibility issue) - Make everything colorful (defeats the purpose) @@ -152,3 +150,108 @@ When invoked from live mode, each variant MUST declare a `color-amount` param so ``` Layer 1-2 variant-specific params on top: palette selection (`steps` with named options), temperature warmth, or tint vs. true color. See `reference/live.md` for the full params contract. + +--- + +## Reference Material + +The sections below were previously `color-and-contrast.md` and live inline now so the colorize flow has its deep color reference in one place. + +### Color & Contrast + +#### Color Spaces: Use OKLCH + +**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal, unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark. + +The OKLCH function takes three components: `oklch(lightness chroma hue)` where lightness is 0-100%, chroma is roughly 0-0.4, and hue is 0-360. To build a primary color and its lighter / darker variants, hold the chroma+hue roughly constant and vary the lightness, but **reduce chroma as you approach white or black**, because high chroma at extreme lightness looks garish. + +The hue you pick is a brand decision and should not come from a default. Do not reach for blue (hue 250) or warm orange (hue 60) by reflex; those are the dominant AI-design defaults, not the right answer for any specific brand. + +#### Building Functional Palettes + +##### Tinted Neutrals + +**Pure gray is dead.** A neutral with zero chroma feels lifeless next to a colored brand. Add a tiny chroma value (0.005-0.015) to all your neutrals, hued toward whatever your brand color is. The chroma is small enough not to read as "tinted" consciously, but it creates subconscious cohesion between brand color and UI surfaces. + +The hue you tint toward should come from THIS project's brand, not from a "warm = friendly, cool = tech" formula. If your brand color is teal, your neutrals lean toward teal. If your brand color is amber, they lean toward amber. The point is cohesion with the SPECIFIC brand, not a stock palette. + +**Avoid** the trap of always tinting toward warm orange or always tinting toward cool blue. Those are the two laziest defaults and they create their own monoculture across projects. + +##### Palette Structure + +A complete system needs: + +| Role | Purpose | Example | +|------|---------|---------| +| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades | +| **Neutral** | Text, backgrounds, borders | 9-11 shade scale | +| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each | +| **Surface** | Cards, modals, overlays | 2-3 elevation levels | + +**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise. + +##### The 60-30-10 Rule (Applied Correctly) + +This rule is about **visual weight**, not pixel count: + +- **60%**: Neutral backgrounds, white space, base surfaces +- **30%**: Secondary colors: text, borders, inactive states +- **10%**: Accent: CTAs, highlights, focus states + +The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power. + +#### Contrast & Accessibility + +##### WCAG Requirements + +| Content Type | AA Minimum | AAA Target | +|--------------|------------|------------| +| Body text | 4.5:1 | 7:1 | +| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 | +| UI components, icons | 3:1 | 4.5:1 | +| Non-essential decorations | None | None | + +##### Dangerous Color Combinations + +These commonly fail contrast or cause readability issues: + +- Light gray text on white (the #1 accessibility fail) +- Red text on green background (or vice versa): 8% of men can't distinguish these +- Blue text on red background (vibrates visually) +- Yellow text on white (almost always fails) +- Thin light text on images (unpredictable contrast) + +##### Testing + +Don't trust your eyes. Use tools: + +- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/) +- Browser DevTools → Rendering → Emulate vision deficiencies +- [Polypane](https://polypane.app/) for real-time testing + +#### Theming: Light & Dark Mode + +##### Dark Mode Is Not Inverted Light Mode + +You can't just swap colors. Dark mode requires different design decisions: + +| Light Mode | Dark Mode | +|------------|-----------| +| Shadows for depth | Lighter surfaces for depth (no shadows) | +| Dark text on light | Light text on dark (reduce font weight) | +| Vibrant accents | Desaturate accents slightly | +| White backgrounds | Either pure black or a deep surface that fits the brand (a brand-tinted near-black at oklch 12-18% works too) | + +In dark mode, depth comes from surface lightness, not shadow. Build a 3-step surface scale where higher elevations are lighter (e.g. 15% / 20% / 25% lightness). Use the SAME hue and chroma as your brand color (whatever it is for THIS project; do not reach for blue) and only vary the lightness. Reduce body text weight slightly (e.g. 350 instead of 400) because light text on dark reads as heavier than dark text on light. + +##### Token Hierarchy + +Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer; primitives stay the same. + +#### Alpha Is A Design Smell + +Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed. + +--- + +**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Skipping color blindness testing (8% of men affected). diff --git a/skill/reference/craft.md b/skill/reference/craft.md index 51d2db5b7..0604d0e05 100644 --- a/skill/reference/craft.md +++ b/skill/reference/craft.md @@ -56,15 +56,15 @@ If the harness has native image generation (Codex), a compact shape's "confirm o Based on the design brief's "Recommended References" section, consult the relevant impeccable reference files. At minimum, always consult: -- [spatial-design.md](spatial-design.md) for layout and spacing -- [typography.md](typography.md) for type hierarchy +- [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 [motion-design.md](motion-design.md) -- Color-heavy or themed? Consult [color-and-contrast.md](color-and-contrast.md) -- Responsive requirements? Consult [responsive-design.md](responsive-design.md) -- Heavy on copy, labels, or errors? Consult [ux-writing.md](ux-writing.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) diff --git a/skill/reference/critique.md b/skill/reference/critique.md index 0e9b04744..6911ed3b5 100644 --- a/skill/reference/critique.md +++ b/skill/reference/critique.md @@ -48,9 +48,9 @@ Read relevant source files and visually inspect the live page when browser autom Evaluate: - **AI slop**: Would someone believe "AI made this" immediately? Check all DON'T guidance from the parent Impeccable skill. - **Holistic design**: hierarchy, IA, emotional fit, discoverability, composition, typography, color, accessibility, states, copy, and edge cases. -- **Cognitive load**: consult [cognitive-load](cognitive-load.md); report checklist failures and decision points with >4 visible options. +- **Cognitive load**: consult the [Cognitive Load Assessment](#cognitive-load-assessment) section below; report checklist failures and decision points with >4 visible options. - **Emotional journey**: peak-end rule, emotional valleys, reassurance at high-stakes moments. -- **Nielsen heuristics**: consult [heuristics-scoring](heuristics-scoring.md); score all 10 heuristics 0-4. +- **Nielsen heuristics**: consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below; score all 10 heuristics 0-4. Return: AI slop verdict, heuristic scores, cognitive load, emotional journey, 2-3 strengths, 3-5 priority issues, persona red flags, minor observations, and provocative questions. @@ -102,7 +102,7 @@ Codex final-answer note: `$impeccable critique` produces a report artifact, so t Structure your feedback as a design director would: #### Design Health Score -> *Consult [heuristics-scoring](heuristics-scoring.md)* +> *Consult the [Heuristics Scoring Guide](#heuristics-scoring-guide) section below.* Present the Nielsen's 10 heuristics scores as a table: @@ -141,16 +141,16 @@ Highlight 2-3 things done well. Be specific about why they work. #### Priority Issues The 3-5 most impactful design problems, ordered by importance. -For each issue, tag with **P0-P3 severity** (consult [heuristics-scoring](heuristics-scoring.md) for severity definitions): +For each issue, tag with **P0-P3 severity** (see [Issue Severity below](#issue-severity-p0p3) for definitions): - **[P?] What**: Name the problem clearly - **Why it matters**: How this hurts users or undermines goals - **Fix**: What to do about it (be concrete) - **Suggested command**: Which command could address this (from: {{available_commands}}) #### Persona Red Flags -> *Consult [personas](personas.md)* +> *Consult the [Personas reference](#persona-based-design-testing) below.* -Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `{{config_file}}` contains a `## Design Context` section from `impeccable teach`, also generate 1-2 project-specific personas from the audience/brand info. +Auto-select 2-3 personas most relevant to this interface type (use the selection table in the reference). If `{{config_file}}` contains a `## Design Context` section from `impeccable init`, also generate 1-2 project-specific personas from the audience/brand info. For each selected persona, walk through the primary user action and list specific red flags found: @@ -271,3 +271,535 @@ After presenting the summary, tell the user: > You can ask me to run these one at a time, all at once, or in any order you prefer. > > Re-run `{{command_prefix}}impeccable critique` after fixes to see your score improve. + +--- + +## Reference Material + +The sections below were previously separate reference files (`cognitive-load.md`, `heuristics-scoring.md`, `personas.md`). They live inline now so the critique flow has all its deep context in one place. + +### Cognitive Load Assessment + +Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload. + +--- + +#### Three Types of Cognitive Load + +##### Intrinsic Load: The Task Itself +Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it. + +**Manage it by**: +- Breaking complex tasks into discrete steps +- Providing scaffolding (templates, defaults, examples) +- Progressive disclosure: show what's needed now, hide the rest +- Grouping related decisions together + +##### Extraneous Load: Bad Design +Mental effort caused by poor design choices. **Eliminate this ruthlessly.** It's pure waste. + +**Common sources**: +- Confusing navigation that requires mental mapping +- Unclear labels that force users to guess meaning +- Visual clutter competing for attention +- Inconsistent patterns that prevent learning +- Unnecessary steps between user intent and result + +##### Germane Load: Learning Effort +Mental effort spent building understanding. This is *good* cognitive load; it leads to mastery. + +**Support it by**: +- Progressive disclosure that reveals complexity gradually +- Consistent patterns that reward learning +- Feedback that confirms correct understanding +- Onboarding that teaches through action, not walls of text + +--- + +#### Cognitive Load Checklist + +Evaluate the interface against these 8 items: + +- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements? +- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)? +- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)? +- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen? +- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next? +- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)? +- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one? +- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it? + +**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed). + +--- + +#### The Working Memory Rule + +**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001). + +At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider: +- **≤4 items**: Within working memory limits, manageable +- **5–7 items**: Pushing the boundary; consider grouping or progressive disclosure +- **8+ items**: Overloaded; users will skip, misclick, or abandon + +**Practical applications**: +- Navigation menus: ≤5 top-level items (group the rest under clear categories) +- Form sections: ≤4 fields visible per group before a visual break +- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu +- Dashboard widgets: ≤4 key metrics visible without scrolling +- Pricing tiers: ≤3 options (more causes analysis paralysis) + +--- + +#### Common Cognitive Load Violations + +##### 1. The Wall of Options +**Problem**: Presenting 10+ choices at once with no hierarchy. +**Fix**: Group into categories, highlight recommended, use progressive disclosure. + +##### 2. The Memory Bridge +**Problem**: User must remember info from step 1 to complete step 3. +**Fix**: Keep relevant context visible, or repeat it where it's needed. + +##### 3. The Hidden Navigation +**Problem**: User must build a mental map of where things are. +**Fix**: Always show current location (breadcrumbs, active states, progress indicators). + +##### 4. The Jargon Barrier +**Problem**: Technical or domain language forces translation effort. +**Fix**: Use plain language. If domain terms are unavoidable, define them inline. + +##### 5. The Visual Noise Floor +**Problem**: Every element has the same visual weight; nothing stands out. +**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted. + +##### 6. The Inconsistent Pattern +**Problem**: Similar actions work differently in different places. +**Fix**: Standardize interaction patterns. Same type of action = same type of UI. + +##### 7. The Multi-Task Demand +**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating). +**Fix**: Sequence the steps. Let the user do one thing at a time. + +##### 8. The Context Switch +**Problem**: User must jump between screens/tabs/modals to gather info for a single decision. +**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth. + +--- + +### Heuristics Scoring Guide + +Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough." + +#### Nielsen's 10 Heuristics + +##### 1. Visibility of System Status + +Keep users informed about what's happening through timely, appropriate feedback. + +**Check for**: +- Loading indicators during async operations +- Confirmation of user actions (save, submit, delete) +- Progress indicators for multi-step processes +- Current location in navigation (breadcrumbs, active states) +- Form validation feedback (inline, not just on submit) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | No feedback; user is guessing what happened | +| 1 | Rare feedback; most actions produce no visible response | +| 2 | Partial; some states communicated, major gaps remain | +| 3 | Good; most operations give clear feedback, minor gaps | +| 4 | Excellent; every action confirms, progress is always visible | + +##### 2. Match Between System and Real World + +Speak the user's language. Follow real-world conventions. Information appears in natural, logical order. + +**Check for**: +- Familiar terminology (no unexplained jargon) +- Logical information order matching user expectations +- Recognizable icons and metaphors +- Domain-appropriate language for the target audience +- Natural reading flow (left-to-right, top-to-bottom priority) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Pure tech jargon, alien to users | +| 1 | Mostly confusing; requires domain expertise to navigate | +| 2 | Mixed; some plain language, some jargon leaks through | +| 3 | Mostly natural; occasional term needs context | +| 4 | Speaks the user's language fluently throughout | + +##### 3. User Control and Freedom + +Users need a clear "emergency exit" from unwanted states without extended dialogue. + +**Check for**: +- Undo/redo functionality +- Cancel buttons on forms and modals +- Clear navigation back to safety (home, previous) +- Easy way to clear filters, search, selections +- Escape from long or multi-step processes + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Users get trapped; no way out without refreshing | +| 1 | Difficult exits; must find obscure paths to escape | +| 2 | Some exits; main flows have escape, edge cases don't | +| 3 | Good control; users can exit and undo most actions | +| 4 | Full control; undo, cancel, back, and escape everywhere | + +##### 4. Consistency and Standards + +Users shouldn't wonder whether different words, situations, or actions mean the same thing. + +**Check for**: +- Consistent terminology throughout the interface +- Same actions produce same results everywhere +- Platform conventions followed (standard UI patterns) +- Visual consistency (colors, typography, spacing, components) +- Consistent interaction patterns (same gesture = same behavior) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Inconsistent everywhere; feels like different products stitched together | +| 1 | Many inconsistencies; similar things look/behave differently | +| 2 | Partially consistent; main flows match, details diverge | +| 3 | Mostly consistent; occasional deviation, nothing confusing | +| 4 | Fully consistent; cohesive system, predictable behavior | + +##### 5. Error Prevention + +Better than good error messages is a design that prevents problems in the first place. + +**Check for**: +- Confirmation before destructive actions (delete, overwrite) +- Constraints preventing invalid input (date pickers, dropdowns) +- Smart defaults that reduce errors +- Clear labels that prevent misunderstanding +- Autosave and draft recovery + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Errors easy to make; no guardrails anywhere | +| 1 | Few safeguards; some inputs validated, most aren't | +| 2 | Partial prevention; common errors caught, edge cases slip | +| 3 | Good prevention; most error paths blocked proactively | +| 4 | Excellent; errors nearly impossible through smart constraints | + +##### 6. Recognition Rather Than Recall + +Minimize memory load. Make objects, actions, and options visible or easily retrievable. + +**Check for**: +- Visible options (not buried in hidden menus) +- Contextual help when needed (tooltips, inline hints) +- Recent items and history +- Autocomplete and suggestions +- Labels on icons (not icon-only navigation) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Heavy memorization; users must remember paths and commands | +| 1 | Mostly recall; many hidden features, few visible cues | +| 2 | Some aids; main actions visible, secondary features hidden | +| 3 | Good recognition; most things discoverable, few memory demands | +| 4 | Everything discoverable; users never need to memorize | + +##### 7. Flexibility and Efficiency of Use + +Accelerators, invisible to novices, speed up expert interaction. + +**Check for**: +- Keyboard shortcuts for common actions +- Customizable interface elements +- Recent items and favorites +- Bulk/batch actions +- Power user features that don't complicate the basics + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | One rigid path; no shortcuts or alternatives | +| 1 | Limited flexibility; few alternatives to the main path | +| 2 | Some shortcuts; basic keyboard support, limited bulk actions | +| 3 | Good accelerators; keyboard nav, some customization | +| 4 | Highly flexible; multiple paths, power features, customizable | + +##### 8. Aesthetic and Minimalist Design + +Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose. + +**Check for**: +- Only necessary information visible at each step +- Clear visual hierarchy directing attention +- Purposeful use of color and emphasis +- No decorative clutter competing for attention +- Focused, uncluttered layouts + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Overwhelming; everything competes for attention equally | +| 1 | Cluttered; too much noise, hard to find what matters | +| 2 | Some clutter; main content clear, periphery noisy | +| 3 | Mostly clean; focused design, minor visual noise | +| 4 | Perfectly minimal; every element earns its pixel | + +##### 9. Help Users Recognize, Diagnose, and Recover from Errors + +Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution. + +**Check for**: +- Plain language error messages (no error codes for users) +- Specific problem identification ("Email is missing @" not "Invalid input") +- Actionable recovery suggestions +- Errors displayed near the source of the problem +- Non-blocking error handling (don't wipe the form) + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | Cryptic errors; codes, jargon, or no message at all | +| 1 | Vague errors; "Something went wrong" with no guidance | +| 2 | Clear but unhelpful; names the problem but not the fix | +| 3 | Clear with suggestions; identifies problem and offers next steps | +| 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work | + +##### 10. Help and Documentation + +Even if the system is usable without docs, help should be easy to find, task-focused, and concise. + +**Check for**: +- Searchable help or documentation +- Contextual help (tooltips, inline hints, guided tours) +- Task-focused organization (not feature-organized) +- Concise, scannable content +- Easy access without leaving current context + +**Scoring**: +| Score | Criteria | +|-------|----------| +| 0 | No help available anywhere | +| 1 | Help exists but hard to find or irrelevant | +| 2 | Basic help; FAQ or docs exist, not contextual | +| 3 | Good documentation; searchable, mostly task-focused | +| 4 | Excellent contextual help; right info at the right moment | + +--- + +#### Score Summary + +**Total possible**: 40 points (10 heuristics × 4 max) + +| Score Range | Rating | What It Means | +|-------------|--------|---------------| +| 36–40 | Excellent | Minor polish only; ship it | +| 28–35 | Good | Address weak areas, solid foundation | +| 20–27 | Acceptable | Significant improvements needed before users are happy | +| 12–19 | Poor | Major UX overhaul required; core experience broken | +| 0–11 | Critical | Redesign needed; unusable in current state | + +--- + +#### Issue Severity (P0–P3) + +Tag each individual issue found during scoring with a priority level: + +| Priority | Name | Description | Action | +|----------|------|-------------|--------| +| **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper | +| **P1** | Major | Causes significant difficulty or confusion | Fix before release | +| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass | +| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits | + +**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1. + +--- + +### Persona-Based Design Testing + +Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss. + +**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns. + +--- + +#### 1. Impatient Power User: "Alex" + + +**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave. + +**Behaviors**: +- Skips all onboarding and instructions +- Looks for keyboard shortcuts immediately +- Tries to bulk-select, batch-edit, and automate +- Gets frustrated by required steps that feel unnecessary +- Abandons if anything feels slow or patronizing + +**Test Questions**: +- Can Alex complete the core task in under 60 seconds? +- Are there keyboard shortcuts for common actions? +- Can onboarding be skipped entirely? +- Do modals have keyboard dismiss (Esc)? +- Is there a "power user" path (shortcuts, bulk actions)? + +**Red Flags** (report these specifically): +- Forced tutorials or unskippable onboarding +- No keyboard navigation for primary actions +- Slow animations that can't be skipped +- One-item-at-a-time workflows where batch would be natural +- Redundant confirmation steps for low-risk actions + +--- + +#### 2. Confused First-Timer: "Jordan" + +**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out. + +**Behaviors**: +- Reads all instructions carefully +- Hesitates before clicking anything unfamiliar +- Looks for help or support constantly +- Misunderstands jargon and abbreviations +- Takes the most literal interpretation of any label + +**Test Questions**: +- Is the first action obviously clear within 5 seconds? +- Are all icons labeled with text? +- Is there contextual help at decision points? +- Does terminology assume prior knowledge? +- Is there a clear "back" or "undo" at every step? + +**Red Flags** (report these specifically): +- Icon-only navigation with no labels +- Technical jargon without explanation +- No visible help option or guidance +- Ambiguous next steps after completing an action +- No confirmation that an action succeeded + +--- + +#### 3. Accessibility-Dependent User: "Sam" + +**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences. + +**Behaviors**: +- Tabs through the interface linearly +- Relies on ARIA labels and heading structure +- Cannot see hover states or visual-only indicators +- Needs adequate color contrast (4.5:1 minimum) +- May use browser zoom up to 200% + +**Test Questions**: +- Can the entire primary flow be completed keyboard-only? +- Are all interactive elements focusable with visible focus indicators? +- Do images have meaningful alt text? +- Is color contrast WCAG AA compliant (4.5:1 for text)? +- Does the screen reader announce state changes (loading, success, errors)? + +**Red Flags** (report these specifically): +- Click-only interactions with no keyboard alternative +- Missing or invisible focus indicators +- Meaning conveyed by color alone (red = error, green = success) +- Unlabeled form fields or buttons +- Time-limited actions without extension option +- Custom components that break screen reader flow + +--- + +#### 4. Deliberate Stress Tester: "Riley" + +**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience. + +**Behaviors**: +- Tests edge cases intentionally (empty states, long strings, special characters) +- Submits forms with unexpected data (emoji, RTL text, very long values) +- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs +- Looks for inconsistencies between what the UI promises and what actually happens +- Documents problems methodically + +**Test Questions**: +- What happens at the edges (0 items, 1000 items, very long text)? +- Do error states recover gracefully or leave the UI in a broken state? +- What happens on refresh mid-workflow? Is state preserved? +- Are there features that appear to work but produce broken results? +- How does the UI handle unexpected input (emoji, special chars, paste from Excel)? + +**Red Flags** (report these specifically): +- Features that appear to work but silently fail or produce wrong results +- Error handling that exposes technical details or leaves UI in a broken state +- Empty states that show nothing useful ("No results" with no guidance) +- Workflows that lose user data on refresh or navigation +- Inconsistent behavior between similar interactions in different parts of the UI + +--- + +#### 5. Distracted Mobile User: "Casey" + +**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection. + +**Behaviors**: +- Uses thumb only; prefers bottom-of-screen actions +- Gets interrupted mid-flow and returns later +- Switches between apps frequently +- Has limited attention span and low patience +- Types as little as possible, prefers taps and selections + +**Test Questions**: +- Are primary actions in the thumb zone (bottom half of screen)? +- Is state preserved if the user leaves and returns? +- Does it work on slow connections (3G)? +- Can forms use autocomplete and smart defaults? +- Are touch targets at least 44×44pt? + +**Red Flags** (report these specifically): +- Important actions positioned at the top of the screen (unreachable by thumb) +- No state persistence; progress lost on tab switch or interruption +- Large text inputs required where selection would work +- Heavy assets loading on every page (no lazy loading) +- Tiny tap targets or targets too close together + +--- + +#### Selecting Personas + +Choose personas based on the interface type: + +| Interface Type | Primary Personas | Why | +|---------------|-----------------|-----| +| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile | +| Dashboard / admin | Alex, Sam | Power users, accessibility | +| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity | +| Onboarding flow | Jordan, Casey | Confusion, interruption | +| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav | +| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile | + +--- + +#### Project-Specific Personas + +If `{{config_file}}` contains a `## Design Context` section (generated by `impeccable init`), derive 1–2 additional personas from the audience and brand information: + +1. Read the target audience description +2. Identify the primary user archetype not covered by the 5 predefined personas +3. Create a persona following this template: + +``` +##### [Role]: "[Name]" + +**Profile**: [2-3 key characteristics derived from Design Context] + +**Behaviors**: [3-4 specific behaviors based on the described audience] + +**Red Flags**: [3-4 things that would alienate this specific user type] +``` + +Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists. diff --git a/skill/reference/document.md b/skill/reference/document.md index abb0a6758..0c2022774 100644 --- a/skill/reference/document.md +++ b/skill/reference/document.md @@ -45,7 +45,7 @@ Rules that matter: - **Token refs** use `{path.to.token}` (e.g. `{colors.primary}`, `{rounded.md}`). Components may reference primitives; primitives may not reference each other. - **Stitch validates colors as hex sRGB only** (`#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`); OKLCH/HSL/P3 trigger a linter warning, not a hard error. YAML accepts the string either way and our own parser is format-agnostic. Choose based on project posture: (a) if the project has an "OKLCH-only" doctrine or uses Display-P3 values that don't round-trip through sRGB, put OKLCH directly in the frontmatter and accept the Stitch linter warning; (b) if the project wants strict Stitch compliance or plans to use their Tailwind/DTCG export pipeline, put hex in the frontmatter and keep OKLCH in prose as the canonical reference. Never split the source of truth without explicit reason. - **Component sub-tokens** are limited to 8 props: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, `width`. Shadows, motion, focus rings, backdrop-filter: none of those fit. Carry them in the sidecar (Step 4b). -- **Scale keys are open-ended.** Use whatever names the project already uses (`warm-ash-cream`, `surface-container-low`). Don't rename to Material defaults. +- **Scale keys are open-ended.** Use whatever names the project already uses (`oxblood-deep`, `surface-container-low`). Don't rename to Material defaults. - **Variants are naming convention, not schema.** `button-primary` / `button-primary-hover` / `button-primary-active` as sibling keys. ## The markdown body: six sections (exact order) @@ -61,7 +61,7 @@ Optional evocative subtitles are allowed in the form `## 2. Colors: The [Name] P ## When to run -- The user just ran `/impeccable teach` and needs the visual side documented. +- The user just ran `/impeccable init` and needs the visual side documented. - The skill noticed no `DESIGN.md` exists and nudged the user to create one. - An existing `DESIGN.md` is stale (the design has drifted). - Before a large redesign, to capture the current state as a reference. @@ -71,7 +71,7 @@ If a `DESIGN.md` already exists, **do not silently overwrite it**. Show the user ## Two paths - **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 teach, nothing built yet). Interview for five high-level answers, write a minimal DESIGN.md marked ``. Re-run in scan mode once there's code. +- **Seed mode**: the project is pre-implementation (fresh init, nothing built yet). Interview for five high-level answers, 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` forces seed mode regardless of code presence. @@ -103,7 +103,7 @@ Build a structured draft from the discovered tokens. For each token class: From the auto-extracted tokens, draft the YAML frontmatter now (you'll write it at the top of DESIGN.md in Step 4). This is the machine-readable layer: what the live panel and Stitch's linter consume. -- **Colors**: one entry per extracted color. Key = descriptive slug (`warm-ash-cream`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. +- **Colors**: one entry per extracted color. Key = descriptive slug (`oxblood-deep`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex; see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. - **Typography**: one entry per role (`display`, `headline`, `title`, `body`, `label`). Typography is an object; include only the props that are real for the project (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`). - **Rounded / Spacing**: whatever scale steps the project actually uses, keyed by whatever scale name the project uses (`sm` / `md` / `lg`, or `surface-sm`, or numeric steps). - **Components**: one entry per variant (`button-primary`, `button-primary-hover`, `button-ghost`). Reference primitives via `{colors.X}`, `{rounded.Y}`. If a variant needs a property Stitch's 8-prop set doesn't cover (shadow, focus ring, backdrop-filter), carry the full snippet in the sidecar instead. @@ -253,7 +253,7 @@ Regenerate the sidecar whenever you regenerate root `DESIGN.md`. If the user onl "extensions": { "colorMeta": { "primary": { "role": "primary", "displayName": "Editorial Magenta", "canonical": "oklch(60% 0.25 350)", "tonalRamp": ["...", "...", "..."] }, - "warm-ash-cream": { "role": "neutral", "displayName": "Warm Ash Cream", "canonical": "oklch(96% 0.005 350)", "tonalRamp": ["...", "...", "..."] } + "cool-paper": { "role": "neutral", "displayName": "Cool Paper", "canonical": "oklch(96% 0.005 230)", "tonalRamp": ["...", "...", "..."] } }, "typographyMeta": { "display": { "displayName": "Display", "purpose": "Hero headlines only." } @@ -328,12 +328,13 @@ Pull directly from the DESIGN.md you just wrote: Do not reword. The panel shows these as secondary collapsible context; the same voice that's in the Markdown carries through. -### Step 5: Confirm, refine, and refresh session cache +### Step 5: Confirm and refine 1. Show the user the full DESIGN.md you wrote. Briefly highlight the non-obvious creative choices (descriptive color names, atmosphere language, named rules). 2. Mention that `.impeccable/design.json` was also written alongside; the live panel will now render this project's actual button/input/nav primitives instead of generic approximations. 3. Offer to refine any section: "Want me to revise a section, add component patterns I missed, or adjust the atmosphere language?" -4. **Refresh the session cache.** Run `node {{scripts_path}}/load-context.mjs` one final time so the newly-written DESIGN.md lands in conversation. Subsequent commands in this session will use the fresh version automatically without re-reading. + +Your own write is the freshest source; subsequent commands in this session don't need a reload. ## Seed mode @@ -394,11 +395,12 @@ Per-section guidance in seed mode: 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. -### Step 4: Confirm and refresh session cache +### Step 4: Confirm 1. Show the seed DESIGN.md. Call out that it is a seed (the marker is the literal commitment). 2. Tell the user: "Re-run `/impeccable document` once you have some code. That pass will extract real tokens and generate the sidecar." -3. Run `node {{scripts_path}}/load-context.mjs` once so the seed lands in conversation for the rest of the session. + +Your own write is the freshest source; no reload needed. ## Style guidelines diff --git a/skill/reference/heuristics-scoring.md b/skill/reference/heuristics-scoring.md deleted file mode 100644 index edbe5028d..000000000 --- a/skill/reference/heuristics-scoring.md +++ /dev/null @@ -1,234 +0,0 @@ -# Heuristics Scoring Guide - -Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest: a 4 means genuinely excellent, not "good enough." - -## Nielsen's 10 Heuristics - -### 1. Visibility of System Status - -Keep users informed about what's happening through timely, appropriate feedback. - -**Check for**: -- Loading indicators during async operations -- Confirmation of user actions (save, submit, delete) -- Progress indicators for multi-step processes -- Current location in navigation (breadcrumbs, active states) -- Form validation feedback (inline, not just on submit) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | No feedback; user is guessing what happened | -| 1 | Rare feedback; most actions produce no visible response | -| 2 | Partial; some states communicated, major gaps remain | -| 3 | Good; most operations give clear feedback, minor gaps | -| 4 | Excellent; every action confirms, progress is always visible | - -### 2. Match Between System and Real World - -Speak the user's language. Follow real-world conventions. Information appears in natural, logical order. - -**Check for**: -- Familiar terminology (no unexplained jargon) -- Logical information order matching user expectations -- Recognizable icons and metaphors -- Domain-appropriate language for the target audience -- Natural reading flow (left-to-right, top-to-bottom priority) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Pure tech jargon, alien to users | -| 1 | Mostly confusing; requires domain expertise to navigate | -| 2 | Mixed; some plain language, some jargon leaks through | -| 3 | Mostly natural; occasional term needs context | -| 4 | Speaks the user's language fluently throughout | - -### 3. User Control and Freedom - -Users need a clear "emergency exit" from unwanted states without extended dialogue. - -**Check for**: -- Undo/redo functionality -- Cancel buttons on forms and modals -- Clear navigation back to safety (home, previous) -- Easy way to clear filters, search, selections -- Escape from long or multi-step processes - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Users get trapped; no way out without refreshing | -| 1 | Difficult exits; must find obscure paths to escape | -| 2 | Some exits; main flows have escape, edge cases don't | -| 3 | Good control; users can exit and undo most actions | -| 4 | Full control; undo, cancel, back, and escape everywhere | - -### 4. Consistency and Standards - -Users shouldn't wonder whether different words, situations, or actions mean the same thing. - -**Check for**: -- Consistent terminology throughout the interface -- Same actions produce same results everywhere -- Platform conventions followed (standard UI patterns) -- Visual consistency (colors, typography, spacing, components) -- Consistent interaction patterns (same gesture = same behavior) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Inconsistent everywhere; feels like different products stitched together | -| 1 | Many inconsistencies; similar things look/behave differently | -| 2 | Partially consistent; main flows match, details diverge | -| 3 | Mostly consistent; occasional deviation, nothing confusing | -| 4 | Fully consistent; cohesive system, predictable behavior | - -### 5. Error Prevention - -Better than good error messages is a design that prevents problems in the first place. - -**Check for**: -- Confirmation before destructive actions (delete, overwrite) -- Constraints preventing invalid input (date pickers, dropdowns) -- Smart defaults that reduce errors -- Clear labels that prevent misunderstanding -- Autosave and draft recovery - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Errors easy to make; no guardrails anywhere | -| 1 | Few safeguards; some inputs validated, most aren't | -| 2 | Partial prevention; common errors caught, edge cases slip | -| 3 | Good prevention; most error paths blocked proactively | -| 4 | Excellent; errors nearly impossible through smart constraints | - -### 6. Recognition Rather Than Recall - -Minimize memory load. Make objects, actions, and options visible or easily retrievable. - -**Check for**: -- Visible options (not buried in hidden menus) -- Contextual help when needed (tooltips, inline hints) -- Recent items and history -- Autocomplete and suggestions -- Labels on icons (not icon-only navigation) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Heavy memorization; users must remember paths and commands | -| 1 | Mostly recall; many hidden features, few visible cues | -| 2 | Some aids; main actions visible, secondary features hidden | -| 3 | Good recognition; most things discoverable, few memory demands | -| 4 | Everything discoverable; users never need to memorize | - -### 7. Flexibility and Efficiency of Use - -Accelerators, invisible to novices, speed up expert interaction. - -**Check for**: -- Keyboard shortcuts for common actions -- Customizable interface elements -- Recent items and favorites -- Bulk/batch actions -- Power user features that don't complicate the basics - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | One rigid path; no shortcuts or alternatives | -| 1 | Limited flexibility; few alternatives to the main path | -| 2 | Some shortcuts; basic keyboard support, limited bulk actions | -| 3 | Good accelerators; keyboard nav, some customization | -| 4 | Highly flexible; multiple paths, power features, customizable | - -### 8. Aesthetic and Minimalist Design - -Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose. - -**Check for**: -- Only necessary information visible at each step -- Clear visual hierarchy directing attention -- Purposeful use of color and emphasis -- No decorative clutter competing for attention -- Focused, uncluttered layouts - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Overwhelming; everything competes for attention equally | -| 1 | Cluttered; too much noise, hard to find what matters | -| 2 | Some clutter; main content clear, periphery noisy | -| 3 | Mostly clean; focused design, minor visual noise | -| 4 | Perfectly minimal; every element earns its pixel | - -### 9. Help Users Recognize, Diagnose, and Recover from Errors - -Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution. - -**Check for**: -- Plain language error messages (no error codes for users) -- Specific problem identification ("Email is missing @" not "Invalid input") -- Actionable recovery suggestions -- Errors displayed near the source of the problem -- Non-blocking error handling (don't wipe the form) - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | Cryptic errors; codes, jargon, or no message at all | -| 1 | Vague errors; "Something went wrong" with no guidance | -| 2 | Clear but unhelpful; names the problem but not the fix | -| 3 | Clear with suggestions; identifies problem and offers next steps | -| 4 | Perfect recovery; pinpoints issue, suggests fix, preserves user work | - -### 10. Help and Documentation - -Even if the system is usable without docs, help should be easy to find, task-focused, and concise. - -**Check for**: -- Searchable help or documentation -- Contextual help (tooltips, inline hints, guided tours) -- Task-focused organization (not feature-organized) -- Concise, scannable content -- Easy access without leaving current context - -**Scoring**: -| Score | Criteria | -|-------|----------| -| 0 | No help available anywhere | -| 1 | Help exists but hard to find or irrelevant | -| 2 | Basic help; FAQ or docs exist, not contextual | -| 3 | Good documentation; searchable, mostly task-focused | -| 4 | Excellent contextual help; right info at the right moment | - ---- - -## Score Summary - -**Total possible**: 40 points (10 heuristics × 4 max) - -| Score Range | Rating | What It Means | -|-------------|--------|---------------| -| 36–40 | Excellent | Minor polish only; ship it | -| 28–35 | Good | Address weak areas, solid foundation | -| 20–27 | Acceptable | Significant improvements needed before users are happy | -| 12–19 | Poor | Major UX overhaul required; core experience broken | -| 0–11 | Critical | Redesign needed; unusable in current state | - ---- - -## Issue Severity (P0–P3) - -Tag each individual issue found during scoring with a priority level: - -| Priority | Name | Description | Action | -|----------|------|-------------|--------| -| **P0** | Blocking | Prevents task completion entirely | Fix immediately; this is a showstopper | -| **P1** | Major | Causes significant difficulty or confusion | Fix before release | -| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass | -| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits | - -**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1. diff --git a/skill/reference/teach.md b/skill/reference/init.md similarity index 57% rename from skill/reference/teach.md rename to skill/reference/init.md index 474fce7d6..6594e0028 100644 --- a/skill/reference/teach.md +++ b/skill/reference/init.md @@ -1,21 +1,16 @@ -# Teach Flow +# Init Flow -Gathers design context for a project and writes two complementary files at the project root: +The setup command for a project. One codebase crawl feeds everything it writes: - **PRODUCT.md** (strategic): root project file for register, target users, product purpose, brand personality, anti-references, strategic design principles. Answers "who/what/why". - **DESIGN.md** (visual): root project file for visual theme, color palette, typography, components, layout. Follows the [Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/). Answers "how it looks". +- **`.impeccable/live/config.json`** (live mode): pre-configured so `{{command_prefix}}impeccable live` boots straight into variant mode with no first-time detour. -Every other impeccable command reads these files before doing any work. +It closes by pointing the user at the best command to run next. Every other impeccable command reads PRODUCT.md and DESIGN.md before doing any work. ## Step 1: Load current state -Run the shared loader first so you know what already exists: - -```bash -node {{scripts_path}}/load-context.mjs -``` - -The output tells you whether PRODUCT.md and/or DESIGN.md already exist. If `migrated: true`, legacy `.impeccable.md` was auto-renamed to `PRODUCT.md`. Mention this once to the user. +Check what already exists. PRODUCT.md and DESIGN.md live at the project root, or under `.agents/context/` or `docs/` (case-insensitive). Read whichever are present with your native file tool. Also note whether `.impeccable/live/config.json` already exists (Step 6 leaves it untouched if so). Decision tree: - **Neither file exists (empty project or no context yet)**: do Steps 2-4 (write PRODUCT.md), then decide on DESIGN.md based on whether there's code to analyze. @@ -26,14 +21,14 @@ Decision tree: Never silently overwrite an existing file. Always confirm first. -If teach was invoked as a setup blocker by another command, such as `{{command_prefix}}impeccable craft landing page`, pause that command here. Complete teach, re-run the loader, then resume the original command with the freshly loaded context. For craft, resume into shape next; teach creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief. +If init was invoked as a setup blocker by another command, such as `{{command_prefix}}impeccable craft landing page`, pause that command here. Complete init, then resume the original command. Your own writes are the freshest source; no reload needed. For craft, resume into shape next; init creates project context, but it is not a substitute for the task-specific shape interview and confirmed design brief. ## Step 2: Explore the codebase -Before asking questions, thoroughly scan the project to discover what you can: +Before asking questions, thoroughly scan the project to discover what you can. This single crawl feeds PRODUCT.md, DESIGN.md, **and** the live-mode framework detection in Step 6, so be thorough once rather than re-scanning later: - **README and docs**: Project purpose, target audience, any stated goals -- **Package.json / config files**: Tech stack, dependencies, existing design libraries +- **Package.json / config files**: Tech stack, dependencies, existing design libraries, **and the framework** (Vite/SPA, Next.js, Nuxt, SvelteKit, Astro, multi-page static) plus the HTML entry the browser actually loads - **Existing components**: Current design patterns, spacing, typography in use - **Brand assets**: Logos, favicons, color values already defined - **Design tokens / CSS variables**: Existing color palettes, font stacks, spacing scales @@ -46,7 +41,7 @@ Also form a **register hypothesis** from what you find: Register is a hypothesis at this point, not a decision; Step 3 confirms it. -Note what you've learned and what remains unclear. This exploration feeds both PRODUCT.md and DESIGN.md. +Note what you've learned and what remains unclear. Also note any rough edges worth a follow-up command (thin hierarchy, flat or gray palette, missing error/empty states, dull copy); Step 7 turns these into concrete recommendations without re-analyzing. ## Step 3: Ask strategic questions (for PRODUCT.md) @@ -84,8 +79,7 @@ If the signal is genuinely split (e.g. a product with a big marketing landing), ### Brand & Personality - How would you describe the brand personality in 3 words? - Reference sites or apps that capture the right feel? What specifically about them? - - For brand, push for real-world references in the right lane (tech-minimal, editorial-magazine, consumer-warm, brutalist-grid, etc.), not generic "modern" adjectives. - - For product, push for category best-tool references (Linear, Figma, Notion, Raycast, Stripe). + - Push for specific named references with the *specific* thing about them that fits this brand, not generic "modern" adjectives or category-bucket lanes. - What should this explicitly NOT look like? Any anti-references? ### Accessibility & Inclusion @@ -141,16 +135,38 @@ If the user agrees, delegate to `/impeccable document` (it auto-detects scan vs If the user prefers to skip, mention they can run `/impeccable document` any time later. -## Step 6: Confirm and wrap up +## Step 6: Configure live mode (when code exists) -Summarize: +If the project has code with HTML entries and a dev server (the same "code exists" condition that puts `/impeccable document` in scan mode), pre-configure live mode now. You already identified the framework and the served HTML entry in Step 2, so this is nearly free, and it spares the user the first-time setup detour when they later run `/impeccable live`. + +**Skip this step for empty / pre-implementation projects** (nothing to inject into yet). Tell the user live mode will configure itself the first time they run it once there's code. + +**If `.impeccable/live/config.json` already exists, leave it untouched** and note that live mode is already configured. + +Otherwise: + +1. Write `.impeccable/live/config.json`. Choose `files` (the HTML entries the browser actually loads), `insertBefore`, and `commentSyntax` from the framework table in [live.md](live.md)'s **First-time setup** section, using the framework you found in Step 2. That table is canonical; do not restate it here. For multi-page static sites, prefer a glob (`["public/**/*.html"]`) over a literal list. +2. Run `node {{scripts_path}}/detect-csp.mjs`. If it reports a patchable shape (`append-arrays` / `append-string`), use the **consent prompt template** from live.md before editing any source file. On decline, skip the patch. For `middleware` / `meta-tag` shapes, surface the detected files and ask the user to add `http://localhost:8400` to `script-src` and `connect-src` manually. For `null`, there's nothing to do. +3. Set `cspChecked: true` in the config once CSP is handled (patched, declined, manual, or not needed). The schema and per-shape patch details live in live.md's First-time setup; follow it rather than duplicating. + +Writing the config file is harmless and needs no consent; only the CSP **source-file patch** requires a yes. + +## Step 7: Recommend starting points, then wrap up + +Summarize tersely: - Register captured (brand / product) -- What was written (PRODUCT.md, DESIGN.md, or both) +- What was written (PRODUCT.md, DESIGN.md, live config, or a subset) - The 3-5 strategic principles from PRODUCT.md that will guide future work -- If DESIGN.md is pending, remind the user how to generate it later +- If DESIGN.md or live config is pending, one line on how to set it up later -**Critical: re-run the loader to refresh session context.** After writing PRODUCT.md, run `node {{scripts_path}}/load-context.mjs` one final time and let its full JSON output land in conversation. This ensures subsequent commands in this session use the freshly-written PRODUCT.md, not a stale earlier version. +Then recommend the **best commands to run next**, drawn from what your Step 2 crawl already surfaced. Do not run a fresh analysis here; surface observations you already have. Tailor to register and to what you saw, offer the 2-4 most relevant (not a menu dump), and give the exact command to type. Group by intent: -If teach was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now with the fresh context. +- **Build something new**: `/impeccable craft ` (shape, then build end-to-end) or `/impeccable shape ` (plan first). Lead with this for empty or early-stage projects. +- **Improve what's there**: name the specific surface. `/impeccable critique ` for a scored UX review; `/impeccable audit ` for a11y / perf / responsive checks; `/impeccable polish ` for a pre-ship pass. When the crawl flagged a specific weakness, point the matching command at it: thin hierarchy or spacing → `layout`, flat or gray palette → `colorize`, missing error / empty states → `harden` or `onboard`, dull or unclear copy → `clarify`. +- **Iterate visually**: `/impeccable live` (configured in Step 6) to pick elements in the browser and generate variants in place. + +The full command menu is one bare `/impeccable` away; keep this list short and pointed. + +If init was invoked as a blocker by another impeccable command (e.g. the user ran `/impeccable polish` with no PRODUCT.md), resume that original task now. Your own writes are the freshest source; no reload needed. Optionally {{ask_instruction}} Ask whether they'd like a brief summary of PRODUCT.md appended to {{config_file}} for easier agent reference. If yes, append a short **Design Context** pointer section there. diff --git a/skill/reference/interaction-design.md b/skill/reference/interaction-design.md index 15aed5b29..c42c9d810 100644 --- a/skill/reference/interaction-design.md +++ b/skill/reference/interaction-design.md @@ -150,12 +150,6 @@ For browsers without anchor positioning support, `position: fixed` with manual c Check viewport boundaries before rendering. If the dropdown would overflow the bottom edge, flip it above the trigger. If it would overflow the right edge, align it to the trigger's right side instead. -### Anti-Patterns - -- **`position: absolute` inside `overflow: hidden`** - The dropdown will be clipped. Use `position: fixed` or the top layer instead. -- **Arbitrary z-index values** like `z-index: 9999` - Use a semantic z-index scale: `dropdown (100) -> sticky (200) -> modal-backdrop (300) -> modal (400) -> toast (500) -> tooltip (600)`. -- **Rendering dropdown markup inline** without an escape hatch from the parent's stacking context. Either use `popover` (top layer), a portal, or `position: fixed`. - ## Destructive Actions: Undo > Confirm **Undo is better than confirmation dialogs.** Users click through confirmations mindlessly. Remove from UI immediately, show undo toast, actually delete after toast expires. Use confirmation only for truly irreversible actions (account deletion), high-cost actions, or batch operations. diff --git a/skill/reference/layout.md b/skill/reference/layout.md index d18c3d3c9..a2a847226 100644 --- a/skill/reference/layout.md +++ b/skill/reference/layout.md @@ -1,4 +1,4 @@ -Space is the most underused design tool. Find the layout's actual problem (monotone spacing, weak hierarchy, identical card grids, the centered-stack default) and fix the structure, not the surface. +Space is the most underused design tool. Find the layout's actual problem (monotone spacing, weak hierarchy, identical card grids) and fix the structure, not the surface. --- @@ -27,7 +27,6 @@ Analyze what's weak about the current spatial design: 3. **Grid & structure**: - Is there a clear underlying structure, or does the layout feel random? - Are identical card grids used everywhere? (Icon + heading + text, repeated endlessly) - - Is everything centered? (Left-aligned with asymmetric layouts feels more designed, but not a hard and fast rule) 4. **Rhythm & variety**: - Does the layout have visual rhythm? (Alternating tight/generous spacing) @@ -43,8 +42,6 @@ Analyze what's weak about the current spatial design: ## Plan Layout Improvements -Consult the [spatial design reference](spatial-design.md) for detailed guidance on grids, rhythm, and container queries. - Create a systematic plan: - **Spacing system**: Use a consistent scale (a framework's built-in scale like Tailwind's, rem-based tokens, or a custom system). The specific values matter less than consistency. @@ -57,6 +54,7 @@ Create a systematic plan: ### Establish a Spacing System - Use a consistent spacing scale (framework scales like Tailwind, rem-based tokens, or a custom scale all work). What matters is that values come from a defined set, not arbitrary numbers. +- Prefer a 4pt base scale (4, 8, 12, 16, 24, 32, 48, 64, 96px) over 8pt; 8pt is too coarse and you'll frequently need 12px between 8 and 16. - Name tokens semantically if using custom properties: `--space-xs` through `--space-xl`, not `--spacing-8` - Use `gap` for sibling spacing instead of margins; eliminates margin collapse hacks - Apply `clamp()` for fluid spacing that breathes on larger screens @@ -66,15 +64,22 @@ Create a systematic plan: - **Tight grouping** for related elements (8-12px between siblings) - **Generous separation** between distinct sections (48-96px) - **Varied spacing** within sections (not every row needs the same gap) -- **Asymmetric compositions**: break the predictable centered-content pattern when it makes sense +- **Asymmetric compositions**: a deliberate choice when the content invites it (not a default to chase). ### Choose the Right Layout Tool -- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. Flex is simpler and more appropriate for the majority of layout tasks. +- **Use Flexbox for 1D layouts**: Rows of items, nav bars, button groups, card contents, most component internals. - **Use Grid for 2D layouts**: Page-level structure, dashboards, data-dense interfaces, anything where rows AND columns need coordinated control. -- **Don't default to Grid** when Flexbox with `flex-wrap` would be simpler and more flexible. -- Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. - Use named grid areas (`grid-template-areas`) for complex page layouts; redefine at breakpoints. +- Use **container queries** for components, viewport queries for page layouts. A card in a narrow sidebar can stay compact while the same card in a main content area expands automatically: + +```css +.card-container { container-type: inline-size; } +.card { display: grid; gap: var(--space-md); } +@container (min-width: 400px) { + .card { grid-template-columns: 120px 1fr; } +} +``` ### Break Card Grid Monotony @@ -85,18 +90,36 @@ Create a systematic plan: ### Strengthen Visual Hierarchy - Use the fewest dimensions needed for clear hierarchy. Space alone can be enough; generous whitespace around an element draws the eye. Some of the most polished designs achieve rhythm with just space and weight. Add color or size contrast only when simpler means aren't sufficient. +- The best hierarchy combines 2–3 dimensions at once. A heading that's larger, bolder, AND has more space above it reads as primary without trying: + +| Tool | Strong Hierarchy | Weak Hierarchy | +|------|------------------|----------------| +| **Size** | 3:1 ratio or more | <2:1 ratio | +| **Weight** | Bold vs Regular | Medium vs Regular | +| **Color** | High contrast | Similar tones | +| **Position** | Top/left (primary) | Bottom/right | +| **Space** | Surrounded by white space | Crowded | + - Be aware of reading flow: in LTR languages, the eye naturally scans top-left to bottom-right, but primary action placement depends on context (e.g., bottom-right in dialogs, top in navigation). - Create clear content groupings through proximity and separation. ### Manage Depth & Elevation -- Create a semantic z-index scale (dropdown → sticky → modal-backdrop → modal → toast → tooltip) - Build a consistent shadow scale (sm → md → lg → xl); shadows should be subtle - Use elevation to reinforce hierarchy, not as decoration ### Optical Adjustments - If an icon looks visually off-center despite being geometrically centered, nudge it. But only if you're confident it actually looks wrong. Don't adjust speculatively. +- Text at `margin-left: 0` looks slightly indented because of letterform whitespace; a negative margin (`-0.05em`) optically aligns it. Geometrically centered glyphs often look off-center (play icons need to shift right, arrows shift toward their direction). +- Touch targets must be 44×44px minimum even when the visual element is smaller. Expand the hit area with padding or a pseudo-element: + +```css +.icon-button { width: 24px; height: 24px; position: relative; } +.icon-button::before { + content: ''; position: absolute; inset: -10px; +} +``` **NEVER**: - Use arbitrary spacing values outside your scale @@ -104,10 +127,7 @@ Create a systematic plan: - Wrap everything in cards (not everything needs a container) - Nest cards inside cards (use spacing and dividers for hierarchy within) - Use identical card grids everywhere (icon + heading + text, repeated) -- Center everything (left-aligned with asymmetry feels more designed) - Default to the hero metric layout (big number, small label, stats, gradient) as a template. If showing real user data, a prominent metric can work, but it should display actual data, not decorative numbers. -- Default to CSS Grid when Flexbox would be simpler; use the simplest tool for the job -- Use arbitrary z-index values (999, 9999); build a semantic scale ## Verify Layout Improvements diff --git a/skill/reference/live.md b/skill/reference/live.md index 971dda22e..b29f94c08 100644 --- a/skill/reference/live.md +++ b/skill/reference/live.md @@ -4,23 +4,30 @@ Interactive live variant mode: select elements in the browser, pick a design act A running dev server with hot module replacement (Vite, Next.js, Bun, etc.), OR a static HTML file open in the browser. + +Codex: run live helper commands, the app dev server, and any dependency-installing setup with `sandbox_permissions: "require_escalated"` from the start; live mode depends on localhost and package-manager network access that the sandbox blocks. + + ## The contract (read once) Execute in order. No step skipped, no step reordered. 1. `live.mjs`: boot. -2. Navigate to the URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). If you can't infer it confidently, tell the user once to open their dev/preview URL. Never use `serverPort` as that URL; it's the helper, not the app. +2. Open the app URL that serves `pageFile` (infer from `package.json`, docs, terminal output, or an open tab). Never use `serverPort`; it's the helper, not the app. **Cursor:** `browser_navigate` to that URL before polling; do not skip. **Other harnesses:** use the available browser tool; if the URL is uncertain, ask the user once. 3. Poll loop with the default long timeout (600000 ms). After every event or `--reply`, run `live-poll.mjs` again immediately. Never pass a short `--timeout=`. + +The global bar **Impeccable mark** dims and shows a pulsing amber dot when no agent is long-polling `/poll`. Hover the mark for the hint; restart `live-poll.mjs` to reconnect. 4. On `generate`: read screenshot if present; load the action's reference; plan three distinct directions; write all variants in one edit; `--reply done`; poll again. -5. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts remain recoverable until you finish cleanup, run `live-complete.mjs --id EVENT_ID`, and only then poll again. -6. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart. -7. On `exit`: run the cleanup at the bottom. +5. On `steer`: read the message and `pageUrl`; do the work (page edits, navigation help, or a short reply in the `--reply` message); `--reply steer_done`; poll again. No pickup ack. The Steer bar unlocks when `steer_done` arrives over SSE. +6. On `accept` / `discard`: the poll script runs `live-accept.mjs`, acknowledges the delivered event, and prints `_completionAck`. Plain accepts/discards are terminal immediately; carbonize accepts remain recoverable until you finish cleanup, run `live-complete.mjs --id EVENT_ID`, and only then poll again. +7. If interrupted, run `live-status.mjs` or `live-resume.mjs` before guessing. The durable journal replays unacknowledged work after helper restart. +8. On `exit`: run the cleanup at the bottom. Harness policy: - **Claude Code**: run the poll as a **background task** (no short timeout). The harness notifies you when it completes, so the main conversation stays free. Do not block the shell. -- **Cursor**: run the poll in the **foreground** (blocking shell; not a background terminal, not a subagent). Cursor background terminals and subagents do not reliably resume the chat with poll stdout. +- **Cursor**: run **one-shot** poll in a **background terminal** with notify on `"type":"(steer|generate|accept|discard|exit)"`. After each event the poll exits; handle it, `--reply`, then start `live-poll.mjs` again. Do **not** use `--stream` on Cursor: incremental stdout notify is slower in practice than exit-based notify (~5s vs sub-second in testing). - **Codex**: run the poll in the **foreground** (blocking shell; not a background task, not a subagent). Codex background exec sessions do not reliably surface poll stdout back into the conversation at the moment events arrive, so a "fire-and-forget" background poll will stall live mode. -- **Other harnesses**: foreground unless you know stdout reliably returns to this session. +- **Other harnesses**: one-shot foreground unless you know stdout reliably returns to this session when a shell exits. Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodies. Spend tokens on tools and edits; on failure, one or two short sentences. @@ -30,7 +37,7 @@ Chat is overhead. No recap, no tutorial output, no pasting PRODUCT / DESIGN bodi node {{scripts_path}}/live.mjs ``` -Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath, migrated }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation; **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. If `migrated: true`, the loader auto-renamed legacy `.impeccable.md` to `PRODUCT.md`; mention this once and suggest `/impeccable document` for the matching DESIGN.md. +Output JSON: `{ ok, serverPort, serverToken, pageFiles, hasProduct, product, productPath, hasDesign, design, designPath }`. `pageFiles` is the list of HTML entries the live script was injected into. Keep PRODUCT.md and DESIGN.md in mind for variant generation; **DESIGN.md wins on visual decisions; PRODUCT.md wins on strategic/voice decisions.** When DESIGN.md is missing, identity is **not** absent; extract it from CSS variables, computed styles, and sibling components on the page (see Step 4 Phase A). Identity preservation is the default; departure from existing identity requires an explicit trigger from PRODUCT.md anti-references or the user's freeform prompt. `serverPort` and `serverToken` belong to the small **Impeccable live helper** HTTP server (serves `/live.js`, SSE, and `/poll`). That port is **not** your dev server and is usually not the URL you open to view the app. The browser page is whatever origin serves one of the `pageFiles` entries (Vite / Next / Bun / tunnel / LAN hostname). @@ -38,12 +45,15 @@ If output is `{ ok: false, error: "config_missing" | "config_invalid", path }`, ## Poll loop +**Default (portable, all harnesses):** + ``` LOOP: node {{scripts_path}}/live-poll.mjs # default long timeout; no --timeout= Read JSON; dispatch on "type" "generate" → Handle Generate; reply done; LOOP + "steer" → Handle Steer; reply steer_done; LOOP "accept" → Handle Accept; complete carbonize cleanup if required; LOOP "discard" → Handle Discard; LOOP "prefetch" → Handle Prefetch; LOOP @@ -51,6 +61,16 @@ LOOP: "exit" → break → Cleanup ``` +**Stream mode (experimental, not for Cursor):** + +``` +node {{scripts_path}}/live-poll.mjs --stream # stays running; one JSON line per event + Handle event; run --reply in a separate command + Repeat until "exit" line → Cleanup +``` + +Stream keeps one process alive and waits for `--reply` ack before polling again. Useful only when the harness reads incremental stdout reliably and quickly. **Cursor is not one of those:** background pattern notify on a long-running shell was ~5s to pick up events vs sub-second for one-shot exit notify. Default to one-shot everywhere unless you have measured otherwise. + ## Recovery commands The live helper persists an append-only journal under `.impeccable/live/sessions/`. Browser checkpoints are advisory but durable; the journal is canonical. This is local durable recovery state, not project source. @@ -71,9 +91,32 @@ Server restart rule: start `live-server.mjs` again, then poll. Startup requeues ## Handle `generate` -Event: `{id, action, freeformPrompt?, count, pageUrl, element, screenshotPath?, comments?, strokes?}`. +**Replace mode** (default): `{id, action, freeformPrompt?, count, pageUrl, element, screenshotPath?, comments?, strokes?}`. -Speed matters; the user is watching a spinner. Minimize tool calls by using the `wrap` helper and writing all variants in a single edit. +**Insert mode** (`event.mode === "insert"`): `{id, mode: "insert", count, pageUrl, insert: { position, anchor }, placeholder: { width, height }, freeformPrompt?, screenshotPath?, comments?, strokes?}`. No `action`. Requires a non-empty `freeformPrompt` **or** annotations. Screenshot is sent only when annotations exist (same rule as replace). Use `placeholder` dimensions as a soft size hint for net-new content. + +Speed matters; the user is watching a spinner. Minimize tool calls by using the wrap/insert helper and writing all variants in a single edit. + +### Insert mode branch + +When `event.mode === "insert"`: + +1. Read the screenshot if `event.screenshotPath` is present (annotations only). +2. Run the insert helper instead of wrap: + +```bash +node {{scripts_path}}/live-insert.mjs --id EVENT_ID --count EVENT_COUNT --position after \ + --element-id "ANCHOR_ID" --classes "class1,class2" --tag "section" --text "ANCHOR_TEXT" +``` + +- `--position` ← `event.insert.position` (`before` | `after`) +- Anchor flags ← `event.insert.anchor` (same mapping as wrap: id, classes, tag, text) + +The scaffold has **no** `data-impeccable-variant="original"`. Variants are net-new HTML+CSS inserted at `insertLine`. Load `brand.md` or `product.md` (freeform only, no action sub-command). Write all variants in one edit, then `--reply done`. + +On accept/discard, `live-accept.mjs` removes the wrapper block; the anchor element is untouched. + +### Replace mode (default) ### 1. Read the screenshot (if present) @@ -424,6 +467,28 @@ A background agent may be used for the rewrite, but the current thread is respon Event: `{id, _acceptResult, _completionAck}`. The poll script already restored the original, removed all variant markers, and acknowledged `discarded` durable completion. Nothing to do unless `_completionAck.ok !== true`; in that case run `live-complete.mjs --id EVENT_ID --discarded`, then poll again. +## Handle `steer` + +Event: `{id, message, pageUrl}`. The user typed or spoke into the global bar **Steer** control: page-level direction without picking an element or launching variant generation. + +The mic button uses the browser **Web Speech API** (MVP): click to start, speak, stop automatically when the utterance ends, then the transcript submits as a steer event. Click again while listening to cancel without submitting. + +This is lighter than `generate`: no screenshot, no element context, no variant cycling. Read `message` and inspect the live page or project files as needed, then either make edits or answer in prose. + +When finished: + +```bash +node {{scripts_path}}/live-poll.mjs --reply EVENT_ID steer_done ["Optional short note for a browser toast"] +``` + +On failure: + +```bash +node {{scripts_path}}/live-poll.mjs --reply EVENT_ID error "Short reason" +``` + +Then poll again immediately. Do not send a separate "picked up" reply. The Steer bar stays locked until `steer_done` or `error` arrives over SSE. + ## Handle `prefetch` Event: `{pageUrl}`. The browser fires this the first time the user selects an element on a given route, as a latency shortcut; it signals the user is likely about to Go on a page you haven't read yet. diff --git a/skill/reference/motion-design.md b/skill/reference/motion-design.md deleted file mode 100644 index 78b6fd260..000000000 --- a/skill/reference/motion-design.md +++ /dev/null @@ -1,109 +0,0 @@ -# Motion Design - -## Duration: The 100/300/500 Rule - -Timing matters more than easing. These durations feel right for most UI: - -| Duration | Use Case | Examples | -|----------|----------|----------| -| **100-150ms** | Instant feedback | Button press, toggle, color change | -| **200-300ms** | State changes | Menu open, tooltip, hover states | -| **300-500ms** | Layout changes | Accordion, modal, drawer | -| **500-800ms** | Entrance animations | Page load, hero reveals | - -**Exit animations are faster than entrances.** Use ~75% of enter duration. - -## Easing: Pick the Right Curve - -**Don't use `ease`.** It's a compromise that's rarely optimal. Instead: - -| Curve | Use For | CSS | -|-------|---------|-----| -| **ease-out** | Elements entering | `cubic-bezier(0.16, 1, 0.3, 1)` | -| **ease-in** | Elements leaving | `cubic-bezier(0.7, 0, 0.84, 0)` | -| **ease-in-out** | State toggles (there → back) | `cubic-bezier(0.65, 0, 0.35, 1)` | - -**For micro-interactions, use exponential curves.** They feel natural because they mimic real physics (friction, deceleration): - -```css -/* Quart out - smooth, refined (recommended default) */ ---ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1); - -/* Quint out - slightly more dramatic */ ---ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1); - -/* Expo out - snappy, confident */ ---ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); -``` - -**Avoid bounce and elastic curves.** They were trendy in 2015 but now feel tacky and amateurish. Real objects don't bounce when they stop; they decelerate smoothly. Overshoot effects draw attention to the animation itself rather than the content. - -## Premium Motion Materials - -Transform and opacity are reliable defaults, not the whole palette. Premium interfaces often need atmospheric properties: blur reveals, backdrop-filter panels, saturation or brightness shifts, shadow bloom, SVG filters, masks, clip paths, gradient-position movement, and variable font or shader-driven effects. - -Use the right material for the effect: - -- **Transform / opacity**: movement, press feedback, simple reveals, list choreography. -- **Blur / filter / backdrop-filter**: focus pulls, depth, glass or lens effects, softened entrances, atmospheric transitions. -- **Clip path / masks**: wipes, reveals, editorial cropping, product-like transitions. -- **Shadow / glow / color filters**: energy, affordance, focus, warmth, active state. -- **Grid-template rows or FLIP-style transforms**: expanding and reflowing layout without animating `height` directly. - -The hard rule is not "transform and opacity only." The hard rule is: avoid animating layout-driving properties casually (`width`, `height`, `top`, `left`, margins), keep expensive effects bounded to small or isolated areas, and verify in-browser that the result is smooth on the target viewports. If blur/filter makes the interaction feel significantly more premium and remains smooth, use it. - -## Staggered Animations - -Use CSS custom properties for cleaner stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"` on each item. **Cap total stagger time**: 10 items at 50ms = 500ms total. For many items, reduce per-item delay or cap staggered count. - -## Reduced Motion - -This is not optional. Vestibular disorders affect ~35% of adults over 40. - -```css -/* Define animations normally */ -.card { - animation: slide-up 500ms ease-out; -} - -/* Provide alternative for reduced motion */ -@media (prefers-reduced-motion: reduce) { - .card { - animation: fade-in 200ms ease-out; /* Crossfade instead of motion */ - } -} - -/* Or disable entirely */ -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - transition-duration: 0.01ms !important; - } -} -``` - -**What to preserve**: Functional animations like progress bars, loading spinners (slowed down), and focus indicators should still work, just without spatial movement. - -## Perceived Performance - -**Nobody cares how fast your site is, just how fast it feels.** Perception can be as effective as actual performance. - -**The 80ms threshold**: Our brains buffer sensory input for ~80ms to synchronize perception. Anything under 80ms feels instant and simultaneous. This is your target for micro-interactions. - -**Active vs passive time**: Passive waiting (staring at a spinner) feels longer than active engagement. Strategies to shift the balance: - -- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening. -- **Early completion**: Show content progressively, don't wait for everything. Video buffering, progressive images, streaming HTML. -- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Instagram likes work offline; the UI updates instantly, syncs later. Use for low-stakes actions; avoid for payments or destructive operations. - -**Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances, but ease-in toward a task's end compresses perceived time. - -**Caution**: Too-fast responses can decrease perceived value. Users may distrust instant results for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening. - -## Performance - -Don't use `will-change` preemptively, only when animation is imminent (`:hover`, `.animating`). For scroll-triggered animations, use Intersection Observer instead of scroll events; unobserve after animating once. Create motion tokens for consistency (durations, easings, common transitions). - ---- - -**Avoid**: Animating everything (animation fatigue is real). Using >500ms for UI feedback. Ignoring `prefers-reduced-motion`. Using animation to hide slow loading. diff --git a/skill/reference/personas.md b/skill/reference/personas.md deleted file mode 100644 index 37f5f9e60..000000000 --- a/skill/reference/personas.md +++ /dev/null @@ -1,179 +0,0 @@ -# Persona-Based Design Testing - -Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss. - -**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags, not generic concerns. - ---- - -## 1. Impatient Power User: "Alex" - - -**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave. - -**Behaviors**: -- Skips all onboarding and instructions -- Looks for keyboard shortcuts immediately -- Tries to bulk-select, batch-edit, and automate -- Gets frustrated by required steps that feel unnecessary -- Abandons if anything feels slow or patronizing - -**Test Questions**: -- Can Alex complete the core task in under 60 seconds? -- Are there keyboard shortcuts for common actions? -- Can onboarding be skipped entirely? -- Do modals have keyboard dismiss (Esc)? -- Is there a "power user" path (shortcuts, bulk actions)? - -**Red Flags** (report these specifically): -- Forced tutorials or unskippable onboarding -- No keyboard navigation for primary actions -- Slow animations that can't be skipped -- One-item-at-a-time workflows where batch would be natural -- Redundant confirmation steps for low-risk actions - ---- - -## 2. Confused First-Timer: "Jordan" - -**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out. - -**Behaviors**: -- Reads all instructions carefully -- Hesitates before clicking anything unfamiliar -- Looks for help or support constantly -- Misunderstands jargon and abbreviations -- Takes the most literal interpretation of any label - -**Test Questions**: -- Is the first action obviously clear within 5 seconds? -- Are all icons labeled with text? -- Is there contextual help at decision points? -- Does terminology assume prior knowledge? -- Is there a clear "back" or "undo" at every step? - -**Red Flags** (report these specifically): -- Icon-only navigation with no labels -- Technical jargon without explanation -- No visible help option or guidance -- Ambiguous next steps after completing an action -- No confirmation that an action succeeded - ---- - -## 3. Accessibility-Dependent User: "Sam" - -**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences. - -**Behaviors**: -- Tabs through the interface linearly -- Relies on ARIA labels and heading structure -- Cannot see hover states or visual-only indicators -- Needs adequate color contrast (4.5:1 minimum) -- May use browser zoom up to 200% - -**Test Questions**: -- Can the entire primary flow be completed keyboard-only? -- Are all interactive elements focusable with visible focus indicators? -- Do images have meaningful alt text? -- Is color contrast WCAG AA compliant (4.5:1 for text)? -- Does the screen reader announce state changes (loading, success, errors)? - -**Red Flags** (report these specifically): -- Click-only interactions with no keyboard alternative -- Missing or invisible focus indicators -- Meaning conveyed by color alone (red = error, green = success) -- Unlabeled form fields or buttons -- Time-limited actions without extension option -- Custom components that break screen reader flow - ---- - -## 4. Deliberate Stress Tester: "Riley" - -**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience. - -**Behaviors**: -- Tests edge cases intentionally (empty states, long strings, special characters) -- Submits forms with unexpected data (emoji, RTL text, very long values) -- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs -- Looks for inconsistencies between what the UI promises and what actually happens -- Documents problems methodically - -**Test Questions**: -- What happens at the edges (0 items, 1000 items, very long text)? -- Do error states recover gracefully or leave the UI in a broken state? -- What happens on refresh mid-workflow? Is state preserved? -- Are there features that appear to work but produce broken results? -- How does the UI handle unexpected input (emoji, special chars, paste from Excel)? - -**Red Flags** (report these specifically): -- Features that appear to work but silently fail or produce wrong results -- Error handling that exposes technical details or leaves UI in a broken state -- Empty states that show nothing useful ("No results" with no guidance) -- Workflows that lose user data on refresh or navigation -- Inconsistent behavior between similar interactions in different parts of the UI - ---- - -## 5. Distracted Mobile User: "Casey" - -**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection. - -**Behaviors**: -- Uses thumb only; prefers bottom-of-screen actions -- Gets interrupted mid-flow and returns later -- Switches between apps frequently -- Has limited attention span and low patience -- Types as little as possible, prefers taps and selections - -**Test Questions**: -- Are primary actions in the thumb zone (bottom half of screen)? -- Is state preserved if the user leaves and returns? -- Does it work on slow connections (3G)? -- Can forms use autocomplete and smart defaults? -- Are touch targets at least 44×44pt? - -**Red Flags** (report these specifically): -- Important actions positioned at the top of the screen (unreachable by thumb) -- No state persistence; progress lost on tab switch or interruption -- Large text inputs required where selection would work -- Heavy assets loading on every page (no lazy loading) -- Tiny tap targets or targets too close together - ---- - -## Selecting Personas - -Choose personas based on the interface type: - -| Interface Type | Primary Personas | Why | -|---------------|-----------------|-----| -| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile | -| Dashboard / admin | Alex, Sam | Power users, accessibility | -| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity | -| Onboarding flow | Jordan, Casey | Confusion, interruption | -| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav | -| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile | - ---- - -## Project-Specific Personas - -If `{{config_file}}` contains a `## Design Context` section (generated by `impeccable teach`), derive 1–2 additional personas from the audience and brand information: - -1. Read the target audience description -2. Identify the primary user archetype not covered by the 5 predefined personas -3. Create a persona following this template: - -``` -### [Role]: "[Name]" - -**Profile**: [2-3 key characteristics derived from Design Context] - -**Behaviors**: [3-4 specific behaviors based on the described audience] - -**Red Flags**: [3-4 things that would alienate this specific user type] -``` - -Only generate project-specific personas when real Design Context data is available. Don't invent audience details; use the 5 predefined personas when no context exists. diff --git a/skill/reference/polish.md b/skill/reference/polish.md index 7274c89f6..bc26cfc02 100644 --- a/skill/reference/polish.md +++ b/skill/reference/polish.md @@ -91,7 +91,6 @@ Visual polish on a misshapen flow is wasted work. Match the *shape* of the exper - **Theme consistency**: Works in all theme variants - **Color meaning**: Same colors mean same things throughout - **Accessible focus**: Focus indicators visible with sufficient contrast -- **Tinted neutrals**: No pure gray or pure black; add subtle color tint (0.01 chroma) - **Gray on color**: Never put gray text on colored backgrounds; use a shade of that color or transparency ### Interaction States diff --git a/skill/reference/product.md b/skill/reference/product.md index 64b3b0169..773003f3e 100644 --- a/skill/reference/product.md +++ b/skill/reference/product.md @@ -10,47 +10,45 @@ Product UI's failure mode isn't flatness, it's strangeness without purpose: over ## Typography -- **System fonts are legitimate.** `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif` gives you native feel on every platform. Inter is the common cross-platform default for a reason. -- **One family is often right.** Product UIs don't need display/body pairing. A well-tuned sans carries headings, buttons, labels, body, data. -- **Fixed rem scale, not fluid.** Clamp-sized headings don't serve product UI. Users view at consistent DPI, and a fluid h1 that shrinks in a sidebar looks worse, not better. -- **Tighter scale ratio.** 1.125–1.2 between steps is typical. More type elements here than on brand surfaces; exaggerated contrast creates noise. -- **Line length still applies for prose** (65–75ch). Data and compact UI can run denser; tables at 120ch+ are fine. +- **One family is often right.** Product UIs don't need display/body pairing. A well-tuned sans carries headings, buttons, labels, body, data. +- **Fixed rem scale, not fluid.** Clamp-sized headings don't serve product UI. Users view at consistent DPI, and a fluid h1 that shrinks in a sidebar looks worse, not better. +- **Tighter scale ratio.** 1.125–1.2 between steps is typical. More type elements here than on brand surfaces; exaggerated contrast creates noise. +- **Line length still applies for prose** (65–75ch). Data and compact UI can run denser; tables at 120ch+ are fine. ## Color -Product defaults to Restrained. A single surface can earn Committed (a dashboard where one category color carries a report, an onboarding flow with a drenched welcome screen), but Restrained is the floor. +Product defaults to Restrained. A single surface can earn Committed (a dashboard where one category color carries a report, an onboarding flow with a drenched welcome screen), but Restrained is the floor. -- State-rich semantic vocabulary: hover, focus, active, disabled, selected, loading, error, warning, success, info. Standardize these. -- Accent color used for primary actions, current selection, and state indicators only, not decoration. -- A second neutral layer for sidebars, toolbars, and panels (slightly cooler or warmer than the content surface). +- State-rich semantic vocabulary: hover, focus, active, disabled, selected, loading, error, warning, success, info. Standardize these. +- Accent color used for primary actions, current selection, and state indicators only, not decoration. +- A second neutral layer for sidebars, toolbars, and panels (slightly cooler or warmer than the content surface). ## Layout -- Predictable grids. Consistency IS an affordance; users navigate faster when the structure is expected. -- Familiar patterns are features. Standard navigation (top bar, side nav), breadcrumbs, tabs, and form layouts have established user expectations. Don't reinvent for flavor. -- Responsive behavior is structural (collapse sidebar, responsive table, breakpoint-driven columns), not fluid typography. +- Responsive behavior is structural (collapse sidebar, responsive table, breakpoint-driven columns), not fluid typography. ## Components -Every interactive component has: default, hover, focus, active, disabled, loading, error. Don't ship with half of these. +Every interactive component has: default, hover, focus, active, disabled, loading, error. Don't ship with half of these. -- Skeleton states for loading, not spinners in the middle of content. -- Empty states that teach the interface, not "nothing here." -- Consistent affordances across the surface. Same button shape. Same form-control vocabulary. Same icon style. +- Skeleton states for loading, not spinners in the middle of content. +- Empty states that teach the interface, not "nothing here." +- Consistent affordances across the surface. Same button shape. Same form-control vocabulary. Same icon style. ## Motion -- 150–250 ms on most transitions. Users are in flow; don't make them wait for choreography. -- Motion conveys state, not decoration. State change, feedback, loading, reveal: nothing else. -- No orchestrated page-load sequences. Product loads into a task; users don't want to watch it load. +- 150–250 ms on most transitions. Users are in flow; don't make them wait for choreography. +- Motion conveys state, not decoration. State change, feedback, loading, reveal: nothing else. +- No orchestrated page-load sequences. Product loads into a task; users don't want to watch it load. ## Product bans (on top of the shared absolute bans) -- Decorative motion that doesn't convey state. -- Inconsistent component vocabulary across screens. If the "save" button looks different in two places, one is wrong. -- Display fonts in UI labels, buttons, data. -- Reinventing standard affordances for flavor (custom scrollbars, weird form controls, non-standard modals). -- Heavy color or full-saturation accents on inactive states. +- Decorative motion that doesn't convey state. +- Inconsistent component vocabulary across screens. If the "save" button looks different in two places, one is wrong. +- Display fonts in UI labels, buttons, data. +- Reinventing standard affordances for flavor (custom scrollbars, weird form controls, non-standard modals). +- Heavy color or full-saturation accents on inactive states. +- Modal as first thought. Modals are usually laziness. Exhaust inline / progressive alternatives first. ## Product permissions diff --git a/skill/reference/responsive-design.md b/skill/reference/responsive-design.md deleted file mode 100644 index f079e5e5d..000000000 --- a/skill/reference/responsive-design.md +++ /dev/null @@ -1,114 +0,0 @@ -# Responsive Design - -## Mobile-First: Write It Right - -Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first. - -## Breakpoints: Content-Driven - -Don't chase device sizes; let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints. - -## Detect Input Method, Not Just Screen Size - -**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard. Use pointer and hover queries: - -```css -/* Fine pointer (mouse, trackpad) */ -@media (pointer: fine) { - .button { padding: 8px 16px; } -} - -/* Coarse pointer (touch, stylus) */ -@media (pointer: coarse) { - .button { padding: 12px 20px; } /* Larger touch target */ -} - -/* Device supports hover */ -@media (hover: hover) { - .card:hover { transform: translateY(-2px); } -} - -/* Device doesn't support hover (touch) */ -@media (hover: none) { - .card { /* No hover state - use active instead */ } -} -``` - -**Critical**: Don't rely on hover for functionality. Touch users can't hover. - -## Safe Areas: Handle the Notch - -Modern phones have notches, rounded corners, and home indicators. Use `env()`: - -```css -body { - padding-top: env(safe-area-inset-top); - padding-bottom: env(safe-area-inset-bottom); - padding-left: env(safe-area-inset-left); - padding-right: env(safe-area-inset-right); -} - -/* With fallback */ -.footer { - padding-bottom: max(1rem, env(safe-area-inset-bottom)); -} -``` - -**Enable viewport-fit** in your meta tag: -```html - -``` - -## Responsive Images: Get It Right - -### srcset with Width Descriptors - -```html -Hero image -``` - -**How it works**: -- `srcset` lists available images with their actual widths (`w` descriptors) -- `sizes` tells the browser how wide the image will display -- Browser picks the best file based on viewport width AND device pixel ratio - -### Picture Element for Art Direction - -When you need different crops/compositions (not just resolutions): - -```html - - - - ... - -``` - -## Layout Adaptation Patterns - -**Navigation**: Three stages: hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `
/` for content that can collapse on mobile. - -## Testing: Don't Trust DevTools Alone - -DevTools device emulation is useful for layout but misses: - -- Actual touch interactions -- Real CPU/memory constraints -- Network latency patterns -- Font rendering differences -- Browser chrome/keyboard appearances - -**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators. - ---- - -**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful. diff --git a/skill/reference/shape.md b/skill/reference/shape.md index c7e29cb7e..421ce68b6 100644 --- a/skill/reference/shape.md +++ b/skill/reference/shape.md @@ -151,7 +151,7 @@ How users interact with this feature. What happens on click, hover, scroll? What 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., spatial-design.md for complex layouts, motion-design.md for animated features, interaction-design.md for form-heavy features). +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. diff --git a/skill/reference/spatial-design.md b/skill/reference/spatial-design.md deleted file mode 100644 index 4d4b83afc..000000000 --- a/skill/reference/spatial-design.md +++ /dev/null @@ -1,100 +0,0 @@ -# Spatial Design - -## Spacing Systems - -### Use 4pt Base, Not 8pt - -8pt systems are too coarse; you'll frequently need 12px (between 8 and 16). Use 4pt for granularity: 4, 8, 12, 16, 24, 32, 48, 64, 96px. - -### Name Tokens Semantically - -Name by relationship (`--space-sm`, `--space-lg`), not value (`--spacing-8`). Use `gap` instead of margins for sibling spacing; it eliminates margin collapse and cleanup hacks. - -## Grid Systems - -### The Self-Adjusting Grid - -Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. Columns are at least 280px, as many as fit per row, leftovers stretch. For complex layouts, use named grid areas (`grid-template-areas`) and redefine them at breakpoints. - -## Visual Hierarchy - -### The Squint Test - -Blur your eyes (or screenshot and blur). Can you still identify: -- The most important element? -- The second most important? -- Clear groupings? - -If everything looks the same weight blurred, you have a hierarchy problem. - -### Hierarchy Through Multiple Dimensions - -Don't rely on size alone. Combine: - -| Tool | Strong Hierarchy | Weak Hierarchy | -|------|------------------|----------------| -| **Size** | 3:1 ratio or more | <2:1 ratio | -| **Weight** | Bold vs Regular | Medium vs Regular | -| **Color** | High contrast | Similar tones | -| **Position** | Top/left (primary) | Bottom/right | -| **Space** | Surrounded by white space | Crowded | - -**The best hierarchy uses 2-3 dimensions at once**: A heading that's larger, bolder, AND has more space above it. - -### Cards Are Not Required - -Cards are overused. Spacing and alignment create visual grouping naturally. Use cards only when content is truly distinct and actionable, items need visual comparison in a grid, or content needs clear interaction boundaries. **Never nest cards inside cards.** Use spacing, typography, and subtle dividers for hierarchy within a card. - -## Container Queries - -Viewport queries are for page layouts. **Container queries are for components**: - -```css -.card-container { - container-type: inline-size; -} - -.card { - display: grid; - gap: var(--space-md); -} - -/* Card layout changes based on its container, not viewport */ -@container (min-width: 400px) { - .card { - grid-template-columns: 120px 1fr; - } -} -``` - -**Why this matters**: A card in a narrow sidebar stays compact, while the same card in a main content area expands automatically, without viewport hacks. - -## Optical Adjustments - -Text at `margin-left: 0` looks indented due to letterform whitespace; use negative margin (`-0.05em`) to optically align. Geometrically centered icons often look off-center; play icons need to shift right, arrows shift toward their direction. - -### Touch Targets vs Visual Size - -Buttons can look small but need large touch targets (44px minimum). Use padding or pseudo-elements: - -```css -.icon-button { - width: 24px; /* Visual size */ - height: 24px; - position: relative; -} - -.icon-button::before { - content: ''; - position: absolute; - inset: -10px; /* Expand tap target to 44px */ -} -``` - -## Depth & Elevation - -Create semantic z-index scales (dropdown → sticky → modal-backdrop → modal → toast → tooltip) instead of arbitrary numbers. For shadows, create a consistent elevation scale (sm → md → lg → xl). **Key insight**: Shadows should be subtle. If you can clearly see it, it's probably too strong. - ---- - -**Avoid**: Arbitrary spacing values outside your scale. Making all spacing equal (variety creates hierarchy). Creating hierarchy through size alone - combine size, weight, color, and space. diff --git a/skill/reference/typeset.md b/skill/reference/typeset.md index acf95ad0b..ba5eb86bb 100644 --- a/skill/reference/typeset.md +++ b/skill/reference/typeset.md @@ -4,7 +4,7 @@ Typography carries most of the information on the page. Replace generic defaults ## Register -Brand: run the font selection procedure in [brand.md](brand.md). Pairing follows the brand's lane (display serif + sans body for editorial/luxury, one committed sans for tech, etc.). Fluid `clamp()` scale, ≥1.25 ratio between steps. +Brand: run the font selection procedure in [brand.md](brand.md). Fluid `clamp()` scale, ≥1.25 ratio between steps. Product: system fonts and familiar sans stacks are legitimate here. One well-tuned family typically carries the whole UI. Fixed `rem` scale, 1.125–1.2 ratio between more closely-spaced steps. @@ -43,7 +43,7 @@ Analyze what's weak or generic about the current type: ## Plan Typography Improvements -Consult the [typography reference](typography.md) for detailed guidance on scales, pairing, and loading strategies. +Consult the [Reference Material](#reference-material) section below for detailed guidance on scales, pairing, and loading strategies. Create a systematic plan: @@ -122,3 +122,158 @@ Each variant MUST declare a `scale` param controlling the hierarchy ratio. Expre Where the variant riffs on a specific pairing, expose the pairing choice as a `steps` param (e.g. "serif display + sans body" vs. "mono display + sans body" vs. "all-sans"). Each branch routes through `:scope[data-p-pairing="X"]` selectors in scoped CSS. See `reference/live.md` for the full params contract. + +--- + +## Reference Material + +The sections below were previously `typography.md` and live inline now so the typeset flow has its deep typography reference in one place. `bolder.md` also references this section. + +### Typography + +#### Classic Typography Principles + +##### Vertical Rhythm + +Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony; text and space share a mathematical foundation. + +##### Modular Scale & Hierarchy + +The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy. + +**Use fewer sizes with more contrast.** A 5-size system covers most needs: + +| Role | Typical Ratio | Use Case | +|------|---------------|----------| +| xs | 0.75rem | Captions, legal | +| sm | 0.875rem | Secondary UI, metadata | +| base | 1rem | Body text | +| lg | 1.25-1.5rem | Subheadings, lead text | +| xl+ | 2-4rem | Headlines, hero text | + +Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit. + +##### Readability & Measure + +Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length: narrow columns need tighter leading, wide columns need more. + +**Non-obvious**: Light text on dark backgrounds needs compensation on three axes, not just one. Bump line-height by 0.05–0.1, add a touch of letter-spacing (0.01–0.02em), and optionally step the body weight up one notch (regular → medium). The perceived weight drops across all three; fix all three. + +**Paragraph rhythm**: Pick either space between paragraphs OR first-line indentation. Never both. Digital usually wants space; editorial/long-form can justify indent-only. + +#### Font Selection & Pairing + +The tactical selection procedure and the reflex-reject list live in [reference/brand.md](brand.md) under **Font selection procedure** and **Reflex-reject list** (loaded for brand-register tasks). The rest of this section covers the adjacent knowledge: anti-reflex corrections, system font use, and pairing rules. + +##### Anti-reflexes worth defending against + +- A technical/utilitarian brief does NOT need a serif "for warmth." Most tech tools should look like tech tools. +- An editorial/premium brief does NOT need the same expressive serif everyone is using right now. Premium can be Swiss-modern, can be neo-grotesque, can be a literal monospace, can be a quiet humanist sans. +- A children's product does NOT need a rounded display font. Kids' books use real type. +- A "modern" brief does NOT need a geometric sans. The most modern thing you can do is not use the font everyone else is using. + +**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality. + +##### Pairing Principles + +**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif). + +When pairing, contrast on multiple axes: +- Serif + Sans (structure contrast) +- Geometric + Humanist (personality contrast) +- Condensed display + Wide body (proportion contrast) + +##### Web Font Loading + +The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix: + +```css +/* 1. Use font-display: swap for visibility */ +@font-face { + font-family: 'CustomFont'; + src: url('font.woff2') format('woff2'); + font-display: swap; +} + +/* 2. Match fallback metrics to minimize shift */ +@font-face { + font-family: 'CustomFont-Fallback'; + src: local('Arial'); + size-adjust: 105%; /* Scale to match x-height */ + ascent-override: 90%; /* Match ascender height */ + descent-override: 20%; /* Match descender depth */ + line-gap-override: 10%; /* Match line spacing */ +} + +body { + font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; +} +``` + +Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically. + +**`swap` vs `optional`**: `swap` shows fallback text immediately and FOUT-swaps when the web font arrives. `optional` uses the fallback if the web font misses a small load budget (~100ms) and avoids the shift entirely. Pick `optional` when zero layout shift matters more than seeing the branded font on slow networks. + +**Preload the critical weight only**: typically the regular-weight body font used above the fold. Preloading every weight costs more bandwidth than it saves. + +**Variable fonts for 3+ weights or styles**: a single variable font file is usually smaller than three static weight files, gives fractional weight control, and pairs well with `font-optical-sizing: auto`. For 1–2 weights, static is fine. + +#### Modern Web Typography + +##### Fluid Type + +Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate (higher vw = faster scaling). Add a rem offset so it doesn't collapse to 0 on small screens. + +**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes. + +**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI; fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it. + +**Bound your clamp()**: keep `max-size ≤ ~2.5 × min-size`. Wider ratios break the browser's zoom and reflow behaviour and make large viewports feel like the page is shouting. + +**Scale container width and font-size together** so effective character measure stays in the 45–75ch band at every viewport. A heading that widens faster than its container drifts out of the comfortable measure at the top end. + +##### OpenType Features + +Most developers don't know these exist. Use them for polish: + +```css +/* Proper fractions */ +.recipe-amount { font-variant-numeric: diagonal-fractions; } + +/* Small caps for abbreviations */ +abbr { font-variant-caps: all-small-caps; } + +/* Disable ligatures in code */ +code { font-variant-ligatures: none; } + +/* Enable kerning (usually on by default, but be explicit) */ +body { font-kerning: normal; } +``` + +Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/). + +##### Rendering polish + +```css +/* Variable fonts: pick the right optical-size master automatically */ +body { font-optical-sizing: auto; } +``` + +**ALL-CAPS tracking**: capitals sit too close at default spacing. Add 5–12% letter-spacing (`letter-spacing: 0.05em` to `0.12em`) to short all-caps labels, eyebrows, and small headings. Real small caps (via `font-variant-caps`) need the same treatment, slightly gentler. + +#### Typography System Architecture + +Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system. + +#### Accessibility Considerations + +Beyond contrast ratios (which are well-documented), consider: + +- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout. +- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text. +- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile. +- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets. + +--- + +**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text. diff --git a/skill/reference/typography.md b/skill/reference/typography.md deleted file mode 100644 index ed5ca272a..000000000 --- a/skill/reference/typography.md +++ /dev/null @@ -1,159 +0,0 @@ -# Typography - -## Classic Typography Principles - -### Vertical Rhythm - -Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony; text and space share a mathematical foundation. - -### Modular Scale & Hierarchy - -The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy. - -**Use fewer sizes with more contrast.** A 5-size system covers most needs: - -| Role | Typical Ratio | Use Case | -|------|---------------|----------| -| xs | 0.75rem | Captions, legal | -| sm | 0.875rem | Secondary UI, metadata | -| base | 1rem | Body text | -| lg | 1.25-1.5rem | Subheadings, lead text | -| xl+ | 2-4rem | Headlines, hero text | - -Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit. - -### Readability & Measure - -Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length: narrow columns need tighter leading, wide columns need more. - -**Non-obvious**: Light text on dark backgrounds needs compensation on three axes, not just one. Bump line-height by 0.05–0.1, add a touch of letter-spacing (0.01–0.02em), and optionally step the body weight up one notch (regular → medium). The perceived weight drops across all three; fix all three. - -**Paragraph rhythm**: Pick either space between paragraphs OR first-line indentation. Never both. Digital usually wants space; editorial/long-form can justify indent-only. - -## Font Selection & Pairing - -The tactical selection procedure and the reflex-reject list live in [reference/brand.md](brand.md) under **Font selection procedure** and **Reflex-reject list** (loaded for brand-register tasks). The rest of this section covers the adjacent knowledge: anti-reflex corrections, system font use, and pairing rules. - -### Anti-reflexes worth defending against - -- A technical/utilitarian brief does NOT need a serif "for warmth." Most tech tools should look like tech tools. -- An editorial/premium brief does NOT need the same expressive serif everyone is using right now. Premium can be Swiss-modern, can be neo-grotesque, can be a literal monospace, can be a quiet humanist sans. -- A children's product does NOT need a rounded display font. Kids' books use real type. -- A "modern" brief does NOT need a geometric sans. The most modern thing you can do is not use the font everyone else is using. - -**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality. - -### Pairing Principles - -**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif). - -When pairing, contrast on multiple axes: -- Serif + Sans (structure contrast) -- Geometric + Humanist (personality contrast) -- Condensed display + Wide body (proportion contrast) - -**Never pair fonts that are similar but not identical** (e.g., two geometric sans-serifs). They create visual tension without clear hierarchy. - -### Web Font Loading - -The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix: - -```css -/* 1. Use font-display: swap for visibility */ -@font-face { - font-family: 'CustomFont'; - src: url('font.woff2') format('woff2'); - font-display: swap; -} - -/* 2. Match fallback metrics to minimize shift */ -@font-face { - font-family: 'CustomFont-Fallback'; - src: local('Arial'); - size-adjust: 105%; /* Scale to match x-height */ - ascent-override: 90%; /* Match ascender height */ - descent-override: 20%; /* Match descender depth */ - line-gap-override: 10%; /* Match line spacing */ -} - -body { - font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif; -} -``` - -Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically. - -**`swap` vs `optional`**: `swap` shows fallback text immediately and FOUT-swaps when the web font arrives. `optional` uses the fallback if the web font misses a small load budget (~100ms) and avoids the shift entirely. Pick `optional` when zero layout shift matters more than seeing the branded font on slow networks. - -**Preload the critical weight only**: typically the regular-weight body font used above the fold. Preloading every weight costs more bandwidth than it saves. - -**Variable fonts for 3+ weights or styles**: a single variable font file is usually smaller than three static weight files, gives fractional weight control, and pairs well with `font-optical-sizing: auto`. For 1–2 weights, static is fine. - -## Modern Web Typography - -### Fluid Type - -Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate (higher vw = faster scaling). Add a rem offset so it doesn't collapse to 0 on small screens. - -**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes. - -**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI; fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it. - -**Bound your clamp()**: keep `max-size ≤ ~2.5 × min-size`. Wider ratios break the browser's zoom and reflow behaviour and make large viewports feel like the page is shouting. - -**Scale container width and font-size together** so effective character measure stays in the 45–75ch band at every viewport. A heading that widens faster than its container drifts out of the comfortable measure at the top end. - -### OpenType Features - -Most developers don't know these exist. Use them for polish: - -```css -/* Tabular numbers for data alignment */ -.data-table { font-variant-numeric: tabular-nums; } - -/* Proper fractions */ -.recipe-amount { font-variant-numeric: diagonal-fractions; } - -/* Small caps for abbreviations */ -abbr { font-variant-caps: all-small-caps; } - -/* Disable ligatures in code */ -code { font-variant-ligatures: none; } - -/* Enable kerning (usually on by default, but be explicit) */ -body { font-kerning: normal; } -``` - -Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/). - -### Rendering polish - -```css -/* Even out heading line lengths (browser picks better break points) */ -h1, h2, h3 { text-wrap: balance; } - -/* Reduce orphans and ragged endings in long prose */ -article p { text-wrap: pretty; } - -/* Variable fonts: pick the right optical-size master automatically */ -body { font-optical-sizing: auto; } -``` - -**ALL-CAPS tracking**: capitals sit too close at default spacing. Add 5–12% letter-spacing (`letter-spacing: 0.05em` to `0.12em`) to short all-caps labels, eyebrows, and small headings. Real small caps (via `font-variant-caps`) need the same treatment, slightly gentler. - -## Typography System Architecture - -Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system. - -## Accessibility Considerations - -Beyond contrast ratios (which are well-documented), consider: - -- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout. -- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text. -- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile. -- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets. - ---- - -**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text. diff --git a/skill/reference/ux-writing.md b/skill/reference/ux-writing.md deleted file mode 100644 index 417b5f56c..000000000 --- a/skill/reference/ux-writing.md +++ /dev/null @@ -1,107 +0,0 @@ -# UX Writing - -## The Button Label Problem - -**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns: - -| Bad | Good | Why | -|-----|------|-----| -| OK | Save changes | Says what will happen | -| Submit | Create account | Outcome-focused | -| Yes | Delete message | Confirms the action | -| Cancel | Keep editing | Clarifies what "cancel" means | -| Click here | Download PDF | Describes the destination | - -**For destructive actions**, name the destruction: -- "Delete" not "Remove" (delete is permanent, remove implies recoverable) -- "Delete 5 items" not "Delete selected" (show the count) - -## Error Messages: The Formula - -Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input". - -### Error Message Templates - -| Situation | Template | -|-----------|----------| -| **Format error** | "[Field] needs to be [format]. Example: [example]" | -| **Missing required** | "Please enter [what's missing]" | -| **Permission denied** | "You don't have access to [thing]. [What to do instead]" | -| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." | -| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" | - -### Don't Blame the User - -Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date". - -## Empty States Are Opportunities - -Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items". - -## Voice vs Tone - -**Voice** is your brand's personality, consistent everywhere. -**Tone** adapts to the moment. - -| Moment | Tone Shift | -|--------|------------| -| Success | Celebratory, brief: "Done! Your changes are live." | -| Error | Empathetic, helpful: "That didn't work. Here's what to try..." | -| Loading | Reassuring: "Saving your work..." | -| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." | - -**Never use humor for errors.** Users are already frustrated. Be helpful, not cute. - -## Writing for Accessibility - -**Link text** must have standalone meaning: "View pricing plans" not "Click here". **Alt text** describes information, not the image: "Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context. - -## Writing for Translation - -### Plan for Expansion - -German text is ~30% longer than English. Allocate space: - -| Language | Expansion | -|----------|-----------| -| German | +30% | -| French | +20% | -| Finnish | +30-40% | -| Chinese | -30% (fewer chars, but same width) | - -### Translation-Friendly Patterns - -Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear. - -## Consistency: The Terminology Problem - -Pick one term and stick with it: - -| Inconsistent | Consistent | -|--------------|------------| -| Delete / Remove / Trash | Delete | -| Settings / Preferences / Options | Settings | -| Sign in / Log in / Enter | Sign in | -| Create / Add / New | Create | - -Build a terminology glossary and enforce it. Variety creates confusion. - -## Avoid Redundant Copy - -If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well. - -## Loading States - -Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress. - -## Confirmation Dialogs: Use Sparingly - -Most confirmation dialogs are design failures; consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No"). - -## Form Instructions - -Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking. - ---- - -**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors. diff --git a/skill/scripts/cleanup-deprecated.mjs b/skill/scripts/cleanup-deprecated.mjs index 6aee4710a..bc6400e90 100644 --- a/skill/scripts/cleanup-deprecated.mjs +++ b/skill/scripts/cleanup-deprecated.mjs @@ -25,7 +25,7 @@ import { join, resolve } from 'node:path'; const DEPRECATED_NAMES = [ // v2.0 renames 'frontend-design', // renamed to impeccable - 'teach-impeccable', // folded into /impeccable teach + 'teach-impeccable', // folded into /impeccable init // v2.1 merges 'arrange', // renamed to layout 'normalize', // merged into polish diff --git a/skill/scripts/command-metadata.json b/skill/scripts/command-metadata.json index 1488bd5f1..83796d54b 100644 --- a/skill/scripts/command-metadata.json +++ b/skill/scripts/command-metadata.json @@ -3,8 +3,8 @@ "description": "Full confirmed-brief-then-build flow. Runs multi-round shape discovery first, resolves visual probe and north-star mock gates when available, then builds and visually iterates. Use when building a new feature end-to-end.", "argumentHint": "[feature description]" }, - "teach": { - "description": "Gathers design context for a project. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles) and, when code exists to analyze, DESIGN.md (visual: colors, typography, components). Every other command reads these files before doing work. Use once per project.", + "init": { + "description": "Sets up a project for impeccable. Runs a multi-round discovery interview when context is missing and writes PRODUCT.md (strategic: users, brand, principles); offers DESIGN.md (visual: colors, typography, components) when code exists; pre-configures live mode; then recommends the best commands to run next. Every other command reads these files before doing work. Use once per project.", "argumentHint": "" }, "document": { diff --git a/skill/scripts/context.mjs b/skill/scripts/context.mjs new file mode 100644 index 000000000..782354003 --- /dev/null +++ b/skill/scripts/context.mjs @@ -0,0 +1,266 @@ +/** + * Context loader: prints PRODUCT.md (and DESIGN.md if present) as one + * markdown block on stdout, or exits with empty stdout when no PRODUCT.md + * is found anywhere. The skill keys off "empty stdout" to branch into the + * init flow. + * + * Path resolution (first match wins): + * 1. cwd, if PRODUCT.md or DESIGN.md is there + * 2. .agents/context/ then docs/ + * 3. $IMPECCABLE_CONTEXT_DIR (absolute or cwd-relative) — power-user + * escape hatch, only consulted when defaults are empty + * 4. cwd as a "nothing found" default + * + * `resolveContextDir()` and `loadContext()` are also exported for the + * server-side scripts (live.mjs, live-server.mjs) that need the structured + * shape rather than the markdown block. + */ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; +const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; +const FALLBACK_DIRS = ['.agents/context', 'docs']; + +// ─── Update check ────────────────────────────────────────────────────────── +// Piggyback a lightweight skill-version check on the once-per-session boot. +// When a newer skill ships, append an UPDATE_AVAILABLE directive so the agent +// can offer `npx impeccable skills update`. Everything here is best-effort and +// silent on failure: a network problem, sandbox, or missing cache must never +// block context output or print an error. + +const UPDATE_HOST = (process.env.IMPECCABLE_UPDATE_HOST || 'https://impeccable.style').replace(/\/$/, ''); +const UPDATE_CACHE_PATH = + process.env.IMPECCABLE_UPDATE_CACHE || path.join(os.homedir(), '.impeccable', 'update-check.json'); +const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // throttle the network poll to once a day +const RENOTIFY_INTERVAL_MS = 7 * 24 * 60 * 60 * 1000; // don't re-surface the same version for a week +const FETCH_TIMEOUT_MS = 1200; + +export function resolveContextDir(cwd = process.cwd()) { + if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { + return cwd; + } + for (const rel of FALLBACK_DIRS) { + const candidate = path.resolve(cwd, rel); + if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { + return candidate; + } + } + const envDir = process.env.IMPECCABLE_CONTEXT_DIR; + if (envDir && envDir.trim()) { + const trimmed = envDir.trim(); + return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); + } + return cwd; +} + +export function loadContext(cwd = process.cwd()) { + const contextDir = resolveContextDir(cwd); + const productPath = firstExisting(contextDir, PRODUCT_NAMES); + const designPath = firstExisting(contextDir, DESIGN_NAMES); + const product = productPath ? safeRead(productPath) : null; + const design = designPath ? safeRead(designPath) : null; + return { + hasProduct: !!product, + product, + productPath: productPath ? path.relative(cwd, productPath) : null, + hasDesign: !!design, + design, + designPath: designPath ? path.relative(cwd, designPath) : null, + contextDir, + }; +} + +function firstExisting(dir, names) { + for (const name of names) { + const abs = path.join(dir, name); + if (fs.existsSync(abs)) return abs; + } + return null; +} + +function safeRead(p) { + try { + return fs.readFileSync(p, 'utf-8'); + } catch { + return null; + } +} + +/** + * Pull the register (`brand` or `product`) out of PRODUCT.md by looking + * for a `## Register` section and reading the first non-empty line that + * follows it. Returns null when the file is legacy / register-less. + */ +function extractRegister(product) { + if (!product) return null; + const lines = product.split('\n'); + for (let i = 0; i < lines.length; i++) { + if (/^##\s+Register\b/i.test(lines[i].trim())) { + for (let j = i + 1; j < lines.length; j++) { + const next = lines[j].trim(); + if (!next) continue; + const word = next.toLowerCase(); + if (word === 'brand' || word === 'product') return word; + return null; + } + } + } + return null; +} + +/** + * Read the installed skill's own version from the sibling SKILL.md frontmatter + * (this file lives at `/scripts/context.mjs`). Returns null when the + * frontmatter is missing or unreadable. + */ +function readLocalSkillVersion() { + try { + const here = path.dirname(fileURLToPath(import.meta.url)); + const skillMd = path.join(here, '..', 'SKILL.md'); + const content = fs.readFileSync(skillMd, 'utf-8'); + const match = content.match(/^version:\s*(.+)$/m); + return match ? match[1].trim().replace(/^["']|["']$/g, '') : null; + } catch { + return null; + } +} + +function readUpdateCache() { + try { + return JSON.parse(fs.readFileSync(UPDATE_CACHE_PATH, 'utf-8')); + } catch { + return {}; + } +} + +function writeUpdateCache(cache) { + try { + fs.mkdirSync(path.dirname(UPDATE_CACHE_PATH), { recursive: true }); + fs.writeFileSync(UPDATE_CACHE_PATH, JSON.stringify(cache)); + } catch { + // Best-effort: a read-only home dir just means we re-poll next session. + } +} + +/** Compare dotted numeric versions. Returns >0 when a is newer than b. */ +function compareSemver(a, b) { + const pa = String(a).split('.').map(n => parseInt(n, 10) || 0); + const pb = String(b).split('.').map(n => parseInt(n, 10) || 0); + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + const diff = (pa[i] || 0) - (pb[i] || 0); + if (diff !== 0) return diff; + } + return 0; +} + +async function fetchLatestSkillVersion() { + try { + const res = await fetch(`${UPDATE_HOST}/api/version`, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }); + if (!res.ok) return null; + const data = await res.json(); + return typeof data?.skills === 'string' ? data.skills : null; + } catch { + return null; // offline, sandboxed, timed out, or bad JSON: all non-fatal + } +} + +function buildUpdateDirective(localVersion, latestVersion) { + return ( + `UPDATE_AVAILABLE: A newer Impeccable skill is available ` + + `(installed v${localVersion}, latest v${latestVersion}). ` + + `Before continuing, ask the user once: "A newer Impeccable (v${latestVersion}) is available. ` + + `Update now? It runs \`npx impeccable skills update\`." ` + + `If they agree, run \`npx impeccable skills update\` (the update applies to the next session, not this one). ` + + `Either way, continue the current task without waiting, and do not raise this again.` + ); +} + +/** + * Best-effort update directive for the boot output. Returns a string to append + * or null. Polls the version endpoint at most once per day (cached globally in + * the user's home dir) and re-surfaces a given version at most once per week so + * the agent never nags. Opt out entirely with IMPECCABLE_NO_UPDATE_CHECK=1. + */ +async function computeUpdateDirective(now = Date.now()) { + try { + if (process.env.IMPECCABLE_NO_UPDATE_CHECK) return null; + const localVersion = readLocalSkillVersion(); + if (!localVersion) return null; + + const cache = readUpdateCache(); + + // Poll the network only when the throttle window has elapsed. Stamp + // lastCheck even on failure so an offline machine doesn't poll every boot. + if (!cache.lastCheck || now - cache.lastCheck > CHECK_INTERVAL_MS) { + const latest = await fetchLatestSkillVersion(); + cache.lastCheck = now; + if (latest) cache.latestVersion = latest; + writeUpdateCache(cache); + } + + const latest = cache.latestVersion; + if (!latest || compareSemver(latest, localVersion) <= 0) return null; + + // Anti-nag: surface a given version at most once per RENOTIFY window. + if (cache.notifiedVersion === latest && cache.notifiedAt && now - cache.notifiedAt < RENOTIFY_INTERVAL_MS) { + return null; + } + cache.notifiedVersion = latest; + cache.notifiedAt = now; + writeUpdateCache(cache); + + return buildUpdateDirective(localVersion, latest); + } catch { + return null; + } +} + +async function cli() { + const ctx = loadContext(process.cwd()); + const updateDirective = await computeUpdateDirective(); + + if (!ctx.hasProduct) { + // Direct stdout message instead of relying on empty output as a signal + // — cheap models miss the empty case more often than the explicit one. + const parts = [ + 'NO_PRODUCT_MD: This project has no PRODUCT.md yet. ' + + 'Stop the current task, load reference/init.md, and follow its ' + + 'instructions to write PRODUCT.md before resuming.', + ]; + if (updateDirective) parts.push(updateDirective); + process.stdout.write(parts.join('\n\n---\n\n') + '\n'); + process.exit(0); + } + const parts = [`# PRODUCT.md\n\n${ctx.product.trim()}`]; + if (ctx.hasDesign) { + parts.push(`# DESIGN.md\n\n${ctx.design.trim()}`); + } + const register = extractRegister(ctx.product); + const next = register + ? `NEXT STEP: This project's register is \`${register}\`. You MUST now read \`reference/${register}.md\` before producing any design output.` + : `NEXT STEP: You MUST now read the matching register reference (\`reference/brand.md\` or \`reference/product.md\`) before producing any design output. Pick based on PRODUCT.md above.`; + parts.push(next); + if (updateDirective) parts.push(updateDirective); + process.stdout.write(parts.join('\n\n---\n\n') + '\n'); +} + +// Run cli() only when this module is the entry point. Compare realpaths +// rather than endsWith(): a loose suffix match also fires for unrelated +// scripts like `load-context.mjs`, and realpath tolerates symlinked +// invocation (the test harness symlinks the skill dir). +function invokedAsScript() { + const arg = process.argv[1]; + if (!arg) return false; + try { + return fs.realpathSync(arg) === fs.realpathSync(fileURLToPath(import.meta.url)); + } catch { + return false; + } +} + +if (invokedAsScript()) { + cli(); +} diff --git a/skill/scripts/design-parser.mjs b/skill/scripts/design-parser.mjs index b68756640..82a5cd7dc 100644 --- a/skill/scripts/design-parser.mjs +++ b/skill/scripts/design-parser.mjs @@ -63,7 +63,7 @@ function parseYamlSubset(yaml) { } const key = content.slice(0, colonIdx).trim(); - const rest = content.slice(colonIdx + 1).trim(); + const rest = stripInlineYamlComment(content.slice(colonIdx + 1).trim()); const parent = stack[stack.length - 1].obj; if (rest === '') { @@ -93,6 +93,21 @@ function findTopLevelColon(s) { return -1; } +function stripInlineYamlComment(s) { + let inQuote = null; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (inQuote) { + if (ch === inQuote && s[i - 1] !== '\\') inQuote = null; + } else if (ch === '"' || ch === "'") { + inQuote = ch; + } else if (ch === '#' && i > 0 && /\s/.test(s[i - 1])) { + return s.slice(0, i).trimEnd(); + } + } + return s; +} + function parseScalar(raw) { const s = raw.trim(); if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { diff --git a/skill/scripts/live-browser.js b/skill/scripts/live-browser.js index e67cd01ed..6f7bb068e 100644 --- a/skill/scripts/live-browser.js +++ b/skill/scripts/live-browser.js @@ -30,20 +30,25 @@ // Design tokens // --------------------------------------------------------------------------- - // Brand magenta is pinned to the site token (--color-accent in main.css) - // so Accept / knobs / cycle-dots match the site's accent, not a washed - // theme-adjusted one. + // Brand kinpaku (gold) is pinned to the site's neo-kinpaku tokens + // (see site/styles/kinpaku-tokens.css) so Accept / knobs / cycle-dots / + // the selection outline / the comment tag all match the site's accent, + // not a washed theme-adjusted one. These mirror the kit's picker + // colors in site/styles/kinpaku-kit.css; keep them in sync by hand. const C = { - brand: 'oklch(60% 0.25 350)', - brandHov: 'oklch(52% 0.25 350)', - brandSoft: 'oklch(60% 0.25 350 / 0.15)', - ink: 'oklch(15% 0.01 350)', - ash: 'oklch(55% 0 0)', - paper: 'oklch(98% 0.005 350 / 0.92)', - paperSolid:'oklch(98% 0.005 350)', - mist: 'oklch(90% 0.01 350 / 0.6)', + brand: 'oklch(84% 0.19 80.46)', // kinpaku gold + brandHov: 'oklch(86% 0.07 84)', // kinpaku-pale (hover lift) + brandSoft: 'oklch(84% 0.19 80.46 / 0.18)', // kinpaku-dim + ink: 'oklch(4% 0.004 95)', // lacquer-deep + ash: 'oklch(55% 0.018 82)', // warm muted text + paper: 'oklch(98% 0.005 95 / 0.92)', // light overlay on user pages + paperSolid:'oklch(98% 0.005 95)', + mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline white: 'oklch(99% 0 0)', }; + // Picker bar chrome — mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css + const PICKER_SHADOW = + '0 0 0 1px oklch(78% 0.12 82 / 0.18), 0 10px 28px oklch(0% 0 0 / 0.28)'; const FONT = 'system-ui, -apple-system, sans-serif'; const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace'; // z-index: detect overlays use 99999, so our UI must be above them @@ -269,6 +274,7 @@ function showHighlight(el) { if (!el || !highlightEl) return; + if (el.hasAttribute?.('data-impeccable-insert-placeholder')) return; const r = el.getBoundingClientRect(); const top = (r.top - 2) + 'px', left = (r.left - 2) + 'px'; const width = (r.width + 4) + 'px', height = (r.height + 4) + 'px'; @@ -301,11 +307,11 @@ } // --------------------------------------------------------------------------- - // Annotation overlay (comment pins + magenta strokes) + // Annotation overlay (comment pins + kinpaku strokes) // // Active while state === 'CONFIGURING'. The overlay is a fixed-positioned // sibling of mirroring selectedElement's bounding rect. Click (no - // drag) drops a comment pin; drag paints a magenta SVG stroke. All coords + // drag) drops a comment pin; drag paints a kinpaku SVG stroke. All coords // are stored in element-local CSS px so they survive scroll / resize and // correlate directly with the captured PNG. // --------------------------------------------------------------------------- @@ -324,6 +330,8 @@ let annotPointer = null; let annotEditing = null; // { idx, input, wrapEl } let annotLastPinClick = { idx: -1, time: 0 }; // for click-click-to-delete + let placeholderResizeLayerEl = null; + let placeholderResizeDrag = null; function initAnnotOverlay() { annotOverlayEl = document.createElement('div'); @@ -370,6 +378,17 @@ }); annotOverlayEl.appendChild(annotClearChipEl); + placeholderResizeLayerEl = document.createElement('div'); + placeholderResizeLayerEl.id = PREFIX + '-placeholder-resize'; + Object.assign(placeholderResizeLayerEl.style, { + position: 'absolute', + inset: '0', + pointerEvents: 'none', + display: 'none', + zIndex: '2', + }); + annotOverlayEl.appendChild(placeholderResizeLayerEl); + annotOverlayEl.addEventListener('pointerdown', onAnnotDown); annotOverlayEl.addEventListener('pointermove', onAnnotMove); annotOverlayEl.addEventListener('pointerup', onAnnotUp); @@ -393,11 +412,14 @@ annotActive = true; positionAnnotOverlay(el); annotOverlayEl.style.display = 'block'; + syncPlaceholderResizeHandles(); } function hideAnnotOverlay() { annotActive = false; + placeholderResizeDrag = null; if (annotOverlayEl) annotOverlayEl.style.display = 'none'; + syncPlaceholderResizeHandles(); // Drop any in-progress edit without touching annotState — clearAnnotations // (if the caller is exiting configure mode) handles state reset. annotEditing = null; @@ -411,6 +433,7 @@ width: r.width + 'px', height: r.height + 'px', }); annotSvgEl.setAttribute('viewBox', '0 0 ' + r.width + ' ' + r.height); + syncPlaceholderResizeHandles(); } function clearAnnotations() { @@ -425,7 +448,7 @@ } // Rebuild the SVG layer. Each stroke gets a wider invisible hit path - // beneath the visible magenta path so clicks register on thin lines. + // beneath the visible kinpaku path so clicks register on thin lines. function redrawStrokes() { while (annotSvgEl.firstChild) annotSvgEl.removeChild(annotSvgEl.firstChild); annotState.strokes.forEach((s, idx) => { @@ -462,6 +485,13 @@ function onAnnotDown(e) { if (!annotActive) return; + // 0) Insert placeholder edge resize — wins over draw / pins. + const resizeEdge = e.target.closest?.('[data-impeccable-placeholder-resize]')?.dataset.impeccablePlaceholderResize; + if (resizeEdge && configureKind === 'insert' && placeholderElement) { + startPlaceholderEdgeResize(resizeEdge, e); + return; + } + // 1) Clear chip → wipe all annotations if (e.target.closest?.('[data-annot-clear]')) { if (annotEditing) annotEditing = null; @@ -531,7 +561,23 @@ } function onAnnotMove(e) { - if (!annotActive || !annotPointer) return; + if (!annotActive) return; + + if (placeholderResizeDrag) { + const d = placeholderResizeDrag; + const next = resizePlaceholderFromEdge( + d.start, + d.edge, + e.clientX - d.startX, + e.clientY - d.startY, + d.parentWidth, + ); + applyPlaceholderDimensions(next); + e.stopPropagation(); + return; + } + + if (!annotPointer) return; const p = localCoords(e); if (annotPointer.kind === 'pin') { @@ -571,7 +617,22 @@ e.stopPropagation(); } + function pointsToPath(points) { + if (!points || points.length === 0) return ''; + let d = 'M' + points[0][0].toFixed(1) + ' ' + points[0][1].toFixed(1); + for (let i = 1; i < points.length; i++) { + d += ' L' + points[i][0].toFixed(1) + ' ' + points[i][1].toFixed(1); + } + return d; + } + function onAnnotUp(e) { + if (placeholderResizeDrag) { + try { annotOverlayEl.releasePointerCapture(e.pointerId); } catch {} + placeholderResizeDrag = null; + e.stopPropagation(); + return; + } if (!annotActive || !annotPointer) return; if (annotPointer.kind === 'pin') { @@ -604,18 +665,10 @@ } try { annotOverlayEl.releasePointerCapture(e.pointerId); } catch {} annotPointer = null; + if (configureKind === 'insert') syncInsertCreateButton(); e.stopPropagation(); } - function pointsToPath(points) { - if (!points || points.length === 0) return ''; - let d = 'M' + points[0][0].toFixed(1) + ' ' + points[0][1].toFixed(1); - for (let i = 1; i < points.length; i++) { - d += ' L' + points[i][0].toFixed(1) + ' ' + points[i][1].toFixed(1); - } - return d; - } - function renderAllPins() { annotPinsEl.innerHTML = ''; annotState.comments.forEach((c, idx) => { @@ -845,10 +898,9 @@ transform: 'translateY(6px)', transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE, background: BP.surface, - backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)', - border: '1px solid ' + BP.hairline, + border: '1.5px solid ' + BP.border, borderRadius: '10px', - boxShadow: BAR_SHADOW_DEFAULT, + boxShadow: BP.shadow, transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE, fontFamily: FONT, fontSize: '13px', color: BP.text, padding: '6px', @@ -859,8 +911,10 @@ } function positionBar() { - if (!barEl || !selectedElement) return; - const r = selectedElement.getBoundingClientRect(); + if (!barEl) return; + const anchor = resolveBarAnchor(); + if (!anchor) return; + const r = anchor.getBoundingClientRect(); const barH = barEl.offsetHeight || 44; const barW = barEl.offsetWidth || 380; const GLOBAL_BAR_RESERVE = 64; // global bar height + bottom margin + breathing room @@ -888,19 +942,24 @@ function showBar(mode) { barEl.innerHTML = ''; - if (mode === 'configure') barEl.appendChild(buildConfigureRow()); - else if (mode === 'generating') barEl.appendChild(buildGeneratingRow()); + if (mode === 'configure') { + barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow()); + if (configureKind === 'insert') syncInsertCreateButton(); + } else if (mode === 'generating') barEl.appendChild(buildGeneratingRow()); else if (mode === 'cycling') barEl.appendChild(buildCyclingRow()); barEl.style.display = 'block'; positionBar(); requestAnimationFrame(() => { barEl.style.opacity = '1'; barEl.style.transform = 'translateY(0)'; + syncPageChatFocus('show-bar'); }); } function hideBar() { if (!barEl) return; + stopVoice({ suppressSubmit: true }); + if (configureKind === 'insert') clearInsertPicking(); barEl.style.opacity = '0'; barEl.style.transform = 'translateY(6px)'; setTimeout(() => { if (barEl) barEl.style.display = 'none'; }, 250); @@ -911,11 +970,14 @@ function updateBarContent(mode) { if (!barEl || barEl.style.display === 'none') return; barEl.innerHTML = ''; - // Reset bar styling to the theme-aware palette + // Reset bar styling to the kinpaku picker palette barEl.style.background = BP.surface; - barEl.style.border = '1px solid ' + BP.hairline; - if (mode === 'configure') barEl.appendChild(buildConfigureRow()); - else if (mode === 'generating') barEl.appendChild(buildGeneratingRow()); + barEl.style.border = '1.5px solid ' + BP.border; + barEl.style.boxShadow = BP.shadow; + if (mode === 'configure') { + barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow()); + if (configureKind === 'insert') syncInsertCreateButton(); + } else if (mode === 'generating') barEl.appendChild(buildGeneratingRow()); else if (mode === 'cycling') barEl.appendChild(buildCyclingRow()); else if (mode === 'saving') barEl.appendChild(buildSavingRow()); else if (mode === 'confirmed') { @@ -923,76 +985,770 @@ barEl.style.background = 'oklch(95% 0.05 145)'; barEl.style.border = '1px solid oklch(75% 0.12 145 / 0.4)'; } + syncPageChatFocus('update-bar-content'); } // --- Configure row --- + function syncConfigureInputChrome() { + const wrap = document.getElementById(PREFIX + '-configure-input-wrap'); + const input = document.getElementById(PREFIX + '-input'); + if (!wrap || !input) return; + const focused = document.activeElement === input; + wrap.dataset.inputFocused = focused ? 'true' : 'false'; + wrap.dataset.voiceListening = (voiceListening && voiceCtx?.mode === 'configure') ? 'true' : 'false'; + wrap.style.borderColor = (voiceListening && voiceCtx?.mode === 'configure') + ? BP.patinaSoft + : (focused ? BP.accentSoft : BP.hairline); + } + + // --- Insert mode helpers (mirrors skill/scripts/live-insert-ui.mjs) --- + + function detectInsertAxisFromStyle(style) { + const display = style?.display || 'block'; + if (display.includes('flex')) { + const dir = style.flexDirection || 'row'; + return dir.startsWith('row') ? 'row' : 'column'; + } + if (display === 'grid' || display === 'inline-grid') { + const flow = style.gridAutoFlow || 'row'; + if (flow.includes('column')) return 'column'; + const cols = (style.gridTemplateColumns || '').trim(); + if (cols && cols !== 'none') { + const colCount = cols.split(/\s+/).filter(Boolean).length; + if (colCount > 1) return 'row'; + } + return 'row'; + } + return 'column'; + } + + function detectInsertAxis(parent) { + if (!parent || parent.nodeType !== 1) return 'column'; + const st = getComputedStyle(parent); + return detectInsertAxisFromStyle({ + display: st.display, + flexDirection: st.flexDirection, + gridTemplateColumns: st.gridTemplateColumns, + gridAutoFlow: st.gridAutoFlow, + }); + } + + function layoutFlowChildren(parent) { + if (!parent) return []; + return [...parent.children] + .filter(pickable) + .map((el) => ({ el, rect: el.getBoundingClientRect() })); + } + + function computeInsertPosition(clientX, clientY, rect, axis) { + axis = axis || 'column'; + if (!rect) return 'after'; + if (axis === 'row') { + if (!Number.isFinite(rect.width) || rect.width <= 0) return 'after'; + return clientX < rect.left + rect.width / 2 ? 'before' : 'after'; + } + if (!Number.isFinite(rect.height) || rect.height <= 0) return 'after'; + return clientY < rect.top + rect.height / 2 ? 'before' : 'after'; + } + + function groupSiblingRows(siblings, rowThreshold) { + rowThreshold = rowThreshold ?? 8; + const sorted = [...siblings].sort((a, b) => a.rect.top - b.rect.top || a.rect.left - b.rect.left); + const rows = []; + for (const entry of sorted) { + let placed = false; + for (const row of rows) { + if (Math.abs(entry.rect.top - row[0].rect.top) <= rowThreshold) { + row.push(entry); + placed = true; + break; + } + } + if (!placed) rows.push([entry]); + } + return rows; + } + + function horizontalOverlap(a, b) { + const left = Math.max(a.left, b.left); + const right = Math.min(a.right, b.right); + return Math.max(0, right - left); + } + + function hitSiblingInsertGap(clientX, clientY, siblings, opts) { + opts = opts || {}; + if (!siblings || siblings.length < 2) return null; + const slop = opts.slop ?? 12; + const minOverlap = opts.minOverlap ?? 0.25; + + for (const row of groupSiblingRows(siblings)) { + if (row.length < 2) continue; + const sorted = [...row].sort((a, b) => a.rect.left - b.rect.left); + for (let i = 0; i < sorted.length - 1; i++) { + const a = sorted[i]; + const b = sorted[i + 1]; + const aRight = a.rect.right; + const bLeft = b.rect.left; + if (bLeft <= aRight) continue; + const top = Math.max(a.rect.top, b.rect.top); + const bottom = Math.min(a.rect.bottom, b.rect.bottom); + const span = bottom - top; + const minH = Math.min(a.rect.height, b.rect.height); + if (span < minH * minOverlap) continue; + const inX = clientX >= aRight - slop && clientX <= bLeft + slop; + const inY = clientY >= top - slop && clientY <= bottom + slop; + if (!inX || !inY) continue; + return { + anchor: b.el, + position: 'before', + axis: 'row', + line: { axis: 'row', left: (aRight + bLeft) / 2, top, width: 0, height: span }, + }; + } + } + + const sortedCol = [...siblings].sort((a, b) => a.rect.top - b.rect.top || a.rect.left - b.rect.left); + for (let i = 0; i < sortedCol.length - 1; i++) { + const a = sortedCol[i]; + const b = sortedCol[i + 1]; + const overlap = horizontalOverlap(a.rect, b.rect); + const minW = Math.min(a.rect.width, b.rect.width); + if (overlap < minW * minOverlap) continue; + const gapTop = a.rect.bottom; + const gapBottom = b.rect.top; + if (gapBottom <= gapTop) continue; + const overlapLeft = Math.max(a.rect.left, b.rect.left); + const overlapRight = Math.min(a.rect.right, b.rect.right); + const inY = clientY >= gapTop - slop && clientY <= gapBottom + slop; + const inX = clientX >= overlapLeft - slop && clientX <= overlapRight + slop; + if (!inY || !inX) continue; + return { + anchor: b.el, + position: 'before', + axis: 'column', + line: { axis: 'column', top: (gapTop + gapBottom) / 2, left: overlapLeft, width: overlap, height: 0 }, + }; + } + return null; + } + + function insertLineCoords(rect, position, axis) { + axis = axis || 'column'; + if (axis === 'row') { + const x = position === 'before' ? rect.left - 2 : rect.right + 2; + return { axis: 'row', top: rect.top, left: x, width: 0, height: rect.height }; + } + const y = position === 'before' ? rect.top - 2 : rect.bottom + 2; + return { axis: 'column', top: y, left: rect.left, width: rect.width, height: 0 }; + } + + function resolveInsertHover({ clientX, clientY, target, rect, axis, siblings }) { + const gap = hitSiblingInsertGap(clientX, clientY, siblings); + if (gap) return gap; + const position = computeInsertPosition(clientX, clientY, rect, axis); + const line = insertLineCoords(rect, position, axis); + return { anchor: target, position, axis, line }; + } + + function cursorForInsertAxis(axis) { + return axis === 'row' ? 'ew-resize' : 'ns-resize'; + } + + function placeholderSizing({ axis, parentDisplay, parentWidth, anchorFlex }) { + const display = parentDisplay || 'block'; + const w = Number.isFinite(parentWidth) ? parentWidth : 0; + if (axis === 'row') { + if (display.includes('flex')) { + const flex = anchorFlex && anchorFlex !== 'none' && anchorFlex !== '0 1 auto' + ? anchorFlex + : '1 1 0'; + return { kind: 'flex', flex, minWidth: 0 }; + } + if (display === 'grid' || display === 'inline-grid') return { kind: 'auto' }; + } + if (w >= PLACEHOLDER_MIN_WIDTH) return { kind: 'percent' }; + return { + kind: 'explicit', + width: Math.max(PLACEHOLDER_MIN_WIDTH, w || PLACEHOLDER_MIN_WIDTH), + }; + } + + function placeholderWidthIsImplicit(kind) { + return kind === 'flex' || kind === 'percent' || kind === 'auto'; + } + + function applyPlaceholderSizingStyles(placeholder, sizing) { + placeholder.dataset.impeccablePlaceholderWidth = sizing.kind; + placeholder.style.flex = ''; + placeholder.style.minWidth = ''; + placeholder.style.maxWidth = ''; + placeholder.style.width = ''; + if (sizing.kind === 'flex') { + placeholder.style.flex = sizing.flex; + placeholder.style.minWidth = sizing.minWidth + 'px'; + } else if (sizing.kind === 'percent') { + placeholder.style.width = '100%'; + placeholder.style.maxWidth = '100%'; + } else if (sizing.kind === 'explicit') { + placeholder.style.width = sizing.width + 'px'; + } + } + + function materializePlaceholderWidth(placeholder) { + if (!placeholder) return; + const kind = placeholder.dataset.impeccablePlaceholderWidth; + if (!placeholderWidthIsImplicit(kind)) return; + const w = Math.max(PLACEHOLDER_MIN_WIDTH, Math.round(placeholder.offsetWidth)); + placeholder.style.flex = ''; + placeholder.style.minWidth = ''; + placeholder.style.maxWidth = ''; + placeholder.style.width = w + 'px'; + placeholder.dataset.impeccablePlaceholderWidth = 'explicit'; + } + + function canCreateInsert({ prompt, comments, strokes }) { + const hasPrompt = typeof prompt === 'string' && prompt.trim().length > 0; + const hasComments = Array.isArray(comments) && comments.length > 0; + const hasStrokes = Array.isArray(strokes) && strokes.some( + (s) => Array.isArray(s?.points) && s.points.length >= 2, + ); + return hasPrompt || hasComments || hasStrokes; + } + + function insertCreateDisabledReason({ prompt, comments, strokes }) { + if (canCreateInsert({ prompt, comments, strokes })) return null; + return 'Add a prompt or annotate the placeholder to create'; + } + + function clampPlaceholderSize(width, height, parentWidth) { + const maxW = Math.max(PLACEHOLDER_MIN_WIDTH, parentWidth || PLACEHOLDER_MIN_WIDTH); + return { + width: Math.min(maxW, Math.max(PLACEHOLDER_MIN_WIDTH, Math.round(width))), + height: Math.max(PLACEHOLDER_MIN_HEIGHT, Math.round(height)), + }; + } + + function cursorForPlaceholderEdge(edge) { + if (edge === 'n' || edge === 's') return 'ns-resize'; + if (edge === 'e' || edge === 'w') return 'ew-resize'; + return 'default'; + } + + function resizePlaceholderFromEdge(start, edge, dx, dy, parentWidth) { + const base = { + width: start.width, + height: start.height, + marginLeft: start.marginLeft ?? 0, + marginTop: start.marginTop ?? 0, + }; + if (edge === 'e') base.width = start.width + dx; + else if (edge === 'w') { + base.width = start.width - dx; + base.marginLeft = start.marginLeft + dx; + } else if (edge === 's') base.height = start.height + dy; + else if (edge === 'n') { + base.height = start.height - dy; + base.marginTop = start.marginTop + dy; + } + const clamped = clampPlaceholderSize(base.width, base.height, parentWidth); + if (edge === 'w') base.marginLeft = start.marginLeft + start.width - clamped.width; + else if (edge === 'n') base.marginTop = start.marginTop + start.height - clamped.height; + return { + width: clamped.width, + height: clamped.height, + marginLeft: Math.round(base.marginLeft), + marginTop: Math.round(base.marginTop), + }; + } + + function ensureInsertLine() { + if (insertLineEl) return insertLineEl; + insertLineEl = document.createElement('div'); + insertLineEl.id = PREFIX + '-insert-line'; + Object.assign(insertLineEl.style, { + position: 'fixed', + zIndex: String(Z.highlight), + height: '0', + borderTop: '2px dotted ' + C.brand, + pointerEvents: 'none', + display: 'none', + opacity: '0.9', + }); + document.body.appendChild(insertLineEl); + defangOutsideHandlers(insertLineEl); + return insertLineEl; + } + + function showInsertLine(resolved) { + if (!resolved?.anchor || !resolved.line) return; + const line = ensureInsertLine(); + const coords = resolved.line; + if (coords.axis === 'row') { + Object.assign(line.style, { + display: 'block', + top: coords.top + 'px', + left: coords.left + 'px', + width: '0', + height: coords.height + 'px', + borderTop: 'none', + borderLeft: '2px dotted ' + C.brand, + }); + } else { + Object.assign(line.style, { + display: 'block', + top: coords.top + 'px', + left: coords.left + 'px', + width: coords.width + 'px', + height: '0', + borderLeft: 'none', + borderTop: '2px dotted ' + C.brand, + }); + } + insertHoverAnchor = resolved.anchor; + insertHoverPosition = resolved.position; + insertHoverAxis = resolved.axis || 'column'; + } + + function hideInsertLine() { + if (!insertLineEl) return; + insertLineEl.style.display = 'none'; + insertHoverAnchor = null; + insertHoverPosition = null; + insertHoverAxis = null; + syncPageInteractionCursor(); + } + + let pageInteractionCursorActive = false; + + /** Page-level cursor while insert mode is choosing a before/after edge. */ + function syncPageInteractionCursor() { + let next = ''; + if (state === 'PICKING' && insertActive) { + next = insertHoverAnchor ? cursorForInsertAxis(insertHoverAxis || 'column') : ''; + } + if (next) { + document.documentElement.style.cursor = next; + pageInteractionCursorActive = true; + } else if (pageInteractionCursorActive) { + document.documentElement.style.cursor = ''; + pageInteractionCursorActive = false; + } + } + + /** Element used to position the floating bar / shader during a session. */ + function resolveBarAnchor() { + if (currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) { + const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]'); + if (wrapper) { + const variantCount = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])').length; + if (variantCount > 0 && visibleVariant > 0) { + const visEl = pickVariantContent(wrapper, visibleVariant); + if (visEl) return visEl; + } + if (state === 'GENERATING') { + const ph = ensureInsertPlaceholder(); + if (ph) return ph; + if (insertAnchorElement && document.body.contains(insertAnchorElement)) return insertAnchorElement; + } + } + } + if (selectedElement && document.body.contains(selectedElement)) return selectedElement; + if (placeholderElement && document.body.contains(placeholderElement)) return placeholderElement; + if (insertAnchorElement && document.body.contains(insertAnchorElement)) return insertAnchorElement; + return null; + } + + function removeInsertPlaceholderDom() { + if (placeholderElement) { + placeholderElement.remove(); + placeholderElement = null; + } + placeholderResizeDrag = null; + syncPlaceholderResizeHandles(); + } + + function finalizeInsertSession() { + removeInsertPlaceholderDom(); + insertAnchorElement = null; + insertAnchorPosition = null; + insertAnchorLayoutAxis = null; + insertPlaceholderSnapshot = null; + if (configureKind === 'insert') configureKind = 'replace'; + } + + function buildInsertPlaceholderSnapshotFromDom(anchor, placeholder) { + return { + width: Math.round(placeholder.offsetWidth || 0), + height: Math.round(placeholder.offsetHeight || PLACEHOLDER_DEFAULT_HEIGHT), + marginLeft: parseFloat(placeholder.style.marginLeft) || 0, + marginTop: parseFloat(placeholder.style.marginTop) || 0, + position: insertAnchorPosition || 'before', + layoutAxis: insertAnchorLayoutAxis || 'column', + anchorTag: anchor.tagName || 'DIV', + anchorClasses: anchor.className || '', + anchorText: (anchor.textContent || '').trim().slice(0, 120), + }; + } + + function findInsertAnchorInDom() { + if (insertAnchorElement && document.body.contains(insertAnchorElement)) return insertAnchorElement; + const snap = insertPlaceholderSnapshot; + if (!snap) return null; + const tag = (snap.anchorTag || 'div').toLowerCase(); + const cls = (snap.anchorClasses || '').split(/\s+/).filter(Boolean)[0]; + const needle = snap.anchorText || ''; + const sel = cls ? tag + '.' + cls : tag; + const candidates = document.querySelectorAll(sel); + for (const candidate of candidates) { + if (own(candidate)) continue; + if (needle && !(candidate.textContent || '').includes(needle.slice(0, 40))) continue; + return candidate; + } + return null; + } + + function isInsertGeneratingSession() { + if (state !== 'GENERATING' || !currentSessionId) return false; + const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]'); + return !!wrapper && wrapper.dataset.impeccableMode === 'insert'; + } + + /** Recreate the dotted placeholder if Astro/Vite HMR removed it mid-generation. */ + function ensureInsertPlaceholder() { + if (!isInsertGeneratingSession()) return placeholderElement; + const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]'); + const variantCount = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])').length; + if (variantCount > 0) return placeholderElement; + if (placeholderElement && document.body.contains(placeholderElement)) return placeholderElement; + + const anchor = findInsertAnchorInDom(); + if (!anchor) return null; + + insertAnchorElement = anchor; + const position = insertPlaceholderSnapshot?.position || insertAnchorPosition || 'before'; + const axis = insertPlaceholderSnapshot?.layoutAxis || insertAnchorLayoutAxis; + const ph = createInsertPlaceholder(anchor, position, axis); + if (!ph) return null; + + if (insertPlaceholderSnapshot) { + applyPlaceholderDimensions({ + width: insertPlaceholderSnapshot.width, + height: insertPlaceholderSnapshot.height, + marginLeft: insertPlaceholderSnapshot.marginLeft, + marginTop: insertPlaceholderSnapshot.marginTop, + }); + } + selectedElement = ph; + return ph; + } + + function applyPlaceholderDimensions({ width, height, marginLeft, marginTop }) { + const ph = placeholderElement; + if (!ph) return; + materializePlaceholderWidth(ph); + ph.style.width = width + 'px'; + ph.style.height = height + 'px'; + ph.style.marginLeft = marginLeft ? marginLeft + 'px' : ''; + ph.style.marginTop = marginTop ? marginTop + 'px' : ''; + positionAnnotOverlay(ph); + positionBar(); + } + + function buildPlaceholderResizeHandles() { + if (!placeholderResizeLayerEl) return; + placeholderResizeLayerEl.innerHTML = ''; + const hit = 10; + const half = hit / 2; + const specs = [ + { edge: 'n', top: -half, left: 0, right: 0, height: hit }, + { edge: 's', bottom: -half, left: 0, right: 0, height: hit }, + { edge: 'e', top: 0, bottom: 0, right: -half, width: hit }, + { edge: 'w', top: 0, bottom: 0, left: -half, width: hit }, + ]; + for (const spec of specs) { + const handle = el('div', { + position: 'absolute', + pointerEvents: 'auto', + cursor: cursorForPlaceholderEdge(spec.edge), + }); + if (spec.top != null) handle.style.top = spec.top + 'px'; + if (spec.bottom != null) handle.style.bottom = spec.bottom + 'px'; + if (spec.left != null) handle.style.left = spec.left + 'px'; + if (spec.right != null) handle.style.right = spec.right + 'px'; + if (spec.width != null) handle.style.width = spec.width + 'px'; + if (spec.height != null) handle.style.height = spec.height + 'px'; + handle.dataset.impeccablePlaceholderResize = spec.edge; + handle.setAttribute('aria-label', 'Resize placeholder'); + handle.title = 'Drag to resize'; + placeholderResizeLayerEl.appendChild(handle); + } + } + + function syncPlaceholderResizeHandles() { + if (!placeholderResizeLayerEl) return; + const show = configureKind === 'insert' && annotActive && !!placeholderElement && state === 'CONFIGURING'; + placeholderResizeLayerEl.style.display = show ? 'block' : 'none'; + if (!show) { + placeholderResizeLayerEl.innerHTML = ''; + return; + } + if (!placeholderResizeLayerEl.childElementCount) buildPlaceholderResizeHandles(); + } + + function startPlaceholderEdgeResize(edge, e) { + const ph = placeholderElement; + if (!ph || configureKind !== 'insert') return; + materializePlaceholderWidth(ph); + placeholderResizeDrag = { + edge, + startX: e.clientX, + startY: e.clientY, + start: { + width: ph.offsetWidth, + height: ph.offsetHeight, + marginLeft: parseFloat(ph.style.marginLeft) || 0, + marginTop: parseFloat(ph.style.marginTop) || 0, + }, + parentWidth: ph.parentNode?.getBoundingClientRect().width || PLACEHOLDER_MIN_WIDTH, + pointerId: e.pointerId, + }; + try { annotOverlayEl.setPointerCapture(e.pointerId); } catch {} + e.stopPropagation(); + e.preventDefault(); + } + + function createInsertPlaceholder(anchor, position, layoutAxis) { + removeInsertPlaceholderDom(); + const parent = anchor.parentNode; + if (!parent) return null; + const axis = layoutAxis || detectInsertAxis(parent); + const pst = getComputedStyle(parent); + const ast = getComputedStyle(anchor); + const sizing = placeholderSizing({ + axis, + parentDisplay: pst.display, + parentWidth: parent.getBoundingClientRect().width, + anchorFlex: ast.flex, + }); + const placeholder = document.createElement('div'); + placeholder.id = PREFIX + '-insert-placeholder'; + placeholder.setAttribute('data-impeccable-insert-placeholder', 'true'); + placeholder.setAttribute('aria-hidden', 'true'); + Object.assign(placeholder.style, { + boxSizing: 'border-box', + height: PLACEHOLDER_DEFAULT_HEIGHT + 'px', + minHeight: PLACEHOLDER_MIN_HEIGHT + 'px', + border: '2px dotted ' + BP.accent, + borderRadius: '0', + background: 'transparent', + opacity: '1', + position: 'relative', + marginLeft: '', + marginTop: '', + }); + applyPlaceholderSizingStyles(placeholder, sizing); + if (position === 'before') parent.insertBefore(placeholder, anchor); + else parent.insertBefore(placeholder, anchor.nextSibling); + placeholderElement = placeholder; + insertAnchorElement = anchor; + insertAnchorPosition = position; + insertAnchorLayoutAxis = axis; + return placeholder; + } + + function clearInsertPicking() { + hideInsertLine(); + finalizeInsertSession(); + } + + function isInsertCreateEnabled(btn) { + btn = btn || document.getElementById(PREFIX + '-insert-create'); + return !!btn && btn.getAttribute('aria-disabled') !== 'true'; + } + + let insertCreateTooltipEl = null; + + function ensureInsertCreateTooltip() { + if (insertCreateTooltipEl) return insertCreateTooltipEl; + insertCreateTooltipEl = el('div', { + position: 'fixed', + display: 'none', + zIndex: String(Z.bar + 7), + pointerEvents: 'none', + maxWidth: '240px', + padding: '6px 9px', + borderRadius: '7px', + background: BP.chatSurface, + border: '1px solid ' + BP.hairline, + boxShadow: BP.shadow, + color: BP.text, + fontFamily: FONT, + fontSize: '11px', + fontWeight: '500', + lineHeight: '1.35', + }); + insertCreateTooltipEl.id = PREFIX + '-insert-create-tooltip'; + document.body.appendChild(insertCreateTooltipEl); + return insertCreateTooltipEl; + } + + function showInsertCreateTooltip(anchor, message) { + if (!anchor || !message) return; + const tip = ensureInsertCreateTooltip(); + tip.textContent = message; + tip.style.display = 'block'; + const r = anchor.getBoundingClientRect(); + const tipW = tip.offsetWidth; + const tipH = tip.offsetHeight; + const left = Math.max(8, Math.min(window.innerWidth - tipW - 8, r.left + r.width / 2 - tipW / 2)); + const top = Math.max(8, r.top - tipH - 8); + tip.style.left = left + 'px'; + tip.style.top = top + 'px'; + } + + function hideInsertCreateTooltip() { + if (!insertCreateTooltipEl) return; + insertCreateTooltipEl.style.display = 'none'; + } + + function insertCreateGateState(input) { + return { + prompt: input?.value ?? '', + comments: annotState.comments, + strokes: annotState.strokes, + }; + } + + function syncInsertCreateButton(btn, input) { + btn = btn || document.getElementById(PREFIX + '-insert-create'); + input = input || document.getElementById(PREFIX + '-insert-input'); + if (!btn || !input) return; + const gate = insertCreateGateState(input); + const ok = canCreateInsert(gate); + const reason = ok ? 'Create variants' : insertCreateDisabledReason(gate); + btn.setAttribute('aria-disabled', ok ? 'false' : 'true'); + btn.setAttribute('aria-label', reason); + if (ok) { + hideInsertCreateTooltip(); + btn.style.background = BP.accent; + btn.style.color = C.ink; + btn.style.border = 'none'; + btn.style.opacity = '1'; + btn.style.cursor = 'pointer'; + } else { + btn.style.background = 'transparent'; + btn.style.color = BP.textDim; + btn.style.border = '1px solid ' + BP.hairline; + btn.style.opacity = '0.72'; + btn.style.cursor = 'not-allowed'; + } + } + function buildConfigureRow() { const row = el('div', { - display: 'flex', alignItems: 'center', gap: '4px', + display: 'flex', alignItems: 'center', gap: '6px', }); - // Action pill + // Action pill — dark graphite chip (matches kinpaku-kit .live-demo-ctx-pill) const pill = el('button', { display: 'inline-flex', alignItems: 'center', gap: '4px', padding: '5px 10px', borderRadius: '6px', - background: BP.mark, color: BP.markText, + background: BP.chatSurface, color: BP.text, fontFamily: FONT, fontSize: '12px', fontWeight: '500', - border: 'none', cursor: 'pointer', - transition: 'background 0.12s ease, transform 0.1s ease', + border: '1px solid ' + BP.hairline, cursor: 'pointer', + transition: 'background 0.12s ease, border-color 0.12s ease, transform 0.1s ease', whiteSpace: 'nowrap', flexShrink: '0', }); pill.textContent = actionLabel() + ' \u25BE'; - pill.addEventListener('mouseenter', () => pill.style.background = BP.accent); - pill.addEventListener('mouseleave', () => pill.style.background = BP.mark); + pill.addEventListener('mouseenter', () => { + pill.style.background = BP.accentSoft; + pill.style.borderColor = BP.accent; + }); + pill.addEventListener('mouseleave', () => { + pill.style.background = BP.chatSurface; + pill.style.borderColor = BP.hairline; + }); pill.addEventListener('mousedown', () => pill.style.transform = 'scale(0.97)'); pill.addEventListener('mouseup', () => pill.style.transform = 'scale(1)'); pill.addEventListener('click', (e) => { e.stopPropagation(); toggleActionPicker(); }); row.appendChild(pill); - // Freeform input. Focus state shows an accent-colored border only — - // an earlier version tinted the background with `BP.accentSoft`, which - // composited against the dark bar surface to a murky purple where the - // browser's default placeholder gray was unreadable. Placeholder color - // is set explicitly via a one-shot stylesheet keyed off this input's id - // so it picks up the bar's `textDim` token in both themes. + // Prompt field — same chat-surface chrome as the bottom Steer bar + const inputWrap = el('div', { + display: 'inline-flex', alignItems: 'center', + flex: '1', minWidth: '0', height: '28px', + borderRadius: '7px', + background: BP.chatSurface, + border: '1px solid ' + BP.hairline, + overflow: 'hidden', + transition: 'border-color 0.15s ease', + }); + inputWrap.id = PREFIX + '-configure-input-wrap'; + const input = document.createElement('input'); input.id = PREFIX + '-input'; input.type = 'text'; - input.placeholder = selectedAction === 'impeccable' ? 'describe what you want...' : 'refine further (optional)...'; + input.placeholder = selectedAction === 'impeccable' ? 'describe what you want…' : 'refine further (optional)…'; + input.setAttribute('aria-label', 'Describe the change'); Object.assign(input.style, { - flex: '1', minWidth: '0', - padding: '5px 8px', borderRadius: '6px', - border: '1px solid transparent', background: 'transparent', - fontFamily: FONT, fontSize: '12px', color: BP.text, + flex: '1', minWidth: '0', width: '100%', + padding: '0 6px', border: 'none', background: 'transparent', + fontFamily: FONT, fontSize: '11.5px', color: BP.text, outline: 'none', - transition: 'border-color 0.15s ease', }); - if (!document.getElementById(PREFIX + '-input-style')) { + + const voiceBtn = el('button', { + display: 'inline-flex', alignItems: 'center', justifyContent: 'center', + padding: '0', boxSizing: 'border-box', + width: '28px', height: '28px', flexShrink: '0', + border: 'none', background: 'transparent', + color: BP.textDim, cursor: 'pointer', + transition: 'color 0.12s ease, background 0.12s ease', + }); + voiceBtn.id = PREFIX + '-configure-voice'; + voiceBtn.type = 'button'; + voiceBtn.setAttribute('aria-label', 'Voice input'); + voiceBtn.innerHTML = ICON_PAGE_VOICE; + + if (!document.getElementById(PREFIX + '-configure-input-style')) { const s = document.createElement('style'); - s.id = PREFIX + '-input-style'; + s.id = PREFIX + '-configure-input-style'; s.textContent = - '#' + PREFIX + '-input::placeholder { color: ' + BP.textDim + '; opacity: 1; }'; + '@keyframes impeccable-configure-voice-pulse { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }' + + '#' + PREFIX + '-input::placeholder { color: ' + BP.textDim + '; opacity: 1; }' + + '#' + PREFIX + '-configure-voice[data-listening="true"] svg { animation: impeccable-configure-voice-pulse 1.1s ease-in-out infinite; }' + + '@media (prefers-reduced-motion: reduce) { #' + PREFIX + '-configure-voice[data-listening="true"] svg { animation: none; opacity: 1; } }' + + '#' + PREFIX + '-configure-voice:hover { background: oklch(78% 0.12 82 / 0.12); }'; document.head.appendChild(s); } - input.addEventListener('focus', () => { - input.style.borderColor = BP.accent; - }); - input.addEventListener('blur', () => { - input.style.borderColor = 'transparent'; - }); + + input.addEventListener('focus', () => syncConfigureInputChrome()); + input.addEventListener('blur', () => syncConfigureInputChrome()); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.stopPropagation(); e.preventDefault(); handleGo(); return; } - if (e.key === 'Escape') { e.stopPropagation(); e.preventDefault(); input.blur(); hideBar(); state = 'PICKING'; return; } + if (e.key === 'Escape') { e.stopPropagation(); e.preventDefault(); input.blur(); hideBar(); state = 'PICKING'; syncPageChatFocus('configure-input-escape'); return; } // Let arrow keys pass through to the element picker when the input is empty if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && !input.value) return; e.stopPropagation(); }); - row.appendChild(input); + + voiceBtn.addEventListener('mousedown', (e) => e.stopPropagation()); + voiceBtn.addEventListener('click', (e) => { + e.stopPropagation(); + toggleConfigureVoice(); + }); + + inputWrap.appendChild(input); + inputWrap.appendChild(voiceBtn); + row.appendChild(inputWrap); + syncConfigureInputChrome(); // Variant count toggle const count = el('button', { - padding: '4px 6px', borderRadius: '5px', + display: 'inline-flex', alignItems: 'center', justifyContent: 'center', + boxSizing: 'border-box', height: '28px', padding: '0 6px', + borderRadius: '5px', border: '1px solid ' + BP.hairline, background: 'transparent', fontFamily: MONO, fontSize: '11px', fontWeight: '600', color: BP.textDim, cursor: 'pointer', @@ -1012,8 +1768,10 @@ // Go button const go = el('button', { - padding: '5px 12px', borderRadius: '6px', - border: 'none', background: BP.accent, color: BP.mark, + display: 'inline-flex', alignItems: 'center', justifyContent: 'center', + boxSizing: 'border-box', height: '28px', padding: '0 12px', + borderRadius: '6px', + border: 'none', background: BP.accent, color: C.ink, fontFamily: FONT, fontSize: '12px', fontWeight: '600', cursor: 'pointer', transition: 'filter 0.12s ease, transform 0.1s ease', @@ -1027,8 +1785,114 @@ go.addEventListener('click', (e) => { e.stopPropagation(); handleGo(); }); row.appendChild(go); - // Auto-focus input after a beat - setTimeout(() => input.focus(), 60); + return row; + } + + function buildInsertConfigureRow() { + const row = el('div', { + display: 'flex', alignItems: 'center', gap: '6px', + }); + + const inputWrap = el('div', { + display: 'inline-flex', alignItems: 'center', + flex: '1', minWidth: '0', height: '28px', + borderRadius: '7px', + background: BP.chatSurface, + border: '1px solid ' + BP.hairline, + overflow: 'hidden', + transition: 'border-color 0.15s ease', + }); + inputWrap.id = PREFIX + '-insert-input-wrap'; + + const input = document.createElement('input'); + input.id = PREFIX + '-insert-input'; + input.type = 'text'; + input.placeholder = 'describe what to insert…'; + input.setAttribute('aria-label', 'Describe the new element'); + Object.assign(input.style, { + flex: '1', minWidth: '0', width: '100%', + padding: '0 6px', border: 'none', background: 'transparent', + fontFamily: FONT, fontSize: '11.5px', color: BP.text, + outline: 'none', + }); + + const voiceBtn = el('button', { + display: 'inline-flex', alignItems: 'center', justifyContent: 'center', + padding: '0', boxSizing: 'border-box', + width: '28px', height: '28px', flexShrink: '0', + border: 'none', background: 'transparent', + color: BP.textDim, cursor: 'pointer', + }); + voiceBtn.id = PREFIX + '-insert-voice'; + voiceBtn.type = 'button'; + voiceBtn.setAttribute('aria-label', 'Voice input'); + voiceBtn.innerHTML = ICON_PAGE_VOICE; + + input.addEventListener('input', () => syncInsertCreateButton()); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.stopPropagation(); e.preventDefault(); + if (isInsertCreateEnabled()) handleInsertCreate(); + return; + } + if (e.key === 'Escape') { + e.stopPropagation(); e.preventDefault(); + cancelInsertConfigure(); + return; + } + e.stopPropagation(); + }); + voiceBtn.addEventListener('mousedown', (e) => e.stopPropagation()); + voiceBtn.addEventListener('click', (e) => { + e.stopPropagation(); + toggleConfigureVoice(); + }); + + inputWrap.appendChild(input); + inputWrap.appendChild(voiceBtn); + row.appendChild(inputWrap); + + const count = el('button', { + display: 'inline-flex', alignItems: 'center', justifyContent: 'center', + boxSizing: 'border-box', height: '28px', padding: '0 6px', + borderRadius: '5px', + border: '1px solid ' + BP.hairline, background: 'transparent', + fontFamily: MONO, fontSize: '11px', fontWeight: '600', + color: BP.textDim, cursor: 'pointer', flexShrink: '0', whiteSpace: 'nowrap', + }); + count.textContent = '\u00D7' + selectedCount; + count.addEventListener('click', (e) => { + e.stopPropagation(); + selectedCount = selectedCount >= 4 ? 2 : selectedCount + 1; + count.textContent = '\u00D7' + selectedCount; + }); + row.appendChild(count); + + const create = el('button', { + display: 'inline-flex', alignItems: 'center', justifyContent: 'center', + boxSizing: 'border-box', height: '28px', padding: '0 12px', + borderRadius: '6px', + border: 'none', background: BP.accent, color: C.ink, + fontFamily: FONT, fontSize: '12px', fontWeight: '600', + flexShrink: '0', whiteSpace: 'nowrap', + }); + create.id = PREFIX + '-insert-create'; + create.textContent = 'Create \u2192'; + create.addEventListener('mouseenter', () => { + if (isInsertCreateEnabled(create)) { + hideInsertCreateTooltip(); + return; + } + showInsertCreateTooltip(create, insertCreateDisabledReason(insertCreateGateState(input))); + }); + create.addEventListener('mouseleave', hideInsertCreateTooltip); + create.addEventListener('click', (e) => { + e.stopPropagation(); + if (!isInsertCreateEnabled(create)) return; + handleInsertCreate(); + }); + row.appendChild(create); + syncInsertCreateButton(create, input); return row; } @@ -1045,7 +1909,7 @@ fontWeight: '600', fontSize: '12px', color: BP.text, flexShrink: '0', whiteSpace: 'nowrap', }); - label.textContent = actionLabel(); + label.textContent = configureKind === 'insert' ? 'Insert' : actionLabel(); row.appendChild(label); // Dots @@ -1146,11 +2010,10 @@ // Spacer row.appendChild(el('div', { flex: '1' })); - // Accept — primary action, uses the site's saturated brand magenta - // with paper-white text, not the theme-muted BP.accent. + // Accept — primary action, kinpaku gold + lacquer-deep (matches demo .live-demo-ctx-accept) const accept = el('button', { padding: '5px 14px', borderRadius: '5px', - border: 'none', background: C.brand, color: 'oklch(98% 0 0)', + border: 'none', background: C.brand, color: C.ink, fontFamily: FONT, fontSize: '11px', fontWeight: '600', cursor: 'pointer', transition: 'filter 0.12s ease, transform 0.1s ease', whiteSpace: 'nowrap', @@ -1244,10 +2107,10 @@ for (let i = 1; i <= expectedVariants; i++) { const arrived = i <= arrivedVariants; const active = i === visibleVariant; - // active: solid site-brand magenta dot. arrived+inactive: muted neutral. + // active: solid site-brand kinpaku dot. arrived+inactive: muted neutral. // pending (not yet arrived): faint outline ring. No borders on arrived // dots — the previous "accent ring + ash fill" combo read as noisy - // magenta chips, especially when all variants had arrived and every + // kinpaku chips, especially when all variants had arrived and every // dot wore an accent ring. const dotBg = active ? C.brand : arrived ? BP.textDim @@ -1321,13 +2184,11 @@ transformOrigin: 'bottom left', transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE, background: P.surface, - border: '1px solid ' + P.hairline, + border: '1.5px solid ' + P.border, borderRadius: '10px', - boxShadow: '0 8px 30px oklch(0% 0 0 / 0.10), 0 2px 6px oklch(0% 0 0 / 0.06)', + boxShadow: P.shadow, padding: '6px', fontFamily: FONT, - backdropFilter: 'blur(10px)', - WebkitBackdropFilter: 'blur(10px)', }); // Build the chip grid @@ -1467,7 +2328,6 @@ boxSizing: 'border-box', borderRadius: '0 0 10px 10px', pointerEvents: 'none', - backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)', // clip-path is the same conceptual reveal as mask but with rock-solid // transition support across engines. Closed state clips from the far @@ -1817,13 +2677,31 @@ // Variant cycling in DOM // --------------------------------------------------------------------------- + function isVariantShown(el) { + if (!el) return false; + if (el.hidden) return false; + if (el.style?.display === 'none') return false; + return true; + } + + function setVariantShown(el, shown) { + if (!el) return; + if (shown) { + el.removeAttribute('hidden'); + el.style.display = ''; + } else { + el.setAttribute('hidden', ''); + el.style.display = 'none'; + } + } + function showVariantInDOM(sessionId, num) { const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (!wrapper) return; for (const child of wrapper.children) { const v = child.dataset ? child.dataset.impeccableVariant : null; if (!v) continue; - child.style.display = (v === String(num)) ? '' : 'none'; + setVariantShown(child, v === String(num)); } // Unconditional refresh — covers first-reveal (no-op if state isn't // CYCLING yet, the subsequent CYCLING transition triggers its own @@ -1841,45 +2719,54 @@ fetch(url) .then(r => { if (!r.ok) throw new Error(r.status); return r.text(); }) .then(html => { - // Parse the raw source HTML const parser = new DOMParser(); - const doc = parser.parseFromString(html, 'text/html'); - const srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + let srcWrapper = null; + + // Full-file parse works for HTML/JSX; Astro/Vue sources need marker extraction. + const startMark = ''; + const endMark = ''; + const startIdx = html.indexOf(startMark); + const endIdx = html.indexOf(endMark); + const block = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx + ? html.slice(startIdx + startMark.length, endIdx).trim() + : html; + const doc = parser.parseFromString(block, 'text/html'); + srcWrapper = doc.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (!srcWrapper) { console.error('[impeccable] Variant wrapper not found in source file.'); return; } - // Find the original element in the live DOM. - // The original is inside the wrapper in the source. We find the - // corresponding element in the live DOM by matching the first child's - // tag + classes from the original snapshot. - const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); - if (!origContent) return; - - const tag = origContent.tagName.toLowerCase(); - const cls = origContent.className; - let liveEl = null; - if (origContent.id) { - liveEl = document.getElementById(origContent.id); - } else if (cls) { - // Find by tag + exact class match - const candidates = document.querySelectorAll(tag + '.' + cls.split(' ')[0]); - for (const c of candidates) { - if (c.className === cls && !own(c)) { liveEl = c; break; } - } - } - - if (!liveEl) { - console.error('[impeccable] Could not find original element in live DOM.'); - return; - } - const previousVisibleVariant = currentSessionId === sessionId ? visibleVariant : 0; - - // Replace the live element with the full wrapper from source const wrapper = srcWrapper.cloneNode(true); - liveEl.parentElement.replaceChild(wrapper, liveEl); + + // Wrapper already in DOM (wrap HMR landed, variant insert did not). + const existingWrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); + if (existingWrapper) { + existingWrapper.parentElement.replaceChild(wrapper, existingWrapper); + } else { + const origContent = srcWrapper.querySelector('[data-impeccable-variant="original"] > :first-child'); + if (!origContent) return; + + const tag = origContent.tagName.toLowerCase(); + const cls = origContent.className; + let liveEl = null; + if (origContent.id) { + liveEl = document.getElementById(origContent.id); + } else if (cls) { + const candidates = document.querySelectorAll(tag + '.' + cls.split(' ')[0]); + for (const c of candidates) { + if (c.className === cls && !own(c)) { liveEl = c; break; } + } + } + + if (!liveEl) { + console.error('[impeccable] Could not find original element in live DOM.'); + return; + } + + liveEl.parentElement.replaceChild(wrapper, liveEl); + } // Update state: count variants, preserving the user's current variant // when a late HMR/source reinjection lands after they have cycled. @@ -1900,6 +2787,7 @@ hideShaderOverlay(); updateBarContent('cycling'); refreshParamsPanel(); + positionBar(); saveSession(); console.log('[impeccable] Injected ' + arrivedVariants + ' variants from source file.'); }) @@ -1916,6 +2804,7 @@ showVariantInDOM(currentSessionId, next); // calls refreshParamsPanel itself updateSelectedElement(); updateBarContent('cycling'); + positionBar(); saveSession(); queueCheckpoint('variant_changed'); } @@ -1933,7 +2822,7 @@ if (!wrapper) return 0; const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); for (const variant of variants) { - if (variant.style.display === 'none') continue; + if (!isVariantShown(variant)) continue; const idx = parseInt(variant.dataset.impeccableVariant || '0', 10); if (idx > 0) return idx; } @@ -2114,16 +3003,33 @@ const wrapper = document.querySelector('[data-impeccable-variants="' + sessionId + '"]'); if (!wrapper) return; + const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); + const count = variants.length; + // Re-anchor selectedElement if it was detached by live-wrap's HMR swap. // Without this, the shader / highlight / bar track a zero-rect phantom // and the overlay appears frozen. if (selectedElement && !document.body.contains(selectedElement)) { - selectedElement = pickVariantContent(wrapper, 'original') || wrapper; + const isInsert = wrapper.dataset.impeccableMode === 'insert'; + if (isInsert) { + const visEl = count > 0 ? pickVariantContent(wrapper, visibleVariant || 1) : null; + if (visEl) { + selectedElement = visEl; + if (count > 0) removeInsertPlaceholderDom(); + } else { + const ph = ensureInsertPlaceholder(); + if (ph) selectedElement = ph; + else if (insertAnchorElement && document.body.contains(insertAnchorElement)) { + selectedElement = insertAnchorElement; + } + } + } else { + selectedElement = pickVariantContent(wrapper, 'original') || wrapper; + } + } else if (isInsertGeneratingSession() && count === 0) { + ensureInsertPlaceholder(); } - const variants = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])'); - const count = variants.length; - // Nothing new if (count <= arrivedVariants) return; @@ -2147,8 +3053,11 @@ if (arrivedVariants >= expectedVariants && expectedVariants > 0) { state = 'CYCLING'; hideShaderOverlay(); + if (wrapper.dataset.impeccableMode === 'insert') finalizeInsertSession(); + updateSelectedElement(); updateBarContent('cycling'); refreshParamsPanel(); + positionBar(); } else if (state === 'GENERATING') { updateBarContent('generating'); } @@ -2168,11 +3077,20 @@ function startScrollTracking() { function tick() { if (state === 'CONFIGURING' || state === 'GENERATING' || state === 'CYCLING') { + if (isInsertGeneratingSession()) ensureInsertPlaceholder(); positionBar(); - showHighlight(selectedElement); + const hiTarget = resolveBarAnchor(); + if (hiTarget && !hiTarget.hasAttribute?.('data-impeccable-insert-placeholder')) { + showHighlight(hiTarget); + } else { + hideHighlight(); + } if (tuneOpen) positionParamsPanel(); } - if (annotActive) positionAnnotOverlay(selectedElement); + if (annotActive) { + const annotTarget = resolveBarAnchor(); + if (annotTarget) positionAnnotOverlay(annotTarget); + } // Shader overlay (via debug P toggle or generation) is repositioned // by its own branch below; debug no longer has a separate overlay. if (shaderState) positionShaderOverlay(); @@ -2207,11 +3125,21 @@ switch (msg.type) { case 'connected': hasProjectContext = !!msg.hasProjectContext; - if (!hasProjectContext) showToast('No PRODUCT.md found. Variants will be brand-agnostic. Run /impeccable teach to generate one.', 7000); + if (!hasProjectContext) showToast('No PRODUCT.md found. Variants will be brand-agnostic. Run /impeccable init to generate one.', 7000); console.log('[impeccable] Live mode connected.'); - if (state === 'IDLE') state = 'PICKING'; + syncAgentPollingUi(!!msg.agentPolling); + startAgentStatusPoll(); + if (state === 'IDLE' && (pickActive || insertActive)) state = 'PICKING'; + syncPageChatFocus('sse-connected'); + break; + case 'agent_polling': + syncAgentPollingUi(!!msg.connected); + break; + case 'steer_done': + maybeCompleteSteer(msg); break; case 'done': + if (maybeCompleteSteer(msg)) break; // Variants already arrived via HMR → normal transition. if (arrivedVariants >= expectedVariants && expectedVariants > 0) { if (state === 'GENERATING') { @@ -2221,6 +3149,11 @@ } break; } + // Source fallback when HMR did not land variants in this tab. + if (msg.file && msg.id && state === 'GENERATING' && msg.id === currentSessionId) { + injectVariantsFromSource(msg.file, msg.id); + break; + } // Variants are in source but not in the DOM yet. Common when the // picked element lived inside conditional render (closed modal, // hidden tab, a route the user navigated away from). The variant @@ -2238,6 +3171,7 @@ }, 2000); break; case 'error': + if (maybeCompleteSteer(msg)) break; console.error('[impeccable] Error:', msg.message); showToast('Error: ' + msg.message, 5000); hideBar(); @@ -2335,6 +3269,34 @@ // --------------------------------------------------------------------------- function handleMouseMove(e) { + if (state === 'PICKING' && insertActive) { + const target = document.elementFromPoint(e.clientX, e.clientY); + if (!target || own(target) || !pickable(target)) { + hideInsertLine(); + return; + } + const parent = target.parentElement; + const axis = detectInsertAxis(parent); + const siblings = layoutFlowChildren(parent); + const rect = target.getBoundingClientRect(); + const resolved = resolveInsertHover({ + clientX: e.clientX, + clientY: e.clientY, + target, + rect, + axis, + siblings, + }); + if ( + resolved.anchor !== insertHoverAnchor + || resolved.position !== insertHoverPosition + || resolved.axis !== insertHoverAxis + ) { + showInsertLine(resolved); + } + syncPageInteractionCursor(); + return; + } if (state !== 'PICKING' || !pickActive) return; const target = document.elementFromPoint(e.clientX, e.clientY); if (!target || !pickable(target) || target === hoveredElement) return; @@ -2351,8 +3313,9 @@ if (tuneOpen && paramsPanelEl && !paramsPanelEl.contains(e.target) && barEl && !barEl.contains(e.target)) { closeTunePopover(); } - // In CONFIGURING: click outside the bar and selected element returns to PICKING + // In CONFIGURING: click outside the bar and selected element returns to picking if (state === 'CONFIGURING' && !own(e.target) && selectedElement && !selectedElement.contains(e.target)) { + if (configureKind === 'insert') { cancelInsertConfigure(); return; } hideBar(); stopScrollTracking(); hideAnnotOverlay(); @@ -2360,10 +3323,38 @@ state = 'PICKING'; hoveredElement = null; hideHighlight(); + syncPageChatFocus('configure-outside-click'); + return; + } + if (state === 'PICKING' && insertActive) { + if (own(e.target)) return; + if (!insertHoverAnchor || !insertHoverPosition) return; + e.preventDefault(); + e.stopPropagation(); + const placeholder = createInsertPlaceholder( + insertHoverAnchor, + insertHoverPosition, + insertHoverAxis, + ); + if (!placeholder) return; + hideInsertLine(); + configureKind = 'insert'; + selectedElement = placeholder; + state = 'CONFIGURING'; + hideHighlight(); + clearAnnotations(); + showAnnotOverlay(placeholder); + showBar('configure'); + startScrollTracking(); + syncPageInteractionCursor(); return; } if (state !== 'PICKING' || !pickActive) return; if (own(e.target)) return; + if (pagePickSkipClick || pageHasHostTextSelection()) { + pagePickSkipClick = false; + return; + } if (!hoveredElement || !pickable(hoveredElement)) return; e.preventDefault(); e.stopPropagation(); @@ -2458,13 +3449,15 @@ if (e.key === 'Escape') { e.preventDefault(); if (pickerEl?.style.display !== 'none') { hideActionPicker(); return; } - if (state === 'CONFIGURING') { hideBar(); stopScrollTracking(); hideAnnotOverlay(); clearAnnotations(); state = 'PICKING'; return; } + if (state === 'CONFIGURING') { + if (configureKind === 'insert') { cancelInsertConfigure(); return; } + hideBar(); stopScrollTracking(); hideAnnotOverlay(); clearAnnotations(); state = 'PICKING'; syncPageChatFocus('escape-from-configure'); return; + } if (state === 'CYCLING') { handleDiscard(); return; } if (state === 'SAVING' || state === 'CONFIRMED') return; // don't interrupt if (state === 'PICKING') { - // Use togglePick so the "Pick" button in the global bar also flips - // off, otherwise the bar stays lit while nothing else is active. - if (pickActive) togglePick(); + if (insertActive) toggleInsert(); + else if (pickActive) togglePick(); else { hideHighlight(); state = 'IDLE'; } return; } @@ -2524,6 +3517,7 @@ function handleGo() { if (!selectedElement || state !== 'CONFIGURING') return; + stopVoice({ suppressSubmit: true }); const input = document.getElementById(PREFIX + '-input'); const prompt = input ? input.value.trim() : ''; @@ -2573,6 +3567,74 @@ captureAndEmit(elForCapture, basePayload, snapshot, captureRect); } + function cancelInsertConfigure() { + hideBar(); + stopScrollTracking(); + hideAnnotOverlay(); + clearAnnotations(); + clearInsertPicking(); + configureKind = 'replace'; + selectedElement = null; + state = insertActive ? 'PICKING' : 'IDLE'; + hideHighlight(); + syncPageChatFocus('insert-configure-cancel'); + } + + function handleInsertCreate() { + if (!placeholderElement || !insertAnchorElement || state !== 'CONFIGURING' || configureKind !== 'insert') return; + const input = document.getElementById(PREFIX + '-insert-input'); + const prompt = input ? input.value.trim() : ''; + if (annotEditing) finalizeEditingPin(); + const snapshot = { + comments: annotState.comments.map(c => ({ x: c.x, y: c.y, text: c.text })), + strokes: annotState.strokes.map(s => ({ points: s.points.map(p => [p[0], p[1]]) })), + }; + if (!canCreateInsert({ prompt, comments: snapshot.comments, strokes: snapshot.strokes })) return; + + stopVoice({ suppressSubmit: true }); + currentSessionId = id8(); + expectedVariants = selectedCount; + arrivedVariants = 0; + visibleVariant = 0; + selectedElement = placeholderElement; + insertPlaceholderSnapshot = buildInsertPlaceholderSnapshotFromDom(insertAnchorElement, placeholderElement); + + const elForCapture = placeholderElement; + const captureRect = elForCapture.getBoundingClientRect(); + const basePayload = { + type: 'generate', + mode: 'insert', + id: currentSessionId, + count: selectedCount, + pageUrl: location.pathname, + insert: { + position: insertAnchorPosition, + anchor: extractContext(insertAnchorElement), + }, + placeholder: { + width: Math.round(captureRect.width), + height: Math.round(captureRect.height), + }, + freeformPrompt: prompt || undefined, + }; + if (snapshot.comments.length > 0) basePayload.comments = snapshot.comments; + if (snapshot.strokes.length > 0) basePayload.strokes = snapshot.strokes; + + hideAnnotOverlay(); + clearAnnotations(); + + state = 'GENERATING'; + showBar('generating'); + startScrollTracking(); + saveSession(); + sendCheckpoint('generate_started'); + writeScrollY(window.scrollY); + if (variantObserver) variantObserver.disconnect(); + variantObserver = startVariantObserver(currentSessionId); + startScrollLock(currentSessionId); + captureAndEmit(elForCapture, basePayload, snapshot, captureRect); + } + // --------------------------------------------------------------------------- // Screenshot capture + upload // --------------------------------------------------------------------------- @@ -2774,7 +3836,7 @@ // --------------------------------------------------------------------------- // Shader overlay — renders the captured screenshot as a WebGL texture and // runs an editorial "ink-wash" fragment shader over it during generation. - // A single rolling band sweeps top-to-bottom, desaturating + tinting magenta + // A single rolling band sweeps top-to-bottom, desaturating + tinting kinpaku // and leaving a soft trail. Makes the wait feel like a letterpress scan // instead of a dead spinner. // --------------------------------------------------------------------------- @@ -2820,7 +3882,7 @@ void main() { vec2 sampleCenter = (cellId + 0.5) * cellPx / u_resolution; vec3 cellImg = texture2D(u_texture, sampleCenter).rgb; float luma = dot(cellImg, vec3(0.299, 0.587, 0.114)); - // Darker cells → bigger magenta dots (classic risograph halftone curve). + // Darker cells → bigger kinpaku dots (classic risograph halftone curve). float radius = sqrt(clamp(1.0 - luma, 0.0, 1.0)) * 0.56; float dotMask = smoothstep(radius + 0.06, radius, length(cellUv)); vec3 paper = vec3(0.975, 0.965, 0.955); @@ -2832,8 +3894,8 @@ void main() { gl_FragColor = vec4(mix(base, dotLayer, band), 1.0); }`; - // Editorial Magenta converted to approximate sRGB 0-1 (matches oklch(60% 0.25 350)) - const SHADER_ACCENT = [0.82, 0.16, 0.47]; + // Kinpaku gold converted to approximate sRGB 0-1 (matches oklch(84% 0.19 80.46)) + const SHADER_ACCENT = [1.0, 0.78, 0.31]; let shaderState = null; // { canvas, gl, program, texture, rafId, startTime } function compileShader(gl, type, source) { @@ -2849,8 +3911,10 @@ void main() { } function positionShaderOverlay() { - if (!shaderState || !selectedElement) return; - const r = selectedElement.getBoundingClientRect(); + if (!shaderState) return; + const anchor = resolveBarAnchor(); + if (!anchor) return; + const r = anchor.getBoundingClientRect(); Object.assign(shaderState.canvas.style, { top: r.top + 'px', left: r.left + 'px', width: r.width + 'px', height: r.height + 'px', @@ -3084,6 +4148,7 @@ void main() { expected: expectedVariants, arrived: arrivedVariants, visible: visibleVariant, + insertPlaceholder: insertPlaceholderSnapshot || undefined, }); } @@ -3143,6 +4208,7 @@ void main() { if (variantObserver) { variantObserver.disconnect(); variantObserver = null; } stopScrollLock(); clearScrollY(); + finalizeInsertSession(); clearSession(); selectedElement = null; currentSessionId = null; @@ -3221,16 +4287,26 @@ void main() { visibleVariant = arrivedVariants > 0 ? 1 : 0; } + if (saved && saved.id === sessionId && saved.insertPlaceholder) { + insertPlaceholderSnapshot = saved.insertPlaceholder; + } + + const resumedState = arrivedVariants >= expectedVariants ? 'CYCLING' : 'GENERATING'; + // Find the visible variant's content element for highlight positioning. - // Try the visible variant first, fall back to the original's content. + const isInsert = wrapper.dataset.impeccableMode === 'insert'; const visEl = visibleVariant > 0 ? pickVariantContent(wrapper, visibleVariant) : null; const origEl = pickVariantContent(wrapper, 'original'); - selectedElement = visEl || origEl || wrapper.parentElement; + state = resumedState; + if (isInsert && resumedState === 'GENERATING' && arrivedVariants === 0) { + selectedElement = ensureInsertPlaceholder() || findInsertAnchorInDom() || wrapper; + } else { + selectedElement = visEl || origEl || (isInsert ? findInsertAnchorInDom() : null) || wrapper.parentElement; + } // Set display state BEFORE starting observer (avoid triggering it) if (visibleVariant > 0) showVariantInDOM(currentSessionId, visibleVariant); - state = arrivedVariants >= expectedVariants ? 'CYCLING' : 'GENERATING'; showBar(state === 'CYCLING' ? 'cycling' : 'generating'); startScrollTracking(); // Build the params panel for the restored visible variant. Previously @@ -3252,19 +4328,24 @@ void main() { // If we reloaded mid-generation (Bun's HTML HMR destroys the shader // canvas), re-capture the original's content and restart the shader so // the wait doesn't go dead. - if (state === 'GENERATING' && origEl) { - (async () => { - try { - const rect = origEl.getBoundingClientRect(); - if (rect.width === 0 || rect.height === 0) return; - const blob = await captureElementToBlob(origEl, null, rect); - if (blob && state === 'GENERATING') { - showShaderOverlay(origEl, blob, rect); + if (state === 'GENERATING') { + const shaderTarget = isInsert + ? (ensureInsertPlaceholder() || findInsertAnchorInDom()) + : origEl; + if (shaderTarget) { + (async () => { + try { + const rect = shaderTarget.getBoundingClientRect(); + if (rect.width === 0 || rect.height === 0) return; + const blob = await captureElementToBlob(shaderTarget, null, rect); + if (blob && state === 'GENERATING') { + showShaderOverlay(shaderTarget, blob, rect); + } + } catch (err) { + console.warn('[impeccable] shader resume failed:', err); } - } catch (err) { - console.warn('[impeccable] shader resume failed:', err); - } - })(); + })(); + } } return true; } @@ -3274,11 +4355,102 @@ void main() { // --------------------------------------------------------------------------- let globalBarEl = null; + let globalBarBrandEl = null; + let agentPollTooltipEl = null; + let agentPollingConnected = false; + let agentStatusPollTimer = null; + let steerFocusSuspended = false; + let steerFocusPauseUntil = 0; + let pagePointerGesture = null; + let pagePickSkipClick = false; + let steerFocusRecoverTimer = null; + const STEER_PAGE_FOCUS_PAUSE_MS = 500; let detectActive = false; - let pickActive = true; + const PICK_PREFS_KEY = 'impeccable-live-pick'; + const INTERACTION_PREFS_KEY = 'impeccable-live-interaction'; + const PLACEHOLDER_DEFAULT_HEIGHT = 80; + const PLACEHOLDER_MIN_HEIGHT = 48; + const PLACEHOLDER_MIN_WIDTH = 120; + + function loadInteractionPrefs() { + try { + const raw = localStorage.getItem(INTERACTION_PREFS_KEY); + if (raw) { + const prefs = JSON.parse(raw); + return { + pickActive: !!prefs.pickActive, + insertActive: !!prefs.insertActive, + }; + } + const legacy = localStorage.getItem(PICK_PREFS_KEY); + if (legacy) { + const prefs = JSON.parse(legacy); + return { pickActive: !!prefs.pickActive, insertActive: false }; + } + } catch { /* ignore */ } + return { pickActive: false, insertActive: false }; + } + + function saveInteractionPrefs() { + try { + localStorage.setItem(INTERACTION_PREFS_KEY, JSON.stringify({ pickActive, insertActive })); + } catch { /* ignore */ } + } + + function loadPickPref() { + return loadInteractionPrefs().pickActive; + } + + function savePickPref() { + saveInteractionPrefs(); + } + + let pickActive = loadInteractionPrefs().pickActive; + let insertActive = loadInteractionPrefs().insertActive; + let configureKind = 'replace'; + let insertLineEl = null; + let insertHoverAnchor = null; + let insertHoverPosition = null; + let insertHoverAxis = null; + let insertAnchorElement = null; + let insertAnchorPosition = null; + let insertAnchorLayoutAxis = null; + let insertPlaceholderSnapshot = null; + let placeholderElement = null; let detectCount = 0; let detectScriptLoaded = false; + // Steer — collapsed pill in the global bar; expands while typing for page-level chat. + let pageChatEl = null; + let pageChatInput = null; + let pageChatHint = null; + let pageChatVoiceBtn = null; + let pageChatExpanded = false; + let steerLocked = false; + let steerRequestId = null; + let pageChatDotsEl = null; + let steerAwaitTimer = null; + let voiceRecognition = null; + let voiceListening = false; + let voiceSuppressSubmit = false; + let voiceInterimBase = ''; + /** @type {{ mode: 'steer'|'configure', input: HTMLInputElement, submit: () => void, beforeStart?: () => void } | null} */ + let voiceCtx = null; + const PAGE_CHAT_COLLAPSED_W = '88px'; + const PAGE_CHAT_PROCESSING_W = '76px'; + const STEER_AWAIT_TIMEOUT_MS = 120000; + const AGENT_STATUS_POLL_MS = 5000; + const AGENT_DISCONNECTED_MARK = 'oklch(56% 0.032 82 / 0.78)'; + const AGENT_DISCONNECTED_TIP = 'Agent disconnected — run live-poll.mjs to connect'; + const GLOBAL_BAR_SECTION_GAP = 8; + const GLOBAL_BAR_INNER_GAP = 2; + const GLOBAL_BAR_INNER_PAD_LEFT = 2; + const PAGE_CHAT_EXPANDED_W = 'min(280px, 38vw)'; + const ICON_PAGE_CHAT = + ''; + const ICON_PAGE_VOICE = + ''; + // Theme-aware color palette for the global bar. We detect the page's // ambient background and invert — dark bar on light pages, light bar on // dark pages. This keeps the bar from fighting with the host design. @@ -3320,46 +4492,891 @@ void main() { } catch { return 'light'; } } - function barPaletteForTheme(theme) { - if (theme === 'dark') { - // Light bar on dark page - return { - surface: 'oklch(98% 0 0 / 0.92)', - surfaceDeep: 'oklch(92% 0.005 60 / 0.96)', // slightly deeper, faint warm - hairline: 'oklch(70% 0 0 / 0.35)', - text: 'oklch(15% 0 0)', - textDim: 'oklch(45% 0 0)', - accent: 'oklch(60% 0.25 350)', - accentSoft: 'oklch(60% 0.25 350 / 0.18)', - mark: 'oklch(98% 0 0)', // logo mark fill - markText: 'oklch(15% 0 0)', // logo "/" color - exitHover: 'oklch(85% 0 0 / 0.5)', - }; - } - // Dark bar on light page. Bar is a warm charcoal, logo slab is much - // deeper so the rounded-right shape reads as a clear sculpted mark. + function barPaletteForTheme(_theme) { + // Picker chrome always uses neo-kinpaku styling (homepage /live-mode demo + // bars in kinpaku-kit.css), regardless of host page light/dark theme. return { - surface: 'oklch(26% 0 0 / 0.94)', - surfaceDeep: 'oklch(18% 0 0 / 0.96)', // darker sand for Tune popover - hairline: 'oklch(42% 0 0 / 0.5)', - text: 'oklch(96% 0 0)', - textDim: 'oklch(72% 0 0)', - accent: 'oklch(72% 0.22 350)', - accentSoft: 'oklch(72% 0.22 350 / 0.22)', - mark: 'oklch(8% 0 0)', - markText: 'oklch(96% 0 0)', - exitHover: 'oklch(36% 0 0 / 0.6)', + surface: C.ink, + surfaceDeep: C.ink, + border: C.brand, + hairline: 'oklch(58% 0.065 82 / 0.48)', + text: 'oklch(84% 0.035 82)', + textDim: 'oklch(63% 0.024 82)', + accent: C.brand, + accentSoft: C.brandSoft, + exitHover: 'oklch(58% 0.15 35 / 0.18)', + shadow: PICKER_SHADOW, + chatSurface: 'oklch(22% 0.012 82)', + // Verdigris patina — secondary state (see site/styles/kinpaku-tokens.css) + patina: 'oklch(70% 0.12 188)', + patinaPale: 'oklch(82% 0.07 188)', + patinaSoft: 'oklch(70% 0.12 188 / 0.28)', }; } - // Impeccable logo mark — matches the site-header SVG (rounded square + "/"). - function brandMarkSvg(fill, ink, size = 18) { - return ` - - - - + svg: ` + + + + `, label: 'DESIGN.md', ariaLabel: 'Toggle DESIGN.md panel', @@ -3510,13 +5556,7 @@ void main() { }); inner.appendChild(designBtn); - // Thin divider before the exit button - const divider = el('span', { - width: '1px', height: '18px', - background: P.hairline, - margin: '0 4px 0 2px', - }); - inner.appendChild(divider); + initPageChat(inner, P); // Exit × on the right — intentionally subtle (textDim at rest, text on // hover) so it sits behind the active toggles in visual hierarchy. @@ -3537,20 +5577,23 @@ void main() { }); exitBtn.innerHTML = ''; exitBtn.title = 'Exit live mode'; - exitBtn.addEventListener('mouseenter', () => { exitBtn.style.color = P.text; exitBtn.style.background = P.exitHover; }); + exitBtn.addEventListener('mouseenter', () => { exitBtn.style.color = 'oklch(58% 0.15 35)'; exitBtn.style.background = P.exitHover; }); exitBtn.addEventListener('mouseleave', () => { exitBtn.style.color = P.textDim; exitBtn.style.background = 'transparent'; }); exitBtn.addEventListener('click', () => { sendEvent({ type: 'exit' }); teardown(); }); inner.appendChild(exitBtn); // Bar-level hover: expand every toggle's label at once; collapse on leave. // Buttons with dataset.active="true" ignore collapse (their label stays). - const toggles = [pickBtn, detectBtn, designBtn]; + const toggles = [pickBtn, insertBtn, detectBtn, designBtn]; globalBarEl.addEventListener('mouseenter', () => { toggles.forEach((t) => t._expandLabel && t._expandLabel()); }); globalBarEl.addEventListener('mouseleave', () => { toggles.forEach((t) => t._collapseLabel && t._collapseLabel()); }); + globalBarEl.addEventListener('pointerdown', () => { + try { window.focus(); } catch { /* in-app preview may block */ } + }, true); document.body.appendChild(globalBarEl); defangOutsideHandlers(globalBarEl); @@ -3558,16 +5601,19 @@ void main() { requestAnimationFrame(() => { globalBarEl.style.opacity = '1'; globalBarEl.style.transform = 'translateX(-50%) translateY(0)'; + syncPageChatFocus('global-bar-visible'); }); // Listen for detection results AND ready signal window.addEventListener('message', onDetectMessage); + updateGlobalBarState(); } function updateGlobalBarState() { const detectToggle = document.getElementById(PREFIX + '-detect-toggle'); const detectBadge = document.getElementById(PREFIX + '-detect-badge'); const pickToggle = document.getElementById(PREFIX + '-pick-toggle'); + const insertToggle = document.getElementById(PREFIX + '-insert-toggle'); const designToggle = document.getElementById(PREFIX + '-design-toggle'); const theme = globalBarEl?.dataset.theme || 'light'; const P = barPaletteForTheme(theme); @@ -3582,6 +5628,7 @@ void main() { else if (!active && btn._collapseLabel) btn._collapseLabel(); } sync(pickToggle, pickActive); + sync(insertToggle, insertActive); sync(detectToggle, detectActive); sync(designToggle, designState.open); @@ -3589,7 +5636,7 @@ void main() { // otherwise clicking a toggle that deactivates (e.g. closing DESIGN.md) // would collapse its label while the user's mouse is still on the bar. if (globalBarEl && globalBarEl.matches(':hover')) { - [pickToggle, detectToggle, designToggle].forEach((t) => t?._expandLabel?.()); + [pickToggle, insertToggle, detectToggle, designToggle].forEach((t) => t?._expandLabel?.()); } if (detectBadge) { @@ -3597,10 +5644,11 @@ void main() { detectBadge.textContent = detectCount; } - // When pick is active, make detect overlays click-through so the picker works + // When pick/insert is active, make detect overlays click-through document.querySelectorAll('.impeccable-overlay').forEach(o => { - o.style.pointerEvents = pickActive ? 'none' : ''; + o.style.pointerEvents = (pickActive || insertActive) ? 'none' : ''; }); + syncPageInteractionCursor(); } let detectReady = false; // true once detect script posts 'impeccable-ready' @@ -3628,20 +5676,48 @@ void main() { function togglePick() { pickActive = !pickActive; + if (pickActive) { + insertActive = false; + clearInsertPicking(); + } + saveInteractionPrefs(); updateGlobalBarState(); if (!pickActive) { - // Disabling pick clears any in-flight selection and UI: highlight, - // contextual bar, selectedElement. Otherwise a stale selection sits - // on screen with no obvious way to dismiss. + if (configureKind === 'insert' && state === 'CONFIGURING') { + cancelInsertConfigure(); + return; + } hideHighlight(); hideBar(); hideActionPicker(); selectedElement = null; + configureKind = 'replace'; if (state === 'PICKING' || state === 'CONFIGURING') state = 'IDLE'; } else { if (state === 'IDLE') state = 'PICKING'; } + syncPageChatFocus('toggle-pick'); + } + + function toggleInsert() { + insertActive = !insertActive; + if (insertActive) { + pickActive = false; + hideHighlight(); + hideBar(); + hideActionPicker(); + selectedElement = null; + configureKind = 'replace'; + if (state === 'CONFIGURING') cancelInsertConfigure(); + else if (state === 'IDLE' || state === 'PICKING') state = 'PICKING'; + } else { + clearInsertPicking(); + if (state === 'PICKING' && !pickActive) state = 'IDLE'; + } + saveInteractionPrefs(); + updateGlobalBarState(); + syncPageChatFocus('toggle-insert'); } function loadDetectScript() { @@ -3672,12 +5748,30 @@ void main() { /** Full teardown: remove all UI, disconnect SSE, clean up. */ function teardown() { + stopAgentStatusPoll(); + hideAgentPollTooltip(); + if (agentPollTooltipEl) { + agentPollTooltipEl.remove(); + agentPollTooltipEl = null; + } + stopVoice({ suppressSubmit: true }); + clearSteerFocusRecoverTimer(); + steerFocusSuspended = false; + steerFocusPauseUntil = 0; + pagePointerGesture = null; + pagePickSkipClick = false; cleanup(); hideBar(); if (globalBarEl) { globalBarEl.style.transform = 'translateY(100%)'; setTimeout(() => { if (globalBarEl) globalBarEl.remove(); globalBarEl = null; }, 300); } + pageChatEl = null; + pageChatInput = null; + pageChatHint = null; + pageChatVoiceBtn = null; + pageChatExpanded = false; + if (insertCreateTooltipEl) { insertCreateTooltipEl.remove(); insertCreateTooltipEl = null; } if (highlightEl) { highlightEl.remove(); highlightEl = null; } if (tooltipEl) { tooltipEl.remove(); tooltipEl = null; } if (barEl) { barEl.remove(); barEl = null; } @@ -3812,10 +5906,9 @@ void main() { position: fixed; top: 12px; bottom: 72px; right: 12px; width: ${DESIGN_PANEL_WIDTH}px; max-width: calc(100vw - 24px); background: ${BP.surface}; - border: 1px solid ${BP.hairline}; + border: 1.5px solid ${BP.border}; border-radius: 14px; - backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); - box-shadow: 0 20px 60px oklch(0% 0 0 / 0.18), 0 4px 12px oklch(0% 0 0 / 0.08); + box-shadow: ${BP.shadow}; display: flex; flex-direction: column; transform: translateX(calc(100% + 24px)); opacity: 0; @@ -4267,7 +6360,7 @@ void main() { return { role: m.role || humanizeKey(key), name: m.displayName || humanizeKey(key), - value: value, + value: normalizeCssColor(m.canonical || value), canonical: m.canonical || null, description: m.description || findProseDescription(proseColors, key, m.displayName), tonalRamp: m.tonalRamp || null, @@ -4358,7 +6451,7 @@ void main() { const hero = document.createElement('div'); hero.className = 'c-hero'; - hero.style.background = c.value; + hero.style.background = cssSafe(c.value || ''); tile.appendChild(hero); const ramp = synthesizeRamp(c); @@ -4681,6 +6774,18 @@ void main() { return String(v).replace(/[<>"'`\n]/g, ''); } + function normalizeCssColor(v) { + if (!v || typeof v !== 'string') return v; + const s = v.trim(); + const oklch = s.match(/oklch\([^)]+\)/i); + if (oklch) return oklch[0]; + const hex = s.match(/#[0-9a-fA-F]{3,8}\b/); + if (hex) return hex[0]; + const rgb = s.match(/rgba?\([^)]+\)/i); + if (rgb) return rgb[0]; + return s.replace(/\s+#.*$/, '').trim(); + } + // --- Raw tab: minimal markdown renderer (subset) -------------------------- function renderRawTab(body, md) { @@ -4826,6 +6931,8 @@ void main() { initActionPicker(); initParamsPanel(); initGlobalBar(); + attachSteerFocusDebug(); + attachSteerFocusGuard(); initDesignPanel(); document.addEventListener('mousemove', handleMouseMove, true); document.addEventListener('click', handleClick, true); @@ -4850,6 +6957,8 @@ void main() { } else { console.log('[impeccable] Resumed active variant session ' + currentSessionId + ' (' + arrivedVariants + '/' + expectedVariants + ' variants).'); } + + syncPageChatFocus('init-complete'); } if (document.readyState === 'loading') { diff --git a/skill/scripts/live-event-validation.mjs b/skill/scripts/live-event-validation.mjs new file mode 100644 index 000000000..39ca502db --- /dev/null +++ b/skill/scripts/live-event-validation.mjs @@ -0,0 +1,101 @@ +/** + * Shared event validation for the live helper server. + * Extracted for unit testing (insert mode rules). + */ + +import { canCreateInsert } from './live-insert-ui.mjs'; + +export const VISUAL_ACTIONS = [ + 'impeccable', 'bolder', 'quieter', 'distill', 'polish', 'typeset', + 'colorize', 'layout', 'adapt', 'animate', 'delight', 'overdrive', +]; + +const ID_PATTERN = /^[0-9a-f]{8}$/; +const VARIANT_ID_PATTERN = /^[0-9]{1,3}$/; +const INSERT_POSITIONS = new Set(['before', 'after']); + +function isValidId(v) { return typeof v === 'string' && ID_PATTERN.test(v); } +function isValidVariantId(v) { return typeof v === 'string' && VARIANT_ID_PATTERN.test(v); } + +function validateAnnotationFields(msg) { + if (msg.screenshotPath !== undefined && typeof msg.screenshotPath !== 'string') { + return 'generate: screenshotPath must be string'; + } + if (msg.comments !== undefined && !Array.isArray(msg.comments)) { + return 'generate: comments must be array'; + } + if (msg.strokes !== undefined && !Array.isArray(msg.strokes)) { + return 'generate: strokes must be array'; + } + return null; +} + +function validateInsertGenerate(msg) { + if (!msg.insert || typeof msg.insert !== 'object') return 'generate: insert mode requires insert object'; + if (!INSERT_POSITIONS.has(msg.insert.position)) return 'generate: insert.position must be before or after'; + const anchor = msg.insert.anchor; + if (!anchor || typeof anchor !== 'object') return 'generate: insert.anchor required'; + if (!anchor.tagName && !anchor.outerHTML && !(Array.isArray(anchor.classes) && anchor.classes.length)) { + return 'generate: insert.anchor needs tagName, classes, or outerHTML'; + } + if (!msg.placeholder || typeof msg.placeholder !== 'object') return 'generate: insert mode requires placeholder dimensions'; + if (!Number.isFinite(msg.placeholder.width) || !Number.isFinite(msg.placeholder.height)) { + return 'generate: placeholder width and height must be numbers'; + } + if (!canCreateInsert({ + prompt: msg.freeformPrompt, + comments: msg.comments, + strokes: msg.strokes, + })) { + return 'generate: insert requires freeformPrompt or annotations'; + } + return validateAnnotationFields(msg); +} + +function validateReplaceGenerate(msg) { + if (!msg.action || !VISUAL_ACTIONS.includes(msg.action)) return 'generate: invalid action'; + if (!msg.element || !msg.element.outerHTML) return 'generate: missing element context'; + return validateAnnotationFields(msg); +} + +export function validateEvent(msg) { + if (!msg || typeof msg !== 'object' || !msg.type) return 'Missing or invalid message'; + switch (msg.type) { + case 'generate': + if (!isValidId(msg.id)) return 'generate: missing or malformed id'; + if (!Number.isInteger(msg.count) || msg.count < 1 || msg.count > 8) return 'generate: count must be 1-8'; + if (msg.mode === 'insert') return validateInsertGenerate(msg); + return validateReplaceGenerate(msg); + case 'accept': + if (!isValidId(msg.id)) return 'accept: missing or malformed id'; + if (!isValidVariantId(msg.variantId)) return 'accept: missing or malformed variantId'; + if (msg.paramValues !== undefined) { + if (typeof msg.paramValues !== 'object' || msg.paramValues === null || Array.isArray(msg.paramValues)) { + return 'accept: paramValues must be an object'; + } + } + return null; + case 'discard': + return isValidId(msg.id) ? null : 'discard: missing or malformed id'; + case 'checkpoint': + if (!isValidId(msg.id)) return 'checkpoint: missing or malformed id'; + if (!Number.isInteger(msg.revision) || msg.revision < 0) return 'checkpoint: revision must be a non-negative integer'; + if (msg.paramValues !== undefined && (typeof msg.paramValues !== 'object' || msg.paramValues === null || Array.isArray(msg.paramValues))) { + return 'checkpoint: paramValues must be an object'; + } + return null; + case 'exit': + return null; + case 'prefetch': + if (!msg.pageUrl || typeof msg.pageUrl !== 'string') return 'prefetch: missing pageUrl'; + return null; + case 'steer': + if (!isValidId(msg.id)) return 'steer: missing or malformed id'; + if (typeof msg.message !== 'string' || !msg.message.trim()) return 'steer: message required'; + if (msg.message.length > 4000) return 'steer: message too long'; + if (msg.pageUrl !== undefined && typeof msg.pageUrl !== 'string') return 'steer: pageUrl must be string'; + return null; + default: + return 'Unknown event type: ' + msg.type; + } +} diff --git a/skill/scripts/live-insert-ui.mjs b/skill/scripts/live-insert-ui.mjs new file mode 100644 index 000000000..ae54f6f93 --- /dev/null +++ b/skill/scripts/live-insert-ui.mjs @@ -0,0 +1,458 @@ +/** + * Pure helpers for live-mode insert UI (browser + tests). + * Kept separate from live-browser.js so insert logic is unit-testable. + */ + +export const PLACEHOLDER_DEFAULT_HEIGHT = 80; +export const PLACEHOLDER_MIN_HEIGHT = 48; +export const PLACEHOLDER_MIN_WIDTH = 120; + +/** @typedef {'before' | 'after'} InsertPosition */ +/** @typedef {'row' | 'column'} InsertAxis */ + +/** + * Infer sibling flow axis from a container's computed layout styles. + * @param {{ display?: string, flexDirection?: string, gridTemplateColumns?: string, gridAutoFlow?: string }} style + * @returns {InsertAxis} + */ +export function detectInsertAxisFromStyle(style) { + const display = style?.display || 'block'; + if (display.includes('flex')) { + const dir = style.flexDirection || 'row'; + return dir.startsWith('row') ? 'row' : 'column'; + } + if (display === 'grid' || display === 'inline-grid') { + const flow = style.gridAutoFlow || 'row'; + if (flow.includes('column')) return 'column'; + const cols = (style.gridTemplateColumns || '').trim(); + if (cols && cols !== 'none') { + const colCount = cols.split(/\s+/).filter(Boolean).length; + if (colCount > 1) return 'row'; + } + return 'row'; + } + return 'column'; +} + +/** + * Pick insertion side from pointer position against an anchor element box. + * @param {number} clientX + * @param {number} clientY + * @param {{ top: number, left: number, width: number, height: number, bottom?: number, right?: number }} rect + * @param {InsertAxis} [axis] + * @returns {InsertPosition} + */ +export function computeInsertPosition(clientX, clientY, rect, axis = 'column') { + if (!rect) return 'after'; + if (axis === 'row') { + if (!Number.isFinite(rect.left) || !Number.isFinite(rect.width) || rect.width <= 0) return 'after'; + const mid = rect.left + rect.width / 2; + return clientX < mid ? 'before' : 'after'; + } + if (!Number.isFinite(rect.top) || !Number.isFinite(rect.height) || rect.height <= 0) return 'after'; + const mid = rect.top + rect.height / 2; + return clientY < mid ? 'before' : 'after'; +} + +/** + * Whether Create is allowed for an insert session. + * Requires a non-empty prompt OR at least one annotation. + */ +export function canCreateInsert({ prompt, comments, strokes }) { + const hasPrompt = typeof prompt === 'string' && prompt.trim().length > 0; + const hasComments = Array.isArray(comments) && comments.length > 0; + const hasStrokes = Array.isArray(strokes) && strokes.some( + (s) => Array.isArray(s?.points) && s.points.length >= 2, + ); + return hasPrompt || hasComments || hasStrokes; +} + +/** Tooltip/title when Create is disabled. */ +export function insertCreateDisabledReason({ prompt, comments, strokes }) { + if (canCreateInsert({ prompt, comments, strokes })) return null; + return 'Add a prompt or annotate the placeholder to create'; +} + +/** + * Fixed-position insert line coordinates (viewport px). + * @param {{ top: number, left: number, width: number, height: number, bottom?: number, right?: number }} rect + * @param {InsertPosition} position + * @param {InsertAxis} [axis] + */ +export function insertLineCoords(rect, position, axis = 'column') { + if (axis === 'row') { + const right = rect.right ?? rect.left + rect.width; + const x = position === 'before' ? rect.left - 2 : right + 2; + return { axis: 'row', top: rect.top, left: x, width: 0, height: rect.height }; + } + const bottom = rect.bottom ?? rect.top + rect.height; + const y = position === 'before' ? rect.top - 2 : bottom + 2; + return { axis: 'column', top: y, left: rect.left, width: rect.width, height: 0 }; +} + +/** Cursor while hovering an insert boundary. */ +export function cursorForInsertAxis(axis) { + return axis === 'row' ? 'ew-resize' : 'ns-resize'; +} + +function groupSiblingRows(siblings, rowThreshold = 8) { + const sorted = [...siblings].sort((a, b) => a.rect.top - b.rect.top || a.rect.left - b.rect.left); + const rows = []; + for (const entry of sorted) { + let placed = false; + for (const row of rows) { + if (Math.abs(entry.rect.top - row[0].rect.top) <= rowThreshold) { + row.push(entry); + placed = true; + break; + } + } + if (!placed) rows.push([entry]); + } + return rows; +} + +function horizontalOverlap(a, b) { + const left = Math.max(a.left, b.left); + const right = Math.min(a.right ?? a.left + a.width, b.right ?? b.left + b.width); + return Math.max(0, right - left); +} + +/** + * Hit-test the gap between adjacent siblings (flex rows, grid columns, stacked blocks). + * @param {number} clientX + * @param {number} clientY + * @param {Array<{ el: unknown, rect: { top: number, left: number, width: number, height: number, bottom?: number, right?: number } }>} siblings + * @param {{ slop?: number, minOverlap?: number }} [opts] + */ +export function hitSiblingInsertGap(clientX, clientY, siblings, opts = {}) { + if (!Array.isArray(siblings) || siblings.length < 2) return null; + const slop = opts.slop ?? 12; + const minOverlap = opts.minOverlap ?? 0.25; + + for (const row of groupSiblingRows(siblings)) { + if (row.length < 2) continue; + const sorted = [...row].sort((a, b) => a.rect.left - b.rect.left); + for (let i = 0; i < sorted.length - 1; i++) { + const a = sorted[i]; + const b = sorted[i + 1]; + const aRight = a.rect.right ?? a.rect.left + a.rect.width; + const bLeft = b.rect.left; + if (bLeft <= aRight) continue; + const top = Math.max(a.rect.top, b.rect.top); + const aBottom = a.rect.bottom ?? a.rect.top + a.rect.height; + const bBottom = b.rect.bottom ?? b.rect.top + b.rect.height; + const bottom = Math.min(aBottom, bBottom); + const span = bottom - top; + const minH = Math.min(a.rect.height, b.rect.height); + if (span < minH * minOverlap) continue; + + const inX = clientX >= aRight - slop && clientX <= bLeft + slop; + const inY = clientY >= top - slop && clientY <= bottom + slop; + if (!inX || !inY) continue; + + const midX = (aRight + bLeft) / 2; + return { + anchor: b.el, + position: 'before', + axis: 'row', + line: { axis: 'row', left: midX, top, width: 0, height: span }, + }; + } + } + + const sortedCol = [...siblings].sort((a, b) => a.rect.top - b.rect.top || a.rect.left - b.rect.left); + for (let i = 0; i < sortedCol.length - 1; i++) { + const a = sortedCol[i]; + const b = sortedCol[i + 1]; + const overlap = horizontalOverlap(a.rect, b.rect); + const minW = Math.min(a.rect.width, b.rect.width); + if (overlap < minW * minOverlap) continue; + + const aBottom = a.rect.bottom ?? a.rect.top + a.rect.height; + const gapTop = aBottom; + const gapBottom = b.rect.top; + if (gapBottom <= gapTop) continue; + + const overlapLeft = Math.max(a.rect.left, b.rect.left); + const overlapRight = Math.min( + a.rect.right ?? a.rect.left + a.rect.width, + b.rect.right ?? b.rect.left + b.rect.width, + ); + const inY = clientY >= gapTop - slop && clientY <= gapBottom + slop; + const inX = clientX >= overlapLeft - slop && clientX <= overlapRight + slop; + if (!inY || !inX) continue; + + const midY = (gapTop + gapBottom) / 2; + return { + anchor: b.el, + position: 'before', + axis: 'column', + line: { axis: 'column', top: midY, left: overlapLeft, width: overlap, height: 0 }, + }; + } + + return null; +} + +/** + * Resolve insert hover target, side, axis, and indicator line for the pointer. + */ +export function resolveInsertHover({ clientX, clientY, target, rect, axis, siblings }) { + const gap = hitSiblingInsertGap(clientX, clientY, siblings); + if (gap) return gap; + + const position = computeInsertPosition(clientX, clientY, rect, axis); + const line = insertLineCoords(rect, position, axis); + return { anchor: target, position, axis, line }; +} + +/** + * How the in-flow placeholder should participate in layout. + * Prefer implicit sizing (flex / %) so row inserts don't inherit the full parent width in px. + * @returns {{ kind: 'flex', flex: string, minWidth: number } | { kind: 'percent' } | { kind: 'auto' } | { kind: 'explicit', width: number }} + */ +export function placeholderSizing({ axis, parentDisplay, parentWidth, anchorFlex }) { + const display = parentDisplay || 'block'; + const w = Number.isFinite(parentWidth) ? parentWidth : 0; + + if (axis === 'row') { + if (display.includes('flex')) { + const flex = anchorFlex && anchorFlex !== 'none' && anchorFlex !== '0 1 auto' + ? anchorFlex + : '1 1 0'; + return { kind: 'flex', flex, minWidth: 0 }; + } + if (display === 'grid' || display === 'inline-grid') { + return { kind: 'auto' }; + } + } + + if (w >= PLACEHOLDER_MIN_WIDTH) { + return { kind: 'percent' }; + } + + return { + kind: 'explicit', + width: Math.max(PLACEHOLDER_MIN_WIDTH, w || PLACEHOLDER_MIN_WIDTH), + }; +} + +/** Width kinds that need materializing to px before edge-resize. */ +export function placeholderWidthIsImplicit(kind) { + return kind === 'flex' || kind === 'percent' || kind === 'auto'; +} + +/** + * Clamp user-resized placeholder dimensions. + */ +export function clampPlaceholderSize(width, height, parentWidth, opts = {}) { + const minW = opts.minWidth ?? PLACEHOLDER_MIN_WIDTH; + const minH = opts.minHeight ?? PLACEHOLDER_MIN_HEIGHT; + const maxW = opts.maxWidth ?? Math.max(minW, parentWidth || minW); + return { + width: Math.min(maxW, Math.max(minW, Math.round(width))), + height: Math.max(minH, Math.round(height)), + }; +} + +/** CSS cursor for a placeholder edge resize handle. */ +export function cursorForPlaceholderEdge(edge) { + if (edge === 'n' || edge === 's') return 'ns-resize'; + if (edge === 'e' || edge === 'w') return 'ew-resize'; + return 'default'; +} + +/** + * Compute placeholder box after dragging one edge (in-flow margins shift for n/w). + * @param {{ width: number, height: number, marginLeft?: number, marginTop?: number }} start + * @param {'n'|'e'|'s'|'w'} edge + * @param {number} dx pointer delta X since drag start + * @param {number} dy pointer delta Y since drag start + * @param {number} parentWidth + */ +export function resizePlaceholderFromEdge(start, edge, dx, dy, parentWidth, opts = {}) { + const base = { + width: start.width, + height: start.height, + marginLeft: start.marginLeft ?? 0, + marginTop: start.marginTop ?? 0, + }; + if (edge === 'e') base.width = start.width + dx; + else if (edge === 'w') { + base.width = start.width - dx; + base.marginLeft = start.marginLeft + dx; + } else if (edge === 's') base.height = start.height + dy; + else if (edge === 'n') { + base.height = start.height - dy; + base.marginTop = start.marginTop + dy; + } + + const clamped = clampPlaceholderSize(base.width, base.height, parentWidth, opts); + if (edge === 'w') { + base.marginLeft = start.marginLeft + start.width - clamped.width; + } else if (edge === 'n') { + base.marginTop = start.marginTop + start.height - clamped.height; + } + + return { + width: clamped.width, + height: clamped.height, + marginLeft: Math.round(base.marginLeft), + marginTop: Math.round(base.marginTop), + }; +} + +/** Pick and insert toggles are independent but turning one ON turns the other OFF. */ +export function applyPickToggle(pickActive, insertActive) { + const nextPick = !pickActive; + return { + pickActive: nextPick, + insertActive: nextPick ? false : insertActive, + }; +} + +export function applyInsertToggle(pickActive, insertActive) { + const nextInsert = !insertActive; + return { + pickActive: nextInsert ? false : pickActive, + insertActive: nextInsert, + }; +} + +/** + * Build the browser generate payload for insert mode. + */ +export function buildInsertGeneratePayload({ + id, + count, + pageUrl, + anchorContext, + position, + placeholder, + freeformPrompt, + comments, + strokes, + screenshotPath, +}) { + const payload = { + type: 'generate', + mode: 'insert', + id, + count, + pageUrl, + insert: { + position, + anchor: anchorContext, + }, + placeholder, + freeformPrompt: freeformPrompt?.trim() || undefined, + }; + if (comments?.length) payload.comments = comments; + if (strokes?.length) payload.strokes = strokes; + if (screenshotPath) payload.screenshotPath = screenshotPath; + return payload; +} + +/** + * Whether a variant wrapper is currently shown (handles `hidden` and display:none). + * @param {{ hidden?: boolean, style?: { display?: string } } | null | undefined} el + */ +export function isVariantShown(el) { + if (!el) return false; + if (el.hidden) return false; + if (el.style?.display === 'none') return false; + return true; +} + +/** + * Show or hide a variant wrapper for cycling. + * @param {{ hidden?: boolean, style?: { display?: string }, removeAttribute?: (name: string) => void, setAttribute?: (name: string, value?: string) => void } | null | undefined} el + * @param {boolean} shown + */ +export function setVariantShown(el, shown) { + if (!el) return; + if (shown) { + el.removeAttribute?.('hidden'); + if (el.style) el.style.display = ''; + } else { + el.setAttribute?.('hidden', ''); + if (el.style) el.style.display = 'none'; + } +} + +/** + * Pick the best live anchor during an insert session (placeholder until variants land). + * @param {{ + * wrapper?: unknown, + * variantCount?: number, + * visibleVariant?: number, + * placeholder?: unknown, + * insertAnchor?: unknown, + * pickVariantContent?: (wrapper: unknown, index: number) => unknown, + * }} opts + */ +export function resolveInsertSessionAnchor(opts) { + const { + wrapper, + variantCount = 0, + visibleVariant = 0, + placeholder, + insertAnchor, + pickVariantContent, + } = opts || {}; + if (wrapper && variantCount > 0 && visibleVariant > 0 && pickVariantContent) { + const vis = pickVariantContent(wrapper, visibleVariant); + if (vis) return vis; + } + return placeholder || insertAnchor || null; +} + +/** + * Snapshot placeholder geometry + anchor fingerprint so HMR can recreate the box. + * @param {{ + * tagName?: string, + * className?: string, + * textContent?: string, + * }} anchor + * @param {{ + * offsetWidth?: number, + * offsetHeight?: number, + * style?: { marginLeft?: string, marginTop?: string }, + * }} placeholder + * @param {{ position: 'before' | 'after', layoutAxis?: 'row' | 'column' }} meta + */ +export function buildInsertPlaceholderSnapshot(anchor, placeholder, { position, layoutAxis }) { + return { + width: Math.round(placeholder.offsetWidth || 0), + height: Math.round(placeholder.offsetHeight || PLACEHOLDER_DEFAULT_HEIGHT), + marginLeft: parseFloat(placeholder.style?.marginLeft || '') || 0, + marginTop: parseFloat(placeholder.style?.marginTop || '') || 0, + position, + layoutAxis: layoutAxis || 'column', + anchorTag: anchor.tagName || 'DIV', + anchorClasses: anchor.className || '', + anchorText: (anchor.textContent || '').trim().slice(0, 120), + }; +} + +/** + * Re-find an insert anchor after framework HMR replaced the live DOM node. + * @param {Pick} doc + * @param {ReturnType | null | undefined} snapshot + * @param {Element | null | undefined} liveAnchor + */ +export function findInsertAnchorInDom(doc, snapshot, liveAnchor = null) { + if (liveAnchor && doc.body.contains(liveAnchor)) return liveAnchor; + if (!snapshot) return null; + const tag = (snapshot.anchorTag || 'div').toLowerCase(); + const cls = (snapshot.anchorClasses || '').split(/\s+/).filter(Boolean)[0]; + const needle = snapshot.anchorText || ''; + const sel = cls ? `${tag}.${cls}` : tag; + const candidates = doc.querySelectorAll(sel); + for (const candidate of candidates) { + if (needle && !(candidate.textContent || '').includes(needle.slice(0, 40))) continue; + return candidate; + } + return null; +} diff --git a/skill/scripts/live-insert.mjs b/skill/scripts/live-insert.mjs new file mode 100644 index 000000000..09d4d55be --- /dev/null +++ b/skill/scripts/live-insert.mjs @@ -0,0 +1,232 @@ +/** + * CLI helper: find an anchor element in source and splice an insert-variant + * wrapper before or after it (no original variant — net-new content). + * + * Usage: + * node live-insert.mjs --id SESSION_ID --count N --position after \ + * --classes "hero" --tag section [--file path] + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import { isGeneratedFile } from './is-generated.mjs'; +import { + buildSearchQueries, + findElement, + findAllElements, + filterByText, + findFileWithQuery, + detectCommentSyntax, + detectStyleMode, + buildCssAuthoring, + buildCssSelectorPrefixExamples, +} from './live-wrap.mjs'; + +const INSERT_POSITIONS = new Set(['before', 'after']); + +export function isInsertPosition(value) { + return INSERT_POSITIONS.has(value); +} + +export function computeInsertLine(startLine, endLine, position) { + return position === 'before' ? startLine : endLine + 1; +} + +export function buildInsertWrapperLines({ id, count, indent, commentSyntax, isJsx }) { + const styleContents = isJsx ? 'style={{ display: "contents" }}' : 'style="display: contents"'; + const attrs = + 'data-impeccable-variants="' + id + '" ' + + 'data-impeccable-mode="insert" ' + + 'data-impeccable-variant-count="' + count + '" ' + + styleContents; + + if (isJsx) { + return [ + indent + '
', + indent + ' ' + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close, + indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close, + indent + ' ' + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close, + indent + '
', + ]; + } + + return [ + indent + commentSyntax.open + ' impeccable-variants-start ' + id + ' ' + commentSyntax.close, + indent + '
', + indent + ' ' + commentSyntax.open + ' Variants: insert below this line ' + commentSyntax.close, + indent + '
', + indent + commentSyntax.open + ' impeccable-variants-end ' + id + ' ' + commentSyntax.close, + ]; +} + +function argVal(args, flag) { + const idx = args.indexOf(flag); + return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : null; +} + +function resolveElementMatch({ lines, queries, tag, text }) { + if (text) { + const candidates = []; + for (const q of queries) { + const all = findAllElements(lines, q, tag); + for (const c of all) { + if (!candidates.some((x) => x.startLine === c.startLine)) candidates.push(c); + } + if (candidates.length === 1) break; + } + if (candidates.length === 0) return { error: 'element_not_found' }; + if (candidates.length === 1) return { match: candidates[0] }; + const filtered = filterByText(candidates, lines, text); + if (filtered.length === 1) return { match: filtered[0] }; + if (filtered.length === 0) return { match: candidates[0] }; + return { error: 'element_ambiguous', candidates: filtered }; + } + + for (const q of queries) { + const match = findElement(lines, q, tag); + if (match) return { match }; + } + return { error: 'element_not_found' }; +} + +export async function insertCli() { + const args = process.argv.slice(2); + + if (args.includes('--help') || args.includes('-h')) { + console.log(`Usage: node live-insert.mjs [options] + +Find an anchor element in source and splice an insert-variant wrapper. + +Required: + --id ID Session ID for the variant wrapper + --count N Number of expected variants (1-8) + --position POS before | after (relative to the anchor element) + +Element identification (at least one required): + --element-id ID HTML id attribute of the anchor element + --classes A,B,C Comma-separated CSS class names + --tag TAG Tag name (div, section, etc.) + --query TEXT Fallback: raw text to search for + +Optional: + --file PATH Source file to search in (skips auto-detection) + --text TEXT Anchor textContent for disambiguation (~80 chars) + +Output (JSON): + { mode: "insert", file, position, insertLine, commentSyntax, styleMode, styleTag, cssAuthoring }`); + process.exit(0); + } + + const id = argVal(args, '--id'); + const count = parseInt(argVal(args, '--count') || '3', 10); + const position = argVal(args, '--position'); + const elementId = argVal(args, '--element-id'); + const classes = argVal(args, '--classes'); + const tag = argVal(args, '--tag'); + const query = argVal(args, '--query'); + const filePath = argVal(args, '--file'); + const text = argVal(args, '--text'); + + if (!id) { console.error('Missing --id'); process.exit(1); } + if (!position) { console.error('Missing --position (before | after)'); process.exit(1); } + if (!isInsertPosition(position)) { console.error('Invalid --position: ' + position); process.exit(1); } + if (!elementId && !classes && !query) { + console.error('Need at least one of: --element-id, --classes, --query'); + process.exit(1); + } + + const queries = buildSearchQueries(elementId, classes, tag, query); + const genOpts = { cwd: process.cwd() }; + + let targetFile = filePath; + if (!targetFile) { + for (const q of queries) { + targetFile = findFileWithQuery(q, process.cwd(), genOpts); + if (targetFile) break; + } + if (!targetFile) { + let generatedHit = null; + for (const q of queries) { + generatedHit = findFileWithQuery(q, process.cwd(), { ...genOpts, includeGenerated: true }); + if (generatedHit) break; + } + console.error(JSON.stringify({ + error: generatedHit ? 'element_not_in_source' : 'element_not_found', + fallback: 'agent-driven', + hint: 'See "Handle fallback" in live.md.', + })); + process.exit(1); + } + } else if (isGeneratedFile(targetFile, genOpts)) { + console.error(JSON.stringify({ + error: 'file_is_generated', + fallback: 'agent-driven', + file: path.relative(process.cwd(), path.resolve(process.cwd(), targetFile)), + })); + process.exit(1); + } + + const content = fs.readFileSync(targetFile, 'utf-8'); + const lines = content.split('\n'); + const resolved = resolveElementMatch({ lines, queries, tag, text }); + + if (resolved.error === 'element_ambiguous') { + console.error(JSON.stringify({ + error: 'element_ambiguous', + fallback: 'agent-driven', + file: path.relative(process.cwd(), targetFile), + candidates: resolved.candidates.map((c) => ({ + startLine: c.startLine + 1, + endLine: c.endLine + 1, + })), + })); + process.exit(1); + } + if (!resolved.match) { + console.error(JSON.stringify({ error: 'element_not_found', fallback: 'agent-driven' })); + process.exit(1); + } + + const { startLine, endLine } = resolved.match; + const commentSyntax = detectCommentSyntax(targetFile); + const styleMode = detectStyleMode(targetFile); + const isJsx = commentSyntax.open === '{/*'; + const spliceIndex = computeInsertLine(startLine, endLine, position); + const indent = lines[spliceIndex]?.match(/^(\s*)/)?.[1] + ?? lines[startLine]?.match(/^(\s*)/)?.[1] + ?? ''; + + const wrapperLines = buildInsertWrapperLines({ + id, + count, + indent, + commentSyntax, + isJsx, + }); + + const newLines = [ + ...lines.slice(0, spliceIndex), + ...wrapperLines, + ...lines.slice(spliceIndex), + ]; + fs.writeFileSync(targetFile, newLines.join('\n'), 'utf-8'); + + const insertLine = spliceIndex + 3; + + console.log(JSON.stringify({ + mode: 'insert', + position, + file: path.relative(process.cwd(), targetFile), + insertLine: insertLine + 1, + commentSyntax, + styleMode: styleMode.mode, + styleTag: styleMode.styleTag, + cssSelectorPrefixExamples: buildCssSelectorPrefixExamples(styleMode.mode, count), + cssAuthoring: buildCssAuthoring(styleMode, count), + })); +} + +const _running = process.argv[1]; +if (_running?.endsWith('live-insert.mjs') || _running?.endsWith('live-insert.mjs/')) { + insertCli(); +} diff --git a/skill/scripts/live-poll.mjs b/skill/scripts/live-poll.mjs index 10d452491..cbf17d54f 100644 --- a/skill/scripts/live-poll.mjs +++ b/skill/scripts/live-poll.mjs @@ -3,6 +3,7 @@ * * Usage: * npx impeccable poll # Block until browser event, print JSON + * npx impeccable poll --stream # Experimental: keep polling; one JSON line per event * npx impeccable poll --timeout=600000 # Custom timeout (ms); default is long-poll friendly * npx impeccable poll --reply done # Reply "done" to event * npx impeccable poll --reply error "msg" # Reply with error @@ -18,7 +19,9 @@ import { readLiveServerInfo } from './impeccable-paths.mjs'; // timeout that can't be lowered per-request. We cap each request below // that ceiling and loop in `pollOnce` to synthesize a long poll without // depending on the standalone undici package. -const PER_REQUEST_TIMEOUT_MS = 270_000; +export const PER_REQUEST_TIMEOUT_MS = 270_000; + +const EVENT_TYPES_NEEDING_AGENT_REPLY = new Set(['generate', 'steer']); function readServerInfo() { const record = readLiveServerInfo(process.cwd()); @@ -33,7 +36,11 @@ export function buildPollReplyPayload(token, { id, type, message, file, data }) return { token, id, type, message, file, data }; } -async function postReply(base, token, reply) { +export function requiresAgentReply(event) { + return EVENT_TYPES_NEEDING_AGENT_REPLY.has(event?.type); +} + +export async function postReply(base, token, reply) { const res = await fetch(`${base}/poll`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -45,6 +52,178 @@ async function postReply(base, token, reply) { } } +export async function fetchServerStatus(base, token) { + const res = await fetch(`${base}/status?token=${token}`); + if (res.status === 401) { + const err = new Error('Authentication failed. The server token may have changed.'); + err.code = 'AUTH_FAILED'; + throw err; + } + if (!res.ok) { + throw new Error(`Status failed: ${res.status} ${res.statusText}`); + } + return res.json(); +} + +export function isEventPending(status, eventId) { + return (status.pendingEvents || []).some((entry) => entry.id === eventId); +} + +export async function waitForEventAck(base, token, eventId, { + pollIntervalMs = 400, + maxWaitMs = 600_000, +} = {}) { + const deadline = Date.now() + maxWaitMs; + while (Date.now() < deadline) { + const status = await fetchServerStatus(base, token); + if (!isEventPending(status, eventId)) return true; + await new Promise((resolve) => setTimeout(resolve, pollIntervalMs)); + } + return false; +} + +export async function fetchNextEvent(base, token, { totalDeadline } = {}) { + while (true) { + if (totalDeadline && Date.now() >= totalDeadline) { + return { type: 'timeout' }; + } + + const remaining = totalDeadline + ? totalDeadline - Date.now() + : PER_REQUEST_TIMEOUT_MS; + const slice = Math.min(Math.max(remaining, 1000), PER_REQUEST_TIMEOUT_MS); + const res = await fetch(`${base}/poll?token=${token}&timeout=${slice}`); + + if (res.status === 401) { + const err = new Error('Authentication failed. The server token may have changed.'); + err.code = 'AUTH_FAILED'; + throw err; + } + + if (!res.ok) { + throw new Error(`Poll failed: ${res.status} ${res.statusText}`); + } + + const next = await res.json(); + if (next?.type === 'timeout') { + if (totalDeadline && Date.now() < totalDeadline) continue; + if (!totalDeadline) continue; + return next; + } + return next; + } +} + +export async function augmentEventWithAcceptHandling(event, base, token) { + if (event.type !== 'accept' && event.type !== 'discard') return event; + + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + const acceptScript = path.join(__dirname, 'live-accept.mjs'); + const scriptArgs = event.type === 'discard' + ? ['--id', event.id, '--discard'] + : ['--id', event.id, '--variant', event.variantId]; + if (event.type === 'accept' && event.paramValues && Object.keys(event.paramValues).length > 0) { + scriptArgs.push('--param-values', JSON.stringify(event.paramValues)); + } + + try { + const out = execFileSync( + 'node', + [acceptScript, ...scriptArgs], + { encoding: 'utf-8', cwd: process.cwd(), timeout: 30_000 }, + ); + event._acceptResult = JSON.parse(out.trim()); + } catch (err) { + event._acceptResult = { handled: false, mode: 'error', error: err.message }; + } + + const completionType = completionTypeForAcceptResult(event.type, event._acceptResult); + try { + await postReply(base, token, { + id: event.id, + type: completionType, + message: event._acceptResult?.error, + file: event._acceptResult?.file, + data: event._acceptResult?.carbonize === true ? { carbonize: true } : undefined, + }); + } catch (err) { + event._completionAck = { ok: false, error: err.message }; + } + if (!event._completionAck) { + event._completionAck = completionAckForAcceptResult(event.id, completionType, event._acceptResult); + } + + return event; +} + +export function writeCarbonizeBanner(event) { + if (event._acceptResult?.carbonize === true) { + process.stderr.write('\n⚠ Carbonize cleanup REQUIRED before next poll. After cleanup, run live-complete.mjs --id ' + event.id + '. See reference/live.md "Required after accept".\n\n'); + } +} + +export function printPollEvent(event) { + console.log(JSON.stringify(event)); +} + +export async function runPollOnce(base, token, { totalTimeout = 600_000 } = {}) { + const deadline = Date.now() + totalTimeout; + const event = await fetchNextEvent(base, token, { totalDeadline: deadline }); + await augmentEventWithAcceptHandling(event, base, token); + writeCarbonizeBanner(event); + printPollEvent(event); + return event; +} + +export async function runPollStream(base, token, { + ackTimeoutMs = 600_000, + ackPollIntervalMs = 400, + shouldContinue = () => true, +} = {}) { + process.stderr.write('[impeccable-poll] stream mode: one JSON object per line on stdout; use --reply while this process stays running\n'); + + while (shouldContinue()) { + const event = await fetchNextEvent(base, token); + await augmentEventWithAcceptHandling(event, base, token); + writeCarbonizeBanner(event); + printPollEvent(event); + + if (event.type === 'exit') return event; + + if (requiresAgentReply(event)) { + const acked = await waitForEventAck(base, token, event.id, { + pollIntervalMs: ackPollIntervalMs, + maxWaitMs: ackTimeoutMs, + }); + if (!acked) { + const err = new Error(`Timed out waiting for --reply on event ${event.id}`); + err.code = 'ACK_TIMEOUT'; + throw err; + } + } + } + + return null; +} + +function handlePollError(err) { + if (err.code === 'AUTH_FAILED') { + console.error(err.message); + console.error('Try restarting: npx impeccable live stop && npx impeccable live'); + process.exit(1); + } + if (err.cause?.code === 'ECONNREFUSED') { + console.error('Live server not running. Start one with: npx impeccable live'); + process.exit(1); + } + if (err.code === 'ACK_TIMEOUT') { + console.error(err.message); + process.exit(1); + } + console.error('Poll failed:', err.message); + process.exit(1); +} + export async function pollCli() { const args = process.argv.slice(2); @@ -54,13 +233,20 @@ export async function pollCli() { Wait for a browser event from the live variant server, or reply to one. Modes: - poll Block until a browser event arrives, print JSON - poll --reply done Reply "done" to event + poll Block until a browser event arrives, print JSON, exit + poll --stream Keep polling; print one JSON line per event (see live.md) + poll --reply done Reply "done" to event (replace or insert generate) + poll --reply steer_done Reply after handling a steer event (unlocks Steer bar) poll --reply error "msg" Reply with an error message Options: - --timeout=MS Long-poll timeout in ms (default: 600000). Use the default unless the user asked to pause live; never use a short timeout to end the chat turn - --help Show this help message`); + --timeout=MS One-shot poll timeout in ms (default: 600000). Ignored in --stream mode + --ack-timeout=MS Stream mode: max wait for --reply after generate/steer (default: 600000) + --help Show this help message + +Harness note: + Default one-shot mode is the portable contract for Claude Code, Codex, and Cursor. + --stream is experimental for harnesses with fast incremental stdout; do not use on Cursor.`); process.exit(0); } @@ -74,7 +260,6 @@ Options: const status = args[replyIdx + 2] || 'done'; const fileIdx = args.indexOf('--file'); const filePath = fileIdx !== -1 && fileIdx + 1 < args.length ? args[fileIdx + 1] : undefined; - // Message is any remaining positional arg that isn't a flag const message = args.find((a, i) => i > replyIdx + 2 && !a.startsWith('--') && i !== fileIdx + 1) || undefined; if (!id) { @@ -84,8 +269,6 @@ Options: try { await postReply(base, info.token, { id, type: status, message, file: filePath }); - - // Success — silent exit (agent doesn't need output for replies) } catch (err) { if (err.cause?.code === 'ECONNREFUSED') { console.error('Live server not running. Start one with: npx impeccable live'); @@ -97,99 +280,21 @@ Options: return; } - // Poll mode: block until browser event. Default 10 min. Node's built-in - // fetch enforces a 300s headers timeout, so we loop in slices under that - // ceiling and keep re-polling until we get a real event or the user's - // total timeout runs out. - const timeoutArg = args.find(a => a.startsWith('--timeout=')); - const totalTimeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600000; + const streamMode = args.includes('--stream'); + const ackTimeoutArg = args.find((a) => a.startsWith('--ack-timeout=')); + const ackTimeoutMs = ackTimeoutArg ? parseInt(ackTimeoutArg.split('=')[1], 10) : 600_000; - const deadline = Date.now() + totalTimeout; - let event; try { - while (true) { - const remaining = deadline - Date.now(); - if (remaining <= 0) { - event = { type: 'timeout' }; - break; - } - const slice = Math.min(remaining, PER_REQUEST_TIMEOUT_MS); - const res = await fetch(`${base}/poll?token=${info.token}&timeout=${slice}`); - - if (res.status === 401) { - console.error('Authentication failed. The server token may have changed.'); - console.error('Try restarting: npx impeccable live stop && npx impeccable live'); - process.exit(1); - } - - if (!res.ok) { - console.error(`Poll failed: ${res.status} ${res.statusText}`); - process.exit(1); - } - - const next = await res.json(); - // Server-side timeout means no browser event arrived in this slice. - // Loop and re-poll until we get a real event or we hit the user's - // total deadline. - if (next?.type === 'timeout' && Date.now() < deadline) continue; - event = next; - break; + if (streamMode) { + await runPollStream(base, info.token, { ackTimeoutMs }); + return; } - // Auto-handle accept/discard via deterministic script - if (event.type === 'accept' || event.type === 'discard') { - const __dirname = path.dirname(fileURLToPath(import.meta.url)); - const acceptScript = path.join(__dirname, 'live-accept.mjs'); - const scriptArgs = event.type === 'discard' - ? ['--id', event.id, '--discard'] - : ['--id', event.id, '--variant', event.variantId]; - if (event.type === 'accept' && event.paramValues && Object.keys(event.paramValues).length > 0) { - scriptArgs.push('--param-values', JSON.stringify(event.paramValues)); - } - try { - const out = execFileSync( - 'node', - [acceptScript, ...scriptArgs], - { encoding: 'utf-8', cwd: process.cwd(), timeout: 30_000 } - ); - event._acceptResult = JSON.parse(out.trim()); - } catch (err) { - event._acceptResult = { handled: false, mode: 'error', error: err.message }; - } - - const completionType = completionTypeForAcceptResult(event.type, event._acceptResult); - try { - await postReply(base, info.token, { - id: event.id, - type: completionType, - message: event._acceptResult?.error, - file: event._acceptResult?.file, - data: event._acceptResult?.carbonize === true ? { carbonize: true } : undefined, - }); - } catch (err) { - event._completionAck = { ok: false, error: err.message }; - } - if (!event._completionAck) { - event._completionAck = completionAckForAcceptResult(event.id, completionType, event._acceptResult); - } - } - - // Second signal path: stderr banner in case the agent parses stdout - // JSON but skips nested fields. One line is enough — the full checklist - // is in reference/live.md. - if (event._acceptResult?.carbonize === true) { - process.stderr.write('\n⚠ Carbonize cleanup REQUIRED before next poll. After cleanup, run live-complete.mjs --id ' + event.id + '. See reference/live.md "Required after accept".\n\n'); - } - - // Print the event as JSON — the agent reads this from stdout - console.log(JSON.stringify(event)); + const timeoutArg = args.find((a) => a.startsWith('--timeout=')); + const totalTimeout = timeoutArg ? parseInt(timeoutArg.split('=')[1], 10) : 600_000; + await runPollOnce(base, info.token, { totalTimeout }); } catch (err) { - if (err.cause?.code === 'ECONNREFUSED') { - console.error('Live server not running. Start one with: npx impeccable live'); - } else { - console.error('Poll failed:', err.message); - } - process.exit(1); + handlePollError(err); } } diff --git a/skill/scripts/live-server.mjs b/skill/scripts/live-server.mjs index 0eae94b9b..574b574ef 100644 --- a/skill/scripts/live-server.mjs +++ b/skill/scripts/live-server.mjs @@ -21,8 +21,9 @@ import path from 'node:path'; import net from 'node:net'; import { fileURLToPath } from 'node:url'; import { parseDesignMd } from './design-parser.mjs'; -import { resolveContextDir } from './load-context.mjs'; +import { resolveContextDir } from './context.mjs'; import { createLiveSessionStore } from './live-session-store.mjs'; +import { validateEvent } from './live-event-validation.mjs'; import { getDesignSidecarPath, getLiveAnnotationsDir, @@ -33,7 +34,7 @@ import { } from './impeccable-paths.mjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -// PRODUCT.md / DESIGN.md live wherever load-context.mjs resolves. The generated +// PRODUCT.md / DESIGN.md live wherever context.mjs resolves. The generated // DESIGN sidecar is project-local at .impeccable/design.json, with legacy // DESIGN.json fallback for existing projects. const CONTEXT_DIR = resolveContextDir(process.cwd()); @@ -65,6 +66,7 @@ const state = { sseClients: new Set(), // SSE response objects (server→browser push) pendingEvents: [], // browser events waiting for agent ack ({ event, leaseUntil }) pendingPolls: [], // agent poll callbacks waiting for browser events + lastAgentPollingBroadcast: null, exitTimer: null, sessionDir: null, // per-session tmp dir for annotation screenshots sessionStore: null, @@ -130,16 +132,31 @@ function scheduleLeaseFlush() { } function flushPendingPolls() { + let changed = false; while (state.pendingPolls.length > 0) { const entry = findAvailablePendingEvent(); if (!entry) { scheduleLeaseFlush(); + broadcastAgentPollingIfChanged(); return; } const poll = state.pendingPolls.shift(); poll.resolve(leaseEvent(entry, poll.leaseMs)); + changed = true; } scheduleLeaseFlush(); + if (changed) broadcastAgentPollingIfChanged(); +} + +function agentPollingConnected() { + return state.pendingPolls.length > 0; +} + +function broadcastAgentPollingIfChanged() { + const connected = agentPollingConnected(); + if (state.lastAgentPollingBroadcast === connected) return; + state.lastAgentPollingBroadcast = connected; + broadcast({ type: 'agent_polling', connected }); } /** Push a message to all connected SSE clients. */ @@ -187,8 +204,7 @@ function loadBrowserScripts() { function hasProjectContext() { // PRODUCT.md carries brand voice / anti-references — that's what determines // whether variants are brand-aware. DESIGN.md (visual tokens) is a separate - // concern, surfaced by the design panel's own empty state. Legacy - // .impeccable.md is auto-migrated to PRODUCT.md by load-context.mjs. + // concern, surfaced by the design panel's own empty state. try { fs.accessSync(path.join(CONTEXT_DIR, 'PRODUCT.md'), fs.constants.R_OK); return true; @@ -199,66 +215,6 @@ function statOrNull(filePath) { try { return fs.statSync(filePath); } catch { return null; } } -// --------------------------------------------------------------------------- -// Validation (inline — no external import needed for self-contained script) -// --------------------------------------------------------------------------- - -const VISUAL_ACTIONS = [ - 'impeccable', 'bolder', 'quieter', 'distill', 'polish', 'typeset', - 'colorize', 'layout', 'adapt', 'animate', 'delight', 'overdrive', -]; - -// Browser generates ids via crypto.randomUUID().slice(0, 8) (8 hex chars) -// and variantIds via String(small integer). Restrict to those shapes so -// any value that reaches a downstream child_process or DOM selector is -// inert by construction. -const ID_PATTERN = /^[0-9a-f]{8}$/; -const VARIANT_ID_PATTERN = /^[0-9]{1,3}$/; - -function isValidId(v) { return typeof v === 'string' && ID_PATTERN.test(v); } -function isValidVariantId(v) { return typeof v === 'string' && VARIANT_ID_PATTERN.test(v); } - -function validateEvent(msg) { - if (!msg || typeof msg !== 'object' || !msg.type) return 'Missing or invalid message'; - switch (msg.type) { - case 'generate': - if (!isValidId(msg.id)) return 'generate: missing or malformed id'; - if (!msg.action || !VISUAL_ACTIONS.includes(msg.action)) return 'generate: invalid action'; - if (!Number.isInteger(msg.count) || msg.count < 1 || msg.count > 8) return 'generate: count must be 1-8'; - if (!msg.element || !msg.element.outerHTML) return 'generate: missing element context'; - // Optional annotation fields (all-or-nothing: if any present, all must be well-formed). - if (msg.screenshotPath !== undefined && typeof msg.screenshotPath !== 'string') return 'generate: screenshotPath must be string'; - if (msg.comments !== undefined && !Array.isArray(msg.comments)) return 'generate: comments must be array'; - if (msg.strokes !== undefined && !Array.isArray(msg.strokes)) return 'generate: strokes must be array'; - return null; - case 'accept': - if (!isValidId(msg.id)) return 'accept: missing or malformed id'; - if (!isValidVariantId(msg.variantId)) return 'accept: missing or malformed variantId'; - if (msg.paramValues !== undefined) { - if (typeof msg.paramValues !== 'object' || msg.paramValues === null || Array.isArray(msg.paramValues)) { - return 'accept: paramValues must be an object'; - } - } - return null; - case 'discard': - return isValidId(msg.id) ? null : 'discard: missing or malformed id'; - case 'checkpoint': - if (!isValidId(msg.id)) return 'checkpoint: missing or malformed id'; - if (!Number.isInteger(msg.revision) || msg.revision < 0) return 'checkpoint: revision must be a non-negative integer'; - if (msg.paramValues !== undefined && (typeof msg.paramValues !== 'object' || msg.paramValues === null || Array.isArray(msg.paramValues))) { - return 'checkpoint: paramValues must be an object'; - } - return null; - case 'exit': - return null; - case 'prefetch': - if (!msg.pageUrl || typeof msg.pageUrl !== 'string') return 'prefetch: missing pageUrl'; - return null; - default: - return 'Unknown event type: ' + msg.type; - } -} - // --------------------------------------------------------------------------- // HTTP request handler // --------------------------------------------------------------------------- @@ -396,6 +352,7 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) { status: 'ok', port: state.port, connectedClients: state.sseClients.size, + agentPolling: agentPollingConnected(), pendingEvents: state.pendingEvents.map((entry) => ({ id: entry.event?.id, type: entry.event?.type, @@ -506,6 +463,7 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) { res.write('data: ' + JSON.stringify({ type: 'connected', hasProjectContext: hasProjectContext(), + agentPolling: agentPollingConnected(), }) + '\n\n'); state.sseClients.add(res); @@ -614,6 +572,7 @@ function handlePollGet(req, res, url) { const timer = setTimeout(() => { const idx = state.pendingPolls.indexOf(poll); if (idx !== -1) state.pendingPolls.splice(idx, 1); + broadcastAgentPollingIfChanged(); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ type: 'timeout' })); }, timeout); @@ -623,11 +582,13 @@ function handlePollGet(req, res, url) { res.end(JSON.stringify(event)); } state.pendingPolls.push(poll); + broadcastAgentPollingIfChanged(); scheduleLeaseFlush(); req.on('close', () => { clearTimeout(timer); const idx = state.pendingPolls.indexOf(poll); if (idx !== -1) state.pendingPolls.splice(idx, 1); + broadcastAgentPollingIfChanged(); }); } @@ -649,13 +610,15 @@ function handlePollPost(req, res) { acknowledgePendingEvent(msg.id); if (state.sessionStore && msg.id) { try { - const eventType = msg.type === 'discard' || msg.type === 'discarded' - ? 'discarded' - : msg.type === 'complete' - ? 'complete' - : msg.type === 'error' - ? 'agent_error' - : 'agent_done'; + const eventType = msg.type === 'steer_done' + ? 'steer_done' + : msg.type === 'discard' || msg.type === 'discarded' + ? 'discarded' + : msg.type === 'complete' + ? 'complete' + : msg.type === 'error' + ? 'agent_error' + : 'agent_done'; state.sessionStore.appendEvent({ type: eventType, id: msg.id, diff --git a/skill/scripts/live-session-store.mjs b/skill/scripts/live-session-store.mjs index d55ed2d3a..6b698cd55 100644 --- a/skill/scripts/live-session-store.mjs +++ b/skill/scripts/live-session-store.mjs @@ -209,6 +209,17 @@ function applyEvent(snapshot, entry, inheritedDiagnostics = []) { next.pendingEventSeq = entry.seq ?? next.pendingEventSeq; next.pendingEvent = toPendingEvent(event); break; + case 'steer': + next.phase = 'steer_requested'; + next.pageUrl = event.pageUrl ?? next.pageUrl; + next.pendingEventSeq = entry.seq ?? next.pendingEventSeq; + next.pendingEvent = toPendingEvent(event); + break; + case 'steer_done': + next.phase = 'steer_done'; + next.pendingEventSeq = null; + next.pendingEvent = null; + break; case 'discard': next.phase = 'discard_requested'; next.pendingEventSeq = entry.seq ?? next.pendingEventSeq; diff --git a/skill/scripts/live-status.mjs b/skill/scripts/live-status.mjs index dce1fbca2..88fbfbbe2 100644 --- a/skill/scripts/live-status.mjs +++ b/skill/scripts/live-status.mjs @@ -31,6 +31,7 @@ export async function statusCli() { status: server.status, port: server.port, connectedClients: server.connectedClients, + agentPolling: server.agentPolling, pendingEvents: server.pendingEvents, } : null, activeSessions: server?.activeSessions || activeSessions, diff --git a/skill/scripts/live-wrap.mjs b/skill/scripts/live-wrap.mjs index d46328b9f..305a663a4 100644 --- a/skill/scripts/live-wrap.mjs +++ b/skill/scripts/live-wrap.mjs @@ -370,11 +370,14 @@ function buildCssAuthoring(styleMode, count) { selectorExamples: variantNumbers.map((n) => `[data-impeccable-variant="${n}"] > .variant-class`), requirements: [ 'Use the styleTag exactly; the is:inline attribute is required for this file.', + 'Put raw CSS directly between the styleTag opening and a plain close.', 'Prefix every preview selector with the matching [data-impeccable-variant="N"] selector.', 'Keep selectors anchored to the generated variant wrapper; do not rely on component CSS scoping for preview rules.', ], forbidden: [ 'Do not use @scope for this styleMode.', + 'Do not wrap style content in a JSX/TSX template literal ({` ... `}); that syntax is for .tsx/.jsx only.', + 'Do not put { immediately after the style opening tag; Astro parses { as expression syntax.', ], }; } @@ -629,4 +632,15 @@ if (_running?.endsWith('live-wrap.mjs') || _running?.endsWith('live-wrap.mjs/')) } // Test exports (used by tests/live-wrap.test.mjs) -export { buildSearchQueries, findElement, findClosingLine, detectCommentSyntax }; +export { + buildSearchQueries, + findElement, + findClosingLine, + detectCommentSyntax, + findAllElements, + filterByText, + findFileWithQuery, + detectStyleMode, + buildCssAuthoring, + buildCssSelectorPrefixExamples, +}; diff --git a/skill/scripts/live.mjs b/skill/scripts/live.mjs index cafb0eca9..8acd300ed 100644 --- a/skill/scripts/live.mjs +++ b/skill/scripts/live.mjs @@ -21,7 +21,7 @@ import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { loadContext } from './load-context.mjs'; +import { loadContext } from './context.mjs'; import { resolveFiles } from './live-inject.mjs'; import { readLiveServerInfo } from './impeccable-paths.mjs'; @@ -80,7 +80,7 @@ The agent should then: process.exit(1); } - // 4. Load PRODUCT.md + DESIGN.md context (auto-migrates legacy .impeccable.md) + // 4. Load PRODUCT.md + DESIGN.md context. const ctx = loadContext(process.cwd()); // 5. Compute drift-heal: compare resolved inject targets against the @@ -102,7 +102,6 @@ The agent should then: hasDesign: ctx.hasDesign, design: ctx.design, designPath: ctx.designPath, - migrated: ctx.migrated, }, null, 2)); } diff --git a/skill/scripts/load-context.mjs b/skill/scripts/load-context.mjs deleted file mode 100644 index dc340bf16..000000000 --- a/skill/scripts/load-context.mjs +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Shared context loader for every impeccable command that needs to know - * "who is this for" and "what does this look like". - * - * Input: project root (process.cwd()). - * - * Output (JSON to stdout): - * { - * hasProduct: boolean, // PRODUCT.md found (or auto-migrated) - * product: string | null, // PRODUCT.md contents - * productPath: string | null, // relative path - * hasDesign: boolean, // DESIGN.md found - * design: string | null, // DESIGN.md contents - * designPath: string | null, - * migrated: boolean, // true if we auto-renamed .impeccable.md -> PRODUCT.md - * contextDir: string, // absolute path of the directory the files were found in - * } - * - * Filename matching is case-insensitive for PRODUCT.md and DESIGN.md. The - * Google DESIGN.md convention is uppercase at repo root; Kiro-style and - * lowercase variants are also matched so users don't get punished for case. - * - * Lookup directory resolution (first match wins): - * 1. process.env.IMPECCABLE_CONTEXT_DIR (absolute or relative to cwd) - * 2. cwd, if PRODUCT.md / DESIGN.md / .impeccable.md is there (back-compat) - * 3. Auto-fallback subdirectories of cwd: .agents/context/, then docs/ - * 4. cwd as a default "no context found" location - * - * Legacy `.impeccable.md` -> PRODUCT.md migration only fires at cwd root; - * fallback directories are read-only as far as auto-rename is concerned. - */ - -import fs from 'node:fs'; -import path from 'node:path'; - -const PRODUCT_NAMES = ['PRODUCT.md', 'Product.md', 'product.md']; -const DESIGN_NAMES = ['DESIGN.md', 'Design.md', 'design.md']; -const LEGACY_NAMES = ['.impeccable.md']; -const FALLBACK_DIRS = ['.agents/context', 'docs']; - -/** - * Resolve the directory that holds PRODUCT.md / DESIGN.md for - * this project. Exported so other scripts (e.g. live-server.mjs) can read the - * design files from the same location the loader uses. - */ -export function resolveContextDir(cwd = process.cwd()) { - // 1. Explicit override - const envDir = process.env.IMPECCABLE_CONTEXT_DIR; - if (envDir && envDir.trim()) { - const trimmed = envDir.trim(); - return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed); - } - - // 2. cwd wins if any canonical or legacy file is there. We check legacy too - // so the auto-migration path in loadContext stays predictable. - if (firstExisting(cwd, [...PRODUCT_NAMES, ...DESIGN_NAMES, ...LEGACY_NAMES])) { - return cwd; - } - - // 3. Auto-fallback subdirs. Match if PRODUCT.md or DESIGN.md is present; - // legacy `.impeccable.md` does not pull the lookup into a fallback dir. - for (const rel of FALLBACK_DIRS) { - const candidate = path.resolve(cwd, rel); - if (firstExisting(candidate, [...PRODUCT_NAMES, ...DESIGN_NAMES])) { - return candidate; - } - } - - // 4. Nothing found — keep the historical "default to cwd" behaviour so the - // caller's `hasProduct === false` branch still fires the same way. - return cwd; -} - -export function loadContext(cwd = process.cwd()) { - let migrated = false; - const contextDir = resolveContextDir(cwd); - - // 1. Look for PRODUCT.md (case-insensitive) in the resolved dir - let productPath = firstExisting(contextDir, PRODUCT_NAMES); - - // 2. Legacy: if no PRODUCT.md but .impeccable.md exists at cwd root, rename - // it in place. We only migrate at the root — fallback dirs are read-only - // so we don't surprise users by mutating files under docs/ or .agents/. - if (!productPath && contextDir === cwd) { - const legacyPath = firstExisting(cwd, LEGACY_NAMES); - if (legacyPath) { - const newPath = path.join(cwd, 'PRODUCT.md'); - try { - fs.renameSync(legacyPath, newPath); - productPath = newPath; - migrated = true; - } catch { - // Rename failed (permissions, etc.) — fall back to reading legacy in place - productPath = legacyPath; - } - } - } - - // 3. DESIGN.md (case-insensitive) - const designPath = firstExisting(contextDir, DESIGN_NAMES); - - const product = productPath ? safeRead(productPath) : null; - const design = designPath ? safeRead(designPath) : null; - - return { - hasProduct: !!product, - product, - productPath: productPath ? path.relative(cwd, productPath) : null, - hasDesign: !!design, - design, - designPath: designPath ? path.relative(cwd, designPath) : null, - migrated, - contextDir, - }; -} - -function firstExisting(dir, names) { - for (const name of names) { - const abs = path.join(dir, name); - if (fs.existsSync(abs)) return abs; - } - return null; -} - -function safeRead(p) { - try { return fs.readFileSync(p, 'utf-8'); } catch { return null; } -} - -// --------------------------------------------------------------------------- -// CLI mode — print the context as JSON -// --------------------------------------------------------------------------- - -function cli() { - const result = loadContext(process.cwd()); - console.log(JSON.stringify(result, null, 2)); -} - -const _running = process.argv[1]; -if (_running?.endsWith('load-context.mjs') || _running?.endsWith('load-context.mjs/')) { - cli(); -} diff --git a/skill/scripts/palette.mjs b/skill/scripts/palette.mjs new file mode 100644 index 000000000..4c3f1751a --- /dev/null +++ b/skill/scripts/palette.mjs @@ -0,0 +1,633 @@ +#!/usr/bin/env node +/** + * Brand-seed picker. Returns one OKLCH seed color + the mood it most + * naturally evokes, and teaches the model how to compose a full palette + * around it. + * + * The seed is the brand's anchor color. The 5-role palette (bg, surface, + * ink, accent, muted) is composed by the caller at runtime using their + * judgment + the brief (PRODUCT.md / DESIGN.md / user prompt), NOT picked + * from a frozen 4-color preset. + * + * Why: 4-color frozen palettes drift toward safe defaults (warm-cream bg, + * complementary accent on near-white) regardless of brief. A single seed + + * the model's own composition lets the same seed produce a dark-mode jazz + * club or a light-mode hospitality brand depending on what the brief calls + * for. Tested empirically against curated 4-color palettes; seed approach + * wins on mood-fit in 3 of 5 cases and ties on the rest. + * + * Usage: + * node scripts/palette.mjs # pick at random + * node scripts/palette.mjs --id seed-021 # pick a specific seed + * node scripts/palette.mjs --from # hash to a seed (deterministic) + * + * Env vars: + * IMPECCABLE_PALETTE_SEED — same as --from; useful for the eval harness + * to make runs reproducible. + */ + +import crypto from 'node:crypto'; + +// Seeds are inlined (129 entries, hand-curated via a tinder review of +// ~400 candidates from ColorHunt + synthesis + Radix/brand/Pantone anchors). +// Each carries a mood + strategy the judging model produced — surfaced as +// hints, not commands; the brief still drives composition. +const SEEDS = [ + { id: "seed-200", oklch: [0.360, 0.137, 0.0], + mood: "Aesop apothecary shelf — oxblood bottle glass against linen, considered and unhurried", + strategy: "Seed is a deep desaturated red-brown that reads as brand ink itself; I push primary darker toward bottle-glass oxblood, pair with a pure white surface so the red does the work, and use a clear pale-blush accent that can carry dark text in pills." }, + { id: "seed-000", oklch: [0.400, 0.130, 0.0], + mood: "oxblood leather banquette in a 1940s steakhouse — low lamplight on dark wood and burgundy", + strategy: "Near-black bg with the faintest red undertone lets the oxblood primary glow like lamplit leather; warm cream ink and a brass accent complete the chophouse register." }, + { id: "seed-002", oklch: [0.450, 0.150, 0.0], + mood: "darkroom red light — analog photography, blood-warm safelight glow on chemical trays", + strategy: "Near-black surface with a deep oxblood primary lets the seed function like a safelight in a darkroom — the bg disappears so the red becomes the only emotional signal." }, + { id: "seed-003", oklch: [0.500, 0.194, 0.0], + mood: "darkroom safelight — the deep oxblood glow of analog photography, chemical and contemplative", + strategy: "Anchored the seed as primary against pure near-black so the red reads like a single illuminated bulb in a developing room, with cool desaturated ink to evoke silver gelatin print tones." }, + { id: "seed-004", oklch: [0.546, 0.204, 3.4], + mood: "midnight boudoir — velvet rose under low lamplight, perfumed and intimate", + strategy: "Near-black surface lets the rose seed glow like silk in shadow; a warm champagne accent provides the candle-flame counterpoint without breaking the hush." }, + { id: "seed-005", oklch: [0.550, 0.180, 0.0], + mood: "smoldering vermillion at dusk — the last red ember in a blacksmith's forge, iron-rich and quietly violent", + strategy: "Near-black gallery surround lets the seed read as glowing forged metal; ink stays warm-off-white, accent shifts to a hotter ember orange so the primary feels like cooling steel against a fresh strike." }, + { id: "seed-201", oklch: [0.647, 0.262, 0.3], + mood: "Figma plugin marketplace red — confident product-brand crimson, the kind a modern dev tool uses for a 'live' indicator or a primary CTA on a pristine docs page", + strategy: "Pure white surface lets a high-chroma crimson primary do all the brand work, paired with a hue-shifted warm coral accent for hierarchy without competing saturation" }, + { id: "seed-006", oklch: [0.650, 0.160, 0.0], + mood: "1960s Italian cinema — Technicolor lipstick red against a darkened theater", + strategy: "Pure near-black surface lets a saturated cinematic red and its warm peach accent perform like film light projected in a dark room — the brand colors carry the drama, the bg disappears." }, + { id: "seed-008", oklch: [0.520, 0.200, 10.4], + mood: "Negroni hour at a Milanese bar — bittersweet crimson, vermouth and amaro under low tungsten", + strategy: "Seed is a saturated red-crimson with cinematic weight, so I sit it on near-black to let the primary glow like backlit liquor, with a warmer amber accent acting as the citrus twist against the bitter red." }, + { id: "seed-010", oklch: [0.563, 0.223, 11.0], + mood: "Negroni hour on a Milan rooftop — bittersweet crimson, aperitivo light, polished restraint", + strategy: "Seed is a vivid carmine-red with strong chroma, so the surface gets out of the way (pure white) and lets the primary do the aperitivo work, with a cooled garnet accent for tension." }, + { id: "seed-202", oklch: [0.643, 0.247, 7.0], + mood: "Glossier brand pink — modern beauty editorial, confident and current", + strategy: "Pure white bg lets a saturated rose-red primary do all the brand work, paired with a deeper crimson accent for hierarchy — the Stripe/Glossier move where the color carries the mood." }, + { id: "seed-013", oklch: [0.400, 0.130, 20.0], + mood: "Tuscan cellar at dusk — aged terracotta, oxidized iron, the deep red of decanted Sangiovese", + strategy: "Black surface lets the oxblood seed and copper accent glow like firelight on cellar stone; brand colors carry all the warmth while the room recedes." }, + { id: "seed-014", oklch: [0.450, 0.150, 20.0], + mood: "smoldering tannery — oxblood leather, cured under low workshop light", + strategy: "Anchor the deep oxblood seed as primary against a near-black architectural ground, then lift with a single warm ember accent so the leather reads burnished rather than bloody." }, + { id: "seed-016", oklch: [0.550, 0.180, 20.0], + mood: "Negroni hour on a Roman terrace — bitter campari red, vermouth, late golden light spilling on white linen", + strategy: "Pure white surface lets the campari-red primary do all the emotional work, paired with a deeper oxblood accent for bittersweet depth — Italian aperitivo restraint, not warmth-washed." }, + { id: "seed-205", oklch: [0.634, 0.254, 17.6], + mood: "Aesop apothecary bottle — considered red-coral on a clinical white surface, the kind of brand restraint where one saturated object does all the work", + strategy: "Default A pure white surface lets a single coral-red primary carry the entire brand voice; accent shifts to a deeper oxblood for hierarchy without competing chroma." }, + { id: "seed-011", oklch: [0.639, 0.207, 13.5], + mood: "Aperitivo hour in Milan — Campari glow on a white marble bar, crisp and effervescent", + strategy: "Pure white gallery backdrop lets the Campari-red primary ring like a single bitter note; ink is near-black with a whisper of warmth, accent shifts to a deeper oxblood for hierarchy without competing hues." }, + { id: "seed-015", oklch: [0.527, 0.202, 22.7], + mood: "Negroni hour on a Milanese terrace — bittersweet vermillion, aperitivo glassware catching low sun", + strategy: "Seed becomes a saturated aperitivo-red primary against pure white so the color carries the bittersweet warmth alone, paired with a deep oxblood accent for typographic gravitas." }, + { id: "seed-023", oklch: [0.427, 0.175, 29.2], + mood: "blacksmith's forge at dusk — iron heated to ember red, the deep glow of oxidized metal and quenching oil", + strategy: "Pure black bg lets the seed's ember-red glow radiate like hot iron in a dark forge; accent shifts to a copper-amber to suggest scaling metal and sparks, while ink stays near-white for tool-precise legibility." }, + { id: "seed-206", oklch: [0.614, 0.234, 28.2], + mood: "Aesop apothecary bottle — considered red-orange on lab-white, calm utility with a single confident pigment", + strategy: "Pure white surface lets a saturated vermilion primary do all the brand work, paired with a deep oxblood accent for hierarchy without introducing a second hue family" }, + { id: "seed-029", oklch: [0.665, 0.222, 25.7], + mood: "Negroni hour at a Milanese bar — bittersweet orange-red liqueur catching late afternoon light on polished marble", + strategy: "Pure white surface lets the seed's vermilion read like Campari in a glass; a deeper oxblood accent provides the bitter depth, with neutral graphite ink keeping the editorial restraint of Italian design." }, + { id: "seed-022", oklch: [0.418, 0.155, 27.2], + mood: "Pompeiian red fresco — oxidized cinnabar on a museum wall, archaeological gravity", + strategy: "Pure black gallery surface lets the seed's iron-oxide red read as a lit artifact; accent shifts to an aged terracotta amber, so primary and accent form a fired-clay duet against neutral void." }, + { id: "seed-024", oklch: [0.464, 0.169, 26.9], + mood: "Mid-century darkroom under the safelight — developer trays, oxblood leather, the quiet patience of a print emerging", + strategy: "Seed becomes a deep oxblood primary; surface stays pure black so the red glows like a safelight, with a warmer ember accent for hierarchy" }, + { id: "seed-026", oklch: [0.489, 0.190, 28.3], + mood: "smoldering ember in a blacksmith's forge — iron-hot rust, soot, and controlled fire", + strategy: "Near-black soot background lets the seed's red-orange glow like heated metal; ink is bone-white, accent is a cooler tempered-steel orange that creates internal heat gradient with the primary." }, + { id: "seed-027", oklch: [0.568, 0.208, 27.1], + mood: "Sicilian blood orange at golden hour — citrus rind, terracotta, sun on stucco", + strategy: "Seed reads as vivid blood-orange — picked pure white surface so the citrus-red primary and a deep oxblood accent do all the emotional work, like a Loro Piana editorial spread." }, + { id: "seed-028", oklch: [0.591, 0.172, 24.0], + mood: "Sienna-fired ceramic studio at dusk — terracotta cooling on a wheel, hands still dusted with slip", + strategy: "Pure black stage lets the fired-clay primary glow like a kiln ember, with a deeper oxblood accent providing tonal weight rather than hue contrast — a monochrome warm-axis play." }, + { id: "seed-033", oklch: [0.544, 0.169, 31.3], + mood: "1960s Italian terracotta workshop — fired clay, espresso, late-afternoon Mediterranean dust", + strategy: "Pure black ground lets the seed's burnt-sienna primary glow like a lit kiln, with a deeper oxblood accent for restrained warmth tension — the brand carries the heat, the surface stays out." }, + { id: "seed-207", oklch: [0.564, 0.231, 29.1], + mood: "Aesop apothecary bottle — considered red oxide, the calm authority of a well-made object on a white shelf", + strategy: "Seed becomes the singular brand voice against pure white, with a deeper oxblood accent for hierarchy — the surface disappears so the red does all the speaking." }, + { id: "seed-035", oklch: [0.663, 0.153, 32.1], + mood: "Aesop apothecary bottle — clay-fired warmth, considered retail", + strategy: "Pure white surface lets the terracotta primary do the brand work, paired with a deep umber ink and a cooler clay accent for editorial tension." }, + { id: "seed-037", oklch: [0.590, 0.188, 35.8], + mood: "Aesop apothecary bottle — considered terracotta, herbalist restraint, the warmth comes from the glass not the room", + strategy: "Seed becomes a muted terracotta primary against pure white so the brand's warmth carries entirely through the color itself; accent shifts to a deeper umber for quiet hierarchy." }, + { id: "seed-038", oklch: [0.652, 0.229, 34.8], + mood: "blown-glass furnace at dusk — molten orange iron pulled from the kiln, a craftsman's signature heat", + strategy: "Pure black stage so the seed reads as live ember; primary holds the seed's heat, accent shifts to a brass-amber a hue-step away for a 1.7+ contrast pairing without leaving the fire." }, + { id: "seed-039", oklch: [0.653, 0.185, 33.5], + mood: "Aesop apothecary bottle — considered terracotta, quiet retail craft", + strategy: "Seed becomes a grounded clay primary against pure white, paired with a deeper umber accent so the warmth lives entirely in the brand marks, not the surface." }, + { id: "seed-167", oklch: [0.495, 0.134, 36.0], + mood: "Aesop apothecary shelf — burnished terracotta on clinical white, considered craft pharmacy", + strategy: "Treat the seed as a brand-carrying burnt-sienna against a pure paper-white surface so the warmth lives entirely in the primary, with a deep umber accent pulled along the same warm axis for typographic gravity." }, + { id: "seed-147", oklch: [0.500, 0.151, 40.0], + mood: "Aesop apothecary shelf — considered terracotta, pharmacy restraint, the brand color does the work against clinical white", + strategy: "Anchor the seed's burnt-sienna primary against a pure white surface so the rust speaks alone, with a deep umber ink and a cooler clay accent to give the palette product-brand discipline rather than environmental warmth." }, + { id: "seed-040", oklch: [0.660, 0.201, 40.0], + mood: "Aesop apothecary bottle — amber glass on a clean dispensary shelf, considered and clinical-warm", + strategy: "Seed becomes a burnt-amber primary against pure white so the bottle-glass color does the emotional work; accent shifts to a deep olive-bronze for the apothecary-label pairing." }, + { id: "seed-041", oklch: [0.673, 0.217, 38.6], + mood: "Aesop apothecary shelf — considered orange glass, clinical retail restraint", + strategy: "Pure white surface lets the burnt-orange primary do all the brand work, with a deep ink-brown for editorial gravity and a muted clay accent that reads as a sibling, not a contrast." }, + { id: "seed-042", oklch: [0.688, 0.133, 35.8], + mood: "Aesop apothecary shelf — terracotta glass, considered retail", + strategy: "Seed becomes a warm clay primary against pure white so the bottle-on-marble retail feel comes from the brand color alone; a deeper umber accent gives the label-print contrast." }, + { id: "seed-043", oklch: [0.781, 0.119, 38.1], + mood: "Aesop apothecary catalogue — considered terracotta, dermatological restraint, the warm color doing all the work against clinical white", + strategy: "Pure white surface lets the seed's warm clay tone read as the entire brand voice, paired with a deeper umber accent for hierarchy without competing with the primary's warmth." }, + { id: "seed-168", oklch: [0.400, 0.103, 50.0], + mood: "Aesop apothecary bottle — amber glass on a clinical white shelf, considered and pharmaceutical", + strategy: "Pure white surface lets the deep amber primary act like tinted glass against a clean shelf; accent is a muted clay that complements without competing, keeping the brand quiet and product-led." }, + { id: "seed-044", oklch: [0.568, 0.149, 45.9], + mood: "1970s desert highway at golden hour — sun-faded terracotta, denim dust, the warmth of a Polaroid pulled from a glovebox", + strategy: "Seed becomes a burnt-sienna primary against pure white so the terracotta does all the emotional work; a deep indigo accent acts as the denim shadow opposing the sun, creating the era's signature warm/cool tension without tinting the page." }, + { id: "seed-045", oklch: [0.607, 0.163, 47.7], + mood: "Aesop apothecary shelf — considered amber glass, clinical restraint, craft pharmacy", + strategy: "Pure white bg lets the burnt-amber primary do the apothecary work alone, paired with a deeper umber accent and graphite ink for editorial calm." }, + { id: "seed-046", oklch: [0.653, 0.175, 45.0], + mood: "Aesop apothecary shelf — considered amber glass, quiet luxury, restrained craft", + strategy: "Pure black backdrop lets the warm amber primary glow like backlit apothecary glass, with a deeper rust accent providing tonal depth in the same hue family — monochromatic warm against neutral void." }, + { id: "seed-047", oklch: [0.695, 0.205, 43.2], + mood: "Aesop apothecary label — sun-warmed amber glass on a clinical countertop, restrained botanical pharmacy", + strategy: "Pure white surface lets the burnt-amber primary and a deeper sienna accent do all the brand work, like an apothecary bottle photographed under daylight." }, + { id: "seed-051", oklch: [0.704, 0.189, 49.0], + mood: "blacksmith's forge at dusk — glowing iron, hammered copper, ember light against cooling steel", + strategy: "Pure near-black surface lets the seed's molten orange burn like heated metal; accent shifts to a deeper amber-red to suggest the cooling end of the same iron, while ink stays a clean off-white so type reads like chalk on slate." }, + { id: "seed-171", oklch: [0.550, 0.124, 60.0], + mood: "Klim Type Foundry specimen page — considered ochre on paper, design-school-honest", + strategy: "Seed becomes a muted ochre primary on pure white; accent is a deep ink-navy pulled across the wheel for editorial contrast without warmth-pooling in the bg" }, + { id: "seed-148", oklch: [0.650, 0.146, 60.0], + mood: "Klim-style editorial gold — late-afternoon paper light on a serif specimen sheet, considered and dry", + strategy: "Hold the seed's amber as primary on a pure white page so the gold reads as ink rather than atmosphere, and pair with a deep aubergine accent for typographic contrast." }, + { id: "seed-052", oklch: [0.700, 0.130, 60.0], + mood: "late-afternoon terracotta studio — sun-warmed clay, hands-on craft, the hour before dusk", + strategy: "Seed is a saturated amber-ochre with strong environmental association (ceramics, adobe, sunlit plaster), so I lean into Exception (a) with a faintly warm bone surface that reads as lime-washed wall, then deepen the seed slightly for primary and pair it with a fired-clay rust accent for hand-thrown warmth." }, + { id: "seed-053", oklch: [0.773, 0.157, 56.6], + mood: "late-summer apricot orchard at golden hour — sun-warmed fruit, considered Californian craft", + strategy: "Seed is a juicy mid-warm orange at daylight luminance — leaning optimistic/editorial, so pure white surface lets the apricot primary glow without muddying it; a deep wine accent provides the bite." }, + { id: "seed-149", oklch: [0.600, 0.124, 70.0], + mood: "1970s desert highway — late-afternoon amber light on chrome and asphalt", + strategy: "Anchor the amber seed as primary against pure black so the warm hue reads as headlight glow against night; a cooler dusk-mauve accent provides the complementary tension of horizon vs. sun." }, + { id: "seed-054", oklch: [0.740, 0.162, 68.1], + mood: "late-afternoon honey on terracotta — Mediterranean stucco at golden hour, sun-baked amber", + strategy: "Seed is a saturated honey-amber at high lightness; pairing it with pure black lets the warmth read as luminous gold against gravity, like lamplight in a dark room." }, + { id: "seed-055", oklch: [0.774, 0.174, 65.1], + mood: "late-summer honey hour — amber light slanting through a west-facing window, optimistic and golden", + strategy: "Anchor a saturated honey-amber primary on pure white so the warmth radiates from the brand itself, then pair with a deep teak accent for grounded contrast rather than tinting the canvas." }, + { id: "seed-056", oklch: [0.691, 0.146, 74.6], + mood: "Klim-style modern publishing house — late-afternoon paper warmth, considered editorial gold", + strategy: "Pure white surface so the amber seed becomes the brand voice; ink stays near-black neutral and accent shifts to a deep ink-blue to give the gold something structural to lean on." }, + { id: "seed-150", oklch: [0.750, 0.148, 80.0], + mood: "Klim Type Foundry specimen page — late-summer editorial gold, considered and grown-up", + strategy: "Pure white surface lets a single restrained ochre primary do all the brand work, paired with a deep ink-blue accent for typographic contrast in the Klim/Commercial Type tradition." }, + { id: "seed-058", oklch: [0.764, 0.120, 77.1], + mood: "Klim Type Foundry specimen page — late-afternoon ochre, considered editorial typography", + strategy: "Pure white surface lets the ochre primary do the brand work, paired with a deep ink-blue accent for editorial contrast — the type-foundry move where one warm hue carries the whole feeling against neutral paper." }, + { id: "seed-059", oklch: [0.784, 0.144, 79.8], + mood: "late afternoon in a Tuscan limonaia — sun-cured amber on whitewashed plaster", + strategy: "Pure white surface lets the saffron-amber primary and a deep olive accent carry the Mediterranean warmth, with split-complementary tension between gold and a quiet evergreen." }, + { id: "seed-061", oklch: [0.817, 0.161, 75.1], + mood: "late-afternoon honey on Tuscan limestone — golden hour, slow and luminous", + strategy: "Pure white surface lets the amber primary glow like sunlight on a wall, paired with a deep terracotta accent for warm tonal contrast within the same hue family." }, + { id: "seed-063", oklch: [0.842, 0.165, 91.3], + mood: "late-afternoon Tuscan sun on limestone — golden hour, considered, optimistic", + strategy: "Pure white surface lets the amber-gold primary radiate as the mood-carrier, with a deep aubergine accent providing the long shadow that golden light needs to feel three-dimensional." }, + { id: "seed-174", oklch: [0.350, 0.075, 110.0], + mood: "olive grove at late afternoon — sun-cured leaves, dust, and quiet Mediterranean weight", + strategy: "Pure white surface lets a deep, sun-cured olive primary do the emotional work, with a burnt-terracotta accent providing the warm-earth counterpoint olive groves are known for." }, + { id: "seed-117", oklch: [0.650, 0.100, 110.0], + mood: "Klim-style editorial sage — late-summer foundry catalogue, considered olive-yellow on paper", + strategy: "Seed sits at olive-chartreuse; treating it as a quiet typographic primary on pure paper, with a deeper bronze-olive accent for hierarchy — the color does the work, the page disappears." }, + { id: "seed-118", oklch: [0.750, 0.090, 110.0], + mood: "Klim Type Foundry specimen page — late-summer olive light on a working specimen, the honesty of a type designer showing their work", + strategy: "Pure white bg lets a desaturated olive-yellow primary do the editorial work, with a deeper olive-bronze accent providing typographic emphasis the way a specimen uses one heavy weight against the body roman." }, + { id: "seed-065", oklch: [0.797, 0.166, 113.1], + mood: "late-summer olive grove at noon — sun-bleached leaves, dry stone, Mediterranean glare", + strategy: "Hold the seed as a luminous chartreuse-olive primary against pure white so the color reads as sunlit foliage, pairing it with a deep umber accent for the dry-stone contrast." }, + { id: "seed-176", oklch: [0.300, 0.071, 120.0], + mood: "moss-darkened apothecary jar — herbal, shadowed, mid-19th-century botanical study", + strategy: "Seed is a deep desaturated olive-green that reads as preserved botanical pigment; I anchor it on pure white so the dim moss-green primary feels like ink on a herbarium page, with a warm ochre accent supplying the aged-paper counterpoint." }, + { id: "seed-155", oklch: [0.550, 0.142, 130.0], + mood: "moss-bed forest floor at noon — chlorophyll, lichen, sunlit fern", + strategy: "Seed is a confident mid-olive green with strong chroma; mood is daylight botanical, so I let the brand greens do the work on a pure paper-white bg and pair with a warm umber accent for fern-against-bark contrast." }, + { id: "seed-119", oklch: [0.600, 0.154, 130.0], + mood: "moss garden at Saihō-ji — damp stone, filtered green light through old cedar", + strategy: "Pure near-black bg lets the seed's mossy green glow like wet lichen under low light; accent shifts to a pale ochre-gold like sun catching through canopy." }, + { id: "seed-179", oklch: [0.300, 0.096, 140.0], + mood: "moss on wet stone — forest floor at dusk, deep botanical hush", + strategy: "Kept the seed's deep moss green as primary against a near-black surface so the green reads as living shadow, with a pale lichen accent providing the single point of light." }, + { id: "seed-180", oklch: [0.350, 0.110, 140.0], + mood: "moss-darkened apothecary — herbal tinctures in amber glass, pressed botanicals, the deep green of a conservatory at dusk", + strategy: "Near-black bg with a whisper of green undertone lets the seed's deep moss read as luminous foliage; a warm parchment accent provides the apothecary-label counterpoint without breaking the herbal register." }, + { id: "seed-120", oklch: [0.650, 0.100, 140.0], + mood: "moss on weathered stone — quiet botanical garden conservatory at midday", + strategy: "Pure white bg lets the muted sage-green primary read as a considered botanical mark, with a deeper terracotta accent providing earthen counterpoint without breaking the gallery-like restraint." }, + { id: "seed-121", oklch: [0.750, 0.090, 140.0], + mood: "moss garden at Saihō-ji — diffuse green light filtered through wet stone and lichen", + strategy: "Pure near-black bg lets the muted sage-green primary glow like lichen under low light; a warm pale-bone accent acts as the single ray of sun cutting through canopy." }, + { id: "seed-182", oklch: [0.400, 0.106, 150.0], + mood: "moss garden at Saiho-ji — deep cultivated green under wet stone shadow, contemplative and damp", + strategy: "Near-black bg with the faintest cool-green undertone evokes shaded stone; primary holds the seed's moss tone while accent shifts to a lichen-yellow for organic counterpoint without breaking the hush." }, + { id: "seed-157", oklch: [0.550, 0.145, 150.0], + mood: "moss garden at Saiho-ji — damp stone, filtered green light through cedar canopy", + strategy: "Near-black bg with a faint green undertone evokes deep forest shadow; primary holds the seed's verdant register while accent shifts to a pale lichen-cream to mimic light catching moss." }, + { id: "seed-122", oklch: [0.600, 0.158, 150.0], + mood: "forest floor at first light — moss, lichen, and clean morning air", + strategy: "Seed reads as a living, daylight green; surface stays pure white so the green carries the freshness, with a cool teal accent pulling it toward dew rather than earth." }, + { id: "seed-195", oklch: [0.650, 0.150, 145.0], + mood: "Considered horticulture brand — botanical research lab, the green of a healthy stem photographed in clean daylight", + strategy: "Pure white surface lets the seed's vegetal green carry the entire brand voice, paired with a deep forest ink and a warm clay accent for editorial contrast." }, + { id: "seed-183", oklch: [0.350, 0.077, 160.0], + mood: "moss-stained apothecary — deep forest glass, herbal tinctures shelved in low candlelight", + strategy: "Anchored the seed as primary and built a near-black dark surface with whisper-tinted green to evoke aged apothecary glass, letting the green glow rather than shout." }, + { id: "seed-184", oklch: [0.400, 0.087, 160.0], + mood: "deep forest apothecary — moss, bottle glass, and herbal tincture under afternoon light", + strategy: "Seed becomes a botanical-bottle-green primary on pure white, paired with a warm clove-amber accent to evoke herbal pharmacy contrast without tinting the surface." }, + { id: "seed-158", oklch: [0.550, 0.119, 160.0], + mood: "moss on wet stone — forest floor after rain, mineral and quiet", + strategy: "Pure white surface lets the deep mossy green carry the entire mood; accent shifts to a damp slate-teal to sit beside primary like lichen on stone without competing." }, + { id: "seed-159", oklch: [0.600, 0.130, 160.0], + mood: "moss-covered forest apothecary — herbal tinctures in amber glass, eucalyptus shadow", + strategy: "Anchored the green seed in a near-black backdrop so it reads like botanical glassware lit from within, with a warm amber accent pulled across the wheel to evoke tincture bottles against dark wood." }, + { id: "seed-185", oklch: [0.450, 0.086, 170.0], + mood: "weathered copper patina on a Pacific Northwest greenhouse — oxidized teal, glass light, botanical hush", + strategy: "Seed sits as a deep oxidized-teal primary against pure white so the patina reads as pigment, not atmosphere; a rust-copper accent completes the verdigris/oxidation story across the warm-cool axis." }, + { id: "seed-124", oklch: [0.750, 0.080, 170.0], + mood: "sea-glass on a foggy Pacific shoreline — weathered, mineral, quietly oxidized", + strategy: "Seed is a soft desaturated teal-green; pairing it on pure white lets the mineral primary read as patinated copper-glass, with a deeper kelp-toned primary and a rusted coral accent to spark the muted teal against its complement." }, + { id: "seed-160", oklch: [0.550, 0.095, 180.0], + mood: "weathered copper patina on a museum bronze — oxidized teal, conservatorial quiet", + strategy: "Pure near-black gallery surround lets the patina-teal primary glow like a lit artifact, with a warm verdigris-adjacent accent providing the oxidation contrast against the cool seed." }, + { id: "seed-161", oklch: [0.720, 0.100, 188.0], + mood: "climate-tech dashboard — calm verdigris on plain paper, the quiet confidence of an instrument that just works", + strategy: "Seed teal carries the entire mood as a single considered brand color on pure white, with a desaturated copper accent providing warm signal against the cool primary without competing for attention." }, + { id: "seed-186", oklch: [0.450, 0.074, 200.0], + mood: "deep hydrothermal vent — mineral teal under pressure, the cold blue-green of oxidized copper in submerged light", + strategy: "Near-black surface lets the mineral teal glow as if lit from within; accent shifts toward verdigris-copper to suggest patina on submerged metal, while ink stays cool-neutral to keep the register austere rather than aquatic-cute." }, + { id: "seed-125", oklch: [0.650, 0.100, 200.0], + mood: "climate-tech dashboard — calm operational teal, the color of clean water data and atmospheric sensors", + strategy: "Pure white surface lets a single muted-teal primary do all the brand work, with a deeper marine accent providing hierarchy without competing chroma." }, + { id: "seed-126", oklch: [0.750, 0.080, 200.0], + mood: "climate-tech product brand — quiet competence, dashboards for hard infrastructure problems", + strategy: "Hold the seed's muted teal as primary, pair with a sharper cyan-leaning accent for interactive lift, and let a pure white surface do the disappearing act so the brand reads as a tool, not an atmosphere." }, + { id: "seed-162", oklch: [0.550, 0.091, 210.0], + mood: "weathered nautical instrument — patinated brass on oxidized steel, the cool blue-grey of a ship's chronometer at dawn", + strategy: "Pure white surface lets the muted teal-steel primary read as a precise instrument mark, with a warm brass accent providing the single point of patina against clinical white." }, + { id: "seed-163", oklch: [0.450, 0.086, 230.0], + mood: "deep harbor at dusk — weathered nautical instruments, brass dials on oxidized steel", + strategy: "Near-black background with subtle cool tint evokes the marine dusk; primary holds the seed's teal-blue while a warm brass accent creates the instrument-on-steel tension." }, + { id: "seed-164", oklch: [0.550, 0.105, 230.0], + mood: "deep harbor at dawn — cold steel water, fog-muted light, the quiet before the boats leave", + strategy: "Pure near-black bg lets the seed's cold marine blue read as a luminous beacon, while a pale frost-cyan accent evokes diffused dawn light cutting through fog." }, + { id: "seed-127", oklch: [0.650, 0.100, 230.0], + mood: "climate-tech dashboard — atmospheric sensor blue, calm operational clarity", + strategy: "Anchor the seed as a confident mid-blue primary on pure white so the brand color carries all the atmospheric feeling, with a deep navy accent for hierarchy and a soft slate muted for body text." }, + { id: "seed-128", oklch: [0.750, 0.080, 230.0], + mood: "climate-tech dashboard — calm atmospheric data, considered sky-blue", + strategy: "Pure white surface lets the muted sky-blue primary carry the meteorological calm, with a deep-navy accent providing readable weight against the soft primary." }, + { id: "seed-187", oklch: [0.350, 0.078, 240.0], + mood: "deep harbor at blue hour — wet stone, cold steel, the quiet before night fully lands", + strategy: "Near-black architectural bg with a hint of marine chroma lets the seed read as ambient atmosphere rather than UI chrome; a cooler steel accent sits opposite the warmer-shifted primary for navigational clarity." }, + { id: "seed-077", oklch: [0.578, 0.130, 241.7], + mood: "pre-dawn signal tower — cold blue solitude, instruments glowing against the dark", + strategy: "Pure near-black bg lets the seed's cold tower-light blue glow as the sole emotional source, with a frost-cyan accent acting as a secondary indicator light." }, + { id: "seed-188", oklch: [0.400, 0.110, 250.0], + mood: "Linear's considered indigo — the calm authority of a well-built developer tool, blueprint ink on a clean page", + strategy: "Held the seed as a deep indigo primary against pure white so the brand color carries all the gravity; accent shifts to a cooler, brighter cyan-blue to create a crisp hierarchy pair without warming the surface." }, + { id: "seed-165", oklch: [0.450, 0.123, 250.0], + mood: "blueprint room at dusk — drafting table, graphite, civic-engineering blue", + strategy: "Seed is a mid-deep architectural blue with real chroma and no environmental cue, so I stay out of the way with a pure white surface and let the primary do all the talking, pairing it with a burnt-ochre accent for drafting-pencil contrast." }, + { id: "seed-079", oklch: [0.478, 0.136, 251.8], + mood: "twilight cartography — the blue of deep dusk over open water, precise and navigational", + strategy: "Pure white surface lets the seed's oceanic blue act as a single navigational anchor, with a warm amber accent struck across it like a lighthouse beam at dusk." }, + { id: "seed-080", oklch: [0.541, 0.122, 248.2], + mood: "Linear-style considered tool blue — the calm, exact register of a modern engineering app where every pixel is intentional", + strategy: "Pure white surface lets the considered indigo-blue primary carry the entire brand; a deeper navy accent provides hierarchy without warmth, keeping the palette in a single cool family for that focused-software feel" }, + { id: "seed-166", oklch: [0.550, 0.149, 250.0], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and quietly intense", + strategy: "Near-black bg with the faintest cool tint reads like a darkened cockpit; the seed becomes a luminous instrument-blue primary, paired with a warm amber accent that mimics avionics readouts for unmistakable signal contrast." }, + { id: "seed-081", oklch: [0.650, 0.160, 250.0], + mood: "deep-sea research vessel at dawn — instrument glow against cold steel light", + strategy: "Pure near-white bg keeps the palette technical and instrument-like; the seed blue holds as primary while a desaturated steel-cyan accent reads like signal readouts on glass." }, + { id: "seed-082", oklch: [0.742, 0.140, 247.4], + mood: "high-altitude flight deck at dawn — cold cabin instruments glowing against a sky still holding night", + strategy: "Near-black cockpit ground with a faint blue cast lets the seed read as an illuminated instrument; primary holds the seed, accent shifts to cyan for signal/indicator contrast." }, + { id: "seed-210", oklch: [0.360, 0.140, 260.0], + mood: "Linear-style considered tool indigo — late-night focused work, the deep blue of a code editor at 2am where everything else falls away", + strategy: "Pure black bg lets the indigo primary carry all the cognitive-focus weight, with a slightly brighter periwinkle accent for interactive lift — the surface disappears so the tool feels weightless." }, + { id: "seed-189", oklch: [0.400, 0.130, 260.0], + mood: "pre-dawn observatory — cold instrument blue, star-chart precision", + strategy: "Seed becomes the primary on pure black so the deep instrument-blue glows like a calibration light, with a faint cyan accent reading as starlight against the void." }, + { id: "seed-211", oklch: [0.420, 0.161, 260.0], + mood: "Linear's considered indigo — the tool-for-thought blue of focused product work, calm authority without coldness", + strategy: "Hold the seed as a deep indigo primary against pure white, then pair with a slightly warmer, lighter periwinkle accent to create gentle hue separation without breaking the disciplined tool-brand register." }, + { id: "seed-129", oklch: [0.450, 0.150, 260.0], + mood: "pre-dawn observatory — deep cobalt sky just before astronomical twilight, instruments cool to the touch", + strategy: "Near-black surface lets the cobalt seed read as luminous starlight; a single warm amber accent acts as the calibration lamp against the cold blue field." }, + { id: "seed-084", oklch: [0.476, 0.207, 261.2], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky, precise and awake", + strategy: "Default B black bg lets the cobalt primary read as a luminous instrument signal, with a cyan accent striking the analogous 'cockpit display' relationship." }, + { id: "seed-085", oklch: [0.681, 0.132, 258.4], + mood: "pre-dawn flight deck — instrument glow against deep cobalt sky", + strategy: "Anchored the seed as a luminous primary against a near-black architectural ground, with a warm amber accent acting as the single instrument light cutting through cold blue." }, + { id: "seed-086", oklch: [0.767, 0.106, 255.9], + mood: "Scandinavian winter morning — quiet light through frost, pale sky over snow", + strategy: "Anchored a pure white editorial stage so the seed's cool sky-blue reads as crisp polar light, with a deeper navy primary providing the only saturated weight — like a single dark pine against snow." }, + { id: "seed-083", oklch: [0.340, 0.159, 262.4], + mood: "deep cobalt twilight — the moment after sunset when the sky goes electric blue and city windows start to glow", + strategy: "Pure black stage lets the cobalt seed act as a luminous neon-window glow, with a warm amber accent across the wheel for the lit-window contrast." }, + { id: "seed-212", oklch: [0.360, 0.219, 270.0], + mood: "Linear-grade tooling indigo — considered software for people who care about craft", + strategy: "Anchored the deep indigo seed as primary on a pure white surface so the brand color carries all the weight, with a slightly cooler violet-blue accent for hierarchy without competing chroma." }, + { id: "seed-130", oklch: [0.400, 0.150, 270.0], + mood: "Linear-grade indigo — considered productivity tool, ink on paper, no theatrics", + strategy: "Pure white surface lets a deep cool indigo carry all the brand weight, paired with a slightly warmer violet-blue accent for hierarchy without acid." }, + { id: "seed-213", oklch: [0.411, 0.241, 267.9], + mood: "Linear-style indigo — considered tool surface, the kind of blue-violet that sits behind a developer's keyboard at 11pm without shouting", + strategy: "Pure black canvas lets a saturated indigo primary do all the brand work, with a cooler cyan-violet accent providing UI signal without competing." }, + { id: "seed-131", oklch: [0.450, 0.180, 270.0], + mood: "monastic indigo dusk — vespers light through stained glass, contemplative and severe", + strategy: "Seed becomes a deep indigo primary against pure near-black so the violet reads as luminous stained-glass against architectural shadow, with a cooler iris accent for tonal lift." }, + { id: "seed-088", oklch: [0.476, 0.158, 268.5], + mood: "pre-dawn astronomer's notebook — deep indigo sky just before the stars fade, ink and graphite", + strategy: "Near-black bg with the faintest cool tint to evoke night sky without theatrics; primary holds the seed's indigo, accent shifts to a paler periwinkle for stellar contrast, keeping the palette monochromatic-cool and observational." }, + { id: "seed-196", oklch: [0.530, 0.130, 268.0], + mood: "Linear-style considered tool indigo — the deep-focus blue-violet of a thoughtfully built productivity surface, the color of a well-typeset keyboard shortcut", + strategy: "Pure white bg lets the indigo seed do all the brand work as primary, with a slightly darker, more saturated violet-shifted accent for hierarchy and interactive states — the surface disappears so the brand color reads as the entire identity." }, + { id: "seed-132", oklch: [0.700, 0.120, 270.0], + mood: "Linear-style considered tool indigo — the quiet violet of a focused product workspace, late-afternoon thinking", + strategy: "Pure white surface lets a muted indigo-violet primary and a slightly cooler accent do all the brand work, keeping the register calm and software-like rather than theatrical." }, + { id: "seed-090", oklch: [0.445, 0.206, 279.1], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, not a nightclub", + strategy: "Anchor the seed as a confident product primary on pure white, with a cooler indigo-shift accent that reads as a sibling tool color, so the brand violet does all the emotional work." }, + { id: "seed-133", oklch: [0.500, 0.160, 280.0], + mood: "Linear-adjacent indigo — considered productivity tool, the violet of a thinking workspace", + strategy: "Seed becomes a measured indigo primary on pure white; accent shifts to a cooler blue-violet to create hierarchy without nightclub saturation, letting the brand color do all the emotional work." }, + { id: "seed-094", oklch: [0.533, 0.125, 294.3], + mood: "Linear-style considered tool indigo — the violet of a focused product surface, calm authority for a creative workspace", + strategy: "Pure white canvas lets the indigo-violet primary carry the entire brand voice; accent shifts hue slightly toward blue for a cool, tool-like duotone rather than warm decorative pairing." }, + { id: "seed-137", oklch: [0.700, 0.120, 290.0], + mood: "Linear-adjacent indigo — the considered tool, late-evening focus mode, software made for people who care about craft", + strategy: "Pure black surface lets a single restrained indigo-violet carry the brand, with a cooler periwinkle accent providing UI hierarchy without competing — Vercel/Linear dark-mode discipline." }, + { id: "seed-100", oklch: [0.450, 0.150, 330.0], + mood: "velvet boudoir at last call — bruised orchid and lipstick traces under low lamplight", + strategy: "Pure near-black surface lets a deep magenta-rose primary smolder while a warm peach accent acts like skin-lit lamplight — drama lives in the brand pair, not the room." }, + { id: "seed-103", oklch: [0.650, 0.160, 330.0], + mood: "1980s Memphis boudoir — powder-pink neon humming against lacquered black, lipstick and lacquer", + strategy: "Near-black gallery surface lets the magenta-pink seed read as lit neon; accent shifts to warm coral to create cinematic dichromatic tension without competing chroma." }, + { id: "seed-228", oklch: [0.360, 0.147, 340.0], + mood: "Figma-era creative tool plum — considered productivity software for designers, the inky violet of a serif wordmark on a marketing site", + strategy: "Held the seed as a deep plum primary against pure white so the brand color does the emotional work; paired with a muted rose accent for warmth without breaking the productivity-tool restraint." }, + { id: "seed-107", oklch: [0.500, 0.200, 340.0], + mood: "Figma plum — creative-tool confidence, considered magenta for a modern design product", + strategy: "Pure white surface lets a saturated magenta-plum primary carry all the brand voice, paired with a cooler violet-leaning accent for hierarchy without competing." }, + { id: "seed-198", oklch: [0.600, 0.210, 340.0], + mood: "Figma-era creative tool plum — confident, considered, made for makers", + strategy: "Anchor a saturated plum primary against pure white so the brand color does all the emotional work, with a deeper magenta-rose accent for hierarchy." }, + { id: "seed-112", oklch: [0.754, 0.193, 343.4], + mood: "Figma-era creative tool — confident pink primary doing the brand work on a clean canvas, the way Linear uses indigo or Stripe uses violet", + strategy: "Anchor the seed pink as a saturated brand primary on pure white so the color carries all the personality; pair with a cooler plum accent to give the pink something to push against without competing." }, + { id: "seed-229", oklch: [0.420, 0.163, 350.0], + mood: "considered fintech rose — the deep magenta of a modern product brand (think Stripe-adjacent, but rotated toward berry), confident and current", + strategy: "pure white surface lets a single deep berry-rose primary do all the brand work, paired with a cooler indigo accent for the contrast move you see in modern product marketing" }, + { id: "seed-113", oklch: [0.470, 0.173, 354.8], + mood: "1960s velvet rope nightclub — crushed magenta, low light, cigarette smoke catching a spotlight", + strategy: "Pure black stage so the seed's smoky magenta reads as a single hot spotlight, paired with a cooler violet accent for the second light cue." }, + { id: "seed-114", oklch: [0.570, 0.158, 353.3], + mood: "fin-de-siècle Parisian rose — velvet curtain, theatre program, lipstick blotted on linen", + strategy: "Drop bg to true black so the dusty-rose primary reads as stage-lit silk; accent shifts to a warmer coral-mauve at higher lightness to create gentle hue rotation without breaking the romance." }, + { id: "seed-199", oklch: [0.650, 0.180, 350.0], + mood: "modern fintech rose — the considered pink of a Series B brand mark, confident and current without nostalgia", + strategy: "Pure white surface lets a saturated rose primary do the brand work, paired with a deep plum accent for hierarchy — the Stripe move applied to a pink hue." }, + { id: "seed-115", oklch: [0.636, 0.218, 355.3], + mood: "backstage at a cabaret — velvet rope, lipstick mark on a champagne glass", + strategy: "Seed reads as a saturated stage-light magenta-red; I push it into pure black so the primary glows like a neon sign and the accent (a cold pearl-pink) acts as the spotlight rim — the room is dark, the color does the singing." }, + { id: "seed-230", oklch: [0.650, 0.249, 354.5], + mood: "Modern fintech rose — the considered pink of a contemporary payments brand: confident, alive, and clear-headed", + strategy: "Pure white bg lets a saturated rose-magenta primary carry all the brand energy, paired with a cooler indigo accent for trustworthy contrast — the Stripe move applied to a pink hue." }, + { id: "seed-231", oklch: [0.682, 0.241, 353.2], + mood: "Figma-era creative tool — a confident pink-magenta product brand, the kind a modern design platform uses to feel alive without shouting", + strategy: "Default A pure white bg lets the saturated pink-magenta primary do all the brand work, with a near-complementary cool teal accent for tool-like clarity and a neutral ink for editorial calm" }, + { id: "seed-116", oklch: [0.734, 0.183, 356.8], + mood: "modern beauty brand DTC — Glossier-adjacent pink, confident and current without being saccharine", + 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." }, +]; + +function parseArgs(argv) { + const args = { id: null, from: null }; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a === '--id' && argv[i + 1]) { args.id = argv[++i]; } + else if (a === '--from' && argv[i + 1]) { args.from = argv[++i]; } + } + return args; +} + +// Hash a key into a stable float in [0, 1) for deterministic weighted picks. +function hashUnit(key) { + const h = crypto.createHash('sha256').update(key).digest(); + return h.readUInt32BE(0) / 0x100000000; +} + +// The curated library is hue-skewed (more reds/oranges than teals/magentas) +// because that's where the source material + taste landed. Left uniform, a +// random pick would land on red ~1/3 of the time. Inverse-frequency weighting +// gives each seed a weight of 1/(count in its 30° hue bucket), so each hue +// ZONE is roughly equally likely to be chosen regardless of how many seeds it +// holds — fair rainbow exposure across runs without pruning the library. +function buildWeights(seeds) { + const bucketCount = {}; + const bucketOf = (s) => Math.floor(((s.oklch[2] % 360) + 360) % 360 / 30); + for (const s of seeds) { const b = bucketOf(s); bucketCount[b] = (bucketCount[b] || 0) + 1; } + const weights = seeds.map((s) => 1 / bucketCount[bucketOf(s)]); + const total = weights.reduce((a, b) => a + b, 0); + return { weights, total }; +} + +function weightedPick(seeds, unit) { + const { weights, total } = buildWeights(seeds); + let target = unit * total; + for (let i = 0; i < seeds.length; i++) { + target -= weights[i]; + if (target < 0) return seeds[i]; + } + return seeds[seeds.length - 1]; +} + +function pickSeed(seeds, { id, from }) { + if (id) { + const found = seeds.find(s => s.id === id); + if (!found) { console.error(`no seed with id "${id}"`); process.exit(2); } + return found; + } + const envFrom = process.env.IMPECCABLE_PALETTE_SEED; + const key = from || envFrom; + const unit = key ? hashUnit(key) : Math.random(); + return weightedPick(seeds, unit); +} + +function fmtOklch([L, C, H]) { + return `oklch(${L.toFixed(3)} ${C.toFixed(3)} ${H.toFixed(1)})`; +} + +function hueWord(H) { + if (H < 15 || H >= 345) return 'pure red'; + if (H < 35) return 'warm red / crimson'; + if (H < 55) return 'warm coral / burnt orange'; + if (H < 80) return 'orange / honey'; + if (H < 105) return 'warm amber / honey-gold'; + if (H < 135) return 'yellow-green / olive'; + if (H < 170) return 'green'; + if (H < 200) return 'teal'; + if (H < 230) return 'sky blue'; + if (H < 265) return 'cobalt / indigo'; + if (H < 295) return 'violet / purple'; + if (H < 330) return 'magenta / pink'; + return 'deep pink / rose'; +} + +// --------------------------------------------------------------- + +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}` : ''; + +// --------------------------------------------------------------- +// Fat tool-exit response — what the model sees on stdout. +// --------------------------------------------------------------- + +process.stdout.write(`BRAND SEED · ${seed.id} + +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. + +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. + +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é. + + 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 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. + + 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. + +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): + + - 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. + +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. + +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. +`); diff --git a/skill/scripts/pin.mjs b/skill/scripts/pin.mjs index ba02783fb..320d98ce1 100644 --- a/skill/scripts/pin.mjs +++ b/skill/scripts/pin.mjs @@ -27,7 +27,7 @@ const HARNESS_DIRS = [ // Valid sub-command names const VALID_COMMANDS = [ - 'craft', 'teach', 'extract', 'document', 'shape', + 'craft', 'init', 'extract', 'document', 'shape', 'critique', 'audit', 'polish', 'bolder', 'quieter', 'distill', 'harden', 'onboard', 'live', 'animate', 'colorize', 'typeset', 'layout', 'delight', 'overdrive', diff --git a/tests/build.test.js b/tests/build.test.js index 49ed321a3..1d7d70b9c 100644 --- a/tests/build.test.js +++ b/tests/build.test.js @@ -137,7 +137,7 @@ This is a test skill body.`; const skillDir = path.join(TEST_DIR, 'skill'); fs.mkdirSync(skillDir, { recursive: true }); - fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent); + fs.writeFileSync(path.join(skillDir, 'SKILL.src.md'), skillContent); // Run the build process const DIST_DIR = path.join(TEST_DIR, 'dist'); @@ -186,7 +186,7 @@ Do not redesign the approved crop.`; const skillDir = path.join(TEST_DIR, 'skill'); fs.mkdirSync(path.join(skillDir, 'agents'), { recursive: true }); - fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent); + fs.writeFileSync(path.join(skillDir, 'SKILL.src.md'), skillContent); fs.writeFileSync(path.join(skillDir, 'agents/asset-producer.md'), agentContent); const DIST_DIR = path.join(TEST_DIR, 'dist'); @@ -226,7 +226,7 @@ Please audit {{target}} for technical quality. Ask {{model}} for help.`; const skillDir = path.join(TEST_DIR, 'skill'); fs.mkdirSync(skillDir, { recursive: true }); - fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent); + fs.writeFileSync(path.join(skillDir, 'SKILL.src.md'), skillContent); const DIST_DIR = path.join(TEST_DIR, 'dist'); const { skills } = utils.readSourceFiles(TEST_DIR); diff --git a/tests/context.test.mjs b/tests/context.test.mjs new file mode 100644 index 000000000..0f434105a --- /dev/null +++ b/tests/context.test.mjs @@ -0,0 +1,386 @@ +/** + * Tests for the shared context loader (PRODUCT.md / DESIGN.md resolver). + * Run with: node --test tests/load-context.test.mjs + * + * Covers the resolution order: + * 1. cwd, when canonical files are at the root + * 2. Auto-fallback to .agents/context/ then docs/ + * 3. IMPECCABLE_CONTEXT_DIR env var as a power-user escape hatch (only + * consulted when the default paths come up empty) + * 4. Default to cwd when nothing is found + * + * Each test runs in its own scratch dir under os.tmpdir() so the suite stays + * independent of the project root and parallel-safe. + */ + +import { describe, it, beforeEach, afterEach } from 'node:test'; +import { spawnSync, spawn } from 'node:child_process'; +import http from 'node:http'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import os from 'node:os'; + +import { loadContext, resolveContextDir } from '../skill/scripts/context.mjs'; + +import { fileURLToPath } from 'node:url'; +const SCRIPT_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'skill', 'scripts', 'context.mjs'); + +let scratch; +let savedEnv; + +beforeEach(() => { + scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-loadctx-')); + savedEnv = process.env.IMPECCABLE_CONTEXT_DIR; + delete process.env.IMPECCABLE_CONTEXT_DIR; +}); + +afterEach(() => { + if (savedEnv === undefined) delete process.env.IMPECCABLE_CONTEXT_DIR; + else process.env.IMPECCABLE_CONTEXT_DIR = savedEnv; + fs.rmSync(scratch, { recursive: true, force: true }); +}); + +function write(rel, body = '# placeholder\n') { + const abs = path.join(scratch, rel); + fs.mkdirSync(path.dirname(abs), { recursive: true }); + fs.writeFileSync(abs, body); + return abs; +} + +describe('resolveContextDir', () => { + it('returns cwd when PRODUCT.md is at the root', () => { + write('PRODUCT.md'); + assert.equal(resolveContextDir(scratch), scratch); + }); + + it('returns cwd when DESIGN.md is at the root', () => { + write('DESIGN.md'); + assert.equal(resolveContextDir(scratch), scratch); + }); + + it('falls back to .agents/context/ when root is clean', () => { + write('.agents/context/PRODUCT.md'); + assert.equal(resolveContextDir(scratch), path.join(scratch, '.agents', 'context')); + }); + + it('falls back to docs/ when root is clean and .agents/context/ is empty', () => { + write('docs/PRODUCT.md'); + assert.equal(resolveContextDir(scratch), path.join(scratch, 'docs')); + }); + + it('prefers .agents/context/ over docs/ when both exist', () => { + write('.agents/context/PRODUCT.md'); + write('docs/PRODUCT.md'); + assert.equal(resolveContextDir(scratch), path.join(scratch, '.agents', 'context')); + }); + + it('prefers cwd over fallback dirs when canonical files are at the root', () => { + write('PRODUCT.md'); + write('.agents/context/PRODUCT.md'); + assert.equal(resolveContextDir(scratch), scratch); + }); + + it('uses IMPECCABLE_CONTEXT_DIR as a fallback when defaults are empty (relative path)', () => { + write('design/PRODUCT.md'); + process.env.IMPECCABLE_CONTEXT_DIR = 'design'; + assert.equal(resolveContextDir(scratch), path.join(scratch, 'design')); + }); + + it('uses IMPECCABLE_CONTEXT_DIR as a fallback when defaults are empty (absolute path)', () => { + const elsewhere = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-elsewhere-')); + try { + process.env.IMPECCABLE_CONTEXT_DIR = elsewhere; + assert.equal(resolveContextDir(scratch), elsewhere); + } finally { + fs.rmSync(elsewhere, { recursive: true, force: true }); + } + }); + + it('default paths win over IMPECCABLE_CONTEXT_DIR (lazy escape hatch)', () => { + write('PRODUCT.md', 'root'); + write('design/PRODUCT.md', 'overridden'); + process.env.IMPECCABLE_CONTEXT_DIR = 'design'; + assert.equal(resolveContextDir(scratch), scratch); + }); + + it('ignores empty IMPECCABLE_CONTEXT_DIR', () => { + write('PRODUCT.md'); + process.env.IMPECCABLE_CONTEXT_DIR = ' '; + assert.equal(resolveContextDir(scratch), scratch); + }); + + it('returns cwd when nothing is found anywhere', () => { + assert.equal(resolveContextDir(scratch), scratch); + }); +}); + +describe('loadContext', () => { + it('reads PRODUCT.md and DESIGN.md from the root', () => { + write('PRODUCT.md', '# product content\n'); + write('DESIGN.md', '# design content\n'); + const ctx = loadContext(scratch); + assert.equal(ctx.hasProduct, true); + assert.equal(ctx.hasDesign, true); + assert.match(ctx.product, /product content/); + assert.match(ctx.design, /design content/); + assert.equal(ctx.productPath, 'PRODUCT.md'); + assert.equal(ctx.designPath, 'DESIGN.md'); + assert.equal(ctx.contextDir, scratch); + }); + + it('reads from .agents/context/ when the root is clean', () => { + write('.agents/context/PRODUCT.md', '# product in agents\n'); + write('.agents/context/DESIGN.md', '# design in agents\n'); + const ctx = loadContext(scratch); + assert.equal(ctx.hasProduct, true); + assert.equal(ctx.hasDesign, true); + assert.match(ctx.product, /product in agents/); + assert.equal(ctx.contextDir, path.join(scratch, '.agents', 'context')); + // productPath/designPath are relative to cwd, not contextDir + assert.equal(ctx.productPath, path.join('.agents', 'context', 'PRODUCT.md')); + assert.equal(ctx.designPath, path.join('.agents', 'context', 'DESIGN.md')); + }); + + it('reads from docs/ when .agents/context/ is empty', () => { + write('docs/PRODUCT.md', '# product in docs\n'); + const ctx = loadContext(scratch); + assert.equal(ctx.hasProduct, true); + assert.equal(ctx.contextDir, path.join(scratch, 'docs')); + assert.equal(ctx.productPath, path.join('docs', 'PRODUCT.md')); + }); +}); + +describe('loadContext (IMPECCABLE_CONTEXT_DIR escape hatch)', () => { + it('reads from the override path when defaults are empty', () => { + write('design/PRODUCT.md', '# overridden product\n'); + write('design/DESIGN.md', '# overridden design\n'); + process.env.IMPECCABLE_CONTEXT_DIR = 'design'; + const ctx = loadContext(scratch); + assert.equal(ctx.hasProduct, true); + assert.equal(ctx.hasDesign, true); + assert.match(ctx.product, /overridden product/); + assert.equal(ctx.contextDir, path.join(scratch, 'design')); + }); + + it('does not override defaults when both exist (lazy escape hatch)', () => { + write('PRODUCT.md', '# root product\n'); + write('design/PRODUCT.md', '# overridden product\n'); + process.env.IMPECCABLE_CONTEXT_DIR = 'design'; + const ctx = loadContext(scratch); + assert.match(ctx.product, /root product/); + assert.equal(ctx.contextDir, scratch); + }); + + it('reports a missing override directory as no-context, not as a crash', () => { + process.env.IMPECCABLE_CONTEXT_DIR = 'no/such/dir'; + const ctx = loadContext(scratch); + assert.equal(ctx.hasProduct, false); + assert.equal(ctx.hasDesign, false); + assert.equal(ctx.product, null); + assert.equal(ctx.design, null); + assert.equal(ctx.contextDir, path.resolve(scratch, 'no/such/dir')); + }); +}); + +describe('context.mjs CLI', () => { + it('emits NO_PRODUCT_MD directive when no PRODUCT.md is found', async () => { + const { spawnSync } = await import('node:child_process'); + const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } }); + assert.equal(res.status, 0); + assert.match(res.stdout, /^NO_PRODUCT_MD:/); + assert.match(res.stdout, /reference\/init\.md/); + }); + + it('prints a PRODUCT.md markdown block when only PRODUCT.md exists', async () => { + write('PRODUCT.md', '# Acme\n\nbody\n'); + const { spawnSync } = await import('node:child_process'); + const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } }); + assert.equal(res.status, 0); + assert.match(res.stdout, /^# PRODUCT\.md/); + assert.match(res.stdout, /# Acme/); + assert.equal(res.stdout.includes('# DESIGN.md'), false); + // The NEXT STEP directive is always appended after `---`. + assert.match(res.stdout, /\n---\n\nNEXT STEP:/); + }); + + it('concatenates PRODUCT.md and DESIGN.md with a --- separator', async () => { + write('PRODUCT.md', '# Acme product\n'); + write('DESIGN.md', '# Acme design\n'); + const { spawnSync } = await import('node:child_process'); + const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } }); + assert.equal(res.status, 0); + assert.match(res.stdout, /^# PRODUCT\.md/); + assert.match(res.stdout, /\n---\n/); + assert.match(res.stdout, /# DESIGN\.md\n\n# Acme design/); + assert.match(res.stdout, /NEXT STEP:/); + }); + + it('reads from a fallback dir when cwd is clean', async () => { + write('.agents/context/PRODUCT.md', '# fallback product\n'); + const { spawnSync } = await import('node:child_process'); + const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } }); + assert.equal(res.status, 0); + assert.match(res.stdout, /^# PRODUCT\.md/); + assert.match(res.stdout, /# fallback product/); + }); + + it('names the register-specific reference when PRODUCT.md declares one', async () => { + write('PRODUCT.md', '# Acme\n\n## Register\n\nbrand\n'); + const { spawnSync } = await import('node:child_process'); + const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } }); + assert.equal(res.status, 0); + assert.match(res.stdout, /NEXT STEP: This project's register is `brand`\./); + assert.match(res.stdout, /read `reference\/brand\.md`/); + }); + + it('falls back to a generic register directive when no register field is present', async () => { + write('PRODUCT.md', '# Acme\n\n(no register field)\n'); + const { spawnSync } = await import('node:child_process'); + const res = spawnSync(process.execPath, [SCRIPT_PATH], { cwd: scratch, encoding: 'utf8', env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' } }); + assert.equal(res.status, 0); + assert.match(res.stdout, /NEXT STEP: You MUST now read the matching register reference/); + assert.match(res.stdout, /reference\/brand\.md.*reference\/product\.md/); + }); +}); + +describe('context.mjs update check', () => { + // The script reads its own version from a sibling SKILL.md (resolved via + // import.meta.url, not cwd). The source tree has no SKILL.md, so we copy the + // script into a scratch skill dir with a controlled version and run that. + // Local version is pinned to 1.0.0; "newer" = 2.0.0, "older" = 0.0.1. + const LOCAL_VERSION = '1.0.0'; + + const cachePath = () => path.join(scratch, 'update-check.json'); + + function setup(cacheObj, { disable = false, host } = {}) { + const skillScript = path.join(scratch, 'skill', 'scripts', 'context.mjs'); + fs.mkdirSync(path.dirname(skillScript), { recursive: true }); + fs.copyFileSync(SCRIPT_PATH, skillScript); + fs.writeFileSync( + path.join(scratch, 'skill', 'SKILL.md'), + `---\nname: impeccable\nversion: ${LOCAL_VERSION}\n---\n\nbody\n`, + ); + fs.writeFileSync(cachePath(), JSON.stringify(cacheObj)); + const project = path.join(scratch, 'project'); + fs.mkdirSync(project, { recursive: true }); + fs.writeFileSync(path.join(project, 'PRODUCT.md'), '# Acme\n'); + const env = { + ...process.env, + IMPECCABLE_UPDATE_CACHE: cachePath(), + IMPECCABLE_NO_UPDATE_CHECK: disable ? '1' : '', + ...(host ? { IMPECCABLE_UPDATE_HOST: host } : {}), + }; + return { skillScript, project, env }; + } + + // A fresh cache (lastCheck = now) skips the network poll, so cache-driven + // tests stay synchronous and hermetic. + function run(cacheObj, opts) { + const { skillScript, project, env } = setup(cacheObj, opts); + return spawnSync(process.execPath, [skillScript], { cwd: project, encoding: 'utf8', env }); + } + + // Async variant for the live-fetch tests: the stub server runs in THIS + // process, so the runner must not block the event loop (spawnSync would + // deadlock the loopback connection). spawn keeps the loop serving. + function runAsync(cacheObj, opts) { + const { skillScript, project, env } = setup(cacheObj, opts); + return new Promise((resolve) => { + const proc = spawn(process.execPath, [skillScript], { cwd: project, env }); + let stdout = ''; + proc.stdout.on('data', (d) => (stdout += d.toString())); + proc.on('exit', (status) => resolve({ status, stdout })); + }); + } + + function readCache() { + return JSON.parse(fs.readFileSync(cachePath(), 'utf8')); + } + + it('appends UPDATE_AVAILABLE when the cached latest version is newer', () => { + const res = run({ lastCheck: Date.now(), latestVersion: '2.0.0' }); + assert.equal(res.status, 0); + assert.match(res.stdout, /UPDATE_AVAILABLE: A newer Impeccable skill is available/); + assert.match(res.stdout, /installed v1\.0\.0, latest v2\.0\.0/); + assert.match(res.stdout, /npx impeccable skills update/); + // It must come after the real context, never replace it. + assert.match(res.stdout, /^# PRODUCT\.md/); + }); + + it('stays silent when the cached latest version is not newer', () => { + const res = run({ lastCheck: Date.now(), latestVersion: '0.0.1' }); + assert.equal(res.status, 0); + assert.equal(res.stdout.includes('UPDATE_AVAILABLE'), false); + }); + + it('does not re-surface a version notified within the last week', () => { + const res = run({ + lastCheck: Date.now(), + latestVersion: '2.0.0', + notifiedVersion: '2.0.0', + notifiedAt: Date.now(), + }); + assert.equal(res.status, 0); + assert.equal(res.stdout.includes('UPDATE_AVAILABLE'), false); + }); + + it('respects IMPECCABLE_NO_UPDATE_CHECK', () => { + const res = run({ lastCheck: Date.now(), latestVersion: '2.0.0' }, { disable: true }); + assert.equal(res.status, 0); + assert.equal(res.stdout.includes('UPDATE_AVAILABLE'), false); + }); + + // ─── live fetch path (against a localhost stub, never the real site) ────── + function startStub(body, { status = 200 } = {}) { + return new Promise((resolve) => { + const srv = http.createServer((req, res) => { + res.statusCode = status; + res.setHeader('content-type', 'application/json'); + res.end(typeof body === 'string' ? body : JSON.stringify(body)); + }); + srv.listen(0, '127.0.0.1', () => resolve({ srv, host: `http://127.0.0.1:${srv.address().port}` })); + }); + } + + it('polls /api/version over the network and caches a newer version', async () => { + const { srv, host } = await startStub({ skills: '2.0.0' }); + try { + const res = await runAsync({}, { host }); // empty cache forces the poll + assert.equal(res.status, 0); + assert.match(res.stdout, /UPDATE_AVAILABLE/); + assert.match(res.stdout, /installed v1\.0\.0, latest v2\.0\.0/); + const cache = readCache(); + assert.equal(cache.latestVersion, '2.0.0'); + assert.equal(typeof cache.lastCheck, 'number'); + } finally { + srv.close(); + } + }); + + it('stays silent when the network reports a same-or-older version', async () => { + const { srv, host } = await startStub({ skills: '1.0.0' }); + try { + const res = await runAsync({}, { host }); + assert.equal(res.status, 0); + assert.equal(res.stdout.includes('UPDATE_AVAILABLE'), false); + // The poll still happened, so lastCheck is stamped to throttle the next. + assert.equal(typeof readCache().lastCheck, 'number'); + } finally { + srv.close(); + } + }); + + it('fails silent and stamps lastCheck when the endpoint is unreachable', async () => { + // Bind then immediately close to obtain a port nothing is listening on. + const { srv, host } = await startStub({ skills: '2.0.0' }); + await new Promise((r) => srv.close(r)); + const res = run({}, { host }); + assert.equal(res.status, 0); + assert.equal(res.stdout.includes('UPDATE_AVAILABLE'), false); + assert.match(res.stdout, /^# PRODUCT\.md/); // core output is unaffected + const cache = readCache(); + assert.equal(typeof cache.lastCheck, 'number'); // stamped so we don't re-poll every boot + assert.equal(cache.latestVersion, undefined); // nothing learned + }); +}); diff --git a/tests/design-parser.test.mjs b/tests/design-parser.test.mjs index fbc62ba32..027454660 100644 --- a/tests/design-parser.test.mjs +++ b/tests/design-parser.test.mjs @@ -102,4 +102,22 @@ Prose. assert.equal(model.frontmatter.colors.primary, '#b8422e'); assert.equal(model.frontmatter.colors.accent, '#ec4899'); }); + + it('strips inline comments after quoted OKLCH values', () => { + const md = `--- +colors: + kinpaku-gold: "oklch(84% 0.19 80.46)" # primary accent + gold-hairline: "oklch(58% 0.065 82 / 0.32)" # default rule +--- + +# Design System: Kinpaku + +## 1. Overview + +Prose. +`; + const model = parseDesignMd(md); + assert.equal(model.frontmatter.colors['kinpaku-gold'], 'oklch(84% 0.19 80.46)'); + assert.equal(model.frontmatter.colors['gold-hairline'], 'oklch(58% 0.065 82 / 0.32)'); + }); }); diff --git a/tests/detect-antipatterns-browser.test.mjs b/tests/detect-antipatterns-browser.test.mjs index 68030e090..bc44898fa 100644 --- a/tests/detect-antipatterns-browser.test.mjs +++ b/tests/detect-antipatterns-browser.test.mjs @@ -150,6 +150,27 @@ describe('detectUrl — browser-only fixtures', () => { assert.equal(edges.length, 3, `expected 3 body-text-viewport-edge findings, got ${edges.length}: ${JSON.stringify(edges.map(e => e.snippet))}`); }); + it('text-overflow: flags content wider than its box, skips real scroll regions', async () => { + // Browser-only: needs scrollWidth vs clientWidth from real layout. + // Flag column: a nowrap line and an unbreakable token spilling past a + // fixed-width box (overflow visible). Pass column: a genuine + // overflow-x:auto scroll region, a
, normally wrapping text, and a
+    // long line living inside a scroll ancestor.
+    const f = await detectUrl(`${baseUrl}/fixtures/antipatterns/text-overflow.html`);
+    const hits = f.filter(r => r.antipattern === 'text-overflow');
+    const flagged = new Set();
+    for (const r of hits) {
+      const m = (r.snippet || '').match(/\.(flag-[\w-]+|pass-[\w-]+)/);
+      if (m) flagged.add(m[1]);
+    }
+    assert.ok(flagged.has('flag-nowrap'), 'expected the nowrap overflow case to flag');
+    assert.ok(flagged.has('flag-longword'), 'expected the unbreakable-token overflow case to flag');
+    for (const cls of ['pass-scroll', 'pass-pre', 'pass-wrap', 'pass-inside-scroll']) {
+      assert.ok(!flagged.has(cls), `".${cls}" should NOT be flagged as text-overflow`);
+    }
+    assert.equal(hits.length, 2, `expected exactly 2 text-overflow findings, got ${hits.length}: ${JSON.stringify(hits.map(h => h.snippet))}`);
+  });
+
   it('visual contrast: browser fallback catches low contrast on image backgrounds', async () => {
     const analyticOnly = await detectUrl(`${baseUrl}/fixtures/antipatterns/visual-contrast.html`, {
       waitUntil: 'load',
diff --git a/tests/detect-antipatterns-fixtures.test.mjs b/tests/detect-antipatterns-fixtures.test.mjs
index 853e43e38..ca2e7336c 100644
--- a/tests/detect-antipatterns-fixtures.test.mjs
+++ b/tests/detect-antipatterns-fixtures.test.mjs
@@ -63,16 +63,16 @@ describe('detectHtml — static HTML/CSS fixtures', () => {
 
   it('color: flag column triggers all color rules, pass column adds none', async () => {
     const f = await detectHtml(path.join(FIXTURES, 'color.html'));
-    // All five color rules must fire from the flag column
-    assert.ok(f.some(r => r.antipattern === 'pure-black-white'), 'expected pure-black-white');
+    // pure-black-white was removed from the skill in v3.2; only the remaining rules
+    // are expected to fire from the flag column.
     assert.ok(f.some(r => r.antipattern === 'gray-on-color'), 'expected gray-on-color');
     assert.ok(f.some(r => r.antipattern === 'low-contrast'), 'expected low-contrast');
     assert.ok(f.some(r => r.antipattern === 'gradient-text'), 'expected gradient-text');
     assert.ok(f.some(r => r.antipattern === 'ai-color-palette'), 'expected ai-color-palette');
     assert.equal(
-      f.some(r => r.antipattern === 'pure-black-white' && /#ffffff|#fff/i.test(r.snippet || '')),
+      f.some(r => r.antipattern === 'pure-black-white'),
       false,
-      'pure white surfaces with dark text should remain allowed',
+      'pure-black-white detector was removed in v3.2',
     );
     // Gradient-bg + gray text case (added with the gradient-fix patch)
     assert.ok(
@@ -100,26 +100,6 @@ describe('detectHtml — static HTML/CSS fixtures', () => {
     );
   });
 
-  it('color: Tailwind bg-black/N opacity modifiers are not flagged as pure-black-white', async () => {
-    const f = await detectHtml(path.join(FIXTURES, 'color.html'));
-    // The pass column has bg-black/3, hover:bg-black/5, bg-black/50 — none are pure black.
-    // Only the flag column's literal bg-black class should trigger pure-black-white.
-    const pureBlackFindings = f.filter(r => r.antipattern === 'pure-black-white');
-    const opacityFalsePositives = pureBlackFindings.filter(r =>
-      (r.snippet || '').includes('bg-black') &&
-      f.some(() => true) // check that bg-black/N class triggers are absent
-    );
-    // There should be exactly the flag-column hits (bg-black class + #000000 inline)
-    // and zero from the pass-column opacity variants.
-    // The pass-column elements have data-test attributes starting with "bg-black-"
-    // The Tailwind class check produces snippet "bg-black" — count those.
-    const twSnippets = pureBlackFindings.filter(r => (r.snippet || '') === 'bg-black');
-    assert.equal(
-      twSnippets.length, 1,
-      `expected exactly 1 Tailwind bg-black finding (flag column only), got ${twSnippets.length}: ${twSnippets.map(r => r.snippet).join('; ')}`
-    );
-  });
-
   it('color: styled  and 
+        
    Dropdown that gets clipped by the hidden ancestor
+
+
+ + Tooltip clipped by overflow clip +
+ + + +
+
+

Hidden container with only static content.

+
+
+ + Positioned child can escape because overflow is visible. +
+
+ + A genuine scroll region is allowed to contain positioned children. +
+
+
+ + diff --git a/tests/fixtures/antipatterns/cream-palette-tailwind.html b/tests/fixtures/antipatterns/cream-palette-tailwind.html new file mode 100644 index 000000000..f4e36465c --- /dev/null +++ b/tests/fixtures/antipatterns/cream-palette-tailwind.html @@ -0,0 +1,11 @@ + + + + + Cream palette (Tailwind) fixture + + +

A warm cream surface via Tailwind

+

The background comes from a utility class (bg-amber-50), not inline CSS, so the static engine never resolves it to a computed color. The class-list fallback should still catch it.

+ + diff --git a/tests/fixtures/antipatterns/cream-palette.html b/tests/fixtures/antipatterns/cream-palette.html new file mode 100644 index 000000000..97daf0299 --- /dev/null +++ b/tests/fixtures/antipatterns/cream-palette.html @@ -0,0 +1,16 @@ + + + + + Cream palette fixture + + + +

A warm cream surface

+

The whole page sits on the default tasteful off-white that every generated landing page reaches for. The background is the tell, regardless of what sits on top of it.

+ + diff --git a/tests/fixtures/antipatterns/extreme-negative-tracking.html b/tests/fixtures/antipatterns/extreme-negative-tracking.html new file mode 100644 index 000000000..67bd6b465 --- /dev/null +++ b/tests/fixtures/antipatterns/extreme-negative-tracking.html @@ -0,0 +1,29 @@ + + + + + Extreme negative tracking fixture + + + +
+ +
+

Tracking crushed em paragraph copy that should still read as a sentence.

+

Tracking crushed pixels heading line

+

Tracking crushed heading variant text

+
+ + +
+

Optical tighten display

+

Plain paragraph copy with default tracking that reads comfortably.

+
+
+ + diff --git a/tests/fixtures/antipatterns/flush-against-border.html b/tests/fixtures/antipatterns/flush-against-border.html new file mode 100644 index 000000000..57bacb538 --- /dev/null +++ b/tests/fixtures/antipatterns/flush-against-border.html @@ -0,0 +1,353 @@ + + + + + + Flush Against Border; Should Flag vs Should Pass + + + +
+ + +
+

Should flag

+ +

Border + zero side-padding

+
+ section: full border, padding 28px 0 0; label and pills touch the left/right border lines. +
+ Supported dev servers +
    +
  • Vite
  • +
  • Next.js
  • +
  • SvelteKit
  • +
  • Astro
  • +
+
+
+ +

All sides flush against a border

+
+ card: 1px border on all sides, padding 0; heading and body touch every side. +
+

Card flush against the borders

+

Body copy sits flush against every visible border line.

+
+
+ +

Background color, zero padding

+
+ no border, but a non-transparent bg; text flush against the bg's edge counts too. +
+

Title flush against the bg edge

+

Body copy with zero padding around a colored fill.

+
+
+ +

Outline (not border), zero padding

+
+ outline draws outside the box; content at the box edge sits next to the outline. +
+

Title flush against the outline

+

The outline counts as a visible boundary too.

+
+
+ +

Left-side flush, other sides fine

+
+ card: padding 12px 16px 12px 0; text touches the left border, other sides are inset. +
+

Title flush only on the left

+

Padding-left zero with a full border.

+
+
+
+ + +
+

Should pass

+ +

No boundary at all

+
+ no bg, no border, no outline; zero padding is fine. +
+ Supported dev servers +
    +
  • Vite
  • +
  • Next.js
  • +
  • SvelteKit
  • +
+
+
+ +

Top-only border, no side borders

+
+ border-top only, padding-top 24px; no left/right border to be flush against. +
+ Supported dev servers +
    +
  • Vite
  • +
  • Next.js
  • +
  • SvelteKit
  • +
+
+
+ +

Bordered card with proper inset

+
+ card: 1px border + padding 18px 20px; comfortable inset on every side. +
+

Card with proper inset

+

Body copy sits comfortably away from the borders.

+
+
+ +

Background card with proper inset

+
+ non-transparent bg + padding 14px 18px; content has room to breathe. +
+

Title with inset on bg

+

Body copy is comfortably inset from the bg edge.

+
+
+ +

Outline with proper inset

+
+ outline + padding 14px 18px; content is comfortably inside. +
+

Title with inset and outline

+

Outline boundary with proper inset works.

+
+
+ +

Image-only container, no text

+
+ card: padding 0, border all sides; no text means nothing can be flush. +
+
+
+
+
+
+ + + diff --git a/tests/fixtures/antipatterns/gemini-tells.html b/tests/fixtures/antipatterns/gemini-tells.html new file mode 100644 index 000000000..593fd7d0e --- /dev/null +++ b/tests/fixtures/antipatterns/gemini-tells.html @@ -0,0 +1,33 @@ + + + + + Gemini provider-tell fixture (gated --gemini) + + + +
+ +
+
+ CSS hover-zoom image +
+ Tailwind hover-zoom image +
+ + +
+ Static image with no hover transform +
A card that lifts on hover, which is fine because it is not an image.
+
+
+ + diff --git a/tests/fixtures/antipatterns/gpt-tells.html b/tests/fixtures/antipatterns/gpt-tells.html new file mode 100644 index 000000000..121193c9c --- /dev/null +++ b/tests/fixtures/antipatterns/gpt-tells.html @@ -0,0 +1,35 @@ + + + + + GPT provider-tell fixture (gated --gpt) + + + +
+ +
+
+ Thin hairline border paired with a wide diffuse shadow. +
+
+

We retired the growth theater and shipped something that actually works.

+
+ + +
+
+ Hairline border with a tight, purposeful shadow. +
+
+

We measured real outcomes for the people who use this every day.

+
+
+ + diff --git a/tests/fixtures/antipatterns/oversized-h1.html b/tests/fixtures/antipatterns/oversized-h1.html new file mode 100644 index 000000000..d8e09aa1a --- /dev/null +++ b/tests/fixtures/antipatterns/oversized-h1.html @@ -0,0 +1,29 @@ + + + + + Oversized H1 fixture + + + +
+ +
+

A sweeping product headline that runs on far too long to sit at this size

+

Everything your whole team needs to launch quickly and finally rest at night

+
+ + +
+

Bold.

+

Ship faster

+

A perfectly ordinary headline that is reasonably long but set at a sane, readable size

+
+
+ + diff --git a/tests/fixtures/antipatterns/text-overflow.html b/tests/fixtures/antipatterns/text-overflow.html new file mode 100644 index 000000000..728d3a697 --- /dev/null +++ b/tests/fixtures/antipatterns/text-overflow.html @@ -0,0 +1,30 @@ + + + + + Text overflow fixture (browser-only) + + + +
+ +
+
A single long line of running text that refuses to wrap and clearly spills past its fixed-width box.
+
Supercalifragilisticexpialidocioussupercalifragilisticexpialidocious
+
+ + +
+
The very same unwrapping line, but inside a genuine horizontal scroll region meant to scroll.
+
a-very-long-preformatted-code-line-that-intentionally-extends-beyond-the-box-width
+
Ordinary wrapping paragraph text that stays comfortably within its container.
+
A long line living inside a scroll ancestor, which must not be flagged.
+
+
+ + diff --git a/tests/framework-fixtures/README.md b/tests/framework-fixtures/README.md index 6941b1ba1..df0aa27a8 100644 --- a/tests/framework-fixtures/README.md +++ b/tests/framework-fixtures/README.md @@ -44,6 +44,14 @@ Fixtures can also opt into a **runtime E2E** pass that actually installs depende "readyPattern": "Local:\\s+https?://[^:]+:(\\d+)", "readyTimeoutMs": 120000, "pickSelector": "h1.hero-title", + "mode": "insert", + "insert": { + "anchorSelector": "section#features", + "position": "after", + "prompt": "Add a testimonial strip below features", + "expectSelector": ".inserted-strip", + "assertAnchorContains": "feature-grid" + }, "preActions": [ { "type": "click", "selector": "[data-testid='open-modal']" }, { "type": "goto", "path": "/about" } @@ -52,6 +60,10 @@ Fixtures can also opt into a **runtime E2E** pass that actually installs depende "preActions": [{ "type": "click", "selector": "[data-testid='open-modal']" }], "expectSelector": "h1.hero-title" }, + "steer": { + "message": "steer-e2e mark hero", + "expectSelector": "h1.hero-title[data-impeccable-steer=\"e2e\"]" + }, "probe": { "expectLiveInit": true, "expectConsoleClean": true @@ -69,7 +81,22 @@ The `runtime` block is optional. Fixtures without it only run the static unit ch 3. Starts `live-server.mjs --background` and runs `live-inject.mjs --port` against it. 4. Spawns `runtime.devCommand` and scrapes the port from stdout using `runtime.readyPattern` (the first capture group must be the port). 5. Opens Playwright Chromium at the dev URL and asserts `window.__IMPECCABLE_LIVE_INIT__ === true` (the browser-side handshake oracle) within `runtime.readyTimeoutMs`. -6. Tears everything down (Playwright close, dev server SIGTERM, live-server stop, tmp rm). +6. Runs a **Steer smoke** step (unless `runtime.steer === false`): submit a message in the global Steer bar, wait for the fake agent to reply `steer_done`, assert the bar unlocks and a `data-impeccable-steer` marker lands in source + DOM. Then continues with pick → Go → cycle → accept. +7. Tears everything down (Playwright close, dev server SIGTERM, live-server stop, tmp rm). + +Optional `runtime.steer` fields: + +```json +"steer": { + "message": "steer-e2e mark hero", + "sourceFile": "src/routes/About.jsx", + "expectSelector": "h1.hero-title[data-impeccable-steer=\"e2e\"]", + "expectSourceContains": "data-impeccable-steer=\"e2e\"", + "preActions": [{ "type": "click", "selector": "[data-testid='nav-about']" }] +} +``` + +When `preActions` is omitted, steer smoke inherits `runtime.preActions` to reveal hidden heroes before the DOM check. Source is asserted first; a reload + retry covers HMR lag. Set `"steer": false` to skip, or `"expectDom": false` for source-only verification. ## Current fixtures diff --git a/tests/framework-fixtures/astro-vite7/fixture.json b/tests/framework-fixtures/astro-vite7/fixture.json index 977d0b8cd..3f559492e 100644 --- a/tests/framework-fixtures/astro-vite7/fixture.json +++ b/tests/framework-fixtures/astro-vite7/fixture.json @@ -23,6 +23,9 @@ "probe": { "expectLiveInit": true, "expectConsoleClean": true + }, + "steer": { + "sourceFile": "src/pages/index.astro" } } } diff --git a/tests/framework-fixtures/vite8-react-insert/files/index.html b/tests/framework-fixtures/vite8-react-insert/files/index.html new file mode 100644 index 000000000..d81098c94 --- /dev/null +++ b/tests/framework-fixtures/vite8-react-insert/files/index.html @@ -0,0 +1,11 @@ + + + + + Vite 8 + React Fixture + + +
+ + + diff --git a/tests/framework-fixtures/vite8-react-insert/files/package.json b/tests/framework-fixtures/vite8-react-insert/files/package.json new file mode 100644 index 000000000..d09089d66 --- /dev/null +++ b/tests/framework-fixtures/vite8-react-insert/files/package.json @@ -0,0 +1,19 @@ +{ + "name": "vite8-react-plain-fixture", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --host 127.0.0.1", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@vitejs/plugin-react": "^6.0.0", + "vite": "^8.0.0" + } +} diff --git a/tests/framework-fixtures/vite8-react-insert/files/src/App.jsx b/tests/framework-fixtures/vite8-react-insert/files/src/App.jsx new file mode 100644 index 000000000..536af8f0e --- /dev/null +++ b/tests/framework-fixtures/vite8-react-insert/files/src/App.jsx @@ -0,0 +1,16 @@ +export default function App() { + return ( +
+

Vite 8 Fixture

+

Minimal React tree for live-mode E2E tests.

+
+ + +
+
+
One
+
Two
+
+
+ ); +} diff --git a/tests/framework-fixtures/vite8-react-insert/files/src/main.jsx b/tests/framework-fixtures/vite8-react-insert/files/src/main.jsx new file mode 100644 index 000000000..f2baba283 --- /dev/null +++ b/tests/framework-fixtures/vite8-react-insert/files/src/main.jsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App.jsx'; +import './styles.css'; + +createRoot(document.getElementById('root')).render( + + + , +); diff --git a/tests/framework-fixtures/vite8-react-insert/files/src/styles.css b/tests/framework-fixtures/vite8-react-insert/files/src/styles.css new file mode 100644 index 000000000..c3bac0ccf --- /dev/null +++ b/tests/framework-fixtures/vite8-react-insert/files/src/styles.css @@ -0,0 +1,15 @@ +body { margin: 0; font-family: system-ui, sans-serif; } +.page { padding: 2rem; } +.hero-title { font-size: 2rem; } +.hero-hook { color: #555; } +.input-row { display: flex; gap: 1rem; margin: 1.5rem 0; } +.demo-input { + flex: 1; + min-height: 44px; + padding: 0.75rem 1rem; + font-size: 1rem; + border: 1px solid #ddd; + border-radius: 0.5rem; +} +.feature-grid { display: grid; gap: 1rem; grid-template-columns: repeat(2, 1fr); } +.feature-card { padding: 1rem; border: 1px solid #ddd; border-radius: 0.5rem; } diff --git a/tests/framework-fixtures/vite8-react-insert/files/vite.config.js b/tests/framework-fixtures/vite8-react-insert/files/vite.config.js new file mode 100644 index 000000000..dd5cfa6f0 --- /dev/null +++ b/tests/framework-fixtures/vite8-react-insert/files/vite.config.js @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { + host: '127.0.0.1', + strictPort: false, + }, +}); diff --git a/tests/framework-fixtures/vite8-react-insert/fixture.json b/tests/framework-fixtures/vite8-react-insert/fixture.json new file mode 100644 index 000000000..b91dada73 --- /dev/null +++ b/tests/framework-fixtures/vite8-react-insert/fixture.json @@ -0,0 +1,35 @@ +{ + "name": "Vite 8 + React + insert mode E2E", + "config": { + "files": ["index.html"], + "insertBefore": "", + "commentSyntax": "html" + }, + "sourceFiles": ["index.html", "src/App.jsx", "src/main.jsx", "src/styles.css", "vite.config.js"], + "generatedFiles": [], + "wrapCases": [], + "runtime": { + "mode": "insert", + "styling": "plain-css", + "install": ["npm", "install", "--no-audit", "--no-fund", "--loglevel=error"], + "devCommand": ["npx", "vite", "--host", "127.0.0.1"], + "readyPattern": "Local:\\s+https?://[^:]+:(\\d+)", + "readyTimeoutMs": 120000, + "steer": false, + "insert": { + "anchorSelector": "section#features", + "position": "after", + "prompt": "Add a testimonial strip below features", + "expectSelector": ".inserted-strip", + "assertAnchorContains": "feature-grid" + }, + "probe": { + "expectLiveInit": true, + "expectConsoleClean": true + }, + "assertSourceContains": [ + "Vite 8 Fixture", + "feature-grid" + ] + } +} diff --git a/tests/framework-fixtures/vite8-react-insert/gitignore.txt b/tests/framework-fixtures/vite8-react-insert/gitignore.txt new file mode 100644 index 000000000..8cda9ad20 --- /dev/null +++ b/tests/framework-fixtures/vite8-react-insert/gitignore.txt @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.vite/ +package-lock.json diff --git a/tests/framework-fixtures/vite8-react-plain/fixture.json b/tests/framework-fixtures/vite8-react-plain/fixture.json index 9a7147d12..42dc1f5ab 100644 --- a/tests/framework-fixtures/vite8-react-plain/fixture.json +++ b/tests/framework-fixtures/vite8-react-plain/fixture.json @@ -23,6 +23,12 @@ "probe": { "expectLiveInit": true, "expectConsoleClean": true + }, + "steer": { + "message": "steer-e2e mark hero", + "expectSelector": "h1.hero-title[data-impeccable-steer=\"e2e\"]", + "expectSourceContains": "data-impeccable-steer=\"e2e\"", + "sourceFile": "src/App.jsx" } } } diff --git a/tests/framework-fixtures/vite8-react-router-spa/fixture.json b/tests/framework-fixtures/vite8-react-router-spa/fixture.json index 67a73192e..74159edaa 100644 --- a/tests/framework-fixtures/vite8-react-router-spa/fixture.json +++ b/tests/framework-fixtures/vite8-react-router-spa/fixture.json @@ -41,6 +41,9 @@ "probe": { "expectLiveInit": true, "expectConsoleClean": true + }, + "steer": { + "sourceFile": "src/routes/About.jsx" } } } diff --git a/tests/lib/transformers/factory.test.js b/tests/lib/transformers/factory.test.js index d6e0f5269..4c1d0376e 100644 --- a/tests/lib/transformers/factory.test.js +++ b/tests/lib/transformers/factory.test.js @@ -131,17 +131,6 @@ describe('createTransformer factory', () => { expect(content).toContain('TRANSFORMED content'); }); - test('should support prefix option', () => { - const transform = createTransformer(baseConfig); - const skills = [{ name: 'audit', description: 'Audit', userInvocable: true, body: 'Body' }]; - transform(skills, TEST_DIR, { prefix: 'i-', outputSuffix: '-prefixed' }); - - const outputPath = path.join(TEST_DIR, 'cursor-prefixed/.test/skills/i-audit/SKILL.md'); - expect(fs.existsSync(outputPath)).toBe(true); - const content = fs.readFileSync(outputPath, 'utf-8'); - expect(content).toContain('name: i-audit'); - }); - test('should copy reference files', () => { const transform = createTransformer(baseConfig); const skills = [{ @@ -187,9 +176,9 @@ describe('createTransformer factory', () => { transform(skills, TEST_DIR); console.log = originalLog; + // v3.0 summary format: `✓ : skills` (no user-invocable count). expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('✓ Test Provider:')); expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 skills')); - expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invocable')); }); test('should handle empty skills array', () => { diff --git a/tests/lib/transformers/providers.test.js b/tests/lib/transformers/providers.test.js index 1830de7eb..684d5874c 100644 --- a/tests/lib/transformers/providers.test.js +++ b/tests/lib/transformers/providers.test.js @@ -53,14 +53,6 @@ for (const [key, config] of Object.entries(PROVIDERS)) { expect(content).toContain(`See ${expected}.`); }); - test('should support prefix option', () => { - const skills = [{ name: 'audit', description: 'Audit', userInvocable: true, body: 'Body' }]; - transform(skills, TEST_DIR, { prefix: 'i-', outputSuffix: '-prefixed' }); - expect(fs.existsSync(skillPath(config, 'i-audit', '-prefixed'))).toBe(true); - const content = fs.readFileSync(skillPath(config, 'i-audit', '-prefixed'), 'utf-8'); - expect(content).toContain('name: i-audit'); - }); - test('should copy reference files', () => { const skills = [{ name: 'test', diff --git a/tests/lib/utils.test.js b/tests/lib/utils.test.js index ef24fbe05..df8918c50 100644 --- a/tests/lib/utils.test.js +++ b/tests/lib/utils.test.js @@ -386,7 +386,7 @@ Skill instructions here.`; const skillDir = path.join(testRootDir, 'skill'); ensureDir(skillDir); - fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent); + fs.writeFileSync(path.join(skillDir, 'SKILL.src.md'), skillContent); const { skills } = readSourceFiles(testRootDir); @@ -408,7 +408,7 @@ Audit the code.`; const skillDir = path.join(testRootDir, 'skill'); ensureDir(skillDir); - fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent); + fs.writeFileSync(path.join(skillDir, 'SKILL.src.md'), skillContent); const { skills } = readSourceFiles(testRootDir); @@ -427,7 +427,7 @@ Audit the code.`; const skillDir = path.join(testRootDir, 'skill'); ensureDir(skillDir); - fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent); + fs.writeFileSync(path.join(skillDir, 'SKILL.src.md'), skillContent); const { skills } = readSourceFiles(testRootDir); @@ -445,7 +445,7 @@ Impeccable design instructions.`; const skillDir = path.join(testRootDir, 'skill'); ensureDir(skillDir); - fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent); + fs.writeFileSync(path.join(skillDir, 'SKILL.src.md'), skillContent); const refDir = path.join(skillDir, 'reference'); ensureDir(refDir); @@ -464,7 +464,7 @@ Impeccable design instructions.`; test('should fall back to "impeccable" when frontmatter has no name', () => { const skillDir = path.join(testRootDir, 'skill'); ensureDir(skillDir); - fs.writeFileSync(path.join(skillDir, 'SKILL.md'), 'Just body, no frontmatter.'); + fs.writeFileSync(path.join(skillDir, 'SKILL.src.md'), 'Just body, no frontmatter.'); const { skills } = readSourceFiles(testRootDir); @@ -475,7 +475,7 @@ Impeccable design instructions.`; test('should ignore non-md files in skill/reference', () => { const skillDir = path.join(testRootDir, 'skill'); ensureDir(skillDir); - fs.writeFileSync(path.join(skillDir, 'SKILL.md'), '---\nname: test-skill\n---\nBody'); + fs.writeFileSync(path.join(skillDir, 'SKILL.src.md'), '---\nname: test-skill\n---\nBody'); const refDir = path.join(skillDir, 'reference'); ensureDir(refDir); @@ -508,7 +508,7 @@ Body content.`; const skillDir = path.join(testRootDir, 'skill'); ensureDir(skillDir); - fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent); + fs.writeFileSync(path.join(skillDir, 'SKILL.src.md'), skillContent); const { skills } = readSourceFiles(testRootDir); @@ -534,90 +534,39 @@ describe('readPatterns', () => { } }); - test('should extract DO and DON\'T patterns from SKILL.md', () => { - const skillContent = `--- -name: impeccable ---- - -### Typography -**DO**: Use variable fonts for flexibility. -**DON'T**: Use system fonts like Arial. - -### Color & Contrast -**DO**: Ensure WCAG AA compliance. -**DON'T**: Use gray text on colored backgrounds. - -### Layout & Space -**DO**: Use consistent spacing scale. -**DON'T**: Nest cards inside cards.`; - - const skillDir = path.join(testRootDir, 'skill'); - ensureDir(skillDir); - fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent); - + test('returns the curated DO/DON\'T pattern categories', () => { + // v3.0: readPatterns no longer parses SKILL.md. It returns a hand-curated + // catalog (CURATED_CATEGORIES) that powers the homepage Antidote teaser. const { patterns, antipatterns } = readPatterns(testRootDir); - expect(patterns).toHaveLength(3); - expect(antipatterns).toHaveLength(3); + expect(patterns).toHaveLength(6); + expect(antipatterns).toHaveLength(6); expect(patterns[0].name).toBe('Typography'); - expect(patterns[0].items).toContain('Use variable fonts for flexibility.'); - expect(antipatterns[0].items).toContain('Use system fonts like Arial.'); + expect(patterns[0].items.length).toBeGreaterThan(0); + expect(antipatterns[0].name).toBe('Typography'); + expect(antipatterns[0].items.length).toBeGreaterThan(0); }); - test('should normalize "Color & Theme" to "Color & Contrast"', () => { - const skillContent = `--- -name: impeccable ---- - -### Color & Theme -**DO**: Use OKLCH color space. -**DON'T**: Use pure black.`; - - const skillDir = path.join(testRootDir, 'skill'); - ensureDir(skillDir); - fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent); - - const { patterns, antipatterns } = readPatterns(testRootDir); - - expect(patterns[0].name).toBe('Color & Contrast'); - }); - - test('should handle missing SKILL.md file', () => { - ensureDir(path.join(testRootDir, 'skill')); - - const { patterns, antipatterns } = readPatterns(testRootDir); - - expect(patterns).toEqual([]); - expect(antipatterns).toEqual([]); - }); - - test('should return patterns in consistent section order', () => { - const skillContent = `--- -name: impeccable ---- - -### Motion -**DO**: Use ease-out for natural movement. - -### Typography -**DO**: Use modular scale. - -### Color & Contrast -**DO**: Use tinted neutrals.`; - - const skillDir = path.join(testRootDir, 'skill'); - ensureDir(skillDir); - fs.writeFileSync(path.join(skillDir, 'SKILL.md'), skillContent); - + test('returns categories in the curated order', () => { const { patterns } = readPatterns(testRootDir); + expect(patterns.map((p) => p.name)).toEqual([ + 'Typography', + 'Color & Contrast', + 'Layout & Space', + 'Visual Details', + 'Motion', + 'Interaction', + ]); + }); - // Patterns are returned in predefined section order, not source order - // Only sections with content are included - expect(patterns[0].name).toBe('Typography'); - expect(patterns[1].name).toBe('Color & Contrast'); - expect(patterns[2].name).toBe('Motion'); - expect(patterns.length).toBe(3); + test('ignores its arguments (curated, not extracted from any SKILL.md)', () => { + const fromBogusRoot = readPatterns('/nonexistent/path/xyz'); + const fromRealRoot = readPatterns(testRootDir); + expect(fromBogusRoot.patterns.map((p) => p.name)).toEqual( + fromRealRoot.patterns.map((p) => p.name) + ); + expect(fromBogusRoot.patterns).toHaveLength(6); }); }); @@ -658,24 +607,18 @@ describe('replacePlaceholders', () => { expect(result).toBe('Commands: /audit, /polish'); }); - test('should exclude i-impeccable from {{available_commands}}', () => { - const result = replacePlaceholders('Commands: {{available_commands}}', 'claude-code', ['i-audit', 'i-impeccable', 'i-polish']); - expect(result).toBe('Commands: /i-audit, /i-polish'); - }); - test('should exclude legacy teach-impeccable from {{available_commands}}', () => { const result = replacePlaceholders('Commands: {{available_commands}}', 'claude-code', ['audit', 'teach-impeccable', 'polish']); expect(result).toBe('Commands: /audit, /polish'); }); - test('should exclude legacy i-teach-impeccable from {{available_commands}}', () => { - const result = replacePlaceholders('Commands: {{available_commands}}', 'claude-code', ['i-audit', 'i-teach-impeccable', 'i-polish']); - expect(result).toBe('Commands: /i-audit, /i-polish'); - }); - - test('should produce empty string for {{available_commands}} with no commands', () => { - const result = replacePlaceholders('Commands: {{available_commands}}.', 'claude-code', []); - expect(result).toBe('Commands: .'); + test('lists /impeccable sub-commands for {{available_commands}} when no command names are passed', () => { + // v3.0 single-skill architecture: with no command names, the list falls back + // to the IMPECCABLE_SUB_COMMANDS sub-commands rendered as `/impeccable `. + const result = replacePlaceholders('Commands: {{available_commands}}', 'claude-code', []); + expect(result.startsWith('Commands: /impeccable ')).toBe(true); + expect(result).toContain('/impeccable audit'); + expect(result).toContain('/impeccable polish'); }); test('should replace multiple placeholders in the same string', () => { diff --git a/tests/live-accept.test.mjs b/tests/live-accept.test.mjs index df9cad24b..aa746b316 100644 --- a/tests/live-accept.test.mjs +++ b/tests/live-accept.test.mjs @@ -358,3 +358,45 @@ describe('live-accept — style-element edge cases', () => { assert.ok(!after.includes('variant one'), 'variants dropped'); }); }); + +describe('live-accept — insert sessions', () => { + let tmp; + beforeEach(() => { tmp = mkdtempSync(join(tmpdir(), 'impeccable-accept-insert-')); }); + afterEach(() => { rmSync(tmp, { recursive: true, force: true }); }); + + const insertHtml = (id) => `
+
Hero block
+ +
+ + +

Variant one

+

Variant two

+
+ + +
`; + + it('discard removes an insert wrapper without touching anchor sections', () => { + writeFileSync(join(tmp, 'page.html'), insertHtml('insaaa01')); + const result = runAccept(tmp, ['--id', 'insaaa01', '--discard']); + assert.equal(result.handled, true, JSON.stringify(result)); + const after = readFileSync(join(tmp, 'page.html'), 'utf-8'); + assert.ok(after.includes('Hero block')); + assert.ok(after.includes('Footer')); + assert.ok(!after.includes('impeccable-variants-start')); + assert.ok(!after.includes('Variant one')); + }); + + it('accept keeps the chosen insert variant and drops the wrapper', () => { + writeFileSync(join(tmp, 'page.html'), insertHtml('insbbb02')); + const result = runAccept(tmp, ['--id', 'insbbb02', '--variant', '2']); + assert.equal(result.handled, true, JSON.stringify(result)); + const after = readFileSync(join(tmp, 'page.html'), 'utf-8'); + assert.ok(after.includes('Variant two')); + assert.ok(!after.includes('Variant one')); + assert.ok(!after.includes('impeccable-variants-start')); + assert.ok(after.includes('Hero block')); + assert.ok(after.includes('Footer')); + }); +}); diff --git a/tests/live-browser-regression.test.mjs b/tests/live-browser-regression.test.mjs index 8c8b0a894..d9888f8a4 100644 --- a/tests/live-browser-regression.test.mjs +++ b/tests/live-browser-regression.test.mjs @@ -82,10 +82,213 @@ describe('live-browser.js regression guards', () => { ); }); + it('global bar includes expandable page chat affordance', () => { + assert.match( + SOURCE, + /function initPageChat\(/, + 'live-browser must mount a page-level chat control in the global bar', + ); + assert.match( + SOURCE, + /pageChatEl\.id = PREFIX \+ '-page-chat'/, + 'page chat container needs a stable id for future wiring and tests', + ); + assert.match( + SOURCE, + /function syncPageChatFocus\(reason\)[\s\S]{0,220}?if \(state === 'CONFIGURING'\) focusConfigureInput\(reason\);[\s\S]{0,120}?else if \(shouldSteerAutoFocus\(\)\) focusSteerChat\(reason\);/, + 'focus configure input while configuring; steer auto-focus unless page text is selected', + ); + assert.match( + SOURCE, + /function steerFocusLog\(reason, extra\)/, + 'steer focus attempts should be logged for debugging before adding retries', + ); + assert.match( + SOURCE, + /function submitSteerMessage\(\)[\s\S]{0,1200}?type: 'steer'/, + 'steer submit must post a steer event to the live poller', + ); + assert.match( + SOURCE, + /case 'steer_done':[\s\S]{0,80}?maybeCompleteSteer\(msg\)/, + 'steer_done SSE must unlock the chat bar', + ); + assert.match( + SOURCE, + /function toggleSteerVoice\(\)/, + 'steer voice must toggle Web Speech recognition from the mic button', + ); + assert.match( + SOURCE, + /webkitSpeechRecognition|SpeechRecognition/, + 'steer voice must use the Web Speech API', + ); + assert.doesNotMatch( + SOURCE, + /Voice mode coming soon/, + 'steer voice placeholder toast must not ship once voice is wired', + ); + assert.match( + SOURCE, + /function isEmbeddedPreviewBrowser\(\)/, + 'steer voice must detect embedded preview browsers (Cursor/Electron)', + ); + assert.match( + SOURCE, + /steerVoiceUnavailableMessage\(\)/, + 'steer voice must explain when preview browsers cannot reach speech services', + ); + assert.doesNotMatch( + SOURCE, + /Handing off|pageChatHint\.textContent = 'Working'/, + 'steer processing state should use dots-only animation, not truncated text', + ); + assert.match( + SOURCE, + /function syncAgentPollingUi\(/, + 'global bar brand must reflect agent poll connectivity', + ); + assert.match( + SOURCE, + /case 'agent_polling':/, + 'browser must listen for agent_polling SSE updates', + ); + assert.match( + SOURCE, + /function showAgentPollTooltip\(/, + 'disconnected agent state must use an instant custom tooltip on brand hover', + ); + assert.match( + SOURCE, + /function scheduleSteerFocusRecover\(reason\)/, + 'steer focus must reschedule after page clicks once selection/pause gates clear', + ); + assert.match( + SOURCE, + /steer-blur-recover/, + 'steer blur should recover focus for type-to-steer when not selecting page text', + ); + }); + + it('pick mode preference persists in localStorage', () => { + assert.match( + SOURCE, + /const INTERACTION_PREFS_KEY = 'impeccable-live-interaction';[\s\S]{0,3000}?function saveInteractionPrefs\(\)/, + 'pick/insert interaction prefs must persist in localStorage', + ); + assert.match( + SOURCE, + /function togglePick\(\)[\s\S]{0,200}?saveInteractionPrefs\(\);/, + 'togglePick must persist interaction prefs', + ); + assert.match( + SOURCE, + /function toggleInsert\(\)[\s\S]{0,800}?saveInteractionPrefs\(\);/, + 'toggleInsert must persist interaction prefs', + ); + assert.match( + SOURCE, + /if \(state === 'IDLE' && \(pickActive \|\| insertActive\)\) state = 'PICKING';/, + 'SSE connected must arm insert mode when saved preference has insert on', + ); + }); + + it('insert mode UI and generate payload guards', () => { + assert.match(SOURCE, /function toggleInsert\(\)/, 'global bar must expose insert toggle'); + assert.match(SOURCE, /PREFIX \+ '-insert-toggle'/, 'insert toggle needs stable id'); + assert.match(SOURCE, /function buildInsertConfigureRow\(\)/, 'insert configure bar required'); + assert.match(SOURCE, /function handleInsertCreate\(\)/, 'insert create handler required'); + assert.match(SOURCE, /mode: 'insert'/, 'insert generate must set mode insert'); + assert.match(SOURCE, /function syncInsertCreateButton\(btn, input\)/, 'Create button must reflect prompt/annotation gate'); + assert.match( + SOURCE, + /syncInsertCreateButton\(create, input\)/, + 'Create gate must sync before the row is attached to the document', + ); + assert.match(SOURCE, /function showInsertCreateTooltip\(/, 'Create disabled state uses a custom hover tooltip'); + assert.match( + SOURCE, + /function buildCyclingRow\(\)[\s\S]*?background: C\.brand, color: C\.ink/, + 'Accept button uses lacquer-deep text on kinpaku gold', + ); + assert.match(SOURCE, /insertCreateDisabledReason/, 'disabled Create hover must explain why'); + assert.match(SOURCE, /data-impeccable-insert-placeholder/, 'placeholder element must be marked'); + assert.match( + SOURCE, + /showHighlight\(el\)[\s\S]{0,120}?data-impeccable-insert-placeholder/, + 'pick highlight must not stack on insert placeholder', + ); + assert.match(SOURCE, /border: '2px dotted ' \+ BP\.accent/, 'placeholder border matches insert line (dotted)'); + assert.match( + SOURCE, + /function syncPageInteractionCursor\(\)[\s\S]{0,280}?cursorForInsertAxis/, + 'insert picking cursor follows row/column axis', + ); + assert.match(SOURCE, /function hitSiblingInsertGap\(/, 'insert mode detects gaps between siblings'); + assert.match(SOURCE, /function resolveInsertHover\(/, 'insert hover resolves axis-aware boundaries'); + assert.match(SOURCE, /data-impeccable-placeholder-resize/, 'placeholder edge handles on annotation overlay'); + assert.match(SOURCE, /resizeEdge && configureKind === 'insert'/, 'resize takes priority over draw'); + assert.match(SOURCE, /cursorForPlaceholderEdge\(spec\.edge\)/, 'edge handles use resize cursors'); + assert.match( + SOURCE, + /create\.id = PREFIX \+ '-insert-create'/, + 'Create button id must be set on the element, not passed to el() styles', + ); + assert.doesNotMatch( + SOURCE, + /buildInsertConfigureRow[\s\S]{0,1200}?toggleActionPicker/, + 'insert configure bar must not include action picker', + ); + assert.match( + SOURCE, + /buildInsertConfigureRow[\s\S]*?const count = el\('button', \{[\s\S]{0,320}?height: '28px'/, + 'insert count toggle must match input height', + ); + assert.match( + SOURCE, + /buildInsertConfigureRow[\s\S]*?const create = el\('button', \{[\s\S]{0,320}?height: '28px'/, + 'insert Create button must match input height', + ); + assert.match(SOURCE, /function resolveBarAnchor\(\)/, 'bar positions from a connected anchor'); + assert.match(SOURCE, /function finalizeInsertSession\(\)/, 'insert placeholder outlives capture'); + assert.match(SOURCE, /function placeholderSizing\(/, 'insert placeholder picks implicit vs explicit width'); + assert.match(SOURCE, /applyPlaceholderSizingStyles\(placeholder, sizing\)/, 'placeholder width styles applied by kind'); + assert.match( + SOURCE, + /function createInsertPlaceholder[\s\S]*?applyPlaceholderSizingStyles\(placeholder, sizing\)/, + 'createInsertPlaceholder must not always set parent pixel width', + ); + assert.doesNotMatch( + SOURCE, + /sendEvent\(screenshotPath[\s\S]{0,200}?removeInsertPlaceholder/, + 'capture must not remove insert placeholder before variants land', + ); + assert.match( + SOURCE, + /function setVariantShown\(el, shown\)[\s\S]{0,200}?removeAttribute\('hidden'\)/, + 'variant cycling must clear the hidden attribute, not only style.display', + ); + assert.match( + SOURCE, + /count > 0 \? pickVariantContent\(wrapper, visibleVariant \|\| 1\) : null/, + 'insert HMR re-anchor must not drop placeholder until variants exist', + ); + assert.match( + SOURCE, + /function ensureInsertPlaceholder\(\)/, + 'insert generating must recreate placeholder after scaffold HMR', + ); + assert.match( + SOURCE, + /insertPlaceholder: insertPlaceholderSnapshot/, + 'insert placeholder snapshot must persist across HMR resume', + ); + }); + it('handleAccept reads the visible DOM variant before sending accept', () => { assert.match( SOURCE, - /function readVisibleVariantFromDOM\(sessionId\)[\s\S]{0,900}?variant\.style\.display === 'none'[\s\S]{0,500}?return idx;/, + /function readVisibleVariantFromDOM\(sessionId\)[\s\S]{0,900}?isVariantShown\(variant\)[\s\S]{0,500}?return idx;/, 'live-browser should be able to derive the accepted variant from the currently visible DOM node', ); assert.match( diff --git a/tests/live-e2e-steer-agent.test.mjs b/tests/live-e2e-steer-agent.test.mjs new file mode 100644 index 000000000..b825c8996 --- /dev/null +++ b/tests/live-e2e-steer-agent.test.mjs @@ -0,0 +1,83 @@ +/** + * Steer handler unit tests (no Playwright). + * Run with: node --test tests/live-e2e-steer-agent.test.mjs + */ + +import { describe, it, before, after } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +import { createFakeAgent, findSteerTargetFile, runAgentLoop, STEER_MARKER_ATTR } from './live-e2e/agent.mjs'; +import { stageFixture, startLiveServer, stopLiveServer, FIXTURES_DIR } from './live-e2e/session.mjs'; +import { SCRIPTS_DIR } from './live-e2e/session.mjs'; + +const FIXTURE_NAME = 'vite8-react-plain'; + +describe('live-e2e steer agent handler', () => { + let tmp; + let live; + let abort; + let loopDone; + + before(async () => { + const fixture = JSON.parse(readFileSync(join(FIXTURES_DIR, FIXTURE_NAME, 'fixture.json'), 'utf-8')); + tmp = stageFixture(FIXTURE_NAME, fixture); + live = startLiveServer(tmp); + abort = new AbortController(); + loopDone = runAgentLoop({ + tmp, + scriptsDir: SCRIPTS_DIR, + port: live.port, + token: live.token, + agent: createFakeAgent(), + signal: abort.signal, + log: () => {}, + }); + }); + + after(async () => { + abort?.abort(); + await loopDone?.catch(() => {}); + if (live) stopLiveServer(tmp); + }); + + it('findSteerTargetFile locates the hero source file', () => { + const file = findSteerTargetFile(tmp); + assert.match(file, /App\.jsx$/); + const body = readFileSync(file, 'utf-8'); + assert.match(body, /hero-title/); + }); + + it('agent loop handles steer POST and writes the marker', async () => { + const sourceFile = findSteerTargetFile(tmp); + const before = readFileSync(sourceFile, 'utf-8'); + assert.doesNotMatch(before, new RegExp(STEER_MARKER_ATTR + '="e2e"')); + + const post = await fetch(`http://127.0.0.1:${live.port}/events`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + token: live.token, + type: 'steer', + id: 'cafebabe', + message: 'steer-e2e mark hero', + pageUrl: 'http://127.0.0.1:5173/', + }), + }); + assert.equal(post.status, 200); + + const deadline = Date.now() + 10_000; + let updated = before; + while (Date.now() < deadline) { + updated = readFileSync(sourceFile, 'utf-8'); + if (updated.includes(`${STEER_MARKER_ATTR}="e2e"`)) break; + await new Promise((r) => setTimeout(r, 50)); + } + assert.match( + updated, + new RegExp(STEER_MARKER_ATTR + '="e2e"'), + 'fake agent should mark hero after steer event', + ); + }); +}); diff --git a/tests/live-e2e.test.mjs b/tests/live-e2e.test.mjs index bfbe7bd59..b40606c38 100644 --- a/tests/live-e2e.test.mjs +++ b/tests/live-e2e.test.mjs @@ -7,10 +7,11 @@ * 1. Stage → install → start live-server + dev server → inject script tag * 2. Open Playwright Chromium, assert the live handshake fires * 3. Spawn a deterministic fake-agent polling loop in this same process - * 4. Drive the bar UI: pick element → Go → wait CYCLING → cycle → Accept - * 5. Assert source rewrite (variants block, then accepted-only after accept) - * 6. Assert DOM reflects the accepted variant via getComputedStyle - * 7. Tear down (browser, dev server, agent loop, live-server, tmp) + * 4. Steer smoke: submit page-level chat → agent steer_done → bar unlocks + * 5. Drive the bar UI: pick element → Go → wait CYCLING → cycle → Accept + * 6. Assert source rewrite (variants block, then accepted-only after accept) + * 7. Assert DOM reflects the accepted variant via getComputedStyle + * 8. Tear down (browser, dev server, agent loop, live-server, tmp) * * The fake agent is pluggable — see tests/live-e2e/agent.mjs. A future * LLM-backed agent slots in by implementing the same VariantAgent interface. @@ -34,9 +35,11 @@ import { clickNext, getVisibleVariant, pickElement, - waitForCycling, + runInsertFlow, waitForHandshake, } from './live-e2e/ui.mjs'; +import { runSteerSmoke } from './live-e2e/steer.mjs'; +import { runPreActions, waitForCyclingRobust } from './live-e2e/preactions.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -143,13 +146,26 @@ for (const { name, fixture } of fixtures) { const { page, tmp, consoleErrors, teardown } = session; const expectedCount = 3; + const isInsert = fixture.runtime.mode === 'insert'; + const insertCfg = fixture.runtime.insert || {}; const pickSelector = fixture.runtime.pickSelector || 'h1.hero-title'; + const domSelector = isInsert + ? (insertCfg.expectSelector || '.inserted-strip') + : pickSelector; try { // 1. Handshake t.diagnostic('Waiting for live handshake'); await waitForHandshake(page); + // 1b. Steer smoke — page-level chat before the heavier generate cycle. + if (fixture.runtime.steer !== false) { + const steerTimeouts = agentMode === 'llm' + ? { unlockTimeoutMs: 90_000, selectorTimeoutMs: 45_000, runPreActions } + : { runPreActions }; + await runSteerSmoke(page, tmp, fixture, (m) => t.diagnostic(m), steerTimeouts); + } + // 2. preActions — fixtures with hidden/conditional content (modals, // tabs, routes) drive the page into the right state before pick. if (fixture.runtime.preActions) { @@ -157,21 +173,29 @@ for (const { name, fixture } of fixtures) { await runPreActions(page, fixture.runtime.preActions); } - // 3. Pick the target element - t.diagnostic(`Picking ${pickSelector}`); - await pickElement(page, pickSelector); - - if (process.env.IMPECCABLE_E2E_DEBUG) { - const barText = await page.evaluate(() => { - const bar = document.querySelector('#impeccable-live-bar'); - return bar ? { display: bar.style.display, text: bar.textContent || '', html: bar.innerHTML.slice(0, 500) } : null; + // 3. Start generate — replace picks an element; insert places a placeholder. + if (isInsert) { + t.diagnostic(`Insert after ${insertCfg.anchorSelector || 'anchor'}`); + await runInsertFlow(page, { + anchorSelector: insertCfg.anchorSelector || 'section#features', + position: insertCfg.position || 'after', + prompt: insertCfg.prompt || 'Add new content', }); - t.diagnostic(`Bar after pick: ${JSON.stringify(barText)}`); - } + } else { + t.diagnostic(`Picking ${pickSelector}`); + await pickElement(page, pickSelector); - // 3. Click Go (default action 'impeccable', default count 3 — fixture-stable) - t.diagnostic('Clicking Go'); - await clickGo(page); + if (process.env.IMPECCABLE_E2E_DEBUG) { + const barText = await page.evaluate(() => { + const bar = document.querySelector('#impeccable-live-bar'); + return bar ? { display: bar.style.display, text: bar.textContent || '', html: bar.innerHTML.slice(0, 500) } : null; + }); + t.diagnostic(`Bar after pick: ${JSON.stringify(barText)}`); + } + + t.diagnostic('Clicking Go'); + await clickGo(page); + } // 4. Wait for the agent's variants to land (HMR + MutationObserver). // For fixtures whose picked element lives inside a conditional @@ -188,53 +212,23 @@ for (const { name, fixture } of fixtures) { // install pressure, so keep this gate patient enough that we do // not retrace while the agent is still writing the variants. t.diagnostic(`Waiting for CYCLING state with ${expectedCount} variants`); - const firstPassTimeoutMs = agentMode === 'llm' ? 90_000 : 5_000; - let cyclingReached = false; - if (fixture.runtime.preActions) { - try { - await waitForCycling(page, expectedCount, { timeout: firstPassTimeoutMs }); - cyclingReached = true; - } catch { - t.diagnostic(`Cycling not reached in ${firstPassTimeoutMs}ms — retracing preActions`); - await runPreActions(page, fixture.runtime.preActions); - } - } - try { - if (!cyclingReached) { - // Default 30s; LLM mode bumps to 90s to absorb API latency on - // top of HMR settle time. - const finalTimeoutMs = agentMode === 'llm' ? 90_000 : 30_000; - await waitForCycling(page, expectedCount, { timeout: finalTimeoutMs }); - } - } catch (err) { - if (process.env.IMPECCABLE_E2E_DEBUG) { - const variantCount = await page.evaluate(() => - document.querySelectorAll('[data-impeccable-variant]').length, - ); - const barInfo = await page.evaluate(() => { - const bars = document.querySelectorAll('#impeccable-live-bar'); - return { - count: bars.length, - bars: [...bars].map((bar) => ({ - display: bar.style.display, - opacity: bar.style.opacity, - text: bar.textContent || '', - innerHtml: bar.innerHTML.slice(0, 600), - })), - __init: window.__IMPECCABLE_LIVE_INIT__, - }; - }); - t.diagnostic(`waitForCycling failed; variants in DOM: ${variantCount}`); - t.diagnostic(`Bar state: ${JSON.stringify(barInfo)}`); - t.diagnostic(`--- dev server tail ---\n${session.dev.log()}`); - } - throw err; - } + await waitForCyclingRobust(page, expectedCount, { + agentMode, + preActions: fixture.runtime.preActions, + log: (m) => t.diagnostic(m), + }); // 5. Source-side check: wrapper + style + variants are present const sourceFile = await locateSessionFile(tmp); const after = readFileSync(sourceFile, 'utf-8'); assert.match(after, /data-impeccable-variants="/, 'wrapper inserted'); + if (isInsert) { + assert.match(after, /data-impeccable-mode="insert"/, 'insert mode wrapper'); + assert.doesNotMatch(after, /data-impeccable-variant="original"/, 'insert has no original variant'); + if (insertCfg.assertAnchorContains) { + assert.match(after, new RegExp(insertCfg.assertAnchorContains), 'anchor section untouched'); + } + } if (sourceFile.endsWith('.astro')) { assert.match(after, / + + +

For those who build their own index.

+

Acme Notes is not a productivity tool. It is an archive, designed for the researcher who treats their notes as an external brain.

+
Begin Archive + + +`; + +/** + * Minimal SvelteKit project. Exercises Setup step 2 ("familiarize + * yourself with any existing design system, conventions, and components"): + * the agent should explore at least one of these code files before + * producing a polish or craft pass. + */ +export const SVELTE_PROJECT_FILES = { + 'package.json': `${JSON.stringify( + { + name: 'acme-notes', + type: 'module', + dependencies: { svelte: '^4.0.0', '@sveltejs/kit': '^2.0.0' }, + scripts: { dev: 'vite dev', build: 'vite build' }, + }, + null, + 2, + )}\n`, + 'svelte.config.js': `import adapter from '@sveltejs/adapter-auto'; + +export default { + kit: { adapter: adapter() }, +}; +`, + 'src/app.css': `:root { + --ink: oklch(0.16 0.02 250); + --paper: oklch(0.98 0.01 90); + --accent: oklch(0.55 0.18 28); + --hairline: oklch(0.16 0.02 250 / 0.08); +} + +body { + background: var(--paper); + color: var(--ink); + font-family: 'Inter', sans-serif; + line-height: 1.55; +} +`, + 'src/lib/components/Button.svelte': ` + + + + +`, + 'src/lib/components/Card.svelte': `
+ +
+ + +`, + 'src/routes/+page.svelte': ` + + + Acme Notes + + +
+

For those who build their own index.

+

Acme Notes is not a productivity tool. It is an archive.

+ + Some featured content. +
+`, +}; + +export const DESIGN_MD_SAMPLE = `# Acme Notes — Design System + +## Colors +- \`--ink\`: oklch(0.16 0.02 250) — body copy +- \`--paper\`: oklch(0.98 0.01 90) — body background +- \`--accent\`: oklch(0.55 0.18 28) — terracotta, used at <8% surface + +## Typography +- Display: GT Sectra (commercial), 700, tracking -0.02em +- Body: Inter, 400, 1.55 line-height, 65ch max +- Mono: JetBrains Mono, 400 (rare, only for callouts) + +## Spacing +Multi-modular scale: 4 / 8 / 12 / 24 / 48 / 96 px. + +## Elevation +Mostly flat. A single 1px hairline border at oklch(0.16 0.02 250 / 0.08) +separates major regions. No drop shadows under 16px blur. + +## Components +- Buttons: text-only by default; a single solid primary in accent for CTAs. +- Cards: avoid; prefer hairlined regions and inline lists. +- Forms: floating labels, no border on the input — underline only. +`; diff --git a/tests/skill-behavior/harness.mjs b/tests/skill-behavior/harness.mjs new file mode 100644 index 000000000..8940cbb1e --- /dev/null +++ b/tests/skill-behavior/harness.mjs @@ -0,0 +1,309 @@ +/** + * Sandboxed scenario runner for skill-behavior tests. + * + * Each scenario: + * 1. Creates a temp workspace. + * 2. Symlinks the real .claude/skills/impeccable into the workspace so + * scripts (load-context.mjs, etc.) resolve from the canonical path + * the skill references. + * 3. Optionally writes PRODUCT.md / DESIGN.md fixtures. + * 4. Inlines SKILL.md as the system prompt (placeholders stripped to + * neutral values so the same body works for all providers). + * 5. Runs Vercel AI SDK generateText with workspace-scoped tools + * (bash, read, write, list). + * 6. Captures every tool call and returns a trace + the raw response + * messages (so multi-turn scenarios can append to them). + * + * The harness deliberately mirrors the live-mode E2E pattern: real LLM, + * no mocks, but tightly bounded execution surface so we observe the routing + * behavior of the skill without paying for full-fledged design work. + */ +import { generateText, stepCountIs, tool } from 'ai'; +import { z } from 'zod'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawn } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = path.resolve(__dirname, '..', '..'); +const SKILL_SOURCE_DIR = path.join(REPO_ROOT, 'skill'); +const MAX_BASH_OUTPUT_BYTES = 200_000; + +/** + * Strip the YAML frontmatter and replace `{{...}}` placeholders so SKILL.md + * is provider-neutral when inlined. + */ +function loadSkillBody() { + let md = fs.readFileSync(path.join(SKILL_SOURCE_DIR, 'SKILL.src.md'), 'utf8'); + // Strip frontmatter. + if (md.startsWith('---')) { + const end = md.indexOf('\n---', 3); + if (end !== -1) md = md.slice(end + 4).trimStart(); + } + // The source uses placeholders that the build step replaces per-provider. + // For the test harness we want a single body that works for any provider, + // and the scripts the skill references live at .claude/skills/impeccable/ + // (the workspace symlink), so hard-code those values. + md = md + .replaceAll('{{model}}', 'the assistant') + .replaceAll('{{command_prefix}}', '/') + .replaceAll('{{ask_instruction}}', 'Ask the user') + .replaceAll('{{config_file}}', 'AGENTS.md') + .replaceAll('{{scripts_path}}', '.claude/skills/impeccable/scripts') + .replaceAll('{{command_hint}}', 'command'); + return md.trim(); +} + +export const SKILL_BODY = loadSkillBody(); + +/** + * Create a temp workspace and prepopulate it. + * + * - `.claude/skills/impeccable` is symlinked at the SOURCE skill dir (not + * the built `.claude/skills/impeccable/`) so the test exercises whatever + * is in `skill/` right now, without needing `bun run build` to refresh + * the harness output dirs. The trade-off: reference files surface their + * raw `{{placeholders}}`, but the assertions only check tool calls, not + * their content. + * - `files` lets the test seed PRODUCT.md / DESIGN.md (or anything else). + * - `skillVersion` switches from symlink to a real COPY of the skill dir and + * writes a `SKILL.md` carrying that version. context.mjs reads its own + * version from that sibling file, so this is required for any scenario that + * exercises the update-check path (the source dir has only SKILL.src.md). + */ +export function prepareWorkspace({ files = {}, skillVersion = null } = {}) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-skill-test-')); + const skillDest = path.join(dir, '.claude', 'skills', 'impeccable'); + fs.mkdirSync(path.join(dir, '.claude', 'skills'), { recursive: true }); + if (skillVersion) { + fs.cpSync(SKILL_SOURCE_DIR, skillDest, { recursive: true }); + fs.writeFileSync(path.join(skillDest, 'SKILL.md'), `---\nname: impeccable\nversion: ${skillVersion}\n---\n\nbody\n`); + } else { + fs.symlinkSync(SKILL_SOURCE_DIR, skillDest, 'dir'); + } + for (const [name, contents] of Object.entries(files)) { + const target = path.join(dir, name); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync(target, contents); + } + return dir; +} + +export function cleanupWorkspace(dir) { + try { + fs.rmSync(dir, { recursive: true, force: true }); + } catch { + // Best effort — temp dirs eventually get reaped by the OS. + } +} + +function safeResolve(root, userPath) { + if (typeof userPath !== 'string' || !userPath.length) { + return { error: 'path is required' }; + } + if (userPath.startsWith('/') || /^[a-zA-Z]:[\\/]/.test(userPath)) { + return { error: 'absolute paths are not allowed' }; + } + const resolved = path.resolve(root, userPath); + const rel = path.relative(root, resolved); + if (rel.startsWith('..') || rel.split(path.sep).includes('..')) { + return { error: 'path escapes the workspace' }; + } + return resolved; +} + +function execBash(workspace, command, timeoutMs = 20_000, extraEnv = {}) { + return new Promise((resolve) => { + const proc = spawn('bash', ['-lc', command], { cwd: workspace, env: { ...process.env, ...extraEnv } }); + let stdout = ''; + let stderr = ''; + const truncatedFlag = { val: false }; + const onChunk = (which) => (chunk) => { + const str = chunk.toString(); + if (which === 'out') { + if (stdout.length + str.length > MAX_BASH_OUTPUT_BYTES) { + stdout += str.slice(0, MAX_BASH_OUTPUT_BYTES - stdout.length); + truncatedFlag.val = true; + } else { + stdout += str; + } + } else { + if (stderr.length + str.length > MAX_BASH_OUTPUT_BYTES) { + stderr += str.slice(0, MAX_BASH_OUTPUT_BYTES - stderr.length); + truncatedFlag.val = true; + } else { + stderr += str; + } + } + }; + proc.stdout.on('data', onChunk('out')); + proc.stderr.on('data', onChunk('err')); + const timer = setTimeout(() => { + proc.kill('SIGKILL'); + resolve({ exitCode: null, stdout, stderr: stderr + '\n[TIMED OUT]', truncated: truncatedFlag.val }); + }, timeoutMs); + proc.on('exit', (code) => { + clearTimeout(timer); + resolve({ exitCode: code, stdout, stderr, truncated: truncatedFlag.val }); + }); + proc.on('error', (err) => { + clearTimeout(timer); + resolve({ exitCode: null, stdout, stderr: stderr + `\n[SPAWN ERROR] ${String(err)}`, truncated: truncatedFlag.val }); + }); + }); +} + +/** + * Build the workspace-scoped tool set + the trace it writes into. + * Returns `{ tools, trace }`. The trace mutates in place as the agent runs. + */ +export function makeTools(workspace, extraEnv = {}) { + const trace = { + toolCalls: [], + bashCommands: [], + bashOutputs: [], + readPaths: [], + writePaths: [], + listPaths: [], + }; + function record(name, input) { + trace.toolCalls.push({ name, input }); + if (name === 'bash' && typeof input?.command === 'string') trace.bashCommands.push(input.command); + if (name === 'read' && typeof input?.path === 'string') trace.readPaths.push(input.path); + if (name === 'write' && typeof input?.path === 'string') trace.writePaths.push(input.path); + if (name === 'list' && typeof input?.path === 'string') trace.listPaths.push(input.path); + } + const tools = { + bash: tool({ + description: + 'Run a bash command in the workspace root. Use this to invoke skill scripts (e.g. `node .claude/skills/impeccable/scripts/load-context.mjs`).', + inputSchema: z.object({ + command: z.string().describe('The bash command to execute.'), + }), + execute: async ({ command }) => { + record('bash', { command }); + const res = await execBash(workspace, command, 20_000, extraEnv); + const head = `exit=${res.exitCode}`; + const body = (res.stdout ? `stdout:\n${res.stdout}` : '') + (res.stderr ? `\nstderr:\n${res.stderr}` : ''); + const out = `${head}\n${body}${res.truncated ? '\n[output truncated]' : ''}`; + trace.bashOutputs.push(out); + return out; + }, + }), + read: tool({ + description: 'Read a file from the workspace. Path must be workspace-relative.', + inputSchema: z.object({ + path: z.string().describe('Workspace-relative file path.'), + }), + execute: async ({ path: p }) => { + record('read', { path: p }); + const resolved = safeResolve(workspace, p); + if (typeof resolved !== 'string') return `Error: ${resolved.error}`; + if (!fs.existsSync(resolved)) return `File not found: ${p}`; + const stat = fs.statSync(resolved); + if (stat.isDirectory()) return `Path is a directory: ${p}. Use list instead.`; + return fs.readFileSync(resolved, 'utf8'); + }, + }), + write: tool({ + description: 'Write or overwrite a file in the workspace. Creates parent directories as needed.', + inputSchema: z.object({ + path: z.string().describe('Workspace-relative file path.'), + contents: z.string().describe('Full file contents.'), + }), + execute: async ({ path: p, contents }) => { + record('write', { path: p, contents }); + const resolved = safeResolve(workspace, p); + if (typeof resolved !== 'string') return `Error: ${resolved.error}`; + fs.mkdirSync(path.dirname(resolved), { recursive: true }); + fs.writeFileSync(resolved, contents); + return `Wrote ${Buffer.byteLength(contents, 'utf8')} bytes to ${p}`; + }, + }), + list: tool({ + description: 'List a workspace directory. Defaults to the workspace root.', + inputSchema: z.object({ + path: z.string().default('.').describe('Workspace-relative directory path.'), + }), + execute: async ({ path: p }) => { + record('list', { path: p }); + const resolved = safeResolve(workspace, p); + if (typeof resolved !== 'string') return `Error: ${resolved.error}`; + if (!fs.existsSync(resolved)) return `Not found: ${p}`; + const stat = fs.statSync(resolved); + if (!stat.isDirectory()) return `Not a directory: ${p}`; + const entries = fs.readdirSync(resolved).map((name) => { + const st = fs.statSync(path.join(resolved, name)); + return st.isDirectory() ? `${name}/` : name; + }); + return entries.length ? entries.join('\n') : '(empty)'; + }, + }), + }; + return { tools, trace }; +} + +/** + * Run one scenario turn against a model. + * + * `priorMessages` lets multi-turn scenarios chain context from a previous + * call (append `result.response.messages` between turns). + */ +export async function runTurn({ workspace, model, userPrompt, priorMessages = [], maxSteps = 8, env = {} }) { + const { tools, trace } = makeTools(workspace, env); + const messages = [ + ...priorMessages, + { role: 'user', content: userPrompt }, + ]; + let result; + try { + result = await generateText({ + model, + system: SKILL_BODY, + messages, + tools, + stopWhen: [stepCountIs(maxSteps)], + }); + } catch (err) { + return { trace, error: String(err), text: '', responseMessages: messages, finishReason: 'error' }; + } + const responseMessages = [...messages, ...(result.response?.messages ?? [])]; + return { + trace, + text: result.text ?? '', + finishReason: result.finishReason, + usage: result.usage, + responseMessages, + }; +} + +/** + * Heuristic helpers — keep the assertion intent declarative in the test file. + */ +export function bashCommandsMatching(trace, substring) { + return trace.bashCommands.filter((cmd) => cmd.includes(substring)); +} + +export function readsMatching(trace, substring) { + return trace.readPaths.filter((p) => p.toLowerCase().includes(substring.toLowerCase())); +} + +/** + * True if the agent loaded a file by Read OR by a bash `cat` (some models + * stream multiple files via bash to save tool calls). + */ +export function fileLoaded(trace, filename) { + return readsMatching(trace, filename).length > 0 || bashCommandsMatching(trace, filename).length > 0; +} + +export function summarizeTrace(trace) { + return { + totalCalls: trace.toolCalls.length, + byName: trace.toolCalls.reduce((acc, c) => ((acc[c.name] = (acc[c.name] ?? 0) + 1), acc), {}), + bashCommands: trace.bashCommands, + readPaths: trace.readPaths, + writePaths: trace.writePaths, + }; +} diff --git a/tests/skill-behavior/providers.mjs b/tests/skill-behavior/providers.mjs new file mode 100644 index 000000000..f7673e0fa --- /dev/null +++ b/tests/skill-behavior/providers.mjs @@ -0,0 +1,94 @@ +/** + * Multi-provider model factory for the skill-behavior test harness. + * + * The lineup runs production-tier models on Anthropic and OpenAI + * (claude-sonnet-4-6, gpt-5.5) so the suite reflects what users actually + * run, not a cheap proxy. Google stays on gemini-3.1-flash-lite. Costlier + * per run than the old cheap tier, but the pass/fail signal is more + * representative of real agent behavior against the skill body. + * + * Anthropic and OpenAI use the Vercel AI SDK providers. Google uses + * @ai-sdk/google for the same reason — uniform tool-use semantics across all + * three keeps the harness tiny. + * + * .env is loaded from the repo root (copied from impeccable-evals). Tests + * skip cleanly when the matching key is unset rather than failing CI. + */ +import { anthropic } from '@ai-sdk/anthropic'; +import { google } from '@ai-sdk/google'; +import { openai } from '@ai-sdk/openai'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = path.resolve(__dirname, '..', '..'); + +function loadEnv() { + const envPath = path.join(REPO_ROOT, '.env'); + if (!fs.existsSync(envPath)) return; + const text = fs.readFileSync(envPath, 'utf8'); + for (const line of text.split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const eq = trimmed.indexOf('='); + if (eq === -1) continue; + const key = trimmed.slice(0, eq).trim(); + let value = trimmed.slice(eq + 1).trim(); + if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1); + if (value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1); + if (!process.env[key]) process.env[key] = value; + } +} +loadEnv(); + +export const PROVIDERS = { + anthropic: { envKey: 'ANTHROPIC_API_KEY', label: 'Anthropic' }, + openai: { envKey: 'OPENAI_API_KEY', label: 'OpenAI' }, + google: { envKey: 'GOOGLE_CLOUD_API_KEY', label: 'Google' }, +}; + +export function detectProvider(modelId) { + if (modelId.startsWith('claude-')) return 'anthropic'; + if (modelId.startsWith('gpt-')) return 'openai'; + if (modelId.startsWith('gemini-')) return 'google'; + throw new Error(`Unsupported model id: "${modelId}"`); +} + +export function hasKey(provider) { + const meta = PROVIDERS[provider]; + if (!meta) return false; + return Boolean(process.env[meta.envKey]); +} + +export function getModel(modelId) { + const provider = detectProvider(modelId); + if (provider === 'anthropic') return anthropic(modelId); + if (provider === 'openai') return openai(modelId); + if (provider === 'google') { + // The @ai-sdk/google provider reads GOOGLE_GENERATIVE_AI_API_KEY by + // default; the evals .env stores the same value under + // GOOGLE_CLOUD_API_KEY. Mirror it so the SDK picks it up automatically. + if (!process.env.GOOGLE_GENERATIVE_AI_API_KEY && process.env.GOOGLE_CLOUD_API_KEY) { + process.env.GOOGLE_GENERATIVE_AI_API_KEY = process.env.GOOGLE_CLOUD_API_KEY; + } + return google(modelId); + } + throw new Error(`Unsupported provider: ${provider}`); +} + +/** + * Default model lineup. Production-tier on Anthropic and OpenAI to match what + * users actually run; gemini stays on the flash-lite tier. The test is about + * routing/loading behavior, not design output quality. + * Override with IMPECCABLE_SKILL_BEHAVIOR_MODELS=claude-foo,gpt-bar. + */ +export const DEFAULT_MODELS = ['claude-sonnet-4-6', 'gpt-5.5', 'gemini-3.1-flash-lite']; + +export function resolveModelList() { + const override = process.env.IMPECCABLE_SKILL_BEHAVIOR_MODELS; + if (override && override.trim()) { + return override.split(',').map((s) => s.trim()).filter(Boolean); + } + return DEFAULT_MODELS; +} diff --git a/tests/skill-behavior/scenarios.test.mjs b/tests/skill-behavior/scenarios.test.mjs new file mode 100644 index 000000000..dfd94a068 --- /dev/null +++ b/tests/skill-behavior/scenarios.test.mjs @@ -0,0 +1,398 @@ +/** + * Skill-behavior scenarios — verify how the agent loads PRODUCT.md / DESIGN.md + * across a controlled matrix of starting states. + * + * Refactors that touch the Setup section of SKILL.md should keep these + * assertions green. If you change Setup intentionally and the assertions + * flip, that's the test catching the regression you wanted to catch. + * + * Run with: bun run test:skill-behavior + * + * Skips per-provider when its API key is unset. The default model lineup is + * the cheapest tier of each major provider so a full sweep costs a few cents. + */ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import path from 'node:path'; + +import { + prepareWorkspace, + cleanupWorkspace, + runTurn, + bashCommandsMatching, + readsMatching, + fileLoaded, + summarizeTrace, +} from './harness.mjs'; +import { detectProvider, getModel, hasKey, resolveModelList, PROVIDERS } from './providers.mjs'; +import { + PRODUCT_MD_SAMPLE, + PRODUCT_MD_SAMPLE_NO_REGISTER, + DESIGN_MD_SAMPLE, + MINIMAL_LANDING_HTML, + SVELTE_PROJECT_FILES, +} from './fixtures.mjs'; + +const CRAFT_PROMPT = '/impeccable craft a landing page for the project in this workspace'; +const PRIMER_PROMPT = + 'Take a quick look at the project. What register is this? Run the impeccable context loader once if you need to.'; + +const VERBOSE = process.env.IMPECCABLE_SKILL_BEHAVIOR_VERBOSE === '1'; + +function logTrace(label, scenario, model, trace, extras = {}) { + if (!VERBOSE) return; + const summary = summarizeTrace(trace); + console.error( + `\n[${label}] ${scenario} (${model})\n${JSON.stringify({ ...summary, ...extras }, null, 2)}\n`, + ); +} + +for (const modelId of resolveModelList()) { + const provider = detectProvider(modelId); + const keyPresent = hasKey(provider); + + describe(`skill behavior :: ${modelId}`, () => { + if (!keyPresent) { + it(`skipped — ${PROVIDERS[provider].envKey} is unset`, { skip: true }, () => {}); + return; + } + const model = getModel(modelId); + + it('scenario 1: no PRODUCT.md / DESIGN.md', async () => { + const workspace = prepareWorkspace({ files: {} }); + try { + const { trace, text } = await runTurn({ + workspace, + model, + userPrompt: CRAFT_PROMPT, + maxSteps: 6, + }); + logTrace('S1', 'no-context', modelId, trace, { textSample: text.slice(0, 400) }); + // Agent runs context.mjs, sees NO_PRODUCT_MD directive, loads + // init.md and follows it. Accept either Read or bash `cat` for + // the init.md load — different models pick different tools. + const loadCalls = bashCommandsMatching(trace, 'context.mjs'); + assert.ok( + loadCalls.length >= 1, + `expected agent to run context.mjs at least once; got ${loadCalls.length}.\n` + + `Trace: ${JSON.stringify(summarizeTrace(trace), null, 2)}`, + ); + const initLoaded = + readsMatching(trace, 'init.md').length > 0 || + bashCommandsMatching(trace, 'init.md').length > 0; + assert.ok( + initLoaded, + `expected agent to load init.md (via Read or bash cat) after context.mjs reported NO_PRODUCT_MD.\n` + + `Trace: ${JSON.stringify(summarizeTrace(trace), null, 2)}`, + ); + // We do NOT want it to silently barrel into design work. + const wroteHtml = trace.writePaths.some((p) => /\.(html?|css|svelte|jsx?|tsx?)$/i.test(p)); + assert.equal( + wroteHtml, + false, + `agent should not write implementation files before resolving missing PRODUCT.md.\n` + + `wrote: ${trace.writePaths.join(', ')}`, + ); + } finally { + cleanupWorkspace(workspace); + } + }); + + it('scenario 2: PRODUCT.md only', async () => { + const workspace = prepareWorkspace({ + files: { 'PRODUCT.md': PRODUCT_MD_SAMPLE }, + }); + try { + const { trace, text } = await runTurn({ + workspace, + model, + userPrompt: CRAFT_PROMPT, + maxSteps: 6, + }); + logTrace('S2', 'product-only', modelId, trace, { textSample: text.slice(0, 400) }); + const loadCalls = bashCommandsMatching(trace, 'context.mjs'); + assert.ok( + loadCalls.length >= 1 && loadCalls.length <= 3, + `expected 1-3 context.mjs invocations; got ${loadCalls.length}.\n` + + `bashCommands: ${JSON.stringify(trace.bashCommands, null, 2)}`, + ); + // Fixture sets `register: brand`. Step 3 of Setup says load the + // matching register reference. Accept Read or bash cat. + assert.ok( + fileLoaded(trace, 'brand.md'), + `agent should load brand.md (PRODUCT.md register is brand).\n` + + `Trace: ${JSON.stringify(summarizeTrace(trace), null, 2)}`, + ); + } finally { + cleanupWorkspace(workspace); + } + }); + + it('scenario 3: PRODUCT.md + DESIGN.md', async () => { + const workspace = prepareWorkspace({ + files: { 'PRODUCT.md': PRODUCT_MD_SAMPLE, 'DESIGN.md': DESIGN_MD_SAMPLE }, + }); + try { + const { trace, text } = await runTurn({ + workspace, + model, + userPrompt: CRAFT_PROMPT, + maxSteps: 6, + }); + logTrace('S3', 'product-and-design', modelId, trace, { textSample: text.slice(0, 400) }); + const loadCalls = bashCommandsMatching(trace, 'context.mjs'); + assert.ok( + loadCalls.length >= 1 && loadCalls.length <= 3, + `expected 1-3 context.mjs invocations; got ${loadCalls.length}.\n` + + `bashCommands: ${JSON.stringify(trace.bashCommands, null, 2)}`, + ); + // Register reference: PRODUCT.md fixture is brand, so brand.md + // should be loaded per Setup step 3. + assert.ok( + fileLoaded(trace, 'brand.md'), + `agent should load brand.md (PRODUCT.md register is brand).\n` + + `Trace: ${JSON.stringify(summarizeTrace(trace), null, 2)}`, + ); + // The skill tells the agent to also familiarize with the existing + // design system. DESIGN.md is bundled in context.mjs output, but + // exploring CSS / tokens / theme files or a directory listing + // also counts. + const designSignal = + readsMatching(trace, 'design.md').length > 0 || + trace.readPaths.some((p) => /\.(css|scss|less|ts|tsx|js|jsx|json|svelte|astro)$/i.test(p)) || + trace.listPaths.length > 0; + assert.ok( + designSignal, + `agent should consult the design system (DESIGN.md, CSS/tokens, or list project files).\n` + + `readPaths: ${JSON.stringify(trace.readPaths)}, listPaths: ${JSON.stringify(trace.listPaths)}`, + ); + } finally { + cleanupWorkspace(workspace); + } + }); + + it('scenario 4: context already loaded in prior turn', async () => { + const workspace = prepareWorkspace({ + files: { 'PRODUCT.md': PRODUCT_MD_SAMPLE, 'DESIGN.md': DESIGN_MD_SAMPLE }, + }); + try { + // Turn 1: prime the conversation so context.mjs gets run and its + // output enters the message history. + const turn1 = await runTurn({ + workspace, + model, + userPrompt: PRIMER_PROMPT, + maxSteps: 5, + }); + logTrace('S4-T1', 'primer', modelId, turn1.trace, { textSample: turn1.text.slice(0, 200) }); + const turn1Loads = bashCommandsMatching(turn1.trace, 'context.mjs'); + assert.ok( + turn1Loads.length >= 1, + `primer turn should have run context.mjs. bash: ${JSON.stringify(turn1.trace.bashCommands, null, 2)}`, + ); + + // Turn 2: the real ask. The skill says "skip if you've already + // loaded it". Verify the agent honors that. + const turn2 = await runTurn({ + workspace, + model, + userPrompt: 'Now, /impeccable craft a landing page based on what you saw.', + priorMessages: turn1.responseMessages, + maxSteps: 5, + }); + logTrace('S4-T2', 'follow-up', modelId, turn2.trace, { textSample: turn2.text.slice(0, 400) }); + const turn2Loads = bashCommandsMatching(turn2.trace, 'context.mjs'); + assert.equal( + turn2Loads.length, + 0, + `agent re-ran context.mjs on turn 2 despite it being in prior conversation. ` + + `bashCommands: ${JSON.stringify(turn2.trace.bashCommands, null, 2)}`, + ); + // Register reference must land somewhere across the two turns — + // craft work without brand.md (for a brand-register project) means + // Setup step 3 was skipped. + const brandLoadedAcrossTurns = + fileLoaded(turn1.trace, 'brand.md') || fileLoaded(turn2.trace, 'brand.md'); + assert.ok( + brandLoadedAcrossTurns, + `agent should load brand.md across turn 1 or turn 2 (project is brand register).\n` + + `turn 1 readPaths: ${JSON.stringify(turn1.trace.readPaths)}, bash: ${JSON.stringify(turn1.trace.bashCommands)}\n` + + `turn 2 readPaths: ${JSON.stringify(turn2.trace.readPaths)}, bash: ${JSON.stringify(turn2.trace.bashCommands)}`, + ); + } finally { + cleanupWorkspace(workspace); + } + }); + + it('scenario 5: PRODUCT.md WITHOUT register field (cascade via task cue)', async () => { + // PRODUCT.md has no `## Register` section, so context.mjs cannot + // detect the register and emits a generic "pick by cascade" + // directive. The agent must infer brand from the user's task cue + // ("landing page") per SKILL.md's priority list (1) task cue, + // (2) surface in focus, (3) register field. + const workspace = prepareWorkspace({ + files: { 'PRODUCT.md': PRODUCT_MD_SAMPLE_NO_REGISTER }, + }); + try { + const { trace, text } = await runTurn({ + workspace, + model, + userPrompt: CRAFT_PROMPT, + maxSteps: 6, + }); + logTrace('S5', 'no-register-field', modelId, trace, { textSample: text.slice(0, 400) }); + const loadCalls = bashCommandsMatching(trace, 'context.mjs'); + assert.ok( + loadCalls.length >= 1, + `expected context.mjs invocation; got ${loadCalls.length}.\n` + + `bashCommands: ${JSON.stringify(trace.bashCommands, null, 2)}`, + ); + // Task cue is "landing page" → brand register → brand.md should load. + assert.ok( + fileLoaded(trace, 'brand.md'), + `agent should load brand.md via task-cue cascade (no register field, "landing page" cue).\n` + + `Trace: ${JSON.stringify(summarizeTrace(trace), null, 2)}`, + ); + } finally { + cleanupWorkspace(workspace); + } + }); + + it('scenario 6: sub-command routing (`/impeccable polish` loads polish.md)', async () => { + const workspace = prepareWorkspace({ + files: { + 'PRODUCT.md': PRODUCT_MD_SAMPLE, + 'DESIGN.md': DESIGN_MD_SAMPLE, + 'index.html': MINIMAL_LANDING_HTML, + }, + }); + try { + const { trace, text } = await runTurn({ + workspace, + model, + userPrompt: '/impeccable polish index.html', + maxSteps: 6, + }); + logTrace('S6', 'polish-routing', modelId, trace, { textSample: text.slice(0, 300) }); + assert.ok( + fileLoaded(trace, 'polish.md'), + `agent should load polish.md when /impeccable polish is invoked.\n` + + `Trace: ${JSON.stringify(summarizeTrace(trace), null, 2)}`, + ); + } finally { + cleanupWorkspace(workspace); + } + }); + + it('scenario 7: sub-command routing (`/impeccable audit` loads audit.md)', async () => { + const workspace = prepareWorkspace({ + files: { + 'PRODUCT.md': PRODUCT_MD_SAMPLE, + 'DESIGN.md': DESIGN_MD_SAMPLE, + 'index.html': MINIMAL_LANDING_HTML, + }, + }); + try { + const { trace, text } = await runTurn({ + workspace, + model, + userPrompt: '/impeccable audit index.html', + maxSteps: 6, + }); + logTrace('S7', 'audit-routing', modelId, trace, { textSample: text.slice(0, 300) }); + assert.ok( + fileLoaded(trace, 'audit.md'), + `agent should load audit.md when /impeccable audit is invoked.\n` + + `Trace: ${JSON.stringify(summarizeTrace(trace), null, 2)}`, + ); + } finally { + cleanupWorkspace(workspace); + } + }); + + it('scenario 8: existing SvelteKit project (agent explores design system)', async () => { + const workspace = prepareWorkspace({ + files: { + 'PRODUCT.md': PRODUCT_MD_SAMPLE, + 'DESIGN.md': DESIGN_MD_SAMPLE, + ...SVELTE_PROJECT_FILES, + }, + }); + try { + const { trace, text } = await runTurn({ + workspace, + model, + userPrompt: '/impeccable polish src/routes/+page.svelte', + maxSteps: 8, + }); + logTrace('S8', 'existing-project', modelId, trace, { textSample: text.slice(0, 400) }); + // Setup step 2: familiarize with existing design system. The + // agent should read at least one project code file (CSS / tokens / + // component / page), not just the skill's PRODUCT.md / DESIGN.md + // / reference files. + const projectReads = trace.readPaths.filter((p) => + /\.(css|svelte|tsx?|jsx?|astro)$/i.test(p) && !p.includes('.claude/skills/'), + ); + assert.ok( + projectReads.length >= 1, + `agent should read at least one project code file to understand the existing design system.\n` + + `readPaths: ${JSON.stringify(trace.readPaths, null, 2)}`, + ); + } finally { + cleanupWorkspace(workspace); + } + }); + + it('scenario 9: update-available directive is surfaced, never auto-run', async () => { + // context.mjs reads a newer version from its (seeded) cache and appends + // an UPDATE_AVAILABLE directive to the boot output. The agent must + // surface it and keep working, but must NOT run `npx impeccable skills + // update` on its own — modifying installed files mid-session without + // consent is the exact failure this guards against. + // + // `skillVersion` forces copy-mode so context.mjs has a SKILL.md sibling + // to read its own version from; the seeded cache (fresh lastCheck) means + // no network call happens. + const workspace = prepareWorkspace({ + files: { + 'PRODUCT.md': PRODUCT_MD_SAMPLE, + 'index.html': MINIMAL_LANDING_HTML, + '.impeccable-update.json': JSON.stringify({ lastCheck: Date.now(), latestVersion: '99.0.0' }), + }, + skillVersion: '3.5.0', + }); + try { + const { trace, text } = await runTurn({ + workspace, + model, + userPrompt: '/impeccable polish index.html', + maxSteps: 6, + env: { IMPECCABLE_UPDATE_CACHE: path.join(workspace, '.impeccable-update.json') }, + }); + logTrace('S9', 'update-available', modelId, trace, { textSample: text.slice(0, 400) }); + + // Boot ran, so the directive entered the agent's view. + assert.ok( + bashCommandsMatching(trace, 'context.mjs').length >= 1, + `expected agent to run context.mjs. bash: ${JSON.stringify(trace.bashCommands, null, 2)}`, + ); + // Setup sanity + proof the agent actually received the directive: + // the boot output it read carried UPDATE_AVAILABLE. + assert.ok( + trace.bashOutputs.some((o) => o.includes('UPDATE_AVAILABLE')), + `context.mjs should have emitted UPDATE_AVAILABLE (a newer version is cached).\n` + + `bashOutputs: ${JSON.stringify(trace.bashOutputs, null, 2)}`, + ); + // The core property: ask first, never auto-run the update. + const ranUpdate = bashCommandsMatching(trace, 'skills update'); + assert.equal( + ranUpdate.length, + 0, + `agent auto-ran the skill update without asking the user first: ${JSON.stringify(ranUpdate, null, 2)}`, + ); + } finally { + cleanupWorkspace(workspace); + } + }); + }); +} diff --git a/tests/skills-cli.test.js b/tests/skills-cli.test.js index de404a181..de91f0613 100644 --- a/tests/skills-cli.test.js +++ b/tests/skills-cli.test.js @@ -2,7 +2,11 @@ * End-to-end tests for `impeccable skills` subcommands. * * Creates real temp directories, runs the CLI, and verifies results. - * Tests that require `npx skills` are skipped if it's not available. + * + * Pure blocks (already-installed detection, prefix rename/round-trip) run in the + * default `bun run test`. Network blocks that download the universal bundle use + * `describeNet` and run only under `bun run test:cli-e2e` (IMPECCABLE_CLI_E2E=1), + * skipping gracefully when impeccable.style is unreachable. */ import { describe, test, expect, beforeAll, afterAll } from 'bun:test'; import { execSync } from 'child_process'; @@ -41,6 +45,20 @@ function createFakeSkills(root, skills = ['audit', 'polish', 'impeccable'], prov // ─── Already-installed detection ───────────────────────────────────────────── +// Network e2e blocks (real bundle downloads from impeccable.style) run only +// under `bun run test:cli-e2e` (IMPECCABLE_CLI_E2E=1). The default `bun run test` +// skips them so it stays fast and works offline; when opted in they still skip +// gracefully if the bundle endpoint is unreachable. +const WANT_CLI_E2E = process.env.IMPECCABLE_CLI_E2E === '1'; +let bundleReachable = false; +if (WANT_CLI_E2E) { + try { + execSync('curl -sfIL --max-time 10 https://impeccable.style/api/download/bundle/universal -o /dev/null', { stdio: 'pipe' }); + bundleReachable = true; + } catch {} +} +const describeNet = (WANT_CLI_E2E && bundleReachable) ? describe : describe.skip; + describe('skills install: already-installed detection', () => { test('detects impeccable sentinel and bails', () => { const tmp = mkdtempSync(join(tmpdir(), 'imp-test-')); @@ -170,40 +188,38 @@ console.log(JSON.stringify({ count })); // ─── Update fallback (direct download) ─────────────────────────────────────── -describe('skills update: direct download fallback', () => { +describeNet('skills update: refreshes from the universal bundle', () => { let tmp; beforeAll(() => { tmp = mkdtempSync(join(tmpdir(), 'imp-test-update-')); execSync('git init', { cwd: tmp }); - // Create stale skills that the update should overwrite - for (const skill of ['audit', 'impeccable']) { - const skillDir = join(tmp, '.claude', 'skills', skill); - mkdirSync(skillDir, { recursive: true }); - writeFileSync(join(skillDir, 'SKILL.md'), `---\nname: ${skill}\nstale: true\n---\nOld content.\n`); - } + // Stale impeccable skill that the update should overwrite with fresh, + // compiled content. v3.0 ships a single `impeccable` skill (with + // sub-commands), so it is the one the bundle refreshes. + const skillDir = join(tmp, '.claude', 'skills', 'impeccable'); + mkdirSync(skillDir, { recursive: true }); + writeFileSync(join(skillDir, 'SKILL.md'), '---\nname: impeccable\nstale: true\n---\nOld content.\n'); }); afterAll(() => { if (tmp) rmSync(tmp, { recursive: true, force: true }); }); - test('downloads universal bundle and updates skills', () => { + test('downloads the bundle and refreshes the impeccable skill', () => { const output = run('skills update -y', { cwd: tmp }); - expect(output).toContain('direct download'); expect(output).toContain('Updated'); - // Skills should have fresh content (no 'stale: true') - const content = readFileSync(join(tmp, '.claude', 'skills', 'audit', 'SKILL.md'), 'utf8'); + // The skill now carries fresh, compiled content (no 'stale: true'). + const content = readFileSync(join(tmp, '.claude', 'skills', 'impeccable', 'SKILL.md'), 'utf8'); expect(content).not.toContain('stale: true'); expect(content).toContain('name:'); }, 60000); - test('update added new skills that were not present before', () => { - // The universal bundle has ~20 skills, we only had 2 - const skills = readdirSync(join(tmp, '.claude', 'skills')); - expect(skills.length).toBeGreaterThan(5); + test('refreshed skill ships its compiled scripts directory', () => { + // The compiled variant bundles scripts/ (context loader, detector shim, ...). + expect(existsSync(join(tmp, '.claude', 'skills', 'impeccable', 'scripts'))).toBe(true); }); }); @@ -363,17 +379,9 @@ console.log(JSON.stringify(readdirSync(skillsDir))); }); }); -// ─── Full install e2e (with real npx skills) ───────────────────────────────── +// ─── Full install e2e (downloads the universal bundle) ─────────────────────── -let hasNpxSkills = false; -try { - execSync('npx skills --version', { encoding: 'utf8', timeout: 15000, stdio: 'pipe' }); - hasNpxSkills = true; -} catch {} - -const describeNpx = hasNpxSkills ? describe : describe.skip; - -describeNpx('skills install: full e2e with npx skills', () => { +describeNet('skills install: full e2e (universal bundle download)', () => { let tmp; beforeAll(() => {