diff --git a/core/base.css b/core/base.css index cc4af6f5..d297c28f 100644 --- a/core/base.css +++ b/core/base.css @@ -35,6 +35,31 @@ --sf-color-error: var(--sf-color-error-dark); --sf-color-info: var(--sf-color-info-dark); --sf-color-danger: var(--sf-color-danger-dark); + + /* Text — inverted direction for dark backgrounds, clamped for any neutral value */ + --sf-color-text: oklch(from var(--sf-color-neutral) clamp(0.70, calc(l + 0.25), 1) c h); + --sf-color-text--secondary: oklch(from var(--sf-color-neutral) clamp(0.55, calc(l + 0.1), 0.90) c h); + --sf-color-text--placeholder: oklch(from var(--sf-color-neutral) clamp(0.35, calc(l - 0.1), 0.65) c h); + --sf-color-text--disabled: oklch(from var(--sf-color-neutral) clamp(0.25, calc(l - 0.2), 0.55) c h); + --sf-color-text--inverse: oklch(from var(--sf-color-neutral) calc(l - 0.4) c h); + --sf-color-heading: oklch(from var(--sf-color-neutral) clamp(0.70, calc(l + 0.25), 1) c h); + + /* Borders — adjusted for dark backgrounds, clamped for any neutral value */ + --sf-color-border: oklch(from var(--sf-color-neutral) clamp(0.25, calc(l - 0.3), 0.55) 0.005 h); + --sf-color-border--subtle: oklch(from var(--sf-color-neutral) clamp(0.20, calc(l - 0.38), 0.45) 0.005 h); + --sf-color-border--strong: oklch(from var(--sf-color-neutral) clamp(0.38, calc(l - 0.1), 0.65) 0.02 h); + --sf-color-border--disabled: oklch(from var(--sf-color-neutral) clamp(0.25, calc(l - 0.3), 0.55) 0.005 h); + --sf-color-border--translucent: oklch(from var(--sf-color-neutral) l c h / 0.15); + + /* Shadow strength — more visible on dark surfaces */ + --sf-shadow-strength: 0.25; + + /* Status text — lightened for dark backgrounds, clamped */ + --sf-status-success-text: oklch(from var(--sf-color-success) clamp(0.70, calc(l + 0.15), 1) c h); + --sf-status-warning-text: oklch(from var(--sf-color-warning) clamp(0.70, calc(l + 0.05), 1) c h); + --sf-status-error-text: oklch(from var(--sf-color-error) clamp(0.70, calc(l + 0.15), 1) c h); + --sf-status-info-text: oklch(from var(--sf-color-info) clamp(0.70, calc(l + 0.15), 1) c h); + --sf-status-danger-text: var(--sf-status-error-text); } } @@ -49,6 +74,11 @@ /* ---------------------------------------------------------- Dark mode — swap brand & status colors to -dark variants. Works on :root (whole page) or any element (section-level). + + NOTE: The derived semantic overrides (text, border, shadow, + status text) are duplicated in the @media block above. + CSS has no syntax to share declarations across a media + condition and a selector — keep both blocks in sync. ---------------------------------------------------------- */ [data-theme="dark"] { @@ -64,6 +94,31 @@ --sf-color-error: var(--sf-color-error-dark); --sf-color-info: var(--sf-color-info-dark); --sf-color-danger: var(--sf-color-danger-dark); + + /* Text — inverted direction for dark backgrounds, clamped for any neutral value */ + --sf-color-text: oklch(from var(--sf-color-neutral) clamp(0.70, calc(l + 0.25), 1) c h); + --sf-color-text--secondary: oklch(from var(--sf-color-neutral) clamp(0.55, calc(l + 0.1), 0.90) c h); + --sf-color-text--placeholder: oklch(from var(--sf-color-neutral) clamp(0.35, calc(l - 0.1), 0.65) c h); + --sf-color-text--disabled: oklch(from var(--sf-color-neutral) clamp(0.25, calc(l - 0.2), 0.55) c h); + --sf-color-text--inverse: oklch(from var(--sf-color-neutral) calc(l - 0.4) c h); + --sf-color-heading: oklch(from var(--sf-color-neutral) clamp(0.70, calc(l + 0.25), 1) c h); + + /* Borders — adjusted for dark backgrounds, clamped for any neutral value */ + --sf-color-border: oklch(from var(--sf-color-neutral) clamp(0.25, calc(l - 0.3), 0.55) 0.005 h); + --sf-color-border--subtle: oklch(from var(--sf-color-neutral) clamp(0.20, calc(l - 0.38), 0.45) 0.005 h); + --sf-color-border--strong: oklch(from var(--sf-color-neutral) clamp(0.38, calc(l - 0.1), 0.65) 0.02 h); + --sf-color-border--disabled: oklch(from var(--sf-color-neutral) clamp(0.25, calc(l - 0.3), 0.55) 0.005 h); + --sf-color-border--translucent: oklch(from var(--sf-color-neutral) l c h / 0.15); + + /* Shadow strength — more visible on dark surfaces */ + --sf-shadow-strength: 0.25; + + /* Status text — lightened for dark backgrounds, clamped */ + --sf-status-success-text: oklch(from var(--sf-color-success) clamp(0.70, calc(l + 0.15), 1) c h); + --sf-status-warning-text: oklch(from var(--sf-color-warning) clamp(0.70, calc(l + 0.05), 1) c h); + --sf-status-error-text: oklch(from var(--sf-color-error) clamp(0.70, calc(l + 0.15), 1) c h); + --sf-status-info-text: oklch(from var(--sf-color-info) clamp(0.70, calc(l + 0.15), 1) c h); + --sf-status-danger-text: var(--sf-status-error-text); } /* ---------------------------------------------------------- diff --git a/core/layout.css b/core/layout.css index 9dbc9b67..43c8696d 100644 --- a/core/layout.css +++ b/core/layout.css @@ -104,6 +104,7 @@ .sf-cluster--2xs { gap: var(--sf-space-2xs); } .sf-cluster--xs { gap: var(--sf-space-xs); } .sf-cluster--s { gap: var(--sf-space-s); } + .sf-cluster--m { gap: var(--sf-space-m); } .sf-cluster--l { gap: var(--sf-space-l); } .sf-cluster--xl { gap: var(--sf-space-xl); } diff --git a/core/tokens.css b/core/tokens.css index 9e31b460..a9527fbf 100644 --- a/core/tokens.css +++ b/core/tokens.css @@ -63,41 +63,46 @@ /* ---------------------------------------------------------- Text — derived from --sf-color-neutral + clamp() ensures safe output for any neutral value. ---------------------------------------------------------- */ - --sf-color-text: oklch(from var(--sf-color-neutral) calc(l - 0.4) c h); - --sf-color-text--secondary: oklch(from var(--sf-color-neutral) calc(l - 0.25) c h); + --sf-color-text: oklch(from var(--sf-color-neutral) clamp(0.05, calc(l - 0.4), 0.35) c h); + --sf-color-text--secondary: oklch(from var(--sf-color-neutral) clamp(0.15, calc(l - 0.25), 0.45) c h); --sf-color-text--muted: var(--sf-color-neutral); - --sf-color-text--placeholder: oklch(from var(--sf-color-neutral) calc(l + 0.15) c h); - --sf-color-text--disabled: oklch(from var(--sf-color-neutral) calc(l + 0.25) c h); + --sf-color-text--placeholder: oklch(from var(--sf-color-neutral) clamp(0.45, calc(l + 0.15), 0.75) c h); + --sf-color-text--disabled: oklch(from var(--sf-color-neutral) clamp(0.55, calc(l + 0.25), 0.82) c h); --sf-color-text--inverse: oklch(from var(--sf-color-neutral) calc(l + 0.4) c h); - --sf-color-heading: oklch(from var(--sf-color-neutral) calc(l - 0.4) c h); + --sf-color-heading: oklch(from var(--sf-color-neutral) clamp(0.05, calc(l - 0.4), 0.35) c h); /* ---------------------------------------------------------- - Text-on-color — white or dark based on luminance + Text-on-color — auto white or dark based on source luminance. + sign(0.6 - l): +1 when dark bg (l<0.6) → light text (0.95), + -1 when light bg (l>0.6) → dark text (0.10). + Works automatically in dark mode: tokens reference the active + color which is already swapped to its -dark variant. ---------------------------------------------------------- */ - --sf-color-text--on-primary: oklch(1 0 0); - --sf-color-text--on-secondary: oklch(0.97 0.005 260); - --sf-color-text--on-tertiary: oklch(1 0 0); - --sf-color-text--on-action: oklch(1 0 0); - --sf-color-text--on-neutral: oklch(1 0 0); + --sf-color-text--on-primary: oklch(from var(--sf-color-primary) clamp(0.1, sign(0.6 - l) * 999, 0.95) 0 0); + --sf-color-text--on-secondary: oklch(from var(--sf-color-secondary) clamp(0.1, sign(0.6 - l) * 999, 0.95) 0 0); + --sf-color-text--on-tertiary: oklch(from var(--sf-color-tertiary) clamp(0.1, sign(0.6 - l) * 999, 0.95) 0 0); + --sf-color-text--on-action: oklch(from var(--sf-color-action) clamp(0.1, sign(0.6 - l) * 999, 0.95) 0 0); + --sf-color-text--on-neutral: oklch(from var(--sf-color-neutral) clamp(0.1, sign(0.6 - l) * 999, 0.95) 0 0); --sf-color-text--on-base: var(--sf-color-text); --sf-color-text--on-inverse: var(--sf-color-text--inverse); - --sf-color-text--on-success: oklch(1 0 0); - --sf-color-text--on-warning: oklch(0.2 0.02 70); - --sf-color-text--on-error: oklch(1 0 0); - --sf-color-text--on-info: oklch(1 0 0); - --sf-color-text--on-danger: oklch(1 0 0); + --sf-color-text--on-success: oklch(from var(--sf-color-success) clamp(0.1, sign(0.6 - l) * 999, 0.95) 0 0); + --sf-color-text--on-warning: oklch(from var(--sf-color-warning) clamp(0.1, sign(0.6 - l) * 999, 0.95) 0 0); + --sf-color-text--on-error: oklch(from var(--sf-color-error) clamp(0.1, sign(0.6 - l) * 999, 0.95) 0 0); + --sf-color-text--on-info: oklch(from var(--sf-color-info) clamp(0.1, sign(0.6 - l) * 999, 0.95) 0 0); + --sf-color-text--on-danger: oklch(from var(--sf-color-danger) clamp(0.1, sign(0.6 - l) * 999, 0.95) 0 0); /* ---------------------------------------------------------- Borders — derived from --sf-color-neutral ---------------------------------------------------------- */ - --sf-color-border: oklch(from var(--sf-color-neutral) calc(l + 0.35) 0.005 h); - --sf-color-border--subtle: oklch(from var(--sf-color-neutral) calc(l + 0.4) 0.005 h); - --sf-color-border--strong: oklch(from var(--sf-color-neutral) calc(l + 0.1) 0.02 h); + --sf-color-border: oklch(from var(--sf-color-neutral) clamp(0.70, calc(l + 0.35), 0.95) 0.005 h); + --sf-color-border--subtle: oklch(from var(--sf-color-neutral) clamp(0.75, calc(l + 0.4), 0.97) 0.005 h); + --sf-color-border--strong: oklch(from var(--sf-color-neutral) clamp(0.55, calc(l + 0.1), 0.85) 0.02 h); --sf-color-border--focus: var(--sf-color-action); - --sf-color-border--disabled: oklch(from var(--sf-color-neutral) calc(l + 0.35) 0.005 h); + --sf-color-border--disabled: oklch(from var(--sf-color-neutral) clamp(0.70, calc(l + 0.35), 0.95) 0.005 h); --sf-color-border--translucent: oklch(from var(--sf-color-neutral) l c h / 0.15); diff --git a/docs/demo.html b/docs/demo.html index e750e50a..088410d3 100644 --- a/docs/demo.html +++ b/docs/demo.html @@ -83,7 +83,6 @@ } #main { - flex: 1; min-width: 0; } @@ -188,8 +187,8 @@ /* ── radius swatches ──────────────────────────────────── */ .demo-radius-swatch { - width: 4.5rem; - height: 4.5rem; + width: 6rem; + height: 5rem; background: var(--sf-color-primary-200, color-mix(in oklch, var(--sf-color-primary) 25%, var(--sf-color-bg))); border: 2px solid var(--sf-color-primary); display: flex; @@ -233,11 +232,12 @@ background: var(--sf-color-primary); position: absolute; top: 0; + left: 0; } @keyframes demo-slide { - from { translate: 0; } - to { translate: calc(100% - 2rem); } + from { left: 0; } + to { left: calc(100% - 2rem); } } .demo-motion-warning { @@ -395,7 +395,7 @@ -
+

