diff --git a/.agents/skills/adapt/SKILL.md b/.agents/skills/adapt/SKILL.md index 165c66dcc..537c15b32 100644 --- a/.agents/skills/adapt/SKILL.md +++ b/.agents/skills/adapt/SKILL.md @@ -9,7 +9,7 @@ Adapt existing designs to work effectively across different contexts - different ## MANDATORY PREPARATION -Invoke /frontend-design — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /teach-impeccable first. Additionally gather: target platforms/devices and usage contexts. +Invoke /impeccable — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /impeccable teach first. Additionally gather: target platforms/devices and usage contexts. --- diff --git a/.agents/skills/animate/SKILL.md b/.agents/skills/animate/SKILL.md index 3d006d15e..a28332e96 100644 --- a/.agents/skills/animate/SKILL.md +++ b/.agents/skills/animate/SKILL.md @@ -9,7 +9,7 @@ Analyze a feature and strategically add animations and micro-interactions that e ## MANDATORY PREPARATION -Invoke /frontend-design — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /teach-impeccable first. Additionally gather: performance constraints. +Invoke /impeccable — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /impeccable teach first. Additionally gather: performance constraints. --- diff --git a/.agents/skills/arrange/SKILL.md b/.agents/skills/arrange/SKILL.md index ce4cf3ad6..d63c1aab0 100644 --- a/.agents/skills/arrange/SKILL.md +++ b/.agents/skills/arrange/SKILL.md @@ -9,7 +9,7 @@ Assess and improve layout and spacing that feels monotonous, crowded, or structu ## MANDATORY PREPARATION -Invoke /frontend-design — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /teach-impeccable first. +Invoke /impeccable — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /impeccable teach first. --- @@ -46,7 +46,7 @@ Analyze what's weak about the current spatial design: ## Plan Layout Improvements -Consult the [spatial design reference](reference/spatial-design.md) from the frontend-design skill for detailed guidance on grids, rhythm, and container queries. +Consult the [spatial design reference](reference/spatial-design.md) from the impeccable skill for detailed guidance on grids, rhythm, and container queries. Create a systematic plan: diff --git a/.agents/skills/audit/SKILL.md b/.agents/skills/audit/SKILL.md index 1debe043e..313959a02 100644 --- a/.agents/skills/audit/SKILL.md +++ b/.agents/skills/audit/SKILL.md @@ -7,7 +7,7 @@ argument-hint: "[area (feature, page, component...)]" ## MANDATORY PREPARATION -Invoke /frontend-design — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /teach-impeccable first. +Invoke /impeccable — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /impeccable teach first. --- @@ -65,7 +65,7 @@ Run comprehensive checks across 5 dimensions. Score each dimension 0-4 using the ### 5. Anti-Patterns (CRITICAL) -Check against ALL the **DON'T** guidelines in the frontend-design skill. Look for AI slop tells (AI color palette, gradient text, glassmorphism, hero metrics, card grids, generic fonts) and general design anti-patterns (gray on color, nested cards, bounce easing, redundant copy). +Check against ALL the **DON'T** guidelines in the impeccable skill. Look for AI slop tells (AI color palette, gradient text, glassmorphism, hero metrics, card grids, generic fonts) and general design anti-patterns (gray on color, nested cards, bounce easing, redundant copy). **Score 0-4**: 0=AI slop gallery (5+ tells), 1=Heavy AI aesthetic (3-4 tells), 2=Some tells (1-2 noticeable), 3=Mostly clean (subtle issues only), 4=No AI tells (distinctive, intentional design) @@ -108,7 +108,7 @@ For each issue, document: - **Impact**: How it affects users - **WCAG/Standard**: Which standard it violates (if applicable) - **Recommendation**: How to fix it -- **Suggested command**: Which command to use (prefer: /animate, /quieter, /optimize, /adapt, /clarify, /distill, /delight, /onboard, /normalize, /audit, /harden, /polish, /extract, /bolder, /arrange, /typeset, /critique, /colorize, /overdrive) +- **Suggested command**: Which command to use (prefer: /animate, /quieter, /shape, /optimize, /adapt, /clarify, /distill, /delight, /onboard, /normalize, /audit, /harden, /polish, /extract, /bolder, /arrange, /typeset, /critique, /colorize, /overdrive) ### Patterns & Systemic Issues @@ -127,7 +127,7 @@ List recommended commands in priority order (P0 first, then P1, then P2): 1. **[P?] `/command-name`** — Brief description (specific context from audit findings) 2. **[P?] `/command-name`** — Brief description (specific context) -**Rules**: Only recommend commands from: /animate, /quieter, /optimize, /adapt, /clarify, /distill, /delight, /onboard, /normalize, /audit, /harden, /polish, /extract, /bolder, /arrange, /typeset, /critique, /colorize, /overdrive. Map findings to the most appropriate command. End with `/polish` as the final step if any fixes were recommended. +**Rules**: Only recommend commands from: /animate, /quieter, /shape, /optimize, /adapt, /clarify, /distill, /delight, /onboard, /normalize, /audit, /harden, /polish, /extract, /bolder, /arrange, /typeset, /critique, /colorize, /overdrive. Map findings to the most appropriate command. End with `/polish` as the final step if any fixes were recommended. After presenting the summary, tell the user: diff --git a/.agents/skills/bolder/SKILL.md b/.agents/skills/bolder/SKILL.md index 0b03f025f..94c6dc8f2 100644 --- a/.agents/skills/bolder/SKILL.md +++ b/.agents/skills/bolder/SKILL.md @@ -9,7 +9,7 @@ Increase visual impact and personality in designs that are too safe, generic, or ## MANDATORY PREPARATION -Invoke /frontend-design — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /teach-impeccable first. +Invoke /impeccable — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /impeccable teach first. --- @@ -35,7 +35,7 @@ If any of these are unclear from the codebase, ask the user directly to clarify **CRITICAL**: "Bolder" doesn't mean chaotic or garish. It means distinctive, memorable, and confident. Think intentional drama, not random chaos. -**WARNING - AI SLOP TRAP**: When making things "bolder," AI defaults to the same tired tricks: cyan/purple gradients, glassmorphism, neon accents on dark backgrounds, gradient text on metrics. These are the OPPOSITE of bold—they're generic. Review ALL the DON'T guidelines in the frontend-design skill before proceeding. Bold means distinctive, not "more effects." +**WARNING - AI SLOP TRAP**: When making things "bolder," AI defaults to the same tired tricks: cyan/purple gradients, glassmorphism, neon accents on dark backgrounds, gradient text on metrics. These are the OPPOSITE of bold—they're generic. Review ALL the DON'T guidelines in the impeccable skill before proceeding. Bold means distinctive, not "more effects." ## Plan Amplification @@ -53,7 +53,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 frontend-design skill for inspiration) +- **Replace generic fonts**: Swap system fonts for distinctive choices (see impeccable skill 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) diff --git a/.agents/skills/clarify/SKILL.md b/.agents/skills/clarify/SKILL.md index 4db9228d6..0ee5eb997 100644 --- a/.agents/skills/clarify/SKILL.md +++ b/.agents/skills/clarify/SKILL.md @@ -9,7 +9,7 @@ Identify and improve unclear, confusing, or poorly written interface text to mak ## MANDATORY PREPARATION -Invoke /frontend-design — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /teach-impeccable first. Additionally gather: audience technical level and users' mental state in context. +Invoke /impeccable — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /impeccable teach first. Additionally gather: audience technical level and users' mental state in context. --- diff --git a/.agents/skills/colorize/SKILL.md b/.agents/skills/colorize/SKILL.md index 0b092ac08..726dfaca0 100644 --- a/.agents/skills/colorize/SKILL.md +++ b/.agents/skills/colorize/SKILL.md @@ -9,7 +9,7 @@ Strategically introduce color to designs that are too monochromatic, gray, or la ## MANDATORY PREPARATION -Invoke /frontend-design — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /teach-impeccable first. Additionally gather: existing brand colors. +Invoke /impeccable — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /impeccable teach first. Additionally gather: existing brand colors. --- diff --git a/.agents/skills/critique/SKILL.md b/.agents/skills/critique/SKILL.md index 70ac82f1d..eba93446f 100644 --- a/.agents/skills/critique/SKILL.md +++ b/.agents/skills/critique/SKILL.md @@ -1,102 +1,112 @@ --- name: critique -description: Evaluate design from a UX perspective, assessing visual hierarchy, information architecture, emotional resonance, cognitive load, and overall quality with quantitative scoring, persona-based testing, and actionable feedback. Use when the user asks to review, critique, evaluate, or give feedback on a design or component. +description: Evaluate design from a UX perspective, assessing visual hierarchy, information architecture, emotional resonance, cognitive load, and overall quality with quantitative scoring, persona-based testing, automated anti-pattern detection, and actionable feedback. Use when the user asks to review, critique, evaluate, or give feedback on a design or component. user-invocable: true argument-hint: "[area (feature, page, component...)]" --- -## MANDATORY PREPARATION +## STEPS -Invoke /frontend-design — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /teach-impeccable first. Additionally gather: what the interface is trying to accomplish. +### Step 1: Preparation ---- +Invoke /impeccable, which contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding. If no design context exists yet, you MUST run /impeccable teach first. Additionally gather: what the interface is trying to accomplish. -Conduct a holistic design critique, evaluating whether the interface actually works — not just technically, but as a designed experience. Think like a design director giving feedback. +### Step 2: Gather Assessments -## Phase 1: Design Critique +Launch two independent assessments. **Neither must see the other's output** to avoid bias. -Evaluate the interface across these dimensions: +You SHOULD delegate each assessment to a separate sub-agent for independence. Use your environment's agent spawning mechanism (e.g., Claude Code's `Agent` tool, or Codex's subagent spawning). Sub-agents should return their findings as structured text. Do NOT output findings to the user yet. -### 1. AI Slop Detection (CRITICAL) +If sub-agents are not available in the current environment, complete each assessment sequentially, writing findings to internal notes before proceeding. -**This is the most important check.** Does this look like every other AI-generated interface from 2024-2025? +**Tab isolation**: When browser automation is available, each assessment MUST create its own new tab. Never reuse an existing tab, even if one is already open at the correct URL. This prevents the two assessments from interfering with each other's page state. -Review the design against ALL the **DON'T** guidelines in the frontend-design skill — they are the fingerprints of AI-generated work. Check for the AI color palette, gradient text, dark mode with glowing accents, glassmorphism, hero metric layouts, identical card grids, generic fonts, and all other tells. +#### Assessment A: LLM Design Review -**The test**: If you showed this to someone and said "AI made this," would they believe you immediately? If yes, that's the problem. +Read the relevant source files (HTML, CSS, JS/TS) and, if browser automation is available, visually inspect the live page. **Create a new tab** for this; do not reuse existing tabs. After navigation, label the tab by setting the document title: +```javascript +document.title = '[LLM] ' + document.title; +``` +Think like a design director. Evaluate: -### 2. Visual Hierarchy -- Does the eye flow to the most important element first? -- Is there a clear primary action? Can you spot it in 2 seconds? -- Do size, color, and position communicate importance correctly? -- Is there visual competition between elements that should have different weights? +**AI Slop Detection (CRITICAL)**: Does this look like every other AI-generated interface? Review against ALL **DON'T** guidelines in the impeccable skill. Check for AI color palette, gradient text, dark glows, glassmorphism, hero metric layouts, identical card grids, generic fonts, and all other tells. **The test**: If someone said "AI made this," would you believe them immediately? -### 3. Information Architecture & Cognitive Load -> *Consult [cognitive-load](reference/cognitive-load.md) for the working memory rule and 8-item checklist* -- Is the structure intuitive? Would a new user understand the organization? -- Is related content grouped logically? -- Are there too many choices at once? Count visible options at each decision point — if >4, flag it -- Is the navigation clear and predictable? -- **Progressive disclosure**: Is complexity revealed only when needed, or dumped on the user upfront? -- **Run the 8-item cognitive load checklist** from the reference. Report failure count: 0–1 = low (good), 2–3 = moderate, 4+ = critical. +**Holistic Design Review**: visual hierarchy (eye flow, primary action clarity), information architecture (structure, grouping, cognitive load), emotional resonance (does it match brand and audience?), discoverability (are interactive elements obvious?), composition (balance, whitespace, rhythm), typography (hierarchy, readability, font choices), color (purposeful use, cohesion, accessibility), states & edge cases (empty, loading, error, success), microcopy (clarity, tone, helpfulness). -### 4. Emotional Journey +**Cognitive Load** (consult [cognitive-load](reference/cognitive-load.md)): +- Run the 8-item cognitive load checklist. Report failure count: 0-1 = low (good), 2-3 = moderate, 4+ = critical. +- Count visible options at each decision point. If >4, flag it. +- Check for progressive disclosure: is complexity revealed only when needed? + +**Emotional Journey**: - What emotion does this interface evoke? Is that intentional? -- Does it match the brand personality? -- Does it feel trustworthy, approachable, premium, playful — whatever it should feel? -- Would the target user feel "this is for me"? -- **Peak-end rule**: Is the most intense moment positive? Does the experience end well (confirmation, celebration, clear next step)? -- **Emotional valleys**: Check for onboarding frustration, error cliffs, feature discovery gaps, or anxiety spikes at high-stakes moments (payment, delete, commit) -- **Interventions at negative moments**: Are there design interventions where users are likely to feel frustrated or anxious? (progress indicators, reassurance copy, undo options, social proof) +- **Peak-end rule**: Is the most intense moment positive? Does the experience end well? +- **Emotional valleys**: Check for anxiety spikes at high-stakes moments (payment, delete, commit). Are there design interventions (progress indicators, reassurance copy, undo options)? -### 5. Discoverability & Affordance -- Are interactive elements obviously interactive? -- Would a user know what to do without instructions? -- Are hover/focus states providing useful feedback? -- Are there hidden features that should be more visible? +**Nielsen's Heuristics** (consult [heuristics-scoring](reference/heuristics-scoring.md)): +Score each of the 10 heuristics 0-4. This scoring will be presented in the report. -### 6. Composition & Balance -- Does the layout feel balanced or uncomfortably weighted? -- Is whitespace used intentionally or just leftover? -- Is there visual rhythm in spacing and repetition? -- Does asymmetry feel designed or accidental? +Return structured findings covering: AI slop verdict, heuristic scores, cognitive load assessment, what's working (2-3 items), priority issues (3-5 with what/why/fix), minor observations, and provocative questions. -### 7. Typography as Communication -- Does the type hierarchy clearly signal what to read first, second, third? -- Is body text comfortable to read? (line length, spacing, size) -- Do font choices reinforce the brand/tone? -- Is there enough contrast between heading levels? +#### Assessment B: Automated Detection -### 8. Color with Purpose -- Is color used to communicate, not just decorate? -- Does the palette feel cohesive? -- Are accent colors drawing attention to the right things? -- Does it work for colorblind users? (not just technically — does meaning still come through?) +Run the bundled deterministic detector, which flags 25 specific patterns (AI slop tells + general design quality). -### 9. States & Edge Cases -- Empty states: Do they guide users toward action, or just say "nothing here"? -- Loading states: Do they reduce perceived wait time? -- Error states: Are they helpful and non-blaming? -- Success states: Do they confirm and guide next steps? +**CLI scan**: +```bash +npx impeccable --json [--fast] [target] +``` -### 10. Microcopy & Voice -- Is the writing clear and concise? -- Does it sound like a human (the right human for this brand)? -- Are labels and buttons unambiguous? -- Does error copy help users fix the problem? +- Pass HTML/JSX/TSX/Vue/Svelte files or directories as `[target]` (anything with markup). Do not pass CSS-only files. +- For URLs, skip the CLI scan (it requires Puppeteer). Use browser visualization instead. +- For large directories (200+ scannable files), use `--fast` (regex-only, skips jsdom) +- For 500+ files, narrow scope or ask the user +- Exit code 0 = clean, 2 = findings -## Phase 2: Present Findings +**Browser visualization** (when browser automation tools are available AND the target is a viewable page): + +The overlay is a **visual aid for the user**. It highlights issues directly in their browser. Do NOT scroll through the page to screenshot overlays. Instead, read the console output to get the results programmatically. + +1. **Start the live detection server**: + ```bash + npx impeccable live & + ``` + Note the port printed to stdout (auto-assigned). Use `--port=PORT` to fix it. +2. **Create a new tab** and navigate to the page (use dev server URL for local files, or direct URL). Do not reuse existing tabs. +3. **Label the tab** via `javascript_tool` so the user can distinguish it: + ```javascript + document.title = '[Human] ' + document.title; + ``` +4. **Scroll to top** to ensure the page is scrolled to the very top before injection +5. **Inject** via `javascript_tool` (replace PORT with the port from step 1): + ```javascript + const s = document.createElement('script'); s.src = 'http://localhost:PORT/detect.js'; document.head.appendChild(s); + ``` +6. Wait 2-3 seconds for the detector to render overlays +7. **Read results from console** using `read_console_messages` with pattern `impeccable`. The detector logs all findings with the `[impeccable]` prefix. Do NOT scroll through the page to take screenshots of the overlays. +8. **Cleanup**: Stop the live server when done: + ```bash + npx impeccable live stop + ``` + +For multi-view targets, inject on 3-5 representative pages. If injection fails, continue with CLI results only. + +Return: CLI findings (JSON), browser console findings (if applicable), and any false positives noted. + +### Step 3: Generate Combined Critique Report + +Synthesize both assessments into a single report. Do NOT simply concatenate. Weave the findings together, noting where the LLM review and detector agree, where the detector caught issues the LLM missed, and where detector findings are false positives. Structure your feedback as a design director would: -### Design Health Score +#### Design Health Score > *Consult [heuristics-scoring](reference/heuristics-scoring.md)* -Score each of Nielsen's 10 heuristics 0–4. Present as a table: +Present the Nielsen's 10 heuristics scores as a table: | # | Heuristic | Score | Key Issue | |---|-----------|-------|-----------| -| 1 | Visibility of System Status | ? | [specific finding or "—" if solid] | +| 1 | Visibility of System Status | ? | [specific finding or "n/a" if solid] | | 2 | Match System / Real World | ? | | | 3 | User Control and Freedom | ? | | | 4 | Consistency and Standards | ? | | @@ -108,84 +118,97 @@ Score each of Nielsen's 10 heuristics 0–4. Present as a table: | 10 | Help and Documentation | ? | | | **Total** | | **??/40** | **[Rating band]** | -Be honest with scores. A 4 means genuinely excellent. Most real interfaces score 20–32. +Be honest with scores. A 4 means genuinely excellent. Most real interfaces score 20-32. -### Anti-Patterns Verdict -**Start here.** Pass/fail: Does this look AI-generated? List specific tells from the skill's Anti-Patterns section. Be brutally honest. +#### Anti-Patterns Verdict -### Overall Impression -A brief gut reaction — what works, what doesn't, and the single biggest opportunity. +**Start here.** Does this look AI-generated? -### What's Working -Highlight 2–3 things done well. Be specific about why they work. +**LLM assessment**: Your own evaluation of AI slop tells. Cover overall aesthetic feel, layout sameness, generic composition, missed opportunities for personality. -### Priority Issues -The 3–5 most impactful design problems, ordered by importance. +**Deterministic scan**: Summarize what the automated detector found, with counts and file locations. Note any additional issues the detector caught that you missed, and flag any false positives. -For each issue, tag with **P0–P3 severity** (consult [heuristics-scoring](reference/heuristics-scoring.md) for severity definitions): +**Visual overlays** (if browser was used): Tell the user that overlays are now visible in the **[Human]** tab in their browser, highlighting the detected issues. Summarize what the console output reported. + +#### Overall Impression +A brief gut reaction: what works, what doesn't, and the single biggest opportunity. + +#### What's Working +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](reference/heuristics-scoring.md) for severity 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: /animate, /quieter, /optimize, /adapt, /clarify, /distill, /delight, /onboard, /normalize, /audit, /harden, /polish, /extract, /bolder, /arrange, /typeset, /critique, /colorize, /overdrive) +- **Suggested command**: Which command could address this (from: /animate, /quieter, /shape, /optimize, /adapt, /clarify, /distill, /delight, /onboard, /normalize, /audit, /harden, /polish, /extract, /bolder, /arrange, /typeset, /critique, /colorize, /overdrive) -### Persona Red Flags +#### Persona Red Flags > *Consult [personas](reference/personas.md)* -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 `teach-impeccable`, 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 teach`, 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: -**Alex (Power User)**: No keyboard shortcuts detected. Form requires 8 clicks for primary action. Forced modal onboarding. ⚠️ High abandonment risk. +**Alex (Power User)**: No keyboard shortcuts detected. Form requires 8 clicks for primary action. Forced modal onboarding. High abandonment risk. -**Jordan (First-Timer)**: Icon-only nav in sidebar. Technical jargon in error messages ("404 Not Found"). No visible help. ⚠️ Will abandon at step 2. +**Jordan (First-Timer)**: Icon-only nav in sidebar. Technical jargon in error messages ("404 Not Found"). No visible help. Will abandon at step 2. -Be specific — name the exact elements and interactions that fail each persona. Don't write generic persona descriptions; write what broke for them. +Be specific. Name the exact elements and interactions that fail each persona. Don't write generic persona descriptions; write what broke for them. -### Minor Observations +#### Minor Observations Quick notes on smaller issues worth addressing. -**Remember**: -- Be direct — vague feedback wastes everyone's time -- Be specific — "the submit button" not "some elements" -- Say what's wrong AND why it matters to users -- Give concrete suggestions, not just "consider exploring..." -- Prioritize ruthlessly — if everything is important, nothing is -- Don't soften criticism — developers need honest feedback to ship great design +#### Questions to Consider +Provocative questions that might unlock better solutions: +- "What if the primary action were more prominent?" +- "Does this need to feel this complex?" +- "What would a confident version of this look like?" -## Phase 3: Ask the User +**Remember**: +- Be direct. Vague feedback wastes everyone's time. +- Be specific. "The submit button," not "some elements." +- Say what's wrong AND why it matters to users. +- Give concrete suggestions, not just "consider exploring..." +- Prioritize ruthlessly. If everything is important, nothing is. +- Don't soften criticism. Developers need honest feedback to ship great design. + +### Step 4: Ask the User **After presenting findings**, use targeted questions based on what was actually found. ask the user directly to clarify what you cannot infer. These answers will shape the action plan. -Ask questions along these lines (adapt to the specific findings — do NOT ask generic questions): +Ask questions along these lines (adapt to the specific findings; do NOT ask generic questions): -1. **Priority direction**: Based on the issues found, ask which category matters most to the user right now. For example: "I found problems with visual hierarchy, color usage, and information overload. Which area should we tackle first?" Offer the top 2–3 issue categories as options. +1. **Priority direction**: Based on the issues found, ask which category matters most to the user right now. For example: "I found problems with visual hierarchy, color usage, and information overload. Which area should we tackle first?" Offer the top 2-3 issue categories as options. -2. **Design intent**: If the critique found a tonal mismatch, ask whether it was intentional. For example: "The interface feels clinical and corporate. Is that the intended tone, or should it feel warmer/bolder/more playful?" Offer 2–3 tonal directions as options based on what would fix the issues found. +2. **Design intent**: If the critique found a tonal mismatch, ask whether it was intentional. For example: "The interface feels clinical and corporate. Is that the intended tone, or should it feel warmer/bolder/more playful?" Offer 2-3 tonal directions as options based on what would fix the issues found. 3. **Scope**: Ask how much the user wants to take on. For example: "I found N issues. Want to address everything, or focus on the top 3?" Offer scope options like "Top 3 only", "All issues", "Critical issues only". -4. **Constraints** (optional — only ask if relevant): If the findings touch many areas, ask if anything is off-limits. For example: "Should any sections stay as-is?" This prevents the plan from touching things the user considers done. +4. **Constraints** (optional; only ask if relevant): If the findings touch many areas, ask if anything is off-limits. For example: "Should any sections stay as-is?" This prevents the plan from touching things the user considers done. **Rules for questions**: -- Every question must reference specific findings from Phase 2 — never ask generic "who is your audience?" questions -- Keep it to 2–4 questions maximum — respect the user's time -- Offer concrete options, not open-ended prompts -- If findings are straightforward (e.g., only 1–2 clear issues), skip questions and go directly to Phase 4 +- Every question must reference specific findings from the report. Never ask generic "who is your audience?" questions. +- Keep it to 2-4 questions maximum. Respect the user's time. +- Offer concrete options, not open-ended prompts. +- If findings are straightforward (e.g., only 1-2 clear issues), skip questions and go directly to Step 5. -## Phase 4: Recommended Actions +### Step 5: Recommended Actions -**After receiving the user's answers**, present a prioritized action summary reflecting the user's priorities and scope from Phase 3. +**After receiving the user's answers**, present a prioritized action summary reflecting the user's priorities and scope from Step 4. -### Action Summary +#### Action Summary List recommended commands in priority order, based on the user's answers: -1. **`/command-name`** — Brief description of what to fix (specific context from critique findings) -2. **`/command-name`** — Brief description (specific context) +1. **`/command-name`**: Brief description of what to fix (specific context from critique findings) +2. **`/command-name`**: Brief description (specific context) ... **Rules for recommendations**: -- Only recommend commands from: /animate, /quieter, /optimize, /adapt, /clarify, /distill, /delight, /onboard, /normalize, /audit, /harden, /polish, /extract, /bolder, /arrange, /typeset, /critique, /colorize, /overdrive +- Only recommend commands from: /animate, /quieter, /shape, /optimize, /adapt, /clarify, /distill, /delight, /onboard, /normalize, /audit, /harden, /polish, /extract, /bolder, /arrange, /typeset, /critique, /colorize, /overdrive - Order by the user's stated priorities first, then by impact - Each item's description should carry enough context that the command knows what to focus on - Map each Priority Issue to the appropriate command diff --git a/.agents/skills/critique/reference/personas.md b/.agents/skills/critique/reference/personas.md index eb2f2b6d1..a22606aa2 100644 --- a/.agents/skills/critique/reference/personas.md +++ b/.agents/skills/critique/reference/personas.md @@ -159,7 +159,7 @@ Choose personas based on the interface type: ## Project-Specific Personas -If `.github/copilot-instructions.md` contains a `## Design Context` section (generated by `teach-impeccable`), derive 1–2 additional personas from the audience and brand information: +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 diff --git a/.agents/skills/delight/SKILL.md b/.agents/skills/delight/SKILL.md index fe39492d7..161a4b2b4 100644 --- a/.agents/skills/delight/SKILL.md +++ b/.agents/skills/delight/SKILL.md @@ -9,7 +9,7 @@ Identify opportunities to add moments of joy, personality, and unexpected polish ## MANDATORY PREPARATION -Invoke /frontend-design — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /teach-impeccable first. Additionally gather: what's appropriate for the domain (playful vs professional vs quirky vs elegant). +Invoke /impeccable — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /impeccable teach first. Additionally gather: what's appropriate for the domain (playful vs professional vs quirky vs elegant). --- diff --git a/.agents/skills/distill/SKILL.md b/.agents/skills/distill/SKILL.md index fd2a8e47f..fbe16f3e8 100644 --- a/.agents/skills/distill/SKILL.md +++ b/.agents/skills/distill/SKILL.md @@ -9,7 +9,7 @@ Remove unnecessary complexity from designs, revealing the essential elements and ## MANDATORY PREPARATION -Invoke /frontend-design — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /teach-impeccable first. +Invoke /impeccable — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /impeccable teach first. --- diff --git a/.agents/skills/frontend-design/SKILL.md b/.agents/skills/frontend-design/SKILL.md index dd7c5dd4c..459cf0542 100644 --- a/.agents/skills/frontend-design/SKILL.md +++ b/.agents/skills/frontend-design/SKILL.md @@ -1,147 +1,12 @@ --- name: frontend-design -description: Create distinctive, production-grade frontend interfaces with high design quality. Generates creative, polished code that avoids generic AI aesthetics. Use when the user asks to build web components, pages, artifacts, posters, or applications, or when any design skill requires project context. -license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution. +description: DEPRECATED: This skill has been renamed to 'impeccable'. Run /impeccable instead. --- -This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices. +This skill has been renamed to **impeccable**. All design functionality now lives in the `/impeccable` skill. -## Context Gathering Protocol +Check if the impeccable skill is available: +- If `/impeccable` exists, use it directly. You can safely inform the user that the `frontend-design` folder is a deprecated leftover and can be deleted. +- If `/impeccable` does NOT exist, tell the user to update their skills by running `npx impeccable skills update` in their terminal. -Design skills produce generic output without project context. You MUST have confirmed design context before doing any design work. - -**Required context** — every design skill needs at minimum: -- **Target audience**: Who uses this product and in what context? -- **Use cases**: What jobs are they trying to get done? -- **Brand personality/tone**: How should the interface feel? - -Individual skills may require additional context — check the skill's preparation section for specifics. - -**CRITICAL**: You cannot infer this context by reading the codebase. Code tells you what was built, not who it's for or what it should feel like. Only the creator can provide this context. - -**Gathering order:** -1. **Check current instructions (instant)**: If your loaded instructions already contain a **Design Context** section, proceed immediately. -2. **Check .impeccable.md (fast)**: If not in instructions, read `.impeccable.md` from the project root. If it exists and contains the required context, proceed. -3. **Run teach-impeccable (REQUIRED)**: If neither source has context, you MUST run /teach-impeccable NOW before doing anything else. Do NOT skip this step. Do NOT attempt to infer context from the codebase instead. - ---- - -## Design Direction - -Commit to a BOLD aesthetic direction: -- **Purpose**: What problem does this interface solve? Who uses it? -- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction. -- **Constraints**: Technical requirements (framework, performance, accessibility). -- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember? - -**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work—the key is intentionality, not intensity. - -Then implement working code that is: -- Production-grade and functional -- Visually striking and memorable -- Cohesive with a clear aesthetic point-of-view -- Meticulously refined in every detail - -## Frontend Aesthetics Guidelines - -### Typography -→ *Consult [typography reference](reference/typography.md) for scales, pairing, and loading strategies.* - -Choose fonts that are beautiful, unique, and interesting. Pair a distinctive display font with a refined body font. - -**DO**: Use a modular type scale with fluid sizing (clamp) -**DO**: Vary font weights and sizes to create clear visual hierarchy -**DON'T**: Use overused fonts—Inter, Roboto, Arial, Open Sans, system defaults -**DON'T**: Use monospace typography as lazy shorthand for "technical/developer" vibes -**DON'T**: Put large icons with rounded corners above every heading—they rarely add value and make sites look templated - -### Color & Theme -→ *Consult [color reference](reference/color-and-contrast.md) for OKLCH, palettes, and dark mode.* - -Commit to a cohesive palette. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. - -**DO**: Use modern CSS color functions (oklch, color-mix, light-dark) for perceptually uniform, maintainable palettes -**DO**: Tint your neutrals toward your brand hue—even a subtle hint creates subconscious cohesion -**DON'T**: Use gray text on colored backgrounds—it looks washed out; use a shade of the background color instead -**DON'T**: Use pure black (#000) or pure white (#fff)—always tint; pure black/white never appears in nature -**DON'T**: Use the AI color palette: cyan-on-dark, purple-to-blue gradients, neon accents on dark backgrounds -**DON'T**: Use gradient text for "impact"—especially on metrics or headings; it's decorative rather than meaningful -**DON'T**: Default to dark mode with glowing accents—it looks "cool" without requiring actual design decisions - -### Layout & Space -→ *Consult [spatial reference](reference/spatial-design.md) for grids, rhythm, and container queries.* - -Create visual rhythm through varied spacing—not the same padding everywhere. Embrace asymmetry and unexpected compositions. Break the grid intentionally for emphasis. - -**DO**: Create visual rhythm through varied spacing—tight groupings, generous separations -**DO**: Use fluid spacing with clamp() that breathes on larger screens -**DO**: Use asymmetry and unexpected compositions; break the grid intentionally for emphasis -**DON'T**: Wrap everything in cards—not everything needs a container -**DON'T**: Nest cards inside cards—visual noise, 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 -**DON'T**: Center everything—left-aligned text with asymmetric layouts feels more designed -**DON'T**: Use the same spacing everywhere—without rhythm, layouts feel monotonous - -### Visual Details -**DO**: Use intentional, purposeful decorative elements that reinforce brand -**DON'T**: Use glassmorphism everywhere—blur effects, glass cards, glow borders used decoratively rather than purposefully -**DON'T**: Use rounded elements with thick colored border on one side—a lazy accent that almost never looks intentional -**DON'T**: Use sparklines as decoration—tiny charts that look sophisticated but convey nothing meaningful -**DON'T**: Use rounded rectangles with generic drop shadows—safe, forgettable, could be any AI output -**DON'T**: Use modals unless there's truly no better alternative—modals are lazy - -### Motion -→ *Consult [motion reference](reference/motion-design.md) for timing, easing, and reduced motion.* - -Focus on high-impact moments: one well-orchestrated page load with staggered reveals creates more delight than scattered micro-interactions. - -**DO**: Use motion to convey state changes—entrances, exits, feedback -**DO**: Use exponential easing (ease-out-quart/quint/expo) for natural deceleration -**DO**: For height animations, use grid-template-rows transitions instead of animating height directly -**DON'T**: Animate layout properties (width, height, padding, margin)—use transform and opacity only -**DON'T**: Use bounce or elastic easing—they feel dated and tacky; real objects decelerate smoothly - -### Interaction -→ *Consult [interaction reference](reference/interaction-design.md) for forms, focus, and loading patterns.* - -Make interactions feel fast. Use optimistic UI—update immediately, sync later. - -**DO**: Use progressive disclosure—start simple, reveal sophistication through interaction (basic options first, advanced behind expandable sections; hover states that reveal secondary actions) -**DO**: Design empty states that teach the interface, not just say "nothing here" -**DO**: Make every interactive surface feel intentional and responsive -**DON'T**: Repeat the same information—redundant headers, intros that restate the heading -**DON'T**: Make every button primary—use ghost buttons, text links, secondary styles; hierarchy matters - -### Responsive -→ *Consult [responsive reference](reference/responsive-design.md) for mobile-first, fluid design, and container queries.* - -**DO**: Use container queries (@container) for component-level responsiveness -**DO**: Adapt the interface for different contexts—don't just shrink it -**DON'T**: Hide critical functionality on mobile—adapt the interface, don't amputate it - -### UX Writing -→ *Consult [ux-writing reference](reference/ux-writing.md) for labels, errors, and empty states.* - -**DO**: Make every word earn its place -**DON'T**: Repeat information users can already see - ---- - -## The AI Slop Test - -**Critical quality check**: If you showed this interface to someone and said "AI made this," would they believe you immediately? If yes, that's the problem. - -A distinctive interface should make someone ask "how was this made?" not "which AI made this?" - -Review the DON'T guidelines above—they are the fingerprints of AI-generated work from 2024-2025. - ---- - -## Implementation Principles - -Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. - -Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices across generations. - -Remember: the model is capable of extraordinary creative work. Don't hold back—show what can truly be created when thinking outside the box and committing fully to a distinctive vision. \ No newline at end of file +Do NOT attempt any design work from this skill. Redirect to `/impeccable` instead. \ No newline at end of file diff --git a/.agents/skills/impeccable/SKILL.md b/.agents/skills/impeccable/SKILL.md new file mode 100644 index 000000000..e6ee18bd6 --- /dev/null +++ b/.agents/skills/impeccable/SKILL.md @@ -0,0 +1,343 @@ +--- +name: impeccable +description: Create distinctive, production-grade frontend interfaces with high design quality. Generates creative, polished code that avoids generic AI aesthetics. Use when the user asks to build web components, pages, artifacts, posters, or applications, or when any design skill requires project context. Call with 'craft' to run the full shape-then-build flow, or 'teach' for design context setup. +user-invocable: true +argument-hint: "[craft|teach]" +license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution. +--- + +This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices. + +## Context Gathering Protocol + +Design skills produce generic output without project context. You MUST have confirmed design context before doing any design work. + +**Required context** (every design skill needs at minimum): +- **Target audience**: Who uses this product and in what context? +- **Use cases**: What jobs are they trying to get done? +- **Brand personality/tone**: How should the interface feel? + +Individual skills may require additional context. Check the skill's preparation section for specifics. + +**CRITICAL**: You cannot infer this context by reading the codebase. Code tells you what was built, not who it's for or what it should feel like. Only the creator can provide this context. + +**Gathering order:** +1. **Check current instructions (instant)**: If your loaded instructions already contain a **Design Context** section, proceed immediately. +2. **Check .impeccable.md (fast)**: If not in instructions, read `.impeccable.md` from the project root. If it exists and contains the required context, proceed. +3. **Run impeccable teach (REQUIRED)**: If neither source has context, you MUST run /impeccable teach NOW before doing anything else. Do NOT skip this step. Do NOT attempt to infer context from the codebase instead. + +--- + +## Design Direction + +Commit to a BOLD aesthetic direction: +- **Purpose**: What problem does this interface solve? Who uses it? +- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction. +- **Constraints**: Technical requirements (framework, performance, accessibility). +- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember? + +**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work. The key is intentionality, not intensity. + +Then implement working code that is: +- Production-grade and functional +- Visually striking and memorable +- Cohesive with a clear aesthetic point-of-view +- Meticulously refined in every detail + +## Frontend Aesthetics Guidelines + +### Typography +→ *Consult [typography reference](reference/typography.md) for OpenType features, web font loading, and the deeper material on scales.* + +Choose fonts that are beautiful, unique, and interesting. Pair a distinctive display font with a refined body font. + + +Always apply these — do not consult a reference, just do them: + +- Use a modular type scale with fluid sizing (clamp) for headings on marketing/content pages. Use fixed `rem` scales for app UIs and dashboards (no major design system uses fluid type in product UI). +- Use fewer sizes with more contrast. A 5-step scale with at least a 1.25 ratio between steps creates clearer hierarchy than 8 sizes that are 1.1× apart. +- Line-height scales inversely with line length. Narrow columns want tighter leading, wide columns want more. For light text on dark backgrounds, ADD 0.05-0.1 to your normal line-height — light type reads as lighter weight and needs more breathing room. +- Cap line length at ~65-75ch. Body text wider than that is fatiguing. + + + +DO THIS BEFORE TYPING ANY FONT NAME. + +The model's natural failure mode is "I was told not to use Inter, so I will pick my next favorite font, which becomes the new monoculture." Avoid this by performing the following procedure on every project, in order: + +Step 1. Read the brief once. Write down 3 concrete words for the brand voice (e.g., "warm and mechanical and opinionated", "calm and clinical and careful", "fast and dense and unimpressed", "handmade and a little weird"). NOT "modern" or "elegant" — those are dead categories. + +Step 2. List the 3 fonts you would normally reach for given those words. Write them down. They are most likely from this list: + + +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 + + +Reject every font that appears in the reflex_fonts_to_reject list. They are your training-data defaults and they create monoculture across projects. **Syne in particular is the most overused "distinctive" display font and is an instant AI design tell. Never use it.** + +Step 3. Browse a font catalog with the 3 brand words in mind. Sources: Google Fonts, Pangram Pangram, Future Fonts, Adobe Fonts, ABC Dinamo, Klim Type Foundry, Velvetyne. Look for something that fits the brand as a *physical object* — a museum exhibit caption, a hand-painted shop sign, a 1970s mainframe terminal manual, a fabric label on the inside of a coat, a children's book printed on cheap newsprint. Reject the first thing that "looks designy" — that's the trained reflex too. Keep looking. + +Step 4. Cross-check the result. The right font for an "elegant" brief is NOT necessarily a serif. The right font for a "technical" brief is NOT necessarily a sans-serif. The right font for a "warm" brief is NOT Fraunces. If your final pick lines up with your reflex pattern, go back to Step 3. + + + +DO use a modular type scale with fluid sizing (clamp) on headings. +DO vary font weights and sizes to create clear visual hierarchy. +DO vary your font choices across projects. If you used a serif display font on the last project, look for a sans, monospace, or display face on this one. + +DO NOT use overused fonts like Inter, Roboto, Arial, Open Sans, or system defaults — but also do not simply switch to your second-favorite. Every font in the reflex_fonts_to_reject list above is banned. Look further. +DO NOT use Syne. Ever. It is an instant AI design tell. +DO NOT use monospace typography as lazy shorthand for "technical/developer" vibes. +DO NOT put large icons with rounded corners above every heading. They rarely add value and make sites look templated. +DO NOT use only one font family for the entire page. Pair a distinctive display font with a refined body font. +DO NOT use a flat type hierarchy where sizes are too close together. Aim for at least a 1.25 ratio between steps. +DO NOT set long body passages in uppercase. Reserve all-caps for short labels and headings. + + +### Color & Theme +→ *Consult [color reference](reference/color-and-contrast.md) for the deeper material on contrast, accessibility, and palette construction.* + +Commit to a cohesive palette. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. + + +Always apply these — do not consult a reference, just do them: + +- Use OKLCH, not HSL. OKLCH is perceptually uniform: equal steps in lightness *look* equal, which HSL does not deliver. As you move toward white or black, REDUCE chroma — high chroma at extreme lightness looks garish. A light blue at 85% lightness wants ~0.08 chroma, not the 0.15 of your base color. +- Tint your neutrals toward your brand hue. Even a chroma of 0.005-0.01 is perceptible and creates subconscious cohesion between brand color and UI surfaces. The hue you tint toward should come from THIS brand, not from a "warm = friendly" or "cool = tech" formula. Pick the brand's actual hue first, then tint everything toward it. +- The 60-30-10 rule is about visual *weight*, not pixel count. 60% neutral / surface, 30% secondary text and borders, 10% accent. Accents work BECAUSE they're rare. Overuse kills their power. + + + +Theme (light vs dark) should be DERIVED from audience and viewing context, not picked from a default. Read the brief and ask: when is this product used, by whom, in what physical setting? + +- A perp DEX consumed during fast trading sessions → dark +- A hospital portal consumed by anxious patients on phones late at night → light +- A children's reading app → light +- A vintage motorcycle forum where users sit in their garage at 9pm → dark +- An observability dashboard for SREs in a dark office → dark +- A wedding planning checklist for couples on a Sunday morning → light +- A music player app for headphone listening at night → dark +- A food magazine homepage browsed during a coffee break → light + +Do not default everything to light "to play it safe." Do not default everything to dark "to look cool." Both defaults are the lazy reflex. The correct theme is the one the actual user wants in their actual context. + + + +DO use modern CSS color functions (oklch, color-mix, light-dark) for perceptually uniform, maintainable palettes. +DO tint your neutrals toward your brand hue. Even a subtle hint creates subconscious cohesion. + +DO NOT use gray text on colored backgrounds; it looks washed out. Use a shade of the background color instead. +DO NOT use pure black (#000) or pure white (#fff). Always tint; pure black/white never appears in nature. +DO NOT use the AI color palette: cyan-on-dark, purple-to-blue gradients, neon accents on dark backgrounds. +DO NOT use gradient text for impact — see below for the strict definition. Solid colors only for text. +DO NOT default to dark mode with glowing accents. It looks "cool" without requiring actual design decisions. +DO NOT default to light mode "to be safe" either. The point is to choose, not to retreat to a safe option. + + +### Layout & Space +→ *Consult [spatial reference](reference/spatial-design.md) for the deeper material on grids, container queries, and optical adjustments.* + +Create visual rhythm through varied spacing, not the same padding everywhere. Embrace asymmetry and unexpected compositions. Break the grid intentionally for emphasis. + + +Always apply these — do not consult a reference, just do them: + +- Use a 4pt spacing scale with semantic token names (`--space-sm`, `--space-md`), not pixel-named (`--spacing-8`). Scale: 4, 8, 12, 16, 24, 32, 48, 64, 96. 8pt is too coarse — you'll often want 12px between two values. +- Use `gap` instead of margins for sibling spacing. It eliminates margin collapse and the cleanup hacks that come with it. +- Vary spacing for hierarchy. A heading with extra space above it reads as more important — make use of that. Don't apply the same padding everywhere. +- Self-adjusting grid pattern: `grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))` is the breakpoint-free responsive grid for card-style content. +- Container queries are for components, viewport queries are for page layout. A card in a sidebar should adapt to the sidebar's width, not the viewport's. + + + +DO create visual rhythm through varied spacing: tight groupings, generous separations. +DO use fluid spacing with clamp() that breathes on larger screens. +DO use asymmetry and unexpected compositions; break the grid intentionally for emphasis. + +DO NOT wrap everything in cards. Not everything needs a container. +DO NOT nest cards inside cards. Visual noise; flatten the hierarchy. +DO NOT use identical card grids (same-sized cards with icon + heading + text, repeated endlessly). +DO NOT use the hero metric layout template (big number, small label, supporting stats, gradient accent). +DO NOT center everything. Left-aligned text with asymmetric layouts feels more designed. +DO NOT use the same spacing everywhere. Without rhythm, layouts feel monotonous. +DO NOT let body text wrap beyond ~80 characters per line. Add a max-width like 65–75ch so the eye can track easily. + + +### Visual Details + + +These CSS patterns are NEVER acceptable. They are the most recognizable AI design tells. Match-and-refuse: if you find yourself about to write any of these, stop and rewrite the element with a different structure entirely. + +BAN 1: Side-stripe borders on cards/list items/callouts/alerts + - PATTERN: `border-left:` or `border-right:` with width greater than 1px + - INCLUDES: hard-coded colors AND CSS variables + - FORBIDDEN: `border-left: 3px solid red`, `border-left: 4px solid #ff0000`, `border-left: 4px solid var(--color-warning)`, `border-left: 5px solid oklch(...)`, etc. + - WHY: this is the single most overused "design touch" in admin, dashboard, and medical UIs. It never looks intentional regardless of color, radius, opacity, or whether the variable name is "primary" or "warning" or "accent." + - REWRITE: use a different element structure entirely. Do not just swap to box-shadow inset. Reach for full borders, background tints, leading numbers/icons, or no visual indicator at all. + +BAN 2: Gradient text + - PATTERN: `background-clip: text` (or `-webkit-background-clip: text`) combined with a gradient background + - FORBIDDEN: any combination that makes text fill come from a `linear-gradient`, `radial-gradient`, or `conic-gradient` + - WHY: gradient text is decorative rather than meaningful and is one of the top three AI design tells + - REWRITE: use a single solid color for text. If you want emphasis, use weight or size, not gradient fill. + + +DO: Use intentional, purposeful decorative elements that reinforce brand. +DO NOT: Use border-left or border-right greater than 1px as a colored accent stripe on cards, list items, callouts, or alerts. See above for the strict CSS pattern. +DO NOT: Use glassmorphism everywhere (blur effects, glass cards, glow borders used decoratively rather than purposefully). +DO NOT: Use sparklines as decoration. Tiny charts that look sophisticated but convey nothing meaningful. +DO NOT: Use rounded rectangles with generic drop shadows. Safe, forgettable, could be any AI output. +DO NOT: Use modals unless there's truly no better alternative. Modals are lazy. + +### Motion +→ *Consult [motion reference](reference/motion-design.md) for timing, easing, and reduced motion.* + +Focus on high-impact moments: one well-orchestrated page load with staggered reveals creates more delight than scattered micro-interactions. + +**DO**: Use motion to convey state changes: entrances, exits, feedback +**DO**: Use exponential easing (ease-out-quart/quint/expo) for natural deceleration +**DO**: For height animations, use grid-template-rows transitions instead of animating height directly +**DON'T**: Animate layout properties (width, height, padding, margin). Use transform and opacity only +**DON'T**: Use bounce or elastic easing. They feel dated and tacky; real objects decelerate smoothly + +### Interaction +→ *Consult [interaction reference](reference/interaction-design.md) for forms, focus, and loading patterns.* + +Make interactions feel fast. Use optimistic UI: update immediately, sync later. + +**DO**: Use progressive disclosure. Start simple, reveal sophistication through interaction (basic options first, advanced behind expandable sections; hover states that reveal secondary actions) +**DO**: Design empty states that teach the interface, not just say "nothing here" +**DO**: Make every interactive surface feel intentional and responsive +**DON'T**: Repeat the same information (redundant headers, intros that restate the heading) +**DON'T**: Make every button primary. Use ghost buttons, text links, secondary styles; hierarchy matters + +### Responsive +→ *Consult [responsive reference](reference/responsive-design.md) for mobile-first, fluid design, and container queries.* + +**DO**: Use container queries (@container) for component-level responsiveness +**DO**: Adapt the interface for different contexts, not just shrink it +**DON'T**: Hide critical functionality on mobile. Adapt the interface, don't amputate it + +### UX Writing +→ *Consult [ux-writing reference](reference/ux-writing.md) for labels, errors, and empty states.* + +**DO**: Make every word earn its place +**DON'T**: Repeat information users can already see + +--- + +## The AI Slop Test + +**Critical quality check**: If you showed this interface to someone and said "AI made this," would they believe you immediately? If yes, that's the problem. + +A distinctive interface should make someone ask "how was this made?" not "which AI made this?" + +Review the DON'T guidelines above. They are the fingerprints of AI-generated work from 2024-2025. + +--- + +## Implementation Principles + +Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. + +Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices across generations. + +Remember: the model is capable of extraordinary creative work. Don't hold back. Show what can truly be created when thinking outside the box and committing fully to a distinctive vision. + +--- + +## Craft Mode + +If this skill is invoked with the argument "craft" (e.g., `/impeccable craft [feature description]`), follow the [craft flow](reference/craft.md). Pass any additional arguments as the feature description. + +--- + +## Teach Mode + +If this skill is invoked with the argument "teach" (e.g., `/impeccable teach`), skip all design work above and instead run the teach flow below. This is a one-time setup that gathers design context for the project. + +### Step 1: Explore the Codebase + +Before asking questions, thoroughly scan the project to discover what you can: + +- **README and docs**: Project purpose, target audience, any stated goals +- **Package.json / config files**: Tech stack, dependencies, existing design libraries +- **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 +- **Any style guides or brand documentation** + +Note what you've learned and what remains unclear. + +### Step 2: Ask UX-Focused Questions + +ask the user directly to clarify what you cannot infer. Focus only on what you couldn't infer from the codebase: + +#### Users & Purpose +- Who uses this? What's their context when using it? +- What job are they trying to get done? +- What emotions should the interface evoke? (confidence, delight, calm, urgency, etc.) + +#### Brand & Personality +- How would you describe the brand personality in 3 words? +- Any reference sites or apps that capture the right feel? What specifically about them? +- What should this explicitly NOT look like? Any anti-references? + +#### Aesthetic Preferences +- Any strong preferences for visual direction? (minimal, bold, elegant, playful, technical, organic, etc.) +- Light mode, dark mode, or both? +- Any colors that must be used or avoided? + +#### Accessibility & Inclusion +- Specific accessibility requirements? (WCAG level, known user needs) +- Considerations for reduced motion, color blindness, or other accommodations? + +Skip questions where the answer is already clear from the codebase exploration. + +### Step 3: Write Design Context + +Synthesize your findings and the user's answers into a `## Design Context` section: + +```markdown +## Design Context + +### Users +[Who they are, their context, the job to be done] + +### Brand Personality +[Voice, tone, 3-word personality, emotional goals] + +### Aesthetic Direction +[Visual tone, references, anti-references, theme] + +### Design Principles +[3-5 principles derived from the conversation that should guide all design decisions] +``` + +Write this section to `.impeccable.md` in the project root. If the file already exists, update the Design Context section in place. + +Then ask the user directly to clarify what you cannot infer. whether they'd also like the Design Context appended to .github/copilot-instructions.md. If yes, append or update the section there as well. + +Confirm completion and summarize the key design principles that will now guide all future work. \ No newline at end of file diff --git a/.agents/skills/frontend-design/reference/color-and-contrast.md b/.agents/skills/impeccable/reference/color-and-contrast.md similarity index 67% rename from .agents/skills/frontend-design/reference/color-and-contrast.md rename to .agents/skills/impeccable/reference/color-and-contrast.md index 77aaf0329..88a15cce5 100644 --- a/.agents/skills/frontend-design/reference/color-and-contrast.md +++ b/.agents/skills/impeccable/reference/color-and-contrast.md @@ -4,36 +4,19 @@ **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. -```css -/* OKLCH: lightness (0-100%), chroma (0-0.4+), hue (0-360) */ ---color-primary: oklch(60% 0.15 250); /* Blue */ ---color-primary-light: oklch(85% 0.08 250); /* Same hue, lighter */ ---color-primary-dark: oklch(35% 0.12 250); /* Same hue, darker */ -``` +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. -**Key insight**: As you move toward white or black, reduce chroma (saturation). High chroma at extreme lightness looks garish. A light blue at 85% lightness needs ~0.08 chroma, not the 0.15 of your base color. +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 -### The Tinted Neutral Trap +### Tinted Neutrals -**Pure gray is dead.** Add a subtle hint of your brand hue to all 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. -```css -/* Dead grays */ ---gray-100: oklch(95% 0 0); /* No personality */ ---gray-900: oklch(15% 0 0); +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. -/* Warm-tinted grays (add brand warmth) */ ---gray-100: oklch(95% 0.01 60); /* Hint of warmth */ ---gray-900: oklch(15% 0.01 60); - -/* Cool-tinted grays (tech, professional) */ ---gray-100: oklch(95% 0.01 250); /* Hint of blue */ ---gray-900: oklch(15% 0.01 250); -``` - -The chroma is tiny (0.01) but perceptible. It creates subconscious cohesion between your brand color and your UI. +**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 @@ -107,17 +90,7 @@ You can't just swap colors. Dark mode requires different design decisions: | Vibrant accents | Desaturate accents slightly | | White backgrounds | Never pure black—use dark gray (oklch 12-18%) | -```css -/* Dark mode depth via surface color, not shadow */ -:root[data-theme="dark"] { - --surface-1: oklch(15% 0.01 250); - --surface-2: oklch(20% 0.01 250); /* "Higher" = lighter */ - --surface-3: oklch(25% 0.01 250); - - /* Reduce text weight slightly */ - --body-weight: 350; /* Instead of 400 */ -} -``` +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 diff --git a/.agents/skills/impeccable/reference/craft.md b/.agents/skills/impeccable/reference/craft.md new file mode 100644 index 000000000..8cddbc9db --- /dev/null +++ b/.agents/skills/impeccable/reference/craft.md @@ -0,0 +1,70 @@ +# Craft Flow + +Build a feature with impeccable UX and UI quality through a structured process: shape the design, load the right references, then build and iterate visually until the result is delightful. + +## Step 1: Shape the Design + +Run /shape, passing along whatever feature description the user provided. + +Wait for the design brief to be fully confirmed before proceeding. The brief is your blueprint, and every implementation decision should trace back to it. + +If the user has already run /shape and has a confirmed design brief, skip this step and use the existing brief. + +## Step 2: Load References + +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 + +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) + +## Step 3: Build + +Implement the feature following the design brief. Work in this order: + +1. **Structure first**: HTML/semantic structure for the primary state. No styling yet. +2. **Layout and spacing**: Establish the spatial rhythm and visual hierarchy. +3. **Typography and color**: Apply the type scale and color system. +4. **Interactive states**: Hover, focus, active, disabled. +5. **Edge case states**: Empty, loading, error, overflow, first-run. +6. **Motion**: Purposeful transitions and animations (if appropriate). +7. **Responsive**: Adapt for different viewports. Don't just shrink; redesign for the context. + +### During Build +- Test with real (or realistic) data at every step, not placeholder text +- Check each state as you build it, not all at the end +- If you discover a design question, stop and ask rather than guessing +- Every visual choice should trace back to something in the design brief + +## Step 4: Visual Iteration + +**This step is critical.** Do not stop after the first implementation pass. + +Open the result in a browser window. If browser automation tools are available, use them to navigate to the page and visually inspect the result. If not, ask the user to open it and provide feedback. + +Iterate through these checks visually: + +1. **Does it match the brief?** Compare the live result against every section of the design brief. Fix discrepancies. +2. **Does it pass the AI slop test?** If someone saw this and said "AI made this," would they believe it immediately? If yes, it needs more design intention. +3. **Check against impeccable's DON'T guidelines.** Fix any anti-pattern violations. +4. **Check every state.** Navigate through empty, error, loading, and edge case states. Each one should feel intentional, not like an afterthought. +5. **Check responsive.** Resize the viewport. Does it adapt well or just shrink? +6. **Check the details.** Spacing consistency, type hierarchy clarity, color contrast, interactive feedback, motion timing. + +After each round of fixes, visually verify again. **Repeat until you would be proud to show this to the user.** The bar is not "it works"; the bar is "this delights." + +## Step 5: Present + +Present the result to the user: +- Show the feature in its primary state +- Walk through the key states (empty, error, responsive) +- Explain design decisions that connect back to the design brief +- Ask: "What's working? What isn't?" + +Iterate based on feedback. Good design is rarely right on the first pass. diff --git a/.agents/skills/frontend-design/reference/interaction-design.md b/.agents/skills/impeccable/reference/interaction-design.md similarity index 100% rename from .agents/skills/frontend-design/reference/interaction-design.md rename to .agents/skills/impeccable/reference/interaction-design.md diff --git a/.agents/skills/frontend-design/reference/motion-design.md b/.agents/skills/impeccable/reference/motion-design.md similarity index 100% rename from .agents/skills/frontend-design/reference/motion-design.md rename to .agents/skills/impeccable/reference/motion-design.md diff --git a/.agents/skills/frontend-design/reference/responsive-design.md b/.agents/skills/impeccable/reference/responsive-design.md similarity index 100% rename from .agents/skills/frontend-design/reference/responsive-design.md rename to .agents/skills/impeccable/reference/responsive-design.md diff --git a/.agents/skills/frontend-design/reference/spatial-design.md b/.agents/skills/impeccable/reference/spatial-design.md similarity index 100% rename from .agents/skills/frontend-design/reference/spatial-design.md rename to .agents/skills/impeccable/reference/spatial-design.md diff --git a/.codex/skills/frontend-design/reference/typography.md b/.agents/skills/impeccable/reference/typography.md similarity index 73% rename from .codex/skills/frontend-design/reference/typography.md rename to .agents/skills/impeccable/reference/typography.md index fc201c8a6..7063af5b2 100644 --- a/.codex/skills/frontend-design/reference/typography.md +++ b/.agents/skills/impeccable/reference/typography.md @@ -34,11 +34,20 @@ Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scal **Avoid the invisible defaults**: Inter, Roboto, Open Sans, Lato, Montserrat. These are everywhere, making your design feel generic. They're fine for documentation or tools where personality isn't the goal—but if you want distinctive design, look elsewhere. -**Better Google Fonts alternatives**: -- Instead of Inter → **Instrument Sans**, **Plus Jakarta Sans**, **Outfit** -- Instead of Roboto → **Onest**, **Figtree**, **Urbanist** -- Instead of Open Sans → **Source Sans 3**, **Nunito Sans**, **DM Sans** -- For editorial/premium feel → **Fraunces**, **Newsreader**, **Lora** +**Pick the font from the brief, not from a category preset.** The most common AI typography failure is reaching for the same "tasteful" font for every editorial brief, the same "modern" font for every tech brief, the same "elegant serif" for every premium brief. Those reflexes produce monoculture across projects. The right font is one whose physical character matches *this specific* brand, audience, and moment. + +A working selection process: + +1. Read the brief once. Write down three concrete words for the brand voice. Not "modern" or "elegant" — those are dead categories. Try "warm and mechanical and opinionated" or "calm and clinical and careful" or "fast and dense and unimpressed" or "handmade and a little weird." +2. Now imagine the font as a physical object the brand could ship: a typewriter ribbon, a hand-lettered shop sign, a 1970s mainframe terminal manual, a fabric label on the inside of a coat, a museum exhibit caption, a tax form, a children's book printed on cheap newsprint. Whichever physical object fits the three words is pointing at the right *kind* of typeface. +3. Browse a font catalog (Google Fonts, Pangram Pangram, Adobe Fonts, Future Fonts, ABC Dinamo) with that physical object in mind. **Reject the first thing that "looks designy."** That's your trained-everywhere reflex. Keep looking. +4. Avoid your defaults from previous projects. If you find yourself reaching for the same display font you used last time, make yourself pick something else. + +**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 in 2026 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. diff --git a/.agents/skills/frontend-design/reference/ux-writing.md b/.agents/skills/impeccable/reference/ux-writing.md similarity index 100% rename from .agents/skills/frontend-design/reference/ux-writing.md rename to .agents/skills/impeccable/reference/ux-writing.md diff --git a/.agents/skills/normalize/SKILL.md b/.agents/skills/normalize/SKILL.md index 4015c54a3..cbc512296 100644 --- a/.agents/skills/normalize/SKILL.md +++ b/.agents/skills/normalize/SKILL.md @@ -9,7 +9,7 @@ Analyze and redesign the feature to perfectly match our design system standards, ## MANDATORY PREPARATION -Invoke /frontend-design — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /teach-impeccable first. +Invoke /impeccable — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /impeccable teach first. --- diff --git a/.agents/skills/onboard/SKILL.md b/.agents/skills/onboard/SKILL.md index 14666050b..01a1d2dd6 100644 --- a/.agents/skills/onboard/SKILL.md +++ b/.agents/skills/onboard/SKILL.md @@ -7,7 +7,7 @@ argument-hint: "[target]" ## MANDATORY PREPARATION -Invoke /frontend-design — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /teach-impeccable first. Additionally gather: the "aha moment" you want users to reach, and users' experience level. +Invoke /impeccable — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /impeccable teach first. Additionally gather: the "aha moment" you want users to reach, and users' experience level. --- diff --git a/.agents/skills/overdrive/SKILL.md b/.agents/skills/overdrive/SKILL.md index fd9199ec6..a389e6346 100644 --- a/.agents/skills/overdrive/SKILL.md +++ b/.agents/skills/overdrive/SKILL.md @@ -16,7 +16,7 @@ Push an interface past conventional limits. This isn't just about visual effects ## MANDATORY PREPARATION -Invoke /frontend-design — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /teach-impeccable first. +Invoke /impeccable — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /impeccable teach first. **EXTRA IMPORTANT FOR THIS SKILL**: Context determines what "extraordinary" means. A particle system on a creative portfolio is impressive. The same particle system on a settings page is embarrassing. But a settings page with instant optimistic saves and animated state transitions? That's extraordinary too. Understand the project's personality and goals before deciding what's appropriate. diff --git a/.agents/skills/polish/SKILL.md b/.agents/skills/polish/SKILL.md index fcaae6e88..1e8751a6a 100644 --- a/.agents/skills/polish/SKILL.md +++ b/.agents/skills/polish/SKILL.md @@ -7,7 +7,7 @@ argument-hint: "[target]" ## MANDATORY PREPARATION -Invoke /frontend-design — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /teach-impeccable first. Additionally gather: quality bar (MVP vs flagship). +Invoke /impeccable — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /impeccable teach first. Additionally gather: quality bar (MVP vs flagship). --- diff --git a/.agents/skills/quieter/SKILL.md b/.agents/skills/quieter/SKILL.md index 555ca446e..314f574be 100644 --- a/.agents/skills/quieter/SKILL.md +++ b/.agents/skills/quieter/SKILL.md @@ -9,7 +9,7 @@ Reduce visual intensity in designs that are too bold, aggressive, or overstimula ## MANDATORY PREPARATION -Invoke /frontend-design — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /teach-impeccable first. +Invoke /impeccable — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /impeccable teach first. --- diff --git a/.agents/skills/shape/SKILL.md b/.agents/skills/shape/SKILL.md new file mode 100644 index 000000000..330b9b02e --- /dev/null +++ b/.agents/skills/shape/SKILL.md @@ -0,0 +1,95 @@ +--- +name: shape +description: Plan the UX and UI for a feature before writing code. Runs a structured discovery interview, then produces a design brief that guides implementation. Use during the planning phase to establish design direction, constraints, and strategy before any code is written. +user-invocable: true +argument-hint: "[feature to shape]" +--- + +## MANDATORY PREPARATION + +Invoke /impeccable, which contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding. If no design context exists yet, you MUST run /impeccable teach first. + +--- + +Shape the UX and UI for a feature before any code is written. This skill produces a **design brief**: a structured artifact that guides implementation through discovery, not guesswork. + +**Scope**: Design planning only. This skill does NOT write code. It produces the thinking that makes code good. + +**Output**: A design brief that can be handed off to /impeccable craft, /impeccable, or any other implementation skill. + +## Philosophy + +Most AI-generated UIs fail not because of bad code, but because of skipped thinking. They jump to "here's a card grid" without asking "what is the user trying to accomplish?" This skill inverts that: understand deeply first, so implementation is precise. + +## Phase 1: Discovery Interview + +**Do NOT write any code or make any design decisions during this phase.** Your only job is to understand the feature deeply enough to make excellent design decisions later. + +Ask these questions in conversation, adapting based on answers. Don't dump them all at once; have a natural dialogue. ask the user directly to clarify what you cannot infer. + +### Purpose & Context +- What is this feature for? What problem does it solve? +- Who specifically will use it? (Not "users"; be specific: role, context, frequency) +- What does success look like? How will you know this feature is working? +- What's the user's state of mind when they reach this feature? (Rushed? Exploring? Anxious? Focused?) + +### Content & Data +- What content or data does this feature display or collect? +- What are the realistic ranges? (Minimum, typical, maximum, e.g., 0 items, 5 items, 500 items) +- What are the edge cases? (Empty state, error state, first-time use, power user) +- Is any content dynamic? What changes and how often? + +### Design Goals +- What's the single most important thing a user should do or understand here? +- What should this feel like? (Fast/efficient? Calm/trustworthy? Fun/playful? Premium/refined?) +- Are there existing patterns in the product this should be consistent with? +- Are there specific examples (inside or outside the product) that capture what you're going for? + +### Constraints +- Are there technical constraints? (Framework, performance budget, browser support) +- Are there content constraints? (Localization, dynamic text length, user-generated content) +- Mobile/responsive requirements? +- Accessibility requirements beyond WCAG AA? + +### Anti-Goals +- What should this NOT be? What would be a wrong direction? +- What's the biggest risk of getting this wrong? + +## Phase 2: Design Brief + +After the interview, synthesize everything into a structured design brief. Present it to the user for confirmation before considering this skill complete. + +### Brief Structure + +**1. Feature Summary** (2-3 sentences) +What this is, who it's for, what it needs to accomplish. + +**2. Primary User Action** +The single most important thing a user should do or understand here. + +**3. Design Direction** +How this should feel. What aesthetic approach fits. Reference the project's design context from `.impeccable.md` and explain how this feature should express it. + +**4. Layout Strategy** +High-level spatial approach: what gets emphasis, what's secondary, how information flows. Describe the visual hierarchy and rhythm, not specific CSS. + +**5. Key States** +List every state the feature needs: default, empty, loading, error, success, edge cases. For each, note what the user needs to see and feel. + +**6. Interaction Model** +How users interact with this feature. What happens on click, hover, scroll? What feedback do they get? What's the flow from entry to completion? + +**7. Content Requirements** +What copy, labels, empty state messages, error messages, and microcopy are needed. Note any dynamic content and its realistic ranges. + +**8. 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). + +**9. Open Questions** +Anything unresolved that the implementer should resolve during build. + +--- + +ask the user directly to clarify what you cannot infer. Get explicit confirmation of the brief before finishing. If the user disagrees with any part, revisit the relevant discovery questions. + +Once confirmed, the brief is complete. The user can now hand it to /impeccable craft to build the feature, or use it to guide any other implementation approach. \ No newline at end of file diff --git a/.agents/skills/teach-impeccable/SKILL.md b/.agents/skills/teach-impeccable/SKILL.md index 423a9a05a..4f39eb5ae 100644 --- a/.agents/skills/teach-impeccable/SKILL.md +++ b/.agents/skills/teach-impeccable/SKILL.md @@ -1,71 +1,11 @@ --- name: teach-impeccable -description: One-time setup that gathers design context for your project and saves it to your AI config file. Run once to establish persistent design guidelines. +description: DEPRECATED: Use /impeccable teach instead. This command has been folded into the impeccable skill. user-invocable: true --- -Gather design context for this project, then persist it for all future sessions. +This command has been renamed. Run `/impeccable teach` instead. -## Step 1: Explore the Codebase +Do NOT proceed with any teach flow here. Simply inform the user: -Before asking questions, thoroughly scan the project to discover what you can: - -- **README and docs**: Project purpose, target audience, any stated goals -- **Package.json / config files**: Tech stack, dependencies, existing design libraries -- **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 -- **Any style guides or brand documentation** - -Note what you've learned and what remains unclear. - -## Step 2: Ask UX-Focused Questions - -ask the user directly to clarify what you cannot infer. Focus only on what you couldn't infer from the codebase: - -### Users & Purpose -- Who uses this? What's their context when using it? -- What job are they trying to get done? -- What emotions should the interface evoke? (confidence, delight, calm, urgency, etc.) - -### Brand & Personality -- How would you describe the brand personality in 3 words? -- Any reference sites or apps that capture the right feel? What specifically about them? -- What should this explicitly NOT look like? Any anti-references? - -### Aesthetic Preferences -- Any strong preferences for visual direction? (minimal, bold, elegant, playful, technical, organic, etc.) -- Light mode, dark mode, or both? -- Any colors that must be used or avoided? - -### Accessibility & Inclusion -- Specific accessibility requirements? (WCAG level, known user needs) -- Considerations for reduced motion, color blindness, or other accommodations? - -Skip questions where the answer is already clear from the codebase exploration. - -## Step 3: Write Design Context - -Synthesize your findings and the user's answers into a `## Design Context` section: - -```markdown -## Design Context - -### Users -[Who they are, their context, the job to be done] - -### Brand Personality -[Voice, tone, 3-word personality, emotional goals] - -### Aesthetic Direction -[Visual tone, references, anti-references, theme] - -### Design Principles -[3-5 principles derived from the conversation that should guide all design decisions] -``` - -Write this section to `.impeccable.md` in the project root. If the file already exists, update the Design Context section in place. - -Then ask the user directly to clarify what you cannot infer. whether they'd also like the Design Context appended to .github/copilot-instructions.md. If yes, append or update the section there as well. - -Confirm completion and summarize the key design principles that will now guide all future work. \ No newline at end of file +> The `/teach-impeccable` command has been renamed to `/impeccable teach`. Please use that instead. \ No newline at end of file diff --git a/.agents/skills/typeset/SKILL.md b/.agents/skills/typeset/SKILL.md index 139153a28..9d89fafad 100644 --- a/.agents/skills/typeset/SKILL.md +++ b/.agents/skills/typeset/SKILL.md @@ -9,7 +9,7 @@ Assess and improve typography that feels generic, inconsistent, or poorly struct ## MANDATORY PREPARATION -Invoke /frontend-design — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /teach-impeccable first. +Invoke /impeccable — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /impeccable teach first. --- @@ -46,7 +46,7 @@ Analyze what's weak or generic about the current type: ## Plan Typography Improvements -Consult the [typography reference](reference/typography.md) from the frontend-design skill for detailed guidance on scales, pairing, and loading strategies. +Consult the [typography reference](reference/typography.md) from the impeccable skill for detailed guidance on scales, pairing, and loading strategies. Create a systematic plan: diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 38d24b704..9b5762125 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -2,7 +2,7 @@ "$schema": "https://anthropic.com/claude-code/marketplace.schema.json", "name": "impeccable", "metadata": { - "description": "Design fluency for AI harnesses. 1 skill, 20 commands, and curated anti-patterns for impeccable frontend design." + "description": "Design fluency for AI harnesses. 1 skill, 21 commands, and curated anti-patterns for impeccable frontend design." }, "owner": { "name": "Paul Bakaus", @@ -11,8 +11,8 @@ "plugins": [ { "name": "impeccable", - "description": "Design vocabulary and skills for frontend development. Includes 20 commands (/polish, /distill, /audit, /typeset, /overdrive, etc.) and an enhanced frontend-design skill with curated anti-patterns.", - "version": "1.6.0", + "description": "Design vocabulary and skills for frontend development. Includes 21 commands (/polish, /distill, /audit, /typeset, /overdrive, etc.) and an enhanced impeccable skill with curated anti-patterns.", + "version": "2.0.7", "author": { "name": "Paul Bakaus", "email": "paul@paulbakaus.com" diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 341f352ff..ea500e5fe 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "impeccable", - "description": "Design vocabulary and skills for frontend development. Includes 21 skills (20 user-invocable: /polish, /distill, /audit, /typeset, /overdrive, etc.) and an enhanced frontend-design skill with curated anti-patterns.", - "version": "1.6.0", + "description": "Design vocabulary and skills for frontend development. Includes 21 commands (/polish, /distill, /audit, /typeset, /overdrive, etc.) and an enhanced impeccable skill with curated anti-patterns.", + "version": "2.0.7", "author": { "name": "Paul Bakaus", "email": "paul@paulbakaus.com" diff --git a/.claude/agents/anti-patterns.md b/.claude/agents/anti-patterns.md new file mode 100644 index 000000000..1ad860923 --- /dev/null +++ b/.claude/agents/anti-patterns.md @@ -0,0 +1,278 @@ +--- +name: anti-patterns +description: Use when adding, modifying, or debugging an anti-pattern detection rule in this repo. Walks through the TDD recipe, the rule schema, all five plug-in points, jsdom constraints, the cross-validation step against the impeccable skill, and the post-implementation checklist. Trigger this for any work touching src/detect-antipatterns.mjs, tests/fixtures/antipatterns/, or extension/detector/. +tools: Read, Edit, Write, Glob, Grep, Bash, mcp__claude-in-chrome__navigate, mcp__claude-in-chrome__javascript_tool, mcp__claude-in-chrome__tabs_context_mcp, mcp__claude-in-chrome__tabs_create_mcp +--- + +# Anti-Pattern Engine Maintenance + +This agent handles every step of adding or modifying an anti-pattern detection rule in the impeccable repo. The rule engine is wired into many places and a single source-of-truth design ties them together. Skip any step at your peril — the build's cross-validator will fail loudly if drift slips in. + +## The five things that need to stay in sync + +When you add a rule, all of these update or get regenerated: + +| Where | What | How it stays in sync | +|---|---|---| +| `src/detect-antipatterns.mjs` `ANTIPATTERNS` | Rule metadata (id, category, name, description, skillSection, skillGuideline) and the detection logic (`checkXxx`) | **Hand-edited.** Source of truth. | +| `src/detect-antipatterns-browser.js` | Browser-bundled engine for the public site overlay | Generated by `bun run build:browser` | +| `extension/detector/detect.js` | Browser-bundled engine for the Chrome extension | Generated by `bun run build:extension` | +| `extension/detector/antipatterns.json` | Rule list (id, name, category, description) for the extension's devtools panel — drives rule toggles UI | Generated by `bun run build:extension` | +| `public/js/generated/counts.js` | `DETECTION_COUNT` integer for homepage display | Generated by `bun run build` | +| `source/skills/impeccable/SKILL.md` | Per-rule **DON'T** line in the right `### Section` — taught to users via the impeccable skill | **Hand-edited.** Validator catches drift. | + +The CLI (`bin/cli.js`) imports `ANTIPATTERNS` directly from `src/detect-antipatterns.mjs` — no separate sync needed. + +## Rule schema + +Each entry in the `ANTIPATTERNS` array (around src/detect-antipatterns.mjs:77) looks like this: + +```js +{ + id: 'icon-tile-stack', // kebab-case, unique, stable + category: 'slop', // 'slop' or 'quality' (see below) + name: 'Icon tile stacked above heading', // human-readable, used in extension UI + description: // 1–2 sentences. Used in CLI output, extension tooltips, web overlay labels + 'A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.', + skillSection: 'Typography', // OPTIONAL but strongly recommended. Must be one of the parser's allowed sections (see below) + skillGuideline: 'large icons with rounded corners above every heading', // OPTIONAL but strongly recommended. Substring that must appear in some **DON'T**: line of the named section in source/skills/impeccable/SKILL.md +} +``` + +### Categories + +- **`slop`** = "AI tells". Patterns that scream *AI generated this*. Things like purple gradients, gradient text, dark glow accents, thick side borders, icon-tile-stacks. Flagging these is about taste and freshness, not correctness. +- **`quality`** = real design or accessibility issues regardless of who wrote the code. WCAG contrast, line length, padding, line height, justified text, skipped headings, etc. + +If you're not sure, ask: *"would a human designer who's careful and tasteful still ship this?"* If no, it's `quality`. If they would (because it works fine, it just looks templated), it's `slop`. + +### `skillSection` allowed values + +These are the section names the `readPatterns()` parser at scripts/lib/utils.js:221 understands. Use **exactly** these strings (note the parser normalizes `Color & Theme` → `Color & Contrast`): + +``` +Typography +Color & Contrast +Layout & Space +Visual Details +Motion +Interaction +Responsive +UX Writing +``` + +### `skillGuideline` substring + +A 3–6 word substring that appears verbatim in some `**DON'T**:` line of the named section in `source/skills/impeccable/SKILL.md`. The build validator checks this with `String.includes()`. Pick a substring that's: + +- Short enough that benign rewordings of the DON'T won't break it +- Specific enough that it can't accidentally collide with an unrelated DON'T + +Examples: `'AI color palette'`, `'large icons with rounded corners above every heading'`, `'WCAG AA contrast'`. + +If a rule genuinely doesn't deserve a skill DON'T (rare — only the most niche a11y-only rules), omit both `skillSection` and `skillGuideline`. The validator skips rules without `skillGuideline`. + +## The TDD recipe (always do it in this order) + +This order is non-negotiable. Fixture and failing test before implementation. The full suite must run between the rule going in and you committing. + +### 1. Write the fixture (two-column convention) + +A single HTML file at `tests/fixtures/antipatterns/{rule-id}.html` with two columns: left = should-flag, right = should-pass. Each test case carries a unique heading text so the test can match snippets back to expectations. + +Convention skeleton: + +```html + + + + + + +
+
+

Should flag

+ +
+
+

Should pass

+ +
+
+ + + +``` + +The script tag at the bottom is critical — it lets you load the fixture in the browser via `http://localhost:3000/fixtures/antipatterns/{rule-id}.html` (served by `server/index.js:62` route for `/fixtures/*`). + +**Should-pass cases must cover the false-positive shapes you can think of in advance.** A good fixture has 5+ pass cases. The icon-tile-stack fixture covers: round avatar, wide thumbnail, side-by-side, no-icon, too-tiny, too-huge. + +### 2. Write the failing test + +Add to `tests/detect-antipatterns-fixtures.test.mjs` in its own `describe` block. Use the snippet-substring matching pattern — the test parses heading text out of each finding's snippet and asserts membership against expected lists: + +```js +describe('detectHtml — {rule-id}', () => { + const SHOULD_FLAG = ['Heading One', 'Heading Two', /* ... */]; + const SHOULD_PASS = ['Pass Heading One', /* ... */]; + + it('{rule-id}: flags only the should-flag column', async () => { + const f = await detectHtml(path.join(FIXTURES, '{rule-id}.html')); + const flagged = new Set(); + for (const r of f) { + if (r.antipattern !== '{rule-id}') continue; + const m = (r.snippet || '').match(/"([^"]+)"/); + if (m) flagged.add(m[1]); + } + for (const text of SHOULD_FLAG) { + assert.ok(flagged.has(text), `expected "${text}" to be flagged`); + } + for (const text of SHOULD_PASS) { + assert.ok(!flagged.has(text), `"${text}" should NOT be flagged`); + } + }); +}); +``` + +For this to work, the rule's snippet **must include the heading text in quotes**. See "Snippet conventions" below. + +Run `node --test tests/detect-antipatterns-fixtures.test.mjs` and **watch it fail**. If it doesn't fail, your test is wrong. + +### 3. Add the rule definition + +Add a new entry to the `ANTIPATTERNS` array in `src/detect-antipatterns.mjs`. Place it in the right category section (slop or quality). Fill in all fields including `skillSection` and `skillGuideline`. + +### 4. Implement the pure check function + +Add a `checkXxx(opts)` function alongside the others (`checkColors`, `checkBorders`, `checkMotion`, `checkGlow`, `checkIconTile`, etc.). The pure function takes a plain options object — no DOM access — and returns an array of `{ id, snippet }`. This makes it testable and reusable across the browser/Node adapters. + +Example shape (see `checkIconTile` in src/detect-antipatterns.mjs for a real one): + +```js +function checkXxx(opts) { + const { tag, /* whatever fields the rule needs */ } = opts; + if (SAFE_TAGS.has(tag)) return []; + // ... your detection logic ... + if (matches) { + return [{ id: 'rule-id', snippet: `... "${headingText}"` }]; + } + return []; +} +``` + +### 5. Add the two adapters + +Two adapters wrap the pure function with environment-specific input gathering: + +- **`checkElementXxxDOM(el)`** — for the browser. Uses `getComputedStyle(el)` and `el.getBoundingClientRect()`. +- **`checkElementXxx(el, tag, window)`** — for jsdom (Node). Uses `window.getComputedStyle(el)` and **must read explicit pixel dimensions from `parseFloat(style.width)`** instead of bounding rects, because **jsdom does not lay out** — `getBoundingClientRect()` returns 0×0 for everything. + +If your rule needs vertical positioning info (e.g. "icon must be above heading"), that check is browser-only — gate it behind `if (headingTop && siblingBottom)` so the Node path skips it. The structural checks alone (sizes, sibling identity, classes) are enough for the fixture. + +### 6. Wire into both element-iteration loops + +Two loops iterate every element on the page. You need to add your DOM-adapter call to **both**: + +- **Browser loop** at src/detect-antipatterns.mjs:1837 (`for (const el of document.querySelectorAll('*'))` with the `findings` spread). Add a line like: + ```js + ...checkElementXxxDOM(el).map(f => ({ type: f.id, detail: f.snippet })), + ``` +- **Node (jsdom) loop** at src/detect-antipatterns.mjs:2058 (in `detectHtml`). Add a block like: + ```js + for (const f of checkElementXxx(el, tag, window)) { + findings.push(finding(f.id, filePath, f.snippet)); + } + ``` + +Forgetting one of these is the most common mistake — the test passes but the live page doesn't show anything (or vice versa). + +### 7. Add the SKILL.md DON'T line if the rule doesn't reuse an existing one + +Open `source/skills/impeccable/SKILL.md`, find the right `### Section`, and add a **DON'T** line that contains your `skillGuideline` substring verbatim. Match the style of existing DON'Ts (terse, prescriptive, 1–2 sentences max). + +If your rule reuses an existing DON'T (e.g. multiple engine rules can map to the same skill guidance, like `side-tab` and `border-accent-on-rounded` both pointing to `'thick colored border on one side'`), no skill edit is needed. + +### 8. Run the build (this regenerates everything and validates) + +```bash +bun run build && bun run build:browser && bun run build:extension +``` + +This regenerates: +- `src/detect-antipatterns-browser.js` (public-site detector) +- `extension/detector/detect.js` (extension detector) +- `extension/detector/antipatterns.json` (extension rule list, includes description) +- `public/js/generated/counts.js` (DETECTION_COUNT) + +And validates: +- Cross-checks every rule with `skillGuideline` against `source/skills/impeccable/SKILL.md` via `validateAntipatternRules()` in scripts/build.js. **Build fails if drift exists.** + +### 9. Run the test suite + +```bash +bun run test +``` + +166 unit tests + N fixture tests, including your new one. All should be green. + +### 10. Verify on a live page in the browser + +Don't skip this. The jsdom path uses `parseFloat(style.width)` and the browser path uses `getBoundingClientRect()` — they can disagree. The fixture test catches one path; manual browser verification catches the other. + +``` +http://localhost:3000/fixtures/antipatterns/{rule-id}.html +http://localhost:3000/antipattern-examples/{your-example}.html (if relevant) +http://localhost:3000/ (no false positives on real pages) +``` + +Use the chrome MCP tools (`mcp__claude-in-chrome__navigate` + `mcp__claude-in-chrome__javascript_tool`) to inject `window.impeccableScan()` and read `.impeccable-overlay` / `.impeccable-label` from the DOM to verify. Don't try to screenshot — the overlays are decorative; read them programmatically. + +## Snippet conventions + +The fixture-test convention extracts the heading text from a finding's snippet using regex `/"([^"]+)"/` — so **wrap the identifying heading text in straight double quotes** in your snippet. Examples: + +- `'80x80px icon tile above h3 "Lightning Fast"'` +- `'4.5:1 (need 4.5:1) — text #808080 on #3b82f6'` ← uses element identifiers instead, since this rule isn't anchored to a heading + +If your rule isn't naturally anchored to a heading, pick another stable identifier (a class name, the parent element's text, etc.) and document the test pattern in the test itself. + +## jsdom constraints (the most common gotcha) + +- **No layout.** `getBoundingClientRect()` returns `0×0` always. Read `parseFloat(style.width)` and `parseFloat(style.height)` instead — jsdom does honor explicit pixel widths in ``, + + 'all-caps-body': `
Long passages in uppercase are hard to read. We recognize words by their shape, which all-caps removes.
`, + + 'pure-black-white': `
Pure black on pure white
Neither exists in nature. Always tint.
`, + + 'gray-on-color': `
Gray text on a colored background. Washed out and hard to read.
`, + + 'low-contrast': `
Light gray text on a white background. 1.6:1 contrast, fails WCAG.
`, + + 'layout-transition': `
Animating width/height causes layout jank.
`, + + 'cramped-padding': `
2px vertical padding.
`, + + 'tight-leading': `
Tight leading makes multi-line body text feel crammed and hard for the eye to track between lines.
`, + + 'skipped-heading': `

Page title (h1)

Subsection (h3), skipped h2

`, + + 'justified-text': `
Justified text on screens creates rivers of whitespace because browsers can't hyphenate well. Leave this for print.
`, + + 'tiny-text': `
Regular body text
And then fine print at 9 pixels that no one will ever read.
`, + + 'wide-tracking': `
Wide tracking on body text slows reading by breaking up natural character groupings.
`, + + 'line-length': `
Paragraphs wider than roughly 75 characters per line become fatiguing because the eye has to track an excessive distance back to the start of the next line, losing its place.
`, + + // ── LLM-only rule visuals ───────────────────────────────────────── + + 'syne-display-font': `
Synelike
Every AI landing page uses this shape.
`, + + 'monospace-as-technical': `
TECHNICAL_TOOL
Mono for "developer" vibes. Lazy.
`, + + 'dark-mode-default': `
Dark by default
Defaulting to dark is a retreat from a decision.
`, + + 'everything-in-cards': `
Title
Card around every single thing.
`, + + 'identical-card-grids': `
${'
Feature
Short copy.
'.repeat(6)}
`, + + 'hero-metric-layout': `
10M+
Active users
99.9% uptime200ms p50
`, + + 'glassmorphism': `
Frosted glass card
`, + + 'sparkline-decoration': `
Revenue
$42.1k
Tiny chart, no real information.
`, + + 'generic-drop-shadows': `
`, + + 'modal-reflex': `
Are you sure?
Really, truly sure about this?
Cancel
OK
`, + + 'every-button-primary': `
Every action shouts equally.
`, + + 'redundant-headers': `
Overview
This is the overview section, which provides an overview of the overview.
`, + + 'mobile-amputation': `
Export to CSV
"Not available on mobile."
`, +}; + +/** + * Anti-patterns that live in the /impeccable skill's DON'T list but + * don't have a deterministic detector. These can only be caught by + * /critique running an LLM assessment pass. + * + * Each entry looks like a detection rule: id, category, name, + * description, skillSection. The generator merges these into the + * grouped sections alongside detected rules with an 'llm' layer badge. + */ +// ─── Gallery: real examples in the wild ────────────────────────────── + +/** + * Curated real-world examples of anti-patterns caught in the wild. + * Each entry maps to: + * - public/antipattern-images/{id}.png (preview thumbnail) + * - public/antipattern-examples/{id}.html (standalone live example) + * Rendered as a dedicated section on the /anti-patterns page, replacing + * the old /gallery route which was confusingly labeled in the top nav. + */ +export const GALLERY_ITEMS = [ + { + id: 'purple-gradients', + title: 'Purple Gradients Everywhere', + desc: + 'The AI color palette: purple-to-blue gradients on everything. Buttons, text, backgrounds, orbs. The new "make it pop."', + }, + { + id: 'lazy-cool', + title: 'Lazy "Cool"', + desc: + 'Glassmorphism, neon glows, blurred orbs, monospace everything. Looks like a hackathon project, not a product.', + }, + { + id: 'lazy-impact', + title: 'Lazy "Impact"', + desc: + 'When in doubt, animate everything. Bouncing buttons, wiggling icons, gradient text, floating badges. Motion without meaning.', + }, + { + id: 'thick-border-cards', + title: 'Side-Tab Cards', + desc: + 'A thick colored border on one side of a rounded card. The single most recognizable tell of AI-generated UI.', + }, + { + id: 'cardocalypse', + title: 'Cardocalypse', + desc: + 'Cards inside cards inside cards. Five levels of nesting, each with its own padding and shadow.', + }, + { + id: 'layout-templates', + title: 'Copy-Paste Layouts', + desc: + 'The same hero-metric-features template repeated with different colors. When every section looks the same, nothing stands out.', + }, + { + id: 'inter-everywhere', + title: 'Inter Everywhere', + desc: + 'One font for everything. Headings, body, labels, buttons. No typographic hierarchy, no personality, no design.', + }, + { + id: 'massive-icons', + title: 'Massive Icons', + desc: + 'Icon containers larger than the content they introduce. When the decoration is bigger than the message, priorities are backwards.', + }, + { + id: 'bad-contrast', + title: 'Bad Contrast Choices', + desc: + 'Gray text on colored backgrounds, low-contrast labels, unreadable combinations. Looking good and being readable should not conflict.', + }, + { + id: 'redundant-ux-writing', + title: 'Redundant UX Writing', + desc: + 'Label, sublabel, helper text, and hint text all saying the same thing in slightly different words. Say it once, say it well.', + }, + { + id: 'modal-abuse', + title: 'Modal Abuse', + desc: + 'Complex settings crammed into a modal. If it needs a scroll bar and three columns, it deserves its own page.', + }, +]; + +// ─── LLM-only rules ────────────────────────────────────────────────── + +export const LLM_ONLY_RULES = [ + { + id: 'syne-display-font', + category: 'slop', + name: 'Syne as display font', + description: + 'Syne is the most overused "distinctive" display font and reads as an instant AI design tell. Pick something else.', + skillSection: 'Typography', + }, + { + id: 'monospace-as-technical', + category: 'slop', + name: 'Monospace as "technical" shorthand', + description: + 'Using a monospace typeface to signal "developer / technical" vibes. Reach for real type choices instead of a lazy stereotype.', + skillSection: 'Typography', + }, + { + id: 'dark-mode-default', + category: 'slop', + name: 'Defaulting to dark mode for "safety"', + description: + 'Defaulting to light mode to be safe is the inverse of defaulting to dark mode to look cool. Either way you are retreating from a decision.', + skillSection: 'Color & Contrast', + }, + { + id: 'everything-in-cards', + category: 'slop', + name: 'Wrapping everything in cards', + description: + 'Not every piece of content needs a bordered container. Spacing and alignment create visual grouping without the overhead of a card.', + skillSection: 'Layout & Space', + }, + { + id: 'identical-card-grids', + category: 'slop', + name: 'Identical card grids', + description: + 'Same-sized cards with icon + heading + text repeated endlessly. The default AI homepage layout.', + skillSection: 'Layout & Space', + }, + { + id: 'hero-metric-layout', + category: 'slop', + name: 'Hero metric layout', + description: + 'Big number, small label, three supporting stats, gradient accent. Used everywhere, trusted nowhere.', + skillSection: 'Layout & Space', + }, + { + id: 'glassmorphism', + category: 'slop', + name: 'Glassmorphism everywhere', + description: + 'Blur effects, glass cards, and glow borders used as decoration rather than to solve a real layering problem.', + skillSection: 'Visual Details', + }, + { + id: 'sparkline-decoration', + category: 'slop', + name: 'Sparklines as decoration', + description: + 'Tiny charts that look sophisticated but convey no meaningful information. If the data matters, give it room.', + skillSection: 'Visual Details', + }, + { + id: 'generic-drop-shadows', + category: 'slop', + name: 'Rounded rectangles with generic drop shadows', + description: + 'The safest, most forgettable shape on the web. Could be the output of any AI. Commit to a stronger visual treatment.', + skillSection: 'Visual Details', + }, + { + id: 'modal-reflex', + category: 'slop', + name: 'Reaching for modals by reflex', + description: + 'Modals interrupt the user and are lazy as a design default. Use them only when there is truly no better place for the interaction.', + skillSection: 'Visual Details', + }, + { + id: 'every-button-primary', + category: 'quality', + name: 'Every button is a primary button', + description: + 'When every button looks equally important, nothing reads as the primary action. Use ghost buttons, text links, and secondary styles to build hierarchy.', + skillSection: 'Interaction', + }, + { + id: 'redundant-headers', + category: 'quality', + name: 'Redundant information', + description: + 'Intros that restate the heading. Section labels that repeat the page title. Cards that echo their own caption. Make every word earn its place.', + skillSection: 'Interaction', + }, + { + id: 'mobile-amputation', + category: 'quality', + name: 'Amputating features on mobile', + description: + 'Hiding critical functionality on mobile because it is inconvenient. Adapt the interface to the context, do not strip it.', + skillSection: 'Responsive', + }, +]; diff --git a/content/site/partials/header.html b/content/site/partials/header.html new file mode 100644 index 000000000..a657b5f00 --- /dev/null +++ b/content/site/partials/header.html @@ -0,0 +1,20 @@ + + diff --git a/content/site/skills/adapt.md b/content/site/skills/adapt.md new file mode 100644 index 000000000..71786e04e --- /dev/null +++ b/content/site/skills/adapt.md @@ -0,0 +1,40 @@ +--- +tagline: "Make designs work across screens, devices, and contexts without amputating features." +--- + +## When to use it + +`/adapt` is for taking a design built for one context and making it work in another. Mobile from desktop, tablet from mobile, print from web, embedded from standalone, email from dashboard. Reach for it when the source design is solid but falls apart at other breakpoints, on touch, or in a different container. + +Not for building responsive from scratch. For that, start with `/impeccable` and shape the layout responsive-first. Adapt is for the "we never thought about mobile" backfill. + +## How it works + +The skill works through four dimensions of contextual fit: + +1. **Breakpoints and fluid layout**: collapse multi-column to single, adjust clamp ranges, introduce new breakpoints where the design genuinely breaks. +2. **Touch targets**: minimum 44px hit areas, sufficient spacing between adjacent targets, larger tap zones than visual bounds where needed. +3. **Navigation patterns**: desktop sidebars become mobile bottom nav or slide-outs, dense toolbars collapse into menus, hover states get touch equivalents. +4. **Content priority**: decide what must be visible, what can collapse into disclosures, what can be removed entirely for that context. + +The non-negotiable rule: adapt, do not amputate. Critical functionality cannot disappear on mobile just because it is inconvenient. Find a way to fit it, redesign the interaction, or reconsider whether it was really critical on desktop. + +## Try it + +``` +/adapt the settings page for mobile +``` + +Expected changes: + +- Three-column grid becomes single column with section headers acting as sticky dividers +- Sidebar nav moves to a horizontal scroller above the content +- Toggles gain 8px vertical padding so they meet 44px touch targets +- Inline help text moves to tap-to-reveal, not hover +- The "Danger zone" section expands fully on mobile instead of collapsing, because it contains irreversible actions and we want users to see them clearly + +## Pitfalls + +- **Amputating features.** If the mobile version hides things the desktop version can do, that is a regression, not an adaptation. Fight for the feature. +- **Treating mobile as "smaller desktop".** Mobile is a different context: thumbs, interruption, short sessions. Adapt to the context, not to the viewport width. +- **Skipping `/harden` afterward.** Responsive layouts reveal edge cases. Run hardening after adapt to catch the ones that only show up at 320px. diff --git a/content/site/skills/animate.md b/content/site/skills/animate.md new file mode 100644 index 000000000..6781a8675 --- /dev/null +++ b/content/site/skills/animate.md @@ -0,0 +1,42 @@ +--- +tagline: "Purposeful motion that conveys state, not decoration." +--- + +## When to use it + +`/animate` is for interfaces that feel lifeless, where state changes are instant and jarring, where loading just pops in, where the user never quite trusts that their click registered. Use it to add the small motions that communicate what is happening: entrances, exits, feedback, transitions between states. + +Do not use it to add bounces or elastic springs for the sake of energy. That is decoration, and this skill will not give it to you. + +## How it works + +The skill identifies static moments that would benefit from motion, then applies them with strict discipline: + +1. **Entrances and exits**: elements appear and leave with 200 to 300ms fades plus subtle Y or scale, never layout properties. +2. **State feedback**: hover, active, focus, loading, success all communicate via motion instead of sudden swaps. +3. **Transitions between views**: shared-element transitions where it makes sense, fade-through otherwise. +4. **Progress and loading**: skeleton screens, determinate bars, motion that says "still working". +5. **Reduced motion**: every animation has a `prefers-reduced-motion` fallback. + +Easing is always exponential (ease-out-quart, quint, or expo) because real objects decelerate smoothly. No bounce, no elastic, no linear for anything except progress indicators. + +The skill animates `transform` and `opacity` only. If you find yourself animating `width`, `height`, `top`, or `left`, it is doing the wrong thing. Use `grid-template-rows` for height transitions. + +## Try it + +``` +/animate the sign-up flow +``` + +Typical additions: + +- Email input gets a focus glow on focus-visible (opacity + shadow, 180ms) +- Submit button shows a spinner inside itself on loading state, not a separate spinner next to it +- Success screen enters with opacity + translateY(8px), 260ms, ease-out-quart +- Error message slides down with grid-template-rows (not height), 220ms +- `@media (prefers-reduced-motion: reduce)` fallback for every transition + +## Pitfalls + +- **Asking for "more animation".** Animate is not a dial. It adds where motion communicates, not everywhere. +- **Removing the reduced-motion fallbacks.** The skill adds them automatically. Non-negotiable for accessibility. diff --git a/content/site/skills/arrange.md b/content/site/skills/arrange.md new file mode 100644 index 000000000..5144ee508 --- /dev/null +++ b/content/site/skills/arrange.md @@ -0,0 +1,41 @@ +--- +tagline: "Fix layout, spacing, and visual rhythm." +--- + +## When to use it + +`/arrange` is for pages where nothing is technically wrong but nothing is breathing either. Equal padding everywhere, monotonous card grids, content that runs edge to edge, hierarchy that relies on size alone. Reach for it when a layout "feels off" and you cannot articulate why. + +Good triggers: "everything feels crowded", "it reads like a wall", "I do not know where to look first". + +## How it works + +The skill runs through five layout dimensions: + +1. **Spacing**: is the spacing scale consistent or are there random 13px gaps, are related elements grouped tightly with generous space between groups, is there any rhythm at all. +2. **Visual hierarchy**: does the eye land on the primary action within 2 seconds, is the hierarchy doing real work or is everything shouting. +3. **Grid and structure**: is there an underlying grid or is the layout random, are elements aligned to baselines. +4. **Rhythm**: does the page alternate between tight and generous spacing, or is everything uniform. +5. **Density**: is the layout cramped or is it wasteful, does density match the content type. + +Fixes usually involve rebuilding the spacing scale, introducing asymmetry, collapsing monotonous grids into a mixed layout with hero and supporting elements, and giving the primary action real space. + +## Try it + +``` +/arrange the settings page +``` + +Typical changes: + +- Spacing scale unified to 8 / 16 / 24 / 48 / 96px +- Section breaks at 48px, row gaps at 16px, form field groups at 8px +- Primary actions pulled out of the form flow with 32px buffer +- Decorative borders removed, replaced with spacing-driven grouping +- Sidebar and main column proportions rebalanced (280 / flex vs 25 / 75) + +## Pitfalls + +- **Confusing arrange with distill.** If the problem is too many things, run `/distill` first. Arrange is for arranging what is already the right set. +- **Expecting it to rescue a broken grid.** If the page has no grid at all, arrange will build one. Just know that the diff is going to be larger than you expect. +- **Ignoring the hierarchy verdict.** If arrange says "nothing is primary", no amount of spacing work fixes that. You need a content decision, not a layout tweak. diff --git a/content/site/skills/audit.md b/content/site/skills/audit.md new file mode 100644 index 000000000..e4a0891ab --- /dev/null +++ b/content/site/skills/audit.md @@ -0,0 +1,50 @@ +--- +tagline: "Five-dimension technical quality check with P0 to P3 severity." +--- + +## When to use it + +`/audit` is the technical counterpart to `/critique`. Where `/critique` asks "does this feel right", `/audit` asks "does this hold up". It runs accessibility, performance, theming, responsive design, and anti-pattern checks against the implementation, scores each dimension 0 to 4, and produces a plan with P0 to P3 severity ratings. + +Use it before shipping, during a quality sprint, or whenever a tech lead says "we should really look at accessibility". + +## How it works + +The skill scans your code across five dimensions: + +1. **Accessibility**: WCAG contrast, ARIA, keyboard nav, semantic HTML, form labels. +2. **Performance**: layout thrashing, expensive animations, missing lazy loading, bundle weight. +3. **Theming**: hard-coded colors, dark mode coverage, token consistency. +4. **Responsive**: breakpoint behavior, touch targets, mobile viewport handling. +5. **Anti-patterns**: the same deterministic 25 checks the detector runs. + +Each dimension gets a 0 to 4 score. Each finding gets a severity: P0 blocks the release, P1 should fix this sprint, P2 is next cycle, P3 is polish. You get back a single document you can paste into a ticket tracker. + +Audit does not fix anything. It documents. Route the findings to `/polish`, `/harden`, `/normalize`, or `/optimize` depending on the category. + +## Try it + +``` +/audit the checkout flow +``` + +Expected output: + +``` +Accessibility: 2/4 (partial) + P0: Missing form labels on 4 inputs + P1: Contrast 3.1:1 on disabled button state + P2: No visible focus indicator on custom dropdown + +Performance: 3/4 (good) + P1: Hero image not lazy-loaded (340KB) + ... +``` + +Hand the P0s to `/harden`, the theming and typography P1s to `/normalize` and `/typeset`, the rest to `/polish`. + +## Pitfalls + +- **Confusing it with `/critique`.** Audit is implementation quality. Critique is design quality. Run both for a full picture. +- **Fixing P3s before P0s.** The severity scale exists for a reason. Start at the top. +- **Skipping the dimensions you think are fine.** Theming and responsive are the ones most people assume are fine until they are not. diff --git a/content/site/skills/bolder.md b/content/site/skills/bolder.md new file mode 100644 index 000000000..2445c097d --- /dev/null +++ b/content/site/skills/bolder.md @@ -0,0 +1,40 @@ +--- +tagline: "Push safe designs toward impact without sliding into chaos." +--- + +## When to use it + +Reach for `/bolder` when the interface looks like every other interface. Generic sans, medium weights, soft shadows, modest accent color, reasonable spacing, forgettable. The design is not wrong, it is just safe. Use bolder when a project can handle presence and the current state is not bringing any. + +Do not use it on dashboards people stare at for hours. Boldness earns its place on marketing pages, hero moments, and content features. Not in operator tools. + +## How it works + +The skill amplifies four axes without breaking usability: + +1. **Scale**: display type gets pushed to clamp(3rem, 6vw, 6rem) or beyond. Headlines that fill the viewport, not hedge it. +2. **Weight contrast**: light 300 against heavy 800 instead of medium against regular. Real tension, not a shrug. +3. **Color commitment**: the accent color shows up at full strength, not diluted. Backgrounds can take a stance (ink, accent, cream) instead of all-paper. +4. **Compositional confidence**: asymmetry, off-grid, pullquotes, hanging punctuation, scale jumps. The layout has a voice. + +The skill does not add more. It amplifies what is already there. If the design has three colors, bolder does not add a fourth, it commits harder to the three. + +## Try it + +``` +/bolder the landing page hero +``` + +Expected changes: + +- Hero heading from 3rem to clamp(3.5rem, 7vw, 6.5rem), display font, weight 700 +- Subhead from regular to italic at 1.5rem, pulled 8px left of the heading for optical alignment +- Background switches from paper to a cream-to-paper gradient, creating a warmer container +- CTA button fills, drops shadow removed, border radius reduced, hover state inverts colors +- Supporting image pushed slightly off-grid with a negative top margin, creating asymmetry + +## Pitfalls + +- **Running it on the wrong page.** Product dashboards, settings, and forms should not be bold. They should be legible. Use `/arrange` or `/polish` instead. +- **Confusing bold with loud.** Bold means committed and confident. Loud means shouting. Bolder is the former. If the result feels aggressive, follow up with `/quieter`. +- **Pairing it with `/delight` in the same pass.** Delight works best against a stable visual baseline. Bold first, stabilize, then delight. diff --git a/content/site/skills/clarify.md b/content/site/skills/clarify.md new file mode 100644 index 000000000..78593f650 --- /dev/null +++ b/content/site/skills/clarify.md @@ -0,0 +1,42 @@ +--- +tagline: "Rewrite confusing UX copy so interfaces explain themselves." +--- + +## When to use it + +`/clarify` is for interface text that makes people stop and think. Confusing labels, ambiguous button copy, error messages that blame the user, tooltips that repeat the label, empty states that say nothing useful. Use it when the problem is not the layout or the color, it is the words. + +Good triggers: "users do not understand this field", "the error message is not helpful", "I cannot write good button copy", "this tooltip is a waste". + +## How it works + +The skill rewrites text across the surfaces where most UX copy problems live: + +1. **Labels and field hints**: direct, specific, say what is expected. +2. **Button copy**: verb-first, describes the outcome, not the action. "Save changes" not "OK". +3. **Error messages**: explain what went wrong, whose fault it is, and what to do next. Never blame the user. +4. **Empty states**: orient the user, explain why the state is empty, offer a next step. +5. **Tooltips and helper text**: add information the label cannot carry, never restate it. +6. **Confirmation dialogs**: name the consequences, not the action. + +The skill uses the audience and mental state from `.impeccable.md` to tune voice. Technical audience gets precise language. Consumer audience gets plain speech. Rushed users get short text. Anxious users (payment, delete) get reassurance. + +## Try it + +``` +/clarify the billing form +``` + +Before and after, typical: + +- Label "Billing address" → "Address on your card" +- Placeholder "Enter your VAT ID" → "VAT ID (optional, for business)" +- Error "Invalid input" → "This card number is 15 digits. You entered 14." +- Button "Submit" → "Charge $29 and subscribe" +- Empty state "No transactions yet" → "Your first charge will show up here after your first order." + +## Pitfalls + +- **Writing cleverer, not clearer.** Clarify is not for voice upgrades. If the copy is already clear, do not reach for this skill. Use `/delight` instead when you want personality. +- **Skipping the audience question.** Clarify needs to know who is reading. If `.impeccable.md` does not specify audience technical level, the rewrites will be generic. +- **Running clarify on marketing copy.** Clarify is for functional UX text: labels, errors, instructions. Marketing copy needs a different set of moves and a human writer. diff --git a/content/site/skills/colorize.md b/content/site/skills/colorize.md new file mode 100644 index 000000000..9617abbd4 --- /dev/null +++ b/content/site/skills/colorize.md @@ -0,0 +1,38 @@ +--- +tagline: "Add strategic color to monochrome interfaces without going garish." +--- + +## When to use it + +`/colorize` is the counterweight to "everything is gray". Dashboards that read as a beige wall, forms with no accent, content pages that could be any SaaS product. Reach for it when the interface is functional but emotionally flat, and you want warmth without tipping into the AI color palette (purple-to-pink, cyan neon, dark mode glow). + +## How it works + +The skill starts by reading your brand color if one exists, then decides where color earns its place: + +1. **Primary action** gets the strongest expression of the brand hue. +2. **Secondary accents** get muted or tinted variants, not a second full color. +3. **Neutrals** get tinted toward the brand hue at low chroma (around 0.005 to 0.01), which is nearly invisible per pixel but creates subconscious cohesion. +4. **Content categories** get a limited, intentional accent system, not a rainbow. + +Importantly, it uses OKLCH rather than HSL so that equal lightness steps look equal. As lightness moves toward the extremes, chroma drops automatically. This is how you get color that feels considered instead of computed. + +## Try it + +``` +/colorize the dashboard +``` + +Expected diff: + +- Brand color moved from a hardcoded hex to `--color-accent: oklch(62% 0.18 240)` +- Neutrals tinted with 0.007 chroma toward the brand hue +- Primary button gets the full accent, secondary buttons get ink/mist +- Chart series uses 3 distinct hues, all at matched lightness so no series visually dominates +- Empty state illustration picks up a soft accent wash + +## Pitfalls + +- **Running it without a brand hue.** Colorize needs a starting point. If `.impeccable.md` does not specify one, it will ask. Do not let it pick from the AI color palette defaults. +- **Expecting it to fix the AI color palette problem.** If your design already has purple gradients and cyan neon, you need `/quieter` first, then colorize can rebuild. +- **Using it on already-colorful interfaces.** That is a `/quieter` job. Colorize adds, it does not subtract. diff --git a/content/site/skills/critique.md b/content/site/skills/critique.md new file mode 100644 index 000000000..49a0a395e --- /dev/null +++ b/content/site/skills/critique.md @@ -0,0 +1,43 @@ +--- +tagline: "A design review with scoring, persona tests, and automated detection." +--- + +## When to use it + +Reach for `/critique` when you want an honest second opinion on something you already built. Not "does it work" but "is it any good". The skill scores your interface against Nielsen's 10 heuristics, runs cognitive load checks, tests through persona lenses, and cross-references an automated detector for 25 concrete anti-patterns. + +Use it when a page is functionally done and you want to know if it reads as intentional or as AI slop. + +## How it works + +`/critique` runs two independent assessments in parallel so they do not bias each other. + +The first is an **LLM design review**: the model reads your source, visually inspects the live page if browser automation is available, and walks the impeccable skill's full DO/DON'T catalog. It scores Nielsen's heuristics, counts cognitive load failures, traces the emotional journey through the flow, and flags AI slop. + +The second is an **automated detector** (`npx impeccable detect`) that deterministically finds gradient text, purple palettes, side-tab borders, nested cards, line length problems, and the other visible fingerprints of generic AI output. + +The two reports merge into one prioritized list: what is working, the three to five things that need fixing, and the provocative questions worth answering before shipping. + +## Try it + +Point it at a page: + +``` +/critique the homepage hero +``` + +You get back a scored report. Typical shape: + +- **AI slop verdict**: pass / fail with the specific tells +- **Heuristic scores**: 10 numbers, 0 to 4 +- **Cognitive load**: failure count out of 8 +- **Priority issues**: three to five items, each with what, why, and fix +- **Questions to answer**: the ones the interface itself cannot decide for you + +From there, pair with `/polish` or `/distill` to act on the fixes. + +## Pitfalls + +- **Running it on incomplete work.** Critique is for finished pages. An empty state with three TODOs will score badly because it is not done, not because it is bad. +- **Ignoring the questions at the end.** They are usually the highest-leverage fixes. +- **Treating the heuristic scores as a grade.** They are diagnostic, not evaluative. A 3/4 on a heuristic that matters less for your context is fine. diff --git a/content/site/skills/delight.md b/content/site/skills/delight.md new file mode 100644 index 000000000..fbd9e805a --- /dev/null +++ b/content/site/skills/delight.md @@ -0,0 +1,42 @@ +--- +tagline: "Small moments of personality that turn functional into memorable." +--- + +## When to use it + +`/delight` is for interfaces that work but do not feel like anything. Use it when the core experience is solid and you want to add the small human touches that make people remember it: a considered empty state, a loading message with a point of view, a success animation that feels earned, a microcopy moment that makes someone smile. + +It is a finishing skill. Never the first thing you run on a new build. + +## How it works + +The skill hunts for delight opportunities in the places most designers skip: + +1. **Empty states**: instead of "No items yet", something with personality appropriate to the brand. +2. **Loading and waiting moments**: the best products turn waits into content. +3. **Success feedback**: a moment of celebration when something worth celebrating happens. +4. **Microcopy**: button labels, tooltips, error messages, placeholder text. Tiny copy with taste. +5. **Easter eggs and secondary states**: things users discover that reward paying attention. + +The skill reads the brand tone from `.impeccable.md`. A serious analytics tool gets serious delight (dry, precise, a little clever). A playful consumer app gets more overt personality. It does not force humor where humor is wrong for the audience. + +The rule is: every delight moment must still work perfectly if you delete the delight. Nothing depends on the smile. + +## Try it + +``` +/delight the first-run experience +``` + +Expected additions: + +- Empty dashboard replaces "No data yet" with "Your dashboard is quiet. Let's fix that." plus a single-action CTA. +- Initial sync gets a 3-state loading message that advances: "Finding your accounts... / Pulling the last 30 days... / Making it look good...". +- First successful action triggers a one-time toast with a tiny celebratory moment. After that, just a quiet checkmark. +- Help tooltip on the tricky field has a voice that sounds like a person wrote it. + +## Pitfalls + +- **Forcing humor.** Not every brand is playful. If the brand voice in `.impeccable.md` is "clinical and precise", delight adds clever restraint, not jokes. +- **Over-decorating.** One moment of delight is memorable. Twenty becomes noise. The skill is conservative on purpose. +- **Running delight before polish.** Polish fixes what is wrong. Delight adds what is missing. In that order. diff --git a/content/site/skills/distill.md b/content/site/skills/distill.md new file mode 100644 index 000000000..422d4fb53 --- /dev/null +++ b/content/site/skills/distill.md @@ -0,0 +1,44 @@ +--- +tagline: "Ruthless subtraction. Strip designs to their essence." +--- + +## When to use it + +`/distill` removes what should not be there. Competing buttons, redundant information, decorative borders, three fonts where one works, six navigation items where three belong. Use it when an interface feels cluttered, busy, or like it is trying to do too much at once. + +Reach for it after `/critique` flags "cognitive load" or "visual noise", or any time a page has grown by accretion and no one has done the editing. + +## How it works + +The skill starts from one question: what is the single job this interface is trying to do? Everything that does not help that job is on the chopping block. + +It works in two passes: + +1. **Assess the complexity sources**. Too many elements, excessive variation, information overload, visual noise, confusing hierarchy, feature creep. Name each one. +2. **Edit ruthlessly**. Remove what is not essential. Combine what can be combined. Hide what can wait. Consolidate variation into a single treatment. Commit to a single visual language. + +The principle: simplicity is not about fewer features. It is about fewer obstacles between users and their goals. Every element on the page has to justify its existence. + +## Try it + +``` +/distill this dashboard +``` + +Before: four card styles, three button variants, two header treatments, a sidebar with 14 items grouped into 5 sections. + +After a `/distill` pass, typical changes: + +- Collapse the four card styles into one +- Pick one button variant, demote the others to text links +- Unify the headers +- Group the sidebar into 3 sections, not 5 +- Hide advanced options behind a disclosure + +Fewer things. Each one clearer. + +## Pitfalls + +- **Confusing distill with delete.** Distill removes obstacles. It does not remove features users need. If a user relies on something daily, find a way to keep it quietly, not a way to cut it. +- **Running it too early.** If the feature is still growing, distilling it now means distilling the same thing again next week. Wait until the shape is stable. +- **Expecting it to replace hierarchy work.** Sometimes the right fix is not removing things, it is arranging them. Reach for `/arrange` when the problem is layout, not quantity. diff --git a/content/site/skills/extract.md b/content/site/skills/extract.md new file mode 100644 index 000000000..963ad985f --- /dev/null +++ b/content/site/skills/extract.md @@ -0,0 +1,43 @@ +--- +tagline: "Pull reusable components, tokens, and patterns into the design system." +--- + +## When to use it + +`/extract` is for the moment your codebase has accidentally become a design system. Repeated button styles in 12 places. Three variants of the same card. Hex colors scattered throughout. Hand-rolled spacing that accidentally matches a scale. Reach for it when you want to consolidate this drift into reusable primitives. + +Use it after a product has shipped enough features to reveal the patterns. Premature extraction creates abstractions that do not match reality. + +## How it works + +The skill discovers the design system structure first, then identifies extraction opportunities: + +1. **Tokens**: find repeated literal values (colors, spacing, radii, shadows, font sizes). Propose token names, add to the token system, replace usages. +2. **Components**: find UI patterns that repeat with minor variation (buttons, cards, inputs, modals). Extract into a single component with variants, migrate callers. +3. **Composition patterns**: find layout or interaction patterns that repeat (form rows, toolbar groups, empty states). Extract into composition primitives. +4. **Type styles**: find repeated font-size + weight + line-height combinations. Extract into text styles. +5. **Animation patterns**: find repeated easing, duration, or keyframe combinations. Extract into motion tokens. + +The skill is cautious. It only extracts things used three or more times, with the same intent. It never extracts "because it might be reused later". Premature abstraction is worse than duplication. + +## Try it + +``` +/extract the button styles +``` + +Expected output: + +- Found 14 button instances across 8 files +- 4 distinct variants: primary (filled accent), secondary (bordered), ghost (text-only), destructive (filled red) +- All 4 variants use the same size scale (small, default, large) +- Extracted into ` + + + + + + + + +
+
+
/
+

No anti-patterns detected

+

Overlays will appear on the page when issues are found

+
+
+ + + + diff --git a/extension/devtools/panel.js b/extension/devtools/panel.js new file mode 100644 index 000000000..ff567ae25 --- /dev/null +++ b/extension/devtools/panel.js @@ -0,0 +1,519 @@ +/** + * Impeccable DevTools Extension - Panel + * + * Displays findings, provides controls for scanning and overlay toggling, + * and allows clicking findings to inspect elements. + */ + +// Match the DevTools theme (light or dark) +if (chrome.devtools.panels.themeName === 'dark') { + document.documentElement.classList.add('theme-dark'); +} + +const tabId = chrome.devtools.inspectedWindow.tabId; + +// Auto-reconnecting port. Service workers in MV3 can be terminated after ~30s of +// inactivity (especially when the browser window is unfocused). When they restart, +// the existing port becomes invalid. We recreate it lazily on the next use. +let port = null; +function getPort() { + if (port) return port; + port = chrome.runtime.connect({ name: `impeccable-panel-${tabId}` }); + port.onMessage.addListener(handlePortMessage); + port.onDisconnect.addListener(() => { port = null; }); + return port; +} +function postToPort(msg) { + try { + getPort().postMessage(msg); + } catch { + // Port died mid-call. Drop it and try once more with a fresh port. + port = null; + try { getPort().postMessage(msg); } catch { /* give up silently */ } + } +} + +const badge = document.getElementById('badge'); +const container = document.getElementById('findings-container'); +const emptyState = document.getElementById('empty-state'); +const btnRescan = document.getElementById('btn-rescan'); +const btnToggle = document.getElementById('btn-toggle'); +const btnCopyAll = document.getElementById('btn-copy-all'); +const settingsContainer = document.getElementById('settings-container'); +const settingsList = document.getElementById('settings-list'); +const btnSettings = document.getElementById('btn-settings'); + +let overlaysVisible = true; +let allAntipatterns = []; +let disabledRules = []; +let currentFindings = []; + +// Load antipatterns list and disabled rules +async function initSettings() { + try { + const resp = await fetch(chrome.runtime.getURL('detector/antipatterns.json')); + allAntipatterns = await resp.json(); + } catch { allAntipatterns = []; } + + const stored = await chrome.storage.sync.get({ + disabledRules: [], + lineLengthMode: 'strict', + spotlightBlur: true, + autoScan: 'panel', + }); + disabledRules = stored.disabledRules; + renderSettings(); + initLineLengthControl(stored.lineLengthMode); + initSpotlightBlurToggle(stored.spotlightBlur); + initAutoScanControl(stored.autoScan); +} + +function initAutoScanControl(currentMode) { + const group = document.getElementById('auto-scan-mode'); + if (!group) return; + for (const btn of group.querySelectorAll('button')) { + btn.classList.toggle('active', btn.dataset.value === currentMode); + btn.addEventListener('click', async () => { + const mode = btn.dataset.value; + for (const b of group.querySelectorAll('button')) { + b.classList.toggle('active', b === btn); + } + await chrome.storage.sync.set({ autoScan: mode }); + }); + } +} + +function initLineLengthControl(currentMode) { + const group = document.getElementById('line-length-mode'); + if (!group) return; + for (const btn of group.querySelectorAll('button')) { + btn.classList.toggle('active', btn.dataset.value === currentMode); + btn.addEventListener('click', async () => { + const mode = btn.dataset.value; + for (const b of group.querySelectorAll('button')) { + b.classList.toggle('active', b === btn); + } + await chrome.storage.sync.set({ lineLengthMode: mode }); + chrome.runtime.sendMessage({ action: 'disabled-rules-changed' }); + }); + } +} + +function initSpotlightBlurToggle(currentValue) { + const cb = document.getElementById('spotlight-blur-toggle'); + if (!cb) return; + cb.checked = currentValue; + cb.addEventListener('change', async () => { + await chrome.storage.sync.set({ spotlightBlur: cb.checked }); + chrome.runtime.sendMessage({ action: 'disabled-rules-changed' }); + }); +} + +function renderSettings() { + settingsList.innerHTML = ''; + + const categories = { + slop: { label: 'AI tells', items: [] }, + quality: { label: 'Quality', items: [] }, + }; + for (const ap of allAntipatterns) { + const cat = ap.category || 'quality'; + (categories[cat] || categories.quality).items.push(ap); + } + + for (const [, group] of Object.entries(categories)) { + if (!group.items.length) continue; + + const header = document.createElement('div'); + header.className = 'settings-header'; + header.textContent = group.label; + settingsList.appendChild(header); + + const grid = document.createElement('div'); + grid.className = 'settings-grid'; + + for (const ap of group.items) { + const label = document.createElement('label'); + label.className = 'setting-rule'; + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.checked = !disabledRules.includes(ap.id); + checkbox.addEventListener('change', () => toggleRule(ap.id, checkbox.checked)); + + const text = document.createElement('span'); + text.textContent = ap.name; + + label.appendChild(checkbox); + label.appendChild(text); + grid.appendChild(label); + } + settingsList.appendChild(grid); + } +} + +async function toggleRule(ruleId, enabled) { + if (enabled) { + disabledRules = disabledRules.filter(id => id !== ruleId); + } else { + if (!disabledRules.includes(ruleId)) disabledRules.push(ruleId); + } + await chrome.storage.sync.set({ disabledRules }); + chrome.runtime.sendMessage({ action: 'disabled-rules-changed' }); +} + +// Listen for messages from the service worker (called by getPort() on each new connection) +function handlePortMessage(msg) { + if (msg.action === 'page-pointer-active') { + // Cursor is active on the page → user has left the panel + setHoveredItem(null); + return; + } + if (msg.action === 'findings' || msg.action === 'state') { + renderFindings(msg.findings || []); + if (msg.overlaysVisible !== undefined) { + overlaysVisible = msg.overlaysVisible; + updateToggleButton(); + } + } + if (msg.action === 'overlays-toggled') { + overlaysVisible = msg.visible; + updateToggleButton(); + } + if (msg.action === 'navigated') { + showScanning(); + } +} + +// Initial connection +getPort(); + +// Heartbeat to keep the MV3 service worker alive while the panel is open. +// SWs can be terminated after ~30s of inactivity, especially when the browser is unfocused. +setInterval(() => postToPort({ action: 'ping' }), 20000); + +// Controls +btnRescan.addEventListener('click', () => { + showScanning(); + postToPort({ action: 'scan' }); +}); + +btnToggle.addEventListener('click', () => { + postToPort({ action: 'toggle-overlays' }); +}); + +btnSettings.addEventListener('click', () => { + const isVisible = settingsContainer.style.display !== 'none'; + settingsContainer.style.display = isVisible ? 'none' : ''; + btnSettings.classList.toggle('active', !isVisible); +}); + +function updateToggleButton() { + btnToggle.title = overlaysVisible ? 'Hide overlays' : 'Show overlays'; + btnToggle.classList.toggle('inactive', !overlaysVisible); +} + +function showScanning() { + container.innerHTML = ` +
+
+ Scanning page... +
`; +} + +// Maps each anti-pattern to the most relevant Impeccable skill(s) for fixing it. +// These are suggestions; the user decides whether and how to apply them. +const FIX_SKILLS = { + // AI slop + 'side-tab': 'distill, polish', + 'border-accent-on-rounded':'distill, polish', + 'overused-font': 'typeset', + 'single-font': 'typeset', + 'flat-type-hierarchy': 'typeset', + 'gradient-text': 'typeset, distill', + 'ai-color-palette': 'colorize, distill', + 'nested-cards': 'distill, arrange', + 'monotonous-spacing': 'arrange', + 'everything-centered': 'arrange', + 'bounce-easing': 'animate', + 'dark-glow': 'quieter, distill', + 'icon-tile-stacked-above-heading': 'distill, arrange', + // Quality + 'pure-black-white': 'colorize', + 'gray-on-color': 'colorize', + 'low-contrast': 'colorize, audit', + 'layout-transition': 'animate, optimize', + 'line-length': 'arrange, typeset', + 'cramped-padding': 'arrange, polish', + 'tight-leading': 'typeset', + 'skipped-heading': 'audit, harden', + 'justified-text': 'typeset', + 'tiny-text': 'typeset', + 'all-caps-body': 'typeset', + 'wide-tracking': 'typeset', +}; + +function fixSkillFor(type) { + const skills = FIX_SKILLS[type] || 'polish'; + // Prefix each comma-separated skill with a slash for clarity + return skills.split(',').map(s => '/' + s.trim()).join(', '); +} + +// Returns a sorted array of unique skills referenced by the given findings, +// most-frequent first. Each entry already has the leading slash. +function uniqueSkillsForFindings(findings) { + const counts = new Map(); + for (const item of findings) { + for (const f of item.findings) { + const list = (FIX_SKILLS[f.type] || 'polish').split(',').map(s => '/' + s.trim()); + for (const s of list) { + counts.set(s, (counts.get(s) || 0) + 1); + } + } + } + return [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([s]) => s); +} + +function getInspectedUrl() { + return new Promise((resolve) => { + // Strip the URL fragment — anchors are noise for "what page is this from" + chrome.devtools.inspectedWindow.eval( + '(function(){var u=new URL(location.href);u.hash="";return u.toString();})()', + (result) => resolve(typeof result === 'string' ? result : '') + ); + }); +} + +async function formatFindingsForCopy(findings) { + if (!findings.length) return 'Impeccable found no anti-patterns on this page.'; + const url = await getInspectedUrl(); + const lines = ['# Impeccable findings']; + if (url) lines.push(`URL: ${url}`); + lines.push(''); + + const groups = { slop: [], quality: [] }; + for (const item of findings) { + for (const f of item.findings) { + const cat = f.category || 'quality'; + groups[cat].push({ ...f, selector: item.selector, isPageLevel: item.isPageLevel }); + } + } + + if (groups.slop.length) { + lines.push(`## AI tells (${groups.slop.length})`); + for (const f of groups.slop) { + const where = f.isPageLevel ? '_(page-level)_' : `\`${f.selector}\``; + lines.push(`- **${f.name}** at ${where}: ${f.detail}`); + } + lines.push(''); + } + + if (groups.quality.length) { + lines.push(`## Quality issues (${groups.quality.length})`); + for (const f of groups.quality) { + const where = f.isPageLevel ? '_(page-level)_' : `\`${f.selector}\``; + lines.push(`- **${f.name}** at ${where}: ${f.detail}`); + } + lines.push(''); + } + + // Roll up suggested skills across all findings (most-relevant first) + const skills = uniqueSkillsForFindings(findings); + if (skills.length) { + lines.push(`Suggested Impeccable skills to fix: ${skills.join(', ')}`); + lines.push(''); + } + + lines.push('---'); + lines.push('Detected by [Impeccable](https://impeccable.style). Skills are suggestions, not required.'); + return lines.join('\n'); +} + +async function formatSingleFindingForCopy(item, finding) { + const url = await getInspectedUrl(); + const where = item.isPageLevel ? '_(page-level)_' : `\`${item.selector}\``; + const lines = [`# Impeccable: ${finding.name}`]; + if (url) lines.push(`URL: ${url}`); + lines.push(`Element: ${where}`); + lines.push(`Detail: ${finding.detail}`); + lines.push(''); + lines.push(finding.description); + lines.push(''); + lines.push(`Suggested Impeccable skill(s) to fix: ${fixSkillFor(finding.type)}`); + return lines.join('\n'); +} + +async function copyToClipboard(text, btn) { + if (text instanceof Promise) text = await text; + try { + await navigator.clipboard.writeText(text); + if (btn) { + const orig = btn.title; + btn.title = 'Copied!'; + btn.classList.add('copied'); + setTimeout(() => { + btn.title = orig; + btn.classList.remove('copied'); + }, 1200); + } + } catch (err) { + console.warn('Copy failed', err); + } +} + +btnCopyAll.addEventListener('click', () => { + copyToClipboard(formatFindingsForCopy(currentFindings), btnCopyAll); +}); + +// Delegated hover tracking on the findings container. +// Reliably handles cursor moving between items, into children, or out of the panel. +let currentHoverSelector = null; +function setHoveredItem(selector) { + if (selector === currentHoverSelector) return; + currentHoverSelector = selector; + if (selector) { + postToPort({ action: 'highlight', selector }); + } else { + postToPort({ action: 'unhighlight' }); + } +} + +container.addEventListener('pointermove', (e) => { + const item = e.target.closest('.finding-item'); + const selector = item && !item.classList.contains('is-hidden') ? item.dataset.selector || null : null; + setHoveredItem(selector); +}); + +// Slow-cursor fallbacks (these fire reliably for slow movements) +container.addEventListener('pointerleave', () => setHoveredItem(null)); +window.addEventListener('blur', () => setHoveredItem(null)); + + +function renderFindings(findings) { + currentFindings = findings; + if (!findings.length) { + container.innerHTML = ''; + container.appendChild(emptyState); + emptyState.style.display = ''; + badge.classList.remove('visible'); + badge.textContent = '0'; + return; + } + + emptyState.style.display = 'none'; + + // Count total element-level findings + const totalCount = findings.reduce((sum, f) => sum + f.findings.length, 0); + badge.textContent = String(totalCount); + badge.classList.add('visible'); + + // Group findings by category, then by anti-pattern type + const categories = { slop: new Map(), quality: new Map() }; + for (const item of findings) { + for (const f of item.findings) { + const cat = f.category || 'quality'; + const groups = categories[cat] || categories.quality; + if (!groups.has(f.type)) { + groups.set(f.type, { name: f.name, description: f.description, items: [] }); + } + groups.get(f.type).items.push({ + selector: item.selector, + tagName: item.tagName, + isPageLevel: item.isPageLevel, + isHidden: item.isHidden, + detail: f.detail, + }); + } + } + + container.innerHTML = ''; + + const CATEGORY_LABELS = { slop: 'AI tells', quality: 'Quality issues' }; + for (const [catKey, groups] of Object.entries(categories)) { + if (groups.size === 0) continue; + + const catCount = [...groups.values()].reduce((sum, g) => sum + g.items.length, 0); + const section = document.createElement('div'); + section.className = 'category-section category-' + catKey; + + const catHeader = document.createElement('div'); + catHeader.className = 'category-header'; + catHeader.innerHTML = ` + + ${CATEGORY_LABELS[catKey]} + ${catCount}`; + section.appendChild(catHeader); + + for (const [type, group] of groups) { + const groupEl = document.createElement('div'); + groupEl.className = 'finding-group'; + + const header = document.createElement('div'); + header.className = 'group-header'; + header.innerHTML = ` + + ${escapeHtml(group.name)} + ${group.items.length}`; + header.addEventListener('click', () => header.classList.toggle('collapsed')); + groupEl.appendChild(header); + + const itemsEl = document.createElement('div'); + itemsEl.className = 'group-items'; + + for (const item of group.items) { + const itemEl = document.createElement('div'); + itemEl.className = 'finding-item' + (item.isHidden ? ' is-hidden' : ''); + const tag = item.isPageLevel + ? 'page' + : item.isHidden ? 'hidden' : ''; + itemEl.innerHTML = ` + ${tag} +
+ ${escapeHtml(item.selector)} + +
+ ${escapeHtml(item.detail)} + ${escapeHtml(group.description)}`; + + const copyBtn = itemEl.querySelector('.finding-copy'); + const finding = { type, name: group.name, description: group.description, detail: item.detail }; + copyBtn.addEventListener('click', (e) => { + e.stopPropagation(); + copyToClipboard(formatSingleFindingForCopy(item, finding), copyBtn); + }); + + if (!item.isPageLevel && !item.isHidden) { + itemEl.dataset.selector = item.selector; + itemEl.addEventListener('click', () => inspectElement(item.selector)); + } + + itemsEl.appendChild(itemEl); + } + + groupEl.appendChild(itemsEl); + section.appendChild(groupEl); + } + + container.appendChild(section); + } +} + +function inspectElement(selector) { + const escaped = selector.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); + chrome.devtools.inspectedWindow.eval( + `(function() { + var el = document.querySelector('${escaped}'); + if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); inspect(el); } + })()` + ); +} + +function escapeHtml(str) { + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; +} + +initSettings(); diff --git a/extension/devtools/sidebar.css b/extension/devtools/sidebar.css new file mode 100644 index 000000000..563297fa7 --- /dev/null +++ b/extension/devtools/sidebar.css @@ -0,0 +1,97 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --bg: #fff; + --text: #1a1a1a; + --text-dim: #666; + --text-faint: #999; + --accent: oklch(48% 0.25 350); + --rule: #e8e6e2; +} + +.theme-dark { + --bg: #1a1a1a; + --text: #f5f3ef; + --text-dim: #9a9590; + --text-faint: #666; + --accent: oklch(60% 0.25 350); + --rule: #2a2a2a; +} + +body { + background: var(--bg); + color: var(--text); + font-family: -apple-system, BlinkMacSystemFont, system-ui, sans-serif; + font-size: 12px; + line-height: 1.5; + padding: 12px 14px; +} + +/* Empty / no-findings states */ + +.state { + color: var(--text-faint); + font-size: 12px; + font-style: italic; + padding: 4px 0; +} + +.state strong { + color: var(--text-dim); + font-style: normal; + font-weight: 600; +} + +/* Finding list */ + +.finding + .finding { + margin-top: 14px; + padding-top: 14px; + border-top: 1px solid var(--rule); +} + +.finding-header { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 8px; + margin-bottom: 6px; +} + +.finding-name { + font-weight: 600; + font-size: 12px; + color: var(--text); + letter-spacing: -0.005em; +} + +.finding-name .marker { + color: var(--accent); + margin-right: 4px; +} + +.finding-kind { + font-size: 9px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-faint); + flex-shrink: 0; +} + +.finding-detail { + font-family: ui-monospace, 'SF Mono', 'Cascadia Code', monospace; + font-size: 11px; + color: var(--text-dim); + margin-bottom: 6px; +} + +.finding-description { + font-size: 11px; + color: var(--text-dim); + line-height: 1.55; +} diff --git a/extension/devtools/sidebar.html b/extension/devtools/sidebar.html new file mode 100644 index 000000000..99ac18f4f --- /dev/null +++ b/extension/devtools/sidebar.html @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/extension/devtools/sidebar.js b/extension/devtools/sidebar.js new file mode 100644 index 000000000..bff8e95cc --- /dev/null +++ b/extension/devtools/sidebar.js @@ -0,0 +1,103 @@ +/** + * Impeccable DevTools Extension - Elements Sidebar Pane + * + * Shows Impeccable findings for the currently selected element ($0) in the Elements panel. + */ + +if (chrome.devtools.panels.themeName === 'dark') { + document.documentElement.classList.add('theme-dark'); +} + +const tabId = chrome.devtools.inspectedWindow.tabId; +const content = document.getElementById('sidebar-content'); +let currentFindings = []; + +// Auto-reconnecting port (service worker may restart in MV3) +let port = null; +function getPort() { + if (port) return port; + port = chrome.runtime.connect({ name: `impeccable-sidebar-${tabId}` }); + port.onMessage.addListener((msg) => { + if (msg.action === 'findings' || msg.action === 'state') { + currentFindings = msg.findings || []; + refreshForCurrentSelection(); + } + }); + port.onDisconnect.addListener(() => { port = null; }); + return port; +} +getPort(); + +chrome.devtools.panels.elements.onSelectionChanged.addListener(refreshForCurrentSelection); + +function refreshForCurrentSelection() { + if (!currentFindings.length) { + renderEmpty('No findings on this page yet.'); + return; + } + + // Collect non-page-level selectors and ask the inspected window which one matches $0 + const selectors = []; + for (const item of currentFindings) { + if (item.isPageLevel || item.isHidden) continue; + selectors.push(item.selector); + } + if (!selectors.length) { + renderEmpty('No element-level findings on this page.'); + return; + } + + const code = `(function() { + var sels = ${JSON.stringify(selectors)}; + var matched = []; + for (var i = 0; i < sels.length; i++) { + try { if (document.querySelector(sels[i]) === $0) matched.push(sels[i]); } catch (e) {} + } + return matched; + })()`; + + chrome.devtools.inspectedWindow.eval(code, (matched) => { + if (!matched || !matched.length) { + renderNoFindings(); + return; + } + const items = currentFindings.filter(item => matched.includes(item.selector)); + render(items); + }); +} + +function renderEmpty(text) { + content.innerHTML = `
${escapeHtml(text)}
`; +} + +function renderNoFindings() { + content.innerHTML = `
Clean. No anti-patterns on this element.
`; +} + +function render(items) { + const html = []; + for (const item of items) { + for (const f of item.findings) { + const isSlop = f.category === 'slop'; + const marker = isSlop ? '\u2726' : ''; + const kind = isSlop ? 'AI tell' : 'Quality'; + html.push(` +
+
+ ${marker}${escapeHtml(f.name)} + ${kind} +
+
${escapeHtml(f.detail)}
+
${escapeHtml(f.description)}
+
+ `); + } + } + content.innerHTML = html.join(''); +} + +function escapeHtml(str) { + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; +} diff --git a/extension/icons/icon-128.png b/extension/icons/icon-128.png new file mode 100644 index 000000000..0f59fc1c6 Binary files /dev/null and b/extension/icons/icon-128.png differ diff --git a/extension/icons/icon-16.png b/extension/icons/icon-16.png new file mode 100644 index 000000000..06eaaf714 Binary files /dev/null and b/extension/icons/icon-16.png differ diff --git a/extension/icons/icon-32.png b/extension/icons/icon-32.png new file mode 100644 index 000000000..ab6d0fc9b Binary files /dev/null and b/extension/icons/icon-32.png differ diff --git a/extension/icons/icon-48.png b/extension/icons/icon-48.png new file mode 100644 index 000000000..a38563534 Binary files /dev/null and b/extension/icons/icon-48.png differ diff --git a/extension/icons/icon.svg b/extension/icons/icon.svg new file mode 100644 index 000000000..02500b774 --- /dev/null +++ b/extension/icons/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extension/icons/promo-small.png b/extension/icons/promo-small.png new file mode 100644 index 000000000..aa9f5ee9d Binary files /dev/null and b/extension/icons/promo-small.png differ diff --git a/extension/manifest.json b/extension/manifest.json new file mode 100644 index 000000000..c1ae628e4 --- /dev/null +++ b/extension/manifest.json @@ -0,0 +1,32 @@ +{ + "manifest_version": 3, + "name": "Impeccable", + "description": "Detect common UI anti-patterns in any web page", + "version": "1.0.0", + "permissions": ["activeTab", "scripting", "storage", "webNavigation"], + "host_permissions": [""], + "background": { + "service_worker": "background/service-worker.js" + }, + "devtools_page": "devtools/devtools.html", + "action": { + "default_popup": "popup/popup.html", + "default_icon": { + "16": "icons/icon-16.png", + "32": "icons/icon-32.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" + } + }, + "icons": { + "16": "icons/icon-16.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" + }, + "web_accessible_resources": [ + { + "resources": ["detector/detect.js"], + "matches": [""] + } + ] +} diff --git a/extension/popup/popup.css b/extension/popup/popup.css new file mode 100644 index 000000000..e6d78b6eb --- /dev/null +++ b/extension/popup/popup.css @@ -0,0 +1,110 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + width: 220px; + background: #1a1a1a; + color: #f5f3ef; + font-family: system-ui, -apple-system, sans-serif; + font-size: 13px; + padding: 16px; +} + +header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 16px; +} + +.logo { + font-size: 18px; + font-weight: 500; + opacity: 0.7; +} + +h1 { + font-size: 14px; + font-weight: 600; +} + +.count-display { + text-align: center; + padding: 16px 0; + margin-bottom: 16px; +} + +.count-number { + display: block; + font-size: 36px; + font-weight: 700; + line-height: 1; + margin-bottom: 4px; + color: #999; + transition: color 0.2s; +} + +.count-number.has-findings { + color: oklch(55% 0.25 350); +} + +.count-label { + font-size: 12px; + color: #999; +} + +.actions { + display: flex; + flex-direction: column; + gap: 6px; + margin-bottom: 16px; +} + +.btn { + display: block; + width: 100%; + padding: 8px 12px; + border: none; + border-radius: 6px; + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: background 0.15s, opacity 0.15s; +} + +.btn-primary { + background: oklch(55% 0.25 350); + color: white; +} + +.btn-primary:hover { + background: oklch(50% 0.25 350); +} + +.btn-secondary { + background: #333; + color: #ccc; +} + +.btn-secondary:hover { + background: #3a3a3a; +} + +footer { + text-align: center; + padding-top: 8px; + border-top: 1px solid #333; +} + +footer a { + font-size: 11px; + color: #666; + text-decoration: none; +} + +footer a:hover { + color: #999; +} diff --git a/extension/popup/popup.html b/extension/popup/popup.html new file mode 100644 index 000000000..591cd908e --- /dev/null +++ b/extension/popup/popup.html @@ -0,0 +1,29 @@ + + + + + + + +
+ +

Impeccable

+
+ +
+ 0 + anti-patterns +
+ +
+ + +
+ + + + + + diff --git a/extension/popup/popup.js b/extension/popup/popup.js new file mode 100644 index 000000000..c8779df13 --- /dev/null +++ b/extension/popup/popup.js @@ -0,0 +1,67 @@ +/** + * Impeccable DevTools Extension - Popup + * + * Quick controls: scan, toggle overlays, and see finding count. + */ + +const countNumber = document.getElementById('count-number'); +const countLabel = document.getElementById('count-label'); +const btnScan = document.getElementById('btn-scan'); +const btnToggle = document.getElementById('btn-toggle'); + +let overlaysVisible = true; + +async function getActiveTabId() { + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + return tab?.id; +} + +function updateFromState(state) { + if (!state) return; + const count = state.findings?.reduce((sum, f) => sum + f.findings.length, 0) || 0; + countNumber.textContent = String(count); + countNumber.classList.toggle('has-findings', count > 0); + countLabel.textContent = count === 1 ? 'anti-pattern' : 'anti-patterns'; + overlaysVisible = state.overlaysVisible !== false; + btnToggle.textContent = overlaysVisible ? 'Hide overlays' : 'Show overlays'; +} + +async function loadState() { + const tabId = await getActiveTabId(); + if (!tabId) return; + chrome.runtime.sendMessage({ action: 'get-state', tabId }, updateFromState); +} + +// Listen for real-time updates from service worker +chrome.runtime.onMessage.addListener((msg) => { + if (msg.action === 'findings-updated') { + const count = msg.findings?.reduce((sum, f) => sum + f.findings.length, 0) || 0; + countNumber.textContent = String(count); + countNumber.classList.toggle('has-findings', count > 0); + countLabel.textContent = count === 1 ? 'anti-pattern' : 'anti-patterns'; + btnScan.textContent = 'Scan page'; + btnScan.disabled = false; + } + if (msg.action === 'overlays-toggled-broadcast') { + overlaysVisible = msg.visible; + btnToggle.textContent = overlaysVisible ? 'Hide overlays' : 'Show overlays'; + } +}); + +btnScan.addEventListener('click', async () => { + const tabId = await getActiveTabId(); + if (!tabId) return; + btnScan.textContent = 'Scanning...'; + btnScan.disabled = true; + chrome.runtime.sendMessage({ action: 'scan', tabId }); +}); + +btnToggle.addEventListener('click', async () => { + const tabId = await getActiveTabId(); + if (!tabId) return; + chrome.runtime.sendMessage({ action: 'toggle-overlays', tabId }); + overlaysVisible = !overlaysVisible; + btnToggle.textContent = overlaysVisible ? 'Hide overlays' : 'Show overlays'; +}); + +loadState(); diff --git a/package.json b/package.json index 3ce585fc0..6fa2df304 100644 --- a/package.json +++ b/package.json @@ -1,36 +1,71 @@ { "name": "impeccable", - "version": "1.6.0", + "version": "2.0.7", "author": "Paul Bakaus", - "dependencies": { - "archiver": "^7.0.1", - "motion": "^12.23.26", - "playwright": "^1.57.0" - }, - "description": "Cross-provider design skills and commands for LLM-powered development tools", + "description": "Design skills, commands, and anti-pattern detection for AI coding agents", "keywords": [ - "cursor", - "claude", - "gemini", - "codex", "design", "frontend", - "ux" + "ux", + "skills", + "ai", + "anti-patterns", + "lint", + "accessibility", + "css", + "html", + "detection", + "ci-cd" + ], + "license": "Apache-2.0", + "homepage": "https://impeccable.style", + "repository": { + "type": "git", + "url": "git+https://github.com/pbakaus/impeccable.git" + }, + "engines": { + "node": ">=18" + }, + "type": "module", + "bin": { + "impeccable": "bin/cli.js" + }, + "main": "./src/detect-antipatterns.mjs", + "exports": { + ".": "./src/detect-antipatterns.mjs", + "./browser": "./src/detect-antipatterns-browser.js" + }, + "files": [ + "bin/", + "src/", + "LICENSE" ], - "license": "SEE LICENSE FILE", "scripts": { "build": "bun run scripts/build.js", + "build:browser": "node scripts/build-browser-detector.js", + "build:extension": "node scripts/build-extension.js", "clean": "rm -rf dist build", "rebuild": "bun run clean && bun run build", "dev": "bun run server/index.js", "preview": "bun run build && wrangler pages dev", "deploy": "bun run build && wrangler pages deploy build/", - "test": "bun test", + "test": "bun test tests/build.test.js tests/detect-antipatterns.test.js && node --test tests/detect-antipatterns-fixtures.test.mjs && node --test tests/detect-antipatterns-browser.test.mjs", + "prepack": "cp README.md README.repo.md && cp README.npm.md README.md", + "postpack": "cp README.repo.md README.md && rm README.repo.md", "screenshot": "bun run scripts/screenshot-antipatterns.js", "og-image": "bun run scripts/generate-og-image.js" }, - "type": "module", + "dependencies": { + "jsdom": "^29.0.0", + "marked": "^16.1.0" + }, + "optionalDependencies": { + "puppeteer": "^24.39.1" + }, "devDependencies": { - "wrangler": "^4.71.0" + "archiver": "^7.0.1", + "motion": "^12.38.0", + "playwright": "^1.58.2", + "wrangler": "^4.75.0" } } diff --git a/public/antipattern-examples/bad-contrast.html b/public/antipattern-examples/bad-contrast.html index 1a8670f81..c77c9d83e 100644 --- a/public/antipattern-examples/bad-contrast.html +++ b/public/antipattern-examples/bad-contrast.html @@ -20,8 +20,8 @@ } .container { - width: 1080px; - height: 1080px; + max-width: 960px; + width: 100%; margin: 0 auto; background: linear-gradient(180deg, #3b82f6 0%, #8b5cf6 100%); border-radius: 24px; @@ -150,22 +150,7 @@ border: none; } - /* Anti-pattern label */ - .antipattern-label { - position: absolute; - bottom: 24px; - left: 50%; - transform: translateX(-50%); - background: rgba(0,0,0,0.9); - color: #f87171; - padding: 8px 20px; - border-radius: 8px; - font-size: 13px; - font-weight: 600; - letter-spacing: 0.5px; - z-index: 10; - border: 1px solid rgba(248, 113, 113, 0.3); - } + @@ -216,7 +201,7 @@ -
🚨 AI SLOP: Gray on Color, Gray Labels, Absolute Black/White
+ diff --git a/public/antipattern-examples/cardocalypse.html b/public/antipattern-examples/cardocalypse.html index 784c0a3de..01e2a24c0 100644 --- a/public/antipattern-examples/cardocalypse.html +++ b/public/antipattern-examples/cardocalypse.html @@ -20,8 +20,8 @@ } .container { - width: 1080px; - height: 1080px; + max-width: 960px; + width: 100%; margin: 0 auto; background: #e5e7eb; border-radius: 24px; @@ -283,22 +283,7 @@ .depth-4 { background: #f9fafb; border: 1px solid #e5e7eb; } .depth-5 { background: #ffffff; border: 1px solid #e5e7eb; } - /* Anti-pattern label */ - .antipattern-label { - position: absolute; - bottom: 24px; - left: 50%; - transform: translateX(-50%); - background: rgba(0,0,0,0.9); - color: #f87171; - padding: 8px 20px; - border-radius: 8px; - font-size: 13px; - font-weight: 600; - letter-spacing: 0.5px; - z-index: 10; - border: 1px solid rgba(248, 113, 113, 0.3); - } + @@ -384,7 +369,7 @@ -
🚨 AI SLOP: Cardocalypse (Cards Within Cards)
+ diff --git a/public/antipattern-examples/inter-everywhere.html b/public/antipattern-examples/inter-everywhere.html index db435b7ea..5fe854475 100644 --- a/public/antipattern-examples/inter-everywhere.html +++ b/public/antipattern-examples/inter-everywhere.html @@ -20,8 +20,8 @@ } .container { - width: 1080px; - height: 1080px; + max-width: 960px; + width: 100%; margin: 0 auto; background: #ffffff; border-radius: 24px; @@ -251,22 +251,7 @@ margin-top: 6px; } - /* Anti-pattern label */ - .antipattern-label { - position: absolute; - bottom: 24px; - left: 50%; - transform: translateX(-50%); - background: rgba(0,0,0,0.9); - color: #f87171; - padding: 8px 20px; - border-radius: 8px; - font-size: 13px; - font-weight: 600; - letter-spacing: 0.5px; - z-index: 10; - border: 1px solid rgba(248, 113, 113, 0.3); - } + @@ -326,7 +311,7 @@ -
🚨 AI SLOP: Inter Font Everywhere
+ diff --git a/public/antipattern-examples/layout-templates.html b/public/antipattern-examples/layout-templates.html index cedf70ab9..9dbda4b12 100644 --- a/public/antipattern-examples/layout-templates.html +++ b/public/antipattern-examples/layout-templates.html @@ -20,8 +20,8 @@ } .container { - width: 1080px; - height: 1080px; + max-width: 960px; + width: 100%; margin: 0 auto; background: #ffffff; border-radius: 24px; @@ -213,22 +213,7 @@ text-transform: uppercase; } - /* Anti-pattern label */ - .antipattern-label { - position: absolute; - bottom: 24px; - left: 50%; - transform: translateX(-50%); - background: rgba(0,0,0,0.9); - color: #f87171; - padding: 8px 20px; - border-radius: 8px; - font-size: 13px; - font-weight: 600; - letter-spacing: 0.5px; - z-index: 10; - border: 1px solid rgba(248, 113, 113, 0.3); - } + @@ -331,7 +316,7 @@ -
🚨 AI SLOP: Hero Metric Layout Template (×5)
+ diff --git a/public/antipattern-examples/lazy-cool.html b/public/antipattern-examples/lazy-cool.html index 58115516b..e80b0d4a1 100644 --- a/public/antipattern-examples/lazy-cool.html +++ b/public/antipattern-examples/lazy-cool.html @@ -20,8 +20,8 @@ } .container { - width: 1080px; - height: 1080px; + max-width: 960px; + width: 100%; margin: 0 auto; background: linear-gradient(135deg, #0f0f23 0%, #0a0a1a 100%); border-radius: 24px; @@ -261,22 +261,7 @@ color: #ff00ff; } - /* Anti-pattern label */ - .antipattern-label { - position: absolute; - bottom: 24px; - left: 50%; - transform: translateX(-50%); - background: rgba(0,0,0,0.9); - color: #f87171; - padding: 8px 20px; - border-radius: 8px; - font-size: 13px; - font-weight: 600; - letter-spacing: 0.5px; - z-index: 10; - border: 1px solid rgba(248, 113, 113, 0.3); - } + @@ -329,7 +314,7 @@ -
🚨 AI SLOP: Lazy "Cool" (Glass, Glow, Neon, Mono)
+ diff --git a/public/antipattern-examples/lazy-impact.html b/public/antipattern-examples/lazy-impact.html index 9089d9859..e6bbbc8af 100644 --- a/public/antipattern-examples/lazy-impact.html +++ b/public/antipattern-examples/lazy-impact.html @@ -20,8 +20,8 @@ } .container { - width: 1080px; - height: 1080px; + max-width: 960px; + width: 100%; margin: 0 auto; background: linear-gradient(180deg, #1a1a2e 0%, #16213e 50%, #0f0f23 100%); border-radius: 24px; @@ -281,22 +281,7 @@ line-height: 1.4; } - /* Anti-pattern label */ - .antipattern-label { - position: absolute; - bottom: 24px; - left: 50%; - transform: translateX(-50%); - background: rgba(0,0,0,0.9); - color: #f87171; - padding: 8px 20px; - border-radius: 8px; - font-size: 13px; - font-weight: 600; - letter-spacing: 0.5px; - z-index: 10; - border: 1px solid rgba(248, 113, 113, 0.3); - } + @@ -387,7 +372,7 @@ -
🚨 AI SLOP: Lazy "Impact" (Gradients, Sparklines, Elastic)
+ diff --git a/public/antipattern-examples/massive-icons.html b/public/antipattern-examples/massive-icons.html index 5a7b5f8ed..aa439e7ec 100644 --- a/public/antipattern-examples/massive-icons.html +++ b/public/antipattern-examples/massive-icons.html @@ -21,8 +21,8 @@ } .container { - width: 1080px; - height: 1080px; + max-width: 960px; + width: 100%; margin: 0 auto; background: #ffffff; border-radius: 24px; @@ -129,22 +129,7 @@ text-align: left; } - /* Anti-pattern label */ - .antipattern-label { - position: absolute; - bottom: 24px; - left: 50%; - transform: translateX(-50%); - background: rgba(0,0,0,0.9); - color: #f87171; - padding: 8px 20px; - border-radius: 8px; - font-size: 13px; - font-weight: 600; - letter-spacing: 0.5px; - z-index: 10; - border: 1px solid rgba(248, 113, 113, 0.3); - } + @@ -204,11 +189,11 @@ -
🚨 AI SLOP: Massive Rounded Lucide Icons in Cards
+ diff --git a/public/antipattern-examples/modal-abuse.html b/public/antipattern-examples/modal-abuse.html index 13714d355..367a03801 100644 --- a/public/antipattern-examples/modal-abuse.html +++ b/public/antipattern-examples/modal-abuse.html @@ -20,8 +20,8 @@ } .container { - width: 1080px; - height: 1080px; + max-width: 960px; + width: 100%; margin: 0 auto; background: #f8fafc; border-radius: 24px; @@ -283,22 +283,7 @@ color: white; } - /* Anti-pattern label */ - .antipattern-label { - position: absolute; - bottom: 24px; - left: 50%; - transform: translateX(-50%); - background: rgba(0,0,0,0.9); - color: #f87171; - padding: 8px 20px; - border-radius: 8px; - font-size: 13px; - font-weight: 600; - letter-spacing: 0.5px; - z-index: 200; - border: 1px solid rgba(248, 113, 113, 0.3); - } + @@ -445,7 +430,7 @@ -
🚨 AI SLOP: Advanced Features Hidden in Modal
+ diff --git a/public/antipattern-examples/purple-gradients.html b/public/antipattern-examples/purple-gradients.html index 9f34bf12f..d5abc3c45 100644 --- a/public/antipattern-examples/purple-gradients.html +++ b/public/antipattern-examples/purple-gradients.html @@ -21,8 +21,8 @@ } .container { - width: 1080px; - height: 1080px; + max-width: 960px; + width: 100%; margin: 0 auto; background: linear-gradient(145deg, #2a1a4a 0%, #1a0a2e 100%); border-radius: 32px; @@ -189,22 +189,7 @@ margin-top: 4px; } - /* Anti-pattern label */ - .antipattern-label { - position: absolute; - bottom: 24px; - left: 50%; - transform: translateX(-50%); - background: rgba(0,0,0,0.8); - color: #f87171; - padding: 8px 20px; - border-radius: 8px; - font-size: 13px; - font-weight: 600; - letter-spacing: 0.5px; - z-index: 10; - border: 1px solid rgba(248, 113, 113, 0.3); - } + @@ -250,7 +235,7 @@ -
🚨 AI SLOP: Purple Gradients Everywhere
+ diff --git a/public/antipattern-examples/redundant-ux-writing.html b/public/antipattern-examples/redundant-ux-writing.html index cd18fdfbf..fd426029e 100644 --- a/public/antipattern-examples/redundant-ux-writing.html +++ b/public/antipattern-examples/redundant-ux-writing.html @@ -20,8 +20,8 @@ } .container { - width: 1080px; - height: 1080px; + max-width: 960px; + width: 100%; margin: 0 auto; background: #ffffff; border-radius: 24px; @@ -148,22 +148,7 @@ gap: 16px; } - /* Anti-pattern label */ - .antipattern-label { - position: absolute; - bottom: 24px; - left: 50%; - transform: translateX(-50%); - background: rgba(0,0,0,0.9); - color: #f87171; - padding: 8px 20px; - border-radius: 8px; - font-size: 13px; - font-weight: 600; - letter-spacing: 0.5px; - z-index: 10; - border: 1px solid rgba(248, 113, 113, 0.3); - } + @@ -216,7 +201,7 @@ -
🚨 AI SLOP: Extreme UX Writing Redundancy
+ diff --git a/public/antipattern-examples/thick-border-cards.html b/public/antipattern-examples/thick-border-cards.html index 7d63c92e6..5c6740a49 100644 --- a/public/antipattern-examples/thick-border-cards.html +++ b/public/antipattern-examples/thick-border-cards.html @@ -20,8 +20,8 @@ } .container { - width: 1080px; - height: 1080px; + max-width: 960px; + width: 100%; margin: 0 auto; background: #ffffff; border-radius: 24px; @@ -140,22 +140,7 @@ font-weight: 500; } - /* Anti-pattern label */ - .antipattern-label { - position: absolute; - bottom: 24px; - left: 50%; - transform: translateX(-50%); - background: rgba(0,0,0,0.9); - color: #f87171; - padding: 8px 20px; - border-radius: 8px; - font-size: 13px; - font-weight: 600; - letter-spacing: 0.5px; - z-index: 10; - border: 1px solid rgba(248, 113, 113, 0.3); - } + @@ -214,7 +199,7 @@ -
🚨 AI SLOP: Left Border Accent on Rounded Cards
+ diff --git a/public/antipattern-examples/visual-mode-demo.html b/public/antipattern-examples/visual-mode-demo.html new file mode 100644 index 000000000..b9561496a --- /dev/null +++ b/public/antipattern-examples/visual-mode-demo.html @@ -0,0 +1,231 @@ + + + + + + Visual Mode Demo + + + + +
+
+ + +
+ New: AI-Powered Everything +

Build the Future
With AI Magic

+

Transform your workflow with our revolutionary AI platform. Purple gradients included.

+
+ +
+
+
+ +

Fast Performance

+
+

Blazing fast inference with our optimized pipeline.

+
+
+
+ 🔒 +

Secure & Safe

+
+

Enterprise-grade security with encrypted data.

+
+
+
+ 📈 +

Analytics

+
+

Powerful insights to help you make better decisions.

+
+
+ +
+
+
10K+
+
Active Users
+
+
+
99.9%
+
Uptime Guaranteed
+
+
+
50M+
+
Data Processed
+
+
+
+
+ + + diff --git a/public/antipattern-images/bad-contrast.png b/public/antipattern-images/bad-contrast.png index a8dd848d4..5bfbdf2e4 100644 Binary files a/public/antipattern-images/bad-contrast.png and b/public/antipattern-images/bad-contrast.png differ diff --git a/public/antipattern-images/cardocalypse.png b/public/antipattern-images/cardocalypse.png index 7d1f839c3..7e71a9192 100644 Binary files a/public/antipattern-images/cardocalypse.png and b/public/antipattern-images/cardocalypse.png differ diff --git a/public/antipattern-images/inter-everywhere.png b/public/antipattern-images/inter-everywhere.png index d133fce67..50955c203 100644 Binary files a/public/antipattern-images/inter-everywhere.png and b/public/antipattern-images/inter-everywhere.png differ diff --git a/public/antipattern-images/layout-templates.png b/public/antipattern-images/layout-templates.png index 71b75776b..c981cc224 100644 Binary files a/public/antipattern-images/layout-templates.png and b/public/antipattern-images/layout-templates.png differ diff --git a/public/antipattern-images/lazy-cool.png b/public/antipattern-images/lazy-cool.png index 768172f76..ea35096f6 100644 Binary files a/public/antipattern-images/lazy-cool.png and b/public/antipattern-images/lazy-cool.png differ diff --git a/public/antipattern-images/lazy-impact.png b/public/antipattern-images/lazy-impact.png index c0a918212..ea05de782 100644 Binary files a/public/antipattern-images/lazy-impact.png and b/public/antipattern-images/lazy-impact.png differ diff --git a/public/antipattern-images/massive-icons.png b/public/antipattern-images/massive-icons.png index e0e88a640..d89cfa300 100644 Binary files a/public/antipattern-images/massive-icons.png and b/public/antipattern-images/massive-icons.png differ diff --git a/public/antipattern-images/modal-abuse.png b/public/antipattern-images/modal-abuse.png index 94c9ca911..4a32d4fb6 100644 Binary files a/public/antipattern-images/modal-abuse.png and b/public/antipattern-images/modal-abuse.png differ diff --git a/public/antipattern-images/purple-gradients.png b/public/antipattern-images/purple-gradients.png index 26a790a89..7cddf07d7 100644 Binary files a/public/antipattern-images/purple-gradients.png and b/public/antipattern-images/purple-gradients.png differ diff --git a/public/antipattern-images/redundant-ux-writing.png b/public/antipattern-images/redundant-ux-writing.png index df51b0165..1fcb8f4a8 100644 Binary files a/public/antipattern-images/redundant-ux-writing.png and b/public/antipattern-images/redundant-ux-writing.png differ diff --git a/public/antipattern-images/thick-border-cards.png b/public/antipattern-images/thick-border-cards.png index e77d67074..c31729454 100644 Binary files a/public/antipattern-images/thick-border-cards.png and b/public/antipattern-images/thick-border-cards.png differ diff --git a/public/app.js b/public/app.js index 2eec178ed..280189f5b 100644 --- a/public/app.js +++ b/public/app.js @@ -7,6 +7,7 @@ import { initFrameworkViz } from "./js/components/framework-viz.js"; import { initScrollReveal } from "./js/utils/reveal.js"; import { initAnchorScroll, initHashTracking } from "./js/utils/scroll.js"; import { initSectionNav } from "./js/components/section-nav.js"; +import { initFoundationGrid } from "./js/components/foundation-grid.js"; // ============================================ // STATE @@ -49,6 +50,9 @@ async function loadContent() { // Render commands (Glass Terminal) renderTerminalLayout(allCommands); + // Initialize gallery card stack + initGalleryStack(); + // Render patterns with tabbed navigation renderPatternsWithTabs(patternsData.patterns, patternsData.antipatterns); } catch (error) { @@ -89,137 +93,75 @@ function showLoadError(error) { } } +function initGalleryStack() { + const container = document.querySelector('.gallery-stack-container'); + const stack = document.getElementById('gallery-stack'); + if (!stack || !container) return; + + const cards = stack.querySelectorAll('.gallery-stack-card'); + const counter = container.querySelector('.gallery-stack-counter'); + const total = cards.length; + let current = 0; + let lastScroll = 0; + + function update() { + cards.forEach((card, i) => { + const offset = (i - current + total) % total; + card.dataset.offset = offset; + }); + } + + function next() { current = (current + 1) % total; update(); } + function prev() { current = (current - 1 + total) % total; update(); } + + container.querySelector('.gallery-stack-prev').addEventListener('click', prev); + container.querySelector('.gallery-stack-next').addEventListener('click', next); + + stack.addEventListener('wheel', (e) => { + e.preventDefault(); + const now = Date.now(); + if (now - lastScroll < 350) return; + lastScroll = now; + if (e.deltaY > 0) next(); else prev(); + }, { passive: false }); + + update(); +} + function renderPatternsWithTabs(patterns, antipatterns) { const container = document.getElementById("patterns-categories"); if (!container || !patterns || !antipatterns) return; - // Create a map of antipatterns by category name const antipatternMap = {}; - antipatterns.forEach(cat => { - antipatternMap[cat.name] = cat.items; - }); + antipatterns.forEach(cat => { antipatternMap[cat.name] = cat.items; }); - // Generate unique IDs for tabs - const tabId = (name) => `pattern-tab-${name.toLowerCase().replace(/\s+/g, '-')}`; - const panelId = (name) => `pattern-panel-${name.toLowerCase().replace(/\s+/g, '-')}`; + const tabsHTML = patterns.map((cat, i) => + `` + ).join(''); - // Build tabs with WAI-ARIA attributes - const tabsHTML = patterns - .map((category, i) => ``) - .join(""); - - // Build panels with WAI-ARIA attributes - const panelsHTML = patterns - .map((category, i) => { - const antiItems = antipatternMap[category.name] || []; - return ` -
-
-
- Don't -
    - ${antiItems.map((item) => `
  • ${escapeHtml(item)}
  • `).join("")} -
-
-
- Do -
    - ${category.items.map((item) => `
  • ${escapeHtml(item)}
  • `).join("")} -
-
+ const panelsHTML = patterns.map((cat, i) => { + const antiItems = antipatternMap[cat.name] || []; + return ` +
+
+
    ${antiItems.map(item => `
  • ${escapeHtml(item)}
  • `).join('')}
-
- `; - }) - .join(""); +
+
    ${cat.items.map(item => `
  • ${escapeHtml(item)}
  • `).join('')}
+
+
`; + }).join(''); - container.innerHTML = ` -
${tabsHTML}
-
${panelsHTML}
- `; + container.innerHTML = `
${tabsHTML}
${panelsHTML}`; - const tabs = container.querySelectorAll('.pattern-tab'); - const panels = container.querySelectorAll('.pattern-panel'); - - // Function to switch tabs - const switchTab = (newTab) => { - const tabName = newTab.dataset.tab; - - // Update ARIA attributes on all tabs - tabs.forEach(t => { - t.classList.remove('active'); - t.setAttribute('aria-selected', 'false'); - t.setAttribute('tabindex', '-1'); - }); - - // Activate the new tab - newTab.classList.add('active'); - newTab.setAttribute('aria-selected', 'true'); - newTab.setAttribute('tabindex', '0'); - newTab.focus(); - - // Update panels - panels.forEach(p => { - p.classList.remove('active'); - p.setAttribute('hidden', ''); - }); - const escapedName = CSS.escape(tabName); - const activePanel = container.querySelector(`[data-panel="${escapedName}"]`); - if (!activePanel) return; - activePanel.classList.add('active'); - activePanel.removeAttribute('hidden'); - }; - - // Tab click handling - tabs.forEach(tab => { - tab.addEventListener('click', () => switchTab(tab)); - }); - - // Keyboard navigation (Arrow keys, Home, End) - tabs.forEach((tab, index) => { - tab.addEventListener('keydown', (e) => { - let targetIndex = index; - - switch (e.key) { - case 'ArrowLeft': - case 'ArrowUp': - e.preventDefault(); - targetIndex = index === 0 ? tabs.length - 1 : index - 1; - break; - case 'ArrowRight': - case 'ArrowDown': - e.preventDefault(); - targetIndex = index === tabs.length - 1 ? 0 : index + 1; - break; - case 'Home': - e.preventDefault(); - targetIndex = 0; - break; - case 'End': - e.preventDefault(); - targetIndex = tabs.length - 1; - break; - default: - return; - } - - switchTab(tabs[targetIndex]); - }); + container.addEventListener('click', (e) => { + const tab = e.target.closest('.patterns-tab'); + if (!tab) return; + const index = tab.dataset.index; + container.querySelectorAll('.patterns-tab').forEach(t => t.classList.remove('is-active')); + container.querySelectorAll('.patterns-content').forEach(p => p.classList.remove('is-active')); + tab.classList.add('is-active'); + container.querySelector(`.patterns-content[data-index="${index}"]`).classList.add('is-active'); }); } @@ -227,28 +169,13 @@ function renderPatternsWithTabs(patterns, antipatterns) { // EVENT HANDLERS // ============================================ -// Sync prefix radio buttons to hidden checkbox + update download button label -document.querySelectorAll('input[name="prefix-choice"]').forEach((radio) => { - radio.addEventListener('change', () => { - const prefixToggle = document.getElementById('prefix-toggle'); - if (prefixToggle) prefixToggle.checked = radio.value === 'prefixed'; - const btnLabel = document.querySelector('#download-zip-btn span'); - if (btnLabel) { - btnLabel.textContent = radio.value === 'prefixed' - ? 'Download prefixed zip' - : 'Download universal zip'; - } - }); -}); - -// Handle bundle download clicks via event delegation +// Handle bundle download clicks via event delegation. +// Each download button carries the full bundle name in data-bundle (e.g. +// "universal" or "universal-prefixed") so the handler is just a redirect. document.addEventListener("click", (e) => { const bundleBtn = e.target.closest("[data-bundle]"); if (bundleBtn) { - const provider = bundleBtn.dataset.bundle; - const prefixToggle = document.getElementById('prefix-toggle'); - const usePrefixed = prefixToggle && prefixToggle.checked; - const bundleName = usePrefixed ? `${provider}-prefixed` : provider; + const bundleName = bundleBtn.dataset.bundle; window.location.href = `/api/download/bundle/${bundleName}`; } @@ -285,6 +212,7 @@ function init() { initScrollReveal(); initGlassTerminal(); initFrameworkViz(); + initFoundationGrid(); initSectionNav(); loadContent(); diff --git a/public/assets/extension-detection.png b/public/assets/extension-detection.png new file mode 100644 index 000000000..cfe3c9b89 Binary files /dev/null and b/public/assets/extension-detection.png differ diff --git a/public/cheatsheet.html b/public/cheatsheet.html index e261b0b84..cdd7f4add 100644 --- a/public/cheatsheet.html +++ b/public/cheatsheet.html @@ -13,11 +13,12 @@ Impeccable Command Cheatsheet - + - + + - -
-

Impeccable Commands

-

Quick reference for all 20 design commands

- ← Back to impeccable.style + + + +
+

Impeccable Commands

+

Quick reference for all 21 design commands

+
+

Loading commands...

@@ -194,7 +212,7 @@ const categoryOrder = ['diagnostic', 'quality', 'intensity', 'adaptation', 'enhancement', 'system']; const commandCategories = { - 'teach-impeccable': 'system', + 'impeccable': 'system', 'audit': 'diagnostic', 'critique': 'diagnostic', 'normalize': 'quality', @@ -217,7 +235,7 @@ }; const commandRelationships = { - 'teach-impeccable': { flow: 'One-time project context gathering' }, + 'impeccable': { flow: 'One-time project context gathering' }, 'audit': { leadsTo: ['normalize', 'harden', 'optimize', 'adapt', 'clarify'], flow: 'Technical quality audit' }, 'critique': { leadsTo: ['polish', 'distill', 'bolder', 'quieter', 'typeset', 'arrange'], flow: 'UX and design review' }, 'normalize': { combinesWith: ['clarify', 'adapt'], flow: 'Align with design system' }, diff --git a/public/css/main.css b/public/css/main.css index dea465a7d..ea681605a 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -5,108 +5,15 @@ * Light, high-contrast, typographically bold. */ -/* Import other CSS partials */ +/* Shared design tokens + minimal reset */ +@import "./tokens.css"; + +/* Section partials (landing page only) */ @import "./problem-section.css"; @import "./workflow.css"; @import "./gallery.css"; @import "./skill-demos.css"; -/* ============================================ - MINIMAL CSS RESET - ============================================ */ - -*, *::before, *::after { - box-sizing: border-box; -} - -* { - margin: 0; -} - -img, picture, video, canvas, svg { - display: block; - max-width: 100%; -} - -button, input, textarea, select { - font: inherit; -} - -/* ============================================ - DESIGN TOKENS - ============================================ */ - -:root { - /* Typography */ - --font-display: 'Cormorant Garamond', Georgia, serif; - --font-body: 'Instrument Sans', system-ui, sans-serif; - --font-mono: 'Space Grotesk', monospace; - - /* Spacing Scale */ - --spacing-xs: 8px; - --spacing-sm: 16px; - --spacing-md: 24px; - --spacing-lg: 32px; - --spacing-xl: 48px; - --spacing-2xl: 80px; - --spacing-3xl: 120px; - - /* Width */ - --width-max: 1400px; - --width-content: 900px; - - /* Animation */ - --ease-out: cubic-bezier(0.16, 1, 0.3, 1); - --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); - --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); - --duration-fast: 0.15s; - --duration-base: 0.3s; - --duration-slow: 0.6s; - --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); - - /* 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); - - /* Framework category colors - Light mode */ - --cat-diagnostic-bg: #fdf4ff; - --cat-diagnostic-border: #d946ef; - --cat-diagnostic-text: #a21caf; - - --cat-quality-bg: #f0fdf4; - --cat-quality-border: #22c55e; - --cat-quality-text: #15803d; - - --cat-intensity-bg: #fffbeb; - --cat-intensity-border: #f59e0b; - --cat-intensity-text: #b45309; - - --cat-adaptation-bg: #eff6ff; - --cat-adaptation-border: #3b82f6; - --cat-adaptation-text: #1d4ed8; - - --cat-enhancement-bg: #fdf2f8; - --cat-enhancement-border: #ec4899; - --cat-enhancement-text: #be185d; - - --cat-system-bg: #f5f5f4; - --cat-system-border: #78716c; - --cat-system-text: #44403c; -} - /* ============================================ SKIP LINK (Accessibility) @@ -219,8 +126,12 @@ button, input, textarea, select { } @media (max-width: 400px) { - .section-nav { - display: none; + .section-nav-item { + padding: 8px; + } + + .section-nav-num { + font-size: 0.625rem; } } @@ -442,6 +353,28 @@ code { } } +@media (max-width: 768px) { + .hero-combined-right { + order: -1; + padding-top: var(--spacing-xl); + } + + .hero-combined-container { + gap: var(--spacing-md); + } + + .hero-combined-left { + gap: var(--spacing-sm); + padding-top: var(--spacing-md); + } + + .hero-hook-text, + .hero-included-box, + .hero-cta-group { + margin-top: var(--spacing-xs); + } +} + .hero-combined-left { display: flex; flex-direction: column; @@ -483,6 +416,20 @@ code { margin: 0; } +.hero-hook-text--short { + display: none; +} + +@media (max-width: 768px) { + .hero-hook-text--full { + display: none; + } + + .hero-hook-text--short { + display: block; + } +} + /* Hero Included Box - Subtle "What's included" */ .hero-included-box { display: flex; @@ -909,6 +856,18 @@ code { color: var(--color-ash); } +@media (max-width: 768px) { + .split-comparison { + padding: 8px; + margin-top: -8px; + margin-bottom: -8px; + } + + .split-labels { + display: none; + } +} + .split-label-dot { width: 8px; height: 8px; @@ -921,12 +880,423 @@ code { } /* ============================================ - SOLUTION SECTION + FOUNDATION SECTION + ============================================ */ + +.foundation-section { + padding: var(--spacing-2xl) 0; + border-top: 1px solid var(--color-mist); +} + +.foundation-content { + display: grid; + gap: var(--spacing-lg); +} + +.foundation-grid { + display: flex; + align-items: flex-end; + height: 600px; + gap: var(--spacing-sm); + margin-top: -80px; /* Pull up into the header whitespace */ + position: relative; +} + +.foundation-column { + flex: 1; + display: flex; + flex-direction: column; + height: 100%; + justify-content: flex-end; + cursor: pointer; + min-width: 0; +} + +.foundation-card { + padding: var(--spacing-md); + background: white; + border: 1px solid var(--color-bg); + border-radius: 12px; + display: flex; + flex-direction: column; + height: 280px; + margin-bottom: var(--spacing-xs); + transition: transform var(--duration-slow) var(--ease-out-quint), border-color var(--duration-base) var(--ease-out), box-shadow var(--duration-base) var(--ease-out); + position: relative; + z-index: 2; + box-sizing: border-box; +} + +.foundation-column:hover .foundation-card { + transform: translateY(-12px); + border-color: var(--color-accent); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.08); +} + +.foundation-plinth { + width: 100%; + background: repeating-linear-gradient(45deg, var(--color-bg), var(--color-bg) 2px, transparent 2px, transparent 10px); + border: 1px solid var(--color-mist); + border-bottom: none; + border-radius: 8px 8px 0 0; + transition: all var(--duration-base) var(--ease-out); +} + +.foundation-column:hover .foundation-plinth { + background: var(--color-mist); + opacity: 0.7; +} + +/* Plinth Heights - More dramatic steps */ +.plinth-1 { height: 4%; } +.plinth-2 { height: 12%; } +.plinth-3 { height: 20%; } +.plinth-4 { height: 28%; } +.plinth-5 { height: 36%; } +.plinth-6 { height: 44%; } +.plinth-7 { height: 52%; } + +.foundation-card-viz { + height: 52px; + display: flex; + align-items: center; + justify-content: flex-start; + color: var(--color-ink); + margin-bottom: var(--spacing-md); +} + +.foundation-card-header { + display: flex; + align-items: baseline; + gap: var(--spacing-xs); + margin-bottom: 8px; + overflow: hidden; +} + +.foundation-card-label { + font-family: var(--font-display); + font-size: 1.35rem; + color: var(--color-ink); + line-height: 1.1; +} + +.foundation-card-count { + font-family: var(--font-mono); + font-size: 0.7rem; + color: var(--color-accent); + flex-shrink: 0; +} + +.foundation-card-detail { + font-size: 0.875rem; + color: var(--color-ash); + line-height: 1.4; + margin: auto 0 0 0; +} + +.foundation-svg { + width: 44px; + height: 44px; +} + +/* Typography & Spatial (Breathing draw + full draw on hover) */ +.anim-draw { stroke-dasharray: 100; stroke-dashoffset: 100; animation: draw-breathe 4s ease-in-out infinite; } +.foundation-column:hover .anim-draw { animation: draw-in 0.8s var(--ease-out) forwards; } +.anim-draw-delay { stroke-dasharray: 100; stroke-dashoffset: 100; } +.foundation-column:hover .anim-draw-delay { animation: draw-in 1s var(--ease-out) 0.2s forwards; } +@keyframes draw-breathe { 0%, 100% { stroke-dashoffset: 100; } 50% { stroke-dashoffset: 40; } } +@keyframes draw-in { from { stroke-dashoffset: 100; } to { stroke-dashoffset: 0; } } + +/* Color & Contrast (Breathing pulse + spread on hover) */ +.anim-move-x { animation: pulse-x 3s ease-in-out infinite; } +.foundation-column:hover .anim-move-x { animation: spread-x 0.6s var(--ease-in-out) forwards; } +.anim-move-x-opp { animation: pulse-x-opp 3s ease-in-out infinite; } +.foundation-column:hover .anim-move-x-opp { animation: spread-x-opp 0.6s var(--ease-in-out) forwards; } +.anim-fade-in { opacity: 0; transition: opacity 0.6s var(--ease-in-out); } +.foundation-column:hover .anim-fade-in { opacity: 1; } +@keyframes pulse-x { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(1.5px); } } +@keyframes pulse-x-opp { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(-1.5px); } } +@keyframes spread-x { from { transform: translateX(0); } to { transform: translateX(4px); } } +@keyframes spread-x-opp { from { transform: translateX(0); } to { transform: translateX(-4px); } } + +/* Responsive (Staggered reflow from desktop 2-col to mobile stack) */ +.anim-res-frame, .anim-res-img, .anim-res-title, .anim-res-line-1, .anim-res-line-2 { + transform-box: fill-box; + transform-origin: 50% 50%; + transition: transform 0.4s var(--ease-in-out); +} +.anim-res-frame { transition-delay: 0s; } +.anim-res-img { transition-delay: 0s; } +.anim-res-title { transition-delay: 0s; } +.anim-res-line-1 { transition-delay: 0s; } +.anim-res-line-2 { transition-delay: 0s; } +.foundation-column:hover .anim-res-frame { transform: scaleX(0.57) scaleY(1.17); transition-delay: 0s; } +.foundation-column:hover .anim-res-img { transform: translate(6px, -4px) scale(0.65); transition-delay: 0.05s; } +.foundation-column:hover .anim-res-title { transform: translate(-7px, 6px) scaleX(0.65); transition-delay: 0.1s; } +.foundation-column:hover .anim-res-line-1 { transform: translate(-7px, 4.75px) scaleX(0.65); transition-delay: 0.15s; } +.foundation-column:hover .anim-res-line-2 { transform: translate(-6px, 4.25px) scaleX(0.6); transition-delay: 0.2s; } + +/* Interaction (Gentle wobble + full toggle on hover) */ +.anim-toggle-move { animation: toggle-wobble 3s ease-in-out infinite; } +.foundation-column:hover .anim-toggle-move { animation: toggle-snap 0.35s var(--ease-in-out) forwards; } +@keyframes toggle-wobble { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(2px); } } +@keyframes toggle-snap { from { transform: translateX(0); fill: var(--color-mist); } to { transform: translateX(8px); fill: var(--color-accent); } } + +/* Motion (Gentle bob + full bounce on hover) */ +.anim-squash-ball { transform-origin: 20px 20px; animation: ball-bob 2.5s ease-in-out infinite; } +.foundation-column:hover .anim-squash-ball { animation: bounce-ball 1.5s linear infinite; } +@keyframes ball-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(3px); } } +@keyframes bounce-ball { + 0% { transform: translateY(0); } + 6% { transform: translateY(0.5px); } + 18% { transform: translateY(4px); } + 35% { transform: translateY(12px); } + 42% { transform: translateY(12px) scaleX(1.3) scaleY(0.6); } + 48% { transform: translateY(12px); } + 65% { transform: translateY(4px); } + 78% { transform: translateY(0.5px); } + 88%, 100% { transform: translateY(0); } +} + +/* UX Writing (Cursor always blinks) */ +.anim-blink { animation: blink-key 1s step-end infinite; } +@keyframes blink-key { 0%, 100% { opacity: 1; } 50% { opacity: 0; } } + +/* Tablet: 3-col grid (switch before stairs clip) */ +@media (max-width: 1200px) { + .foundation-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + height: auto; + gap: var(--spacing-md); + margin-top: var(--spacing-xl); + } + .foundation-column { height: auto; } + .foundation-card { height: auto; min-height: 200px; } + .foundation-plinth { display: none; } + .foundation-column:hover .foundation-card { transform: translateY(-4px); } + + /* Enable hover animations on card hover for grid */ + .foundation-card:hover .anim-draw { animation: draw-in 0.8s var(--ease-out) forwards; } + .foundation-card:hover .anim-draw-delay { animation: draw-in 1s var(--ease-out) 0.2s forwards; } + .foundation-card:hover .anim-move-x { animation: spread-x 0.6s var(--ease-in-out) forwards; } + .foundation-card:hover .anim-move-x-opp { animation: spread-x-opp 0.6s var(--ease-in-out) forwards; } + .foundation-card:hover .anim-fade-in { opacity: 1; } + .foundation-card:hover .anim-res-frame { transform: scaleX(0.57) scaleY(1.17); } + .foundation-card:hover .anim-res-img { transform: translate(6px, -4px) scale(0.65); } + .foundation-card:hover .anim-res-title { transform: translate(-7px, 6px) scaleX(0.65); } + .foundation-card:hover .anim-res-line-1 { transform: translate(-7px, 4.75px) scaleX(0.65); } + .foundation-card:hover .anim-res-line-2 { transform: translate(-6px, 4.25px) scaleX(0.6); } + .foundation-card:hover .anim-toggle-move { animation: toggle-snap 0.35s var(--ease-in-out) forwards; } + .foundation-card:hover .anim-squash-ball { animation: bounce-ball 1.5s linear infinite; } +} + +/* Small tablet: 2-col grid */ +@media (max-width: 768px) { + .foundation-grid { grid-template-columns: repeat(2, 1fr); } +} + +/* Small mobile: compact horizontal cards */ +@media (max-width: 520px) { + .foundation-grid { + grid-template-columns: 1fr; + gap: var(--spacing-sm); + } + .foundation-card { + display: grid; + grid-template-columns: auto 1fr; + grid-template-rows: auto auto; + align-items: start; + min-height: auto; + padding: var(--spacing-sm) var(--spacing-md); + gap: 0 var(--spacing-md); + } + .foundation-card-viz { + grid-row: 1 / -1; + align-self: center; + height: auto; + margin-bottom: 0; + } + .foundation-card-header { margin-bottom: 2px; } + .foundation-card-label { font-size: 1.1rem; } + .foundation-card-detail { margin: 0; font-size: 0.8125rem; } +} + +.foundation-cta { + text-align: center; +} + +.foundation-cta-text { + font-size: 0.9375rem; + color: var(--color-charcoal); + margin: 0; +} + +.foundation-cta-text code { + font-family: var(--font-mono); + font-size: 0.875rem; + color: var(--color-ink); +} + +.foundation-slash { + color: var(--color-accent); +} + +/* ============================================ + LANGUAGE SECTION (wraps periodic table + commands) + ============================================ */ + +.language-section { + padding: var(--spacing-2xl) 0; + border-top: 1px solid var(--color-mist); +} + +.language-content { + display: grid; + gap: var(--spacing-lg); +} + +/* Grid and flex items default to min-width: auto, which lets them expand to + fit their intrinsic content. The mobile commands layout has a split-compare + demo wide enough to blow out the entire section on mobile (~2400px wide). + Cascade min-width: 0 through every layer of the language section so the + intrinsic width can't propagate up the parent chain. */ +.language-section, +.language-section * { + min-width: 0; +} + +.language-content .section-lead { + margin-bottom: 0; +} + +.commands-subsection { + display: grid; + gap: var(--spacing-md); +} + +.commands-header-subtitle { + font-size: 0.9375rem; + color: var(--color-warm-gray); + margin: 0; +} + +/* ============================================ + VISUAL MODE SECTION + ============================================ */ + +.visual-mode-section { + padding: var(--spacing-2xl) 0; + border-top: 1px solid var(--color-mist); +} + +.visual-mode-content { + display: grid; + gap: var(--spacing-lg); +} + +.visual-mode-demo { + display: grid; + grid-template-columns: 3fr 2fr; + gap: var(--spacing-xl); + align-items: start; +} + +@media (max-width: 900px) { + .visual-mode-demo { + grid-template-columns: 1fr; + } +} + +/* Browser chrome frame for the iframe */ +.visual-mode-preview { + border-radius: 8px; + overflow: hidden; + border: 1px solid var(--color-mist); + box-shadow: 0 8px 30px -6px rgba(0,0,0,0.12); +} + +.visual-mode-preview-header { + display: flex; + align-items: center; + gap: 6px; + padding: 10px 14px; + background: var(--color-cream); + border-bottom: 1px solid var(--color-mist); +} + +.visual-mode-preview-dot { + width: 10px; + height: 10px; + border-radius: 50%; +} + +.visual-mode-preview-dot.red { background: #ff5f56; } +.visual-mode-preview-dot.yellow { background: #ffbd2e; } +.visual-mode-preview-dot.green { background: #27c93f; } + +.visual-mode-preview-title { + margin-left: auto; + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--color-ash); +} + +.visual-mode-iframe { + display: block; + width: 100%; + height: 480px; + border: none; + background: white; +} + +/* Details sidebar — editorial blocks, no cards. + The live detection overlay (left iframe) is the visual anchor for the + whole section; the right column should be quiet text + a single image + for the Chrome extension CTA. Hierarchy via typography + space, not + bordered containers. */ +.visual-mode-details { + display: flex; + flex-direction: column; + gap: var(--spacing-xl); + padding: var(--spacing-md) 0; +} + +.visual-mode-feature { + display: flex; + flex-direction: column; + gap: var(--spacing-xs); +} + +.visual-mode-feature-label { + display: block; + font-family: var(--font-mono); + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--color-accent); +} + +.visual-mode-feature p { + font-size: 0.9375rem; + line-height: 1.55; + color: var(--color-charcoal); + margin: 0; +} + +.visual-mode-feature code { + font-family: var(--font-mono); + font-size: 0.875rem; + color: var(--color-ink); +} + +/* ============================================ + SOLUTION SECTION (legacy styles for periodic table) ============================================ */ .solution-section { padding: var(--spacing-2xl) 0; - border-top: 1px solid var(--color-mist); } .solution-content { @@ -961,7 +1331,7 @@ code { border: 1px solid var(--color-mist); border-radius: 8px; position: relative; - overflow: hidden; + overflow: visible; } .solution-pillar { @@ -1197,6 +1567,16 @@ code { display: flex; flex-direction: column; gap: var(--spacing-md); + /* Without min-width: 0, the flex container expands to fit any intrinsically + wide descendant (the split-compare demo blows it out to ~2400px otherwise). + The same constraint cascades to direct children via the next rule. */ + min-width: 0; + width: 100%; + } + + .mobile-commands-layout > * { + min-width: 0; + max-width: 100%; } .commands-container { @@ -1228,31 +1608,26 @@ code { font-family: var(--font-mono); font-size: 0.8125rem; font-weight: 500; - color: var(--color-ash); - background: var(--color-cream); - border: 1px solid var(--color-mist); + color: var(--color-charcoal); + background: none; + border: none; border-radius: 100px; cursor: pointer; - transition: all 0.2s ease; + transition: background 0.2s ease, color 0.2s ease; white-space: nowrap; } .mobile-cmd-pill:hover { - color: var(--color-charcoal); - border-color: var(--color-charcoal); + background: var(--color-mist); } .mobile-cmd-pill.active { color: var(--color-paper); background: var(--color-ink); - border-color: var(--color-ink); } .mobile-demo-area { - background: var(--color-cream); - border: 1px solid var(--color-mist); - border-radius: 8px; - padding: var(--spacing-sm); + padding: 0; } .mobile-demo-area .demo-split-comparison { @@ -1537,27 +1912,15 @@ code { background: var(--color-cream); } -.footer-content { +/* Single horizontal row: wordmark left, nav centered, credit right. + Three groups, no two-tier split, no dividers. */ +.footer-row { max-width: var(--width-max); margin: 0 auto; - display: flex; - justify-content: space-between; + display: grid; + grid-template-columns: auto 1fr auto; align-items: center; - flex-wrap: wrap; - gap: var(--spacing-lg); -} - -@media (max-width: 768px) { - .footer-content { - flex-direction: column; - text-align: center; - } -} - -.footer-brand { - display: flex; - flex-direction: column; - gap: var(--spacing-xs); + gap: var(--spacing-xl); } .footer-logo { @@ -1565,15 +1928,12 @@ code { font-size: 1.25rem; font-weight: 400; color: var(--color-ink); -} - -.footer-tagline { - font-size: 0.875rem; - color: var(--color-ash); + text-decoration: none; } .footer-links { display: flex; + justify-content: center; gap: var(--spacing-lg); flex-wrap: wrap; } @@ -1581,6 +1941,7 @@ code { .footer-links a { font-size: 0.875rem; color: var(--color-ash); + text-decoration: none; transition: color var(--duration-fast) var(--ease-out); } @@ -1588,99 +1949,54 @@ code { color: var(--color-accent); } -.footer-author { - display: flex; - align-items: center; - justify-content: center; - gap: var(--spacing-md); - flex-wrap: wrap; - width: 100%; - padding-top: var(--spacing-lg); - margin-top: var(--spacing-md); - border-top: 1px solid var(--color-mist); -} - -.footer-author-label { - font-size: 0.875rem; - color: var(--color-ash); -} - -.footer-author-label a { - color: var(--color-text); - transition: color var(--duration-fast) var(--ease-out); -} - -.footer-author-label a:hover { - color: var(--color-accent); -} - -.footer-author-links { +.footer-credit { display: flex; align-items: center; gap: var(--spacing-sm); + font-size: 0.875rem; + color: var(--color-ash); + white-space: nowrap; +} + +.footer-credit a { + color: var(--color-text); + text-decoration: none; + transition: color var(--duration-fast) var(--ease-out); +} + +.footer-credit a:hover { + color: var(--color-accent); } .footer-social-link { - display: flex; + display: inline-flex; align-items: center; justify-content: center; - width: 36px; - height: 36px; + padding: 4px; color: var(--color-ash); - background: transparent; - border-radius: 50%; - transition: all var(--duration-fast) var(--ease-out); - text-decoration: none; + transition: color var(--duration-fast) var(--ease-out); } .footer-social-link:hover { color: var(--color-accent); - background: var(--color-accent-dim); } -.footer-author-divider { - width: 1px; - height: 20px; - background: var(--color-mist); - margin: 0 var(--spacing-xs); -} - -.footer-newsletter { - display: inline-flex; - align-items: center; - gap: 6px; - font-size: 0.875rem; - font-weight: 500; - color: var(--color-text); - text-decoration: none; - padding: 8px 14px; - background: var(--color-paper); - border: 1px solid var(--color-mist); - border-radius: 100px; - transition: all var(--duration-fast) var(--ease-out); -} - -.footer-newsletter:hover { - border-color: var(--color-accent); - color: var(--color-accent); -} - -.footer-newsletter svg { - transition: transform var(--duration-fast) var(--ease-out); -} - -.footer-newsletter:hover svg { - transform: translateX(3px); -} - -@media (max-width: 768px) { - .footer-author { - flex-direction: column; - gap: var(--spacing-sm); +@media (max-width: 900px) { + .footer-row { + grid-template-columns: 1fr; + justify-items: center; + text-align: center; + gap: var(--spacing-md); } - .footer-author-divider { - display: none; + .footer-links { + justify-content: center; + gap: var(--spacing-sm) var(--spacing-md); + font-size: 0.8125rem; + } + + .site-footer { + padding: var(--spacing-lg) var(--spacing-md); } } @@ -1948,143 +2264,374 @@ code { border-top: 1px solid var(--color-mist); } -/* Patterns - tabbed layout */ -.patterns-categories { +/* Antidote row: patterns + gallery side by side */ +.antidote-row { + display: flex; + gap: var(--spacing-xl); + align-items: stretch; margin-bottom: var(--spacing-xl); } -/* Tab bar */ -.pattern-tabs { +.antidote-row .patterns-tabbed { + flex: 1; + min-width: 0; + margin-bottom: 0; +} + +/* Gallery of Shame — 3D card stack */ +.gallery-stack-container { + flex: 0 0 clamp(320px, 34vw, 420px); + display: flex; + flex-direction: column; + gap: var(--spacing-xs); + position: relative; +} + + +/* Header: ← Gallery of Shame → */ +.gallery-stack-header { + display: flex; + align-items: center; + gap: var(--spacing-xs); + white-space: nowrap; +} + +.gallery-stack-title { + font-family: var(--font-display); + font-size: 1rem; + font-weight: 600; + color: var(--color-ink); + flex: 1; +} + +.gallery-stack-btn { + width: 24px; + height: 24px; + border-radius: 50%; + border: 1px solid var(--color-mist); + background: white; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.6875rem; + color: var(--color-charcoal); + flex-shrink: 0; + transition: border-color var(--duration-fast), background var(--duration-fast); + position: relative; +} + +/* Invisible 44px touch target */ +.gallery-stack-btn::after { + content: ''; + position: absolute; + inset: -10px; +} + +.gallery-stack-btn:hover { + border-color: var(--color-charcoal); +} + +/* Card stack — stacked deck offset to bottom-right */ +.gallery-stack { + position: relative; + width: calc(100% - 20px); + aspect-ratio: 3 / 2; + margin-bottom: 20px; +} + +.gallery-stack-card { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + border-radius: 8px; + overflow: hidden; + border: 1px solid var(--color-mist); + text-decoration: none; + background: white; + transition: transform 0.45s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.35s ease, box-shadow 0.35s ease; + will-change: transform, opacity; +} + +.gallery-stack-card img { + flex: 1; + min-height: 0; + width: 100%; + object-fit: cover; + object-position: top left; +} + +.gallery-stack-label { + padding: 6px 10px; + font-size: 0.75rem; + font-weight: 600; + color: var(--color-charcoal); + flex-shrink: 0; +} + +/* Stack depth: cards peek behind at bottom-right */ +.gallery-stack-card[data-offset="0"] { + z-index: 3; + transform: translate(0, 0); + opacity: 1; + box-shadow: 0 2px 12px oklch(0.2 0 0 / 0.1); +} + +.gallery-stack-card[data-offset="1"] { + z-index: 2; + transform: translate(8px, 8px); + opacity: 1; + box-shadow: 0 1px 6px oklch(0.2 0 0 / 0.08); + pointer-events: none; +} + +.gallery-stack-card[data-offset="2"] { + z-index: 1; + transform: translate(16px, 16px); + opacity: 1; + box-shadow: 0 1px 4px oklch(0.2 0 0 / 0.06); + pointer-events: none; +} + +.gallery-stack-card:not([data-offset="0"]):not([data-offset="1"]):not([data-offset="2"]) { + z-index: 0; + transform: translate(20px, 20px); + opacity: 0; + pointer-events: none; +} + +.gallery-stack-link { + font-size: 0.75rem; + font-weight: 500; + color: var(--color-accent); + text-decoration: none; + text-align: center; +} + +.gallery-stack-link:hover { + text-decoration: underline; +} + +@media (max-width: 900px) { + .antidote-row { + flex-direction: column; + } + + .gallery-stack-container { + flex: none; + width: 100%; + max-width: 360px; + } + +} + +/* ---- Pattern tabs ---- */ +.patterns-tabbed { + margin-bottom: var(--spacing-xl); + background: white; + border-radius: 8px; + padding: var(--spacing-lg); +} + +.patterns-tabs { display: flex; flex-wrap: wrap; - gap: var(--spacing-xs); - margin-bottom: var(--spacing-lg); -} - -.pattern-tab { - background: none; - border: none; - border-bottom: 2px solid transparent; - font-family: var(--font-body); - font-size: 0.875rem; - color: var(--color-ash); - padding: var(--spacing-xs) var(--spacing-sm); - cursor: pointer; - transition: color var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out); -} - -.pattern-tab:hover { - color: var(--color-charcoal); -} - -.pattern-tab.active { - color: var(--color-ink); - font-weight: 500; - border-bottom-color: var(--color-accent); -} - -.pattern-tab:focus-visible { - outline: 2px solid var(--color-accent); - outline-offset: 2px; - border-radius: 4px; -} - -/* Panels */ -.pattern-panel { - display: none; -} - -.pattern-panel.active { - display: block; -} - -.pattern-columns { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--spacing-xl); + gap: 6px; + margin-bottom: var(--spacing-md); } @media (max-width: 768px) { - .pattern-columns { - grid-template-columns: 1fr; - gap: var(--spacing-md); + .patterns-tabs { + flex-wrap: nowrap; + overflow-x: auto; + scrollbar-width: none; + -webkit-overflow-scrolling: touch; + margin-left: calc(-1 * var(--spacing-lg)); + margin-right: calc(-1 * var(--spacing-lg)); + padding-left: var(--spacing-lg); + padding-right: var(--spacing-lg); + } + + .patterns-tabs::-webkit-scrollbar { + display: none; } } -.pattern-column-label { - display: block; - font-size: 0.6875rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.1em; - margin-bottom: var(--spacing-sm); - color: var(--color-ash); +.patterns-tab { + font-family: var(--font-body); + font-size: 0.75rem; + font-weight: 500; + color: var(--color-charcoal); + background: none; + border: none; + border-radius: 100px; + padding: 6px 12px; + cursor: pointer; + white-space: nowrap; + transition: background 0.2s ease, color 0.2s ease; } -.pattern-column--anti .pattern-column-label { - color: var(--color-accent); +.patterns-tab:hover { + background: var(--color-mist); } -.pattern-column--do .pattern-column-label { - color: var(--color-success, #22c55e); +.patterns-tab.is-active { + background: var(--color-ink); + color: var(--color-paper); } -/* Pattern list styles */ -.pattern-list { +.patterns-content { + display: none; +} + +.patterns-content.is-active { + display: flex; + flex-direction: column; + gap: var(--spacing-md); +} + +.patterns-col ul { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; - gap: var(--spacing-xs); + gap: 6px; } -.pattern-item { +.patterns-col li { font-size: 0.8125rem; - padding-left: var(--spacing-md); + line-height: 1.55; + padding-left: var(--spacing-sm); position: relative; - line-height: 1.5; + color: var(--color-charcoal); + overflow-wrap: break-word; + word-break: break-word; } -/* Anti-pattern items - muted */ -.pattern-item--anti { - color: var(--color-ash); -} - -.pattern-item--anti::before { - content: '×'; +.patterns-col--dont li::before { + content: '\00d7'; position: absolute; left: 0; color: var(--color-accent); font-weight: 600; } -/* Pattern items - prominent */ -.pattern-item--do { - color: var(--color-charcoal); -} - -.pattern-item--do::before { - content: '✓'; +.patterns-col--do li::before { + content: '\2713'; position: absolute; left: 0; color: var(--color-success, #22c55e); font-weight: 600; } -/* Contribution inline link */ -.contribute-inline { - font-size: 0.875rem; - color: var(--color-ash); - margin-top: var(--spacing-md); + +/* Detection callout — flat editorial CTA, no card chrome. + Image thumbnail on the left, NEW eyebrow + CTA stacked on the right. + The whole element is a clickable anchor. Hover shifts color, no + transform or shadow theatrics. */ +.detection-callout { + display: flex; + align-items: center; + gap: var(--spacing-md); + text-decoration: none; + color: inherit; + padding: var(--spacing-sm) 0; + border-top: 1px solid var(--color-mist); } -.contribute-inline a { +.detection-callout-image { + display: block; + width: 132px; + height: 112px; + flex-shrink: 0; + object-fit: cover; + object-position: top left; + border: 1px solid var(--color-mist); + border-radius: 6px; + transition: border-color 0.2s ease; +} + +.detection-callout:hover .detection-callout-image { + border-color: var(--color-accent); +} + +.detection-callout-inner { + flex: 1; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 6px; +} + +.detection-eyebrow { + font-family: var(--font-mono); + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; color: var(--color-accent); +} + +.detection-callout-title { + font-family: var(--font-display); + font-size: 1.25rem; + font-weight: 600; + color: var(--color-ink); + letter-spacing: -0.01em; + line-height: 1.15; +} + +.detection-callout:hover .detection-callout-title { + color: var(--color-accent); +} + +.detection-desc { + flex: 1; + font-size: 0.8125rem; + line-height: 1.55; + color: var(--color-ash); + min-width: 200px; +} + +.detection-desc code { + font-family: var(--font-mono); + font-size: 0.8125rem; + color: var(--color-charcoal); +} + +.detection-cmd { + flex-shrink: 0; + font-family: var(--font-body); + font-size: 0.875rem; + font-weight: 600; + color: var(--color-ink); + white-space: nowrap; + transition: color 0.15s ease; +} + +.detection-callout:hover .detection-cmd { + color: var(--color-accent); +} + +/* Antidote footer */ +.antidote-footer { + text-align: center; +} + +.antidote-suggest-link { + font-size: 0.8125rem; + color: var(--color-ash); text-decoration: none; } -.contribute-inline a:hover { +.antidote-suggest-link:hover { + color: var(--color-accent); text-decoration: underline; } @@ -2180,123 +2727,667 @@ code { border-top: 1px solid var(--color-mist); } +@media (min-width: 1100px) { + .platforms-section { + border-top: none; + } +} + .platforms-section .section-subtitle { max-width: 60ch; } -/* Install steps layout */ -.install-steps { +/* Two-path install layout */ +/* Row 1: full-width primary install card with internal 2-column split */ +.install-row-primary { + display: grid; + grid-template-columns: 1.1fr 0.9fr; + gap: var(--spacing-xl); + align-items: start; + margin: 0 0 var(--spacing-xl); + background: white; + border-radius: 16px; + padding: var(--spacing-lg); + min-width: 0; +} + +.install-primary-main { display: flex; flex-direction: column; - gap: var(--spacing-xl); - max-width: 640px; - margin: 0 auto var(--spacing-xl); + min-width: 0; } -.install-step-header { +.install-primary-alts { display: flex; - align-items: flex-start; - gap: var(--spacing-md); - margin-bottom: var(--spacing-md); + flex-direction: column; + gap: var(--spacing-lg); + min-width: 0; + padding-left: var(--spacing-xl); + border-left: 1px solid var(--color-mist); } -.install-step-number { +.install-alts-label { + display: block; + font-family: var(--font-mono); + font-size: 0.625rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.14em; + color: var(--color-ash); +} + +.install-alt-sublabel { + font-family: var(--font-body); + font-size: 0.75rem; + font-weight: 400; + color: var(--color-ash); + text-transform: none; + letter-spacing: 0; + margin-left: 4px; +} + +/* Compact zip download links — replaces the old big black download button. + Two equal-weight options stacked vertically, each is its own affordance. */ +.install-zip-links { + display: flex; + flex-direction: column; + gap: 6px; + margin-top: var(--spacing-xs); +} + +.install-zip-link { display: flex; align-items: center; - justify-content: center; - width: 40px; - height: 40px; - flex-shrink: 0; - border-radius: 50%; - border: 1.5px solid var(--color-accent); + gap: var(--spacing-sm); + padding: 8px 12px; + font-family: var(--font-body); + font-size: 0.8125rem; + color: var(--color-charcoal); + background: transparent; + border: 1px solid var(--color-mist); + border-radius: 6px; + cursor: pointer; + text-align: left; + transition: border-color var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out); +} + +.install-zip-link:hover { + border-color: var(--color-accent); color: var(--color-accent); - font-family: var(--font-display); - font-size: 1.125rem; - font-weight: 600; - line-height: 1; } -.install-step-meta { - padding-top: 6px; +.install-zip-link svg { + flex-shrink: 0; + color: var(--color-ash); } -.install-step-title { - font-size: 1.125rem; +.install-zip-link:hover svg { + color: var(--color-accent); +} + +.install-zip-link-name { font-weight: 600; + color: var(--color-ink); + white-space: nowrap; +} + +.install-zip-link:hover .install-zip-link-name { + color: var(--color-accent); +} + +.install-zip-link-examples { + display: flex; + flex-wrap: nowrap; + gap: 4px; + margin-left: auto; +} + +.install-alt-method .install-zip-link-examples code { + font-family: var(--font-mono); + font-size: 0.6875rem; + background: var(--color-accent-dim); + color: var(--color-accent); + padding: 2px 6px; + border-radius: 3px; + white-space: nowrap; +} + +.install-path { + display: flex; + flex-direction: column; + min-width: 0; +} + +.install-path-primary { + background: white; + border-radius: 16px; + padding: var(--spacing-lg); +} + +/* Steps 2-4: vertical accordion. Native
+ gives mutual exclusion (opening one closes the others). Items are + separated by hairlines, not card chrome. */ +.install-accordion { + display: flex; + flex-direction: column; + margin: 0 0 var(--spacing-xl); +} + +.install-step { + border-top: 1px solid var(--color-mist); +} + +.install-step:last-child { + border-bottom: 1px solid var(--color-mist); +} + +.install-step-summary { + display: flex; + align-items: center; + gap: var(--spacing-md); + padding: var(--spacing-md) var(--spacing-xs); + cursor: pointer; + list-style: none; + transition: background var(--duration-fast) var(--ease-out); +} + +.install-step-summary::-webkit-details-marker { + display: none; +} + +.install-step-summary:hover { + background: var(--color-cream); +} + +.install-step-summary h3 { + flex: 1; margin: 0; +} + +.install-step-arrow { + flex-shrink: 0; + color: var(--color-ash); + transition: transform var(--duration-base) var(--ease-out); +} + +.install-step[open] .install-step-arrow { + transform: rotate(90deg); +} + +.install-step-body { + padding: 0 var(--spacing-xs) var(--spacing-md); + display: flex; + flex-direction: column; +} + +.install-step-body .install-path-desc { + margin-top: 0; +} + +.install-step-status { + font-size: 0.8125rem; + color: var(--color-ash); + margin: 0 0 var(--spacing-sm); + font-style: italic; +} + +.install-path-title { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 10px; + font-family: var(--font-display); + font-size: 1.5rem; + font-weight: 600; + margin: 0 0 var(--spacing-sm); + line-height: 1.2; +} + +/* titles unified - no size override for step 3 */ + +.install-path-badge { + font-family: var(--font-mono); + font-size: 0.5rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--color-accent); + border: 1px solid var(--color-accent); + border-radius: 3px; + padding: 2px 6px; + vertical-align: middle; + margin-left: 6px; + position: relative; + top: -2px; +} + +.install-path-badge-muted { + color: var(--color-ash); + border-color: var(--color-mist); +} + +.install-path-desc { + font-size: 0.9375rem; + line-height: 1.6; + color: var(--color-charcoal); + margin: 0 0 var(--spacing-md); +} + +.terminal-header-license { + margin-left: auto; + font-size: 0.6875rem; + color: oklch(65% 0.01 350); + letter-spacing: 0.01em; +} + +.terminal-header-license a { + color: inherit; + text-decoration: underline; + text-decoration-color: oklch(85% 0.005 350); + text-underline-offset: 2px; +} + +.terminal-header-license a:hover { + color: var(--color-charcoal); +} + +/* removed - unified font sizes across columns */ + +.install-path-terminal { + margin-bottom: var(--spacing-sm); +} + +.install-path-terminal .glass-terminal { + height: auto; +} + +.install-path-terminal .terminal-body { + padding: 0; +} + +.install-path-next { + font-size: 0.8125rem; + color: var(--color-charcoal); + line-height: 1.5; + margin-top: auto; + padding-top: var(--spacing-md); +} + +.install-path-next code { + font-family: var(--font-mono); + font-size: 0.8125rem; + font-weight: 600; + color: var(--color-ink); +} + +.install-path-slash { + color: var(--color-accent); +} + +.install-path-subcommands { + display: flex; + flex-direction: column; + gap: 6px; + margin-top: var(--spacing-sm); +} + +.install-path-subcommand { + font-size: 0.75rem; + color: var(--color-charcoal); line-height: 1.4; } -.install-step-desc { - font-size: 0.875rem; - color: var(--color-ash); - margin: 2px 0 0; -} - -/* Subscribe card (step 2) */ -.install-subscribe { - max-width: 640px; - margin: 0 auto; -} - -.install-subscribe iframe { - display: block; - max-width: 100%; -} - -.install-subscribe-btn { - display: inline-flex; - align-items: center; - gap: 10px; - font-size: 0.9375rem; +.install-path-subcommand code { + font-family: var(--font-mono); + font-size: 0.6875rem; font-weight: 600; - letter-spacing: 0.03em; - color: var(--color-paper); - text-decoration: none; - padding: 1rem 2rem; - background: var(--color-ink); - border: 1px solid var(--color-ink); - transition: all var(--duration-base) var(--ease-out); - flex-shrink: 0; - position: relative; - overflow: hidden; + color: var(--color-ink); + background: var(--color-mist); + padding: 2px 6px; + border-radius: 3px; + margin-right: 4px; } -.install-subscribe-btn::before { - content: ''; - position: absolute; - inset: 0; - background: var(--color-accent); - transform: translateY(100%); - transition: transform var(--duration-base) var(--ease-out); - z-index: 0; -} - -.install-subscribe-btn:hover::before { - transform: translateY(0); -} - -.install-subscribe-btn svg, -.install-subscribe-btn span { - position: relative; - z-index: 1; -} - -.install-subscribe-btn:hover { - color: var(--color-paper); -} - -.install-subscribe-note { +.install-path-link { + margin-top: auto; + padding-top: var(--spacing-md); font-size: 0.8125rem; +} + +.install-path-link a { + color: var(--color-accent); + text-decoration: none; +} + +.install-path-link a:hover { + text-decoration: underline; +} + +/* Lightweight terminal block — used in row 2 (CLI) where the heavy + skeuomorphic glass-terminal would be visually too much. Just a thin + bordered code line with a copy button and an optional note below. */ +.install-cmd-block { + display: flex; + flex-direction: column; + gap: var(--spacing-xs); + margin-bottom: var(--spacing-sm); +} + +.install-cmd-line { + display: flex; + align-items: center; + gap: var(--spacing-sm); + padding: 10px 14px; + background: var(--color-cream); + border: 1px solid var(--color-mist); + border-radius: 6px; + min-width: 0; +} + +.install-cmd-line .terminal-prompt { + flex-shrink: 0; + color: var(--color-accent); + font-family: var(--font-mono); + font-size: 0.8125rem; +} + +.install-cmd-line code { + flex: 1; + font-family: var(--font-mono); + font-size: 0.8125rem; + color: var(--color-ink); + background: transparent; + padding: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.install-cmd-line .copy-btn { + flex-shrink: 0; +} + +.install-cmd-note { + font-size: 0.75rem; color: var(--color-ash); - margin: 0; line-height: 1.5; } -@media (max-width: 600px) { - .install-subscribe { +.install-cmd-note code { + font-family: var(--font-mono); + font-size: 0.6875rem; + background: var(--color-mist); + padding: 2px 5px; + border-radius: 3px; + color: var(--color-ink); +} + + +/* Better together callout */ +.install-together { + max-width: 960px; + margin: 0 0 var(--spacing-xl); + border: 1px solid var(--color-mist); + border-radius: 8px; + padding: var(--spacing-sm) var(--spacing-md); +} + +.install-together-inner { + display: flex; + align-items: center; + gap: var(--spacing-md); +} + +.install-together-badge { + font-family: var(--font-mono); + font-size: 0.625rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--color-accent); + border: 1px solid var(--color-accent); + padding: 2px 10px; + border-radius: 99px; + flex-shrink: 0; + white-space: nowrap; +} + +.install-together-text { + font-size: 0.8125rem; + line-height: 1.55; + color: var(--color-ash); + margin: 0; +} + +/* Inline step numbers — sit to the left of the title text inside each h3. + The h3 uses flex with align-items: center so the step is centered with + the text without needing vertical-align fiddling. Margin/gap is handled + by the parent flex's gap. */ +.install-path-step { + display: inline-flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + border-radius: 50%; + border: 1.5px solid var(--color-accent); + color: var(--color-accent); + font-family: var(--font-mono); + font-size: 0.625rem; + font-weight: 700; + line-height: 1; + flex-shrink: 0; +} + +/* Stay updated (third column) */ +.install-updated-subscribe { + display: grid; + grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); + gap: var(--spacing-sm); + margin-bottom: var(--spacing-md); + align-items: stretch; +} + +.install-updated-substack { + display: block; + width: 100%; + min-width: 0; + height: 130px; + border: 1px solid var(--color-mist); + border-radius: 8px; + background: var(--color-paper); +} + +.install-updated-x { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: space-between; + gap: var(--spacing-xs); + height: 130px; + padding: var(--spacing-sm) var(--spacing-md); + border: 1px solid var(--color-mist); + border-radius: 8px; + background: var(--color-paper); + color: var(--color-ink); + text-decoration: none; + transition: border-color var(--duration-fast) var(--ease-out), + background var(--duration-fast) var(--ease-out), + transform var(--duration-fast) var(--ease-out); +} + +.install-updated-x:hover { + border-color: var(--color-ink); + background: var(--color-cream); +} + +.install-updated-x-icon { + color: var(--color-ink); + flex-shrink: 0; +} + +.install-updated-x-label { + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--color-ash); + font-family: var(--font-mono); +} + +.install-updated-x-handle { + font-size: 1rem; + font-weight: 600; + color: var(--color-ink); + letter-spacing: -0.01em; +} + +.install-updated-x:hover .install-updated-x-handle { + color: var(--color-accent); +} + +.install-updated-refs { + margin-top: auto; + padding-top: var(--spacing-md); + border-top: 1px solid var(--color-mist); +} + +.install-updated-refs-label { + display: block; + font-family: var(--font-mono); + font-size: 0.5625rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--color-ash); + margin-bottom: 8px; +} + +.install-updated-ref { + display: block; + font-size: 0.8125rem; + color: var(--color-charcoal); + text-decoration: none; + margin-bottom: 6px; + transition: color var(--duration-fast) var(--ease-out); +} + +a.install-updated-ref:hover { + color: var(--color-accent); +} + +.install-updated-ref-terminal { + display: flex; + align-items: center; + gap: var(--spacing-xs); + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--color-charcoal); + background: var(--color-cream); + border: 1px solid var(--color-mist); + border-radius: 6px; + padding: 6px 10px; + margin-top: 8px; +} + +.install-updated-ref-terminal .terminal-prompt { + color: var(--color-ash); + font-size: 0.75rem; +} + +.install-updated-ref-terminal code { + font-family: inherit; + font-size: inherit; + background: none; + padding: 0; + color: var(--color-ink); +} + +@media (max-width: 900px) { + .install-row-primary { + grid-template-columns: 1fr; + gap: var(--spacing-lg); + max-width: calc(100vw - var(--spacing-md) * 2); + } + + .install-primary-alts { + padding-left: 0; + padding-top: var(--spacing-lg); + border-left: none; + border-top: 1px solid var(--color-mist); + } + + .install-path-primary { + margin-bottom: var(--spacing-sm); + padding: var(--spacing-md); + } + + .install-path-title { + font-size: 1.25rem; + } + + .install-path-desc { + font-size: 0.875rem; + overflow-wrap: break-word; + } + + .install-together-inner { flex-direction: column; align-items: flex-start; + gap: var(--spacing-xs); + } + + .install-updated-subscribe { + grid-template-columns: 1fr; + } + + .install-updated-substack { + height: 130px; + } + + .install-updated-x { + height: auto; + flex-direction: row; + align-items: center; + justify-content: flex-start; + gap: var(--spacing-sm); + padding: var(--spacing-sm) var(--spacing-md); + } + + .install-updated-x-label { + display: none; + } + + .install-terminal-cmd code { + font-size: 0.75rem; + white-space: normal; + overflow-wrap: break-word; + } + + .install-alt-method { + padding: var(--spacing-sm); + } + + .install-alt-method .install-terminal-cmd code { + font-size: 0.6875rem; + white-space: normal; + overflow-wrap: break-word; + } + + .install-alt-method .install-terminal-cmd .copy-btn { + display: none; + } + + .install-path-terminal .glass-terminal { + box-shadow: none; + } + + .install-terminal { + max-width: 100%; + } + + .install-alternatives { + max-width: 100%; } } @@ -2476,10 +3567,6 @@ code { } .install-alternatives-content { - border: 1px solid var(--color-mist); - border-radius: 8px; - background: var(--color-paper); - overflow: hidden; animation: altFadeIn 0.2s var(--ease-out); } @@ -2538,142 +3625,7 @@ code { color: var(--color-ink); } -.install-alt-download { - display: flex; - align-items: center; - gap: var(--spacing-sm); -} - -.install-alt-divider { - height: 1px; - background: var(--color-mist); -} - -.install-prefix-options { - display: flex; - align-items: flex-start; - gap: var(--spacing-sm); - margin-top: var(--spacing-md); -} - -.install-prefix-option { - flex: 1; - display: flex; - align-items: flex-start; - gap: 8px; - padding: 10px 12px; - border: 1.5px solid var(--color-mist); - border-radius: 8px; - cursor: pointer; - transition: all var(--duration-fast) var(--ease-out); - line-height: 1; -} - -.install-prefix-option:hover { - border-color: var(--color-charcoal); -} - -.install-prefix-option:has(input:checked) { - border-color: var(--color-accent); -} - -.install-prefix-option input { - appearance: none; - -webkit-appearance: none; - width: 16px; - height: 16px; - border: 1.5px solid var(--color-mist); - border-radius: 50%; - flex-shrink: 0; - margin: 0; - transition: all var(--duration-fast) var(--ease-out); - position: relative; -} - -.install-prefix-option input:checked { - border-color: var(--color-accent); - background: var(--color-accent); -} - -.install-prefix-option input:checked::after { - content: ''; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 6px; - height: 6px; - background: var(--color-paper); - border-radius: 50%; -} - -.install-prefix-option input:focus-visible { - outline: 2px solid var(--color-accent); - outline-offset: 2px; -} - -.install-prefix-option-content { - display: flex; - flex-direction: column; - gap: 3px; - padding-top: 1px; -} - -.install-prefix-option-label { - font-size: 0.8125rem; - font-weight: 600; - color: var(--color-ink); - line-height: 1; -} - -.install-prefix-option-examples { - display: flex; - flex-wrap: wrap; - gap: 4px; -} - -.install-prefix-option-examples code { - font-family: var(--font-mono); - font-size: 0.6875rem; - background: var(--color-accent-dim); - color: var(--color-accent); - padding: 2px 6px; - border-radius: 3px; -} - -.install-prefix-option-wrap { - flex: 1; - display: flex; - flex-direction: column; - gap: 4px; - align-self: flex-start; -} - -.install-prefix-reason { - font-size: 0.6875rem; - color: var(--color-ash); - line-height: 1.4; - padding-left: 2px; -} - -.install-prefix-option-label code { - font-family: var(--font-mono); - font-size: 0.6875rem; - background: var(--color-accent-dim); - color: var(--color-accent); - padding: 1px 5px; - border-radius: 3px; -} - -.install-alt-method .install-prefix-options { - margin-top: var(--spacing-sm); -} - -.install-alt-method .install-alt-download { - margin-top: var(--spacing-sm); -} - -/* Hidden prefix checkbox for download logic */ +/* Visually hidden helper (kept for accessibility patterns elsewhere) */ .sr-only { position: absolute; width: 1px; @@ -2687,14 +3639,6 @@ code { } @media (max-width: 600px) { - .install-prefix-options { - flex-direction: column; - } - - .install-alt-method { - padding: var(--spacing-sm) var(--spacing-md); - } - .install-alt-method code { font-size: 0.75rem; } diff --git a/public/css/skill-demos.css b/public/css/skill-demos.css index df334c6ba..59b9861e6 100644 --- a/public/css/skill-demos.css +++ b/public/css/skill-demos.css @@ -180,7 +180,6 @@ font-size: 0.6875rem; letter-spacing: 0.03em; color: var(--color-ash); - background: var(--color-paper); text-align: center; } diff --git a/public/css/styles.css b/public/css/styles.css deleted file mode 100644 index 910b3cec8..000000000 --- a/public/css/styles.css +++ /dev/null @@ -1,4347 +0,0 @@ -/*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */ -.split-container::before { - content: ''; - position: absolute; - inset: 0; - background-image: linear-gradient(var(--color-mist) 1px, transparent 1px), linear-gradient(90deg, var(--color-mist) 1px, transparent 1px); - background-size: 20px 20px; - opacity: 0.3; - pointer-events: none; -} -.split-container::after { - content: '← Drag →'; - position: absolute; - bottom: 12px; - left: 50%; - transform: translateX(-50%); - font-size: 0.625rem; - font-weight: 600; - letter-spacing: 0.1em; - text-transform: uppercase; - color: var(--color-ash); - background: var(--color-paper); - padding: 4px 12px; - border-radius: 4px; - opacity: 0.8; - transition: opacity 0.3s ease; - z-index: 10; -} -.split-container:hover::after { - opacity: 0; -} -.split-after .impeccable-card { - box-shadow: 0 10px 40px rgba(0,0,0,0.08); -} -@keyframes splitEntry { - from { - opacity: 0; - transform: translateX(-50%) skewX(-10deg) scaleY(0.8); - } - to { - opacity: 1; - transform: translateX(-50%) skewX(-10deg) scaleY(1); - } -} -.split-divider { - animation: splitEntry 0.6s var(--ease-out) 0.3s backwards; -} -.split-label-item { - transition: color var(--duration-fast) var(--ease-out); - cursor: default; -} -.split-label-item:hover { - color: var(--color-text); -} -.split-label-item[data-point="after"]:hover .split-label-dot--accent { - transform: scale(1.3); -} -.split-label-dot { - transition: transform var(--duration-fast) var(--ease-spring); -} -.split-badge { - position: absolute; - top: 10px; - font-size: 0.625rem; - font-weight: 600; - letter-spacing: 0.08em; - text-transform: uppercase; - padding: 3px 8px; - border-radius: 3px; - z-index: 5; - pointer-events: none; -} -.split-badge--before { - left: 10px; - color: var(--color-ash); - background: var(--color-paper); - border: 1px solid var(--color-mist); -} -.split-badge--after { - right: 10px; - color: var(--color-paper); - background: var(--color-accent); -} -@media (hover: none) { - .split-container::after { - content: '← Swipe →'; - } -} -@media (max-width: 600px) { - .split-label { - font-size: 0.5625rem; - padding: 4px 10px; - } -} -.commands-section { - position: relative; - padding: var(--spacing-xl) 0; - background: var(--color-paper); -} -.commands-gallery { - display: block; -} -.commands-container { - display: grid; - grid-template-columns: 1fr 1.2fr; - gap: var(--spacing-2xl); - align-items: start; -} -@media (max-width: 900px) { - .commands-container { - grid-template-columns: 1fr; - } -} -.command-manual { - display: flex; - flex-direction: column; - gap: var(--spacing-sm); - padding-bottom: 20vh; -} -.command-category-header { - font-family: var(--font-display); - font-size: 0.875rem; - font-weight: 600; - color: var(--color-accent); - text-transform: uppercase; - letter-spacing: 0.1em; - padding: var(--spacing-lg) var(--spacing-lg) var(--spacing-sm); - margin-top: var(--spacing-md); - border-bottom: 1px solid var(--color-mist); -} -.command-category-header:first-child { - margin-top: 0; -} -.manual-entry { - position: relative; - padding: var(--spacing-lg); - padding-left: calc(var(--spacing-lg) + 16px); - border-left: 2px solid var(--color-mist); - transition: border-color 0.4s var(--ease-out), opacity 0.4s var(--ease-out), background 0.4s var(--ease-out), transform 0.4s var(--ease-out); - opacity: 0.4; - cursor: pointer; - transform: translateX(-16px); -} -.manual-entry:hover { - opacity: 0.7; -} -.manual-entry.active { - border-left-color: var(--color-accent); - opacity: 1; - transform: translateX(0); - background: linear-gradient(to right, var(--color-bg), transparent); -} -.manual-cmd-name { - font-family: var(--font-mono); - font-size: 1.5rem; - margin: 0 0 var(--spacing-sm); - color: var(--color-ink); - font-weight: 500; -} -.beta-badge { - font-family: var(--font-body); - font-size: 0.55rem; - font-weight: 600; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--color-accent); - border: 1px solid var(--color-accent); - border-radius: 3px; - padding: 1px 5px; - vertical-align: middle; - margin-left: 6px; -} -.manual-cmd-desc { - font-size: 0.875rem; - line-height: 1.5; - color: var(--color-charcoal); - margin: 0; -} -.manual-cmd-rel { - font-size: 0.8125rem; - color: var(--color-ash); - margin-top: var(--spacing-sm); - display: flex; - align-items: center; - gap: 0.5ch; - flex-wrap: wrap; -} -.manual-cmd-rel .rel-icon { - color: var(--color-accent); - font-weight: 600; -} -.manual-cmd-rel code { - font-family: var(--font-mono); - font-size: 0.75rem; - background: var(--color-mist); - padding: 2px 6px; - border-radius: 3px; - color: var(--color-ink); -} -.glass-terminal-wrapper { - position: sticky; - top: 50vh; - transform: translateY(-50%); - height: calc(100vh - var(--spacing-xl) * 2); - max-height: 800px; - min-height: 500px; -} -.terminal-stack { - position: relative; - height: 100%; - perspective: 1200px; -} -.terminal-stack-tabs { - position: absolute; - top: -31px; - right: 8px; - display: flex; - gap: 4px; - z-index: 10; -} -.terminal-stack-tab { - font-family: var(--font-mono); - font-size: 0.75rem; - padding: 5px 12px; - background: var(--color-cream); - border: 1px solid var(--color-mist); - border-bottom: none; - border-radius: 6px 6px 0 0; - color: var(--color-ash); - cursor: pointer; - transition: all 0.2s ease; -} -.terminal-stack-tab:hover { - background: var(--color-paper); - color: var(--color-charcoal); -} -.terminal-stack-tab.active { - background: var(--color-paper); - color: var(--color-ink); - border-color: var(--color-mist); -} -.terminal-window { - position: absolute; - inset: 0; - transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease, filter 0.3s ease; - transform-origin: center bottom; -} -.terminal-window--demo { - z-index: 2; -} -.terminal-window--demo.is-back { - transform: translateY(16px) translateX(12px) scale(0.96); - opacity: 0.6; - filter: brightness(0.92); - pointer-events: none; - z-index: 1; -} -.terminal-window--source { - z-index: 1; - transform: translateY(16px) translateX(12px) scale(0.96); - opacity: 0.6; - filter: brightness(0.92); - pointer-events: none; -} -.terminal-window--source.is-front { - transform: translateY(0) translateX(0) scale(1); - opacity: 1; - filter: brightness(1); - pointer-events: auto; - z-index: 2; -} -.source-window { - background: var(--color-paper); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid var(--color-mist); - border-radius: 8px; - box-shadow: 0 20px 60px -10px rgba(0,0,0,0.15); - height: 100%; - display: flex; - flex-direction: column; - overflow: hidden; -} -.source-header { - background: var(--color-cream); - padding: 12px 16px; - display: flex; - align-items: center; - gap: 8px; - border-bottom: 1px solid var(--color-mist); - flex-shrink: 0; -} -.source-title { - font-family: var(--font-mono); - font-size: 0.875rem; - color: var(--color-ink); - font-weight: 500; -} -.source-body { - flex: 1; - padding: var(--spacing-md); - font-family: var(--font-mono); - font-size: 0.75rem; - line-height: 1.5; - color: var(--color-charcoal); - overflow-y: auto; - overscroll-behavior: contain; - white-space: pre-wrap; - word-break: break-word; - background: var(--color-cream); -} -.source-loading { - color: var(--color-ash); - font-style: italic; -} -@media (max-width: 900px) { - .glass-terminal-wrapper { - display: none; - } -} -.glass-terminal { - background: var(--color-paper); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid var(--color-mist); - border-radius: 8px; - box-shadow: 0 20px 60px -10px rgba(0,0,0,0.15); - height: 100%; - display: flex; - flex-direction: column; - overflow: hidden; -} -.terminal-header { - background: var(--color-cream); - padding: 12px 16px; - display: flex; - align-items: center; - gap: 8px; - border-bottom: 1px solid var(--color-mist); -} -.terminal-dot { - width: 10px; - height: 10px; - border-radius: 50%; -} -.terminal-dot.red { - background: #ff5f56; -} -.terminal-dot.yellow { - background: #ffbd2e; -} -.terminal-dot.green { - background: #27c93f; -} -.terminal-title { - margin-left: auto; - font-family: var(--font-mono); - font-size: 0.75rem; - color: var(--color-ash); -} -.terminal-body { - flex: 1; - padding: var(--spacing-md); - font-family: var(--font-mono); - font-size: 0.9375rem; - color: var(--color-ink); - overflow-y: auto; - display: flex; - flex-direction: column; - min-height: 0; -} -.terminal-line { - margin-bottom: var(--spacing-sm); - display: flex; - gap: var(--spacing-sm); - line-height: 1.5; -} -.terminal-prompt { - color: var(--color-accent); - user-select: none; - font-weight: bold; -} -.terminal-cursor { - display: inline-block; - width: 8px; - height: 1.2em; - background: var(--color-accent); - vertical-align: middle; - animation: blink 1s step-end infinite; -} -.terminal-output { - color: var(--color-ash); - margin-bottom: var(--spacing-md); - white-space: pre-wrap; -} -@media (max-height: 800px) { - .terminal-output { - display: none; - } -} -.terminal-cmd { - color: var(--color-accent); - font-weight: 600; -} -.terminal-step { - color: var(--color-charcoal); -} -.terminal-done { - color: var(--color-success, #22c55e); - font-weight: 500; -} -.terminal-preview { - background: var(--color-paper); - margin: var(--spacing-sm) 0; - flex: 1; - min-height: 0; - overflow: hidden; - border-radius: 12px; -} -.terminal-cursor-line { - flex-shrink: 0; - margin-top: var(--spacing-sm) !important; -} -.terminal-preview .demo-split-comparison { - display: flex; - flex-direction: column; - height: 100%; -} -.terminal-preview .demo-split-comparison .split-container { - position: relative; - flex: 1; - min-height: 0; - overflow: hidden; - cursor: ew-resize; - user-select: none; - background: var(--color-cream); -} -.terminal-preview .demo-split-comparison .split-before, .terminal-preview .demo-split-comparison .split-after { - position: absolute; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - padding: var(--spacing-md); -} -.terminal-preview .demo-split-comparison .split-before { - z-index: 1; - background: var(--color-cream); -} -.terminal-preview .demo-split-comparison .split-after { - z-index: 2; - background: var(--color-paper); - clip-path: polygon(58% 0%, 100% 0%, 100% 100%, 42% 100%); -} -.terminal-preview .demo-split-comparison .split-content { - width: 100%; - max-width: 280px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} -.terminal-preview .demo-split-comparison .split-divider { - position: absolute; - top: 0; - bottom: 0; - left: 50%; - width: 2px; - background: var(--color-accent); - transform: translateX(-50%) skewX(-10deg); - pointer-events: none; - z-index: 3; - box-shadow: 0 0 12px rgba(0,0,0,0.1); -} -.terminal-preview .demo-split-comparison .split-label { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%) skewX(10deg); - font-size: 0.5625rem; - font-weight: 600; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--color-paper); - background: var(--color-accent); - padding: 4px 10px; - border-radius: 3px; - white-space: nowrap; -} -.terminal-preview .demo-split-comparison .demo-caption { - flex-shrink: 0; - font-size: 0.75rem; - color: var(--color-ash); - text-align: center; - padding: var(--spacing-sm) var(--spacing-md); -} -@keyframes blink { - 50% { - opacity: 0; - } -} -.casestudies-section { - position: relative; - padding: var(--spacing-2xl) 0; - border-top: 1px solid var(--color-mist); -} -.transformations-tabbed { - margin-top: var(--spacing-xl); -} -.transformation-tabs { - display: flex; - gap: var(--spacing-xs); - border-bottom: 1px solid var(--color-mist); - margin-bottom: var(--spacing-lg); -} -.transformation-tab { - font-family: var(--font-display); - font-size: 0.9375rem; - font-weight: 500; - color: var(--color-ash); - background: none; - border: none; - padding: var(--spacing-sm) var(--spacing-md); - cursor: pointer; - position: relative; - transition: color 0.2s ease; -} -.transformation-tab:hover { - color: var(--color-charcoal); -} -.transformation-tab.active { - color: var(--color-ink); -} -.transformation-tab.active::after { - content: ''; - position: absolute; - bottom: -1px; - left: 0; - right: 0; - height: 2px; - background: var(--color-accent); -} -.transformation-panels { - position: relative; -} -.transformation-panel { - display: none; - flex-direction: column; - gap: var(--spacing-lg); - animation: fadeInPanel 0.3s ease; -} -.transformation-panel.active { - display: flex; -} -@keyframes fadeInPanel { - from { - opacity: 0; - transform: translateY(8px); - } - to { - opacity: 1; - transform: translateY(0); - } -} -.transformation-images { - display: flex; - align-items: center; - gap: var(--spacing-md); -} -.transformation-before, .transformation-after { - flex: 1; - margin: 0; -} -.transformation-before img, .transformation-after img, .transformation-placeholder { - width: 100%; - aspect-ratio: 16 / 10; - object-fit: cover; - border-radius: 8px; - border: 1px solid var(--color-mist); - cursor: pointer; - transition: transform 0.2s ease, box-shadow 0.2s ease; -} -.transformation-before img:hover, .transformation-after img:hover, .transformation-placeholder:hover { - transform: scale(1.02); - box-shadow: 0 8px 24px -4px rgba(0,0,0,0.15); -} -.transformation-placeholder { - background: linear-gradient(135deg, var(--color-mist) 0%, var(--color-cream) 100%); - display: flex; - align-items: center; - justify-content: center; - color: var(--color-ash); - font-size: 0.8125rem; - font-style: italic; -} -.transformation-before figcaption, .transformation-after figcaption { - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--color-ash); - margin-top: var(--spacing-xs); - text-align: center; -} -.transformation-arrow { - font-size: 1.5rem; - color: var(--color-accent); - font-weight: 300; - flex-shrink: 0; -} -.transformation-info { - max-width: 600px; -} -.transformation-title { - font-family: var(--font-display); - font-size: 1.25rem; - font-weight: 600; - color: var(--color-ink); - margin: 0 0 var(--spacing-xs); -} -.transformation-desc { - font-size: 0.9375rem; - color: var(--color-charcoal); - line-height: 1.6; - margin: 0 0 var(--spacing-sm); -} -.transformation-commands { - display: flex; - flex-wrap: wrap; - gap: 6px; -} -.transformation-command { - font-family: var(--font-mono); - font-size: 0.75rem; - background: var(--color-mist); - color: var(--color-charcoal); - padding: 4px 10px; - border-radius: 4px; -} -.lightbox { - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.9); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - opacity: 0; - visibility: hidden; - transition: opacity 0.3s ease, visibility 0.3s ease; -} -.lightbox.active { - opacity: 1; - visibility: visible; -} -.lightbox-close { - position: absolute; - top: 20px; - right: 24px; - background: none; - border: none; - color: white; - font-size: 2.5rem; - cursor: pointer; - opacity: 0.7; - transition: opacity 0.2s ease; - line-height: 1; -} -.lightbox-close:hover { - opacity: 1; -} -.lightbox-image { - max-width: 90vw; - max-height: 85vh; - object-fit: contain; - border-radius: 8px; - box-shadow: 0 20px 60px rgba(0,0,0,0.5); -} -@media (max-width: 768px) { - .transformation-images { - flex-direction: column; - } - .transformation-arrow { - transform: rotate(90deg); - } - .transformation-before, .transformation-after { - width: 100%; - } -} -.hero-version-link { - font-size: 0.8125rem; - color: var(--color-ash); - margin-top: var(--spacing-sm); -} -.hero-version-link a { - color: var(--color-ash); - text-decoration: none; - border-bottom: 1px solid transparent; - transition: color 0.2s ease, border-color 0.2s ease; -} -.hero-version-link a:hover { - color: var(--color-accent); - border-bottom-color: var(--color-accent); -} -.changelog-section { - position: relative; - padding: var(--spacing-xl) 0; - border-top: 1px solid var(--color-mist); -} -.changelog-list { - display: flex; - flex-direction: column; - gap: 0; -} -.changelog-entry { - padding: var(--spacing-md) 0; -} -.changelog-entry + .changelog-entry { - border-top: 1px solid var(--color-mist); -} -.changelog-version-header { - display: flex; - align-items: baseline; - gap: var(--spacing-sm); - margin-bottom: var(--spacing-sm); -} -.changelog-version { - font-family: var(--font-mono); - font-size: 1.125rem; - font-weight: 600; - color: var(--color-ink); -} -.changelog-date { - font-size: 0.8125rem; - color: var(--color-ash); -} -.changelog-items { - margin: 0; - padding-left: var(--spacing-md); - color: var(--color-charcoal); - line-height: 1.7; -} -.changelog-items li { - margin-bottom: var(--spacing-xs); -} -.changelog-items code { - font-family: var(--font-mono); - font-size: 0.875em; - background: var(--color-mist); - padding: 2px 6px; - border-radius: 3px; - color: var(--color-ink); -} -.changelog-older { - margin-top: var(--spacing-sm); -} -.changelog-older-toggle { - font-family: var(--font-body); - font-size: 0.875rem; - font-weight: 500; - color: var(--color-accent); - padding: var(--spacing-md) 0; - cursor: pointer; - list-style: none; - display: flex; - align-items: center; - gap: 6px; - transition: color 0.2s ease; -} -.changelog-older-toggle::-webkit-details-marker { - display: none; -} -.changelog-older-toggle::before { - content: '+'; - font-size: 1.25rem; - font-weight: 300; - transition: transform 0.3s var(--ease-out); -} -.changelog-older[open] .changelog-older-toggle::before { - transform: rotate(45deg); -} -.changelog-older-toggle:hover { - color: var(--color-accent-hover); -} -.changelog-older-entries { - animation: faqFadeIn 0.3s var(--ease-out); -} -.faq-section { - position: relative; - padding: var(--spacing-xl) 0; - border-top: 1px solid var(--color-mist); -} -.faq-list { - display: flex; - flex-direction: column; - gap: 0; -} -.faq-item { - border-bottom: 1px solid var(--color-mist); -} -.faq-question { - font-family: var(--font-display); - font-size: 1.125rem; - font-weight: 500; - color: var(--color-ink); - padding: var(--spacing-md) 0; - cursor: pointer; - list-style: none; - display: flex; - align-items: center; - justify-content: space-between; - transition: color 0.2s ease; -} -.faq-question::-webkit-details-marker { - display: none; -} -.faq-question::after { - content: '+'; - font-family: var(--font-body); - font-size: 1.5rem; - font-weight: 300; - color: var(--color-accent); - transition: transform 0.3s var(--ease-out); -} -.faq-item[open] .faq-question::after { - transform: rotate(45deg); -} -.faq-question:hover { - color: var(--color-accent); -} -.faq-answer { - padding: 0 0 var(--spacing-md); - color: var(--color-charcoal); - line-height: 1.7; - animation: faqFadeIn 0.3s var(--ease-out); -} -.faq-answer p { - margin: 0 0 var(--spacing-sm); -} -.faq-answer p:last-child { - margin-bottom: 0; -} -.faq-answer ul { - margin: var(--spacing-sm) 0; - padding-left: var(--spacing-md); -} -.faq-answer li { - margin-bottom: var(--spacing-xs); -} -.faq-answer code { - font-family: var(--font-mono); - font-size: 0.875em; - background: var(--color-mist); - padding: 2px 6px; - border-radius: 3px; - color: var(--color-ink); -} -.faq-answer a { - color: var(--color-accent); - text-decoration: none; - border-bottom: 1px solid transparent; - transition: border-color 0.2s ease; -} -.faq-answer a:hover { - border-bottom-color: var(--color-accent); -} -@keyframes faqFadeIn { - from { - opacity: 0; - transform: translateY(-8px); - } - to { - opacity: 1; - transform: translateY(0); - } -} -.skills-section { - position: relative; - padding: var(--spacing-xl) 0; - overflow: hidden; - background: var(--color-bg); -} -.skills-gallery { - display: block; - position: relative; -} -.gallery-track { - display: flex; - gap: var(--spacing-lg); - overflow-x: auto; - scroll-snap-type: x mandatory; - padding: var(--spacing-md) var(--spacing-lg) var(--spacing-xl); - -webkit-overflow-scrolling: touch; - scrollbar-width: none; - cursor: grab; -} -.gallery-track:active { - cursor: grabbing; -} -.gallery-track::-webkit-scrollbar { - display: none; -} -.gallery-frame { - flex: 0 0 80vw; - max-width: 1100px; - scroll-snap-align: center; - position: relative; - background: var(--color-paper); - border: 1px solid var(--color-mist); - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 20px 50px -10px rgba(0, 0, 0, 0.1); - border-radius: 2px; - overflow: hidden; - opacity: 0.4; - transform: scale(0.95); - transition: opacity 0.6s var(--ease-out), transform 0.6s var(--ease-out), box-shadow 0.6s var(--ease-out); -} -.gallery-frame.active { - opacity: 1; - transform: scale(1); - box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 40px 100px -20px rgba(0, 0, 0, 0.2); - border-color: var(--color-charcoal); - border-width: 1px; -} -.gallery-content { - display: grid; - grid-template-columns: 1.2fr 1fr; - height: 600px; -} -@media (max-width: 900px) { - .gallery-frame { - flex: 0 0 90vw; - } - .gallery-content { - grid-template-columns: 1fr; - height: auto; - min-height: 600px; - } -} -.gallery-visual { - background: var(--color-cream); - border-right: 1px solid var(--color-mist); - position: relative; - overflow: hidden; - display: flex; - align-items: center; - justify-content: center; - padding: var(--spacing-lg); -} -.gallery-info { - padding: var(--spacing-xl); - display: flex; - flex-direction: column; - overflow-y: auto; -} -.gallery-header { - margin-bottom: var(--spacing-lg); -} -.gallery-title { - font-family: var(--font-display); - font-size: 2.5rem; - font-style: italic; - margin: 0 0 var(--spacing-xs); - color: var(--color-ink); -} -.gallery-meta { - font-family: var(--font-mono); - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 0.1em; - color: var(--color-ash); -} -.gallery-desc { - font-size: 1.125rem; - line-height: 1.6; - color: var(--color-charcoal); - margin-bottom: var(--spacing-xl); - max-width: 45ch; -} -.gallery-tags { - display: flex; - flex-wrap: wrap; - gap: var(--spacing-xs); - margin-top: auto; -} -.gallery-tag { - padding: 6px 12px; - border: 1px solid var(--color-mist); - border-radius: 4px; - font-size: 0.8125rem; - color: var(--color-ash); -} -.gallery-map { - display: flex; - justify-content: center; - gap: 8px; - margin-top: var(--spacing-lg); -} -.gallery-dot { - width: 40px; - height: 2px; - background: var(--color-mist); - cursor: pointer; - transition: all 0.3s ease; - position: relative; - border: none; - padding: 0; - font: inherit; -} -.gallery-dot::after { - content: ''; - position: absolute; - top: -10px; - bottom: -10px; - left: 0; - right: 0; -} -.gallery-dot:focus-visible { - outline: 2px solid var(--color-accent); - outline-offset: 4px; - border-radius: 1px; -} -.gallery-dot.active { - background: var(--color-accent); - height: 4px; -} -.demo-tabbed-container { - display: flex; - flex-direction: column; -} -.demo-tabs { - display: flex; - gap: 0; - margin-bottom: 0; - justify-content: center; - background: var(--color-paper); - border-bottom: 1px solid var(--color-mist); -} -.demo-tab { - padding: var(--spacing-sm) var(--spacing-lg); - background: transparent; - border: none; - border-bottom: 2px solid transparent; - font-family: var(--font-mono); - font-size: 0.75rem; - font-weight: 500; - letter-spacing: 0.05em; - text-transform: uppercase; - color: var(--color-ash); - cursor: pointer; - transition: all var(--duration-fast) var(--ease-out); -} -.demo-tab:hover { - color: var(--color-text); - background: var(--color-cream); -} -.demo-tab.active { - color: var(--color-accent); - border-bottom-color: var(--color-accent); - background: var(--color-accent-dim); -} -.demo-panels { - flex: 1; -} -.demo-panel { - display: none; -} -.demo-panel.active { - display: block; - animation: fadeSlideIn 0.3s var(--ease-out); -} -@keyframes fadeSlideIn { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} -.demo-container { - background: var(--color-paper); - border: none; - border-radius: 0; - overflow: hidden; -} -.demo-header { - display: flex; - align-items: center; - justify-content: center; - padding: var(--spacing-sm) var(--spacing-md); - background: var(--color-paper); - border-bottom: 1px solid var(--color-mist); - min-height: 48px; -} -.demo-toggle { - display: flex; - align-items: center; - gap: var(--spacing-md); -} -.demo-toggle-label { - font-family: var(--font-mono); - font-size: 0.6875rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--color-ash); - transition: color var(--duration-fast) var(--ease-out); - cursor: pointer; -} -.demo-toggle-label:hover { - color: var(--color-text); -} -.demo-toggle-label.active { - color: var(--color-accent); -} -.demo-toggle-switch { - position: relative; - width: 44px; - height: 24px; - background: var(--color-mist); - border-radius: 12px; - cursor: pointer; - transition: background var(--duration-fast) var(--ease-out); - border: 1px solid transparent; - padding: 0; - font: inherit; -} -.demo-toggle-switch:focus-visible { - outline: 2px solid var(--color-accent); - outline-offset: 2px; -} -.demo-toggle-switch:hover { - border-color: var(--color-ash); -} -.demo-toggle-switch::after { - content: ''; - position: absolute; - top: 3px; - left: 3px; - width: 16px; - height: 16px; - background: var(--color-paper); - border-radius: 50%; - box-shadow: 0 1px 4px rgba(0,0,0,0.15); - transition: transform var(--duration-base) var(--ease-spring); -} -.demo-toggle-switch.active { - background: var(--color-accent); -} -.demo-toggle-switch.active::after { - transform: translateX(20px); -} -.demo-viewport { - padding: var(--spacing-xl); - display: flex; - align-items: center; - justify-content: center; - min-height: 280px; - background: var(--color-cream); - transition: background var(--duration-base) var(--ease-out); -} -.demo-viewport[data-state="after"] { - background: var(--color-paper); -} -.demo-caption { - padding: var(--spacing-sm) var(--spacing-md); - font-family: var(--font-mono); - font-size: 0.6875rem; - letter-spacing: 0.03em; - color: var(--color-ash); - background: var(--color-paper); - text-align: center; -} -.uxw-demo { - width: 100%; - max-width: 320px; - padding: var(--spacing-lg); - background: var(--color-paper); - border: 1px solid var(--color-mist); - border-radius: 6px; - text-align: center; -} -.uxw-error-icon { - font-size: 2rem; - margin-bottom: var(--spacing-sm); -} -.uxw-error-title { - font-weight: 600; - color: #c00; - margin-bottom: var(--spacing-xs); -} -.uxw-error-text { - font-size: 0.875rem; - color: var(--color-ash); -} -.uxw-error-action { - margin-top: var(--spacing-sm); - font-size: 0.875rem; - color: var(--color-accent); - cursor: pointer; - text-decoration: underline; -} -.uxw-error-after .uxw-error-icon { - color: var(--color-accent); -} -.uxw-error-after .uxw-error-title { - color: var(--color-text); -} -.uxw-error-after .uxw-error-text { - color: var(--color-charcoal); -} -.uxw-button-context { - font-size: 0.875rem; - color: var(--color-charcoal); - margin-bottom: var(--spacing-md); - font-weight: 500; -} -.uxw-button-row { - display: flex; - gap: var(--spacing-sm); - justify-content: center; -} -.uxw-btn { - padding: var(--spacing-xs) var(--spacing-md); - border-radius: 4px; - font-size: 0.875rem; - font-weight: 500; - cursor: pointer; - border: none; -} -.uxw-btn-primary { - background: var(--color-text); - color: var(--color-paper); -} -.uxw-btn-secondary { - background: transparent; - color: var(--color-ash); - border: 1px solid var(--color-mist); -} -.uxw-btn-danger { - background: #c00; - color: white; -} -.uxw-empty-icon { - font-size: 2.5rem; - margin-bottom: var(--spacing-sm); - opacity: 0.4; -} -.uxw-empty-title { - font-weight: 500; - color: var(--color-ash); -} -.uxw-empty-text { - font-size: 0.875rem; - color: var(--color-charcoal); - margin-top: var(--spacing-xs); -} -.uxw-empty-action { - margin-top: var(--spacing-md); -} -.uxw-empty-after .uxw-empty-icon { - opacity: 1; -} -.uxw-empty-after .uxw-empty-title { - color: var(--color-text); -} -.spatial-demo { - width: 100%; - max-width: 340px; - padding: var(--spacing-md); - background: var(--color-paper); - border: 1px solid var(--color-mist); - border-radius: 6px; -} -.spatial-grid-before { - display: flex; - flex-wrap: wrap; - gap: 6px; -} -.spatial-grid-after { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--spacing-sm); -} -.spatial-card-item { - padding: var(--spacing-sm); - background: var(--color-bg); - border: 1px solid var(--color-mist); - border-radius: 4px; - font-size: 0.8125rem; - color: var(--color-charcoal); - text-align: center; -} -.spatial-grid-after .spatial-card-item { - width: auto !important; -} -.spatial-hierarchy-before .spatial-h-title, .spatial-hierarchy-before .spatial-h-subtitle, .spatial-hierarchy-before .spatial-h-cta, .spatial-hierarchy-before .spatial-h-link { - font-size: 0.9375rem; - margin-bottom: var(--spacing-xs); - color: var(--color-charcoal); -} -.spatial-hierarchy-after .spatial-h-title { - font-family: var(--font-display); - font-size: 1.75rem; - font-weight: 300; - font-style: italic; - margin-bottom: var(--spacing-xs); - color: var(--color-text); -} -.spatial-hierarchy-after .spatial-h-subtitle { - font-size: 0.6875rem; - text-transform: uppercase; - letter-spacing: 0.1em; - color: var(--color-ash); - margin-bottom: var(--spacing-md); -} -.spatial-hierarchy-after .spatial-h-cta { - display: inline-block; - padding: var(--spacing-sm) var(--spacing-lg); - background: var(--color-text); - color: var(--color-paper); - font-size: 0.875rem; - font-weight: 500; - border-radius: 4px; - margin-bottom: var(--spacing-sm); -} -.spatial-hierarchy-after .spatial-h-link { - font-size: 0.75rem; - color: var(--color-ash); -} -.spatial-whitespace-before { - padding: var(--spacing-xs) !important; -} -.spatial-whitespace-before .spatial-ws-title { - font-size: 1rem; - font-weight: 600; - margin-bottom: 2px; -} -.spatial-whitespace-before .spatial-ws-price { - font-size: 0.875rem; - color: var(--color-ash); - margin-bottom: 4px; -} -.spatial-whitespace-before .spatial-ws-features { - font-size: 0.75rem; - color: var(--color-ash); - margin-bottom: 6px; -} -.spatial-whitespace-before .spatial-ws-btn { - width: 100%; - padding: 6px; - font-size: 0.75rem; - background: var(--color-text); - color: var(--color-paper); - border: none; - border-radius: 3px; - cursor: pointer; -} -.spatial-whitespace-after { - padding: var(--spacing-lg) !important; -} -.spatial-whitespace-after .spatial-ws-title { - font-family: var(--font-display); - font-size: 1.5rem; - font-weight: 400; - margin-bottom: var(--spacing-sm); -} -.spatial-whitespace-after .spatial-ws-price { - font-size: 1.25rem; - font-weight: 600; - color: var(--color-text); - margin-bottom: var(--spacing-sm); -} -.spatial-whitespace-after .spatial-ws-features { - font-size: 0.8125rem; - color: var(--color-ash); - margin-bottom: var(--spacing-lg); - line-height: 1.6; -} -.spatial-whitespace-after .spatial-ws-btn { - width: 100%; - padding: var(--spacing-sm); - font-size: 0.875rem; - background: var(--color-text); - color: var(--color-paper); - border: none; - border-radius: 4px; - cursor: pointer; - font-weight: 500; -} -.motion-demo { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--spacing-sm); - width: 100%; - max-width: 280px; -} -.motion-stagger-demo { - align-items: stretch; -} -.motion-list-item { - display: flex; - align-items: center; - gap: var(--spacing-sm); - padding: var(--spacing-sm) var(--spacing-md); - background: var(--color-bg); - border: 1px solid var(--color-mist); - border-radius: 4px; - font-size: 0.875rem; - color: var(--color-charcoal); -} -.motion-dot { - width: 8px; - height: 8px; - background: var(--color-accent); - border-radius: 50%; -} -.demo-viewport[data-state="after"] .motion-list-item { - opacity: 0; - transform: translateY(12px); - animation: staggerIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards; -} -.demo-viewport[data-state="after"] .motion-list-item:nth-child(1) { - animation-delay: 0s; -} -.demo-viewport[data-state="after"] .motion-list-item:nth-child(2) { - animation-delay: 0.05s; -} -.demo-viewport[data-state="after"] .motion-list-item:nth-child(3) { - animation-delay: 0.1s; -} -.demo-viewport[data-state="after"] .motion-list-item:nth-child(4) { - animation-delay: 0.15s; -} -@keyframes staggerIn { - to { - opacity: 1; - transform: translateY(0); - } -} -.motion-btn { - padding: 12px 24px; - font-size: 0.9375rem; - font-weight: 500; - border: none; - border-radius: 4px; - cursor: pointer; -} -.motion-btn-before { - background: var(--color-charcoal); - color: var(--color-paper); -} -.motion-btn-after { - background: var(--color-text); - color: var(--color-paper); - transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.2s ease; -} -.motion-btn-after:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0,0,0,0.15); -} -.motion-btn-after:active { - transform: translateY(0) scale(0.98); -} -.motion-card { - padding: var(--spacing-md); - background: var(--color-bg); - border: 1px solid var(--color-mist); - border-radius: 6px; - text-align: center; - min-width: 140px; -} -.motion-card-icon { - font-size: 1.5rem; - margin-bottom: var(--spacing-xs); -} -.motion-card-text { - font-size: 0.8125rem; - color: var(--color-charcoal); -} -.motion-card-after { - transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); -} -.demo-viewport[data-state="after"] .motion-card-after { - background: var(--color-accent); - @supports (color: color-mix(in lab, red, red)) { - background: color-mix(in oklch, var(--color-accent) 10%, var(--color-paper)); - } - border-color: var(--color-accent); -} -.demo-viewport[data-state="after"] .motion-card-after .motion-card-icon { - animation: checkPop 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); -} -@keyframes checkPop { - 50% { - transform: scale(1.3); - } -} -.typo-demo { - width: 100%; - max-width: 320px; - text-align: left; -} -.typo-pairing-before { - font-family: 'Inter', system-ui, sans-serif; -} -.typo-pairing-before .typo-heading { - font-size: 1.5rem; - font-weight: 600; - margin-bottom: var(--spacing-xs); -} -.typo-pairing-before .typo-body { - font-size: 0.9375rem; - line-height: 1.5; - color: var(--color-ash); -} -.typo-pairing-after .typo-heading { - font-family: var(--font-display); - font-size: 2rem; - font-weight: 300; - font-style: italic; - letter-spacing: -0.02em; - margin-bottom: var(--spacing-sm); - color: var(--color-text); -} -.typo-pairing-after .typo-body { - font-family: var(--font-body); - font-size: 0.9375rem; - line-height: 1.7; - color: var(--color-charcoal); -} -.typo-hierarchy-before .typo-h1 { - font-size: 1.125rem; - font-weight: 600; - margin-bottom: 4px; -} -.typo-hierarchy-before .typo-meta { - font-size: 0.9375rem; - color: var(--color-ash); - margin-bottom: var(--spacing-xs); -} -.typo-hierarchy-before .typo-p { - font-size: 0.875rem; - line-height: 1.5; - color: var(--color-charcoal); -} -.typo-hierarchy-after .typo-h1 { - font-family: var(--font-display); - font-size: 2.25rem; - font-weight: 300; - letter-spacing: -0.03em; - margin-bottom: 2px; - line-height: 1.1; -} -.typo-hierarchy-after .typo-meta { - font-size: 0.6875rem; - text-transform: uppercase; - letter-spacing: 0.12em; - color: var(--color-accent); - margin-bottom: var(--spacing-md); -} -.typo-hierarchy-after .typo-p { - font-size: 0.9375rem; - line-height: 1.7; - color: var(--color-ash); -} -.int-demo { - display: flex; - flex-direction: column; - gap: var(--spacing-md); - width: 100%; - max-width: 280px; -} -.int-states-demo { - gap: var(--spacing-lg); -} -.int-state-row { - display: flex; - align-items: center; - gap: var(--spacing-md); -} -.int-state-label { - font-size: 0.6875rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--color-ash); - width: 40px; -} -.int-btn { - flex: 1; - padding: var(--spacing-sm) var(--spacing-md); - font-size: 0.875rem; - font-weight: 500; - border-radius: 4px; - cursor: pointer; -} -.int-btn-poor { - background: var(--color-charcoal); - color: var(--color-paper); - border: none; -} -.int-btn-good { - background: var(--color-text); - color: var(--color-paper); - border: 2px solid transparent; - transition: all 0.15s ease; -} -.int-btn-good:hover { - background: var(--color-charcoal); -} -.int-btn-good:focus { - outline: none; - border-color: var(--color-accent); - box-shadow: 0 0 0 3px var(--color-accent); - @supports (color: color-mix(in lab, red, red)) { - box-shadow: 0 0 0 3px color-mix(in oklch, var(--color-accent) 25%, transparent); - } -} -.int-btn-good:active { - transform: scale(0.98); -} -.int-aff-item { - padding: var(--spacing-sm) var(--spacing-md); - border-radius: 4px; - font-size: 0.875rem; - cursor: pointer; -} -.int-aff-poor { - color: var(--color-charcoal); -} -.int-aff-good { - color: var(--color-accent); - text-decoration: underline; - text-underline-offset: 2px; -} -.int-aff-good::after { - content: ' →'; -} -.int-affordance-after .int-aff-item { - background: var(--color-bg); - border: 1px solid var(--color-mist); - color: var(--color-accent); - text-decoration: underline; - text-underline-offset: 2px; - transition: background 0.15s ease; -} -.int-affordance-after .int-aff-item:hover { - background: var(--color-accent); - @supports (color: color-mix(in lab, red, red)) { - background: color-mix(in oklch, var(--color-accent) 5%, var(--color-paper)); - } -} -.int-affordance-after .int-aff-item::after { - content: ' →'; -} -.int-feedback-before, .int-feedback-after { - display: flex; - align-items: center; - gap: var(--spacing-md); - flex-direction: row; -} -.int-fb-btn { - width: 48px; - height: 48px; - border-radius: 50%; - border: none; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; -} -.int-fb-btn svg { - width: 22px; - height: 22px; -} -.int-fb-silent { - background: var(--color-mist); - color: var(--color-ash); -} -.int-fb-active { - background: var(--color-charcoal); - color: var(--color-paper); - transition: all 0.15s cubic-bezier(0.34, 1.56, 0.64, 1); -} -.int-fb-active:hover { - transform: scale(1.1); -} -.int-fb-active:active { - transform: scale(0.95); -} -.int-fb-active.liked { - background: var(--color-accent); - animation: heartPop 0.35s cubic-bezier(0.34, 1.56, 0.64, 1); -} -@keyframes heartPop { - 50% { - transform: scale(1.25); - } -} -.int-fb-label { - font-size: 0.875rem; - color: var(--color-charcoal); -} -.color-demo { - width: 100%; - max-width: 300px; -} -.color-palette-before, .color-palette-after { - display: flex; - flex-wrap: wrap; - gap: var(--spacing-xs); - padding: var(--spacing-md); - background: var(--color-paper); - border: 1px solid var(--color-mist); - border-radius: 6px; -} -.color-swatch { - width: 40px; - height: 40px; - border-radius: 4px; - transition: background 0.2s ease; -} -.color-card { - width: 100%; - margin-top: var(--spacing-sm); - padding: var(--spacing-sm); - background: var(--color-paper); - border: 1px solid var(--color-mist); - border-radius: 4px; - display: flex; - flex-direction: column; - gap: 4px; -} -.color-card span { - font-size: 0.8125rem; - font-weight: 500; - transition: color 0.2s ease; -} -.color-card button { - padding: 6px; - border: none; - border-radius: 3px; - font-size: 0.75rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; -} -.color-palette-before .swatch-1 { - background: #ff6b6b; -} -.color-palette-before .swatch-2 { - background: #4ecdc4; -} -.color-palette-before .swatch-3 { - background: #ffe66d; -} -.color-palette-before .swatch-4 { - background: #95e1d3; -} -.color-palette-before .swatch-5 { - background: #f38181; -} -.color-palette-before .card-title { - color: #ff6b6b; -} -.color-palette-before .card-subtitle { - color: #4ecdc4; -} -.color-palette-before .card-btn { - background: #ffe66d; - color: #333; -} -.color-palette-after .swatch-1 { - background: var(--color-text); -} -.color-palette-after .swatch-2 { - background: var(--color-charcoal); -} -.color-palette-after .swatch-3 { - background: var(--color-ash); -} -.color-palette-after .swatch-4 { - background: var(--color-mist); -} -.color-palette-after .swatch-5 { - background: var(--color-accent); -} -.color-palette-after .card-title { - color: var(--color-text); -} -.color-palette-after .card-subtitle { - color: var(--color-ash); -} -.color-palette-after .card-btn { - background: var(--color-accent); - color: var(--color-paper); -} -.color-accent-card { - padding: var(--spacing-md); - border-radius: 6px; -} -.color-accent-before .color-accent-card { - background: #f5f5f5; - border: 1px solid #e0e0e0; -} -.color-accent-before .color-accent-title { - font-weight: 600; - color: #333; - margin-bottom: 4px; -} -.color-accent-before .color-accent-text { - font-size: 0.8125rem; - color: #666; - margin-bottom: var(--spacing-sm); -} -.color-accent-before .color-accent-btn { - width: 100%; - padding: var(--spacing-xs); - background: #333; - color: white; - border: none; - border-radius: 4px; - font-size: 0.8125rem; - cursor: pointer; -} -.color-accent-after .color-accent-card { - background: var(--color-accent); - @supports (color: color-mix(in lab, red, red)) { - background: color-mix(in oklch, var(--color-accent) 8%, var(--color-paper)); - } - border: 1px solid var(--color-accent); - @supports (color: color-mix(in lab, red, red)) { - border: 1px solid color-mix(in oklch, var(--color-accent) 20%, var(--color-paper)); - } -} -.color-accent-after .color-accent-title { - font-weight: 600; - color: var(--color-text); - margin-bottom: 4px; -} -.color-accent-after .color-accent-text { - font-size: 0.8125rem; - color: var(--color-ash); - margin-bottom: var(--spacing-sm); -} -.color-accent-after .color-accent-btn { - width: 100%; - padding: var(--spacing-xs); - background: var(--color-accent); - color: var(--color-paper); - border: none; - border-radius: 4px; - font-size: 0.8125rem; - font-weight: 500; - cursor: pointer; -} -.color-contrast-static { - display: flex; - flex-direction: column; - gap: var(--spacing-sm); -} -.contrast-example { - padding: var(--spacing-md); - border-radius: 6px; - text-align: center; -} -.contrast-fail { - background: #f0f0f0; - color: #a0a0a0; -} -.contrast-pass { - background: var(--color-charcoal); - color: var(--color-paper); -} -.contrast-badge { - display: inline-block; - font-size: 0.5625rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.1em; - padding: 2px 6px; - border-radius: 2px; - margin-bottom: 4px; -} -.contrast-fail .contrast-badge { - background: #ddd; -} -.contrast-pass .contrast-badge { - background: var(--color-accent); - color: var(--color-paper); -} -.contrast-text { - font-size: 1rem; - font-weight: 500; - margin-bottom: 2px; -} -.contrast-ratio { - font-size: 0.6875rem; - opacity: 0.7; -} -.resp-demo { - width: 100%; - max-width: 340px; -} -.resp-touch-demo { - display: flex; - flex-direction: column; - gap: var(--spacing-lg); -} -.resp-touch-row { - display: flex; - align-items: center; - gap: var(--spacing-md); -} -.resp-label { - font-size: 0.6875rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--color-ash); - width: 70px; -} -.resp-touch-targets { - display: flex; - gap: 4px; -} -.resp-touch-targets button { - border: none; - border-radius: 4px; - cursor: pointer; - font-weight: 500; -} -.resp-touch-bad button { - width: 24px; - height: 24px; - font-size: 0.75rem; - background: var(--color-mist); - color: var(--color-ash); -} -.resp-touch-good button { - width: 44px; - height: 44px; - font-size: 1rem; - background: var(--color-text); - color: var(--color-paper); -} -.resp-fluid-demo { - padding: var(--spacing-md); - background: var(--color-bg); - border: 1px solid var(--color-mist); - border-radius: 6px; -} -.resp-fluid-container { - display: flex; - flex-direction: column; - gap: var(--spacing-md); -} -.resp-fluid-fixed, .resp-fluid-adaptive { - font-size: 0.75rem; - color: var(--color-ash); -} -.resp-fluid-fixed span, .resp-fluid-adaptive span { - display: block; - margin-bottom: 4px; -} -.resp-fluid-bar { - height: 24px; - background: var(--color-mist); - border-radius: 4px; -} -.resp-fluid-adaptive .resp-fluid-bar { - background: var(--color-accent); -} -.resp-adapt-demo { - display: flex; - gap: var(--spacing-sm); - align-items: flex-end; -} -.resp-device { - text-align: center; -} -.resp-device > span { - display: block; - margin-top: 4px; - font-size: 0.625rem; - color: var(--color-ash); - text-transform: uppercase; - letter-spacing: 0.08em; -} -.resp-device-screen { - background: var(--color-paper); - border: 2px solid var(--color-mist); - border-radius: 4px; - padding: 4px; - display: flex; - flex-direction: column; - gap: 3px; -} -.resp-device-mobile .resp-device-screen { - width: 50px; - height: 80px; -} -.resp-device-tablet .resp-device-screen { - width: 80px; - height: 60px; -} -.resp-device-desktop .resp-device-screen { - width: 120px; - height: 70px; -} -.resp-block { - background: var(--color-mist); - border-radius: 2px; -} -.resp-block-row { - display: flex; - gap: 3px; - flex: 1; -} -.resp-header { - height: 16px; - background: var(--color-charcoal); -} -.resp-sidebar { - width: 30%; - background: var(--color-charcoal); -} -.resp-content { - flex: 1; -} -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } -} -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - } -} -*, *::before, *::after { - box-sizing: border-box; -} -* { - margin: 0; -} -img, picture, video, canvas, svg { - display: block; - max-width: 100%; -} -button, input, textarea, select { - font: inherit; -} -:root { - --font-display: 'Cormorant Garamond', Georgia, serif; - --font-body: 'Instrument Sans', system-ui, sans-serif; - --font-mono: 'Space Grotesk', monospace; - --spacing-xs: 8px; - --spacing-sm: 16px; - --spacing-md: 24px; - --spacing-lg: 32px; - --spacing-xl: 48px; - --spacing-2xl: 80px; - --spacing-3xl: 120px; - --width-max: 1400px; - --width-content: 900px; - --ease-out: cubic-bezier(0.16, 1, 0.3, 1); - --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); - --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); - --duration-fast: 0.15s; - --duration-base: 0.3s; - --duration-slow: 0.6s; - --duration-slower: 0.8s; - --duration-slowest: 1.2s; - --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); - --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); - --cat-diagnostic-bg: #fdf4ff; - --cat-diagnostic-border: #d946ef; - --cat-diagnostic-text: #a21caf; - --cat-quality-bg: #f0fdf4; - --cat-quality-border: #22c55e; - --cat-quality-text: #15803d; - --cat-intensity-bg: #fffbeb; - --cat-intensity-border: #f59e0b; - --cat-intensity-text: #b45309; - --cat-adaptation-bg: #eff6ff; - --cat-adaptation-border: #3b82f6; - --cat-adaptation-text: #1d4ed8; - --cat-enhancement-bg: #fdf2f8; - --cat-enhancement-border: #ec4899; - --cat-enhancement-text: #be185d; - --cat-system-bg: #f5f5f4; - --cat-system-border: #78716c; - --cat-system-text: #44403c; -} -.skip-link { - position: absolute; - top: -100%; - left: 50%; - transform: translateX(-50%); - z-index: 10000; - padding: var(--spacing-sm) var(--spacing-lg); - background: var(--color-ink); - color: var(--color-paper); - font-weight: 600; - text-decoration: none; - border-radius: 0 0 8px 8px; - transition: top 0.2s ease; -} -.skip-link:focus { - top: 0; - outline: 2px solid var(--color-accent); - outline-offset: 2px; -} -.section-nav { - position: fixed; - bottom: var(--spacing-md); - left: 50%; - transform: translateX(-50%) translateY(20px); - z-index: 100; - display: flex; - align-items: center; - gap: 2px; - padding: 4px; - background: oklch(98% 0 0 / 0.85); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - border: 1px solid var(--color-mist); - border-radius: 100px; - box-shadow: 0 4px 24px -4px rgba(0,0,0,0.12), 0 1px 3px rgba(0,0,0,0.06); - opacity: 0; - pointer-events: none; - transition: opacity 0.4s var(--ease-out), transform 0.4s var(--ease-out); -} -.section-nav.is-visible { - opacity: 1; - pointer-events: auto; - transform: translateX(-50%) translateY(0); -} -.section-nav-item { - display: flex; - align-items: center; - gap: 4px; - padding: 8px 12px; - text-decoration: none; - border-radius: 100px; - transition: background 0.2s ease, color 0.2s ease; -} -.section-nav-item:hover { - background: var(--color-mist); - text-decoration: none; -} -.section-nav-item.is-active { - background: var(--color-ink); -} -.section-nav-item.is-active .section-nav-num { - color: var(--color-paper); -} -.section-nav-item.is-active .section-nav-label { - color: var(--color-paper); -} -.section-nav-num { - font-family: var(--font-mono); - font-size: 0.5625rem; - font-weight: 500; - color: var(--color-ash); - letter-spacing: 0.02em; - transition: color 0.2s ease; -} -.section-nav-label { - font-family: var(--font-body); - font-size: 0.6875rem; - font-weight: 500; - color: var(--color-charcoal); - transition: color 0.2s ease; - white-space: nowrap; -} -@media (max-width: 700px) { - .section-nav-label { - display: none; - } - .section-nav-item { - padding: 8px 10px; - } -} -@media (max-width: 400px) { - .section-nav { - display: none; - } -} -html { - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - text-rendering: optimizeLegibility; - overflow-x: clip; -} -body { - font-family: var(--font-body); - font-size: 16px; - line-height: 1.625; - color: var(--color-text); - background: var(--color-paper); - overflow-x: clip; - min-height: 100vh; - min-height: 100dvh; -} -h1, h2, h3, h4, h5, h6 { - font-family: var(--font-display); - font-weight: 400; - line-height: 1.1; - letter-spacing: -0.02em; - color: var(--color-ink); -} -a { - color: var(--color-accent); - text-decoration: underline; - text-decoration-thickness: 1px; - text-underline-offset: 2px; - transition: color var(--duration-fast) var(--ease-out), text-decoration-color var(--duration-fast) var(--ease-out); -} -a:hover { - color: var(--color-accent-hover); - text-decoration-thickness: 2px; -} -.btn, .footer-logo, [class*="nav-item"] { - text-decoration: none; -} -strong { - font-weight: 600; - color: var(--color-ink); -} -code { - font-family: var(--font-mono); - font-size: 0.9em; - padding: 0.15em 0.4em; - background: var(--color-accent-dim); - color: var(--color-accent); - border-radius: 4px; -} -::selection { - background: var(--color-accent-soft); - color: var(--color-ink); -} -.grain-overlay { - position: fixed; - inset: 0; - pointer-events: none; - z-index: 9999; - opacity: 0.03; - background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E"); - background-repeat: repeat; -} -.site-content { - max-width: var(--width-max); - margin: 0 auto; - padding: 0 var(--spacing-lg); -} -@media (max-width: 768px) { - .site-content { - padding: 0 var(--spacing-md); - } -} -.section-header { - margin-bottom: var(--spacing-lg); - position: relative; -} -.section-number { - display: block; - font-family: var(--font-mono); - font-size: 0.625rem; - font-weight: 500; - letter-spacing: 0.05em; - color: var(--color-ash); - margin-bottom: var(--spacing-xs); - text-transform: uppercase; -} -.section-title { - font-size: clamp(1.75rem, 4vw, 2.5rem); - font-weight: 400; - line-height: 1.2; - margin: 0; -} -.section-subtitle { - font-size: 1rem; - line-height: 1.6; - color: var(--color-charcoal); - margin-top: var(--spacing-sm); - max-width: 55ch; -} -.cheatsheet-link { - color: var(--color-accent); - text-decoration: none; - font-size: 0.875rem; - margin-left: 0.5em; -} -.cheatsheet-link:hover { - text-decoration: underline; -} -.section-lead { - font-size: 1rem; - line-height: 1.6; - color: var(--color-charcoal); - max-width: 55ch; - margin-bottom: var(--spacing-lg); -} -.hero-combined { - position: relative; - display: flex; - flex-direction: column; - justify-content: center; - min-height: 100vh; - min-height: 100dvh; - padding: var(--spacing-lg) 0 var(--spacing-2xl); - background: var(--color-paper); -} -.github-link { - position: absolute; - top: var(--spacing-md); - right: var(--spacing-lg); - z-index: 10; - color: var(--color-ash); - transition: color 0.2s ease; - display: flex; - align-items: center; - gap: 6px; - text-decoration: none; -} -.github-link:hover { - color: var(--color-ink); - text-decoration: none; -} -.github-stars { - font-family: var(--font-mono); - font-size: 0.75rem; - font-weight: 500; - color: var(--color-ash); - transition: color 0.2s ease; -} -.github-link:hover .github-stars { - color: var(--color-ink); -} -.hero-combined-container { - max-width: var(--width-max); - margin: 0 auto; - padding: 0 var(--spacing-lg); - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--spacing-xl); - align-items: center; - width: 100%; -} -@media (max-width: 1024px) { - .hero-combined-container { - grid-template-columns: 1fr; - gap: var(--spacing-lg); - text-align: center; - } -} -.hero-combined-left { - display: flex; - flex-direction: column; - gap: var(--spacing-md); -} -@media (max-width: 1024px) { - .hero-combined-left { - align-items: center; - } -} -.hero-title-combined { - 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; - color: var(--color-ink); -} -.hero-tagline-combined { - 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); -} -.hero-hook-text { - font-size: 1rem; - line-height: 1.6; - color: var(--color-charcoal); - max-width: 45ch; - margin: 0; -} -.hero-included-box { - display: flex; - flex-direction: column; - gap: 6px; - padding: 10px 14px; - border: 1px solid var(--color-mist); - background: transparent; - max-width: 45ch; -} -.hero-included-title { - font-family: var(--font-body); - font-size: 0.5625rem; - font-weight: 500; - text-transform: uppercase; - letter-spacing: 0.1em; - color: var(--color-ash); -} -.hero-included-items { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 6px; - font-size: 0.8125rem; - color: var(--color-charcoal); - line-height: 1.5; -} -.hero-included-items em { - font-style: normal; - font-family: var(--font-mono); - font-size: 0.75rem; -} -.hero-included-sep { - color: var(--color-mist); -} -@media (max-width: 500px) { - .hero-included-items { - flex-direction: column; - align-items: flex-start; - gap: 4px; - } - .hero-included-sep { - display: none; - } -} -.hero-cta-group { - display: flex; - align-items: center; - gap: var(--spacing-lg); - margin-top: var(--spacing-sm); -} -@media (max-width: 600px) { - .hero-cta-group { - flex-direction: column; - gap: var(--spacing-md); - } -} -.hero-cta-combined { - display: inline-block; - padding: var(--spacing-sm) var(--spacing-xl); - font-family: var(--font-body); - font-size: 0.9rem; - font-weight: 500; - letter-spacing: 0.05em; - text-transform: uppercase; - text-decoration: none; - color: var(--color-paper); - background: var(--color-ink); - border: none; - transition: transform 0.2s ease, background 0.2s ease; -} -.hero-cta-combined:hover { - transform: translateY(-2px); - background: var(--color-accent); - color: var(--color-paper); -} -.hero-logos-inline { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 6px; -} -.hero-logos-inline .hero-logos-label { - font-size: 0.6875rem; - color: var(--color-ash); - letter-spacing: 0.03em; -} -.hero-logos-inline .hero-logos-row { - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; -} -.hero-logos-inline .hero-logos-row img { - border-radius: 4px; - opacity: 0.7; - transition: opacity 0.2s ease; -} -.hero-logos-inline .hero-logos-row img:hover { - opacity: 1; -} -.hero-combined-right { - display: flex; - justify-content: center; -} -.hero-combined-right .split-comparison { - max-width: 520px; - width: 100%; -} -.hero-combined-right .split-container { - max-width: 100%; -} -.hero-bias-tags { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--spacing-xs); - margin-top: var(--spacing-lg); - padding-top: var(--spacing-md); - border-top: 1px solid var(--color-mist); - max-width: var(--width-max); - margin-left: auto; - margin-right: auto; - width: 100%; - padding-bottom: var(--spacing-md); -} -.problem-section { - padding: var(--spacing-2xl) 0; - border-top: 1px solid var(--color-mist); -} -.problem-content { - display: grid; - gap: var(--spacing-xl); -} -.split-comparison { - position: relative; - width: 100%; - max-width: 600px; - margin: 0 auto; - padding: 20px; - margin-top: -20px; - margin-bottom: -20px; -} -.split-container { - position: relative; - width: 100%; - max-width: 500px; - height: 380px; - margin: 0 auto; - border-radius: 12px; - overflow: hidden; - background: var(--color-cream); - border: 1px solid var(--color-mist); - cursor: ew-resize; - user-select: none; -} -.split-before, .split-after { - position: absolute; - inset: 0; - display: flex; - align-items: center; - justify-content: center; -} -.split-before { - z-index: 1; -} -.split-content { - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; -} -.split-after { - clip-path: polygon(58% 0%, 100% 0%, 100% 100%, 42% 100%); - z-index: 2; - background: var(--color-paper); -} -.split-divider { - position: absolute; - top: 0; - bottom: 0; - left: 50%; - width: 3px; - background: var(--color-accent); - transform: translateX(-50%) skewX(-10deg); - pointer-events: none; - z-index: 3; - box-shadow: 0 0 20px rgba(0,0,0,0.15); -} -.split-label { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%) skewX(10deg); - font-size: 0.6875rem; - font-weight: 600; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--color-paper); - background: var(--color-accent); - padding: 6px 14px; - border-radius: 4px; - white-space: nowrap; - box-shadow: 0 2px 8px rgba(0,0,0,0.2); -} -.slop-card { - width: 280px; - height: 280px; - background: linear-gradient(135deg, #f5f3ff 0%, #ede9fe 50%, #ddd6fe 100%); - border-radius: 16px; - padding: 24px; - box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); - font-family: 'Inter', system-ui, sans-serif; - display: flex; - flex-direction: column; -} -.slop-header { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 16px; -} -.slop-avatar { - width: 40px; - height: 40px; - border-radius: 50%; - background: linear-gradient(135deg, #8b5cf6, #7c3aed); - flex-shrink: 0; -} -.slop-text { - flex: 1; -} -.slop-title { - font-size: 14px; - font-weight: 600; - color: #1f2937; - margin-bottom: 2px; -} -.slop-subtitle { - font-size: 12px; - color: #6b7280; -} -.slop-body { - font-size: 13px; - line-height: 1.5; - color: #4b5563; - margin-bottom: auto; - flex: 1; -} -.slop-button { - width: 100%; - padding: 10px 20px; - background: linear-gradient(135deg, #8b5cf6, #7c3aed); - color: white; - border: none; - border-radius: 8px; - font-family: 'Inter', system-ui, sans-serif; - font-size: 13px; - font-weight: 500; - cursor: pointer; - margin-top: auto; -} -.slop-callouts { - position: absolute; - inset: 0; - pointer-events: none; -} -.slop-callout { - position: absolute; - font-size: 0.625rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--color-accent); - background: var(--color-paper); - padding: 4px 8px; - border: 1px solid var(--color-accent); - border-radius: 3px; - white-space: nowrap; - opacity: 0; - animation: calloutFadeIn 0.4s var(--ease-out) forwards; - box-shadow: 0 2px 8px rgba(0,0,0,0.1); -} -.slop-callout[data-point="font"] { - top: 15%; - right: 5%; - animation-delay: 0.1s; -} -.slop-callout[data-point="gradient"] { - top: 40%; - left: 5%; - animation-delay: 0.25s; -} -.slop-callout[data-point="copy"] { - bottom: 35%; - right: 8%; - animation-delay: 0.4s; -} -.slop-callout[data-point="rounded"] { - bottom: 12%; - left: 10%; - animation-delay: 0.55s; -} -@keyframes calloutFadeIn { - from { - opacity: 0; - transform: scale(0.9); - } - to { - opacity: 1; - transform: scale(1); - } -} -.impeccable-card { - width: 280px; - height: 300px; - background: var(--color-paper); - border: 1px solid var(--color-mist); - padding: var(--spacing-lg); - text-align: left; - display: flex; - flex-direction: column; -} -.impeccable-eyebrow { - font-family: var(--font-mono); - font-size: 0.625rem; - font-weight: 500; - letter-spacing: 0.15em; - text-transform: uppercase; - color: var(--color-accent); - margin-bottom: var(--spacing-xs); -} -.impeccable-title { - 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; -} -.impeccable-body { - font-size: 0.875rem; - line-height: 1.6; - color: var(--color-ash); - margin-bottom: auto; - flex: 1; -} -.impeccable-button { - display: inline-flex; - margin-top: var(--spacing-sm); - padding: 0.625rem 1.5rem; - background: var(--color-ink); - color: var(--color-paper); - border: none; - font-family: var(--font-body); - font-size: 0.8125rem; - font-weight: 500; - letter-spacing: 0.03em; - cursor: pointer; - transition: all var(--duration-base) var(--ease-out); - align-self: flex-start; -} -.impeccable-button:hover { - background: var(--color-accent); -} -.split-labels { - display: flex; - justify-content: center; - gap: var(--spacing-xl); - margin-top: var(--spacing-md); -} -.split-label-item { - display: flex; - align-items: center; - gap: var(--spacing-xs); - font-size: 0.8125rem; - color: var(--color-ash); -} -.split-label-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--color-mist); -} -.split-label-dot--accent { - background: var(--color-accent); -} -.solution-section { - padding: var(--spacing-2xl) 0; - border-top: 1px solid var(--color-mist); -} -.solution-content { - display: grid; - gap: var(--spacing-lg); -} -.solution-content .section-lead { - margin-bottom: 0; -} -.solution-visual { - display: grid; - grid-template-columns: 1fr auto 1fr; - gap: var(--spacing-lg); - align-items: stretch; -} -@media (max-width: 900px) { - .solution-visual { - grid-template-columns: 1fr; - gap: var(--spacing-md); - } -} -.solution-visual-interactive { - width: 100%; - min-height: 380px; - background: var(--color-paper); - border: 1px solid var(--color-mist); - border-radius: 8px; - position: relative; - overflow: hidden; -} -.solution-pillar { - background: var(--color-cream); - border: 1px solid var(--color-mist); - padding: var(--spacing-lg); - transition: all var(--duration-base) var(--ease-out); -} -.solution-pillar:hover { - border-color: var(--color-accent); - transform: translateY(-4px); - box-shadow: 0 20px 60px var(--color-accent-dim); -} -.pillar-header { - text-align: center; - margin-bottom: var(--spacing-lg); - padding-bottom: var(--spacing-md); - border-bottom: 1px solid var(--color-mist); -} -.pillar-icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 56px; - height: 56px; - border-radius: 50%; - background: var(--color-accent-dim); - color: var(--color-accent); - margin-bottom: var(--spacing-sm); -} -.pillar-title { - font-family: var(--font-display); - font-size: 1.75rem; - font-weight: 400; - margin: 0 0 var(--spacing-xs); -} -.pillar-subtitle { - font-size: 0.875rem; - color: var(--color-ash); - margin: 0; -} -.pillar-content { - display: flex; - flex-direction: column; - gap: var(--spacing-sm); -} -.pillar-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: var(--spacing-sm); - background: var(--color-paper); - border-radius: 4px; - transition: all var(--duration-fast) var(--ease-out); -} -.pillar-item:hover { - background: var(--color-accent-dim); -} -.pillar-item-name { - font-weight: 500; - color: var(--color-ink); - font-size: 0.9375rem; -} -.pillar-item-code { - font-family: var(--font-mono); - font-size: 0.875rem; - font-weight: 500; - color: var(--color-accent); - background: transparent; - padding: 0; -} -.pillar-item-desc { - font-size: 0.75rem; - color: var(--color-ash); -} -.pillar-item--more { - justify-content: center; - font-size: 0.8125rem; - font-weight: 500; - color: var(--color-accent); - background: transparent; - border: 1px dashed var(--color-mist); -} -.solution-connector { - display: flex; - align-items: center; - justify-content: center; -} -.connector-plus { - font-family: var(--font-display); - font-size: 3rem; - font-weight: 300; - color: var(--color-accent); - opacity: 0.5; -} -@media (max-width: 900px) { - .solution-connector { - padding: var(--spacing-sm) 0; - } - .connector-plus { - font-size: 2rem; - } -} -.skills-section { - padding: var(--spacing-2xl) 0; - border-top: 1px solid var(--color-mist); -} -.skills-gallery { - display: grid; - grid-template-columns: 200px 1fr; - gap: var(--spacing-xl); - align-items: start; -} -@media (max-width: 968px) { - .skills-gallery { - grid-template-columns: 1fr; - gap: var(--spacing-lg); - } -} -.skills-nav { - display: flex; - flex-direction: column; - gap: 2px; - position: sticky; - top: var(--spacing-lg); -} -@media (max-width: 968px) { - .skills-nav { - flex-direction: row; - flex-wrap: wrap; - gap: var(--spacing-xs); - position: static; - } -} -.skill-nav-item { - padding: var(--spacing-sm) var(--spacing-md); - background: transparent; - border: none; - border-left: 2px solid transparent; - color: var(--color-ash); - font-family: var(--font-body); - font-size: 0.9375rem; - font-weight: 400; - cursor: pointer; - transition: all 0.2s ease; - text-align: left; - text-decoration: none; - display: block; -} -.skill-nav-item:hover { - color: var(--color-text); - background: var(--color-cream); -} -.skill-nav-item.active { - color: var(--color-accent); - border-left-color: var(--color-accent); - background: var(--color-accent-dim); - font-weight: 500; -} -@media (max-width: 968px) { - .skill-nav-item { - border-left: none; - border-bottom: 2px solid transparent; - padding: var(--spacing-xs) var(--spacing-md); - } - .skill-nav-item.active { - border-bottom-color: var(--color-accent); - } -} -.skills-showcase { - display: grid; - grid-template-columns: 1.2fr 1fr; - gap: var(--spacing-lg); - align-items: start; -} -@media (max-width: 1100px) { - .skills-showcase { - grid-template-columns: 1fr; - } -} -.loading-state { - padding: var(--spacing-xl); - text-align: center; - color: var(--color-ash); - font-style: italic; -} -.mobile-commands-layout { - display: none; -} -@media (max-width: 900px) { - .mobile-commands-layout { - display: flex; - flex-direction: column; - gap: var(--spacing-md); - } - .commands-container { - display: none; - } -} -.mobile-carousel-wrapper { - overflow-x: auto; - -webkit-overflow-scrolling: touch; - scrollbar-width: none; - padding: var(--spacing-xs) 0; -} -.mobile-carousel-wrapper::-webkit-scrollbar { - display: none; -} -.mobile-carousel { - display: flex; - gap: var(--spacing-xs); - padding-right: var(--spacing-md); -} -.mobile-cmd-pill { - flex-shrink: 0; - padding: var(--spacing-sm) var(--spacing-md); - min-height: 44px; - font-family: var(--font-mono); - font-size: 0.8125rem; - font-weight: 500; - color: var(--color-ash); - background: var(--color-cream); - border: 1px solid var(--color-mist); - border-radius: 100px; - cursor: pointer; - transition: all 0.2s ease; - white-space: nowrap; -} -.mobile-cmd-pill:hover { - color: var(--color-charcoal); - border-color: var(--color-charcoal); -} -.mobile-cmd-pill.active { - color: var(--color-paper); - background: var(--color-ink); - border-color: var(--color-ink); -} -.mobile-demo-area { - background: var(--color-cream); - border: 1px solid var(--color-mist); - border-radius: 8px; - padding: var(--spacing-sm); -} -.mobile-demo-area .demo-split-comparison { - width: 100%; -} -.mobile-demo-area .split-container { - width: 100%; - max-width: 100%; - height: 320px; -} -.mobile-demo-area .demo-caption { - font-size: 0.75rem; - margin-top: var(--spacing-sm); -} -.mobile-info-area { - padding-top: var(--spacing-sm); -} -.mobile-cmd-info { - display: none; - padding: var(--spacing-sm) 0; -} -.mobile-cmd-info.active { - display: block; -} -.mobile-cmd-name { - font-family: var(--font-mono); - font-size: 1.125rem; - font-weight: 600; - color: var(--color-ink); - margin: 0 0 var(--spacing-xs) 0; -} -.mobile-cmd-desc { - font-size: 0.875rem; - color: var(--color-charcoal); - line-height: 1.5; - margin: 0; -} -.mobile-cmd-rel { - margin-top: var(--spacing-xs); - font-size: 0.75rem; - color: var(--color-ash); -} -.mobile-cmd-rel code { - font-family: var(--font-mono); - color: var(--color-ink); -} -.downloads-section { - padding: var(--spacing-2xl) 0; - border-top: 1px solid var(--color-mist); -} -.downloads-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: var(--spacing-lg); -} -.download-card { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - padding: var(--spacing-lg); - background: var(--color-cream); - border: 1px solid var(--color-mist); - transition: all var(--duration-base) var(--ease-out); -} -.download-card:hover { - border-color: var(--color-accent); - transform: translateY(-4px); - box-shadow: 0 20px 60px var(--color-accent-dim); -} -.download-card-icon { - margin-bottom: var(--spacing-sm); -} -.download-card-icon img { - width: 40px; - height: 40px; - object-fit: contain; - border-radius: 8px; -} -.download-card-title { - font-family: var(--font-display); - font-size: 1.25rem; - font-weight: 400; - margin: 0 0 var(--spacing-sm) 0; -} -.download-card-note { - font-size: 0.75rem; - color: var(--color-ash); - margin-bottom: var(--spacing-xs); -} -.download-card .btn { - margin-top: var(--spacing-xs); -} -.install-command { - display: flex; - align-items: center; - gap: var(--spacing-xs); - background: var(--color-paper); - border: 1px solid var(--color-mist); - border-radius: 6px; - padding: var(--spacing-sm); - margin-top: var(--spacing-sm); - width: 100%; -} -.install-command code { - flex: 1; - font-family: var(--font-mono); - font-size: 0.75rem; - color: var(--color-ink); - background: transparent; - padding: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.copy-btn { - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - background: transparent; - border: 1px solid var(--color-mist); - border-radius: 4px; - color: var(--color-ash); - cursor: pointer; - flex-shrink: 0; - transition: all var(--duration-fast) var(--ease-out); -} -.copy-btn:hover { - background: var(--color-accent-dim); - border-color: var(--color-accent); - color: var(--color-accent); -} -.copy-btn.copied { - background: var(--color-accent); - border-color: var(--color-accent); - color: var(--color-paper); -} -.install-hint { - font-size: 0.75rem; - color: var(--color-ash); - margin: var(--spacing-xs) 0 0 0; -} -.install-hint code { - font-family: var(--font-mono); - font-size: 0.6875rem; - background: var(--color-mist); - padding: 2px 5px; - border-radius: 3px; -} -.download-card-details { - width: 100%; - margin-top: var(--spacing-sm); - font-size: 0.8125rem; - text-align: left; -} -.download-card-details summary { - cursor: pointer; - color: var(--color-ash); - font-size: 0.75rem; - padding: var(--spacing-xs) 0; - list-style: none; - display: flex; - align-items: center; - justify-content: center; - gap: 4px; -} -.download-card-details summary::before { - content: '▶'; - font-size: 0.5rem; - transition: transform var(--duration-fast) var(--ease-out); -} -.download-card-details[open] summary::before { - transform: rotate(90deg); -} -.download-card-details summary::-webkit-details-marker { - display: none; -} -.download-card-details ol { - margin: var(--spacing-sm) 0; - padding-left: var(--spacing-md); - color: var(--color-charcoal); - line-height: 1.6; -} -.download-card-details li { - margin-bottom: 4px; -} -.download-card-details code { - font-family: var(--font-mono); - font-size: 0.6875rem; - background: var(--color-mist); - padding: 2px 5px; - border-radius: 3px; -} -.download-card-details a { - color: var(--color-accent); - text-decoration: none; - font-size: 0.75rem; -} -.download-card-details a:hover { - text-decoration: underline; -} -.opensource-section { - padding: var(--spacing-2xl) 0; - border-top: 1px solid var(--color-mist); - text-align: center; -} -.opensource-content { - max-width: 500px; - margin: 0 auto; - display: flex; - flex-direction: column; - align-items: center; - gap: var(--spacing-md); -} -.opensource-title { - font-size: clamp(1.5rem, 4vw, 2rem); - font-weight: 300; -} -.opensource-desc { - font-size: 1.125rem; - color: var(--color-ash); - line-height: 1.6; -} -.site-footer { - border-top: 1px solid var(--color-mist); - padding: var(--spacing-xl) var(--spacing-lg); - background: var(--color-cream); -} -.footer-content { - max-width: var(--width-max); - margin: 0 auto; - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: var(--spacing-lg); -} -@media (max-width: 768px) { - .footer-content { - flex-direction: column; - text-align: center; - } -} -.footer-brand { - display: flex; - flex-direction: column; - gap: var(--spacing-xs); -} -.footer-logo { - font-family: var(--font-display); - font-size: 1.25rem; - font-weight: 400; - color: var(--color-ink); -} -.footer-tagline { - font-size: 0.875rem; - color: var(--color-ash); -} -.footer-links { - display: flex; - gap: var(--spacing-lg); - flex-wrap: wrap; -} -.footer-links a { - font-size: 0.875rem; - color: var(--color-ash); - transition: color var(--duration-fast) var(--ease-out); -} -.footer-links a:hover { - color: var(--color-accent); -} -.footer-author { - display: flex; - align-items: center; - justify-content: center; - gap: var(--spacing-md); - flex-wrap: wrap; - width: 100%; - padding-top: var(--spacing-lg); - margin-top: var(--spacing-md); - border-top: 1px solid var(--color-mist); -} -.footer-author-label { - font-size: 0.875rem; - color: var(--color-ash); -} -.footer-author-label a { - color: var(--color-text); - transition: color var(--duration-fast) var(--ease-out); -} -.footer-author-label a:hover { - color: var(--color-accent); -} -.footer-author-links { - display: flex; - align-items: center; - gap: var(--spacing-sm); -} -.footer-social-link { - display: flex; - align-items: center; - justify-content: center; - width: 36px; - height: 36px; - color: var(--color-ash); - background: transparent; - border-radius: 50%; - transition: all var(--duration-fast) var(--ease-out); - text-decoration: none; -} -.footer-social-link:hover { - color: var(--color-accent); - background: var(--color-accent-dim); -} -.footer-author-divider { - width: 1px; - height: 20px; - background: var(--color-mist); - margin: 0 var(--spacing-xs); -} -.footer-newsletter { - display: inline-flex; - align-items: center; - gap: 6px; - font-size: 0.875rem; - font-weight: 500; - color: var(--color-text); - text-decoration: none; - padding: 8px 14px; - background: var(--color-paper); - border: 1px solid var(--color-mist); - border-radius: 100px; - transition: all var(--duration-fast) var(--ease-out); -} -.footer-newsletter:hover { - border-color: var(--color-accent); - color: var(--color-accent); -} -.footer-newsletter svg { - transition: transform var(--duration-fast) var(--ease-out); -} -.footer-newsletter:hover svg { - transform: translateX(3px); -} -@media (max-width: 768px) { - .footer-author { - flex-direction: column; - gap: var(--spacing-sm); - } - .footer-author-divider { - display: none; - } -} -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: var(--spacing-xs); - padding: 1rem 2rem; - font-family: var(--font-body); - font-size: 0.9375rem; - font-weight: 600; - letter-spacing: 0.03em; - border: none; - cursor: pointer; - transition: all var(--duration-base) var(--ease-out); - position: relative; - overflow: hidden; - text-decoration: none; -} -.btn-primary { - background: var(--color-ink); - color: var(--color-paper); -} -.btn-primary::before { - content: ''; - position: absolute; - inset: 0; - background: var(--color-accent); - transform: translateY(100%); - transition: transform var(--duration-base) var(--ease-out); - z-index: 0; -} -.btn-primary:hover::before { - transform: translateY(0); -} -.btn-primary:hover { - color: var(--color-paper); -} -.btn-primary span, .btn-primary svg { - position: relative; - z-index: 1; -} -.btn-primary:not(:has(span)) { - position: relative; - z-index: 1; -} -.btn-secondary { - background: transparent; - color: var(--color-ink); - border: 1px solid var(--color-ink); -} -.btn-secondary:hover { - background: var(--color-ink); - color: var(--color-paper); -} -.btn:focus-visible { - outline: 2px solid var(--color-accent); - outline-offset: 2px; -} -.btn-primary:focus-visible { - outline-color: var(--color-paper); - box-shadow: 0 0 0 4px var(--color-accent); -} -.btn-secondary:focus-visible { - outline-color: var(--color-accent); -} -@keyframes revealUp { - to { - opacity: 1; - transform: translateY(0); - } -} -@keyframes fadeIn { - to { - opacity: 1; - } -} -@keyframes float { - 0%, 100% { - transform: translateX(-50%) translateY(0); - } - 50% { - transform: translateX(-50%) translateY(-8px); - } -} -@keyframes bounce { - 0%, 100% { - transform: translateY(0); - } - 50% { - transform: translateY(4px); - } -} -[data-reveal] { - opacity: 0; - transform: translateY(30px); - transition: opacity 0.8s var(--ease-out), transform 0.8s var(--ease-out); -} -[data-reveal].revealed { - opacity: 1; - transform: translateY(0); -} -[data-reveal]:nth-child(1) { - transition-delay: 0s; -} -[data-reveal]:nth-child(2) { - transition-delay: 0.1s; -} -[data-reveal]:nth-child(3) { - transition-delay: 0.2s; -} -[data-reveal]:nth-child(4) { - transition-delay: 0.3s; -} -@media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - scroll-behavior: auto !important; - } - html { - scroll-behavior: auto; - } - .hero-canvas { - display: none; - } - .hero-scroll-indicator { - animation: none; - opacity: 1; - } - [data-reveal] { - opacity: 1; - transform: none; - } - .gallery-frame { - opacity: 1; - transform: none; - } -} -.load-error { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; - padding: var(--spacing-2xl) var(--spacing-lg); - gap: var(--spacing-md); - background: var(--color-cream); - border: 1px solid var(--color-mist); - border-radius: 8px; -} -.load-error-icon { - font-size: 2.5rem; - color: var(--color-accent); -} -.load-error-title { - font-family: var(--font-display); - font-size: 1.5rem; - font-weight: 400; - color: var(--color-ink); - margin: 0; -} -.load-error-text { - font-size: 1rem; - color: var(--color-ash); - max-width: 40ch; - line-height: 1.5; -} -.load-error-retry { - margin-top: var(--spacing-sm); -} -.bias-tags { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--spacing-sm); - margin-top: var(--spacing-lg); -} -.bias-tags-label { - font-family: var(--font-mono); - font-size: 0.6875rem; - font-weight: 500; - letter-spacing: 0.1em; - text-transform: uppercase; - color: var(--color-ash); -} -.bias-tags-list { - display: flex; - flex-wrap: wrap; - justify-content: center; - gap: var(--spacing-xs); -} -.bias-tag { - font-size: 0.75rem; - font-weight: 500; - padding: 6px 12px; - background: var(--color-cream); - border: 1px solid var(--color-mist); - color: var(--color-charcoal); - transition: all var(--duration-fast) var(--ease-out); -} -.bias-tag:hover { - border-color: var(--color-accent); - color: var(--color-accent); -} -.antidote-section { - padding: var(--spacing-2xl) 0; - border-top: 1px solid var(--color-mist); -} -.patterns-categories { - margin-bottom: var(--spacing-xl); -} -.pattern-tabs { - display: flex; - flex-wrap: wrap; - gap: var(--spacing-xs); - margin-bottom: var(--spacing-lg); -} -.pattern-tab { - background: none; - border: none; - border-bottom: 2px solid transparent; - font-family: var(--font-body); - font-size: 0.875rem; - color: var(--color-ash); - padding: var(--spacing-xs) var(--spacing-sm); - cursor: pointer; - transition: color var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out); -} -.pattern-tab:hover { - color: var(--color-charcoal); -} -.pattern-tab.active { - color: var(--color-ink); - font-weight: 500; - border-bottom-color: var(--color-accent); -} -.pattern-tab:focus-visible { - outline: 2px solid var(--color-accent); - outline-offset: 2px; - border-radius: 4px; -} -.pattern-panel { - display: none; -} -.pattern-panel.active { - display: block; -} -.pattern-columns { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--spacing-xl); -} -@media (max-width: 768px) { - .pattern-columns { - grid-template-columns: 1fr; - gap: var(--spacing-md); - } -} -.pattern-column-label { - display: block; - font-size: 0.6875rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.1em; - margin-bottom: var(--spacing-sm); - color: var(--color-ash); -} -.pattern-column--anti .pattern-column-label { - color: var(--color-accent); -} -.pattern-column--do .pattern-column-label { - color: var(--color-success, #22c55e); -} -.pattern-list { - list-style: none; - padding: 0; - margin: 0; - display: flex; - flex-direction: column; - gap: var(--spacing-xs); -} -.pattern-item { - font-size: 0.8125rem; - padding-left: var(--spacing-md); - position: relative; - line-height: 1.5; -} -.pattern-item--anti { - color: var(--color-ash); -} -.pattern-item--anti::before { - content: '×'; - position: absolute; - left: 0; - color: var(--color-accent); - font-weight: 600; -} -.pattern-item--do { - color: var(--color-charcoal); -} -.pattern-item--do::before { - content: '✓'; - position: absolute; - left: 0; - color: var(--color-success, #22c55e); - font-weight: 600; -} -.contribute-inline { - font-size: 0.875rem; - color: var(--color-ash); - margin-top: var(--spacing-md); -} -.contribute-inline a { - color: var(--color-accent); - text-decoration: none; -} -.contribute-inline a:hover { - text-decoration: underline; -} -.pillar-item--main { - background: var(--color-accent-dim); - border: 1px solid var(--color-accent); -} -.pillar-item--main .pillar-item-name { - font-size: 1.125rem; - font-weight: 600; - color: var(--color-accent); -} -.pillar-item--ref { - background: transparent; - padding: var(--spacing-xs) var(--spacing-sm); -} -.pillar-item-label { - font-size: 0.75rem; - font-weight: 500; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--color-ash); -} -.pillar-refs { - display: flex; - flex-wrap: wrap; - gap: var(--spacing-xs); - padding: 0 var(--spacing-sm); -} -.pillar-ref { - font-size: 0.6875rem; - font-weight: 500; - text-transform: uppercase; - letter-spacing: 0.03em; - padding: 4px 10px; - background: var(--color-paper); - color: var(--color-ash); - border: 1px solid var(--color-mist); - border-radius: 3px; - transition: all var(--duration-fast) var(--ease-out); -} -.pillar-ref:hover { - border-color: var(--color-accent); - color: var(--color-accent); -} -.pillar-command-group { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: var(--spacing-xs); - padding: var(--spacing-sm); - background: var(--color-paper); - border-radius: 4px; -} -.pillar-group-label { - font-size: 0.6875rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--color-ash); - width: 100%; - margin-bottom: 4px; -} -.pillar-command-group .pillar-item-code { - font-size: 0.8125rem; - padding: 4px 8px; - background: var(--color-accent-dim); - border-radius: 3px; -} -.platforms-section { - padding: var(--spacing-2xl) 0; - border-top: 1px solid var(--color-mist); -} -.platforms-section .section-subtitle { - max-width: 60ch; -} -.install-steps { - display: flex; - flex-direction: column; - gap: var(--spacing-xl); - max-width: 640px; - margin: 0 auto var(--spacing-xl); -} -.install-step-header { - display: flex; - align-items: flex-start; - gap: var(--spacing-md); - margin-bottom: var(--spacing-md); -} -.install-step-number { - display: flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - flex-shrink: 0; - border-radius: 50%; - border: 1.5px solid var(--color-accent); - color: var(--color-accent); - font-family: var(--font-display); - font-size: 1.125rem; - font-weight: 600; - line-height: 1; -} -.install-step-meta { - padding-top: 6px; -} -.install-step-title { - font-size: 1.125rem; - font-weight: 600; - margin: 0; - line-height: 1.4; -} -.install-step-desc { - font-size: 0.875rem; - color: var(--color-ash); - margin: 2px 0 0; -} -.install-subscribe { - max-width: 640px; - margin: 0 auto; -} -.install-subscribe iframe { - display: block; - max-width: 100%; -} -.install-subscribe-btn { - display: inline-flex; - align-items: center; - gap: 10px; - font-size: 0.9375rem; - font-weight: 600; - letter-spacing: 0.03em; - color: var(--color-paper); - text-decoration: none; - padding: 1rem 2rem; - background: var(--color-ink); - border: 1px solid var(--color-ink); - transition: all var(--duration-base) var(--ease-out); - flex-shrink: 0; - position: relative; - overflow: hidden; -} -.install-subscribe-btn::before { - content: ''; - position: absolute; - inset: 0; - background: var(--color-accent); - transform: translateY(100%); - transition: transform var(--duration-base) var(--ease-out); - z-index: 0; -} -.install-subscribe-btn:hover::before { - transform: translateY(0); -} -.install-subscribe-btn svg, .install-subscribe-btn span { - position: relative; - z-index: 1; -} -.install-subscribe-btn:hover { - color: var(--color-paper); -} -.install-subscribe-note { - font-size: 0.8125rem; - color: var(--color-ash); - margin: 0; - line-height: 1.5; -} -@media (max-width: 600px) { - .install-subscribe { - flex-direction: column; - align-items: flex-start; - } -} -.install-terminal { - max-width: 640px; - margin: 0 auto; -} -.install-terminal .glass-terminal { - height: auto; -} -.install-terminal .terminal-body { - padding: 0; - display: flex; - flex-direction: column; -} -.install-terminal-row { - display: flex; - flex-direction: column; - gap: 6px; - padding: var(--spacing-md) var(--spacing-lg); -} -.install-terminal-label { - font-family: var(--font-mono); - font-size: 0.625rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.1em; - color: var(--color-ash); -} -.install-terminal-cmd { - display: flex; - align-items: center; - gap: var(--spacing-sm); -} -.install-terminal-cmd .terminal-prompt { - flex-shrink: 0; -} -.install-terminal-cmd code { - flex: 1; - font-family: var(--font-mono); - font-size: 0.9375rem; - color: var(--color-ink); - background: transparent; - padding: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} -.install-terminal-cmd .copy-btn { - flex-shrink: 0; -} -.install-terminal-cmd .btn { - padding: 0.5rem 1rem; - font-size: 0.8125rem; -} -.install-terminal-note { - font-size: 0.75rem; - color: var(--color-ash); - padding-left: calc(0.75rem + var(--spacing-sm)); -} -.install-terminal-note code { - font-family: var(--font-mono); - font-size: 0.6875rem; - background: var(--color-mist); - padding: 2px 5px; - border-radius: 3px; - color: var(--color-ink); -} -.install-terminal-divider { - height: 1px; - background: var(--color-mist); - margin: 0; -} -@media (max-width: 600px) { - .install-terminal-row { - padding: var(--spacing-sm) var(--spacing-md); - } - .install-terminal-cmd code { - font-size: 0.75rem; - } -} -.install-providers { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--spacing-sm); - margin-top: var(--spacing-md); -} -.install-providers-label { - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.1em; - color: var(--color-ash); -} -.install-providers-row { - display: flex; - flex-wrap: wrap; - justify-content: center; - gap: var(--spacing-sm); -} -.install-provider-badge { - display: flex; - align-items: center; - gap: 6px; - font-size: 0.8125rem; - color: var(--color-charcoal); -} -.install-provider-badge img { - border-radius: 4px; -} -.install-alternatives { - max-width: 640px; - margin: var(--spacing-sm) auto 0; -} -.install-alternatives-toggle { - font-family: var(--font-mono); - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--color-ash); - cursor: pointer; - padding: var(--spacing-sm) 0; - list-style: none; - display: flex; - align-items: center; - gap: 6px; - transition: color var(--duration-fast) var(--ease-out); -} -.install-alternatives-toggle::-webkit-details-marker { - display: none; -} -.install-alternatives-toggle::before { - content: ''; - display: inline-block; - width: 0; - height: 0; - border-left: 5px solid currentColor; - border-top: 4px solid transparent; - border-bottom: 4px solid transparent; - transition: transform var(--duration-fast) var(--ease-out); -} -.install-alternatives[open] .install-alternatives-toggle::before { - transform: rotate(90deg); -} -.install-alternatives-toggle:hover { - color: var(--color-charcoal); -} -.install-alternatives-content { - border: 1px solid var(--color-mist); - border-radius: 8px; - background: var(--color-paper); - overflow: hidden; - animation: altFadeIn 0.2s var(--ease-out); -} -@keyframes altFadeIn { - from { - opacity: 0; - transform: translateY(-4px); - } - to { - opacity: 1; - transform: translateY(0); - } -} -.install-alt-method { - padding: var(--spacing-md) var(--spacing-lg); -} -.install-alt-label { - display: block; - font-family: var(--font-mono); - font-size: 0.625rem; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.1em; - color: var(--color-ash); - margin-bottom: 6px; -} -.install-alt-method .install-terminal-cmd { - display: flex; - align-items: center; - gap: var(--spacing-sm); -} -.install-alt-method .terminal-prompt { - color: var(--color-accent); - font-family: var(--font-mono); - font-weight: bold; - user-select: none; -} -.install-alt-method code { - font-family: var(--font-mono); - font-size: 0.875rem; - color: var(--color-ink); -} -.install-alt-note { - display: block; - font-size: 0.75rem; - color: var(--color-ash); - margin-top: 4px; -} -.install-alt-note code { - font-family: var(--font-mono); - font-size: 0.6875rem; - background: var(--color-mist); - padding: 2px 5px; - border-radius: 3px; - color: var(--color-ink); -} -.install-alt-download { - display: flex; - align-items: center; - gap: var(--spacing-sm); -} -.install-alt-divider { - height: 1px; - background: var(--color-mist); -} -.install-prefix-options { - display: flex; - align-items: flex-start; - gap: var(--spacing-sm); - margin-top: var(--spacing-md); -} -.install-prefix-option { - flex: 1; - display: flex; - align-items: flex-start; - gap: 8px; - padding: 10px 12px; - border: 1.5px solid var(--color-mist); - border-radius: 8px; - cursor: pointer; - transition: all var(--duration-fast) var(--ease-out); - line-height: 1; -} -.install-prefix-option:hover { - border-color: var(--color-charcoal); -} -.install-prefix-option:has(input:checked) { - border-color: var(--color-accent); -} -.install-prefix-option input { - appearance: none; - -webkit-appearance: none; - width: 16px; - height: 16px; - border: 1.5px solid var(--color-mist); - border-radius: 50%; - flex-shrink: 0; - margin: 0; - transition: all var(--duration-fast) var(--ease-out); - position: relative; -} -.install-prefix-option input:checked { - border-color: var(--color-accent); - background: var(--color-accent); -} -.install-prefix-option input:checked::after { - content: ''; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 6px; - height: 6px; - background: var(--color-paper); - border-radius: 50%; -} -.install-prefix-option input:focus-visible { - outline: 2px solid var(--color-accent); - outline-offset: 2px; -} -.install-prefix-option-content { - display: flex; - flex-direction: column; - gap: 3px; - padding-top: 1px; -} -.install-prefix-option-label { - font-size: 0.8125rem; - font-weight: 600; - color: var(--color-ink); - line-height: 1; -} -.install-prefix-option-examples { - display: flex; - flex-wrap: wrap; - gap: 4px; -} -.install-prefix-option-examples code { - font-family: var(--font-mono); - font-size: 0.6875rem; - background: var(--color-accent-dim); - color: var(--color-accent); - padding: 2px 6px; - border-radius: 3px; -} -.install-prefix-option-wrap { - flex: 1; - display: flex; - flex-direction: column; - gap: 4px; - align-self: flex-start; -} -.install-prefix-reason { - font-size: 0.6875rem; - color: var(--color-ash); - line-height: 1.4; - padding-left: 2px; -} -.install-prefix-option-label code { - font-family: var(--font-mono); - font-size: 0.6875rem; - background: var(--color-accent-dim); - color: var(--color-accent); - padding: 1px 5px; - border-radius: 3px; -} -.install-alt-method .install-prefix-options { - margin-top: var(--spacing-sm); -} -.install-alt-method .install-alt-download { - margin-top: var(--spacing-sm); -} -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; -} -@media (max-width: 600px) { - .install-prefix-options { - flex-direction: column; - } - .install-alt-method { - padding: var(--spacing-sm) var(--spacing-md); - } - .install-alt-method code { - font-size: 0.75rem; - } -} -.has-tooltip { - position: relative; - cursor: default; -} -.has-tooltip::after { - content: attr(data-tooltip); - position: absolute; - bottom: calc(100% + 8px); - left: 50%; - transform: translateX(-50%); - padding: 6px 10px; - background: var(--color-ink); - color: var(--color-paper); - font-size: 0.6875rem; - line-height: 1.4; - border-radius: 6px; - white-space: nowrap; - pointer-events: none; - opacity: 0; - transition: opacity 0.15s ease; - z-index: 100; -} -.has-tooltip:hover::after { - opacity: 1; -} -.hero-logo-icon { - display: inline-flex; - align-items: center; -} -.download-tip { - font-size: 0.8125rem; - color: var(--color-ash); - margin-top: var(--spacing-sm); - text-align: center; -} -.download-tip a { - color: var(--color-accent); - text-decoration: none; -} -.download-tip a:hover { - text-decoration: underline; -} -.consulting-section { - padding: var(--spacing-xl) 0; - border-top: 1px solid var(--color-mist); -} -.consulting-content { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--spacing-lg); - flex-wrap: wrap; -} -.consulting-actions { - display: flex; - gap: var(--spacing-sm); - flex-shrink: 0; -} -.consulting-text { - flex: 1; - min-width: 280px; -} -.consulting-title { - font-size: clamp(1.5rem, 4vw, 2rem); - font-weight: 300; - font-style: italic; - margin: 0 0 var(--spacing-sm) 0; -} -.consulting-desc { - font-size: 1rem; - color: var(--color-charcoal); - line-height: 1.6; - margin: 0; - max-width: 45ch; -} -@media (max-width: 600px) { - .consulting-content { - flex-direction: column; - align-items: flex-start; - } - .consulting-actions { - flex-direction: column; - width: 100%; - } - .consulting-actions .btn { - width: 100%; - justify-content: center; - } -} diff --git a/public/css/sub-pages.css b/public/css/sub-pages.css new file mode 100644 index 000000000..13fd1ef85 --- /dev/null +++ b/public/css/sub-pages.css @@ -0,0 +1,2063 @@ +/* + * impeccable.style — shared styles for sub-pages and the site-wide header + * + * Imported by all generated pages under /skills, /anti-patterns, /tutorials, + * and by the hand-authored pages (index, cheatsheet, gallery, privacy) via + * a direct tag so they all share the same header + prose typography. + */ + +@import "./tokens.css"; + +/* ============================================ + SKIP LINK (a11y) + ============================================ */ + +.skip-link { + position: absolute; + top: -100%; + left: 50%; + transform: translateX(-50%); + z-index: 10000; + padding: var(--spacing-sm) var(--spacing-lg); + background: var(--color-ink); + color: var(--color-paper); + font-weight: 600; + text-decoration: none; + border-radius: 0 0 8px 8px; + transition: top 0.2s ease; +} + +.skip-link:focus { + top: 0; + outline: 2px solid var(--color-accent); + outline-offset: 2px; +} + +/* ============================================ + BASE + ============================================ */ + +html { + scroll-behavior: smooth; +} + +body { + font-family: var(--font-body); + color: var(--color-ink); + background: var(--color-paper); + line-height: 1.6; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; +} + +a { + color: inherit; + text-decoration: none; +} + +/* ============================================ + SHARED SITE HEADER + ============================================ */ + +.site-header { + position: sticky; + top: 0; + z-index: 100; + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--spacing-md); + padding: 14px clamp(var(--spacing-md), 4vw, var(--spacing-2xl)); + background: color-mix(in oklch, var(--color-paper) 94%, transparent); + -webkit-backdrop-filter: saturate(1.4) blur(16px); + backdrop-filter: saturate(1.4) blur(16px); + border-bottom: 1px solid var(--color-mist); +} + +/* Kill any inherited text-decoration from page-level `a` rules. */ +.site-header a, +.site-header a:hover { + text-decoration: none; + text-decoration-thickness: 0; +} + +.site-header-brand { + display: inline-flex; + align-items: center; + gap: 10px; + font-family: var(--font-display); + font-size: 1.25rem; + font-weight: 600; + letter-spacing: -0.01em; + color: var(--color-ink); + flex-shrink: 0; + transition: color var(--duration-fast) var(--ease-out); +} + +.site-header-brand:hover { + color: var(--color-accent); +} + +.site-header-brand-logo { + width: 26px; + height: 26px; + border-radius: 6px; + flex-shrink: 0; +} + +.site-header-brand-name { + display: inline-block; +} + +.site-header-right { + display: flex; + align-items: center; + gap: clamp(var(--spacing-sm), 2vw, var(--spacing-md)); + min-width: 0; +} + +.site-header-nav { + display: flex; + align-items: center; + gap: clamp(var(--spacing-sm), 2vw, var(--spacing-md)); + font-family: var(--font-body); + font-size: 0.9375rem; + font-weight: 500; + min-width: 0; +} + +.site-header-nav a { + position: relative; + display: inline-block; + white-space: nowrap; + color: var(--color-charcoal); + padding: 4px 0; + transition: color var(--duration-fast) var(--ease-out); +} + +.site-header-nav a:hover { + color: var(--color-accent); +} + +.site-header-nav a[aria-current="page"] { + color: var(--color-ink); + font-weight: 600; +} + +.site-header-nav a[aria-current="page"]::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: -4px; + height: 2px; + background: var(--color-accent); + border-radius: 2px; +} + +/* GitHub star pill — subtle, inviting nudge without shouting */ +.site-header-github { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px 4px 8px; + border-radius: 99px; + background: var(--color-cream); + border: 1px solid var(--color-mist); + font-family: var(--font-mono); + font-size: 0.8125rem; + font-weight: 500; + color: var(--color-charcoal); + flex-shrink: 0; + transition: + background var(--duration-fast) var(--ease-out), + border-color var(--duration-fast) var(--ease-out), + color var(--duration-fast) var(--ease-out); +} + +.site-header-github svg { + width: 14px; + height: 14px; + flex-shrink: 0; +} + +.site-header-github:hover { + background: var(--color-paper); + border-color: var(--color-ink); + color: var(--color-ink); +} + +.site-header-github-star { + color: #f59e0b; + width: 12px; + height: 12px; + flex-shrink: 0; +} + +@media (max-width: 820px) { + .site-header { + padding: 12px var(--spacing-md); + gap: var(--spacing-sm); + } + + .site-header-brand { + font-size: 1.0625rem; + } + + .site-header-brand-logo { + width: 22px; + height: 22px; + } + + .site-header-right { + gap: var(--spacing-sm); + } + + .site-header-nav { + font-size: 0.8125rem; + gap: var(--spacing-sm); + overflow-x: auto; + scrollbar-width: none; + -webkit-overflow-scrolling: touch; + /* fade the right edge so users see there's more to scroll */ + mask-image: linear-gradient(to right, black 0%, black calc(100% - 24px), transparent 100%); + -webkit-mask-image: linear-gradient(to right, black 0%, black calc(100% - 24px), transparent 100%); + } + + .site-header-nav::-webkit-scrollbar { + display: none; + } + + .site-header-nav a[data-nav="home"] { + /* Brand already acts as home link on mobile */ + display: none; + } + + .site-header-github { + padding: 4px 8px; + } + + .site-header-github-label { + display: none; + } +} + +@media (max-width: 480px) { + .site-header-brand-name { + /* Logo alone on narrow screens */ + display: none; + } +} + +/* ============================================ + SUB-PAGE LAYOUT SHELL + ============================================ */ + +main#main { + display: block; +} + +.sub-page-content { + max-width: 820px; + margin: 0 auto; + padding: clamp(2rem, 5vw, 4rem) clamp(1.25rem, 4vw, 2.5rem) 6rem; +} + +.skill-detail { + /* No max-width on the article itself. The editorial body sections + (header, editorial wrapper, skill source, references) each cap + themselves at 720px for readable line length, while the hero is + allowed to break out on wide viewports. */ + width: 100%; +} + +.sub-page-header { + margin-bottom: clamp(2.5rem, 6vw, 4rem); +} + +.sub-page-eyebrow { + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.14em; + color: var(--color-accent); + margin-bottom: var(--spacing-sm); +} + +.sub-page-title { + font-family: var(--font-display); + font-size: clamp(2.5rem, 6vw, 4.5rem); + font-weight: 400; + line-height: 1.05; + letter-spacing: -0.02em; + color: var(--color-ink); + margin-bottom: var(--spacing-md); +} + +.sub-page-lede { + font-size: clamp(1rem, 1.4vw, 1.125rem); + line-height: 1.55; + color: var(--color-charcoal); + max-width: 62ch; +} + +/* ============================================ + DOCS-BROWSER LAYOUT (used by /skills) + ============================================ */ + +:root { + --site-header-height: 62px; + --skills-sidebar-width: 200px; +} + +.skills-layout-page main#main { + /* Reset: the docs layout provides its own max-width container. */ + max-width: none; + margin: 0; + padding: 0; +} + +.skills-layout { + display: grid; + grid-template-columns: var(--skills-sidebar-width) minmax(0, 1fr); + gap: clamp(var(--spacing-lg), 4vw, var(--spacing-2xl)); + /* + * Match horizontal padding to .site-header so the sidebar content lines + * up with the brand logo in the nav bar above. Intentionally no max-width + * so the docs-browser feels wall-to-wall on wide screens. + */ + padding: 0 clamp(var(--spacing-md), 4vw, var(--spacing-2xl)); + align-items: start; +} + +/* ============================================ + SIDEBAR + ============================================ */ + +.skills-sidebar { + position: sticky; + top: var(--site-header-height); + align-self: start; + max-height: calc(100vh - var(--site-header-height)); + overflow-y: auto; + padding: var(--spacing-lg) 0 var(--spacing-2xl); + border-right: 1px solid var(--color-mist); + scrollbar-width: thin; + scrollbar-color: var(--color-mist) transparent; +} + +/* Desktop only: reserve full viewport height so the border-right + divider reaches the bottom of the viewport even on short pages. + On mobile this rule would leave a viewport-sized empty block + above the main content when the menu is collapsed. */ +@media (min-width: 921px) { + .skills-sidebar { + min-height: calc(100vh - var(--site-header-height)); + } +} + +.skills-sidebar::-webkit-scrollbar { + width: 6px; +} + +.skills-sidebar::-webkit-scrollbar-thumb { + background: var(--color-mist); + border-radius: 3px; +} + +.skills-sidebar-inner { + padding-right: var(--spacing-md); +} + +/* Mobile menu toggle. Hidden on desktop. */ +.skills-sidebar-toggle { + display: none; +} + +.skills-sidebar-toggle-chevron { + transition: transform var(--duration-base) var(--ease-out); + flex-shrink: 0; + color: var(--color-ash); +} + +/* Label at the top of the sidebar — hidden visually, kept for a11y tree. */ +.skills-sidebar-label { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} + +.skills-sidebar-group { + margin-bottom: 1.5rem; +} + +.skills-sidebar-group:last-child { + margin-bottom: 0; +} + +.skills-sidebar-divider { + border: none; + height: 1px; + background: var(--color-mist); + margin: 0 0 1.5rem 0; + width: 100%; +} + +.skills-sidebar-group-title { + font-family: var(--font-mono); + font-size: 0.625rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.14em; + color: var(--color-ash); + margin-bottom: 6px; + /* Match the link text's 14px left inset (2 border + 12 padding) so + category titles and link labels start at the same x position. */ + padding: 0 0 0 14px; +} + +.skills-sidebar-list { + list-style: none; + padding: 0; + margin: 0; +} + +.skills-sidebar-list li { + margin: 0; +} + +.skills-sidebar-list a { + display: block; + /* Border sits inside the normal flow (no negative margin) because + the sidebar's `overflow-y: auto` coerces overflow-x to auto too + per CSS spec, which would clip any border hanging left of the + column. Text is inset 14px (2 border + 12 padding) from the column + edge; group titles above use the same 14px inset. Accepts a small + misalignment with the header logo in exchange for a visible + active-state accent. */ + padding: 4px 0 4px 12px; + border-left: 2px solid transparent; + font-family: var(--font-mono); + font-size: 0.875rem; + font-weight: 500; + line-height: 1.5; + color: var(--color-charcoal); + text-decoration: none; + transition: color var(--duration-fast) var(--ease-out), + border-color var(--duration-fast) var(--ease-out), + background var(--duration-fast) var(--ease-out); +} + +.skills-sidebar-list a:hover { + color: var(--color-accent); + background: color-mix(in oklch, var(--color-accent-dim) 30%, transparent); +} + +.skills-sidebar-list a[aria-current="page"] { + color: var(--color-ink); + font-weight: 600; + border-left-color: var(--color-accent); +} + +.skills-sidebar-list a[aria-current="page"]:hover { + color: var(--color-ink); + background: transparent; +} + +/* ============================================ + OVERVIEW MAIN CONTENT + ============================================ */ + +.skills-main { + min-width: 0; + padding: clamp(2rem, 4vw, 3.5rem) 0 clamp(4rem, 8vw, 6rem); +} + +.skills-overview-content { + max-width: 720px; +} + +.skills-overview-header { + margin-bottom: clamp(2.5rem, 5vw, 4rem); +} + +.skills-overview-header .sub-page-lede a { + color: var(--color-ink); + text-decoration: underline; + text-decoration-thickness: 1px; + text-decoration-color: var(--color-accent); + text-underline-offset: 4px; + font-family: var(--font-mono); + font-weight: 500; +} + +.skills-overview-howto { + padding: var(--spacing-lg) var(--spacing-lg); + background: var(--color-cream); + border: 1px solid var(--color-mist); + border-radius: 10px; + margin-bottom: clamp(2.5rem, 5vw, 4rem); +} + +.skills-overview-howto-title { + font-family: var(--font-display); + font-size: 1.25rem; + font-style: italic; + font-weight: 500; + color: var(--color-ink); + margin-bottom: var(--spacing-sm); +} + +.skills-overview-howto p { + font-size: 0.9375rem; + line-height: 1.7; + color: var(--color-charcoal); + max-width: 60ch; +} + +.skills-overview-howto a { + color: var(--color-ink); + font-family: var(--font-mono); + font-size: 0.875em; + font-weight: 500; + text-decoration: none; + border-bottom: 1px solid var(--color-accent); + transition: color var(--duration-fast) var(--ease-out); +} + +.skills-overview-howto a:hover { + color: var(--color-accent); +} + +.skills-overview-categories { + display: flex; + flex-direction: column; + gap: clamp(2rem, 4vw, 3rem); +} + +.skills-overview-category { + padding-bottom: clamp(2rem, 4vw, 2.5rem); + border-bottom: 1px solid var(--color-mist); +} + +.skills-overview-category:last-child { + border-bottom: none; +} + +.skills-overview-category-meta { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: var(--spacing-md); + margin-bottom: 6px; +} + +.skills-overview-category-title { + 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; +} + +.skills-overview-category-count { + font-family: var(--font-mono); + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--color-ash); +} + +.skills-overview-category-desc { + font-size: 0.9375rem; + line-height: 1.7; + color: var(--color-charcoal); + max-width: 58ch; + margin-bottom: var(--spacing-md); +} + +.skills-overview-chips { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.skills-overview-chip { + display: inline-flex; + align-items: center; + padding: 6px 12px; + font-family: var(--font-mono); + font-size: 0.8125rem; + font-weight: 500; + color: var(--color-ink); + background: var(--color-paper); + border: 1px solid var(--color-mist); + border-radius: 99px; + text-decoration: none; + transition: border-color var(--duration-fast) var(--ease-out), + color var(--duration-fast) var(--ease-out), + background var(--duration-fast) var(--ease-out); +} + +.skills-overview-chip:hover { + color: var(--color-accent); + border-color: var(--color-accent); + background: var(--color-cream); +} + +/* ============================================ + ANTI-PATTERNS INDEX + ============================================ */ + +.anti-patterns-sidebar-list a { + display: flex !important; + align-items: baseline; + justify-content: space-between; + gap: var(--spacing-sm); +} + +.anti-patterns-sidebar-count { + font-size: 0.6875rem; + font-weight: 500; + color: var(--color-ash); + font-variant-numeric: tabular-nums; +} + +/* The anti-patterns page lets the grid of rule cards use the full + column width. Header, lede, and legend stay capped for line length. */ +.anti-patterns-content { + width: 100%; +} + +.anti-patterns-header { + max-width: 720px; + margin-bottom: clamp(2rem, 4vw, 3rem); +} + +.anti-patterns-header .sub-page-lede code { + font-family: var(--font-mono); + font-size: 0.875em; + color: var(--color-ink); + background: var(--color-cream); + padding: 2px 6px; + border-radius: 4px; + border: 1px solid var(--color-mist); +} + +.anti-patterns-header .sub-page-lede a { + color: var(--color-ink); + text-decoration: underline; + text-decoration-color: var(--color-accent); + text-decoration-thickness: 1px; + text-underline-offset: 4px; + font-family: var(--font-mono); + font-weight: 500; + font-size: 0.9375em; +} + +.anti-patterns-legend { + background: var(--color-cream); + border: 1px solid var(--color-mist); + border-radius: 10px; + margin-bottom: clamp(2rem, 4vw, 3rem); + max-width: 720px; +} + +.anti-patterns-legend-summary { + list-style: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--spacing-sm); + padding: 14px var(--spacing-lg); + transition: background var(--duration-fast) var(--ease-out); +} + +.anti-patterns-legend-summary::-webkit-details-marker { + display: none; +} + +.anti-patterns-legend-summary:hover { + background: color-mix(in oklch, var(--color-mist) 40%, transparent); +} + +.anti-patterns-legend-title { + font-family: var(--font-body); + font-size: 0.875rem; + font-weight: 600; + color: var(--color-ink); +} + +.anti-patterns-legend-chevron { + color: var(--color-ash); + transition: transform var(--duration-base) var(--ease-out); + flex-shrink: 0; +} + +.anti-patterns-legend[open] .anti-patterns-legend-chevron { + transform: rotate(180deg); +} + +.anti-patterns-legend-body { + padding: 0 var(--spacing-lg) var(--spacing-lg); + border-top: 1px solid var(--color-mist); + margin-top: 4px; + padding-top: var(--spacing-md); +} + +.anti-patterns-legend-body p { + font-size: 0.9375rem; + line-height: 1.7; + color: var(--color-charcoal); +} + +.anti-patterns-legend-body a { + color: var(--color-ink); + text-decoration: none; + border-bottom: 1px solid var(--color-accent); + font-family: var(--font-mono); + font-size: 0.875em; + font-weight: 500; +} + +.anti-patterns-legend-body a:hover { + color: var(--color-accent); +} + +.anti-patterns-sections { + display: flex; + flex-direction: column; + gap: clamp(3rem, 6vw, 4.5rem); +} + +.anti-patterns-section-header { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: var(--spacing-md); + padding-bottom: var(--spacing-sm); + border-bottom: 1px solid var(--color-mist); + margin-bottom: var(--spacing-lg); +} + +.anti-patterns-section-title { + font-family: var(--font-display); + font-size: clamp(1.75rem, 3vw, 2.25rem); + font-weight: 500; + font-style: italic; + color: var(--color-ink); + letter-spacing: -0.01em; +} + +.anti-patterns-section-count { + font-family: var(--font-mono); + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--color-ash); +} + +.rule-card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: var(--spacing-md); +} + +.rule-card { + background: var(--color-paper); + border: 1px solid var(--color-mist); + border-radius: 10px; + display: flex; + flex-direction: column; + overflow: hidden; + transition: border-color var(--duration-fast) var(--ease-out); +} + +.rule-card:hover { + border-color: var(--color-ash); +} + +/* Visual example preview at the top of each card. */ +.rule-card-visual { + position: relative; + height: 160px; + background: var(--color-cream); + border-bottom: 1px solid var(--color-mist); + overflow: hidden; +} + +.rule-card-visual-inner { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: var(--spacing-md); + /* Prevent the inline snippet's styles from bleeding outside the box. */ + overflow: hidden; +} + +.rule-card-body { + padding: var(--spacing-md); + display: flex; + flex-direction: column; + gap: 8px; + flex: 1; +} + +.rule-card-head { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 2px; + flex-wrap: wrap; +} + +.rule-card-category { + font-family: var(--font-mono); + font-size: 0.625rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.1em; + padding: 3px 8px; + border-radius: 99px; +} + +.rule-card-category[data-category="slop"] { + color: var(--color-accent); + background: var(--color-accent-dim); +} + +.rule-card-category[data-category="quality"] { + color: var(--color-charcoal); + background: var(--color-mist); +} + +/* Detection layer badge: CLI, Browser, or LLM only. */ +.rule-card-layer { + font-family: var(--font-mono); + font-size: 0.625rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.1em; + padding: 3px 8px; + border-radius: 99px; + border: 1px solid var(--color-mist); +} + +.rule-card-layer[data-layer="cli"] { + color: var(--color-charcoal); + border-color: var(--color-mist); + background: var(--color-paper); +} + +.rule-card-layer[data-layer="browser"] { + color: oklch(40% 0.12 230); + border-color: oklch(90% 0.05 230); + background: oklch(97% 0.02 230); +} + +.rule-card-layer[data-layer="llm"] { + color: oklch(45% 0.15 45); + border-color: oklch(92% 0.08 45); + background: oklch(98% 0.03 45); +} + +/* Layer legend dl inside the How-to-read block. */ +.anti-patterns-legend-layers { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); + margin-top: var(--spacing-md); +} + +.anti-patterns-legend-layers > div { + display: grid; + grid-template-columns: 90px 1fr; + gap: var(--spacing-md); + align-items: baseline; +} + +.anti-patterns-legend-layers dt { + margin: 0; +} + +.anti-patterns-legend-layers dd { + margin: 0; + font-size: 0.875rem; + line-height: 1.55; + color: var(--color-charcoal); +} + +.anti-patterns-legend-layers dd code { + font-family: var(--font-mono); + font-size: 0.8125rem; + background: var(--color-cream); + border: 1px solid var(--color-mist); + padding: 1px 6px; + border-radius: 4px; +} + +.rule-card-name { + font-family: var(--font-body); + font-size: 1rem; + font-weight: 600; + color: var(--color-ink); + line-height: 1.35; +} + +.rule-card-desc { + font-size: 0.875rem; + line-height: 1.6; + color: var(--color-charcoal); + flex: 1; +} + +.rule-card-skill-link { + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 500; + color: var(--color-charcoal); + text-decoration: none; + align-self: flex-start; + padding-top: 4px; + transition: color var(--duration-fast) var(--ease-out); +} + +.rule-card-skill-link:hover { + color: var(--color-accent); +} + +.rule-card-skill-link::after { + content: " →"; +} + +/* ============================================ + TUTORIALS INDEX + ============================================ */ + +.tutorials-content { + max-width: 720px; +} + +.tutorial-cards { + display: flex; + flex-direction: column; + gap: var(--spacing-sm); +} + +.tutorial-card { + display: grid; + grid-template-columns: auto 1fr auto; + gap: var(--spacing-md); + align-items: center; + padding: var(--spacing-md) var(--spacing-lg); + background: var(--color-paper); + border: 1px solid var(--color-mist); + border-radius: 10px; + color: var(--color-ink); + text-decoration: none; + transition: border-color var(--duration-fast) var(--ease-out), + transform var(--duration-fast) var(--ease-out); +} + +.tutorial-card:hover { + border-color: var(--color-ink); +} + +.tutorial-card-number { + font-family: var(--font-mono); + font-size: 1.5rem; + font-weight: 500; + color: var(--color-ash); + line-height: 1; + font-variant-numeric: tabular-nums; +} + +.tutorial-card:hover .tutorial-card-number { + color: var(--color-accent); +} + +.tutorial-card-body { + min-width: 0; +} + +.tutorial-card-title { + font-family: var(--font-display); + font-size: 1.375rem; + font-style: italic; + font-weight: 500; + color: var(--color-ink); + line-height: 1.2; + margin-bottom: 4px; +} + +.tutorial-card-tagline { + font-size: 0.9375rem; + color: var(--color-charcoal); + line-height: 1.5; +} + +.tutorial-card-arrow { + font-family: var(--font-body); + font-size: 1.25rem; + color: var(--color-ash); + transition: color var(--duration-fast) var(--ease-out), + transform var(--duration-fast) var(--ease-out); +} + +.tutorial-card:hover .tutorial-card-arrow { + color: var(--color-accent); + transform: translateX(4px); +} + +/* ============================================ + TUTORIAL EMBEDS (live overlays, screenshots) + ============================================ */ + +.tutorial-embed { + margin: var(--spacing-lg) 0 var(--spacing-xl); + border-radius: 10px; + overflow: hidden; + border: 1px solid var(--color-mist); + box-shadow: 0 8px 30px -6px rgba(0, 0, 0, 0.12); + max-width: none; +} + +.tutorial-embed-header { + display: flex; + align-items: center; + gap: 6px; + padding: 10px 14px; + background: var(--color-cream); + border-bottom: 1px solid var(--color-mist); +} + +.tutorial-embed-dot { + width: 10px; + height: 10px; + border-radius: 50%; + flex-shrink: 0; +} + +.tutorial-embed-dot.red { background: #ff5f56; } +.tutorial-embed-dot.yellow { background: #ffbd2e; } +.tutorial-embed-dot.green { background: #27c93f; } + +.tutorial-embed-title { + margin-left: auto; + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--color-ash); +} + +.tutorial-embed-iframe { + display: block; + width: 100%; + height: 520px; + border: none; + background: white; +} + +.tutorial-embed-caption { + font-size: 0.8125rem; + color: var(--color-ash); + margin-top: var(--spacing-sm); + font-style: italic; + max-width: 60ch; +} + +/* ============================================ + TUTORIAL DETAIL + ============================================ */ + +.tutorial-detail { + max-width: 680px; +} + +.tutorial-detail-header { + margin-bottom: clamp(2.5rem, 5vw, 3.5rem); +} + +.tutorial-detail-title { + font-family: var(--font-display); + font-size: clamp(2.5rem, 6vw, 4.5rem); + font-weight: 400; + line-height: 1.05; + letter-spacing: -0.02em; + color: var(--color-ink); + margin-bottom: var(--spacing-md); +} + +.tutorial-detail-tagline { + font-family: var(--font-body); + font-size: clamp(1.0625rem, 1.5vw, 1.1875rem); + line-height: 1.55; + color: var(--color-charcoal); + max-width: 60ch; +} + +.tutorial-detail-body { + max-width: 65ch; +} + +/* ============================================ + VISUAL MODE PAGE + ============================================ */ + +.visual-mode-page-body main#main { + /* Reset the docs-layout override; this page uses its own max-width. */ + max-width: none; + margin: 0; + padding: 0; +} + +.visual-mode-page { + max-width: 1100px; + margin: 0 auto; + padding: clamp(2rem, 4vw, 3.5rem) clamp(var(--spacing-md), 4vw, var(--spacing-2xl)) clamp(4rem, 8vw, 6rem); +} + +.visual-mode-page-header { + max-width: 720px; + margin-bottom: clamp(2.5rem, 5vw, 3.5rem); +} + +.visual-mode-demo-wrap { + margin-bottom: clamp(3rem, 6vw, 4.5rem); +} + +.visual-mode-demo-caption { + font-size: 0.8125rem; + color: var(--color-ash); + font-style: italic; + margin-top: var(--spacing-sm); + text-align: center; +} + +/* Mac-window chrome around the iframe. Scoped to .visual-mode-page so + the sub-page's preview styles don't leak onto the homepage, which + carries its own .visual-mode-preview rules in main.css. The auto + margins below would disable grid-item stretch on the homepage and + collapse the preview to the iframe's 300px intrinsic width. */ +.visual-mode-page .visual-mode-preview { + border-radius: 10px; + overflow: hidden; + border: 1px solid var(--color-mist); + box-shadow: 0 12px 40px -8px rgba(0, 0, 0, 0.12); + max-width: 1040px; + margin: 0 auto; +} + +.visual-mode-page .visual-mode-preview-header { + display: flex; + align-items: center; + gap: 6px; + padding: 10px 14px; + background: var(--color-cream); + border-bottom: 1px solid var(--color-mist); +} + +.visual-mode-page .visual-mode-preview-dot { + width: 10px; + height: 10px; + border-radius: 50%; + flex-shrink: 0; +} + +.visual-mode-page .visual-mode-preview-dot.red { background: #ff5f56; } +.visual-mode-page .visual-mode-preview-dot.yellow { background: #ffbd2e; } +.visual-mode-page .visual-mode-preview-dot.green { background: #27c93f; } + +.visual-mode-page .visual-mode-preview-title { + margin-left: auto; + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--color-ash); +} + +.visual-mode-frame { + display: block; + width: 100%; + height: 580px; + border: none; + background: white; +} + +/* Three invocation methods. */ +.visual-mode-methods { + margin-bottom: clamp(3rem, 6vw, 4.5rem); +} + +.visual-mode-methods-title { + font-family: var(--font-display); + font-size: clamp(1.75rem, 3vw, 2.25rem); + font-weight: 500; + font-style: italic; + color: var(--color-ink); + letter-spacing: -0.01em; + margin-bottom: var(--spacing-lg); +} + +.visual-mode-methods-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: var(--spacing-md); +} + +.visual-mode-method { + padding: var(--spacing-lg); + background: var(--color-paper); + border: 1px solid var(--color-mist); + border-radius: 10px; + display: flex; + flex-direction: column; + gap: 8px; + transition: border-color var(--duration-fast) var(--ease-out); +} + +.visual-mode-method:hover { + border-color: var(--color-ash); +} + +.visual-mode-method[data-coming-soon] { + background: var(--color-cream); +} + +.visual-mode-method-label { + font-family: var(--font-mono); + font-size: 0.625rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.14em; + color: var(--color-accent); +} + +.visual-mode-method-name { + font-family: var(--font-display); + font-size: 1.375rem; + font-style: italic; + font-weight: 500; + color: var(--color-ink); + line-height: 1.25; +} + +.visual-mode-method-name a { + color: inherit; + text-decoration: none; + border-bottom: 1px solid var(--color-accent); +} + +.visual-mode-method-name a:hover { + color: var(--color-accent); +} + +.visual-mode-method-name code { + font-family: var(--font-mono); + font-size: 0.875em; + font-style: normal; + background: var(--color-cream); + border: 1px solid var(--color-mist); + padding: 2px 8px; + border-radius: 4px; + color: var(--color-ink); +} + +.visual-mode-method-desc { + font-size: 0.9375rem; + line-height: 1.6; + color: var(--color-charcoal); + flex: 1; +} + +.visual-mode-method-desc a { + color: var(--color-ink); + text-decoration: none; + border-bottom: 1px solid var(--color-accent); + font-family: var(--font-mono); + font-size: 0.875em; + font-weight: 500; +} + +.visual-mode-method-desc a:hover { + color: var(--color-accent); +} + +/* Gallery (specimens) section */ +.visual-mode-gallery-header { + margin-bottom: var(--spacing-lg); +} + +.visual-mode-gallery-title { + font-family: var(--font-display); + font-size: clamp(1.75rem, 3vw, 2.25rem); + font-weight: 500; + font-style: italic; + color: var(--color-ink); + letter-spacing: -0.01em; + margin-bottom: var(--spacing-sm); +} + +.visual-mode-gallery-lede { + font-size: 0.9375rem; + line-height: 1.6; + color: var(--color-charcoal); + max-width: 60ch; +} + +/* ============================================ + ANTI-PATTERNS: IN THE WILD (merged gallery) + ============================================ */ + +.gallery-section { + margin-top: clamp(3rem, 6vw, 4.5rem); +} + +.gallery-section-lede { + font-size: 0.9375rem; + line-height: 1.6; + color: var(--color-charcoal); + max-width: 60ch; + margin-bottom: var(--spacing-lg); +} + +.gallery-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: var(--spacing-md); +} + +.gallery-card { + display: flex; + flex-direction: column; + background: var(--color-paper); + border: 1px solid var(--color-mist); + border-radius: 10px; + overflow: hidden; + text-decoration: none; + color: inherit; + transition: border-color var(--duration-fast) var(--ease-out), + transform var(--duration-fast) var(--ease-out); +} + +.gallery-card:hover { + border-color: var(--color-ink); + transform: translateY(-2px); +} + +.gallery-card-thumb { + aspect-ratio: 1; + overflow: hidden; + background: var(--color-cream); + border-bottom: 1px solid var(--color-mist); +} + +.gallery-card-thumb img { + width: 100%; + height: 100%; + object-fit: cover; + object-position: top left; + display: block; +} + +.gallery-card-body { + padding: var(--spacing-md); + display: flex; + flex-direction: column; + gap: 6px; + flex: 1; +} + +.gallery-card-title { + font-family: var(--font-display); + font-size: 1.125rem; + font-style: italic; + font-weight: 500; + color: var(--color-ink); + line-height: 1.25; +} + +.gallery-card:hover .gallery-card-title { + color: var(--color-accent); +} + +.gallery-card-desc { + font-size: 0.8125rem; + line-height: 1.55; + color: var(--color-charcoal); +} + +/* ============================================ + MOBILE: collapsible sidebar behind a toggle + ============================================ */ + +@media (max-width: 920px) { + .skills-layout { + grid-template-columns: 1fr; + gap: 0; + } + + .skills-sidebar { + position: static; + max-height: none; + overflow: visible; + border-right: none; + padding: var(--spacing-md) 0; + margin-bottom: var(--spacing-lg); + } + + /* Show the toggle button and collapse the menu by default. */ + .skills-sidebar-toggle { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--spacing-sm); + width: 100%; + padding: 12px 16px; + background: var(--color-cream); + border: 1px solid var(--color-mist); + border-radius: 8px; + cursor: pointer; + font-family: var(--font-mono); + font-size: 0.8125rem; + font-weight: 600; + color: var(--color-ink); + text-align: left; + transition: border-color var(--duration-fast) var(--ease-out); + } + + .skills-sidebar-toggle:hover { + border-color: var(--color-ink); + } + + .skills-sidebar-toggle[aria-expanded="true"] .skills-sidebar-toggle-chevron { + transform: rotate(180deg); + } + + .skills-sidebar-inner { + display: none; + padding-right: 0; + padding-top: var(--spacing-md); + border-top: 1px solid var(--color-mist); + margin-top: var(--spacing-md); + } + + .skills-sidebar-toggle[aria-expanded="true"] + .skills-sidebar-inner { + display: block; + } + + .skills-sidebar-group { + margin-bottom: var(--spacing-md); + } +} + +/* ============================================ + SKILL DETAIL — BEFORE/AFTER DEMO + ============================================ */ + +/* Ported from main.css for use on sub-pages. The split-compare effect + is initialized by js/effects/split-compare.js loaded on demand. */ + +.split-comparison { + position: relative; + width: 100%; + /* 500px visible + 64px buffer (32px per side) = 564px total. + box-sizing: border-box is inherited, so children sit in 500px. */ + max-width: 564px; + /* 32px padding is an invisible hover buffer so the divider does not + immediately snap back when the pointer grazes the visible box + edge. Negative top/bottom margins collapse the padding out of + layout flow so the demo keeps its rhythm in the page. */ + padding: 32px; + margin: -32px 0 calc(clamp(2rem, 4vw, 3rem) - 32px); +} + +.split-container { + position: relative; + width: 100%; + height: 360px; + border-radius: 12px; + overflow: hidden; + /* Match the page background so the 32px padding area is invisible. + The border + border-radius define the visible box; individual + demo content provides its own colors on top. */ + background: var(--color-paper); + border: 1px solid var(--color-mist); + cursor: ew-resize; + user-select: none; +} + +.split-before, +.split-after { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.split-before { + z-index: 1; +} + +.split-content { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + /* No padding: demos that fill the container (overdrive) need full + bleed, and demos with smaller content (polish, bolder) already + center themselves via flex. */ +} + +.split-after { + clip-path: polygon(58% 0%, 100% 0%, 100% 100%, 42% 100%); + z-index: 2; + background: var(--color-paper); +} + +.split-divider { + position: absolute; + top: 0; + bottom: 0; + left: 50%; + width: 3px; + background: var(--color-accent); + transform: translateX(-50%) skewX(-10deg); + pointer-events: none; + z-index: 3; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.15); +} + +/* Before | caption | After all on one row. The caption lives in the + middle column; if a skill has no caption we emit an empty so + the grid still has three cells and Before/After sit at the edges. */ +.split-labels { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: baseline; + gap: var(--spacing-md); + margin-top: 10px; + font-family: var(--font-mono); + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--color-ash); +} + +.split-label-item[data-point="before"] { + color: var(--color-ash); + justify-self: start; +} + +.split-label-item[data-point="after"] { + color: var(--color-accent); + justify-self: end; +} + +.skill-demo-caption { + margin: 0; + font-family: var(--font-body); + font-size: 0.8125rem; + font-weight: 400; + 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. */ + max-width: 100%; +} + +.skill-demo-eyebrow { + font-family: var(--font-mono); + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.14em; + color: var(--color-ash); + margin-bottom: var(--spacing-sm); + /* Eyebrow now lives inside .split-comparison (which has 32px + padding), so it aligns with the visible card edge automatically. */ +} + +/* ============================================ + SKILL DETAIL + ============================================ */ + +/* Editorial hero: on wide viewports, text header on the left and the + before/after demo floats to the right as a hero module and is allowed + to break out of the 720px body cap. On narrow viewports everything + stacks within the 720px column. */ +.skill-detail-hero { + max-width: 720px; + margin-bottom: clamp(2.5rem, 5vw, 3.5rem); +} + +/* Stacked spacing between header and demo on narrow viewports; cancelled + when the hero switches to two columns at >=1280px. */ +.skill-detail-hero--has-demo .skill-demo { + margin-top: clamp(2rem, 4vw, 2.5rem); +} + +@media (min-width: 1280px) { + .skill-detail-hero--has-demo { + /* Wider editorial hero at large viewports. Fixed 564px demo column + (500px visible + 64px hover buffer) keeps the split-container at + its designed 500x360 landscape aspect ratio. Text column takes + whatever's left, clamped by the hero's own max-width. */ + max-width: 1200px; + display: grid; + grid-template-columns: minmax(0, 1fr) 564px; + gap: clamp(var(--spacing-xl), 4vw, var(--spacing-2xl)); + align-items: center; + } + + .skill-detail-hero--has-demo .skill-detail-header { + margin-bottom: 0; + } + + .skill-detail-hero--has-demo .skill-demo { + align-self: center; + margin-top: 0; + } +} + +/* Body sections stay at a readable line length regardless of the hero + width above them. */ +.skill-detail-editorial, +.skill-source-card, +.skill-references { + max-width: 720px; +} + +.skill-detail-eyebrow { + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--color-ash); + margin-bottom: var(--spacing-sm); +} + +.skill-detail-eyebrow a { + color: inherit; + text-decoration: none; +} + +.skill-detail-eyebrow a:hover { + color: var(--color-accent); +} + +.skill-detail-title { + font-family: var(--font-display); + font-size: clamp(3rem, 5.5vw, 5rem); + font-weight: 600; + line-height: 0.95; + letter-spacing: -0.02em; + color: var(--color-ink); + margin-bottom: var(--spacing-md); + white-space: nowrap; +} + +.skill-detail-title-slash { + color: var(--color-accent); + font-weight: 300; +} + +.skill-detail-tagline { + font-family: var(--font-body); + font-size: clamp(1rem, 1.4vw, 1.125rem); + font-weight: 400; + color: var(--color-charcoal); + line-height: 1.55; + max-width: 60ch; + margin-bottom: var(--spacing-md); +} + +.skill-meta-strip { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: var(--spacing-sm); +} + +.skill-meta-chip { + display: inline-flex; + align-items: center; + font-family: var(--font-mono); + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + padding: 4px 10px; + border-radius: 99px; + background: var(--color-cream); + border: 1px solid var(--color-mist); + color: var(--color-charcoal); +} + +.skill-meta-category[data-category="create"] { + background: var(--cat-create-bg); + border-color: var(--cat-create-border); + color: var(--cat-create-text); +} + +.skill-meta-category[data-category="evaluate"] { + background: var(--cat-evaluate-bg); + border-color: var(--cat-evaluate-border); + color: var(--cat-evaluate-text); +} + +.skill-meta-category[data-category="refine"] { + background: var(--cat-refine-bg); + border-color: var(--cat-refine-border); + color: var(--cat-refine-text); +} + +.skill-meta-category[data-category="simplify"] { + background: var(--cat-simplify-bg); + border-color: var(--cat-simplify-border); + color: var(--cat-simplify-text); +} + +.skill-meta-category[data-category="harden"] { + background: var(--cat-harden-bg); + border-color: var(--cat-harden-border); + color: var(--cat-harden-text); +} + +.skill-meta-category[data-category="system"] { + background: var(--cat-system-bg); + border-color: var(--cat-system-border); + color: var(--cat-system-text); +} + +.skill-meta-args { + font-family: var(--font-mono); + text-transform: none; + letter-spacing: 0; + font-weight: 500; +} + +.skill-detail-editorial { + margin-bottom: clamp(2rem, 4vw, 3rem); +} + +/* "The skill itself" card: visually contains the auto-rendered SKILL.md + body so it reads as a distinct reference block, not a continuation of + the editorial section above. */ +.skill-source-card { + background: var(--color-paper); + border: 1px solid var(--color-mist); + border-radius: 12px; + padding: clamp(var(--spacing-md), 3vw, var(--spacing-xl)); + margin-top: clamp(2rem, 4vw, 3rem); + box-shadow: 0 1px 0 oklch(90% 0 0); +} + +.skill-source-card-header { + display: flex; + align-items: baseline; + flex-wrap: wrap; + gap: var(--spacing-sm); + padding-bottom: var(--spacing-md); + margin-bottom: var(--spacing-md); + border-bottom: 1px solid var(--color-mist); +} + +.skill-source-card-label { + font-family: var(--font-mono); + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--color-accent); + padding: 3px 8px; + background: var(--color-accent-dim); + border-radius: 4px; + flex-shrink: 0; +} + +.skill-source-card-subtitle { + font-size: 0.8125rem; + color: var(--color-ash); + font-style: italic; +} + +.skill-source-card-body { + max-width: none; +} + +.skill-source-card-body > :first-child { + margin-top: 0; +} + +.skill-source-card-body > :last-child { + margin-bottom: 0; +} + +.skill-references { + margin-top: clamp(3rem, 6vw, 4.5rem); + padding-top: clamp(2rem, 4vw, 3rem); + border-top: 1px solid var(--color-mist); +} + +.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); +} + +.skill-reference { + border-top: 1px solid var(--color-mist); +} + +.skill-reference:last-child { + border-bottom: 1px solid var(--color-mist); +} + +.skill-reference > summary { + list-style: none; + cursor: pointer; + padding: 16px 0; + display: flex; + align-items: center; + gap: var(--spacing-md); + transition: color var(--duration-fast) var(--ease-out); +} + +.skill-reference > summary::-webkit-details-marker { + display: none; +} + +.skill-reference > summary::before { + content: "+"; + flex-shrink: 0; + width: 18px; + font-family: var(--font-display); + font-size: 1.5rem; + color: var(--color-accent); + line-height: 1; + transition: transform var(--duration-base) var(--ease-out); +} + +.skill-reference[open] > summary::before { + transform: rotate(45deg); +} + +.skill-reference > summary:hover { + color: var(--color-accent); +} + +.skill-reference-label { + font-family: var(--font-mono); + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--color-ash); + flex-shrink: 0; +} + +.skill-reference-title { + font-family: var(--font-display); + font-size: 1.125rem; + font-style: italic; + color: var(--color-ink); +} + +.skill-reference-body { + padding: var(--spacing-sm) 0 var(--spacing-md) 34px; + max-width: 62ch; +} + +/* ============================================ + PROSE — rendered markdown bodies + ============================================ */ + +.prose { + font-size: 1rem; + line-height: 1.7; + color: var(--color-charcoal); + max-width: 65ch; +} + +.prose h1, +.prose h2, +.prose h3, +.prose h4 { + color: var(--color-ink); + font-weight: 600; + line-height: 1.25; + margin-top: 2em; + 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 h3 { font-size: 1.125rem; margin-top: 1.8em; } +.prose h4 { font-size: 1rem; } + +.prose h2:first-child, +.prose h3:first-child { + margin-top: 0; +} + +.prose p { + margin-top: 0; + margin-bottom: 1.1em; +} + +.prose ul, +.prose ol { + margin: 0 0 1.2em 0; + padding-left: 1.25rem; +} + +.prose li { + margin-bottom: 0.4em; +} + +.prose a { + color: var(--color-accent); + text-decoration: underline; + text-decoration-thickness: 1px; + text-underline-offset: 3px; + text-decoration-color: var(--color-accent-dim); + transition: text-decoration-color var(--duration-fast) var(--ease-out); +} + +.prose a:hover { + text-decoration-color: var(--color-accent); +} + +.prose strong { + color: var(--color-ink); + font-weight: 600; +} + +.prose em { + font-style: italic; +} + +.prose code { + font-family: var(--font-mono); + font-size: 0.875em; + background: var(--color-cream); + border: 1px solid var(--color-mist); + padding: 2px 6px; + border-radius: 4px; + color: var(--color-ink); +} + +.prose .code-block-wrap { + position: relative; + margin: 1.25em 0; +} + +.prose .code-block-wrap .code-block { + margin: 0; +} + +.prose .code-block { + padding: var(--spacing-md); + background: oklch(12% 0.005 350); + color: oklch(92% 0.005 350); + border-radius: 10px; + overflow-x: auto; + font-family: var(--font-mono); + font-size: 0.8125rem; + line-height: 1.55; + border: 1px solid oklch(20% 0.005 350); +} + +.code-block-copy { + position: absolute; + top: 10px; + right: 10px; + padding: 4px 10px; + background: oklch(20% 0.005 350); + border: 1px solid oklch(30% 0.005 350); + color: oklch(78% 0.005 350); + border-radius: 4px; + font-family: var(--font-mono); + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + cursor: pointer; + opacity: 0; + transition: + opacity var(--duration-fast) 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); +} + +.code-block-wrap:hover .code-block-copy, +.code-block-copy:focus-visible { + opacity: 1; +} + +.code-block-copy::before { + content: "Copy"; +} + +.code-block-copy:hover { + background: oklch(30% 0.005 350); + color: oklch(92% 0.005 350); + border-color: oklch(40% 0.005 350); +} + +.code-block-copy.is-copied { + opacity: 1; + background: var(--color-accent); + color: var(--color-paper); + border-color: var(--color-accent); +} + +.code-block-copy.is-copied::before { + content: "Copied"; +} + +.prose .code-block code { + background: transparent; + border: none; + padding: 0; + color: inherit; + font-size: inherit; +} + +.prose blockquote { + margin: 1.5em 0; + padding: 0 0 0 var(--spacing-md); + border-left: 3px solid var(--color-mist); + color: var(--color-ash); + font-style: italic; +} + +.prose hr { + border: none; + height: 1px; + background: var(--color-mist); + margin: 2.5em 0; +} diff --git a/public/css/tokens.css b/public/css/tokens.css new file mode 100644 index 000000000..e5517adac --- /dev/null +++ b/public/css/tokens.css @@ -0,0 +1,102 @@ +/* + * impeccable.style — design tokens + minimal reset + * + * Shared by every page on the site (landing + generated sub-pages). + * Keep this file small and dependency-free. + */ + +/* ============================================ + MINIMAL CSS RESET + ============================================ */ + +*, *::before, *::after { + box-sizing: border-box; +} + +* { + margin: 0; +} + +img, picture, video, canvas, svg { + display: block; + max-width: 100%; +} + +button, input, textarea, select { + font: inherit; +} + +/* ============================================ + DESIGN TOKENS + ============================================ */ + +:root { + /* Typography */ + --font-display: 'Cormorant Garamond', Georgia, serif; + --font-body: 'Instrument Sans', system-ui, sans-serif; + --font-mono: 'Space Grotesk', monospace; + + /* Spacing Scale */ + --spacing-xs: 8px; + --spacing-sm: 16px; + --spacing-md: 24px; + --spacing-lg: 32px; + --spacing-xl: 48px; + --spacing-2xl: 80px; + --spacing-3xl: 120px; + + /* Width */ + --width-max: 1400px; + --width-content: 900px; + + /* Animation */ + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); + --ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1); + --duration-fast: 0.15s; + --duration-base: 0.3s; + --duration-slow: 0.6s; + --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); + + /* 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); + + /* Framework category colors - Light mode */ + --cat-create-bg: #fdf2f8; + --cat-create-border: #ec4899; + --cat-create-text: #be185d; + + --cat-evaluate-bg: #fdf4ff; + --cat-evaluate-border: #d946ef; + --cat-evaluate-text: #a21caf; + + --cat-refine-bg: #eff6ff; + --cat-refine-border: #3b82f6; + --cat-refine-text: #1d4ed8; + + --cat-simplify-bg: #fffbeb; + --cat-simplify-border: #f59e0b; + --cat-simplify-text: #b45309; + + --cat-harden-bg: #f0fdf4; + --cat-harden-border: #22c55e; + --cat-harden-text: #15803d; + + --cat-system-bg: #f5f5f4; + --cat-system-border: #78716c; + --cat-system-text: #44403c; +} diff --git a/public/css/workflow.css b/public/css/workflow.css index f75d90171..8dfa984dd 100644 --- a/public/css/workflow.css +++ b/public/css/workflow.css @@ -8,9 +8,351 @@ .commands-gallery { /* Reset grid */ - display: block; + display: block; } +/* ============================================ + MAGAZINE SPREAD - Desktop Layout + ============================================ */ + +/* Category accent colors */ +.magazine-spread[data-category="create"] { --spread-accent: oklch(55% 0.20 340); } +.magazine-spread[data-category="evaluate"] { --spread-accent: oklch(55% 0.20 300); } +.magazine-spread[data-category="refine"] { --spread-accent: oklch(55% 0.18 260); } +.magazine-spread[data-category="simplify"] { --spread-accent: oklch(55% 0.20 80); } +.magazine-spread[data-category="harden"] { --spread-accent: oklch(55% 0.20 160); } +.magazine-spread[data-category="system"] { --spread-accent: oklch(45% 0.05 0); } + +.magazine-container { + position: relative; + width: 100%; + display: grid; + grid-template-columns: 200px 1fr; + gap: var(--spacing-lg); + align-items: start; + overflow: hidden; +} + +@media (min-width: 1100px) { + .magazine-container { + background: white; + border-radius: 16px; + padding: var(--spacing-lg); + } +} + +/* Fisheye command list */ +.fisheye-list { + position: relative; + height: 400px; + align-self: center; + overflow: hidden; + mask-image: linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%); + -webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%); +} + +.fisheye-scroll { + height: 100%; + overflow-y: auto; + scrollbar-width: none; + position: relative; +} + +.fisheye-scroll::-webkit-scrollbar { display: none; } + +.fisheye-item { + display: block; + font-family: var(--font-display); + font-size: 1.5rem; + font-weight: 400; + color: var(--color-ink); + background: none; + border: none; + padding: 0; + position: absolute; + left: 0; + cursor: pointer; + white-space: nowrap; + text-align: left; + width: 100%; + line-height: 1.3; + transform-origin: left center; + will-change: transform, opacity; +} + +.fisheye-slash { + color: var(--color-mist); + font-weight: 300; +} + +.fisheye-item.is-active { + font-weight: 600; +} + +.fisheye-item.is-active .fisheye-slash { + color: var(--color-accent); +} + + +.fisheye-beta { + font-family: var(--font-body); + font-size: 0.45rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--color-ash); + border: 1px solid var(--color-mist); + border-radius: 3px; + padding: 1px 4px; + margin-left: 0.35em; + vertical-align: middle; +} + +/* The viewport that clips to one spread at a time */ +.magazine-viewport { + position: relative; + height: 520px; + overflow: hidden; +} + +/* Individual spread */ +.magazine-spread { + position: absolute; + inset: 0; + display: grid; + grid-template-columns: 38% 1fr; + grid-template-rows: 1fr auto; + gap: 0 var(--spacing-2xl); + padding: var(--spacing-lg) var(--spacing-lg); + opacity: 0; + pointer-events: none; + transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), + transform 0.5s cubic-bezier(0.16, 1, 0.3, 1); + transform: translateY(16px); + will-change: opacity, transform; +} + +.magazine-spread.active { + opacity: 1; + pointer-events: auto; + transform: translateY(0); + z-index: 2; +} + +.magazine-spread.exiting { + opacity: 0; + transform: translateY(-16px); + z-index: 1; +} + +/* Left column: Big name + meta */ +.spread-identity { + grid-column: 1; + grid-row: 1 / 3; + display: flex; + flex-direction: column; + justify-content: center; + gap: var(--spacing-md); + padding-right: var(--spacing-lg); +} + +.spread-category-label { + font-family: var(--font-body); + font-size: 0.6875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.14em; + color: var(--spread-accent); +} + +.spread-command-name { + white-space: nowrap; + font-family: var(--font-display); + font-size: clamp(3rem, 5.5vw, 5.5rem); + font-weight: 600; + line-height: 0.95; + color: var(--color-ink); + margin: 0; + letter-spacing: -0.02em; +} + +.spread-command-name .spread-slash { + color: var(--spread-accent); + font-weight: 300; +} + +.spread-command-name .beta-badge { + font-family: var(--font-body); + font-size: 0.55rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--spread-accent); + border: 1px solid var(--spread-accent); + border-radius: 3px; + padding: 2px 6px; + vertical-align: super; + margin-left: 8px; +} + +.spread-description { + font-family: var(--font-body); + font-size: 0.9375rem; + line-height: 1.6; + color: var(--color-charcoal); + max-width: 360px; +} + +/* Flow / relationship line */ +.spread-flow { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: nowrap; + margin-top: var(--spacing-xs); + white-space: nowrap; +} + +.spread-flow-label { + font-family: var(--font-body); + font-size: 0.75rem; + color: var(--color-ash); +} + +.spread-flow-icon { + color: var(--spread-accent); + font-weight: 600; + font-size: 0.8125rem; +} + +.spread-flow-cmd { + font-family: var(--font-mono); + font-size: 0.6875rem; + background: var(--color-mist); + padding: 2px 7px; + border-radius: 3px; + color: var(--color-ink); +} + +/* Right column: Demo area */ +.spread-demo-area { + grid-column: 2; + grid-row: 1 / 3; + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; +} + +/* Demo split comparison inside the spread */ +.spread-demo-area .demo-split-comparison { + display: flex; + flex-direction: column; + height: 100%; + flex: 1; + min-height: 0; +} + +.spread-demo-area .demo-split-comparison .split-container { + position: relative; + flex: 1; + min-height: 0; + overflow: hidden; + cursor: ew-resize; + user-select: none; + background: var(--color-cream); +} + +.spread-demo-area .demo-split-comparison .split-before, +.spread-demo-area .demo-split-comparison .split-after { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: var(--spacing-md); +} + +.spread-demo-area .demo-split-comparison .split-before { + z-index: 1; + background: var(--color-cream); +} + +.spread-demo-area .demo-split-comparison .split-after { + z-index: 2; + background: var(--color-paper); + clip-path: polygon(58% 0%, 100% 0%, 100% 100%, 42% 100%); +} + +.spread-demo-area .demo-split-comparison .split-content { + width: 100%; + max-width: 320px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.spread-demo-area .demo-split-comparison .split-divider { + position: absolute; + top: 0; + bottom: 0; + left: 50%; + width: 2px; + background: var(--spread-accent, var(--color-accent)); + transform: translateX(-50%) skewX(-10deg); + pointer-events: none; + z-index: 3; + box-shadow: 0 0 12px rgba(0,0,0,0.1); +} + +.spread-demo-area .demo-split-comparison .split-label { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) skewX(10deg); + font-size: 0.5625rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--color-paper); + background: var(--spread-accent, var(--color-accent)); + padding: 4px 10px; + border-radius: 3px; + white-space: nowrap; +} + +.spread-demo-area .demo-split-comparison .demo-caption { + flex-shrink: 0; + font-size: 0.75rem; + color: var(--color-ash); + text-align: center; + padding: var(--spacing-sm) var(--spacing-md); +} + +/* Fallback demo container inside spread */ +.spread-demo-area .demo-container { + flex: 1; + display: flex; + flex-direction: column; +} + +.spread-demo-area .demo-container .demo-viewport { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + max-width: 420px; + background: none; + min-height: auto; +} + +/* (nav moved to fisheye list) */ + +/* ============================================ + OLD DESKTOP STYLES (kept for mobile reuse) + ============================================ */ + .commands-container { display: grid; grid-template-columns: 1fr 1.2fr; @@ -264,9 +606,10 @@ } @media (max-width: 900px) { + .magazine-container { display: none; } + .fisheye-list { display: none; } .glass-terminal-wrapper { display: none; /* Hide on mobile for now, or stack */ - /* Alternatively: Position fixed bottom sheet */ } } @@ -725,6 +1068,34 @@ border-bottom-color: var(--color-accent); } +/* ============================================ + CHANGELOG + FAQ ROW (side-by-side on desktop) + ============================================ */ + +.changelog-faq-row { + position: relative; +} + +@media (min-width: 1024px) { + .changelog-faq-row { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + column-gap: calc(var(--spacing-xl) * 1.5); + border-top: 1px solid var(--color-mist); + } + + .changelog-faq-row > .changelog-section, + .changelog-faq-row > .faq-section { + border-top: none; + } + + .changelog-faq-row > .faq-section { + border-left: 1px solid var(--color-mist); + padding-left: calc(var(--spacing-xl) * 0.75); + margin-left: calc(var(--spacing-xl) * -0.75); + } +} + /* ============================================ CHANGELOG SECTION ============================================ */ @@ -773,6 +1144,7 @@ padding-left: var(--spacing-md); color: var(--color-charcoal); line-height: 1.7; + max-width: 70ch; } .changelog-items li { @@ -890,6 +1262,7 @@ padding: 0 0 var(--spacing-md); color: var(--color-charcoal); line-height: 1.7; + max-width: 70ch; animation: faqFadeIn 0.3s var(--ease-out); } @@ -930,6 +1303,38 @@ border-bottom-color: var(--color-accent); } +/* Mobile: FAQ + Changelog */ +@media (max-width: 600px) { + .faq-question { + font-size: 1rem; + padding: var(--spacing-md) 0; + min-height: 44px; + gap: var(--spacing-sm); + } + + .faq-answer { + font-size: 0.875rem; + } + + .changelog-version { + font-size: 1rem; + } + + .changelog-version-header { + flex-wrap: wrap; + gap: var(--spacing-xs); + } + + .changelog-items { + padding-left: var(--spacing-sm); + font-size: 0.875rem; + } + + .changelog-older-toggle { + min-height: 44px; + } +} + @keyframes faqFadeIn { from { opacity: 0; diff --git a/public/gallery.html b/public/gallery.html new file mode 100644 index 000000000..bc9255676 --- /dev/null +++ b/public/gallery.html @@ -0,0 +1,307 @@ + + + + + + Gallery of Shame | AI Anti-Patterns in the Wild | Impeccable + + + + + + + + + + + + + + + + + + + + +
+

Gallery of Shame

+

The Tell-Tale Signs of AI-Generated UI

+
+ + + + + + + + diff --git a/public/index.html b/public/index.html index 6947f2b94..a8530c541 100644 --- a/public/index.html +++ b/public/index.html @@ -12,8 +12,8 @@ - Impeccable: The missing upgrade to Anthropic's frontend-design skill - + Impeccable: The missing upgrade to Anthropic's impeccable skill + @@ -21,7 +21,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -38,7 +38,8 @@ - + + @@ -47,36 +48,55 @@ + + +
- - - 13.3k -

Impeccable

Design fluency for AI harnesses

-

Great design prompts require design vocabulary. Most people don't have it. You can't ask for "more vertical rhythm" if you've never used those words. Impeccable gives you commands that put designer language in your hands.

+

Great design prompts require design vocabulary. Most people don't have it. Impeccable teaches your AI deep design knowledge and gives you 21 commands to steer the result.

+

Impeccable teaches your AI real design and gives you 21 commands to steer the result.

What's included
- Enhanced frontend-design skill + anti-patterns + Enhanced impeccable skill + anti-patterns · - 20 design skills: /polish, /audit, /typeset, /overdrive... + 21 design commands: /polish, /audit, /typeset, /overdrive...
@@ -97,7 +117,7 @@
- + @@ -120,7 +140,7 @@
- Inter font (again) + Inter font Purple gradient Generic copy Cards on cards @@ -131,7 +151,7 @@ After

Introducing

-

Thoughtful Design

+

Thoughtful Design

Every element serves a purpose. Hierarchy guides the eye. Whitespace breathes.

@@ -156,81 +176,169 @@
- -
+ +
01 +

The Foundation

+
+
+

Before commands, before detection, Impeccable teaches your AI real design. Deep reference knowledge across 7 dimensions, loaded every time your AI writes code.

+ +
+ +
+ +
+

Run /impeccable teach once to set your project's design context. Every command benefits.

+
+
+
+ + +
+ +
+ 02 +

The Language

+
+
+

21 commands form a shared vocabulary between you and your AI. Each one encodes a specific design discipline, so you can steer with precision.

+ +
+ +
+ + + +
+
+

Pick any command to see it in action. View cheatsheet →

+
+ +
+
+
+ + +
+
+ 03

The Antidote

-

Anthropic's original frontend-design skill laid the foundation. Impeccable builds on it with curated patterns and anti-patterns across typography, color, layout, motion, and more.

+

Every AI model learned from the same templates. Without intervention, they all produce the same predictable mistakes. Impeccable names them, detects them, and teaches the AI to avoid them.

- -
- -
- -

Missing something? Suggest a pattern →

-
-
- - -
-
- 02 -

The Framework

-
-
-

One comprehensive skill with deep expertise, plus 20 commands that form the language of design.

- -
- -
- -
-
- - -
-
- 03 -

Commands in Action

-

Interactive demos for every command View cheatsheet →

-
- - -
- - - -
-
- 04 -

Get Started

-

Two steps to impeccable design.

-
- -
- -
-
- 1 -
-

Install the skills

-

One command. Every provider.

-
+ + +
+ + +
+
+ 04 +

Visual Mode

+
+
+

See design issues highlighted directly on the page. No screenshots, no guesswork. Impeccable’s overlay shows you exactly what’s wrong and where.

+ +
+
+
+ + + + Live detection overlay +
+ +
+
+
+ 25 deterministic checks +

No LLM needed. Pattern matching catches purple gradients, overused fonts, nested cards, low contrast, and more.

+
+
+ Three ways to use it +

The Chrome extension on any site, embedded in /critique during an AI design review, or standalone via npx impeccable live.

+
+ + Impeccable Chrome extension panel listing detected anti-patterns +
+ Coming soon + Chrome extension + Get notified → +
+
+
+
+
+
+ + +
+
+ 05 +

Get Started

+
+ + +
+
+

1Install the skills Recommended

+

21 commands that steer your AI toward better design, in real time. The full Impeccable experience.

+ +
- install
@@ -244,111 +352,167 @@
- Works with Cursor, Claude Code, Gemini CLI, Codex CLI, and more. Auto-detects your AI harness. + Works with Cursor, Claude Code, Gemini CLI, Codex CLI, and more.
- -
- Other install methods -
-
- Claude Code plugin -
- $ - /plugin marketplace add pbakaus/impeccable - -
- Then open /plugin to install from Discover tab -
-
-
- Manual download - Contains all provider directories. Extract to your project root. -
- -
- - Avoids conflicts with built-in commands or other skill packs -
-
-
- -
- -
-
-
+
+ Then run /impeccable teach to set up your project's design context. +
- -
-
- 2 -
-

Stay up to date

-

New skills, pattern updates, and design tips.

+ +
+ Other install methods + +
+ Claude Code plugin +
+ $ + /plugin marketplace add pbakaus/impeccable +
+ Then open /plugin in Claude Code
-
-

Keep the command cheatsheet handy for quick reference. To update skills, run npx skills update.

+ +
+ +
+ +

2Add the CLI Beta

+ +
+
+

Anti-pattern detection from the terminal. Scans HTML, CSS, JSX/TSX, Vue, and Svelte. Perfect for CI, pre-commit hooks, and one-off audits.

+ +
+
+ $ + npm i -g impeccable + +
+ Or use npx impeccable directly without installing. +
+ + +
+
+ + +
+ +

3Browser extension Coming soon

+ +
+
+

One-click anti-pattern detection on any live page: yours, staging, production, or anyone else's. Same detection engine as /critique, in your browser.

+

Currently in Chrome Web Store review.

+ +
+
+ + +
+ +

4Stay updated

+ +
+
+

Roughly monthly notes on new commands, anti-patterns, and the design thinking behind Impeccable.

+ +
+
+
+ +
- 05 + 06

What's New

- v1.6.0 - March 24, 2026 + v2.0 + April 8, 2026
    -
  • New provider: Trae (China + International)
  • -
  • /critique now scores against Nielsen's 10 heuristics, tests with persona archetypes, and assesses cognitive load
  • -
  • /audit now scores 5 dimensions with P0-P3 severity ratings and structured action plans
  • -
  • Improved skill descriptions for better agent auto-discovery
  • -
  • Fixed invalid YAML frontmatter that broke GitHub preview and Codex loading (#67)
  • -
  • Codex CLI now uses correct $ prefix for command references
  • +
  • Renamed frontend-design to impeccable. The core skill now shares its name with the project, and the teach subcommand moved from /teach-impeccable to /impeccable teach. One skill, one namespace.
  • +
  • Data-driven skill rewrite. The core skill was rebuilt against an internal eval framework that runs the same brief through frontier models with and without the skill loaded, then measures how much the output collapses into monoculture. The result: dramatically more font and color diversity, sharper overall design quality, and much stronger Codex support. The biggest unlock was an anti-attractor procedure that forces the model to enumerate and reject its reflex defaults before picking. Validated on gpt-5.4 and Qwen 3.6 Plus across 15 niches.
  • +
  • Anti-pattern detection engine. 25 deterministic rules across typography, color, layout, motion, and quality. Handles oklch, oklab, lch, and lab color formats, CSS variables inside border shorthands, gradient-backed text, and emoji-only nodes.
  • +
  • CLI: npx impeccable detect. Scans HTML, CSS, JSX/TSX, Vue, Svelte, and CSS-in-JS. Framework detection, multi-file import tracking, Puppeteer-backed live URL scanning, CI-ready JSON output, and a --fast regex mode for huge codebases.
  • +
  • Chrome DevTools extension. One-click detection on any page: yours, staging, production, or someone else's. Reads live computed styles, surfaces findings in an interactive panel, and highlights elements on the page. In Chrome Web Store review.
  • +
  • /critique got teeth. Persona sub-agents review in parallel, score against Nielsen's heuristics, run the detector automatically, and open a live browser overlay so you can walk each finding in place.
  • +
  • New ways to create with Impeccable. /shape runs a structured discovery interview about purpose, audience, and goals, then produces a design brief before any code is written. /impeccable craft chains that brief straight into the full implementation flow so you ship a designed feature instead of a reflex card grid.
  • +
  • New docs site. Top-level Docs, Anti-Patterns, and Visual Mode sections. 21 per-skill pages with before/after demos and the canonical SKILL.md inline, two tutorials, and 38 rule cards with inline visual examples.
  • +
  • New harness: Rovo Dev. 11 supported AI tools total.
View older releases
+
+
+ v1.6.0 + March 18, 2026 +
+
    +
  • New provider: Trae (China + International)
  • +
  • /critique now scores against Nielsen's 10 heuristics, tests with persona archetypes, and assesses cognitive load
  • +
  • /audit now scores 5 dimensions with P0-P3 severity ratings and structured action plans
  • +
  • Improved skill descriptions for better agent auto-discovery
  • +
  • Fixed invalid YAML frontmatter that broke GitHub preview and Codex loading (#67)
  • +
  • Codex CLI now uses correct $ prefix for command references
  • +
+
+
v1.5.1 @@ -366,7 +530,7 @@
  • 3 new skills: /typeset (fix typography), /arrange (fix layout & spacing), /overdrive (technically extraordinary effects, beta)
  • -
  • Skills now auto-gather design context via .impeccable.md — run /teach-impeccable once, all skills benefit
  • +
  • Skills now auto-gather design context via .impeccable.md. Run /teach-impeccable once, all skills benefit
  • Deep linking to commands (#cmd-overdrive, etc.)
@@ -390,7 +554,7 @@
  • Added Kiro support (.kiro/skills/)
  • -
  • Restored prefix toggle — download i- prefixed bundles to avoid naming conflicts
  • +
  • Restored prefix toggle: download i- prefixed bundles to avoid naming conflicts
  • Audit and critique skills only suggest real, installed commands
@@ -401,7 +565,7 @@ March 4, 2026
    -
  • Unified skills architecture — commands are now skills with user-invocable: true
  • +
  • Unified skills architecture: commands are now skills with user-invocable: true
  • Added VS Code Copilot and Google Antigravity support (.agents/skills/)
  • New install flow: npx skills add as primary, universal ZIP as fallback
  • Added universal ZIP containing all 5 provider directories
  • @@ -430,7 +594,7 @@
    - 06 + 07

    Frequently Asked Questions

    @@ -438,7 +602,7 @@
    Where do I put the downloaded files?
    -

    The easiest way is npx skills add pbakaus/impeccable — it auto-detects your AI harness and places files correctly.

    +

    The easiest way is npx skills add pbakaus/impeccable, which auto-detects your AI harness and places files correctly.

    If you downloaded the universal ZIP, extract it to your project root (same level as your package.json or src/ folder). It creates hidden folders for each supported tool: .cursor/, .claude/, .gemini/, .codex/, and .agents/.

    Project-level installation takes precedence and lets you version control your skills.

    @@ -452,7 +616,7 @@
  • Claude Code plugin: Open /plugin, go to the Discover tab, and update from there
  • Manual ZIP: Download the latest ZIP from above and extract to your project root, overwriting existing files
-

Your .impeccable.md design context file (created by /teach-impeccable) is never overwritten — your project context is preserved across updates.

+

Your .impeccable.md design context file (created by /impeccable teach) is never overwritten. Your project context is preserved across updates.

@@ -460,7 +624,7 @@ Commands or skills aren't appearing. What do I do?

For commands: Type / in your AI harness and look for commands like /audit, /polish, etc. If they don't appear, double-check the files are in the correct location.

-

For skills: Skills are applied automatically when relevant. To verify, explicitly mention "use the frontend-design skill" in your prompt—this forces the AI to acknowledge and apply it.

+

For skills: Skills are applied automatically when relevant. To verify, explicitly mention "use the impeccable skill" in your prompt. This forces the AI to acknowledge and apply it.

Tool-specific setup:

  • Cursor: Requires Nightly channel + Agent Skills enabled in Settings → Rules
  • @@ -482,72 +646,59 @@

    Once you're comfortable with basic prompting and code generation, come back and give Impeccable a try.

+ +
+ Is Impeccable free? +
+

Yes. Everything is Apache 2.0: skills, commands, CLI, and the detection engine. Fully open source, free for everyone.

+
+
+ - +

Work with me

-

I help teams navigate AI transformation: upleveling developers, building AI-native workflows, and shipping products. Deep expertise in developer experience, design systems, and full-stack architecture.

+

Impeccable is built by Renaissance Geek. I work with enterprise teams on large-scale rollouts, custom integrations, and training for designers and developers. If you're a frontier lab, design tool company, or enterprise looking to raise the bar on AI-generated design, let's talk.

diff --git a/public/js/components/art-gallery.js b/public/js/components/art-gallery.js index 785250c1e..509742509 100644 --- a/public/js/components/art-gallery.js +++ b/public/js/components/art-gallery.js @@ -10,8 +10,8 @@ export function renderGallery(skills) { const container = document.querySelector(".skills-gallery"); if (!container) return; - // Filter skills (hide frontend-design as per original) - const filteredSkills = skills.filter((s) => s.id !== "frontend-design"); + // Filter skills (hide impeccable as per original) + const filteredSkills = skills.filter((s) => s.id !== "impeccable"); container.innerHTML = `