Files
FabledScribe/frontend/src/assets/theme.css
T
bvandeusenandClaude Opus 5 4c9a637507
CI & Build / Python lint (push) Successful in 4s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Successful in 29s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 55s
CI & Build / Build & push image (push) Successful in 41s
fix(theme): text on a filled colour needs its own token — the old one inverts
76 hardcoded `color: #fff` now resolve to --fs-text-on-action, a new token
that is parchment in BOTH modes.

The design system said they should supersede to --fs-text-primary, on the
recorded reasoning that "there is no 'text on action' colour, there is just the
text colour." That is true on dark and wrong on light. --fs-text-primary
inverts to #14171A; the surfaces underneath it do not invert at all — every one
of these 76 sits on an action colour, a semantic colour, the accent, or the CTA
gradient, all of which hold a single value across modes.

Sweeping as recorded would have put obsidian text on moss green: roughly 2.4:1,
against a house style whose stated floor is WCAG AA. It would have looked
correct to me, because I checked it in the mode where it was correct.

--color-accent-fg had the same defect independently and is repointed too.

The token check now reports zero superseded literals, down from 30 files, and
raw colour literals drop 246 -> 169.

Closes #2275.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
2026-07-31 22:40:58 -04:00

426 lines
15 KiB
CSS

@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 {
/* 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 */
--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);
/* 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; /* body, headings, labels — inverts by mode */
--fs-text-secondary: #C2BFB4;
--fs-text-tertiary: #9C9A92;
--fs-text-on-action: #E8E4D8; /* text on a filled colour — NOT mode-dependent */
/* 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="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;
}
/* SUPERSEDES — write the token, not the literal.
* #fff -> --fs-text-on-action
* #ffffff -> --fs-text-on-action
* white -> --fs-text-on-action
* bold -> --fs-weight-medium
* bolder -> --fs-weight-medium
*/
/* ==========================================================================
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);
/* 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);
/* Foreground ON the accent, so it follows the accent's mode-independence,
not the page text's. Pointing this at --fs-text-primary made it invert to
obsidian on light — over a mid-tone accent, well under the AA floor. */
--color-accent-fg: var(--fs-text-on-action);
--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 {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--fs-surface-page);
color: var(--fs-text-primary);
font-family: var(--fs-font-body);
font-feature-settings: "cv11";
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: var(--fs-font-display);
font-optical-sizing: auto;
font-weight: var(--fs-weight-medium);
line-height: var(--fs-leading-heading);
}
h3 {
font-family: var(--fs-font-body);
font-weight: var(--fs-weight-medium);
line-height: var(--fs-leading-heading);
}
code, pre, kbd, samp {
font-family: var(--fs-font-mono);
font-feature-settings: "liga", "calt";
}
::selection {
background: var(--fs-accent-wash);
color: var(--fs-text-primary);
}
input:focus-visible,
textarea:focus-visible,
select:focus-visible,
button:focus-visible,
a:focus-visible {
outline: none;
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 var(--fs-ease);
}
/* Responsive breakpoints: 480px (phone), 768px (tablet), 1024px (desktop) */
@media (max-width: 768px) {
.hide-mobile {
display: none !important;
}
button,
[role="button"],
.btn-new-conv,
.btn-send {
min-height: 44px;
}
}
@media (min-width: 769px) {
.hide-desktop {
display: none !important;
}
}
/* Neutral hairline scrollbars — chrome is structural, not branded */
::-webkit-scrollbar {
width: 4px;
height: 4px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--fs-border-color);
border-radius: var(--fs-radius-pill);
}
::-webkit-scrollbar-thumb:hover {
background: var(--fs-text-tertiary);
}
/* Floating inline assist button (teleported to body, cannot be scoped) */
.inline-assist-btn {
position: fixed;
z-index: 150;
transform: translateX(-50%);
background: var(--fs-accent);
color: var(--fs-text-primary);
border: none;
border-radius: var(--fs-radius-pill);
padding: 0.3rem 0.8rem;
font-size: var(--fs-size-body-sm);
cursor: pointer;
box-shadow: var(--fs-shadow-2);
white-space: nowrap;
}
.inline-assist-btn:hover {
filter: brightness(1.1);
}