PayCargo · AI-Sandbox · Process

6.fCSS / design system doctrine

The platform owns the visual language. Vibe apps consume it via SDK classes; they never extend it locally. The SDK ships PayCargo design tokens, component classes, and utilities — versioned, append-only, with a facade pattern for opt-in evolution. Four doctrines (§15–§18) consolidated into one focused reference.
Block: 6.f (Console doctrine — visual layer) · State: Doctrine fixed; SDK packages land in Phase A · Pins: §15, §16, §17, §18
Doctrine

The CSS/design surface of the SDK. Pulled together from doctrines pinned in Step 6 and Step 6.a so the SDK section is self-coherent: anyone proposing a new component / utility / theme override starts here.

1. The four doctrines

The CSS layer of the SDK is governed by four doctrines, each pinned earlier in the Step 6 thread, consolidated here for reference.

§15

Facebook, not MySpace — the platform owns the design language

Vibe apps consume SDK components and CSS classes. They do not import third-party UI libraries, embed inline custom styles beyond what the SDK permits, or extend the SDK's visual vocabulary outside an explicit doctrine PR. Apps that need a capability the SDK doesn't ship propose it as a platform addition that everyone inherits, not as an app-local extension that diverges. The platform retains the right to evolve the design system without breaking apps because apps never reach past the SDK boundary into the design.

§16

No embedded CSS

Vibe apps consume CSS exclusively through external <link> to SDK stylesheets. Inline style="..." attributes, <style> blocks in HTML, and JS direct style assignment (element.style.X = ...) are prohibited for color, spacing, typography, and layout concerns. The narrow exception: runtime-computed positional or transform values for behaviors that cannot be expressed as static CSS classes (drag operations, measurement passes, cursor-anchored positioning).

The operational payoff: changing --pc-blue in design-tokens.css from #0094d9 to #005f99 updates every button, link, focus ring, and accent across every vibe app instantly. Zero per-app refactor.

§17

SDK versioned + class-level facade — Lotus Notes durability

The SDK is distributed at versioned paths (/sdk/v{major}/...). Each app copies the SDK version it was deployed with into its own S3 bucket at apply time. Within a major version, CSS classes' visual behavior is never edited, deleted, or renamed — only added to. New visual variants are new class names; design changes are new additions, not silent edits. Apps that don't redeploy continue to render exactly as they did at their deploy date, indefinitely.

The unversioned facade class (e.g., .pc-btn-primary) is deliberately mutable; it tracks the SDK release's chosen "current" version. Versioned classes (.pc-btn-primary-v1, -v2, etc.) are append-only, never changed. Apps choose: reference the facade to opt into visual evolution; reference a -vN class to pin to a specific visual forever.

§18

The UI team owns the design language; vibe coders consume

Vibe app developers propose new visual components or CSS classes as part of the App Review PR process. The UI team reviews and either:

  • Promotes to the SDK — the new class/component is added to vibecode/sdk/v{N}/components.css as a -v1 variant. Becomes available to every vibe app; append-only forever within the SDK major version.
  • Keeps app-local — class lives in the app's own app.css, scoped exclusively to the app's body class (.pc-app-{name} parent selector). Not visible to other apps. The app accepts a long-term maintenance burden in exchange for keeping a pattern that doesn't generalize.
  • Rejects — the app must use existing SDK components, or re-propose with a design that does generalize.

2. What the SDK ships

FileSizeContains
sdk/v1/design-tokens.css~2KBThe :root { --pc-* } custom properties from CLAUDE.md — colors, typography scale, spacing, border radii. Single source of truth for the design system's values.
sdk/v1/components.css~6KBComponent classes: .pc-btn-*, .pc-input, .pc-data-table, .pc-sidebar, .pc-app-menu, .pc-badge-*, .pc-streaming-message, etc. Each in both facade and -v1 forms per §17.
sdk/v1/utilities.css~3KBSmall set of layout utilities: .pc-flex, .pc-stack-{xs,sm,md,lg}, .pc-grid-cols-{2,3,4}, .pc-text-{xs,sm,md}, .pc-hidden, .pc-mobile-only. Composable; not Tailwind.
sdk/v1/assets/paycargo-logo.svg~9KBCanonical PayCargo logo, white variant (paths recolored at render time for blue / sidebar variants).
sdk/v1/themes/{default,exec,partner}.css~1KB eachOptional theme variants. Apps that need different accent colors consume a theme file in addition to the base; theme overrides happen via custom property reassignment at body.pc-app-{name} scope.