Status Triplets

-
+
Success
Warning
Error
@@ -482,7 +482,7 @@

Status Triplets

Text Hierarchy

-
+

--sf-color-text (primary)

--sf-color-text--secondary

--sf-color-text--muted

@@ -492,7 +492,7 @@

Text Hierarchy

Borders

-
+
--sf-color-border
--sf-color-border--subtle
--sf-color-border--strong
@@ -547,7 +547,7 @@
h6 — The quick brown fox

Fluid Text Scale (--sf-text-*)

All sizes use clamp() — resize the window to see fluid scaling.

-
+
2xs — The quick brown fox jumps over the lazy dog
xs — The quick brown fox jumps over the lazy dog
s — The quick brown fox jumps
@@ -563,7 +563,7 @@

Fluid Text Scale (--sf-text-*)

Font Weights

-
+
body 400 — The quick heading 600 — The quick bold 700 — The quick @@ -591,7 +591,7 @@

Line Heights (--sf-leading-*)

Letter Spacing (--sf-tracking-*)

-
+
tracking-tight — THE QUICK BROWN FOX JUMPS OVER
tracking-normal — THE QUICK BROWN FOX JUMPS OVER
tracking-wide — THE QUICK BROWN FOX
@@ -624,7 +624,7 @@

Semantic Elements (base.css)

