Files
multica/apps/web/app/custom.css
Jiayuan Zhang e54296eabb fix(ui): raise light muted-foreground to clear WCAG AA (MUL-5447) (#6095)
In light mode `--muted-foreground` at oklch(0.552 0.016 285.938) missed the
4.5:1 AA floor on every light surface except pure white. The worst pair was
3.98:1 — nav labels on --sidebar-accent, i.e. the primary navigation in its
hover state. One token backs ~2k `text-muted-foreground` call sites, so the
blast radius is most of the product's secondary text.

Drop lightness to 0.505 and leave hue and chroma alone so the neutral ramp
keeps its cast. Worst case is now 4.88:1. Dark mode already passed at
5.2-7.2:1 and is untouched.

The `.landing-light` block in apps/web re-declares the light palette so
token-driven components stay light under next-themes' `.dark` class; it moves
with the source or it silently drifts.

The new test asserts on the token file rather than on components: the token is
the contract every consumer inherits, and a component test could only prove a
class name is present, not that the pixels are legible.

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-29 15:37:20 +08:00

137 lines
6.8 KiB
CSS

/* =============================================================================
* Multica Web — Custom styles (non-shadcn, web-only)
* Shared styles (shiki, entrance-spin, sidebar, sonner, scrollbar) are in
* @multica/ui/styles/base.css
* ============================================================================= */
/* The landing route tree is intentionally always-light (hero/cli/cloud
* sections use hardcoded dark/light palettes). Shared components rendered
* inside (e.g. CloudWaitlistExpand on /download) use semantic tokens that
* otherwise flip to dark values under the `.dark` class set by next-themes,
* producing a palette mismatch against the hardcoded section. Re-declare
* tokens to their light values so nested token-driven components stay in
* lockstep with the surrounding design. */
.landing-light,
.landing-light * {
color-scheme: light;
}
.landing-light {
--app-shell: oklch(0.964435 0.001327 286.375);
--page-canvas: oklch(0.988087 0 0);
--surface: oklch(1 0 0);
--surface-foreground: oklch(0.141 0.005 285.823);
--surface-raised: oklch(1 0 0);
--surface-hover: oklch(0.967 0.001 286.375);
--surface-selected: oklch(0.95 0.002 286.375);
--surface-selected-foreground: oklch(0.141 0.005 285.823);
--surface-border: oklch(0.92 0.004 286.32);
--surface-shadow: 0 1px 2px rgb(15 23 42 / 0.04), 0 1px 1px rgb(15 23 42 / 0.03);
--floating-shadow: 0 16px 40px rgb(15 23 42 / 0.14), 0 3px 10px rgb(15 23 42 / 0.08);
--menu-shadow: 0 8px 24px rgb(15 23 42 / 0.08), 0 2px 6px rgb(15 23 42 / 0.05);
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
/* Kept in lockstep with the light value in tokens.css — see the AA
reasoning there before changing this number. */
--muted-foreground: oklch(0.505 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--brand: oklch(0.55 0.16 255);
--brand-foreground: oklch(0.985 0 0);
--success: oklch(0.55 0.16 145);
--warning: oklch(0.75 0.16 85);
--info: oklch(0.55 0.18 250);
--scrollbar-thumb: oklch(0 0 0 / 10%);
--scrollbar-thumb-hover: oklch(0 0 0 / 18%);
--scrollbar-track: transparent;
}
/* -----------------------------------------------------------------------------
* Landing editorial serif
*
* The landing tree swaps the app's `--font-serif` (Source Serif 4, set on <html>
* by app/layout.tsx) for Instrument Serif, loaded by app/(landing)/layout.tsx as
* `--font-instrument-serif`. The stack is composed here in static CSS rather
* than in next/font's `fallback` for the same reasons `--font-sans` is composed
* in app/globals.css: it has to be overridable per `<html lang>`, and it stays
* CSP-safe (no inline <style>). Keep the CJK ordering here in sync with the
* `--font-sans` chain in app/globals.css.
*
* Instrument Serif ships Latin only, so CJK headlines used to fall out of the
* stack entirely and land on the browser's default face — a sans, next to Latin
* set in a high-contrast display serif. The tail hands Han and Hangul to a
* platform Songti/Mincho/Myeongjo instead of shipping a multi-megabyte CJK
* webfont (an earlier `Noto_Serif_SC` next/font entry was never referenced by
* any rule and requested `subsets: ["latin"]`, so it downloaded no CJK glyphs).
*
* Default (en / zh / ko): Latin renders with Instrument Serif; Han falls through
* to the Chinese serifs, then Hangul to the Korean ones. Chinese MUST stay
* before Korean so zh users never get Korean Hanja glyph shapes (Hangul is a
* separate Unicode block, so ko users still get Korean fonts for Hangul). */
.landing-light {
--font-serif: var(--font-instrument-serif), "Songti SC", "SimSun",
"Noto Serif CJK SC", "Nanum Myeongjo", "AppleMyungjo", "Batang",
"Noto Serif CJK KR", ui-serif, serif;
}
/* Japanese: Kanji are Han ideographs sharing the same Unicode block as Chinese,
and CSS font-fallback order is NOT affected by `<html lang>` — so the
Chinese-first default above would hand Japanese users Chinese glyph shapes for
shared ideographs. Promote a Mincho-first chain only for Japanese. Instrument
Serif still leads for Latin; zh/ko remain as a deep fallback. */
html[lang|="ja"] .landing-light {
--font-serif: var(--font-instrument-serif), "Hiragino Mincho ProN",
"Yu Mincho", "MS Mincho", "Noto Serif CJK JP", "Songti SC", "SimSun",
"Noto Serif CJK SC", "Nanum Myeongjo", "AppleMyungjo", "Batang",
"Noto Serif CJK KR", ui-serif, serif;
}
/* Single hook for every landing surface set in the editorial serif. Declared
outside Tailwind's layers so the per-locale resets below outrank the inline
`tracking-*` / `leading-*` utilities they have to correct. */
.landing-serif {
font-family: var(--font-serif);
}
/* The landing display sizes are tuned for Latin: negative tracking and sub-1
leading, down to `tracking-[-0.038em] leading-[0.93]` at 6.4rem on the hero.
Both are wrong for CJK — Han and Hangul already fill their em box, so negative
tracking jams the glyphs together and a 0.93 line-height collides the two
lines of a `<br />`-split headline. Reset them for CJK locales only; Latin
locales keep the original treatment untouched.
`[lang|="zh"]` is the BCP-47 language-range selector: it matches exactly `zh`
or `zh-<region>` (app/layout.tsx emits `zh-CN`, `ja-JP`, `ko-KR`), never an
unrelated subtag such as `zha`. Scoped to headings so the footer wordmark — a
Latin-only <span> rendering "multica" at up to 16rem — keeps its tuned
tracking on every locale. */
html[lang|="zh"] :is(h1, h2).landing-serif,
html[lang|="ja"] :is(h1, h2).landing-serif,
html[lang|="ko"] :is(h1, h2).landing-serif {
letter-spacing: 0;
line-height: 1.25;
}
/* Korean only: Hangul is written with spaces between words, but the default
line-break rules treat every syllable as a break opportunity, so a wrapped
headline splits mid-word (모릅니 / 다). `keep-all` restores word-boundary
breaking; `break-word` is the escape hatch so a word wider than the column
still breaks instead of overflowing at narrow viewports. Chinese and Japanese
are written without spaces and break per character correctly by default. */
html[lang|="ko"] :is(h1, h2).landing-serif {
word-break: keep-all;
overflow-wrap: break-word;
}