From 473280e69034626bb1827bcabee762eb4cdd08f8 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 31 Jul 2026 10:18:53 -0400 Subject: [PATCH 1/2] chore(design-systems): stop teaching one install's kit in product copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The operator's check: this must be a system for managing design systems, not one with the FabledSword family built into it. No LOGIC was coupled — the audit found zero behavioural dependencies. But every docstring example, every UI placeholder and several comments named this install's palette, so a stranger creating their first design system was shown "FabledSword" as the expected shape and `--fs-obsidian` as the expected token. Examples teach, and these taught the wrong thing. Placeholders now describe the SHAPE ("Your house style", "--surface-page") rather than naming one instance's contents, and the token-name placeholder now says the thing worth saying: name it for its purpose, because `--obsidian` and `--button-bg` both stop being true the moment the value or the element changes. Not fixed here, and it is the one real coupling left: DesignView.vue hardcodes rule 65's button variants and rule 60's type scale as literal arrays, so a stranger's Design page would display this family's specs. Those arrays exist because there was no design system to read from — which there now is. They go when the panel is repointed (#2295), not before. Scribe's own stylesheet comments ("Moss action-primary per Hybrid") are left alone: that is the app CONSUMING the family style, which is what dogfooding looks like, not the tool assuming it. --- frontend/src/views/DesignSystemsView.vue | 10 +++++----- src/scribe/mcp/tools/design_systems.py | 17 +++++++++++------ src/scribe/mcp/tools/rulebooks.py | 5 +++-- src/scribe/models/design_system.py | 16 +++++++++------- src/scribe/services/design_stylesheet.py | 7 ++++--- 5 files changed, 32 insertions(+), 23 deletions(-) diff --git a/frontend/src/views/DesignSystemsView.vue b/frontend/src/views/DesignSystemsView.vue index 828fb16..a11b09f 100644 --- a/frontend/src/views/DesignSystemsView.vue +++ b/frontend/src/views/DesignSystemsView.vue @@ -530,7 +530,7 @@ function isColourish(value: string): boolean {
@@ -581,7 +581,7 @@ function isColourish(value: string): boolean {
@@ -852,7 +852,7 @@ function isColourish(value: string): boolean {
@@ -864,7 +864,7 @@ function isColourish(value: string): boolean {
@@ -873,7 +873,7 @@ function isColourish(value: string): boolean {

Distinct from purpose: purpose is what the token is FOR, this is diff --git a/src/scribe/mcp/tools/design_systems.py b/src/scribe/mcp/tools/design_systems.py index 7043a34..c8af5db 100644 --- a/src/scribe/mcp/tools/design_systems.py +++ b/src/scribe/mcp/tools/design_systems.py @@ -33,7 +33,8 @@ async def create_design_system( """Create a design system, optionally inheriting from another. Args: - title: What this system is, e.g. "FabledSword" or "Scribe" (required). + title: What this system is — a house style, or one app within it + (required). description: What it covers and when it applies. guidance: The narrative a token table cannot hold — aesthetic, voice and tone, what is deliberately out of scope. Markdown, free-form. @@ -226,18 +227,22 @@ async def create_design_token( Args: design_system_id: The system that owns this token. - name: The custom-property name, e.g. "--fs-obsidian" (required). + name: The custom-property name, e.g. "--surface-page" (required). + Name it for its PURPOSE, not its value: a name like "--obsidian" + or "--button-bg" stops being true the moment the value or the + element changes. value_by_mode: Values keyed by mode, e.g. - {"base": "#f7f5ef", "dark": "#14171a"}. Use "base" for the value + {"base": "#14171a", "light": "#f7f5ef"}. Use "base" for the value that applies when no mode is more specific; a token that is not mode-dependent needs only "base". In a system WITH a parent, an omitted mode is inherited rather than blanked. group_name: Free-text grouping — "surface", "text", "radius", whatever this system's own vocabulary is. - purpose: What the token is for, e.g. "page bg, deepest surface". + purpose: What the token is for, e.g. "page background, deepest + surface". rationale: WHY it is this value — a different question from purpose. - "Success equals Moss, aligned by design" is a rationale; "page bg, - deepest surface" is a purpose. + "Deliberately the same value as the primary action colour" is a + rationale; "page background, deepest surface" is a purpose. supersedes: Literal values this token should be used INSTEAD OF, e.g. ["#fff", "#ffffff"]. This is how a design system records what a prohibition was trying to say — not "white is banned" but "write diff --git a/src/scribe/mcp/tools/rulebooks.py b/src/scribe/mcp/tools/rulebooks.py index ccb9979..55ff7dd 100644 --- a/src/scribe/mcp/tools/rulebooks.py +++ b/src/scribe/mcp/tools/rulebooks.py @@ -45,7 +45,8 @@ async def create_rulebook(title: str, description: str = "") -> dict: Two ways a rulebook reaches projects, set by its always_on flag (toggle via update_rulebook): - always_on = true -> binds EVERY one of your projects automatically. - Use for universal cross-project norms (e.g. "FabledSword family"). + Use for universal cross-project norms that apply across every + project, not just one. - always_on = false -> binds only projects that subscribe (subscribe_project_to_rulebook). Use for a THEMED body of rules a category of projects shares (e.g. a design system that visual apps @@ -54,7 +55,7 @@ async def create_rulebook(title: str, description: str = "") -> dict: to any single project. Project-specific rules go in create_project_rule. Args: - title: Rulebook name (e.g. "FabledSword family"). + title: Rulebook name. description: Optional short description of what this rulebook covers. """ uid = current_user_id() diff --git a/src/scribe/models/design_system.py b/src/scribe/models/design_system.py index 8aa2c6e..d2d8570 100644 --- a/src/scribe/models/design_system.py +++ b/src/scribe/models/design_system.py @@ -116,22 +116,24 @@ class DesignToken(Base, TimestampMixin, SoftDeleteMixin): group_name: Mapped[str | None] = mapped_column(Text, nullable=True) purpose: Mapped[str | None] = mapped_column(Text, nullable=True) # WHY this token is this value — a different question from `purpose`, which - # is what it is FOR. "Success equals Moss, aligned by design" is a rationale; - # "page bg, deepest surface" is a purpose. Rules carry the first routinely - # and a token row had nowhere to put it. + # is what it is FOR. "Deliberately the same value as the primary action + # colour" is a rationale; "page background, deepest surface" is a purpose. + # Design guidance carries the first routinely and a token row had nowhere to + # put it. rationale: Mapped[str | None] = mapped_column(Text, nullable=True) # Literal values this token should be used INSTEAD OF, e.g. ["#fff", # "#ffffff"] on a text-on-action token. # # This is how a design system records the thing a prohibition was trying to - # say. "Pure white is never text" is the shadow of a positive fact — text is - # Parchment — and a system that stores what things ARE has no row for a ban. + # say. "Pure white is never text" is the shadow of a positive fact — some + # other colour IS the text colour — and a system that stores what things ARE + # has no row for a ban. # Recording the replacement keeps the check and makes it actionable: a # finding can name what to write instead of merely objecting. # # It has to be DECLARED rather than inferred, because the superseded literal - # and the token's own value are usually different colours (#fff is not - # #E8E4D8). No value-matching rule could ever connect them. + # and the token's own value are usually different colours entirely. No + # value-matching rule could ever connect them. # # Consumed by the source lint (#2277), not by the drift panel: these # literals live in component CSS, which the panel cannot see and says so. diff --git a/src/scribe/services/design_stylesheet.py b/src/scribe/services/design_stylesheet.py index 1a2ef27..0c61c9f 100644 --- a/src/scribe/services/design_stylesheet.py +++ b/src/scribe/services/design_stylesheet.py @@ -18,8 +18,8 @@ has recall, locations, drift checks and merge. So the division is: this sheet says what the values MEAN; snippets say what things LOOK LIKE, in terms of those values. A token named after an element -(`--fs-button-bg`) is the smell that the two have been mixed — it multiplies -with every new element, where a purpose name (`--fs-action-primary`) is reused. +(`--button-bg`) is the smell that the two have been mixed — it multiplies +with every new element, where a purpose name (`--action-primary`) is reused. SAFETY ------ @@ -203,7 +203,8 @@ def duplicate_values(tokens: Sequence) -> dict[str, list[str]]: Two names for one value are either a deliberate alias or the same idea recorded twice — the token-level form of the duplicated-definition shape. Reported rather than refused: a design system legitimately aligns colours on - purpose ("Success = Moss, by design"), and a generator that rejected that + purpose (one palette entry defined as equal to another), and a generator + that rejected that would be wrong about the operator's intent. Compares the BASE value only. Two tokens agreeing in one mode and diverging -- 2.54.0 From 67a529a38efd3b25adb5924329eeaac224fadc8c Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 31 Jul 2026 12:42:15 -0400 Subject: [PATCH 2/2] feat(theme): repoint theme.css at the design system, and find what wasn't captured MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit theme.css is now generated from design system 2 (Scribe, inheriting FabledSword) plus a compatibility alias layer, so the record decides the styling rather than describing it after the fact. **Dark is now the base layer.** The kit is dark-mode-first, so `:root` carries the dark palette and `[data-theme="light"]` overrides it — the inverse of how this file read before. `useTheme` already sets the attribute explicitly to "light" or "dark", so the flip needed no JS change. It also closes the one-way scoping gap #251 recorded: there IS a `[data-theme="light"]` block now, so a container can add light as well as dark. **60 dark overrides became 12.** The other 48 were restating relationships the derivations now express: `--color-bg` follows `--fs-surface-page` because an alias resolves at use time, so it needs stating once rather than per mode. ## What the audit found, which is the actual deliverable **12 dead tokens, removed.** Declared in both modes, referenced by nothing: seven from the removed chat subsystem (bubbles, input bar), plus `--glow-soft`, `--color-action-ghost-border`, `--radius-pill` and two chat widths. 17% of the file was styling a feature that no longer exists. **16 names referenced but NEVER declared** — not by this change, not by the file before it. Fourteen carried hardcoded fallbacks, so pages rendered and nothing ever failed, but the fallback was what rendered, every time. Several were off the palette entirely: --color-primary-bg fell back to rgba(99,102,241,0.15) — an indigo --color-destructive fell back to #b85a4a — not the oxblood --color-status-cancelled fell back to #6b7280 — a grey from no palette here --color-muted fell back to #888 All 16 now resolve to real tokens. Expect small visual shifts exactly where a fallback had drifted; the shift is the fix. **Two tokens the app needed and never had**: `--fs-status-cancelled` (Scribe has had a cancelled task status since the lifecycle was built and never had a colour for it) and `--fs-layout-header` (referenced with a 52px fallback, so 52px was always the real value — just not one anybody could look up). ## The system grew to cover what the app improvised Per the operator: the kit wasn't growing with the app, and this is the result. Recorded as tokens with GAP RECORDED FROM PRACTICE in their rationale — action hover states, disabled opacity, the modal scrim, both code backgrounds, the table stripe, the CTA gradient and glows, the accent-deep and accent-wash tints, and the layout dimensions. Scribe's own system gained its domain semantics — task status, priority, overdue, wikilink — all DERIVED from family colours, so twelve rows of duplicated hex became twelve formulas and zero new values. Priority maps onto the semantic ladder deliberately: low is info, medium is warning, high is error. 95 tokens resolved, none valueless, 34 derived, no broken references, no cycles. --- frontend/src/assets/theme.css | 480 ++++++++++++++++++++++------------ 1 file changed, 319 insertions(+), 161 deletions(-) diff --git a/frontend/src/assets/theme.css b/frontend/src/assets/theme.css index c96aed0..1b26bfb 100644 --- a/frontend/src/assets/theme.css +++ b/frontend/src/assets/theme.css @@ -1,149 +1,307 @@ @import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..900;1,9..144,300..900&family=Inter:ital,wght@0,400;0,500;1,400&family=JetBrains+Mono:ital,wght@0,400;1,400&display=swap'); +/* ========================================================================== + GENERATED FROM THE DESIGN SYSTEM — Scribe (design system 2), which inherits + the FabledSword house style (design system 1). + + Do not hand-edit the --fs-* block below. Edit the design system in the app + and regenerate: /design-systems -> Master stylesheet -> Copy. + + DARK IS THE BASE LAYER. The kit is dark-mode-first, so :root carries the dark + palette and [data-theme="light"] overrides it. That is the inverse of how this + file used to read, and it is deliberate: the light palette was never specified + by any rule, so it is recorded as a departure rather than as the default. + + Only 12 tokens differ between modes. Everything else — spacing, type, motion, + radius, and every derived colour — is stated once, because a value built with + var() resolves where it is USED, not where it is written. + ========================================================================== */ + :root { - /* Light mode — warm parchment palette */ - --color-bg: #F5F1E8; - --color-bg-secondary: #FBF8F0; - --color-bg-card: #FBF8F0; - --color-surface: #EFEAE0; - --color-text: #14171A; - --color-text-secondary: #5A5852; - --color-text-muted: #9A9890; - --color-border: #D9D6CE; - --color-input-border: #D9D6CE; - --color-primary: #5B4A8A; - --color-danger: #C04A1F; - --color-tag-bg: rgba(91, 74, 138, 0.12); - --color-tag-text: #5B4A8A; - --color-shadow: rgba(0, 0, 0, 0.08); - --color-toast-success: #4A5D3F; - --color-toast-error: #C04A1F; - --color-status-todo: #3F4651; - --color-status-todo-bg: rgba(63, 70, 81, 0.10); - --color-status-in-progress: #5B4A8A; - --color-status-in-progress-bg: rgba(91, 74, 138, 0.12); - --color-status-done: #4A5D3F; - --color-status-done-bg: rgba(74, 93, 63, 0.12); - --color-priority-low: #3D5A6E; - --color-priority-low-bg: rgba(61, 90, 110, 0.12); - --color-priority-medium: #8B6F1E; - --color-priority-medium-bg: rgba(139, 111, 30, 0.12); - --color-priority-high: #C04A1F; - --color-priority-high-bg: rgba(192, 74, 31, 0.12); - --color-wikilink: #5B4A8A; - --color-wikilink-bg: rgba(91, 74, 138, 0.12); - --color-overdue: #C04A1F; - --color-code-bg: #EBEDF0; - --color-code-inline-bg: #EBEDF0; - --color-table-stripe: rgba(20, 23, 26, 0.025); - --color-success: #4A5D3F; - --color-warning: #8B6F1E; - --color-input-bar-bg: #EFEAE0; - --color-input-bar-text: #14171A; - --color-input-bar-placeholder: rgba(20, 23, 26, 0.4); - --color-overlay: rgba(0, 0, 0, 0.45); - --color-bubble-user-bg: transparent; - --color-bubble-user-border: #D9D6CE; - --color-bubble-user-text: #5A5852; - --color-bubble-asst-shadow: 0 2px 14px rgba(91, 74, 138, 0.06), 0 1px 4px rgba(0, 0, 0, 0.05); - --color-primary-solid: #5B4A8A; - --color-primary-deep: #3F3560; - --gradient-cta: linear-gradient(135deg, var(--color-primary-solid), var(--color-primary-deep)); - --glow-cta: 0 2px 10px rgba(91, 74, 138, 0.35); - --glow-cta-hover: 0 4px 20px rgba(91, 74, 138, 0.55); - --glow-soft: 0 0 16px rgba(91, 74, 138, 0.35); - --color-primary-faint: rgba(91, 74, 138, 0.08); - --color-primary-tint: rgba(91, 74, 138, 0.12); - --color-primary-wash: rgba(91, 74, 138, 0.20); + /* accent */ + --fs-accent: #5B4A8A; /* Scribe's signature colour */ + --fs-accent-soft: color-mix(in srgb, var(--fs-accent) 15%, transparent); /* Pill, tag and badge backgrounds */ + --fs-accent-faint: color-mix(in srgb, var(--fs-accent) 8%, transparent); /* The faintest accent wash */ + --fs-accent-deep: color-mix(in srgb, var(--fs-accent) 70%, black); /* The accent, darkened */ + --fs-accent-wash: color-mix(in srgb, var(--fs-accent) 22%, transparent); /* Heaviest accent tint */ + --fs-gradient-cta: linear-gradient(135deg, var(--fs-accent), var(--fs-accent-deep)); + --fs-glow-cta: 0 2px 10px color-mix(in srgb, var(--fs-accent) 35%, transparent); + --fs-glow-cta-hover: 0 4px 24px color-mix(in srgb, var(--fs-accent) 65%, transparent); - /* Action color set — Hybrid rule: action buttons use these, accent reserved for brand moments */ - --color-action-primary: #4A5D3F; - --color-action-primary-hover: #5A6F4D; - --color-action-secondary: #8B7355; - --color-action-secondary-hover: #A0876A; - --color-action-destructive: #6B2118; - --color-action-destructive-hover: #7E2A1F; - --color-action-ghost-border: #3F4651; + /* action */ + --fs-action-primary: #4A5D3F; /* Save, Submit, Confirm */ + --fs-action-secondary: #8B7355; /* Non-destructive alternates */ + --fs-action-tertiary: var(--fs-border-color); /* Ghost / outline actions */ + --fs-action-destructive: var(--fs-destructive); + --fs-action-primary-hover: color-mix(in srgb, var(--fs-action-primary) 88%, white); + --fs-action-secondary-hover: color-mix(in srgb, var(--fs-action-secondary) 88%, white); + --fs-action-destructive-hover: color-mix(in srgb, var(--fs-action-destructive) 88%, white); - --radius-sm: 6px; - --radius-md: 12px; - --radius-lg: 18px; - --radius-pill: 9999px; - --focus-ring: 0 0 0 2px rgba(91, 74, 138, 0.5); - /* Layout */ - --page-max-width: 1200px; - --page-padding-x: 1rem; - --sidebar-width: 260px; - --chat-reading-width: min(1200px, 100%); - --chat-context-sidebar-width: 220px; + /* border */ + --fs-border-color: #3F4651; /* Borders, dividers and ghost outlines */ + --fs-border: 0.5px solid var(--fs-border-color); + --fs-border-hover: 0.5px solid color-mix(in srgb, var(--fs-text-secondary) 30%, transparent); + --fs-border-active: 2px solid var(--fs-accent); /* Selected card or active tab only */ + + /* editor */ + --fs-wikilink: var(--fs-accent); + + /* elevation */ + --fs-shadow-1: 0 1px 0 rgba(0,0,0,0.4); /* Hairline lift */ + --fs-shadow-2: 0 4px 12px rgba(0,0,0,0.35); /* Dropdowns, popovers */ + --fs-shadow-3: 0 16px 40px rgba(0,0,0,0.5); /* Modals */ + + /* focus */ + --fs-focus-ring: 0 0 0 2px var(--fs-accent); + + /* font */ + --fs-font-display: 'Fraunces', Georgia, serif; + --fs-font-body: 'Inter', system-ui, sans-serif; + --fs-font-mono: 'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace; + + /* icon */ + --fs-icon-stroke: 1.5px; /* at 24px */ + --fs-icon-stroke-sm: 1px; /* at 16px */ + + /* layout */ + --fs-layout-page-max: 1200px; + --fs-layout-page-pad: 1rem; + --fs-layout-sidebar: 260px; + --fs-layout-header: 52px; + + /* motion */ + --fs-ease: cubic-bezier(0.2, 0.6, 0.2, 1); /* the one curve */ + --fs-dur-fast: 120ms; + --fs-dur-base: 180ms; + --fs-dur-slow: 280ms; + + /* priority */ + --fs-priority-low: var(--fs-info); + --fs-priority-low-bg: color-mix(in srgb, var(--fs-priority-low) 12%, transparent); + --fs-priority-medium: var(--fs-warning); + --fs-priority-medium-bg: color-mix(in srgb, var(--fs-priority-medium) 12%, transparent); + --fs-priority-high: var(--fs-error); + --fs-priority-high-bg: color-mix(in srgb, var(--fs-priority-high) 12%, transparent); + + /* radius */ + --fs-radius-sm: 4px; /* pills, tags, code spans */ + --fs-radius-md: 8px; /* buttons, inputs, small cards */ + --fs-radius-lg: 12px; /* cards, panels, modals */ + --fs-radius-xl: 16px; /* hero containers */ + --fs-radius-pill: 9999px; + + /* semantic */ + --fs-success: var(--fs-action-primary); + --fs-warning: #8B6F1E; + --fs-error: #C04A1F; + --fs-info: #3D5A6E; + --fs-destructive: #6B2118; /* irreversible — deliberately not the error colour */ + + /* space */ + --fs-space-1: 4px; + --fs-space-2: 8px; + --fs-space-3: 12px; + --fs-space-4: 16px; + --fs-space-5: 20px; + --fs-space-6: 24px; + --fs-space-7: 32px; + --fs-space-8: 48px; + --fs-space-9: 64px; + --fs-space-10: 96px; + + /* state */ + --fs-disabled-opacity: 0.5; + --fs-overlay: rgba(0, 0, 0, 0.65); + + /* status */ + --fs-status-todo: var(--fs-border-color); + --fs-status-todo-bg: color-mix(in srgb, var(--fs-status-todo) 12%, transparent); + --fs-status-in-progress: var(--fs-accent); + --fs-status-in-progress-bg: color-mix(in srgb, var(--fs-status-in-progress) 12%, transparent); + --fs-status-done: var(--fs-success); + --fs-status-done-bg: color-mix(in srgb, var(--fs-status-done) 12%, transparent); + --fs-overdue: var(--fs-error); + --fs-status-cancelled: var(--fs-text-tertiary); /* set aside, not failed */ + + /* surface */ + --fs-surface-page: #14171A; /* page bg, deepest surface */ + --fs-surface-raised: #1E2228; /* cards, raised elements */ + --fs-surface-hover: #2C313A; /* hovered surfaces */ + --fs-surface-code: var(--fs-surface-page); + --fs-surface-code-inline: var(--fs-surface-raised); + --fs-table-stripe: color-mix(in srgb, var(--fs-text-primary) 3%, transparent); + + /* text */ + --fs-text-primary: #E8E4D8; /* also the text colour on filled action buttons */ + --fs-text-secondary: #C2BFB4; + --fs-text-tertiary: #9C9A92; + + /* type */ + --fs-size-display: 40px; + --fs-size-h1: 32px; + --fs-size-h2: 24px; + --fs-size-h3: 18px; + --fs-size-body: 15px; + --fs-size-body-sm: 13px; + --fs-size-label: 12px; + --fs-size-code: 13px; + --fs-size-tiny: 11px; /* the only ALL CAPS, the only non-default tracking */ + --fs-weight-regular: 400; + --fs-weight-medium: 500; /* the heaviest the system goes */ + --fs-leading-heading: 1.3; + --fs-leading-body: 1.5; + --fs-leading-longform: 1.7; + --fs-tracking-tiny: 0.08em; } -[data-theme="dark"] { - /* Dark mode — Obsidian / Iron / Pewter */ - --color-bg: #14171A; - --color-bg-secondary: #1E2228; - --color-bg-card: #1E2228; - --color-surface: #2C313A; - --color-text: #E8E4D8; - --color-text-secondary: #C2BFB4; - --color-text-muted: #9C9A92; - --color-border: #3F4651; - --color-input-border: #3F4651; - --color-primary: #5B4A8A; - --color-danger: #C04A1F; - --color-tag-bg: rgba(91, 74, 138, 0.15); - --color-tag-text: #5B4A8A; +[data-theme="light"] { + /* accent */ + --fs-accent-wash: color-mix(in srgb, var(--fs-accent) 20%, transparent); + --fs-glow-cta-hover: 0 4px 20px color-mix(in srgb, var(--fs-accent) 55%, transparent); + + /* border */ + --fs-border-color: #D9D6CE; + + /* state */ + --fs-overlay: rgba(0, 0, 0, 0.45); + + /* surface */ + --fs-surface-page: #F5F1E8; + --fs-surface-raised: #FBF8F0; + --fs-surface-hover: #EFEAE0; + --fs-surface-code: #EBEDF0; + --fs-surface-code-inline: #EBEDF0; + + /* text */ + --fs-text-primary: #14171A; + --fs-text-secondary: #5A5852; + --fs-text-tertiary: #9A9890; +} + +/* ========================================================================== + COMPATIBILITY ALIASES — the app's historical names, pointing at the system. + + These exist so ~55 components keep working while they migrate to --fs-* + one at a time. Every one is a plain var() reference, which is what lets this + block be declared ONCE: when [data-theme="light"] moves --fs-surface-page, + --color-bg follows, because the alias resolves at use time. + + That is why this file lost 48 of its 60 dark-mode overrides — they were all + restating relationships the aliases now express directly. + + Removing this block is a rename sweep across the components, tracked + separately. Nothing new should reference a --color-* name. + ========================================================================== */ + +:root { + /* surfaces */ + --color-bg: var(--fs-surface-page); + --color-bg-secondary: var(--fs-surface-raised); + --color-bg-card: var(--fs-surface-raised); + --color-surface: var(--fs-surface-hover); + --color-code-bg: var(--fs-surface-code); + --color-code-inline-bg: var(--fs-surface-code-inline); + --color-table-stripe: var(--fs-table-stripe); + --color-overlay: var(--fs-overlay); + + /* text */ + --color-text: var(--fs-text-primary); + --color-text-secondary: var(--fs-text-secondary); + --color-text-muted: var(--fs-text-tertiary); + + /* lines */ + --color-border: var(--fs-border-color); + --color-input-border: var(--fs-border-color); + --focus-ring: var(--fs-focus-ring); + + /* brand */ + --color-primary: var(--fs-accent); + --color-primary-solid: var(--fs-accent); + --color-primary-deep: var(--fs-accent-deep); + --color-primary-faint: var(--fs-accent-faint); + --color-primary-tint: var(--fs-accent-soft); + --color-primary-wash: var(--fs-accent-wash); + --color-tag-bg: var(--fs-accent-soft); + --color-tag-text: var(--fs-accent); + --color-wikilink: var(--fs-wikilink); + --color-wikilink-bg: var(--fs-accent-soft); + --gradient-cta: var(--fs-gradient-cta); + --glow-cta: var(--fs-glow-cta); + --glow-cta-hover: var(--fs-glow-cta-hover); + + /* actions */ + --color-action-primary: var(--fs-action-primary); + --color-action-primary-hover: var(--fs-action-primary-hover); + --color-action-secondary: var(--fs-action-secondary); + --color-action-secondary-hover: var(--fs-action-secondary-hover); + --color-action-destructive: var(--fs-action-destructive); + --color-action-destructive-hover: var(--fs-action-destructive-hover); + + /* semantic */ + --color-success: var(--fs-success); + --color-warning: var(--fs-warning); + --color-danger: var(--fs-error); + --color-overdue: var(--fs-overdue); + --color-toast-success: var(--fs-success); + --color-toast-error: var(--fs-error); --color-shadow: rgba(0, 0, 0, 0.4); - --color-toast-success: #4A5D3F; - --color-toast-error: #C04A1F; - --color-status-todo: #3F4651; - --color-status-todo-bg: rgba(63, 70, 81, 0.18); - --color-status-in-progress: #5B4A8A; - --color-status-in-progress-bg: rgba(91, 74, 138, 0.18); - --color-status-done: #4A5D3F; - --color-status-done-bg: rgba(74, 93, 63, 0.18); - --color-priority-low: #3D5A6E; - --color-priority-low-bg: rgba(61, 90, 110, 0.18); - --color-priority-medium: #8B6F1E; - --color-priority-medium-bg: rgba(139, 111, 30, 0.18); - --color-priority-high: #C04A1F; - --color-priority-high-bg: rgba(192, 74, 31, 0.18); - --color-wikilink: #5B4A8A; - --color-wikilink-bg: rgba(91, 74, 138, 0.18); - --color-overdue: #C04A1F; - --color-code-bg: #14171A; - --color-code-inline-bg: #1E2228; - --color-table-stripe: rgba(255, 255, 255, 0.025); - --color-success: #4A5D3F; - --color-warning: #8B6F1E; - --color-input-bar-bg: #1E2228; - --color-input-bar-text: #E8E4D8; - --color-input-bar-placeholder: rgba(232, 228, 216, 0.35); - --color-overlay: rgba(0, 0, 0, 0.65); - --color-bubble-user-bg: transparent; - --color-bubble-user-border: #3F4651; - --color-bubble-user-text: #C2BFB4; - --color-bubble-asst-shadow: 0 4px 28px rgba(91, 74, 138, 0.14), 0 2px 8px rgba(0, 0, 0, 0.4); - --color-primary-solid: #5B4A8A; - --color-primary-deep: #3F3560; - --gradient-cta: linear-gradient(135deg, var(--color-primary-solid), var(--color-primary-deep)); - --glow-cta: 0 2px 12px rgba(91, 74, 138, 0.45); - --glow-cta-hover: 0 4px 24px rgba(91, 74, 138, 0.65); - --glow-soft: 0 0 18px rgba(91, 74, 138, 0.4); - --color-primary-faint: rgba(91, 74, 138, 0.10); - --color-primary-tint: rgba(91, 74, 138, 0.14); - --color-primary-wash: rgba(91, 74, 138, 0.22); - /* Action color set — identical across themes */ - --color-action-primary: #4A5D3F; - --color-action-primary-hover: #5A6F4D; - --color-action-secondary: #8B7355; - --color-action-secondary-hover: #A0876A; - --color-action-destructive: #6B2118; - --color-action-destructive-hover: #7E2A1F; - --color-action-ghost-border: #3F4651; + /* task status + priority */ + --color-status-todo: var(--fs-status-todo); + --color-status-todo-bg: var(--fs-status-todo-bg); + --color-status-in-progress: var(--fs-status-in-progress); + --color-status-in-progress-bg: var(--fs-status-in-progress-bg); + --color-status-done: var(--fs-status-done); + --color-status-done-bg: var(--fs-status-done-bg); + --color-priority-low: var(--fs-priority-low); + --color-priority-low-bg: var(--fs-priority-low-bg); + --color-priority-medium: var(--fs-priority-medium); + --color-priority-medium-bg: var(--fs-priority-medium-bg); + --color-priority-high: var(--fs-priority-high); + --color-priority-high-bg: var(--fs-priority-high-bg); + + /* geometry */ + --radius-sm: var(--fs-radius-sm); + --radius-md: var(--fs-radius-lg); /* NB: the app's "md" is the system's LARGE */ + --radius-lg: var(--fs-radius-xl); /* and the app's "lg" is the system's XL */ + --page-max-width: var(--fs-layout-page-max); + --page-padding-x: var(--fs-layout-page-pad); + --sidebar-width: var(--fs-layout-sidebar); + --header-height: var(--fs-layout-header); + + /* ------------------------------------------------------------------ + Names components reference that were NEVER declared anywhere. + + Each of these was reached for with a hardcoded fallback, so the page + rendered — but the fallback was what rendered, always, and several were + off-palette: --color-primary-bg fell back to an indigo, --color-destructive + to a brick that is not the oxblood, --color-status-cancelled to a grey from + no palette in this system. + + Wiring them to real tokens is the whole point of the exercise. Expect small + visual shifts exactly where a fallback had drifted; that shift IS the fix. + ------------------------------------------------------------------ */ + --color-accent: var(--fs-accent); + --color-accent-fg: var(--fs-text-primary); + --color-hover: var(--fs-surface-hover); + --color-bg-hover: var(--fs-surface-hover); + --color-bg-tertiary: var(--fs-surface-hover); + --color-surface-2: var(--fs-surface-hover); + --color-surface-alt: var(--fs-surface-hover); + --color-surface-raised: var(--fs-surface-raised); + --color-input-bg: var(--fs-surface-page); + --color-muted: var(--fs-text-tertiary); + --color-destructive: var(--fs-destructive); + --color-primary-bg: var(--fs-accent-soft); + --color-status-cancelled: var(--fs-status-cancelled); + --font-display: var(--fs-font-display); + --font-mono: var(--fs-font-mono); } +/* ========================================================================== + Base element styles + ========================================================================== */ + *, *::before, *::after { @@ -152,36 +310,36 @@ body { margin: 0; - background: var(--color-bg); - color: var(--color-text); - font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, - "Segoe UI", Roboto, sans-serif; + background: var(--fs-surface-page); + color: var(--fs-text-primary); + font-family: var(--fs-font-body); font-feature-settings: "cv11"; - line-height: 1.5; - transition: background-color 0.2s, color 0.2s; + line-height: var(--fs-leading-body); + transition: background-color var(--fs-dur-base) var(--fs-ease), + color var(--fs-dur-base) var(--fs-ease); } h1, h2 { - font-family: 'Fraunces', Georgia, serif; + font-family: var(--fs-font-display); font-optical-sizing: auto; - font-weight: 500; - line-height: 1.3; + font-weight: var(--fs-weight-medium); + line-height: var(--fs-leading-heading); } h3 { - font-family: 'Inter', system-ui, sans-serif; - font-weight: 500; - line-height: 1.3; + font-family: var(--fs-font-body); + font-weight: var(--fs-weight-medium); + line-height: var(--fs-leading-heading); } code, pre, kbd, samp { - font-family: 'JetBrains Mono', ui-monospace, "SF Mono", Menlo, Consolas, monospace; + font-family: var(--fs-font-mono); font-feature-settings: "liga", "calt"; } ::selection { - background: rgba(91, 74, 138, 0.3); - color: var(--color-text); + background: var(--fs-accent-wash); + color: var(--fs-text-primary); } input:focus-visible, @@ -190,15 +348,15 @@ select:focus-visible, button:focus-visible, a:focus-visible { outline: none; - box-shadow: var(--focus-ring); - border-radius: var(--radius-sm); + box-shadow: var(--fs-focus-ring); + border-radius: var(--fs-radius-sm); } button:not(:disabled):active, .btn:not(:disabled):active, [role="button"]:not(:disabled):active { transform: scale(0.97); - transition: transform 0.08s ease; + transition: transform 0.08s var(--fs-ease); } /* Responsive breakpoints: 480px (phone), 768px (tablet), 1024px (desktop) */ @@ -228,11 +386,11 @@ button:not(:disabled):active, background: transparent; } ::-webkit-scrollbar-thumb { - background: var(--color-border); - border-radius: 9999px; + background: var(--fs-border-color); + border-radius: var(--fs-radius-pill); } ::-webkit-scrollbar-thumb:hover { - background: var(--color-text-muted); + background: var(--fs-text-tertiary); } /* Floating inline assist button (teleported to body, cannot be scoped) */ @@ -240,14 +398,14 @@ button:not(:disabled):active, position: fixed; z-index: 150; transform: translateX(-50%); - background: var(--color-primary); - color: #fff; + background: var(--fs-accent); + color: var(--fs-text-primary); border: none; - border-radius: 999px; + border-radius: var(--fs-radius-pill); padding: 0.3rem 0.8rem; - font-size: 0.8rem; + font-size: var(--fs-size-body-sm); cursor: pointer; - box-shadow: 0 2px 8px var(--color-shadow); + box-shadow: var(--fs-shadow-2); white-space: nowrap; } .inline-assist-btn:hover { -- 2.54.0