Total CSS shipped per app: ~11KB minified, ~3KB gzipped. App pages add four <link> tags (tokens + components + utilities + Font Awesome kit) and consume from there.

3. The class facade pattern in practice

What the SDK ships

/* sdk/v1.0/components.css */
.pc-btn-primary-v1 {
  background: var(--pc-accent-orange);
  height: 44px;
  border-radius: 3px;
}

/* The facade — mirrors the SDK's "current" variant.
   At v1.0 there's only v1, so it equals v1. */
.pc-btn-primary {
  background: var(--pc-accent-orange);
  height: 44px;
  border-radius: 3px;
}

What the SDK ships at v1.3 (after two design evolutions)

/* sdk/v1.3/components.css */
.pc-btn-primary-v1 {  /* UNCHANGED from v1.0 — Lotus Notes durability */
  background: var(--pc-accent-orange);
  height: 44px;
  border-radius: 3px;
}

.pc-btn-primary-v2 {  /* added in v1.1 — NEVER edited again */
  background: var(--pc-accent-orange);
  height: 48px;
  border-radius: 6px;
  font-weight: 600;
}

.pc-btn-primary-v3 {  /* added in v1.3 — current */
  background: var(--pc-accent-orange);
  height: 48px;
  border-radius: 8px;
  font-weight: 600;
  box-shadow: 0 1px 2px rgba(0,0,0,0.08);
}

/* Facade now mirrors v3 */
.pc-btn-primary {
  background: var(--pc-accent-orange);
  height: 48px;
  border-radius: 8px;
  font-weight: 600;
  box-shadow: 0 1px 2px rgba(0,0,0,0.08);
}

What vibe apps consume

App's HTMLPin semanticWhen SDK upgrades
class="pc-btn-primary"^ — give me currentRe-deploy fetches the new SDK and the button gets the new visual
class="pc-btn-primary-v2"exact pinRe-deploy doesn't change anything; visual is locked to v2 forever within v1.x

Most apps use the facade. Apps with strict visual contracts (customer-facing brand-book commitments) pin to versioned classes.

4. The npm semver mapping

npm semverSDK CSS equivalentMeaning
^1.2.3 (caret).pc-btn-primary (facade)"Give me the latest non-breaking version"
1.2.3 (exact).pc-btn-primary-v2"Give me exactly this version, forever"
~1.2.3 (tilde)(not provided in v1)"Accept patch updates only" — no clean analog; added by doctrine PR if a real need emerges

Updates ripple at app re-deployment, not continuously — the Terraform apply is the equivalent of npm install. The SDK file in each app's S3 bucket is the lockfile.

5. App.css — the narrow escape valve

When a vibe app needs visual customization that doesn't fit the SDK's defaults, it consumes a per-app stylesheet at cartridges/<dept>/<app>/app.css.

Allowed in app.cssProhibited in app.css
Layout overrides scoped to body.pc-app-{name}New component class definitions (those go through promotion review)
CSS custom property overrides scoped to the body classGlobal overrides of SDK classes (anything not scoped to the body class)
App-specific media-query layoutsColor / typography / spacing decisions that should be design tokens
One-off keyframe animations for app-specific behaviorsNew keyframe animations for general UI behaviors (those promote to SDK)

Example app.css for the chatbot:

/* cartridges/engineering/chatbot/app.css */

/* Layout: thread list on left, message pane on right */
body.pc-app-chatbot main {
  display: grid;
  grid-template-columns: 280px 1fr;
  gap: 0;
}