Link States

-
+
default link hover state visited state @@ -639,7 +639,7 @@

Link States

Spacing

All fluid tokens use clamp() — resize the window to see bars animate.

-
+
--sf-space-none
@@ -688,7 +688,7 @@

Spacing

Spacing Aliases

-
+
--sf-gap → --sf-space-m --sf-content-gap → --sf-space-s --sf-component-pad → --sf-space-m @@ -724,7 +724,7 @@

Shadows

Border Radius

All values multiplied by --sf-radius-scale (1 by default).

-
+
none
0
xs
2px
s
4px
@@ -749,7 +749,7 @@

Motion & States

Transitions (motion.css — hover these)

-
+
@@ -760,7 +760,7 @@

Transitions (motion.css — hover these)

Easing Curves

-
+
ease-linear
@@ -788,7 +788,7 @@

Easing Curves

Duration Tokens

-
+
instant: 100ms fast: 150ms normal: 250ms @@ -797,14 +797,14 @@

Duration Tokens

States (states.css)

-
+

.is-loading

.is-skeleton

-
+
@@ -812,7 +812,7 @@

States (states.css)

.is-hidden / .is-invisible

-
+
visible @@ -836,7 +836,7 @@

Skip Link

Focus Ring (accessibility.css)

Tab through these elements to verify :focus-visible ring (2px solid, 2px offset, action color). The ring uses !important so it's never overridden.

