Files
pbakaus_impeccable/skill/reference/brand.md
T
e587004ee4 Refactor: cleaner top-level directory structure (#138)
* refactor(content): merge content/site/ into site/content/

Phase 1 step 1 of the directory restructure. The dual content tree was
called out in CLAUDE.md as cleanup; both trees were already in sync
except for anti-patterns-catalog.js, which moves to site/data/.

- Delete content/site/skills/ and content/site/tutorials/ (duplicates of
  site/content/, which is what Astro's content collection actually reads).
- Move content/site/anti-patterns-catalog.js -> site/data/.
- Update scripts/lib/sub-pages-data.js and scripts/build.js to read from
  site/content/ and site/data/.
- Drop content/site/ from validateProse target list (site/content was
  already there).
- Rewrite the "Two content trees" section in CLAUDE.md as a single-tree
  pointer; update stale dev-server text mentioning the deleted
  server/index.js.

Tests: 186/186 pass. Skills build: clean.

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

* refactor(skill): rename source/skills/impeccable/ -> skill/

Phase 1 step 2 of the directory restructure. The path was redundantly
nested ("source/" wrapper plus "skills/impeccable/" — singular content
hidden behind the plural). Collapses to flat skill/SKILL.md +
skill/reference/ + skill/scripts/.

- Move source/skills/impeccable/ -> skill/.
- Rewrite scripts/lib/utils.js readSourceFiles(): drop the multi-skill
  iteration (CLAUDE.md commits to a single user-invocable skill); read
  skill/SKILL.md directly.
- Update scripts/build.js, scripts/generate-og-image.js, and the
  sub-pages data layer to point at skill/.
- Update tests/lib/utils.test.js: drop the "multi-skill" and "dir-name
  fallback" cases, update single-skill paths to skill/.
- Update tests/build.test.js similarly: drop "multiple skills"
  integration test, update paths.
- Update non-glob path joins in tests/framework-fixtures.test.mjs,
  tests/live-e2e/session.mjs, tests/live-e2e/agents/llm-agent.mjs,
  tools/live-loop.mjs.
- Update prose/text references in CLAUDE.md, AGENTS.md, DEVELOP.md,
  README.md, scripts/lib/sub-pages-data.js, bin/commands/skills.mjs,
  site/data/anti-patterns-catalog.js, site/pages/docs/[...slug].astro,
  docs/adr-live-variant-mode.md, docs/plans/.

Eval framework note: the separate impeccable-evals repo reads
../impeccable/source/skills/impeccable/ and needs a coordinated
rename to ../impeccable/skill/.

Tests: 186/186 pass. Skills build: clean.

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

* refactor: rename docs/ -> notes/

Phase 1 step 3 of the directory restructure. The internal docs/ dir
(ADRs and plans) clashed with the site's /docs route. Renaming it
"notes/" makes the difference unambiguous: notes/ is project-internal
process, /docs is the user-facing route under site/pages/docs/.

No code references the dir; the rename is a clean git mv.

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

* refactor(site): move public/ under site/public/

Phase 2 step 4 of the directory restructure. Public assets and the
Astro publicDir now live alongside the rest of the site, so site/
is fully self-contained for static content.

- git mv public site/public.
- astro.config.mjs: add publicDir: './site/public'. Astro defaults to
  ./public at the project root, so the override is required.
- scripts/build.js: write generated _data, _headers, _redirects,
  _routes.json, and js/detect-antipatterns-browser.js into
  site/public/. Also delete the dead _REMOVED() Bun static-site
  builder (replaced by Astro at #130; the placeholder no longer earns
  its keep).
- scripts/build.js validateProse: replace the stale public/index.html
  reference (deleted at the Astro migration) with site/pages/index.astro
  in the count-validation file list, restoring homepage drift detection.
- scripts/generate-og-image.js: write OG image into site/public/.
- scripts/screenshot-antipatterns.js: read examples from + write
  screenshots to site/public/antipattern-{examples,images}/.
- scripts/lib/sub-pages-data.js: load command demos from
  site/public/js/demos/commands.
- .gitignore: rename the public/* generator-output entries to
  site/public/*.
- CLAUDE.md: refresh CSS/data-file paths (still pointing at the old
  pre-Astro public/css/ + public/js/ tree), point the changelog and
  command-add checklists at site/pages/index.astro and
  site/scripts/data.js + site/scripts/components/framework-viz.js.

Cloudflare Pages note: functions/ stays at the repo root because
CF Pages auto-discovers it there with no configuration knob to
relocate. Moving it under site/ would either break deployment or
require a build-time copy step that adds more complexity than the
cleanup is worth.

Tests: 186/186 pass. Skills + site build clean. _headers,
_redirects, _routes.json, _data/ all land in build/ correctly.

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

* refactor(cli): consolidate bin/ + src/ + lib/ under cli/

Phase 2 step 5 of the directory restructure. The CLI surface was split
across three top-level dirs whose names were easy to mistake for each
other (especially src/ vs source/ pre-step-2). Consolidates under cli/.

- git mv bin -> cli/bin (CLI entry + skills sub-command)
- git mv src -> cli/engine (detect-antipatterns engine + browser variant)
- git mv lib -> cli/lib (download-providers helper)

Update package.json:
- bin.impeccable: cli/bin/cli.js
- main + exports: cli/engine/detect-antipatterns.mjs and the
  ./browser variant
- files: ["cli/", "LICENSE"]

Update internal references:
- cli/bin/cli.js: dynamic import points at ../engine/, package.json
  read goes one level deeper (../../package.json).
- functions/api/download/[type]/[provider]/[id].js + bundle/[provider].js:
  cli/lib/download-providers.js path.
- scripts/build.js, scripts/build-browser-detector.js,
  scripts/build-extension.js: cli/engine path constants.
- scripts/lib/sub-pages-data.js, scripts/lib/utils.js, skill/scripts/
  live-server.mjs: comment refs.
- tests/detect-antipatterns{,-browser,-fixtures}.test.{js,mjs},
  tests/windows-path-fix.test.js: import + read paths.
- AGENTS.md, CLAUDE.md: doc paths.

Verified:
- npx node cli/bin/cli.js --version, --help, detect --help all work.
- bun run build, bun run build:browser, bun run build:extension all
  clean. Browser detector lands at cli/engine/detect-antipatterns-browser.js;
  extension/detector/detect.js still emits to the same location.
- bun run test: 186/186 pass.

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

* fix: update browser-detector paths missed in cli/ rename

Bugbot caught two runtime path leaks where the comment got renamed
to cli/engine/ but the actual code still used the old src/ segment.

- skill/scripts/live-server.mjs: detectPaths array now joins cli, engine,
  detect-antipatterns-browser.js for both the repo-relative lookup
  (4 dirs up from .claude/skills/impeccable/scripts/ to repo root) and
  the npm node_modules fallback. Without this fix, the detection
  overlay would silently not load during live-server sessions.

- scripts/build.js: the post-build copy of the browser detector into
  site/public/js/ was reading from src/. The if (fs.existsSync(...))
  guard meant the copy was silently skipping, so antipattern-examples
  pages would 404 on /js/detect-antipatterns-browser.js once the site
  was deployed.

Tests: 186/186 pass. Build clean. site/public/js/detect-antipatterns-browser.js
re-emits as expected.

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

* fix: cleanup-deprecated import path missed an extra .. in cli/ rename

Bugbot caught three call sites in cli/bin/commands/skills.mjs that
import '../../skill/scripts/cleanup-deprecated.mjs'. Pre-rename, that
was correct from bin/commands/ (one parent to bin/, one to repo root).
After moving the file from bin/commands/ to cli/bin/commands/, the
path is one directory deeper, so it needs three .. segments to reach
the repo root. Without the fix, every cleanup invocation throws on
import and gets swallowed by the surrounding try/catch — silent skip.

cli/bin/cli.js's package.json read already uses '../../package.json'
(the same depth pattern), confirming three levels is correct.

Verified: dynamic import resolves and exports the expected functions.

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

* chore: sweep stale path/file references missed in the restructure

Same root cause as the two bugbot finds: some references in moved or
related files weren't tracked because they didn't match a simple
sed pattern. Caught the rest by walking each moved dir's depth and
each Astro-migration deletion.

Stale path references (post-Astro migration, missed earlier):
- CLAUDE.md: legacy URL redirects "live in server/index.js" -> point
  at the actual sources (scripts/build.js generateCFConfig +
  site/public/_redirects).
- AGENTS.md: counts.js path (public/ -> site/public/), changelog file
  (public/index.html -> site/pages/index.astro), screenshots note
  (public/ -> site/), source-of-truth dirs (source/, src/ -> skill/,
  cli/).
- tests/detect-antipatterns-browser.test.mjs: comment about routes
  "in server/index.js".
- skill/reference/live.md: workflow.css example for "this repo" was
  pre-Astro (public/css/) -> site/styles/. (User-project Vite/Next
  example unchanged.)

Stale path that pointed at moved files:
- tests/skills-cli.test.js: CLI path was '..', 'bin', 'cli.js'; now
  '..', 'cli', 'bin', 'cli.js'. Test isn't wired into bun run test
  but it would have failed if invoked.

Dead files (orphaned by Astro migration, never cleaned up):
- tests/server/download-validation.test.js: imported from
  ../../server/lib/{validation,api-handlers}.js which were deleted in
  b8f09c8. Test was a silent failure waiting to happen.
- scripts/lib/render-markdown.js: 156-line module with zero consumers
  (the only caller, scripts/lib/render-page.js, was deleted in the
  Astro cleanup).
- scripts/build.js: dead commented-out generateSubPages import.

Tests: 186/186 pass. Build clean.

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

* fix(build): remove invalid Corepack packageManager spec

Cloudflare Pages rejects the build with `Unsupported package manager
specification (bun@1.3.11)`. The packageManager field follows
Corepack's syntax which only validates npm/pnpm/yarn — `bun@X.Y.Z`
parses as a malformed Corepack directive even though Bun itself
treats it as a hint.

Pre-existing on main since d874af0 (CF Pages deploy on main also
failing); just surfaces here because the PR triggers a fresh deploy.

CF Pages auto-detects Bun anyway (the build log confirms:
"Detected the following tools from environment: bun@1.3.11,
pnpm@10.11.1, nodejs@22.16.0"). Removing the field unblocks the
deploy without changing local dev behavior.

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

---------

Co-authored-by: Paul Bakaus <paulbakaus@pauls-mbp-3.lan>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 16:38:03 -07:00

10 KiB
Raw Blame History

Brand register

When design IS the product: brand sites, landing pages, marketing surfaces, campaign pages, portfolios, long-form content, about pages. The deliverable is the design itself; a visitor's impression is the thing being made.

The register spans every genre. A tech brand (Stripe, Linear, Vercel). A luxury brand (a hotel, a fashion house). A consumer product (a restaurant, a travel site, a CPG packaging page). A creative studio, an agency portfolio, a band's album page. They all share the stance (communicate, not transact) and diverge wildly in aesthetic. Don't collapse them into a single look.

The brand slop test

If someone could look at this and say "AI made that" without hesitation, it's failed. The bar is distinctiveness; a visitor should ask "how was this made?", not "which AI made this?"

Brand isn't a neutral register. AI-generated landing pages have flooded the internet, and average is no longer findable. Restraint without intent now reads as mediocre, not refined. Brand surfaces need a POV, a specific audience, a willingness to risk strangeness. Go big or go home.

The second slop test: aesthetic lane. Before committing to moves, name the reference. A Klim-style specimen page is one lane; Stripe-minimal is another; Liquid-Death-acid-maximalism is another. Don't drift into editorial-magazine aesthetics on a brief that isn't editorial. A hiking brand with Cormorant italic drop caps has the wrong register within the register.

Typography

Font selection procedure

Every project. Never skip.

  1. Read the brief. Write three concrete brand-voice words. Not "modern" or "elegant," but "warm and mechanical and opinionated" or "calm and clinical and careful." Physical-object words.
  2. List the three fonts you'd reach for by reflex. If any appear in the reflex-reject list below, reject them; they are training-data defaults and they create monoculture.
  3. Browse a real catalog (Google Fonts, Pangram Pangram, Future Fonts, Adobe Fonts, ABC Dinamo, Klim, Velvetyne) with the three words in mind. Find the font for the brand as a physical object: a museum caption, a 1970s terminal manual, a fabric label, a cheap-newsprint children's book, a concert poster, a receipt from a mid-century diner. Reject the first thing that "looks designy."
  4. Cross-check. "Elegant" is not necessarily serif. "Technical" is not necessarily sans. "Warm" is not Fraunces. If the final pick lines up with the original reflex, start over.

Reflex-reject list

Training-data defaults. Ban list. Look further:

Fraunces · Newsreader · Lora · Crimson · Crimson Pro · Crimson Text · Playfair Display · Cormorant · Cormorant Garamond · Syne · IBM Plex Mono · IBM Plex Sans · IBM Plex Serif · Space Mono · Space Grotesk · Inter · DM Sans · DM Serif Display · DM Serif Text · Outfit · Plus Jakarta Sans · Instrument Sans · Instrument Serif

Reflex-reject aesthetic lanes

Parallel to the font list. Currently saturated aesthetic families that have flooded brand surfaces. If a brief lands in one of these lanes without a register reason that requires it (a literal magazine, a literal terminal, a literal industrial signage system), it's the second-order training reflex: the trap one tier deeper than picking a Fraunces font. Look further.

  • Editorial-typographic. Display serif (often italic) + small mono labels + ruled separators + monochromatic restraint. Klim-influenced, magazine-cover affectation. By 2026, every Stripe-adjacent and Notion-adjacent brand has landed here. The fingerprint: three rule-separated columns, an italic Fraunces / Recoleta / Newsreader headline, lowercase track-spaced metadata, no imagery.

(More entries land here on the same cadence the font list updates. Brutalist-utility and acid-maximalism may join when they saturate. Removing entries when they fall back below saturation is also fine.)

The reflex-reject lists apply to new design choices. When the existing brand has already committed to a font or a lane as part of its identity, identity-preservation wins; variants on an existing surface don't second-guess what's already shipping. The reflex-reject lists are for greenfield decisions and for departure-mode variants in live.md.

Pairing and voice

Distinctive + refined is the goal. The specific shape depends on the brand:

  • Editorial / long-form / luxury: display serif + sans body (a magazine shape).
  • Tech / dev tools / fintech: one committed sans, usually; custom-tight tracking, strong weight contrast inside a single family.
  • Consumer / food / travel: warmer pairings, often a humanist sans plus a script or display serif.
  • Creative studios / agencies: rule-breaking welcome. Mono-only, or display-only, or custom-drawn type as voice.

Two families minimum is the rule only when the voice needs it. A single well-chosen family with committed weight/size contrast is stronger than a timid display+body pair.

Vary across projects. If the last brief was a serif-display landing page, this one isn't.

Scale

Modular scale, fluid clamp() for headings, ≥1.25 ratio between steps. Flat scales (1.1× apart) read as uncommitted.

Light text on dark backgrounds: add 0.050.1 to line-height. Light type reads as lighter weight and needs more breathing room.

Color

Brand surfaces have permission for Committed, Full palette, and Drenched strategies. Use them. A single saturated color spread across a hero is not excess; it's voice. A beige-and-muted-slate landing page ignores the register.

  • Name a real reference before picking a strategy. "Klim Type Foundry #ff4500 orange drench", "Stripe purple-on-white restraint", "Liquid Death acid-green full palette", "Mailchimp yellow full palette", "Condé Nast Traveler muted navy restraint", "Vercel pure black monochrome". Unnamed ambition becomes beige.
  • Palette IS voice. A calm brand and a restless brand should not share palette mechanics.
  • When the strategy is Committed or Drenched, color carries the brand. Don't hedge with neutrals around the edges. Commit.
  • Don't converge across projects. If the last brand surface was restrained-on-cream, this one is not.

Layout

  • Asymmetric compositions are one option. Break the grid intentionally for emphasis.
  • Fluid spacing with clamp() that breathes on larger viewports. Vary for rhythm: generous separations, tight groupings.
  • Alternative: a strict, visible grid as the voice (brutalist / Swiss / tech-spec aesthetics). Either asymmetric or rigorously-gridded can be "designed"; the failure mode is splitting the difference into a generic centered stack.
  • Don't default to centering everything. Left-aligned with asymmetric layouts feels more designed; a strict grid reads as confident structure. A centered-stack hero with icon-title-subtitle cards reads as template.
  • When cards ARE the right affordance, use grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)) for breakpoint-free responsiveness.

Imagery

Brand surfaces lean on imagery. A restaurant, hotel, magazine, or product landing page without any imagery reads as incomplete, not as restrained. A solid-color rectangle where a hero image should go is worse than a representative stock photo.

When the brief implies imagery (restaurants, hotels, magazines, photography, hobbyist communities, food, travel, fashion, product), you must ship imagery. Zero images is a bug, not a design choice. "Restraint" is not an excuse.

  • For greenfield work without local assets, use stock imagery. Unsplash is the default. The URL shape is https://images.unsplash.com/photo-{id}?auto=format&fit=crop&w=1600&q=80. Pick real Unsplash photo IDs you're confident exist (photo-1559339352-11d035aa65de, photo-1590490360182-c33d57733427, etc.); if unsure, pick fewer photos but don't substitute colored <div> placeholders.
  • Search for the brand's physical object, not the generic category: "handmade pasta on a scratched wooden table" beats "Italian food"; "cypress trees above a limestone hotel facade at dusk" beats "luxury hotel".
  • One decisive photo beats five mediocre ones. Hero imagery should commit to a mood; padding with more stock doesn't rescue an indecisive one.
  • Alt text is part of the voice. "Coastal fettuccine, hand-cut, served on the terrace" beats "pasta dish".

Tech / dev-tool brands are the exception where zero imagery can be correct; a developer landing page often carries its voice through typography, code samples, diagrams. Know which kind of brand you're working on.

Motion

  • One well-orchestrated page-load with staggered reveals beats scattered micro-interactions, when the brand invites it. Tech-minimal brands often skip entrance motion entirely; the restraint is the voice.
  • For collapsing/expanding sections, transition grid-template-rows rather than height.

Brand bans (on top of the shared absolute bans)

  • Monospace as lazy shorthand for "technical / developer." If the brand isn't technical, mono reads as costume.
  • Large rounded-corner icons above every heading. Screams template.
  • Single-family pages that picked the family by reflex, not voice. (A single family chosen deliberately is fine.)
  • All-caps body copy. Reserve caps for short labels and headings.
  • Timid palettes and average layouts. Safe = invisible.
  • Zero imagery on a brief that implies imagery (restaurant, hotel, food, travel, fashion, photography, hobbyist). Colored blocks where a hero photo belongs.
  • Defaulting to editorial-magazine aesthetics (display serif + italic + drop caps + broadsheet grid) on briefs that aren't magazine-shaped. Editorial is ONE aesthetic lane, not the default brand aesthetic.

Brand permissions

Brand can afford things product can't. Take them.

  • Ambitious first-load motion. Reveals, scroll-triggered transitions, typographic choreography.
  • Single-purpose viewports. One dominant idea per fold, long scroll, deliberate pacing.
  • Typographic risk. Enormous display type, unexpected italic cuts, mixed cases, hand-drawn headlines, a single oversize word as a hero.
  • Unexpected color strategies. Palette IS voice; a calm brand and a restless brand should not share palette mechanics.
  • Art direction per section. Different sections can have different visual worlds if the narrative demands it. Consistency of voice beats consistency of treatment.