/* Minor theme override: slightly softer chat bubble background */
body.pc-app-chatbot {
  --pc-chat-bubble-bg: #f0f9ff;
}

That's it. The chatbot's app.css contains only layout that's structurally specific to the chatbot and one custom property override. No new components; no overrides of platform classes outside the body scope.

6. The promotion / demotion flow

StageOwnerAuthority
Propose new CSSVibe app developer"I think the platform needs this component" — submitted as part of the App Review PR
ReviewUI team"Promote / app-local / reject"
If promoted: add to SDKUI teamEdits vibecode/sdk/v1/components.css; new class enters Lotus Notes durability immediately
If app-local: stays in app.cssVibe app developerScoped to body.pc-app-{name}; never consumable by other apps
ConsumeVibe app developerImports SDK classes; reads design tokens; no inline styles

App Review PR template — CSS section

## CSS / design changes

Pick one:
- [ ] No new CSS introduced — app consumes existing SDK classes only.
- [ ] New SDK promotion proposed (UI team review required).
- [ ] App-local additions in app.css only (UI team review still required, just for boundary check).

If proposing SDK promotion:
| Proposed class             | Component purpose                       | Reusability rationale                                           |
|----------------------------|-----------------------------------------|-----------------------------------------------------------------|
| .pc-streaming-message-v1   | Animated typing chatbot bubble          | Every future chat/assistant vibe app will need this             |
| .pc-chat-bubble-v1         | User/assistant chat message container   | Chat shape is structurally similar across vibe apps             |

UI team reviewer checklist:
- [ ] Does this fit the platform's visual vocabulary?
- [ ] Is this likely to be needed by future vibe apps?
- [ ] Is the class naming aligned with existing patterns (.pc-{component}-v{N})?
- [ ] Does it work with existing design tokens, or does it require new ones?
- [ ] Decision: PROMOTE / KEEP APP-LOCAL / REJECT

7. Failure modes — what the doctrine catches

MistakeCaught byOperator sees
Developer adds inline style="color: red" to an app's HTMLPR linter (Phase D); discipline + review for nowPR comment: "§16 violation — use a design token or SDK class"
Developer imports Tailwind / Bootstrap / Bulma into their appApp Review PR template + dependency auditPR rejected with §15 reference
Developer adds a new .pc-shiny-button class to their app.cssUI team reviewReviewer asks: "Promote to SDK or scope to body.pc-app-{name}?"
UI team edits .pc-btn-primary-v1 to change its background colorDoctrine PR reviewDoctrine PR rejected: §17 violation — -v1 classes are append-only; add -v2 instead
Developer overrides .pc-btn-primary globally in their app.cssUI team reviewReviewer demands the override be scoped to body.pc-app-{name} or proposed as a theme variant
App's CSS tries to import a third-party stylesheet via @importApp Review PR + dependency auditPR rejected with §15 reference; if legitimate need (e.g., a calendar widget), triggers doctrine PR for a Tier 2 addition

8. Why this doctrine matters operationally

The compounding payoff at scale

With one platform-owned design language and one set of versioned components, the platform can evolve the visual identity globally with a single PR. Without these doctrines, every vibe app drifts independently; by app number 10 the platform looks like ten different platforms; by app 20 the "PayCargo brand" is unrecognizable.

The Lotus Notes durability promise

An obs app deployed at SDK v1.0 in week 1 will render exactly the same way at SDK v1.5 in week 20 — as long as it doesn't redeploy. New vibe apps shipped at v1.5 get the latest facade. Both can coexist; neither is forced to migrate. Predictability buys trust.

The honest cost

CSS bundle size grows over time (no deletions). For 60 days of additions, the bundle goes from ~11KB to maybe ~25KB minified. Still small. After 2 years, with the right discipline on facade promotions, it stays under ~50KB. If it ever exceeds that, the platform has earned a major-version bump (v2) where deprecation becomes possible.