PICK YOUR AI. HIT COPY. PASTE INTO A FRESH CHAT.
Install the Wardrobe Pascal theme in my project. I'm using Claude Code / Cursor. Apply every step below as written.
Step 1 — Add this to globals.css:
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Space+Grotesk:wght@300;400;500;600;700&display=swap");
/* =========================================================
Wardrobe — PASCAL tokens
Brutalist editorial bureaucracy. Paper-logic UI, form-field
aesthetic, Swiss-grid-meets-tax-document. Black grid lines,
paper cells with mint/pink accents, Space Grotesk display
+ IBM Plex Mono labels. Activate via data-system="pascal".
========================================================= */
:where([data-system="pascal"]) {
/* ---- Surface ---- */
--color-bg: #000000; /* ink — body bg, grid lines */
--color-paper: #f2f2f2; /* cell bg */
--color-paper-soft: #e5e5e5; /* table headers, dividers */
--color-mint: #4ea884; /* mint accent */
--color-pink: #ebaec1; /* pink accent */
--color-ink: #000000; /* primary text */
--color-ink-soft: rgba(0, 0, 0, 0.6);
--color-ink-faint: rgba(0, 0, 0, 0.1);
/* Aliases used throughout the SuperYes-style designs */
--bg-paper: #f2f2f2;
--bg-mint: #4ea884;
--bg-pink: #ebaec1;
--ink: #000000;
/* ---- Borders + grid ---- */
--border-width: 2px;
--border-ink: 2px solid var(--color-ink);
/* ---- Typography ---- */
--font-display: "Space Grotesk", system-ui, -apple-system, sans-serif;
--font-main: "Space Grotesk", system-ui, -apple-system, sans-serif;
--font-mono: "IBM Plex Mono", "Courier New", Courier, monospace;
--weight-light: 300;
--weight-regular: 400;
--weight-medium: 500;
--weight-semibold: 600;
--weight-bold: 700;
--tight-tracking: -0.04em;
--label-tracking: 0.05em;
/* ---- Radii (almost none — this theme is angular) ---- */
--radius-none: 0;
--radius-stamp: 50%;
}
Step 2 — Create components/ui/wardrobe/cn.ts:
export function cn(...classes: Array<string | false | null | undefined>): string {
return classes.filter(Boolean).join(" ");
}
Step 3 — Create the following 10 component files in components/ui/wardrobe/:
--- components/ui/wardrobe/Button.tsx ---
import * as React from "react";
import { cn } from "./cn";
type ButtonProps = {
variant?: "primary" | "secondary" | "destructive" | "ghost";
size?: "sm" | "md" | "lg";
children: React.ReactNode;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
export function Button({
variant = "primary",
size = "md",
className,
children,
...props
}: ButtonProps) {
return (
<button
className={cn("psc-btn", `psc-btn--${variant}`, size !== "md" && `psc-btn--${size}`, className)}
{...props}
>
{children}
</button>
);
}
--- components/ui/wardrobe/Input.tsx ---
import * as React from "react";
import { cn } from "./cn";
type InputProps = {
size?: "sm" | "md" | "lg";
variant?: "default" | "error";
label?: string;
hint?: string;
fieldCode?: string;
} & React.InputHTMLAttributes<HTMLInputElement>;
export function Input({
label,
hint,
fieldCode,
className,
id,
...props
}: InputProps) {
const inputId = id ?? React.useId();
return (
<div className="psc-field">
{(label || fieldCode) && (
<div className="psc-field__head">
{fieldCode && <span className="psc-field__code">{fieldCode}</span>}
{label && (
<label htmlFor={inputId} className="psc-field__label">
{label}
</label>
)}
</div>
)}
<input
id={inputId}
className={cn("psc-field__input", className)}
{...props}
/>
{hint && (
<span style={{ fontFamily: "var(--font-mono)", fontSize: 10, opacity: 0.6, textTransform: "uppercase", letterSpacing: "0.05em" }}>
{hint}
</span>
)}
</div>
);
}
--- components/ui/wardrobe/Textarea.tsx ---
import * as React from "react";
import { cn } from "./cn";
type TextareaProps = {
size?: "sm" | "md" | "lg";
variant?: "default" | "error";
label?: string;
hint?: string;
fieldCode?: string;
} & React.TextareaHTMLAttributes<HTMLTextAreaElement>;
export function Textarea({
label,
hint,
fieldCode,
className,
id,
...props
}: TextareaProps) {
const textareaId = id ?? React.useId();
return (
<div className="psc-field">
{(label || fieldCode) && (
<div className="psc-field__head">
{fieldCode && <span className="psc-field__code">{fieldCode}</span>}
{label && (
<label htmlFor={textareaId} className="psc-field__label">
{label}
</label>
)}
</div>
)}
<textarea
id={textareaId}
className={cn("psc-field__input", className)}
{...props}
/>
{hint && (
<span style={{ fontFamily: "var(--font-mono)", fontSize: 10, opacity: 0.6, textTransform: "uppercase", letterSpacing: "0.05em" }}>
{hint}
</span>
)}
</div>
);
}
--- components/ui/wardrobe/Select.tsx ---
import * as React from "react";
import { cn } from "./cn";
type Option = { value: string; label: string };
type SelectProps = {
options: Option[];
value: string;
onValueChange: (value: string) => void;
placeholder?: string;
} & Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "value" | "onChange">;
export function Select({
options,
value,
onValueChange,
placeholder,
className,
...props
}: SelectProps) {
return (
<select
className={cn("psc-field__input", className)}
value={value}
onChange={(e) => onValueChange(e.target.value)}
{...props}
>
{placeholder && <option value="" disabled>{placeholder}</option>}
{options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
);
}
--- components/ui/wardrobe/Card.tsx ---
import * as React from "react";
import { cn } from "./cn";
type CardProps = {
variant?: "default" | "mint" | "pink" | "ink";
children: React.ReactNode;
} & React.HTMLAttributes<HTMLDivElement>;
export function Card({
variant = "default",
className,
children,
...props
}: CardProps) {
return (
<div
className={cn("psc-card", variant !== "default" && `psc-card--${variant}`, className)}
{...props}
>
{children}
</div>
);
}
--- components/ui/wardrobe/Badge.tsx ---
import * as React from "react";
import { cn } from "./cn";
type BadgeProps = {
variant?: "default" | "mint" | "pink" | "ink";
children: React.ReactNode;
} & React.HTMLAttributes<HTMLSpanElement>;
export function Badge({
variant = "default",
className,
children,
...props
}: BadgeProps) {
return (
<span
className={cn("psc-badge", variant !== "default" && `psc-badge--${variant}`, className)}
{...props}
>
{children}
</span>
);
}
--- components/ui/wardrobe/Dialog.tsx ---
import * as React from "react";
type DialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
children: React.ReactNode;
};
export function Dialog({ open, onOpenChange, title, children }: DialogProps) {
React.useEffect(() => {
if (!open) return;
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") onOpenChange(false);
}
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, [open, onOpenChange]);
if (!open) return null;
return (
<div
className="psc-dialog-backdrop"
role="dialog"
aria-modal="true"
aria-label={title}
onClick={(e) => {
if (e.target === e.currentTarget) onOpenChange(false);
}}
>
<div className="psc-dialog">
<div className="psc-dialog__head">
<h2 className="psc-dialog__title">{title}</h2>
<button
type="button"
className="psc-dialog__close"
onClick={() => onOpenChange(false)}
aria-label="Close"
>
✕
</button>
</div>
<div className="psc-dialog__body">{children}</div>
</div>
</div>
);
}
--- components/ui/wardrobe/Tabs.tsx ---
import * as React from "react";
import { cn } from "./cn";
type TabsProps = {
tabs: Array<{ id: string; label: string }>;
activeId: string;
onTabChange: (id: string) => void;
};
export function Tabs({ tabs, activeId, onTabChange }: TabsProps) {
return (
<div
className="psc-tabs"
role="tablist"
style={{ ["--psc-tab-count" as string]: tabs.length }}
>
{tabs.map((tab) => (
<button
key={tab.id}
type="button"
role="tab"
aria-selected={tab.id === activeId}
className={cn("psc-tab", tab.id === activeId && "is-active")}
onClick={() => onTabChange(tab.id)}
>
{tab.label}
</button>
))}
</div>
);
}
--- components/ui/wardrobe/Switch.tsx ---
import * as React from "react";
type SwitchProps = {
checked: boolean;
onCheckedChange: (checked: boolean) => void;
label?: string;
};
export function Switch({ checked, onCheckedChange, label }: SwitchProps) {
return (
<label className="psc-switch" data-checked={checked}>
<span className="psc-switch__track">
<span className="psc-switch__thumb" />
</span>
<input
type="checkbox"
checked={checked}
onChange={(e) => onCheckedChange(e.target.checked)}
style={{ position: "absolute", opacity: 0, width: 0, height: 0 }}
/>
{label && <span>{label}</span>}
</label>
);
}
--- components/ui/wardrobe/Toast.tsx ---
import * as React from "react";
import { cn } from "./cn";
type ToastProps = {
title: string;
description?: string;
variant?: "default" | "success" | "error";
};
export function Toast({ title, description, variant = "default" }: ToastProps) {
return (
<div
className={cn("psc-toast", variant !== "default" && `psc-toast--${variant}`)}
role="status"
aria-live="polite"
>
<span className="psc-toast__title">{title}</span>
{description && <span className="psc-toast__desc">{description}</span>}
</div>
);
}
Step 4 — Add globals.css component styles. Append the following to globals.css below the tokens:
/* =========================================================
Wardrobe — PASCAL globals
Loads Space Grotesk + IBM Plex Mono, then component +
decoration styles. Active under data-system="pascal".
========================================================= */
[data-system="pascal"] *,
[data-system="pascal"] *::before,
[data-system="pascal"] *::after {
box-sizing: border-box;
}
[data-system="pascal"] {
background-color: var(--color-ink);
color: var(--color-ink);
font-family: var(--font-display);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
/* ===========================
CELL (paper card with 2px ink grid)
=========================== */
.psc-cell {
background-color: var(--color-paper);
padding: 24px;
position: relative;
color: var(--color-ink);
}
.psc-cell--mint { background-color: var(--color-mint); }
.psc-cell--pink { background-color: var(--color-pink); }
.psc-cell--ink {
background-color: var(--color-ink);
color: var(--color-paper);
}
/* Grid wrapper — uses gap + black bg to paint 2px ink lines */
.psc-grid {
display: grid;
gap: var(--border-width);
background-color: var(--color-ink);
}
.psc-stack {
display: flex;
flex-direction: column;
gap: var(--border-width);
background-color: var(--color-ink);
}
.psc-row {
display: flex;
gap: var(--border-width);
background-color: var(--color-ink);
}
/* ===========================
TYPOGRAPHY
=========================== */
.psc-mono {
font-family: var(--font-mono);
font-size: 13px;
}
.psc-label-tiny {
font-family: var(--font-mono);
font-size: 11px;
line-height: 1;
text-transform: uppercase;
letter-spacing: var(--label-tracking);
opacity: 0.8;
display: block;
margin-bottom: 10px;
}
.psc-display-title {
font-family: var(--font-display);
font-size: clamp(48px, 8vw, 80px);
line-height: 0.85;
font-weight: 700;
text-transform: uppercase;
letter-spacing: var(--tight-tracking);
}
.psc-display-huge {
font-family: var(--font-display);
font-size: clamp(32px, 5vw, 64px);
line-height: 0.9;
font-weight: 500;
text-transform: uppercase;
letter-spacing: -0.02em;
}
.psc-display-large {
font-family: var(--font-display);
font-size: clamp(36px, 6vw, 72px);
line-height: 0.9;
font-weight: 600;
text-transform: uppercase;
letter-spacing: -0.03em;
}
.psc-display-med {
font-family: var(--font-display);
font-size: 24px;
line-height: 0.95;
font-weight: 500;
text-transform: uppercase;
}
/* ===========================
MARQUEE TICKER
=========================== */
.psc-marquee {
background-color: var(--color-ink);
color: var(--color-paper);
padding: 12px 0;
overflow: hidden;
white-space: nowrap;
font-family: var(--font-mono);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.05em;
position: relative;
}
.psc-marquee__track {
display: inline-block;
padding-left: 100%;
animation: psc-marquee-scroll 28s linear infinite;
}
@keyframes psc-marquee-scroll {
0% { transform: translateX(0); }
100% { transform: translateX(-100%); }
}
/* ===========================
STAMP (rotated round border)
=========================== */
.psc-stamp {
width: 80px;
height: 80px;
border: 2px solid var(--color-ink);
border-radius: var(--radius-stamp);
display: flex;
align-items: center;
justify-content: center;
text-align: center;
font-family: var(--font-mono);
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
line-height: 1.1;
transform: rotate(-15deg);
padding: 8px;
}
.psc-stamp--double {
border: 3px double var(--color-ink);
}
/* ===========================
VERTICAL LABEL (writing-mode)
=========================== */
.psc-vertical {
writing-mode: vertical-rl;
transform: rotate(180deg);
font-family: var(--font-mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.15em;
opacity: 0.65;
white-space: nowrap;
line-height: 1;
}
/* ===========================
STATUS BADGE
=========================== */
.psc-status {
display: inline-block;
padding: 4px 10px;
border: 2px solid var(--color-ink);
background: var(--color-paper);
font-family: var(--font-mono);
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
line-height: 1.2;
color: var(--color-ink);
}
.psc-status--mint { background: var(--color-mint); }
.psc-status--pink { background: var(--color-pink); }
.psc-status--ink {
background: var(--color-ink);
color: var(--color-paper);
}
/* ===========================
FIELD GROUP (FLD_NN inputs)
=========================== */
.psc-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.psc-field__head {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 8px;
font-family: var(--font-mono);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.1em;
}
.psc-field__code {
color: var(--color-ink);
opacity: 0.55;
font-weight: 600;
}
.psc-field__label {
color: var(--color-ink);
font-weight: 600;
}
.psc-field__input {
background: transparent;
border: none;
border-bottom: 2px solid var(--color-ink);
border-radius: 0;
padding: 8px 0;
font-family: var(--font-mono);
font-size: 14px;
color: var(--color-ink);
width: 100%;
}
.psc-field__input:focus {
outline: none;
background: rgba(0, 0, 0, 0.04);
}
.psc-field__input::placeholder {
color: var(--color-ink);
opacity: 0.35;
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 12px;
}
textarea.psc-field__input {
border: 2px solid var(--color-ink);
padding: 10px 12px;
min-height: 100px;
line-height: 1.5;
resize: vertical;
}
select.psc-field__input {
appearance: none;
border: 2px solid var(--color-ink);
padding: 10px 12px;
background-image: linear-gradient(45deg, transparent 50%, var(--color-ink) 50%),
linear-gradient(135deg, var(--color-ink) 50%, transparent 50%);
background-position: calc(100% - 18px) 55%, calc(100% - 13px) 55%;
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
padding-right: 2.5rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* ===========================
BUTTONS
=========================== */
.psc-btn {
font-family: var(--font-mono);
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
font-size: 12px;
border: 2px solid var(--color-ink);
cursor: pointer;
background: var(--color-paper);
color: var(--color-ink);
padding: 12px 22px;
transition: background 100ms ease, color 100ms ease;
line-height: 1.1;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
border-radius: 0;
}
.psc-btn:hover { background: var(--color-mint); }
.psc-btn:active { transform: translate(1px, 1px); }
.psc-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; background: var(--color-paper); }
.psc-btn--primary {
background: var(--color-ink);
color: var(--color-paper);
}
.psc-btn--primary:hover {
background: var(--color-mint);
color: var(--color-ink);
}
.psc-btn--secondary {
background: var(--color-paper);
color: var(--color-ink);
}
.psc-btn--secondary:hover { background: var(--color-pink); }
.psc-btn--destructive {
background: var(--color-pink);
color: var(--color-ink);
}
.psc-btn--destructive:hover { background: var(--color-ink); color: var(--color-pink); }
.psc-btn--ghost {
background: transparent;
color: var(--color-ink);
}
.psc-btn--ghost:hover { background: rgba(0, 0, 0, 0.06); }
.psc-btn--sm { padding: 8px 14px; font-size: 10px; }
.psc-btn--lg { padding: 16px 28px; font-size: 13px; }
/* ===========================
DASHED CTA (empty-state container)
=========================== */
.psc-dashed-cta {
border: 2px dashed var(--color-ink);
background: transparent;
padding: 32px 28px;
text-align: center;
cursor: pointer;
transition: background 120ms ease;
}
.psc-dashed-cta:hover { background: rgba(0, 0, 0, 0.04); }
/* ===========================
TOC ITEMS (used in reading view)
=========================== */
.psc-toc-item {
padding: 12px 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
font-family: var(--font-mono);
font-size: 12px;
cursor: pointer;
transition: background 0.2s;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--color-ink);
display: block;
text-decoration: none;
}
.psc-toc-item:hover { background: rgba(0, 0, 0, 0.05); }
.psc-toc-item.is-active {
background: var(--color-mint);
font-weight: 600;
}
/* ===========================
TABLE (table-row pattern from log-index)
=========================== */
.psc-table {
display: flex;
flex-direction: column;
gap: var(--border-width);
background-color: var(--color-ink);
width: 100%;
}
.psc-thead,
.psc-trow {
display: grid;
gap: var(--border-width);
background-color: var(--color-ink);
}
.psc-th {
background-color: var(--color-paper-soft);
padding: 10px 12px;
font-family: var(--font-mono);
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: var(--label-tracking);
}
.psc-td {
background-color: var(--color-paper);
padding: 12px;
font-family: var(--font-mono);
font-size: 12px;
display: flex;
align-items: center;
color: var(--color-ink);
}
.psc-td--title {
font-family: var(--font-display);
font-weight: 500;
text-transform: uppercase;
line-height: 1.1;
font-size: 14px;
}
.psc-trow:hover .psc-td { background-color: #ffffff; }
/* ===========================
CARD (standard component)
=========================== */
.psc-card {
background: var(--color-paper);
border: 2px solid var(--color-ink);
padding: 24px;
color: var(--color-ink);
}
.psc-card--mint { background: var(--color-mint); }
.psc-card--pink { background: var(--color-pink); }
.psc-card--ink {
background: var(--color-ink);
color: var(--color-paper);
}
/* ===========================
BADGE (standard component, status variants)
=========================== */
.psc-badge {
display: inline-block;
padding: 4px 10px;
border: 2px solid var(--color-ink);
background: var(--color-paper);
font-family: var(--font-mono);
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
line-height: 1.2;
color: var(--color-ink);
}
.psc-badge--mint { background: var(--color-mint); }
.psc-badge--pink { background: var(--color-pink); }
.psc-badge--ink {
background: var(--color-ink);
color: var(--color-paper);
}
/* ===========================
DIALOG
=========================== */
.psc-dialog-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.55);
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.psc-dialog {
background: var(--color-paper);
border: 2px solid var(--color-ink);
padding: 0;
max-width: 520px;
width: 100%;
color: var(--color-ink);
display: flex;
flex-direction: column;
}
.psc-dialog__head {
padding: 16px 20px;
border-bottom: 2px solid var(--color-ink);
display: flex;
justify-content: space-between;
align-items: center;
background: var(--color-pink);
}
.psc-dialog__title {
font-family: var(--font-display);
font-weight: 600;
font-size: 16px;
text-transform: uppercase;
letter-spacing: -0.01em;
}
.psc-dialog__close {
background: none;
border: none;
font-family: var(--font-mono);
font-size: 14px;
cursor: pointer;
}
.psc-dialog__body { padding: 24px; }
/* ===========================
TABS
=========================== */
.psc-tabs {
display: grid;
grid-template-columns: repeat(var(--psc-tab-count, 3), 1fr);
gap: var(--border-width);
background-color: var(--color-ink);
}
.psc-tab {
background: var(--color-paper);
padding: 12px 16px;
border: none;
font-family: var(--font-mono);
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: var(--label-tracking);
cursor: pointer;
color: var(--color-ink);
text-align: center;
}
.psc-tab.is-active {
background: var(--color-ink);
color: var(--color-paper);
}
/* ===========================
SWITCH
=========================== */
.psc-switch {
display: inline-flex;
align-items: center;
gap: 10px;
cursor: pointer;
font-family: var(--font-mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.psc-switch__track {
width: 44px;
height: 22px;
background: var(--color-paper);
border: 2px solid var(--color-ink);
position: relative;
transition: background 120ms ease;
}
.psc-switch__thumb {
position: absolute;
top: 0;
left: 0;
width: 18px;
height: 18px;
background: var(--color-ink);
transition: left 120ms ease;
}
.psc-switch[data-checked="true"] .psc-switch__track {
background: var(--color-mint);
}
.psc-switch[data-checked="true"] .psc-switch__thumb {
left: 22px;
}
/* ===========================
TOAST
=========================== */
.psc-toast {
background: var(--color-paper);
border: 2px solid var(--color-ink);
padding: 12px 16px;
min-width: 240px;
color: var(--color-ink);
font-family: var(--font-display);
display: flex;
flex-direction: column;
gap: 4px;
}
.psc-toast__title {
font-family: var(--font-mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.1em;
font-weight: 700;
}
.psc-toast__desc {
font-size: 13px;
opacity: 0.75;
}
.psc-toast--success { border-left: 8px solid var(--color-mint); }
.psc-toast--error { border-left: 8px solid var(--color-pink); }
/* ===========================
RESPONSIVE
=========================== */
@media (max-width: 720px) {
.psc-cell { padding: 16px; }
.psc-display-title { font-size: 56px; }
.psc-vertical { display: none; }
}
Step 5 — Wrap the section/page you want themed with <div data-system="pascal">:
<div data-system="pascal">
<Button>HELLO PASCAL</Button>
<Card>...</Card>
</div>
Done. Now <Button>, <Input>, <Card>, <Badge>, <Dialog>, <Tabs>, <Switch>, <Toast>, <Select>, <Textarea> render in Pascal.
Install the Wardrobe Pascal theme in this v0 project. Treat each step below as a concrete file or edit you must make.
Step 1 — Add this to globals.css:
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Space+Grotesk:wght@300;400;500;600;700&display=swap");
/* =========================================================
Wardrobe — PASCAL tokens
Brutalist editorial bureaucracy. Paper-logic UI, form-field
aesthetic, Swiss-grid-meets-tax-document. Black grid lines,
paper cells with mint/pink accents, Space Grotesk display
+ IBM Plex Mono labels. Activate via data-system="pascal".
========================================================= */
:where([data-system="pascal"]) {
/* ---- Surface ---- */
--color-bg: #000000; /* ink — body bg, grid lines */
--color-paper: #f2f2f2; /* cell bg */
--color-paper-soft: #e5e5e5; /* table headers, dividers */
--color-mint: #4ea884; /* mint accent */
--color-pink: #ebaec1; /* pink accent */
--color-ink: #000000; /* primary text */
--color-ink-soft: rgba(0, 0, 0, 0.6);
--color-ink-faint: rgba(0, 0, 0, 0.1);
/* Aliases used throughout the SuperYes-style designs */
--bg-paper: #f2f2f2;
--bg-mint: #4ea884;
--bg-pink: #ebaec1;
--ink: #000000;
/* ---- Borders + grid ---- */
--border-width: 2px;
--border-ink: 2px solid var(--color-ink);
/* ---- Typography ---- */
--font-display: "Space Grotesk", system-ui, -apple-system, sans-serif;
--font-main: "Space Grotesk", system-ui, -apple-system, sans-serif;
--font-mono: "IBM Plex Mono", "Courier New", Courier, monospace;
--weight-light: 300;
--weight-regular: 400;
--weight-medium: 500;
--weight-semibold: 600;
--weight-bold: 700;
--tight-tracking: -0.04em;
--label-tracking: 0.05em;
/* ---- Radii (almost none — this theme is angular) ---- */
--radius-none: 0;
--radius-stamp: 50%;
}
Step 2 — Create components/ui/wardrobe/cn.ts:
export function cn(...classes: Array<string | false | null | undefined>): string {
return classes.filter(Boolean).join(" ");
}
Step 3 — Create the following 10 component files in components/ui/wardrobe/:
--- components/ui/wardrobe/Button.tsx ---
import * as React from "react";
import { cn } from "./cn";
type ButtonProps = {
variant?: "primary" | "secondary" | "destructive" | "ghost";
size?: "sm" | "md" | "lg";
children: React.ReactNode;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
export function Button({
variant = "primary",
size = "md",
className,
children,
...props
}: ButtonProps) {
return (
<button
className={cn("psc-btn", `psc-btn--${variant}`, size !== "md" && `psc-btn--${size}`, className)}
{...props}
>
{children}
</button>
);
}
--- components/ui/wardrobe/Input.tsx ---
import * as React from "react";
import { cn } from "./cn";
type InputProps = {
size?: "sm" | "md" | "lg";
variant?: "default" | "error";
label?: string;
hint?: string;
fieldCode?: string;
} & React.InputHTMLAttributes<HTMLInputElement>;
export function Input({
label,
hint,
fieldCode,
className,
id,
...props
}: InputProps) {
const inputId = id ?? React.useId();
return (
<div className="psc-field">
{(label || fieldCode) && (
<div className="psc-field__head">
{fieldCode && <span className="psc-field__code">{fieldCode}</span>}
{label && (
<label htmlFor={inputId} className="psc-field__label">
{label}
</label>
)}
</div>
)}
<input
id={inputId}
className={cn("psc-field__input", className)}
{...props}
/>
{hint && (
<span style={{ fontFamily: "var(--font-mono)", fontSize: 10, opacity: 0.6, textTransform: "uppercase", letterSpacing: "0.05em" }}>
{hint}
</span>
)}
</div>
);
}
--- components/ui/wardrobe/Textarea.tsx ---
import * as React from "react";
import { cn } from "./cn";
type TextareaProps = {
size?: "sm" | "md" | "lg";
variant?: "default" | "error";
label?: string;
hint?: string;
fieldCode?: string;
} & React.TextareaHTMLAttributes<HTMLTextAreaElement>;
export function Textarea({
label,
hint,
fieldCode,
className,
id,
...props
}: TextareaProps) {
const textareaId = id ?? React.useId();
return (
<div className="psc-field">
{(label || fieldCode) && (
<div className="psc-field__head">
{fieldCode && <span className="psc-field__code">{fieldCode}</span>}
{label && (
<label htmlFor={textareaId} className="psc-field__label">
{label}
</label>
)}
</div>
)}
<textarea
id={textareaId}
className={cn("psc-field__input", className)}
{...props}
/>
{hint && (
<span style={{ fontFamily: "var(--font-mono)", fontSize: 10, opacity: 0.6, textTransform: "uppercase", letterSpacing: "0.05em" }}>
{hint}
</span>
)}
</div>
);
}
--- components/ui/wardrobe/Select.tsx ---
import * as React from "react";
import { cn } from "./cn";
type Option = { value: string; label: string };
type SelectProps = {
options: Option[];
value: string;
onValueChange: (value: string) => void;
placeholder?: string;
} & Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "value" | "onChange">;
export function Select({
options,
value,
onValueChange,
placeholder,
className,
...props
}: SelectProps) {
return (
<select
className={cn("psc-field__input", className)}
value={value}
onChange={(e) => onValueChange(e.target.value)}
{...props}
>
{placeholder && <option value="" disabled>{placeholder}</option>}
{options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
);
}
--- components/ui/wardrobe/Card.tsx ---
import * as React from "react";
import { cn } from "./cn";
type CardProps = {
variant?: "default" | "mint" | "pink" | "ink";
children: React.ReactNode;
} & React.HTMLAttributes<HTMLDivElement>;
export function Card({
variant = "default",
className,
children,
...props
}: CardProps) {
return (
<div
className={cn("psc-card", variant !== "default" && `psc-card--${variant}`, className)}
{...props}
>
{children}
</div>
);
}
--- components/ui/wardrobe/Badge.tsx ---
import * as React from "react";
import { cn } from "./cn";
type BadgeProps = {
variant?: "default" | "mint" | "pink" | "ink";
children: React.ReactNode;
} & React.HTMLAttributes<HTMLSpanElement>;
export function Badge({
variant = "default",
className,
children,
...props
}: BadgeProps) {
return (
<span
className={cn("psc-badge", variant !== "default" && `psc-badge--${variant}`, className)}
{...props}
>
{children}
</span>
);
}
--- components/ui/wardrobe/Dialog.tsx ---
import * as React from "react";
type DialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
children: React.ReactNode;
};
export function Dialog({ open, onOpenChange, title, children }: DialogProps) {
React.useEffect(() => {
if (!open) return;
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") onOpenChange(false);
}
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, [open, onOpenChange]);
if (!open) return null;
return (
<div
className="psc-dialog-backdrop"
role="dialog"
aria-modal="true"
aria-label={title}
onClick={(e) => {
if (e.target === e.currentTarget) onOpenChange(false);
}}
>
<div className="psc-dialog">
<div className="psc-dialog__head">
<h2 className="psc-dialog__title">{title}</h2>
<button
type="button"
className="psc-dialog__close"
onClick={() => onOpenChange(false)}
aria-label="Close"
>
✕
</button>
</div>
<div className="psc-dialog__body">{children}</div>
</div>
</div>
);
}
--- components/ui/wardrobe/Tabs.tsx ---
import * as React from "react";
import { cn } from "./cn";
type TabsProps = {
tabs: Array<{ id: string; label: string }>;
activeId: string;
onTabChange: (id: string) => void;
};
export function Tabs({ tabs, activeId, onTabChange }: TabsProps) {
return (
<div
className="psc-tabs"
role="tablist"
style={{ ["--psc-tab-count" as string]: tabs.length }}
>
{tabs.map((tab) => (
<button
key={tab.id}
type="button"
role="tab"
aria-selected={tab.id === activeId}
className={cn("psc-tab", tab.id === activeId && "is-active")}
onClick={() => onTabChange(tab.id)}
>
{tab.label}
</button>
))}
</div>
);
}
--- components/ui/wardrobe/Switch.tsx ---
import * as React from "react";
type SwitchProps = {
checked: boolean;
onCheckedChange: (checked: boolean) => void;
label?: string;
};
export function Switch({ checked, onCheckedChange, label }: SwitchProps) {
return (
<label className="psc-switch" data-checked={checked}>
<span className="psc-switch__track">
<span className="psc-switch__thumb" />
</span>
<input
type="checkbox"
checked={checked}
onChange={(e) => onCheckedChange(e.target.checked)}
style={{ position: "absolute", opacity: 0, width: 0, height: 0 }}
/>
{label && <span>{label}</span>}
</label>
);
}
--- components/ui/wardrobe/Toast.tsx ---
import * as React from "react";
import { cn } from "./cn";
type ToastProps = {
title: string;
description?: string;
variant?: "default" | "success" | "error";
};
export function Toast({ title, description, variant = "default" }: ToastProps) {
return (
<div
className={cn("psc-toast", variant !== "default" && `psc-toast--${variant}`)}
role="status"
aria-live="polite"
>
<span className="psc-toast__title">{title}</span>
{description && <span className="psc-toast__desc">{description}</span>}
</div>
);
}
Step 4 — Add globals.css component styles. Append the following to globals.css below the tokens:
/* =========================================================
Wardrobe — PASCAL globals
Loads Space Grotesk + IBM Plex Mono, then component +
decoration styles. Active under data-system="pascal".
========================================================= */
[data-system="pascal"] *,
[data-system="pascal"] *::before,
[data-system="pascal"] *::after {
box-sizing: border-box;
}
[data-system="pascal"] {
background-color: var(--color-ink);
color: var(--color-ink);
font-family: var(--font-display);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
/* ===========================
CELL (paper card with 2px ink grid)
=========================== */
.psc-cell {
background-color: var(--color-paper);
padding: 24px;
position: relative;
color: var(--color-ink);
}
.psc-cell--mint { background-color: var(--color-mint); }
.psc-cell--pink { background-color: var(--color-pink); }
.psc-cell--ink {
background-color: var(--color-ink);
color: var(--color-paper);
}
/* Grid wrapper — uses gap + black bg to paint 2px ink lines */
.psc-grid {
display: grid;
gap: var(--border-width);
background-color: var(--color-ink);
}
.psc-stack {
display: flex;
flex-direction: column;
gap: var(--border-width);
background-color: var(--color-ink);
}
.psc-row {
display: flex;
gap: var(--border-width);
background-color: var(--color-ink);
}
/* ===========================
TYPOGRAPHY
=========================== */
.psc-mono {
font-family: var(--font-mono);
font-size: 13px;
}
.psc-label-tiny {
font-family: var(--font-mono);
font-size: 11px;
line-height: 1;
text-transform: uppercase;
letter-spacing: var(--label-tracking);
opacity: 0.8;
display: block;
margin-bottom: 10px;
}
.psc-display-title {
font-family: var(--font-display);
font-size: clamp(48px, 8vw, 80px);
line-height: 0.85;
font-weight: 700;
text-transform: uppercase;
letter-spacing: var(--tight-tracking);
}
.psc-display-huge {
font-family: var(--font-display);
font-size: clamp(32px, 5vw, 64px);
line-height: 0.9;
font-weight: 500;
text-transform: uppercase;
letter-spacing: -0.02em;
}
.psc-display-large {
font-family: var(--font-display);
font-size: clamp(36px, 6vw, 72px);
line-height: 0.9;
font-weight: 600;
text-transform: uppercase;
letter-spacing: -0.03em;
}
.psc-display-med {
font-family: var(--font-display);
font-size: 24px;
line-height: 0.95;
font-weight: 500;
text-transform: uppercase;
}
/* ===========================
MARQUEE TICKER
=========================== */
.psc-marquee {
background-color: var(--color-ink);
color: var(--color-paper);
padding: 12px 0;
overflow: hidden;
white-space: nowrap;
font-family: var(--font-mono);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.05em;
position: relative;
}
.psc-marquee__track {
display: inline-block;
padding-left: 100%;
animation: psc-marquee-scroll 28s linear infinite;
}
@keyframes psc-marquee-scroll {
0% { transform: translateX(0); }
100% { transform: translateX(-100%); }
}
/* ===========================
STAMP (rotated round border)
=========================== */
.psc-stamp {
width: 80px;
height: 80px;
border: 2px solid var(--color-ink);
border-radius: var(--radius-stamp);
display: flex;
align-items: center;
justify-content: center;
text-align: center;
font-family: var(--font-mono);
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
line-height: 1.1;
transform: rotate(-15deg);
padding: 8px;
}
.psc-stamp--double {
border: 3px double var(--color-ink);
}
/* ===========================
VERTICAL LABEL (writing-mode)
=========================== */
.psc-vertical {
writing-mode: vertical-rl;
transform: rotate(180deg);
font-family: var(--font-mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.15em;
opacity: 0.65;
white-space: nowrap;
line-height: 1;
}
/* ===========================
STATUS BADGE
=========================== */
.psc-status {
display: inline-block;
padding: 4px 10px;
border: 2px solid var(--color-ink);
background: var(--color-paper);
font-family: var(--font-mono);
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
line-height: 1.2;
color: var(--color-ink);
}
.psc-status--mint { background: var(--color-mint); }
.psc-status--pink { background: var(--color-pink); }
.psc-status--ink {
background: var(--color-ink);
color: var(--color-paper);
}
/* ===========================
FIELD GROUP (FLD_NN inputs)
=========================== */
.psc-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.psc-field__head {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 8px;
font-family: var(--font-mono);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.1em;
}
.psc-field__code {
color: var(--color-ink);
opacity: 0.55;
font-weight: 600;
}
.psc-field__label {
color: var(--color-ink);
font-weight: 600;
}
.psc-field__input {
background: transparent;
border: none;
border-bottom: 2px solid var(--color-ink);
border-radius: 0;
padding: 8px 0;
font-family: var(--font-mono);
font-size: 14px;
color: var(--color-ink);
width: 100%;
}
.psc-field__input:focus {
outline: none;
background: rgba(0, 0, 0, 0.04);
}
.psc-field__input::placeholder {
color: var(--color-ink);
opacity: 0.35;
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 12px;
}
textarea.psc-field__input {
border: 2px solid var(--color-ink);
padding: 10px 12px;
min-height: 100px;
line-height: 1.5;
resize: vertical;
}
select.psc-field__input {
appearance: none;
border: 2px solid var(--color-ink);
padding: 10px 12px;
background-image: linear-gradient(45deg, transparent 50%, var(--color-ink) 50%),
linear-gradient(135deg, var(--color-ink) 50%, transparent 50%);
background-position: calc(100% - 18px) 55%, calc(100% - 13px) 55%;
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
padding-right: 2.5rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* ===========================
BUTTONS
=========================== */
.psc-btn {
font-family: var(--font-mono);
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
font-size: 12px;
border: 2px solid var(--color-ink);
cursor: pointer;
background: var(--color-paper);
color: var(--color-ink);
padding: 12px 22px;
transition: background 100ms ease, color 100ms ease;
line-height: 1.1;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
border-radius: 0;
}
.psc-btn:hover { background: var(--color-mint); }
.psc-btn:active { transform: translate(1px, 1px); }
.psc-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; background: var(--color-paper); }
.psc-btn--primary {
background: var(--color-ink);
color: var(--color-paper);
}
.psc-btn--primary:hover {
background: var(--color-mint);
color: var(--color-ink);
}
.psc-btn--secondary {
background: var(--color-paper);
color: var(--color-ink);
}
.psc-btn--secondary:hover { background: var(--color-pink); }
.psc-btn--destructive {
background: var(--color-pink);
color: var(--color-ink);
}
.psc-btn--destructive:hover { background: var(--color-ink); color: var(--color-pink); }
.psc-btn--ghost {
background: transparent;
color: var(--color-ink);
}
.psc-btn--ghost:hover { background: rgba(0, 0, 0, 0.06); }
.psc-btn--sm { padding: 8px 14px; font-size: 10px; }
.psc-btn--lg { padding: 16px 28px; font-size: 13px; }
/* ===========================
DASHED CTA (empty-state container)
=========================== */
.psc-dashed-cta {
border: 2px dashed var(--color-ink);
background: transparent;
padding: 32px 28px;
text-align: center;
cursor: pointer;
transition: background 120ms ease;
}
.psc-dashed-cta:hover { background: rgba(0, 0, 0, 0.04); }
/* ===========================
TOC ITEMS (used in reading view)
=========================== */
.psc-toc-item {
padding: 12px 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
font-family: var(--font-mono);
font-size: 12px;
cursor: pointer;
transition: background 0.2s;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--color-ink);
display: block;
text-decoration: none;
}
.psc-toc-item:hover { background: rgba(0, 0, 0, 0.05); }
.psc-toc-item.is-active {
background: var(--color-mint);
font-weight: 600;
}
/* ===========================
TABLE (table-row pattern from log-index)
=========================== */
.psc-table {
display: flex;
flex-direction: column;
gap: var(--border-width);
background-color: var(--color-ink);
width: 100%;
}
.psc-thead,
.psc-trow {
display: grid;
gap: var(--border-width);
background-color: var(--color-ink);
}
.psc-th {
background-color: var(--color-paper-soft);
padding: 10px 12px;
font-family: var(--font-mono);
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: var(--label-tracking);
}
.psc-td {
background-color: var(--color-paper);
padding: 12px;
font-family: var(--font-mono);
font-size: 12px;
display: flex;
align-items: center;
color: var(--color-ink);
}
.psc-td--title {
font-family: var(--font-display);
font-weight: 500;
text-transform: uppercase;
line-height: 1.1;
font-size: 14px;
}
.psc-trow:hover .psc-td { background-color: #ffffff; }
/* ===========================
CARD (standard component)
=========================== */
.psc-card {
background: var(--color-paper);
border: 2px solid var(--color-ink);
padding: 24px;
color: var(--color-ink);
}
.psc-card--mint { background: var(--color-mint); }
.psc-card--pink { background: var(--color-pink); }
.psc-card--ink {
background: var(--color-ink);
color: var(--color-paper);
}
/* ===========================
BADGE (standard component, status variants)
=========================== */
.psc-badge {
display: inline-block;
padding: 4px 10px;
border: 2px solid var(--color-ink);
background: var(--color-paper);
font-family: var(--font-mono);
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
line-height: 1.2;
color: var(--color-ink);
}
.psc-badge--mint { background: var(--color-mint); }
.psc-badge--pink { background: var(--color-pink); }
.psc-badge--ink {
background: var(--color-ink);
color: var(--color-paper);
}
/* ===========================
DIALOG
=========================== */
.psc-dialog-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.55);
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.psc-dialog {
background: var(--color-paper);
border: 2px solid var(--color-ink);
padding: 0;
max-width: 520px;
width: 100%;
color: var(--color-ink);
display: flex;
flex-direction: column;
}
.psc-dialog__head {
padding: 16px 20px;
border-bottom: 2px solid var(--color-ink);
display: flex;
justify-content: space-between;
align-items: center;
background: var(--color-pink);
}
.psc-dialog__title {
font-family: var(--font-display);
font-weight: 600;
font-size: 16px;
text-transform: uppercase;
letter-spacing: -0.01em;
}
.psc-dialog__close {
background: none;
border: none;
font-family: var(--font-mono);
font-size: 14px;
cursor: pointer;
}
.psc-dialog__body { padding: 24px; }
/* ===========================
TABS
=========================== */
.psc-tabs {
display: grid;
grid-template-columns: repeat(var(--psc-tab-count, 3), 1fr);
gap: var(--border-width);
background-color: var(--color-ink);
}
.psc-tab {
background: var(--color-paper);
padding: 12px 16px;
border: none;
font-family: var(--font-mono);
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: var(--label-tracking);
cursor: pointer;
color: var(--color-ink);
text-align: center;
}
.psc-tab.is-active {
background: var(--color-ink);
color: var(--color-paper);
}
/* ===========================
SWITCH
=========================== */
.psc-switch {
display: inline-flex;
align-items: center;
gap: 10px;
cursor: pointer;
font-family: var(--font-mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.psc-switch__track {
width: 44px;
height: 22px;
background: var(--color-paper);
border: 2px solid var(--color-ink);
position: relative;
transition: background 120ms ease;
}
.psc-switch__thumb {
position: absolute;
top: 0;
left: 0;
width: 18px;
height: 18px;
background: var(--color-ink);
transition: left 120ms ease;
}
.psc-switch[data-checked="true"] .psc-switch__track {
background: var(--color-mint);
}
.psc-switch[data-checked="true"] .psc-switch__thumb {
left: 22px;
}
/* ===========================
TOAST
=========================== */
.psc-toast {
background: var(--color-paper);
border: 2px solid var(--color-ink);
padding: 12px 16px;
min-width: 240px;
color: var(--color-ink);
font-family: var(--font-display);
display: flex;
flex-direction: column;
gap: 4px;
}
.psc-toast__title {
font-family: var(--font-mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.1em;
font-weight: 700;
}
.psc-toast__desc {
font-size: 13px;
opacity: 0.75;
}
.psc-toast--success { border-left: 8px solid var(--color-mint); }
.psc-toast--error { border-left: 8px solid var(--color-pink); }
/* ===========================
RESPONSIVE
=========================== */
@media (max-width: 720px) {
.psc-cell { padding: 16px; }
.psc-display-title { font-size: 56px; }
.psc-vertical { display: none; }
}
Step 5 — Wrap the section/page you want themed with <div data-system="pascal">:
<div data-system="pascal">
<Button>HELLO PASCAL</Button>
<Card>...</Card>
</div>
Done. Now <Button>, <Input>, <Card>, <Badge>, <Dialog>, <Tabs>, <Switch>, <Toast>, <Select>, <Textarea> render in Pascal.
Install the Wardrobe Pascal theme in my Lovable project. Each step below maps to a specific file or change.
Step 1 — Add this to globals.css:
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Space+Grotesk:wght@300;400;500;600;700&display=swap");
/* =========================================================
Wardrobe — PASCAL tokens
Brutalist editorial bureaucracy. Paper-logic UI, form-field
aesthetic, Swiss-grid-meets-tax-document. Black grid lines,
paper cells with mint/pink accents, Space Grotesk display
+ IBM Plex Mono labels. Activate via data-system="pascal".
========================================================= */
:where([data-system="pascal"]) {
/* ---- Surface ---- */
--color-bg: #000000; /* ink — body bg, grid lines */
--color-paper: #f2f2f2; /* cell bg */
--color-paper-soft: #e5e5e5; /* table headers, dividers */
--color-mint: #4ea884; /* mint accent */
--color-pink: #ebaec1; /* pink accent */
--color-ink: #000000; /* primary text */
--color-ink-soft: rgba(0, 0, 0, 0.6);
--color-ink-faint: rgba(0, 0, 0, 0.1);
/* Aliases used throughout the SuperYes-style designs */
--bg-paper: #f2f2f2;
--bg-mint: #4ea884;
--bg-pink: #ebaec1;
--ink: #000000;
/* ---- Borders + grid ---- */
--border-width: 2px;
--border-ink: 2px solid var(--color-ink);
/* ---- Typography ---- */
--font-display: "Space Grotesk", system-ui, -apple-system, sans-serif;
--font-main: "Space Grotesk", system-ui, -apple-system, sans-serif;
--font-mono: "IBM Plex Mono", "Courier New", Courier, monospace;
--weight-light: 300;
--weight-regular: 400;
--weight-medium: 500;
--weight-semibold: 600;
--weight-bold: 700;
--tight-tracking: -0.04em;
--label-tracking: 0.05em;
/* ---- Radii (almost none — this theme is angular) ---- */
--radius-none: 0;
--radius-stamp: 50%;
}
Step 2 — Create components/ui/wardrobe/cn.ts:
export function cn(...classes: Array<string | false | null | undefined>): string {
return classes.filter(Boolean).join(" ");
}
Step 3 — Create the following 10 component files in components/ui/wardrobe/:
--- components/ui/wardrobe/Button.tsx ---
import * as React from "react";
import { cn } from "./cn";
type ButtonProps = {
variant?: "primary" | "secondary" | "destructive" | "ghost";
size?: "sm" | "md" | "lg";
children: React.ReactNode;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
export function Button({
variant = "primary",
size = "md",
className,
children,
...props
}: ButtonProps) {
return (
<button
className={cn("psc-btn", `psc-btn--${variant}`, size !== "md" && `psc-btn--${size}`, className)}
{...props}
>
{children}
</button>
);
}
--- components/ui/wardrobe/Input.tsx ---
import * as React from "react";
import { cn } from "./cn";
type InputProps = {
size?: "sm" | "md" | "lg";
variant?: "default" | "error";
label?: string;
hint?: string;
fieldCode?: string;
} & React.InputHTMLAttributes<HTMLInputElement>;
export function Input({
label,
hint,
fieldCode,
className,
id,
...props
}: InputProps) {
const inputId = id ?? React.useId();
return (
<div className="psc-field">
{(label || fieldCode) && (
<div className="psc-field__head">
{fieldCode && <span className="psc-field__code">{fieldCode}</span>}
{label && (
<label htmlFor={inputId} className="psc-field__label">
{label}
</label>
)}
</div>
)}
<input
id={inputId}
className={cn("psc-field__input", className)}
{...props}
/>
{hint && (
<span style={{ fontFamily: "var(--font-mono)", fontSize: 10, opacity: 0.6, textTransform: "uppercase", letterSpacing: "0.05em" }}>
{hint}
</span>
)}
</div>
);
}
--- components/ui/wardrobe/Textarea.tsx ---
import * as React from "react";
import { cn } from "./cn";
type TextareaProps = {
size?: "sm" | "md" | "lg";
variant?: "default" | "error";
label?: string;
hint?: string;
fieldCode?: string;
} & React.TextareaHTMLAttributes<HTMLTextAreaElement>;
export function Textarea({
label,
hint,
fieldCode,
className,
id,
...props
}: TextareaProps) {
const textareaId = id ?? React.useId();
return (
<div className="psc-field">
{(label || fieldCode) && (
<div className="psc-field__head">
{fieldCode && <span className="psc-field__code">{fieldCode}</span>}
{label && (
<label htmlFor={textareaId} className="psc-field__label">
{label}
</label>
)}
</div>
)}
<textarea
id={textareaId}
className={cn("psc-field__input", className)}
{...props}
/>
{hint && (
<span style={{ fontFamily: "var(--font-mono)", fontSize: 10, opacity: 0.6, textTransform: "uppercase", letterSpacing: "0.05em" }}>
{hint}
</span>
)}
</div>
);
}
--- components/ui/wardrobe/Select.tsx ---
import * as React from "react";
import { cn } from "./cn";
type Option = { value: string; label: string };
type SelectProps = {
options: Option[];
value: string;
onValueChange: (value: string) => void;
placeholder?: string;
} & Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "value" | "onChange">;
export function Select({
options,
value,
onValueChange,
placeholder,
className,
...props
}: SelectProps) {
return (
<select
className={cn("psc-field__input", className)}
value={value}
onChange={(e) => onValueChange(e.target.value)}
{...props}
>
{placeholder && <option value="" disabled>{placeholder}</option>}
{options.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
);
}
--- components/ui/wardrobe/Card.tsx ---
import * as React from "react";
import { cn } from "./cn";
type CardProps = {
variant?: "default" | "mint" | "pink" | "ink";
children: React.ReactNode;
} & React.HTMLAttributes<HTMLDivElement>;
export function Card({
variant = "default",
className,
children,
...props
}: CardProps) {
return (
<div
className={cn("psc-card", variant !== "default" && `psc-card--${variant}`, className)}
{...props}
>
{children}
</div>
);
}
--- components/ui/wardrobe/Badge.tsx ---
import * as React from "react";
import { cn } from "./cn";
type BadgeProps = {
variant?: "default" | "mint" | "pink" | "ink";
children: React.ReactNode;
} & React.HTMLAttributes<HTMLSpanElement>;
export function Badge({
variant = "default",
className,
children,
...props
}: BadgeProps) {
return (
<span
className={cn("psc-badge", variant !== "default" && `psc-badge--${variant}`, className)}
{...props}
>
{children}
</span>
);
}
--- components/ui/wardrobe/Dialog.tsx ---
import * as React from "react";
type DialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
title: string;
children: React.ReactNode;
};
export function Dialog({ open, onOpenChange, title, children }: DialogProps) {
React.useEffect(() => {
if (!open) return;
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") onOpenChange(false);
}
document.addEventListener("keydown", onKey);
return () => document.removeEventListener("keydown", onKey);
}, [open, onOpenChange]);
if (!open) return null;
return (
<div
className="psc-dialog-backdrop"
role="dialog"
aria-modal="true"
aria-label={title}
onClick={(e) => {
if (e.target === e.currentTarget) onOpenChange(false);
}}
>
<div className="psc-dialog">
<div className="psc-dialog__head">
<h2 className="psc-dialog__title">{title}</h2>
<button
type="button"
className="psc-dialog__close"
onClick={() => onOpenChange(false)}
aria-label="Close"
>
✕
</button>
</div>
<div className="psc-dialog__body">{children}</div>
</div>
</div>
);
}
--- components/ui/wardrobe/Tabs.tsx ---
import * as React from "react";
import { cn } from "./cn";
type TabsProps = {
tabs: Array<{ id: string; label: string }>;
activeId: string;
onTabChange: (id: string) => void;
};
export function Tabs({ tabs, activeId, onTabChange }: TabsProps) {
return (
<div
className="psc-tabs"
role="tablist"
style={{ ["--psc-tab-count" as string]: tabs.length }}
>
{tabs.map((tab) => (
<button
key={tab.id}
type="button"
role="tab"
aria-selected={tab.id === activeId}
className={cn("psc-tab", tab.id === activeId && "is-active")}
onClick={() => onTabChange(tab.id)}
>
{tab.label}
</button>
))}
</div>
);
}
--- components/ui/wardrobe/Switch.tsx ---
import * as React from "react";
type SwitchProps = {
checked: boolean;
onCheckedChange: (checked: boolean) => void;
label?: string;
};
export function Switch({ checked, onCheckedChange, label }: SwitchProps) {
return (
<label className="psc-switch" data-checked={checked}>
<span className="psc-switch__track">
<span className="psc-switch__thumb" />
</span>
<input
type="checkbox"
checked={checked}
onChange={(e) => onCheckedChange(e.target.checked)}
style={{ position: "absolute", opacity: 0, width: 0, height: 0 }}
/>
{label && <span>{label}</span>}
</label>
);
}
--- components/ui/wardrobe/Toast.tsx ---
import * as React from "react";
import { cn } from "./cn";
type ToastProps = {
title: string;
description?: string;
variant?: "default" | "success" | "error";
};
export function Toast({ title, description, variant = "default" }: ToastProps) {
return (
<div
className={cn("psc-toast", variant !== "default" && `psc-toast--${variant}`)}
role="status"
aria-live="polite"
>
<span className="psc-toast__title">{title}</span>
{description && <span className="psc-toast__desc">{description}</span>}
</div>
);
}
Step 4 — Add globals.css component styles. Append the following to globals.css below the tokens:
/* =========================================================
Wardrobe — PASCAL globals
Loads Space Grotesk + IBM Plex Mono, then component +
decoration styles. Active under data-system="pascal".
========================================================= */
[data-system="pascal"] *,
[data-system="pascal"] *::before,
[data-system="pascal"] *::after {
box-sizing: border-box;
}
[data-system="pascal"] {
background-color: var(--color-ink);
color: var(--color-ink);
font-family: var(--font-display);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
/* ===========================
CELL (paper card with 2px ink grid)
=========================== */
.psc-cell {
background-color: var(--color-paper);
padding: 24px;
position: relative;
color: var(--color-ink);
}
.psc-cell--mint { background-color: var(--color-mint); }
.psc-cell--pink { background-color: var(--color-pink); }
.psc-cell--ink {
background-color: var(--color-ink);
color: var(--color-paper);
}
/* Grid wrapper — uses gap + black bg to paint 2px ink lines */
.psc-grid {
display: grid;
gap: var(--border-width);
background-color: var(--color-ink);
}
.psc-stack {
display: flex;
flex-direction: column;
gap: var(--border-width);
background-color: var(--color-ink);
}
.psc-row {
display: flex;
gap: var(--border-width);
background-color: var(--color-ink);
}
/* ===========================
TYPOGRAPHY
=========================== */
.psc-mono {
font-family: var(--font-mono);
font-size: 13px;
}
.psc-label-tiny {
font-family: var(--font-mono);
font-size: 11px;
line-height: 1;
text-transform: uppercase;
letter-spacing: var(--label-tracking);
opacity: 0.8;
display: block;
margin-bottom: 10px;
}
.psc-display-title {
font-family: var(--font-display);
font-size: clamp(48px, 8vw, 80px);
line-height: 0.85;
font-weight: 700;
text-transform: uppercase;
letter-spacing: var(--tight-tracking);
}
.psc-display-huge {
font-family: var(--font-display);
font-size: clamp(32px, 5vw, 64px);
line-height: 0.9;
font-weight: 500;
text-transform: uppercase;
letter-spacing: -0.02em;
}
.psc-display-large {
font-family: var(--font-display);
font-size: clamp(36px, 6vw, 72px);
line-height: 0.9;
font-weight: 600;
text-transform: uppercase;
letter-spacing: -0.03em;
}
.psc-display-med {
font-family: var(--font-display);
font-size: 24px;
line-height: 0.95;
font-weight: 500;
text-transform: uppercase;
}
/* ===========================
MARQUEE TICKER
=========================== */
.psc-marquee {
background-color: var(--color-ink);
color: var(--color-paper);
padding: 12px 0;
overflow: hidden;
white-space: nowrap;
font-family: var(--font-mono);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.05em;
position: relative;
}
.psc-marquee__track {
display: inline-block;
padding-left: 100%;
animation: psc-marquee-scroll 28s linear infinite;
}
@keyframes psc-marquee-scroll {
0% { transform: translateX(0); }
100% { transform: translateX(-100%); }
}
/* ===========================
STAMP (rotated round border)
=========================== */
.psc-stamp {
width: 80px;
height: 80px;
border: 2px solid var(--color-ink);
border-radius: var(--radius-stamp);
display: flex;
align-items: center;
justify-content: center;
text-align: center;
font-family: var(--font-mono);
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
line-height: 1.1;
transform: rotate(-15deg);
padding: 8px;
}
.psc-stamp--double {
border: 3px double var(--color-ink);
}
/* ===========================
VERTICAL LABEL (writing-mode)
=========================== */
.psc-vertical {
writing-mode: vertical-rl;
transform: rotate(180deg);
font-family: var(--font-mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.15em;
opacity: 0.65;
white-space: nowrap;
line-height: 1;
}
/* ===========================
STATUS BADGE
=========================== */
.psc-status {
display: inline-block;
padding: 4px 10px;
border: 2px solid var(--color-ink);
background: var(--color-paper);
font-family: var(--font-mono);
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
line-height: 1.2;
color: var(--color-ink);
}
.psc-status--mint { background: var(--color-mint); }
.psc-status--pink { background: var(--color-pink); }
.psc-status--ink {
background: var(--color-ink);
color: var(--color-paper);
}
/* ===========================
FIELD GROUP (FLD_NN inputs)
=========================== */
.psc-field {
display: flex;
flex-direction: column;
gap: 6px;
}
.psc-field__head {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 8px;
font-family: var(--font-mono);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.1em;
}
.psc-field__code {
color: var(--color-ink);
opacity: 0.55;
font-weight: 600;
}
.psc-field__label {
color: var(--color-ink);
font-weight: 600;
}
.psc-field__input {
background: transparent;
border: none;
border-bottom: 2px solid var(--color-ink);
border-radius: 0;
padding: 8px 0;
font-family: var(--font-mono);
font-size: 14px;
color: var(--color-ink);
width: 100%;
}
.psc-field__input:focus {
outline: none;
background: rgba(0, 0, 0, 0.04);
}
.psc-field__input::placeholder {
color: var(--color-ink);
opacity: 0.35;
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 12px;
}
textarea.psc-field__input {
border: 2px solid var(--color-ink);
padding: 10px 12px;
min-height: 100px;
line-height: 1.5;
resize: vertical;
}
select.psc-field__input {
appearance: none;
border: 2px solid var(--color-ink);
padding: 10px 12px;
background-image: linear-gradient(45deg, transparent 50%, var(--color-ink) 50%),
linear-gradient(135deg, var(--color-ink) 50%, transparent 50%);
background-position: calc(100% - 18px) 55%, calc(100% - 13px) 55%;
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
padding-right: 2.5rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* ===========================
BUTTONS
=========================== */
.psc-btn {
font-family: var(--font-mono);
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
font-size: 12px;
border: 2px solid var(--color-ink);
cursor: pointer;
background: var(--color-paper);
color: var(--color-ink);
padding: 12px 22px;
transition: background 100ms ease, color 100ms ease;
line-height: 1.1;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
border-radius: 0;
}
.psc-btn:hover { background: var(--color-mint); }
.psc-btn:active { transform: translate(1px, 1px); }
.psc-btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; background: var(--color-paper); }
.psc-btn--primary {
background: var(--color-ink);
color: var(--color-paper);
}
.psc-btn--primary:hover {
background: var(--color-mint);
color: var(--color-ink);
}
.psc-btn--secondary {
background: var(--color-paper);
color: var(--color-ink);
}
.psc-btn--secondary:hover { background: var(--color-pink); }
.psc-btn--destructive {
background: var(--color-pink);
color: var(--color-ink);
}
.psc-btn--destructive:hover { background: var(--color-ink); color: var(--color-pink); }
.psc-btn--ghost {
background: transparent;
color: var(--color-ink);
}
.psc-btn--ghost:hover { background: rgba(0, 0, 0, 0.06); }
.psc-btn--sm { padding: 8px 14px; font-size: 10px; }
.psc-btn--lg { padding: 16px 28px; font-size: 13px; }
/* ===========================
DASHED CTA (empty-state container)
=========================== */
.psc-dashed-cta {
border: 2px dashed var(--color-ink);
background: transparent;
padding: 32px 28px;
text-align: center;
cursor: pointer;
transition: background 120ms ease;
}
.psc-dashed-cta:hover { background: rgba(0, 0, 0, 0.04); }
/* ===========================
TOC ITEMS (used in reading view)
=========================== */
.psc-toc-item {
padding: 12px 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
font-family: var(--font-mono);
font-size: 12px;
cursor: pointer;
transition: background 0.2s;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--color-ink);
display: block;
text-decoration: none;
}
.psc-toc-item:hover { background: rgba(0, 0, 0, 0.05); }
.psc-toc-item.is-active {
background: var(--color-mint);
font-weight: 600;
}
/* ===========================
TABLE (table-row pattern from log-index)
=========================== */
.psc-table {
display: flex;
flex-direction: column;
gap: var(--border-width);
background-color: var(--color-ink);
width: 100%;
}
.psc-thead,
.psc-trow {
display: grid;
gap: var(--border-width);
background-color: var(--color-ink);
}
.psc-th {
background-color: var(--color-paper-soft);
padding: 10px 12px;
font-family: var(--font-mono);
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: var(--label-tracking);
}
.psc-td {
background-color: var(--color-paper);
padding: 12px;
font-family: var(--font-mono);
font-size: 12px;
display: flex;
align-items: center;
color: var(--color-ink);
}
.psc-td--title {
font-family: var(--font-display);
font-weight: 500;
text-transform: uppercase;
line-height: 1.1;
font-size: 14px;
}
.psc-trow:hover .psc-td { background-color: #ffffff; }
/* ===========================
CARD (standard component)
=========================== */
.psc-card {
background: var(--color-paper);
border: 2px solid var(--color-ink);
padding: 24px;
color: var(--color-ink);
}
.psc-card--mint { background: var(--color-mint); }
.psc-card--pink { background: var(--color-pink); }
.psc-card--ink {
background: var(--color-ink);
color: var(--color-paper);
}
/* ===========================
BADGE (standard component, status variants)
=========================== */
.psc-badge {
display: inline-block;
padding: 4px 10px;
border: 2px solid var(--color-ink);
background: var(--color-paper);
font-family: var(--font-mono);
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
line-height: 1.2;
color: var(--color-ink);
}
.psc-badge--mint { background: var(--color-mint); }
.psc-badge--pink { background: var(--color-pink); }
.psc-badge--ink {
background: var(--color-ink);
color: var(--color-paper);
}
/* ===========================
DIALOG
=========================== */
.psc-dialog-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.55);
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.psc-dialog {
background: var(--color-paper);
border: 2px solid var(--color-ink);
padding: 0;
max-width: 520px;
width: 100%;
color: var(--color-ink);
display: flex;
flex-direction: column;
}
.psc-dialog__head {
padding: 16px 20px;
border-bottom: 2px solid var(--color-ink);
display: flex;
justify-content: space-between;
align-items: center;
background: var(--color-pink);
}
.psc-dialog__title {
font-family: var(--font-display);
font-weight: 600;
font-size: 16px;
text-transform: uppercase;
letter-spacing: -0.01em;
}
.psc-dialog__close {
background: none;
border: none;
font-family: var(--font-mono);
font-size: 14px;
cursor: pointer;
}
.psc-dialog__body { padding: 24px; }
/* ===========================
TABS
=========================== */
.psc-tabs {
display: grid;
grid-template-columns: repeat(var(--psc-tab-count, 3), 1fr);
gap: var(--border-width);
background-color: var(--color-ink);
}
.psc-tab {
background: var(--color-paper);
padding: 12px 16px;
border: none;
font-family: var(--font-mono);
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: var(--label-tracking);
cursor: pointer;
color: var(--color-ink);
text-align: center;
}
.psc-tab.is-active {
background: var(--color-ink);
color: var(--color-paper);
}
/* ===========================
SWITCH
=========================== */
.psc-switch {
display: inline-flex;
align-items: center;
gap: 10px;
cursor: pointer;
font-family: var(--font-mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.psc-switch__track {
width: 44px;
height: 22px;
background: var(--color-paper);
border: 2px solid var(--color-ink);
position: relative;
transition: background 120ms ease;
}
.psc-switch__thumb {
position: absolute;
top: 0;
left: 0;
width: 18px;
height: 18px;
background: var(--color-ink);
transition: left 120ms ease;
}
.psc-switch[data-checked="true"] .psc-switch__track {
background: var(--color-mint);
}
.psc-switch[data-checked="true"] .psc-switch__thumb {
left: 22px;
}
/* ===========================
TOAST
=========================== */
.psc-toast {
background: var(--color-paper);
border: 2px solid var(--color-ink);
padding: 12px 16px;
min-width: 240px;
color: var(--color-ink);
font-family: var(--font-display);
display: flex;
flex-direction: column;
gap: 4px;
}
.psc-toast__title {
font-family: var(--font-mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.1em;
font-weight: 700;
}
.psc-toast__desc {
font-size: 13px;
opacity: 0.75;
}
.psc-toast--success { border-left: 8px solid var(--color-mint); }
.psc-toast--error { border-left: 8px solid var(--color-pink); }
/* ===========================
RESPONSIVE
=========================== */
@media (max-width: 720px) {
.psc-cell { padding: 16px; }
.psc-display-title { font-size: 56px; }
.psc-vertical { display: none; }
}
Step 5 — Wrap the section/page you want themed with <div data-system="pascal">:
<div data-system="pascal">
<Button>HELLO PASCAL</Button>
<Card>...</Card>
</div>
Done. Now <Button>, <Input>, <Card>, <Badge>, <Dialog>, <Tabs>, <Switch>, <Toast>, <Select>, <Textarea> render in Pascal.
<div data-system="pascal">
<Button variant="primary">FILE ENTRY</Button>
<Card variant="mint">LIVE_NODE</Card>
</div> See it in the wild → ARCHIVE.NODE →
GOT IT WORKING?
File for the next theme + occasional builder notes.
No spam. ~1 email per theme drop. Maybe one extra when I ship something interesting.
One email per theme drop. Plus the occasional builder note when I ship something. No spam.
By signing up, you agree to receive emails about Wardrobe. See Privacy policy.
Liked this? Get the next theme drop →