Couldn't load the data
Try again
diff --git a/engine/.claude/skills/ss-lint/SKILL.md b/engine/.claude/skills/ss-lint/SKILL.md
index 659e2fc..659af14 100644
--- a/engine/.claude/skills/ss-lint/SKILL.md
+++ b/engine/.claude/skills/ss-lint/SKILL.md
@@ -81,6 +81,28 @@ grep -n 'className={`' [file]
**Violation:** Template literal className
**Fix:** Use `cn()` for all className composition
+---
+
+## Token contrast — WCAG (check 9, runs when a theme file exists)
+
+If the project has `css/theme.css` (or the detected theme file), run the bundled
+deterministic checker — NEVER compute WCAG luminance by mental arithmetic, the
+gamma math is exactly where LLMs slip:
+```bash
+python3 "$(dirname "$SKILL_MD")/scripts/contrast_check.py" css/theme.css
+# self-check if in doubt: ... contrast_check.py --self-test → must print OK
+```
+(`$SKILL_MD` = this SKILL.md's directory; use the literal path of the skill folder.)
+It parses `:root` + `.dark` custom properties (hex / rgb() / hsl() / oklch()),
+resolves `var()` chains, and checks the standard pairs in BOTH scopes:
+body & muted text on background/card ≥ 4.5:1 · brand UI on page ≥ 3:1 ·
+label on brand/primary/destructive controls ≥ 4.5:1 (brand-foreground —
+or the hardcoded white that pre-2.9 components used — on brand, etc.).
+**Violation:** any `🔴 FAIL` line (exit code 1).
+**Fix:** darken/lighten the failing token in `theme.css` — adjust the token, never
+the component. `🟡 SKIP` lines (alpha compositing, `color-mix()`) are NOT passes:
+report them and eyeball those pairs manually.
+
## Output Format
```
diff --git a/engine/.claude/skills/ss-lint/scripts/contrast_check.py b/engine/.claude/skills/ss-lint/scripts/contrast_check.py
new file mode 100644
index 0000000..37a88db
--- /dev/null
+++ b/engine/.claude/skills/ss-lint/scripts/contrast_check.py
@@ -0,0 +1,226 @@
+#!/usr/bin/env python3
+"""WCAG token-contrast checker for StyleSeed theme.css (ss-lint check 9).
+
+stdlib only. Parses CSS custom properties in `:root` and `.dark` blocks,
+resolves var() chains, converts hex / rgb() / hsl() / oklch() to WCAG relative
+luminance, and checks the standard StyleSeed token pairs in BOTH scopes.
+
+Exit codes: 0 = all checked pairs pass, 1 = at least one FAIL, 2 = usage/parse error.
+Unparseable values are reported as skipped (never silently passed).
+
+Usage:
+ python3 contrast_check.py path/to/theme.css
+ python3 contrast_check.py --self-test
+"""
+import math
+import re
+import sys
+
+# ---------- color parsing → linear-sRGB (0..1 per channel) ----------
+
+def _hex_to_linear(s):
+ s = s.lstrip("#")
+ if len(s) == 3:
+ s = "".join(c * 2 for c in s)
+ if len(s) == 8: # RRGGBBAA
+ if int(s[6:8], 16) != 255:
+ raise ValueError("alpha < 1 needs backdrop compositing; skipped")
+ s = s[:6]
+ if len(s) != 6:
+ raise ValueError("bad hex length")
+ srgb = [int(s[i:i + 2], 16) / 255.0 for i in (0, 2, 4)]
+ return [_srgb_decode(c) for c in srgb]
+
+
+def _srgb_decode(c):
+ # WCAG 2.x published constant (0.03928)
+ return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4
+
+
+def _num(tok, scale=1.0, pct_base=None):
+ tok = tok.strip()
+ if tok.endswith("%"):
+ base = pct_base if pct_base is not None else 100.0
+ return float(tok[:-1]) / 100.0 * base
+ return float(tok) * scale
+
+
+def _rgb_func_to_linear(args):
+ # rgb(255 0 0) | rgb(255, 0, 0) | rgba(255,0,0,1) | rgb(100% 0% 0%)
+ if len(args) == 4 and float(_num(args[3])) < 1.0:
+ raise ValueError("alpha < 1; skipped")
+ srgb = []
+ for a in args[:3]:
+ v = _num(a, pct_base=255.0) if a.strip().endswith("%") else float(a)
+ srgb.append(max(0.0, min(1.0, v / 255.0)))
+ return [_srgb_decode(c) for c in srgb]
+
+
+def _hsl_to_linear(args):
+ if len(args) == 4 and float(_num(args[3])) < 1.0:
+ raise ValueError("alpha < 1; skipped")
+ h = float(re.sub(r"deg$", "", args[0].strip())) % 360.0
+ s = _num(args[1], pct_base=1.0) if args[1].strip().endswith("%") else float(args[1])
+ l = _num(args[2], pct_base=1.0) if args[2].strip().endswith("%") else float(args[2])
+ c = (1 - abs(2 * l - 1)) * s
+ x = c * (1 - abs((h / 60.0) % 2 - 1))
+ m = l - c / 2
+ rgb1 = [(c, x, 0), (x, c, 0), (0, c, x), (0, x, c), (x, 0, c), (c, 0, x)][int(h // 60) % 6]
+ return [_srgb_decode(max(0.0, min(1.0, v + m))) for v in rgb1]
+
+
+def _oklch_to_linear(args):
+ # oklch(L C H [/ alpha]) — L may be 0..1 or a percentage
+ if len(args) == 4 and float(_num(args[3])) < 1.0:
+ raise ValueError("alpha < 1; skipped")
+ L = _num(args[0], pct_base=1.0) if args[0].strip().endswith("%") else float(args[0])
+ C = float(args[1])
+ H = math.radians(float(re.sub(r"deg$", "", args[2].strip())))
+ a, b = C * math.cos(H), C * math.sin(H)
+ l_ = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3
+ m_ = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3
+ s_ = (L - 0.0894841775 * a - 1.2914855480 * b) ** 3
+ r = 4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_
+ g = -1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_
+ bb = -0.0041960863 * l_ - 0.7034186147 * m_ + 1.7076147010 * s_
+ return [max(0.0, min(1.0, v)) for v in (r, g, bb)]
+
+
+_FUNC = {"rgb": _rgb_func_to_linear, "rgba": _rgb_func_to_linear,
+ "hsl": _hsl_to_linear, "hsla": _hsl_to_linear, "oklch": _oklch_to_linear}
+
+
+def luminance(value):
+ """CSS color string → WCAG relative luminance. Raises ValueError if unsupported."""
+ v = value.strip()
+ if v.startswith("#"):
+ lin = _hex_to_linear(v)
+ else:
+ m = re.match(r"^([a-zA-Z]+)\((.*)\)$", v)
+ if not m or m.group(1).lower() not in _FUNC:
+ raise ValueError("unsupported color syntax: %s" % v)
+ raw = m.group(2).replace("/", " ").replace(",", " ")
+ args = [t for t in raw.split() if t]
+ lin = _FUNC[m.group(1).lower()](args)
+ return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2]
+
+
+def contrast(l1, l2):
+ hi, lo = max(l1, l2), min(l1, l2)
+ return (hi + 0.05) / (lo + 0.05)
+
+# ---------- theme.css parsing ----------
+
+def _extract_block_vars(css, selector):
+ """Collect --var: value pairs from every `selector { ... }` block (flat braces).
+
+ ponytail: naive brace scan — no nested @media handling; token blocks are flat
+ in StyleSeed themes. Upgrade path: a real CSS tokenizer if themes grow nesting.
+ """
+ out = {}
+ for m in re.finditer(re.escape(selector) + r"\s*\{", css):
+ depth, i, start = 1, m.end(), m.end()
+ while i < len(css) and depth:
+ depth += {"{": 1, "}": -1}.get(css[i], 0)
+ i += 1
+ body = css[start:i - 1]
+ for vm in re.finditer(r"--([\w-]+)\s*:\s*([^;]+);", body):
+ out[vm.group(1)] = vm.group(2).strip()
+ return out
+
+
+def _resolve(name, scope, depth=0):
+ if depth > 8 or name not in scope:
+ return None
+ val = scope[name]
+ vm = re.match(r"^var\(\s*--([\w-]+)\s*(?:,[^)]*)?\)$", val)
+ return _resolve(vm.group(1), scope, depth + 1) if vm else val
+
+# ---------- pair table ----------
+
+# (role_fg candidates — token names or '#literal', role_bg candidates, min ratio, label)
+# The fg candidates mirror what StyleSeed components actually render: buttons/chips put
+# brand-foreground (older skins: hardcoded white) on brand/destructive, and badge/tooltip/
+# checkbox put primary-foreground on primary. Don't pair primary-foreground with brand —
+# no component renders that combination.
+PAIRS = [
+ (["foreground", "text-primary"], ["background"], 4.5, "body text on page"),
+ (["foreground", "text-primary"], ["card"], 4.5, "body text on card"),
+ (["muted-foreground", "text-secondary"], ["background"], 4.5, "muted text on page"),
+ (["muted-foreground", "text-secondary"], ["card"], 4.5, "muted text on card"),
+ (["brand", "primary"], ["background"], 3.0, "brand UI on page (3:1 non-text)"),
+ (["brand-foreground", "#ffffff"], ["brand"], 4.5, "label on brand button"),
+ (["primary-foreground"], ["primary"], 4.5, "label on primary (badge/tooltip)"),
+ (["destructive-foreground", "#ffffff"], ["destructive"], 4.5, "label on destructive"),
+]
+
+
+def check_file(path):
+ try:
+ css = open(path, encoding="utf-8").read()
+ except OSError as e:
+ print("🔴 ERROR cannot read %s: %s" % (path, e))
+ return 2
+ root = _extract_block_vars(css, ":root")
+ dark_only = _extract_block_vars(css, ".dark")
+ if not root and not dark_only:
+ print("🔴 ERROR no :root/.dark custom properties found in %s" % path)
+ return 2
+ scopes = [("light", root)]
+ if dark_only:
+ merged = dict(root)
+ merged.update(dark_only)
+ scopes.append(("dark", merged))
+
+ fails, skipped, checked = 0, 0, 0
+ for scope_name, scope in scopes:
+ for fg_names, bg_names, minimum, label in PAIRS:
+ fg_name = next((n for n in fg_names if n.startswith("#") or n in scope), None)
+ bg_name = next((n for n in bg_names if n in scope), None)
+ if not fg_name or not bg_name:
+ continue # pair not present in this theme — not an error
+ fg_val = fg_name if fg_name.startswith("#") else _resolve(fg_name, scope)
+ bg_val = _resolve(bg_name, scope)
+ fg_disp = fg_name if fg_name.startswith("#") else "--" + fg_name
+ try:
+ ratio = contrast(luminance(fg_val), luminance(bg_val))
+ except (ValueError, TypeError) as e:
+ skipped += 1
+ print("🟡 SKIP [%s] %s (%s on --%s): %s" % (scope_name, label, fg_disp, bg_name, e))
+ continue
+ checked += 1
+ mark = "🟢 PASS" if ratio >= minimum else "🔴 FAIL"
+ fails += 0 if ratio >= minimum else 1
+ print("%s [%s] %s: %s on --%s = %.2f:1 (min %.1f)"
+ % (mark, scope_name, label, fg_disp, bg_name, ratio, minimum))
+ print("Contrast: %d checked, %d failed, %d skipped" % (checked, fails, skipped))
+ return 1 if fails else 0
+
+# ---------- self-test ----------
+
+def self_test():
+ assert abs(contrast(luminance("#ffffff"), luminance("#000000")) - 21.0) < 1e-6
+ assert abs(contrast(luminance("#767676"), luminance("#ffffff")) - 4.54) < 0.02
+ assert abs(luminance("hsl(0 100% 50%)") - luminance("#ff0000")) < 1e-6
+ assert abs(luminance("oklch(0.627955 0.257683 29.2338)") - luminance("#ff0000")) < 0.005
+ assert abs(luminance("rgb(255 0 0)") - luminance("#ff0000")) < 1e-9
+ assert abs(luminance("oklch(100% 0 0)") - 1.0) < 0.001
+ scope = {"a": "var(--b)", "b": "#fff"}
+ assert _resolve("a", scope) == "#fff"
+ for bad in ("#ffffff80", "rgba(0,0,0,0.5)", "color-mix(in srgb, red, blue)"):
+ try:
+ luminance(bad)
+ raise AssertionError("should have raised: %s" % bad)
+ except ValueError:
+ pass
+ print("OK — self-test passed (7 groups)")
+ return 0
+
+
+if __name__ == "__main__":
+ if len(sys.argv) == 2 and sys.argv[1] == "--self-test":
+ sys.exit(self_test())
+ if len(sys.argv) != 2:
+ print(__doc__)
+ sys.exit(2)
+ sys.exit(check_file(sys.argv[1]))
diff --git a/engine/.claude/skills/ss-page/SKILL.md b/engine/.claude/skills/ss-page/SKILL.md
index 37e9ee3..edb43a0 100644
--- a/engine/.claude/skills/ss-page/SKILL.md
+++ b/engine/.claude/skills/ss-page/SKILL.md
@@ -20,6 +20,8 @@ Description: $ARGUMENTS
## Instructions
1. Read the design system reference:
+ - `STYLESEED.md` (project root) FIRST — the Design Lock incl. **Brand intent**; obey its
+ "Never" constraints in every section and keep the reference's implied traits visible
- `CLAUDE.md` for file structure and conventions
- `components/patterns/page-shell.tsx` for page layout
- `components/patterns/top-bar.tsx` for header pattern
@@ -72,4 +74,5 @@ export default function PageName() {
- [ ] Spacing uses 6px multiples (p-1.5, p-3, p-6)
- [ ] `mx-6` for single cards, `px-6` for grids/carousels
- [ ] Touch targets ≥ 44px on all interactive elements
+ - [ ] Brand intent respected — no "Never" violation, implied traits present (skip if no lock)
If any violation is found, fix it before presenting the page to the user.
diff --git a/engine/.claude/skills/ss-setup/SKILL.md b/engine/.claude/skills/ss-setup/SKILL.md
index afbb10c..956fe00 100644
--- a/engine/.claude/skills/ss-setup/SKILL.md
+++ b/engine/.claude/skills/ss-setup/SKILL.md
@@ -42,6 +42,13 @@ What type of app are you building?
Remember the answer — it determines which page composition recipe to use (DESIGN-LANGUAGE.md Section 63).
+Then ask the **surface** (same message is fine):
+```
+Is this primarily a mobile app or a desktop/web (B2B) product?
+```
+The surface decides the type scale (Golden Rule 16: desktop/web B2B body ≥16px,
+mobile-tight allows 14px) — record it for the lock.
+
### Step 2: Brand Color
Ask:
@@ -100,6 +107,21 @@ If they pick a brand (options 1-7 or 9):
If they pick 8 (No thanks): skip, keep current brand color from Step 2.
+**Then, whatever they chose, distill a Brand intent** (goes into the lock in Step 5):
+- **Reference** — ONE concrete reference (the brand they picked, or a product/era/artifact
+ they name). Never bare adjectives like "modern, clean".
+- **Implied traits** — 3 concrete attributes that reference implies (e.g. Stripe →
+ "restrained color · dense type · precise borders"). Spelling these out anchors the
+ reference so every model/agent reads it the same way.
+- **Never** — 3 negative constraints the reference rules out (e.g. "no gradients ·
+ no glassmorphism · no decorative serif").
+- **Feeling** — one line: what the user should feel in 3 seconds.
+In the SAME confirmation message, also propose the **Mood** (4 axes with defaults from
+the chosen skin: Edges sharp/soft/pill · Feel minimal/expressive · Density airy/compact ·
+Tone calm/playful) and the **Motion seed** derived from Tone (calm → Silk or Spring,
+playful → Pulse; skin default wins: Toss → Spring, Stripe/Notion → Silk, Linear → Snap).
+Confirm everything in ONE message, then move on.
+
### Step 4: Font
Ask:
@@ -156,11 +178,19 @@ Then:
# StyleSeed — Design Lock
- App domain: [Step 1 app type]
+ - Surface: [mobile-app | desktop-web (B2B) — from Step 1]
+ - Mood: [edges · feel · density · tone — from Step 3, e.g. soft · minimal · airy · calm]
- Skin: [Step 3 concept, or "custom"]
- Key color (accent): [Step 2 hex] # the ONLY accent — everything else greyscale
- Radius personality: [sharp | soft | pill — one everywhere]
- - Motion seed: [Spring | Silk | Snap | Float | Pulse]
- - Type: [Step 4 font]
+ - Motion seed: [Spring | Silk | Snap | Float | Pulse — from Step 3 Tone]
+ - Font: [Step 4 font]
+ - Type scale: [mobile-tight | desktop (body 16-18px) — from Surface]
+ - Brand intent:
+ - Reference: [Step 3 reference — ONE concrete product/era/artifact]
+ - Implied traits: [3 attributes the reference implies]
+ - Never: [3 negative constraints]
+ - Feeling: [one line]
- Locked: [today]
```
Tell the user this file is the source of truth — editing a value changes it project-wide,
diff --git a/engine/.claude/skills/ss-update/SKILL.md b/engine/.claude/skills/ss-update/SKILL.md
index e7a7046..e182bdc 100644
--- a/engine/.claude/skills/ss-update/SKILL.md
+++ b/engine/.claude/skills/ss-update/SKILL.md
@@ -21,9 +21,10 @@ Automatically detect and update StyleSeed files in the current project.
Updating is **safe and reversible**. Updates are additive — new rules,
components, skins, and skills get added; your `theme.css`, your components, and
-your app code are never overwritten, and design rules only ever get added (never
-changed in a breaking way). A big version jump looks like a lot changed, but
-it's almost all additions. **Do NOT warn the user that the build will break**
+your app code are never overwritten, and design rules almost only ever get added.
+Rarely an existing rule VALUE is corrected (e.g. the 2026-07 grid-unit fix, 8px → 6px);
+those are surfaced one-by-one in the Step 4 changed-rule gate — never applied silently.
+A big version jump looks like a lot changed, but it's almost all additions. **Do NOT warn the user that the build will break**
unless you actually find a changed component/import API. Tell them: commit first,
copy the new rules + skills, run a build, and `git reset --hard` if anything is
off — they can't permanently break their project.
@@ -115,16 +116,31 @@ For each update, in order:
For DESIGN-LANGUAGE.md:
- Show diff summary: how many new rules, what sections added
+- **Changed-rule gate (design-diff):** diff the Golden Rules and enforcement values
+ (grid unit, radius scale, type minimums, accent policy) specifically. Any rule that
+ CHANGED (not merely added) gets its own ⚠️ line — old value → new value → what it
+ re-flags in this project — and a per-item OK before applying. A changed enforcement
+ value can flip existing passing screens to failing, so it is never bundled into a
+ blanket "Y".
- Ask: "Update DESIGN-LANGUAGE.md? (Y/N)"
- If yes: copy to the detected location
+For theme.css — `--brand-foreground` (v2.9 components reference it):
+- If the updated components use `text-brand-foreground` but the project's theme.css
+ has no `--brand-foreground`, offer to APPEND it (one line per scope + the
+ `--color-brand-foreground: var(--brand-foreground);` mapping in `@theme inline`).
+ Pick the value by contrast: white if it reaches ≥ 4.5:1 on the project's `--brand`
+ (run the `/ss-lint` contrast script to verify), otherwise the skin's darkest ink.
+ This is ADDITIVE only — never modify existing token values.
+
For CLAUDE.md (Golden Rules):
- Check if Golden Rules section already exists
- If not: ask "Add Golden Rules section to your CLAUDE.md? This adds 10 lines at the top. Your existing content stays untouched."
- If yes: insert Golden Rules after the first heading
**Never touch:**
-- theme.css — say "Your theme.css (skin) is untouched."
+- theme.css — say "Your theme.css (skin) is untouched." (Sole exception: the additive
+ `--brand-foreground` append above, and only with the user's OK.)
- components/ — say "Your components are untouched. Run `/ss-lint` to check compliance."
### Step 5: Summary
diff --git a/engine/.cursorrules b/engine/.cursorrules
index e10f599..2b7eec9 100644
--- a/engine/.cursorrules
+++ b/engine/.cursorrules
@@ -17,7 +17,7 @@ Follow the StyleSeed design language strictly. Full reference: https://github.co
# Layout
- Mobile-first, max-w-[430px], bg-surface-page.
- Page structure: PageShell > TopBar > PageContent (space-y-6) > BottomNav.
-- 4 section types: Full Card (mx-6), Grid (px-6 grid-cols-2 gap-4), Carousel (px-6 flex gap-3 overflow-x-auto), Hero (mx-6 p-8).
+- 4 section types: Full Card (mx-6), Grid (px-6 grid-cols-2 gap-4), Carousel (px-6 flex gap-3 overflow-x-auto), Hero (mx-6 p-9).
- Section spacing: space-y-6 between all sections. No section dividers.
- Bottom padding: pb-24 for BottomNav clearance.
diff --git a/engine/AGENTS.md b/engine/AGENTS.md
index 21f0d5c..6379918 100644
--- a/engine/AGENTS.md
+++ b/engine/AGENTS.md
@@ -16,7 +16,7 @@ build any UI, dashboard, page, or component in this project, follow the rules be
2. Single accent color (--brand) — everything else grayscale
3. No pure black (#000) — darkest text is defined by the skin (~#2A2A2A)
4. Numbers 2:1 with units — 48px number + 24px unit, always
- 5. One spatial rhythm on the 8px grid — mobile: space-y-6 · mx-6 · px-6; desktop: container + gap-6/gap-8
+ 5. One spatial rhythm on the 6px grid (all spacing = multiples of 6px) — mobile: space-y-6 · mx-6 · px-6; desktop: container + gap-6/gap-9
6. Never repeat the same section type consecutively — create visual rhythm
7. Elevation, one language: LIGHT = layered shadows ≤ 8%; DARK = tonal surface ramp + hairline borders (shadows don't read on dark)
8. Touch targets ≥ 44×44px on touch surfaces; pointer-first desktop controls may be 36–40px (keep focus rings)
@@ -62,11 +62,22 @@ live only in chat memory and drift. Fix with a project design-lock file:
- Radius personality: soft (12px) # sharp 0-4 | soft 8-12 | pill — one everywhere
- Motion seed: Spring # Spring | Silk | Snap | Float | Pulse
- Type scale: desktop (body 16-18px) # mobile-tight | desktop-larger
+- Brand intent: # prose carries intent — tokens are context, not instructions
+ - Reference: Toss home tab # ONE concrete reference (product/era/artifact) — never bare adjectives
+ - Implied traits: flat surfaces · single hue + grey · generous whitespace # 3 traits the reference implies — spell them out so every model tier reads it the same
+ - Never: gradients · glassmorphism · decorative serif # 3 negative constraints — what this design is NOT
+ - Feeling: calm, in-control, scannable in 3 seconds
```
When the user later says "make it more X," update the lock *and* the UI together. The lock is
what keeps the result consistent across prompts — without it, even perfect rules drift.
+**Why Brand intent works (concept ported from google-labs-code/design.md):** a specific
+reference carries more constraint than a dozen adjectives, and a strong reference implies its
+own "Never" list for free. But reference interpretation varies by model — so the lock always
+pairs the reference with its 3 implied traits as an explicit anchor. A reference alone is
+FORBIDDEN in the lock; adjectives alone ("modern, clean") are equally forbidden.
+
## Quick Setup — MANDATORY before building (consistency comes from constraints)
**Not optional.** If there's no `STYLESEED.md` lock and you're about to build UI, this is the
@@ -89,6 +100,9 @@ regulation → **Toss `#3182F6`**; premium SaaS → **Stripe**; dev/dark → **L
compact) · **Tone** → motion+saturation (calm · playful). Default from skin (Toss → soft·
minimal·airy·calm; Linear → sharp·minimal·compact·calm), let the user tweak ("sharper
corners"), lock all four. This is what makes it feel *chosen*, not defaulted.
+ Then capture **Brand intent**: ONE concrete reference (a product, era, or artifact — not
+ adjectives), the 3 traits it implies, 3 "Never" constraints, and a one-line feeling —
+ confirm with the user and write all of it into the lock (see the Brand intent template).
3. **Accent (key color)** — a domain-fit color or skin (see Smart defaults), or the user's brand
hex. **One accent only; everything else greyscale.**
4. **Font** — recommend by skin/language, don't leave the default: Korean/CJK → Pretendard ·
@@ -115,7 +129,7 @@ between "looks generated" and "looks designed." Never show UI that hasn't passed
□ Color=meaning — normal/OK/"보통" rows GREY; color only the minority needing attention; no
rainbow list; same value → same color
□ Hierarchy — one clear primary; numbers 2:1 with unit
-□ Layout — content in cards; 8px rhythm; gap-around-group > gap-inside
+□ Layout — content in cards; 6px rhythm; gap-around-group > gap-inside
□ States — empty + loading + error on every data surface (static mockup / landing with no
data surface → N/A, don't fail)
□ Copy — buttons name the action; errors help, not blame
diff --git a/engine/CLAUDE.md b/engine/CLAUDE.md
index ea1276c..950fa84 100644
--- a/engine/CLAUDE.md
+++ b/engine/CLAUDE.md
@@ -10,8 +10,8 @@ The engine provides layout rules, components, and skills. The skin provides colo
2. Single accent color (--brand) — everything else grayscale
3. No pure black (#000) — darkest text is defined by skin (~#2A2A2A)
4. Numbers 2:1 with units — 48px number + 24px unit, always
- 5. One spatial rhythm on the 8px grid — mobile: space-y-6 · mx-6 · px-6; desktop: same
- principle via container + gap-6/gap-8 (don't mix off-grid one-offs)
+ 5. One spatial rhythm on the 6px grid (all spacing = multiples of 6px) — mobile: space-y-6 ·
+ mx-6 · px-6; desktop: same principle via container + gap-6/gap-9 (don't mix off-grid one-offs)
6. Never repeat same section type consecutively — create visual rhythm
7. Elevation, one language: LIGHT = layered shadows ≤ 8% opacity (if visible, too strong);
DARK = shadows don't read — tonal surface ramp (page < card < raised) + hairline borders
@@ -68,6 +68,11 @@ design-lock file.** Before building any UI:
- Imagery palette: (optional) sand #E5CBAA · oak #D9B084 · charcoal #3A2E27 # locked content tones, not accents
- Semantic resolve: (if accent ≈ green/red) positive-progress uses accent; success reserved for confirmation moments
- Signature move: (optional) oversized serif index on the hero step ONLY # one treatment, not a uniform (CC-9c)
+- Brand intent: # prose carries intent — tokens are context, not instructions
+ - Reference: Toss home tab # ONE concrete reference (product/era/artifact) — never bare adjectives
+ - Implied traits: flat surfaces · single hue + grey · generous whitespace # 3 traits the reference implies — spell them out so every model tier reads it the same
+ - Never: gradients · glassmorphism · decorative serif # 3 negative constraints — what this design is NOT
+ - Feeling: calm, in-control, scannable in 3 seconds
- Locked: 2026-06-23
```
@@ -75,6 +80,12 @@ Keep it short and human-editable. When the user later says "make it more X," upd
*and* the UI so they never diverge. **The lock is what makes the result consistent across
prompts** — without it, even perfect rules drift.
+**Why Brand intent works (concept ported from google-labs-code/design.md):** a specific
+reference carries more constraint than a dozen adjectives, and a strong reference implies its
+own "Never" list for free. But reference interpretation varies by model — so the lock always
+pairs the reference with its 3 implied traits as an explicit anchor. A reference alone is
+FORBIDDEN in the lock; adjectives alone ("modern, clean") are equally forbidden.
+
## Quick Setup — MANDATORY before building (consistency comes from constraints)
**This is not optional.** If there is no `STYLESEED.md` lock in the project and you are about
@@ -117,6 +128,9 @@ Run this setup with the user (in plan mode), then build:
compact·calm · Arc → soft·expressive·airy·playful), let the user tweak in their words
("make the corners sharper", "more playful"), then **lock all four**. One mood → one radius,
one shadow language, one density, one motion — applied everywhere.
+ Then capture **Brand intent**: ONE concrete reference (a product, era, or artifact — not
+ adjectives), the 3 traits it implies, 3 "Never" constraints, and a one-line feeling —
+ confirm with the user and write all of it into the lock (see the Brand intent template).
3. **Accent (key color)** — recommend a domain-fit color or skin (see Smart defaults). If the
user has a brand hex, use it. **One accent only; everything else greyscale.** Skins:
Toss/Stripe/Linear/Notion/Raycast/Arc/Vercel.
@@ -163,7 +177,7 @@ demo was reviewed and fixed, not a first draft. **Never show the user UI that ha
□ Color=meaning — normal/OK/"보통" rows are GREY; color marks only the minority that needs
attention; no rainbow list; same value → same color (§65, CL-2a)
□ Hierarchy — one clear primary per screen; numbers 2:1 with unit; sizes from the table
-□ Layout — content in cards (not bare bg); 8px rhythm; gap-around-group > gap-inside
+□ Layout — content in cards (not bare bg); 6px rhythm; gap-around-group > gap-inside
□ States — every data surface has empty + loading + error (not just the full state).
Static mockup / marketing landing with no data surface → mark N/A, don't fail
□ Copy — buttons name the action ("Send $2,400" not "Submit"); errors help, not blame
@@ -214,6 +228,7 @@ Modify in `:root` of `src/styles/theme.css`:
| Variable | Purpose | Default |
|----------|---------|---------|
| `--brand` | Brand accent color | Defined by skin (e.g. `#721FE5` for toss) |
+| `--brand-foreground` | Label on brand surfaces (buttons/chips) — components use `text-brand-foreground`, never hardcoded white | `#FFFFFF` (skins with a light brand use dark ink) |
| `--primary` | Buttons, links, primary UI | `#030213` |
| `--destructive` | Error/danger | `#d4183d` |
| `--success` | Success indicator | `#6B9B7A` |
@@ -365,8 +380,8 @@ escape the "default sans everything" look. Set both in the lock and `css/fonts.c
### Spacing
- Uses Tailwind default utilities
-- **One base grid: 8px** (`p-2`/`p-4`/`p-6`/`p-8` — 4px allowed as a half-step for icon↔label gaps).
- This matches VISUAL-CRAFT CR-1; don't mix in 6/10/14px one-offs (`p-1.5`, `gap-2.5`, `py-3.5`).
+- **One base grid: 6px** (`p-1.5`/`p-3`/`p-6`/`p-9` — all margin/padding/gap in multiples of 6px).
+ This matches VISUAL-CRAFT CR-1; don't mix in 8/10/14px one-offs (`p-2`, `gap-2.5`, `py-3.5`).
- Page horizontal padding: `px-6` (24px)
- Between sections: `space-y-6` (24px)
@@ -762,6 +777,7 @@ Exact contrast ratios depend on your skin's color values. Verify your skin meets
| `--foreground` | 7:1+ | Body text |
| `--muted-foreground` | 4.5:1+ | Secondary text |
| `--brand` | 4.5:1+ | Accent (verify with your brand color) |
+| `--brand-foreground` | 4.5:1+ vs `--brand` | Button/chip labels on brand |
| `--destructive` | 4.5:1+ | Error |
| `--warning` | 4.5:1+ | Warning text |
| `--success` | 3:1+ | Large text/icons only |
diff --git a/engine/DESIGN-LANGUAGE.md b/engine/DESIGN-LANGUAGE.md
index 8a4436c..1319835 100644
--- a/engine/DESIGN-LANGUAGE.md
+++ b/engine/DESIGN-LANGUAGE.md
@@ -400,7 +400,7 @@ Rules:
Normal row: bg-surface-subtle (#FAFAF9)
My row: bg-brand-tint (#F0E8FF) + border-2 border-brand
Normal rank: bg-surface-muted (#E8E6E1) + text-text-tertiary
-My rank: bg-brand + text-white
+My rank: bg-brand + text-brand-foreground
Normal name: text-text-primary (#3C3C3C)
My name: text-brand
```
@@ -427,14 +427,14 @@ If there are 2-4 options, use a **Pill toggle**. If 5 or more, **separate into a
### Pattern 1: Capsule Pill Toggle
```
Container: bg-surface-muted rounded-full p-1
-Active button: bg-brand text-white rounded-full shadow
+Active button: bg-brand text-brand-foreground rounded-full shadow
Inactive: text-text-disabled (#9B9B9B)
```
| Property | Active | Inactive |
|----------|--------|----------|
| Background | `bg-brand` | transparent |
-| Text | `text-white` | `text-text-disabled` |
+| Text | `text-brand-foreground` | `text-text-disabled` |
| Corners | `rounded-full` | `rounded-full` |
| Shadow | `shadow-sm` | none |
| Size | 11px bold | 11px bold |
@@ -442,7 +442,7 @@ Inactive: text-text-disabled (#9B9B9B)
```tsx
-
+
1W
@@ -589,7 +589,7 @@ Placing content directly on the page background without a card breaks the design
### Type D: Hero Card — Special Large Format
```
┌── mx-6 ──────────────────────────────────┐
-│ bg-card rounded-2xl p-8 shadow-card │
+│ bg-card rounded-2xl p-9 shadow-card │
│ relative overflow-hidden │
│ │
│ [background chart/watermark] │
@@ -599,7 +599,7 @@ Placing content directly on the page background without a card breaks the design
└──────────────────────────────────────────┘
```
- **Use for**: hero revenue card
-- `p-8` (32px): more generous padding than standard cards
+- `p-9` (36px): more generous padding than standard cards
- Transparent chart/icon watermark in background
- No title, straight to metric
@@ -723,7 +723,7 @@ If existing pattern combinations cannot solve it, confirm with the user first.
✗ Placing dividers (hr, border-b, Separator) between sections
✗ Changing section gap to anything other than space-y-6
✗ Using left/right margin/padding other than mx-6/px-6
-✗ Changing card padding to anything other than p-6/p-8
+✗ Changing card padding to anything other than p-6/p-9
✗ Changing card radius to anything other than rounded-2xl
✗ Placing floating buttons above bottom nav
```
@@ -799,7 +799,7 @@ When building a new page from scratch, follow this order:
### Step 5: Layout Check
- [ ] Are all section gaps space-y-6?
- [ ] Single cards use mx-6, multiple use px-6?
-- [ ] Card padding is p-6 (hero only p-8)?
+- [ ] Card padding is p-6 (hero only p-9)?
- [ ] Card radius is rounded-2xl?
- [ ] No overlapping elements?
@@ -942,7 +942,7 @@ Applies to: hero metrics, KPI metrics, list amounts, chart prices, donut center,
| TopBar icon button | Tap | Shadow change (hover) |
| Donut chart segment | Tap → select/deselect | opacity 0.3 ↔ 1 + key color change |
| Donut legend item | Tap → select/deselect | opacity 0.4 ↔ 1 + glow |
-| Period toggle button | Tap → switch | bg-brand + text-white |
+| Period toggle button | Tap → switch | bg-brand + text-brand-foreground |
| Bottom nav item | Tap → page switch | text-brand |
### Hover Effect Types
@@ -1938,7 +1938,7 @@ NO More than 1 Primary CTA per screen
#### 3. Pill Badge
```tsx
-
MY WORKSPACE
@@ -2190,7 +2190,7 @@ Inactive segment: bg-transparent text-text-disabled
### Pill Toggle vs Segment Control
| | Pill Toggle | Segment Control |
|-|-------------|----------------|
-| Active style | `bg-brand text-white` | `bg-card text-text-primary shadow` |
+| Active style | `bg-brand text-brand-foreground` | `bg-card text-text-primary shadow` |
| Usage | Key color emphasis needed (period switch) | Neutral switching (filter, view mode) |
| Option count | 2-3 | 2-5 |
@@ -2240,7 +2240,7 @@ Buttons: horizontal layout, gap-2, both flex-1
### Button Rules
```
Left: "Close" -- outline style (border-brand text-brand bg-card)
-Right: Action CTA -- solid style (bg-brand text-white)
+Right: Action CTA -- solid style (bg-brand text-brand-foreground)
```
- Button corners: `rounded-full` (pill)
- Height: `h-11` (44px)
@@ -2248,7 +2248,7 @@ Right: Action CTA -- solid style (bg-brand text-white)
### Dangerous Action Dialog
```
-Right button: bg-destructive text-white ("Delete")
+Right button: bg-destructive text-destructive-foreground ("Delete")
Left button: same ("Close")
```
@@ -2628,7 +2628,7 @@ screen — otherwise keep one color.
| Card Purpose | Padding | Internal Spacing | Resulting Height |
|-------------|---------|-----------------|-----------------|
-| **Hero** | `p-8` (32px) | Generous `gap-3` | ~200px (tallest) |
+| **Hero** | `p-9` (36px) | Generous `gap-3` | ~200px (tallest) |
| **Stat/KPI** | `p-6` (24px) | Tight `gap-2` | ~140px |
| **Chart** | `p-6` (24px) | Chart `h-40` + stats | ~280px |
| **List** | `p-6` (24px) | `space-y-3` items | ~200px (3 items) |
diff --git a/engine/VISUAL-CRAFT.md b/engine/VISUAL-CRAFT.md
index ea2f3e9..4bc228a 100644
--- a/engine/VISUAL-CRAFT.md
+++ b/engine/VISUAL-CRAFT.md
@@ -37,7 +37,7 @@ of this table.
| **Corner / radius** | One personality: **sharp 0–4px** · **soft 8–12px** · **pill 9999px**. Card, button, input, modal, image, avatar all obey it. | Sharp dialog + rounded buttons = "two products glued together." The #1 tell of un-designed UI. |
| **Shadow** | One scale, one light source (**above-left**), one hue tint. A modal and a card use the *same* family, different tiers. | Mixed light directions / some-black-some-tinted = "scene with two suns." |
| **Accent color** | **One** primary accent for interactive emphasis (+ semantic red/green/amber). One CTA color across the whole app. | Multiple accents → nothing reads as "the" action; hierarchy collapses. |
-| **Spacing unit** | One base grid: **8px** (4px allowed as a half-step for icon↔label). Every margin/padding/gap is a multiple. | Off-grid values (7, 13, 19px) read as "sloppy" without users knowing why. |
+| **Spacing unit** | One base grid: **6px** (3px allowed as a half-step for icon↔label). Every margin/padding/gap is a multiple. | Off-grid values (7, 13, 19px) read as "sloppy" without users knowing why. |
| **Icon style** | One family, one fill mode (all outline **or** all filled), one stroke weight (e.g. **2px @ 24px**). | Mixing Material + Feather + emoji, or 1.5px and 2px strokes, looks "out of place." |
| **Type scale** | One modular scale, ≤2 font families, one weight ramp. | Arbitrary sizes destroy rhythm; >2 families looks amateur. |
| **Motion / easing** | One duration set (~150/200/300ms) + one easing family; same enter/exit logic everywhere. | Some snappy, some sluggish = feels like different apps. |
@@ -52,10 +52,11 @@ decision already made elsewhere in the product rather than inventing a new one.
## §C1 — Spacing & rhythm
-**CR-1 · Snap everything to one scale.** Use `{2, 4, 8, 12, 16, 24, 32, 40, 48, 64, 80, 96}`px.
-No arbitrary values (no 13px, no 7px). 8px base; 4px only as a half-step for
+**CR-1 · Snap everything to one scale.** Use `{3, 6, 12, 18, 24, 30, 36, 48, 60, 72, 96}`px.
+No arbitrary values (no 13px, no 7px). 6px base; 3px only as a half-step for
icon↔text and tightly stacked small text. *Why: a constrained scale forces
-deliberate, repeatable layout; 8px divides cleanly across 1x/1.5x/2x/3x densities.*
+deliberate, repeatable layout; 6px maps 1:1 onto the Tailwind 1.5-step utilities
+(p-1.5 · p-3 · p-6 · p-9) the component rules prescribe.*
([Refactoring UI], [Material 3 Spacing], [IBM Carbon 2x Grid])
**CR-2 · Proximity = relatedness.** The space *around* a group must be **≥ 2×** the
@@ -63,12 +64,12 @@ space *within* it. Uniform spacing everywhere is the #1 beginner failure — it
destroys all grouping signal. *Why: Gestalt — the eye reads tight items as one unit,
loose items as separate.* ([Refactoring UI "Avoid ambiguous spacing"])
-**CR-3 · The form spacing ladder.** label→input **4–8px** · field→field **12–16px** ·
-section→section **24–32px** mobile / **32–48px** desktop. Each tier ~doubles the last,
+**CR-3 · The form spacing ladder.** label→input **3–6px** · field→field **12–18px** ·
+section→section **24–36px** mobile / **36–48px** desktop. Each tier ~doubles the last,
so hierarchy is unambiguous. ([Designary], [Atlassian Spacing])
-**CR-4 · Card padding by size.** compact **12–16px** · standard **16–24px** · large/marketing
-**24–32px** · hero **48–64px+**. And the outer margin between cards must be **≥** the
+**CR-4 · Card padding by size.** compact **12–18px** · standard **18–24px** · large/marketing
+**24–36px** · hero **48–60px+**. And the outer margin between cards must be **≥** the
card's inner padding, so each card reads as self-contained. ([Refactoring UI], [UX Lab])
**CR-5 · Start over-spaced, then reduce.** Designers under-space by default; "ample"
@@ -94,7 +95,7 @@ fixed heights it doesn't have. Repetition of the spacing scale creates rhythm; a
baseline grid does not. ([Imperavi Vertical Rhythm])
**CR-10 · Everything lines up to something.** Establish a small set of shared
-left/right edges (a 12-col grid, 16px gutters) and align all content to them. Apply
+left/right edges (a 12-col grid, 18px gutters) and align all content to them. Apply
*optical* (not pixel) alignment for weighted glyphs: nudge arrow/play icons, trim
the icon-side padding of icon-buttons, center type by cap-height. ([IBM Carbon], [Liferay Optical Alignment])
@@ -177,14 +178,14 @@ corner "bulges" past the outer arc.* Apple's Liquid Glass formalizes this. ([Clo
### Buttons
**CC-5 · Heights 36 / 40 / 44–48px;** touch target ≥ **44px** (iOS) / **48dp** (Android).
-Horizontal padding ≈ **2× vertical**, on grid. Icon+label gap **8px** (4px dense). ([Justinmind], [Apple HIG])
+Horizontal padding ≈ **2× vertical**, on grid. Icon+label gap **6px** (3px dense). ([Justinmind], [Apple HIG])
**CC-6 · One primary button per view.** Hierarchy by **contrast** (filled → outline →
ghost), never by size. Destructive is red but **never** more prominent than the
primary. All buttons in a row share one height. ([Cieden Button Hierarchy], [Carbon])
### Cards
-**CC-7 · Padding 16 / 24 / 32px** equal on all sides; radius obeys the system
+**CC-7 · Padding 18 / 24 / 36px** equal on all sides; radius obeys the system
personality and the nested law for inner media/buttons. **Border XOR shadow, never
both** — flat/dense UI uses a 1px border (elevation 0); standalone/floating uses a soft
shadow (elevation 1). Hover on an interactive card = raise **one** elevation tier over
@@ -195,7 +196,7 @@ shadow (elevation 1). Hover on an interactive card = raise **one** elevation tie
field (top-aligned scans fastest); **labels never vanish** (no placeholder-as-label).
Focus ring **≥2px, 3:1 contrast**, visible on light *and* dark (WCAG 2.2). Errors
**never color alone**: red border + icon + a message saying what's wrong and how to fix
-it. Rhythm: 8px label→field, 4px field→helper, 16–24px between fields. ([UX Collective Text Fields], [WCAG 2.2])
+it. Rhythm: 6px label→field, 3px field→helper, 18–24px between fields. ([UX Collective Text Fields], [WCAG 2.2])
### Icons
**CC-9 · One family, one fill mode, one stroke weight** (typically **2px @ 24px**).
diff --git a/engine/components/patterns/chart-card.tsx b/engine/components/patterns/chart-card.tsx
index e5a7091..c1f28a2 100644
--- a/engine/components/patterns/chart-card.tsx
+++ b/engine/components/patterns/chart-card.tsx
@@ -57,7 +57,7 @@ function ChartCard({
className={cn(
"px-4 py-1.5 text-[11px] font-semibold rounded-full transition-all",
activePeriod === period
- ? "bg-brand text-white shadow-sm"
+ ? "bg-brand text-brand-foreground shadow-sm"
: "text-text-tertiary",
)}
>
diff --git a/engine/components/patterns/donut-chart-card.tsx b/engine/components/patterns/donut-chart-card.tsx
index 0487b98..2a3d3ab 100644
--- a/engine/components/patterns/donut-chart-card.tsx
+++ b/engine/components/patterns/donut-chart-card.tsx
@@ -56,7 +56,7 @@ function DonutChartCard({
{title}
-
+
{chartElement}
{(centerValue !== undefined || centerLabel) && (
diff --git a/engine/components/patterns/hero-card.tsx b/engine/components/patterns/hero-card.tsx
index e8063f0..79bd2a7 100644
--- a/engine/components/patterns/hero-card.tsx
+++ b/engine/components/patterns/hero-card.tsx
@@ -31,7 +31,7 @@ function HeroCard({
@@ -67,7 +67,7 @@ function RankedList({
{item.name}
{item.badge && (
-
+
{item.badge}
)}
diff --git a/engine/components/ui/button.tsx b/engine/components/ui/button.tsx
index 736c83f..56c8217 100644
--- a/engine/components/ui/button.tsx
+++ b/engine/components/ui/button.tsx
@@ -9,9 +9,9 @@ const buttonVariants = cva(
{
variants: {
variant: {
- // 토스 스타일: brandSolid — 키컬러 배경, 흰 텍스트
+ // 토스 스타일: brandSolid — 키컬러 배경, 스킨이 정한 라벨색(--brand-foreground)
default:
- "bg-brand text-white active:bg-brand/85 disabled:bg-surface-muted disabled:text-text-disabled",
+ "bg-brand text-brand-foreground active:bg-brand/85 disabled:bg-surface-muted disabled:text-text-disabled",
// neutralSolid — 어두운 배경, 흰 텍스트
neutral:
"bg-[#2A2A2A] text-white active:bg-[#3C3C3C] disabled:bg-surface-muted disabled:text-text-disabled dark:bg-[#E0E0E0] dark:text-[#121212] dark:active:bg-[#C0C0C0]",
@@ -20,7 +20,7 @@ const buttonVariants = cva(
"bg-[#F3F4F5] text-text-primary active:bg-[#EAEBEC] disabled:bg-surface-muted disabled:text-text-disabled dark:bg-[#2B2E35] dark:text-[#E0E0E0] dark:active:bg-[#393D46]",
// criticalSolid — 위험 액션
destructive:
- "bg-destructive text-white active:bg-destructive/85 focus-visible:ring-destructive/20 disabled:bg-surface-muted disabled:text-text-disabled",
+ "bg-destructive text-destructive-foreground active:bg-destructive/85 focus-visible:ring-destructive/20 disabled:bg-surface-muted disabled:text-text-disabled",
// outline — 테두리만
outline:
"border border-border bg-transparent text-text-primary active:bg-surface-muted/50 disabled:border-surface-muted disabled:text-text-disabled dark:border-white/8",
diff --git a/engine/components/ui/confirm-modal.tsx b/engine/components/ui/confirm-modal.tsx
index 551ad3c..b2b7c7a 100644
--- a/engine/components/ui/confirm-modal.tsx
+++ b/engine/components/ui/confirm-modal.tsx
@@ -66,8 +66,10 @@ function ConfirmModal({
{confirmText}
diff --git a/skins/_from-awesome-design-md/README.md b/skins/_from-awesome-design-md/README.md
index 55b50de..f49e14e 100644
--- a/skins/_from-awesome-design-md/README.md
+++ b/skins/_from-awesome-design-md/README.md
@@ -20,6 +20,8 @@ cp skins/toss/theme.css skins/[brand]/theme.css
# 4. Replace --brand and color values with the brand's palette
# (found in the "Color Palette & Roles" section of DESIGN.md)
+# Keep --brand-foreground ≥ 4.5:1 on the new --brand (white on a dark brand,
+# dark ink on a light brand) — verify with the /ss-lint contrast script.
```
## Available Brands
diff --git a/skins/arc/theme.css b/skins/arc/theme.css
index 261b562..b62acea 100644
--- a/skins/arc/theme.css
+++ b/skins/arc/theme.css
@@ -4,8 +4,9 @@
--font-size: 16px;
/* === CUSTOMIZE: Brand Colors === */
- --brand: #FF5E7E;
- --primary: #FF5E7E;
+ --brand: #e11d48;
+ --brand-foreground: #FFFFFF;
+ --primary: #e11d48;
--primary-foreground: #FFFFFF;
/* === Arc signature rainbow gradient === */
@@ -30,7 +31,7 @@
--accent-foreground: #1A1A1A;
/* === Status Colors === */
- --destructive: #FF5E7E;
+ --destructive: #e11d48;
--destructive-foreground: #FFFFFF;
--success: #4ECDC4;
--success-foreground: #FFFFFF;
@@ -51,7 +52,7 @@
--surface-subtle: #F5F2ED;
--surface-muted: #EFECE6;
--brand-tint: rgba(255, 94, 126, 0.1);
- --alert-badge: #FF5E7E;
+ --alert-badge: #e11d48;
/* === Borders & Inputs === */
--border: rgba(26, 26, 26, 0.08);
@@ -96,7 +97,7 @@
/* === Sidebar === */
--sidebar: #FFFFFF;
--sidebar-foreground: #1A1A1A;
- --sidebar-primary: #FF5E7E;
+ --sidebar-primary: #e11d48;
--sidebar-primary-foreground: #FFFFFF;
--sidebar-accent: #F7F4EF;
--sidebar-accent-foreground: #1A1A1A;
@@ -127,6 +128,7 @@
--color-destructive-tint: color-mix(in srgb, var(--destructive) 14%, var(--card));
--color-info-tint: color-mix(in srgb, var(--info) 14%, var(--card));
--color-brand: var(--brand);
+ --color-brand-foreground: var(--brand-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
diff --git a/skins/linear/theme.css b/skins/linear/theme.css
index 4fb5f2f..4a8d5f4 100644
--- a/skins/linear/theme.css
+++ b/skins/linear/theme.css
@@ -5,6 +5,7 @@
/* === CUSTOMIZE: Brand Colors === */
--brand: #5e6ad2;
+ --brand-foreground: #ffffff;
--primary: #171717;
--primary-foreground: #ffffff;
@@ -20,7 +21,7 @@
--secondary: #f3f4f5;
--secondary-foreground: #171717;
--muted: #e6e6e6;
- --muted-foreground: #8a8f98;
+ --muted-foreground: #70757f;
--accent: #f3f4f5;
--accent-foreground: #171717;
@@ -124,6 +125,7 @@
/* === Key Color & Status (brightened for dark mode) === */
--brand: #7170ff;
+ --brand-foreground: #08090a;
--primary: #f7f8f8;
--primary-foreground: #08090a;
--secondary: #191a1b;
@@ -193,6 +195,7 @@
--color-destructive-tint: color-mix(in srgb, var(--destructive) 14%, var(--card));
--color-info-tint: color-mix(in srgb, var(--info) 14%, var(--card));
--color-brand: var(--brand);
+ --color-brand-foreground: var(--brand-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
diff --git a/skins/notion/theme.css b/skins/notion/theme.css
index b04bf1e..e3bcb8a 100644
--- a/skins/notion/theme.css
+++ b/skins/notion/theme.css
@@ -5,6 +5,7 @@
/* === CUSTOMIZE: Brand Colors === */
--brand: #0075de;
+ --brand-foreground: #ffffff;
--primary: rgba(0,0,0,0.95);
--primary-foreground: #ffffff;
@@ -25,11 +26,11 @@
--accent-foreground: #31302e;
/* === Status Colors === */
- --destructive: #dd5b00;
+ --destructive: #c65200;
--destructive-foreground: #ffffff;
--success: #2a9d99;
--success-foreground: #ffffff;
- --warning: #dd5b00;
+ --warning: #c65200;
--warning-foreground: #ffffff;
--info: #0075de;
--info-foreground: #ffffff;
@@ -46,7 +47,7 @@
--surface-subtle: #f6f5f4;
--surface-muted: #e8e5e1;
--brand-tint: #f2f9ff;
- --alert-badge: #dd5b00;
+ --alert-badge: #c65200;
/* === Borders & Inputs === */
--border: rgba(0,0,0,0.1);
@@ -124,12 +125,13 @@
/* === Key Color & Status (brightened for dark mode) === */
--brand: #62aef0;
+ --brand-foreground: #191919;
--primary: #e0dfde;
--primary-foreground: #191919;
--secondary: #252422;
--secondary-foreground: #e0dfde;
--muted: #252422;
- --muted-foreground: #7a7672;
+ --muted-foreground: #908c88;
--accent: #252422;
--accent-foreground: #e0dfde;
--destructive: #ff8c4c;
@@ -193,6 +195,7 @@
--color-destructive-tint: color-mix(in srgb, var(--destructive) 14%, var(--card));
--color-info-tint: color-mix(in srgb, var(--info) 14%, var(--card));
--color-brand: var(--brand);
+ --color-brand-foreground: var(--brand-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
diff --git a/skins/raycast/theme.css b/skins/raycast/theme.css
index bb08827..520f258 100644
--- a/skins/raycast/theme.css
+++ b/skins/raycast/theme.css
@@ -5,8 +5,9 @@
/* === CUSTOMIZE: Brand Colors === */
--brand: #FF4E8B;
+ --brand-foreground: #0A0A0A;
--primary: #FF4E8B;
- --primary-foreground: #FFFFFF;
+ --primary-foreground: #0A0A0A;
/* === Raycast signature gradient === */
--gradient-brand: linear-gradient(135deg, #FF6363 0%, #FF8E3C 30%, #E84A8E 65%, #A855F7 100%);
@@ -30,7 +31,7 @@
/* === Status Colors === */
--destructive: #FF5C5C;
- --destructive-foreground: #FFFFFF;
+ --destructive-foreground: #0A0A0A;
--success: #4ADE80;
--success-foreground: #0A0A0A;
--warning: #FBBF24;
@@ -127,6 +128,7 @@
--color-destructive-tint: color-mix(in srgb, var(--destructive) 14%, var(--card));
--color-info-tint: color-mix(in srgb, var(--info) 14%, var(--card));
--color-brand: var(--brand);
+ --color-brand-foreground: var(--brand-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
diff --git a/skins/stripe/theme.css b/skins/stripe/theme.css
index 49e9a25..20bd495 100644
--- a/skins/stripe/theme.css
+++ b/skins/stripe/theme.css
@@ -5,6 +5,7 @@
/* === CUSTOMIZE: Brand Colors === */
--brand: #533afd;
+ --brand-foreground: #ffffff;
--primary: #061b31;
--primary-foreground: #ffffff;
@@ -25,7 +26,7 @@
--accent-foreground: #061b31;
/* === Status Colors === */
- --destructive: #ea2261;
+ --destructive: #e41657;
--destructive-foreground: #ffffff;
--success: #15be53;
--success-foreground: #ffffff;
@@ -46,7 +47,7 @@
--surface-subtle: #f6f9fc;
--surface-muted: #e5edf5;
--brand-tint: #ededff;
- --alert-badge: #ea2261;
+ --alert-badge: #e41657;
/* === Borders & Inputs === */
--border: #e5edf5;
@@ -124,6 +125,7 @@
/* === Key Color & Status (brightened for dark mode) === */
--brand: #665efd;
+ --brand-foreground: #ffffff;
--primary: #e2e8f0;
--primary-foreground: #0d253d;
--secondary: #1c1e54;
@@ -193,6 +195,7 @@
--color-destructive-tint: color-mix(in srgb, var(--destructive) 14%, var(--card));
--color-info-tint: color-mix(in srgb, var(--info) 14%, var(--card));
--color-brand: var(--brand);
+ --color-brand-foreground: var(--brand-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
diff --git a/skins/toss/theme.css b/skins/toss/theme.css
index 386b70c..9e8f7a0 100644
--- a/skins/toss/theme.css
+++ b/skins/toss/theme.css
@@ -5,6 +5,7 @@
/* === CUSTOMIZE: Brand Colors === */
--brand: #721FE5;
+ --brand-foreground: #ffffff;
--primary: #030213;
--primary-foreground: oklch(1 0 0);
@@ -124,12 +125,13 @@
/* === Key Color & Status (brightened for dark mode) === */
--brand: #9B5FFF;
+ --brand-foreground: #121212;
--primary: #E0E0E0;
--primary-foreground: #121212;
--secondary: #252525;
--secondary-foreground: #E0E0E0;
--muted: #252525;
- --muted-foreground: #808080;
+ --muted-foreground: #868686;
--accent: #252525;
--accent-foreground: #E0E0E0;
--destructive: #FF5C5C;
@@ -193,6 +195,7 @@
--color-destructive-tint: color-mix(in srgb, var(--destructive) 14%, var(--card));
--color-info-tint: color-mix(in srgb, var(--info) 14%, var(--card));
--color-brand: var(--brand);
+ --color-brand-foreground: var(--brand-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
diff --git a/skins/vercel/theme.css b/skins/vercel/theme.css
index 549f0fa..f231a4f 100644
--- a/skins/vercel/theme.css
+++ b/skins/vercel/theme.css
@@ -5,6 +5,7 @@
/* === CUSTOMIZE: Brand Colors === */
--brand: #171717;
+ --brand-foreground: #ffffff;
--primary: #171717;
--primary-foreground: #ffffff;
@@ -25,7 +26,7 @@
--accent-foreground: #171717;
/* === Status Colors === */
- --destructive: #ff5b4f;
+ --destructive: #dc2626;
--destructive-foreground: #ffffff;
--success: #0a72ef;
--success-foreground: #ffffff;
@@ -46,7 +47,7 @@
--surface-subtle: #fafafa;
--surface-muted: #ebebeb;
--brand-tint: #f5f5f5;
- --alert-badge: #ff5b4f;
+ --alert-badge: #dc2626;
/* === Borders & Inputs === */
--border: #ebebeb;
@@ -124,12 +125,13 @@
/* === Key Color & Status (brightened for dark mode) === */
--brand: #ffffff;
+ --brand-foreground: #0a0a0a;
--primary: #ededed;
--primary-foreground: #0a0a0a;
--secondary: #1a1a1a;
--secondary-foreground: #ededed;
--muted: #1a1a1a;
- --muted-foreground: #777777;
+ --muted-foreground: #7f7f7f;
--accent: #1a1a1a;
--accent-foreground: #ededed;
--destructive: #ff6b61;
@@ -193,6 +195,7 @@
--color-destructive-tint: color-mix(in srgb, var(--destructive) 14%, var(--card));
--color-info-tint: color-mix(in srgb, var(--info) 14%, var(--card));
--color-brand: var(--brand);
+ --color-brand-foreground: var(--brand-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);