-
+
Link @@ -854,7 +854,7 @@

.sr-only / .visually-hidden

Hidden from sighted users but available to screen readers: [THIS TEXT IS SCREEN-READER ONLY — not visible] (check with DevTools or a screen reader)

Disabled States

-
+
@@ -885,10 +885,10 @@

Color Scheme Token

Layout — Stack

.sf-stack — flex column with uniform gap. Modifier suffix sets gap size.

-
+
.sf-stack--xs -
+
Item 1
Item 2
Item 3
@@ -896,7 +896,7 @@

Layout — Stack

.sf-stack--m -
+
Item 1
Item 2
Item 3
@@ -904,7 +904,7 @@

Layout — Stack

.sf-stack--xl -
+
Item 1
Item 2
Item 3
@@ -921,7 +921,7 @@

Layout — Stack

Layout — Cluster

.sf-cluster — flex wrapping group with uniform gap. Ideal for tags, buttons, inline groups.

-
+
Design tokens
CSS layers
Layout primitives
@@ -1006,26 +1006,26 @@

Layout — Cover

Layout — Frame

.sf-frame — aspect-ratio container. Media children use object-fit: cover.

-
-
+
+
sf-frame--video (16:9)
16 / 9
-
+
sf-frame--square (1:1)
1 / 1
-
+
sf-frame--portrait (3:4)
3 / 4
-
+
sf-frame--cinema (21:9)
21 / 9 @@ -1086,7 +1086,7 @@

Lists Are Restored

Layout — Container & Section

.sf-container — centered max-width with horizontal gutters and container-type: inline-size. .sf-section adds vertical padding.

-
+
sf-container--narrow (38rem)
narrow content
@@ -1169,19 +1169,19 @@

Reset & Base

Form Elements (reset.css + base.css)

-
+
-
+
-
+
-
+
-
+
-
+