diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 87f7dba7..fdd77d0a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -103,5 +103,4 @@ jobs: dist/slashed.full.flat.css dist/slashed.full.flat.min.css dist/slashed.full.flat.min.css.map - dist/slashed.zip fail_on_unmatched_files: true diff --git a/.github/workflows/version-sync.yml b/.github/workflows/version-sync.yml index 8bbb4bc7..417bfa18 100644 --- a/.github/workflows/version-sync.yml +++ b/.github/workflows/version-sync.yml @@ -1,24 +1,19 @@ name: Sync version artifacts # Triggered whenever a GitHub release or pre-release is published. -# Checks out main, aligns all non-JS version references to the release tag, -# rebuilds CSS bundles, publishes them to the dist branch, captures the -# resulting commit SHA, stores it in SLASHED_DIST_SHA (slashed.php), -# SLASHED_BRICKS_DIST_SHA (slashed-bricks.php), and -# SLASHED_GUTENBERG_DIST_SHA (slashed-gutenberg.php), then commits -# everything back to main. +# Checks out main, aligns non-JS version references (docs/roadmap.md) and +# package.json to the release tag, then commits any changes back to main. # # CSS bundles are NOT committed to the main branch — they live only on the -# dedicated `dist` branch and in GitHub Release assets. The dist-branch -# commit SHA is stored in each integration entry point so the plugin can -# construct an immutable jsDelivr CDN URL that never changes between releases. +# dedicated `dist` branch (published by publish-dist.yml on every push to +# main) and in GitHub Release assets. # # Full release pipeline: # 1. Developer runs `npm run release` locally → bumps package.json, runs # version-sync + build, commits chore(release): vX.Y.Z, pushes tag. # 2. release.yml fires on the tag → uploads dist assets to the GitHub release. -# 3. This workflow fires on the release.published event → rebuilds CSS, -# publishes to dist branch, captures SHA, updates PHP, commits to main. +# 3. This workflow fires on the release.published event → re-aligns version +# references on main in case the release was cut outside `npm run release`. on: release: @@ -64,76 +59,13 @@ jobs: - name: Sync non-JS version references run: node scripts/version-sync.js - - name: Rebuild CSS bundles - run: npm run build - - - name: Push CSS bundles to dist branch and capture SHA - id: dist - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RELEASE_TAG: ${{ steps.ver.outputs.tag }} - GIT_AUTHOR_NAME: github-actions[bot] - GIT_AUTHOR_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com - GIT_COMMITTER_NAME: github-actions[bot] - GIT_COMMITTER_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com - run: | - set -euo pipefail - - # Stage dist/ contents for the orphan dist branch. - mkdir -p /tmp/sf-dist - cp -R dist/. /tmp/sf-dist/ - { - echo "source-commit: ${GITHUB_SHA}" - echo "source-ref: refs/heads/main" - echo "release-tag: ${RELEASE_TAG}" - echo "built-at: $(date -u +%Y-%m-%dT%H:%M:%SZ)" - echo "built-by: .github/workflows/version-sync.yml" - } > /tmp/sf-dist/SOURCE.txt - echo 'node_modules/' > /tmp/sf-dist/.gitignore - - git checkout --orphan dist-release - git rm -rf . > /dev/null - shopt -s dotglob - cp -R /tmp/sf-dist/. . - git add -A - git commit -m "build: dist for ${RELEASE_TAG}" - - DIST_SHA=$(git rev-parse HEAD) - echo "sha=${DIST_SHA}" >> "$GITHUB_OUTPUT" - - git push --force \ - "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \ - dist-release:dist - - - name: Switch back to main working tree - run: git checkout main - - - name: Update DIST_SHA constants in PHP - env: - DIST_SHA: ${{ steps.dist.outputs.sha }} - run: | - # Write the captured SHA into the canonical constant, then let - # version-sync.js propagate it to the two integration entry points. - sed -i \ - "s|define( 'SLASHED_DIST_SHA', '[^']*' )|define( 'SLASHED_DIST_SHA', '${DIST_SHA}' )|" \ - plugins/SLASHED-for-WP/slashed.php - # Hard-fail if the substitution did not land (sed exits 0 even on no-op, - # e.g. if the define format ever changes) so we never propagate a stale SHA. - if ! grep -Fq "define( 'SLASHED_DIST_SHA', '${DIST_SHA}' )" plugins/SLASHED-for-WP/slashed.php; then - echo "Failed to update SLASHED_DIST_SHA in plugins/SLASHED-for-WP/slashed.php" >&2 - exit 1 - fi - node scripts/version-sync.js - - name: Commit and push if anything changed env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - # Stage only tracked files (dist/*.css are gitignored). - git add plugins/SLASHED-for-WP/slashed.php plugins/SLASHED-for-WP/integrations/bricks/slashed-bricks.php plugins/SLASHED-for-WP/integrations/gutenberg/slashed-gutenberg.php package.json package-lock.json - git add -u + git add package.json package-lock.json docs/roadmap.md if git diff --cached --quiet; then echo "Nothing to commit — all version artifacts already up to date." else diff --git a/.stylelintrc.json b/.stylelintrc.json index 3ac85685..39a77ce1 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -1,6 +1,6 @@ { "extends": ["stylelint-config-standard"], - "ignoreFiles": ["dist/**", "node_modules/**", "plugins/SLASHED-for-WP/integrations/bricks/assets/admin-app/**", "plugins/SLASHED-for-WP/integrations/bricks/assets/editor-app/**", "plugins/SLASHED-for-WP/integrations/bricks/editor-app/**", "plugins/SLASHED-for-WP/integrations/gutenberg/assets/admin-app/**"], + "ignoreFiles": ["dist/**", "node_modules/**"], "rules": { "color-function-notation": "modern", "alpha-value-notation": "number", diff --git a/CHANGELOG.md b/CHANGELOG.md index 95d51e00..82149f64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -145,9 +145,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **New tokens**: `--sf-header-height-mobile`, `--sf-header-height-desktop`, `--sf-sticky-offset-mobile`, `--sf-sticky-offset-desktop` — paired mobile/desktop source tokens for the header height and sticky-offset - computed tokens. The WordPress plugin admin (Layouts tab) exposes these - as number fields and generates a fluid `clamp()` for - `--sf-header-height` / `--sf-sticky-offset` when the two values differ. + computed tokens. `--sf-header-height` / `--sf-sticky-offset` fluidly + `clamp()` between the mobile and desktop endpoints when they differ. - **Contextual color cascade** — `.sf-surface--*` variants now inherit a full set of on-surface tokens (`--sf-color-text`, `--sf-color-heading`, @@ -155,21 +154,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), CSS relative-color syntax inside an `@supports` guard. Requires Chrome 123 / Safari 17.5 / Firefox 128 (the existing framework floor). -- **reBEMer** (`integrations/bricks/`) — Subtree-scoped BEM class manager - for the Bricks Builder structure panel. Adds a "BEM" badge to every - structure-panel item; clicking it opens a draggable modal that names a - block + every descendant element + an optional modifier and applies the - result as global classes in one transaction. Five operation modes - (Add / Rename / Replace / Add Modifier / Migrate ID styles), a - client-side reference-usage check (via `GET /rebemer/unused`) so destructive - ops surface cross-element class usage before they run, snapshot + rollback for - every apply, an in-panel undo ring buffer, and a reserved-name guard - against SLASHED's own utility classes. Full design at - [docs/rebemer.md](docs/rebemer.md). The editor app lives in - `integrations/bricks/editor-app/` (Svelte 5 runes + Vite); the PHP side - is `Slashed_Bricks_ReBEMer_{Policy,REST,Enqueue}` under - `integrations/bricks/includes/`. - ### ⚠️ Breaking Changes (pre-freeze cleanup) The framework is in pre-freeze cleanup. No published consumer is affected. @@ -416,9 +400,8 @@ See `docs/migration.md` for details. between their `-mobile` and `-desktop` companion tokens across the framework viewport range, instead of statically equalling the desktop value. This wires up the previously-orphaned `--sf-header-height-mobile`, - `--sf-sticky-offset-mobile`, and `--sf-sticky-offset-desktop` tokens and - matches the clamp the WP plugin's CSS generator already emitted. Default - endpoints (3.5rem mobile, 5rem desktop) are unchanged; the header is now + `--sf-sticky-offset-mobile`, and `--sf-sticky-offset-desktop` tokens. + Default endpoints (3.5rem mobile, 5rem desktop) are unchanged; the header is now shorter on small viewports. - **Shadows tint to the surface on `.sf-surface--*`** — `core/macros.css` @@ -493,12 +476,7 @@ See `docs/migration.md` for details. ## [0.2.12] - 2026-05-23 -Release infrastructure and Bricks Builder integration. - -### Added - -- **Bricks Builder integration** — WordPress plugin for one-click SLASHED - loading in Bricks Builder themes (PR #71) +Release infrastructure. ### Changed diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5c98ac04..c862fe8e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -69,12 +69,12 @@ release pipeline reads the matching `## [x.y.z]` section, so keep it accurate: `## [x.y.z] - YYYY-MM-DD` heading and leave a fresh empty `## [Unreleased]`. 2. Run `npm run release` (patch), `npm run release:minor`, or `npm run release:major`. This bumps `package.json`, runs - `scripts/version-sync.js` (propagates the version to the PHP plugins, - `docs/roadmap.md`, the Bricks README CDN example, and `readme.txt`'s - `Stable tag`), rebuilds bundles, commits, tags, and pushes. + `scripts/version-sync.js` (propagates the version to `docs/roadmap.md`), + rebuilds bundles, commits, tags, and pushes. 3. The pushed tag triggers `release.yml` (GitHub Release + dist assets) and the - release event triggers `version-sync.yml` (publishes the `dist` branch and - writes the captured commit SHA into the `SLASHED_*_DIST_SHA` constants). + release event triggers `version-sync.yml` (re-aligns version references on + main). The `dist` branch is published by `publish-dist.yml` on every push to + main. ## Scope diff --git a/README.md b/README.md index 3b2a0323..09066d83 100644 --- a/README.md +++ b/README.md @@ -195,55 +195,10 @@ versions above — colors will collapse to `initial` on older engines. | [Browser support](docs/browser-support.md) | the support floor and why | | [Performance](docs/performance.md) | modern-CSS footguns to avoid | | [Migration](docs/migration.md) | upgrading SLASHED versions + migrating from other frameworks | -| [reBEMer](docs/rebemer.md) | BEM class manager for Bricks Builder | -| [Bricks plugin](plugins/SLASHED-for-WP/integrations/bricks/README.md) | full Bricks Builder integration docs | | [Contributing](CONTRIBUTING.md) | setup, conventions, tests | -## WordPress plugin - -SLASHED ships a companion WordPress plugin (`plugins/SLASHED-for-WP/`) with two independent integrations that can each be activated as a standalone plugin or loaded together from the main `slashed.php` bootstrap. - -### Bricks Builder - -Activate `integrations/bricks/slashed-bricks.php` (requires Bricks 1.9.2+, WordPress 6.4+, PHP 7.4+). - -- **CSS loading** — enqueues the SLASHED bundle on the frontend and inside the Bricks editor iframe -- **Variable pickers** — registers all `--sf-*` tokens (572 in `essential`, 812 in `optimal`/`full`) in the Bricks Global Variable Manager, organised by category -- **Class autocomplete** — registers every `.sf-*` layout class and `.is-*` state class in the Bricks class input as locked entries -- **Color System panel** — a floating in-builder browser for every `--sf-color-*` token; each swatch previews its light and dark value at once, and picking one applies the `var(--sf-color-*)` reference (never a baked hex) to the selected element -- **Variable-picker swatches** — paints a colour square next to each `--sf-color-*` entry in the variable-picker dropdown, powered by server-resolved hex values so dark-mode stays framework-driven -- **Dynamic detection** — parses the active CSS bundle at runtime; no hand-curated list to drift out of date -- **reBEMer** — subtree-scoped BEM class manager inside the Bricks structure panel: rename, replace, or add modifiers across an element and its children in one transaction with reference-count checks (`GET /rebemer/unused`) and snapshot/rollback - -See [`integrations/bricks/README.md`](plugins/SLASHED-for-WP/integrations/bricks/README.md) and [`docs/rebemer.md`](docs/rebemer.md). - -### Gutenberg (block editor) - -Activate `integrations/gutenberg/slashed-gutenberg.php` (requires WordPress 6.4+, PHP 7.4+). - -- **CSS loading** — enqueues the SLASHED bundle in the block editor canvas and on the frontend -- **Color palette** — syncs 20 `--sf-color-*` tokens (brand, status, surface, text, border, link) with the WordPress editor color palette -- **Dark-mode bridge** — maps the `data-wp-dark-mode-active` attribute from the block editor's dark-mode toggle to SLASHED's `color-scheme` system - -### Filters - -Shared (fire in both integrations and standalone, when the unified `slashed.php` bootstrap is active): - -- `slashed/css_bundle_url` — override the resolved CSS bundle URL for every integration -- `slashed/override_css` — replace/extend the generated token-override CSS - -Per integration: - -- `slashed_gutenberg/css_bundle_url` — override the bundle URL for the Gutenberg integration only -- Bricks exposes a larger hook set (variables, classes, inventory, colour panel/swatches) — see the [Bricks plugin README](plugins/SLASHED-for-WP/integrations/bricks/README.md) and the in-admin **Hooks** tab. - -### Packaging - -```sh -npm run build:plugin # packages the plugin as a ZIP in dist/ -``` - ---- +The WordPress plugin (Bricks Builder + Gutenberg integrations) lives in its own +repository: [slashed-for-wp](https://github.com/codeslash-dev/slashed-for-wp). ## Development diff --git a/docs/bricks-template-workflow.md b/docs/bricks-template-workflow.md deleted file mode 100644 index 802389eb..00000000 --- a/docs/bricks-template-workflow.md +++ /dev/null @@ -1,290 +0,0 @@ -# Bricks template workflow - -A repeatable, step-by-step process for building and moving Bricks templates -that look identical on every site running the SLASHED plugin. - ---- - -## Why templates break - -Bricks stores colour values in the template JSON in one of three ways: - -| Form | How it gets into the template | Portable? | -|------|-------------------------------|-----------| -| `var(--sf-color-primary)` | Picked from a SLASHED palette in the colour picker | ✅ Yes — works on any site with SLASHED | -| `#4a90e2` | Typed by hand or picked from the colour wheel | ❌ Hard-coded — ignores brand tokens | -| `bricks-color-abc123` | Picked from Bricks' "Global Colors" | ❌ Site-specific ID — renders transparent on another site | - -The same problem applies to: - -- **Spacing** — `padding: 24px` instead of `var(--sf-space-6)` doesn't scale. -- **Typography** — `font-family: "Inter"` instead of `var(--sf-font-body)` ignores tokens. -- **SLASHED tokens** — values from the admin panel (`slashed_bricks_tokens`) do - **not** travel inside the Bricks JSON file; export them separately (see Phase 2). - ---- - -## Prerequisites - -On **both** the source and destination sites: - -- Bricks Builder ≥ 1.9.2 -- SLASHED Bricks plugin active -- A chosen CSS bundle (Essential / Optimal / Full) — the same or higher on the destination -- Fonts loaded via WordPress or Bricks (Google Fonts, self-hosted, …) - ---- - -## Phase 1 — Design the template (source site) - -### Rule 1: colours only from SLASHED palettes - -In the Bricks colour picker, expand the palette groups and use **only**: - -- `SLASHED · Primary` -- `SLASHED · Secondary` -- `SLASHED · Tertiary` -- `SLASHED · Action` -- `SLASHED · Neutral` -- `SLASHED · Base` -- `SLASHED · Status` -- `SLASHED · Semantic` - -**Never** use: - -- The colour wheel (writes a hex `#rrggbb`). -- Bricks' "Global Colors" section (writes a site-internal ID). -- The colour text field with a hand-typed value. - -> **How to check:** inspect the element in DevTools. The colour should read like -> `color: var(--sf-color-primary)`, not `color: #4a90e2`. - -### Rule 2: CSS variables in "Custom CSS" fields - -In an element's CSS tab, use tokens in every custom rule: - -```css -/* ✅ Good */ -.my-element { - background: var(--sf-color-primary); - padding: var(--sf-space-4) var(--sf-space-6); - font-size: var(--sf-text-base); - font-family: var(--sf-font-body); - border-radius: var(--sf-radius-md); - box-shadow: var(--sf-shadow-md); - transition: all var(--sf-duration-normal) var(--sf-ease-out); -} - -/* ❌ Bad */ -.my-element { - background: #4a90e2; - padding: 16px 24px; - font-size: 16px; - font-family: "Inter", sans-serif; -} -``` - -### Rule 3: `sf-*` classes for layout - -Use SLASHED layout classes instead of hand-written Flexbox/Grid values: - -| Instead of… | Use the class | -|-------------|---------------| -| `display: flex; gap: 16px; flex-wrap: wrap` | `sf-cluster` | -| `display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px` | `sf-grid` | -| `max-width: 1200px; margin: 0 auto` | `sf-center` | -| `display: flex; flex-direction: column; gap: 16px` | `sf-stack` | - -The classes are available in the Bricks class manager (category "SLASHED Layout"). - -### Rule 4: don't use Bricks Global Colors - -Bricks has its own "Global Colors" (a colour with a custom name, e.g. "Accent -Blue"). These carry an internal ID specific to the WordPress install. -**After importing the template on another site those IDs don't exist → the -colour is invisible.** Use the SLASHED Color Palette instead. - ---- - -## Phase 2 — Export (source site) - -### Step 1: export SLASHED tokens - -1. Open **WordPress Admin → SLASHED → Design Tokens**. -2. Go to the **Export / Import** tab. -3. Click **Download token file**. -4. Save `slashed-tokens-YYYY-MM-DD.json`. - -This file contains all your colour, typography, spacing, etc. overrides. - -### Step 2: export the Bricks template - -1. In Bricks, open the page/section you want to export. -2. **Bricks → Templates → Export**, or save it as a template. -3. Download the template JSON file. - -### Step 3: bundle both files together - -Suggested naming convention: - -```text -my-template/ - ├── bricks-template-hero.json # Bricks template - ├── slashed-tokens-2026-05-28.json # SLASHED tokens - └── README.txt # optional: font / bundle notes -``` - ---- - -## Phase 3 — Import (destination site) - -### Step 1: check prerequisites - -- [ ] SLASHED Bricks plugin installed and active -- [ ] CSS bundle the same or higher (Essential ≤ Optimal ≤ Full) -- [ ] Bricks ≥ 1.9.2 -- [ ] Fonts configured (Google Fonts, Adobe Fonts, self-hosted) - -### Step 2: import the SLASHED tokens - -1. **Admin → SLASHED → Design Tokens → Export / Import**. -2. Choose the `slashed-tokens-*.json` file. -3. Click **Import token file**. -4. Wait for "Imported N section(s) successfully". -5. **Reload the page** — the tokens are now active. - -### Step 3: import the Bricks template - -1. **Bricks → Templates → Import**. -2. Choose the `bricks-template-*.json` file. -3. Import. - -### Step 4: check in the Bricks editor - -Open the page in the Bricks editor. The canvas should render the template with -the correct colours. - ---- - -## Phase 4 — Verification - -### Visual checklist - -- [ ] Colours match the tokens (Primary, Secondary, etc.) -- [ ] Dark mode switches correctly (`data-brx-theme="dark"`) -- [ ] Spacing/gaps look proportional -- [ ] Fonts load correctly (not the serif/sans-serif fallback) -- [ ] Radii, shadows, and animations work - -### Technical check (DevTools) - -Open the inspector and run: - -```js -// In the browser console, on the page with the template: -getComputedStyle(document.documentElement).getPropertyValue('--sf-color-primary') -// Should return an OKLCH colour, not an empty string -``` - -If the value is empty → the SLASHED CSS isn't loaded (see Troubleshooting). - ---- - -## Troubleshooting - -### Colours are wrong / don't change - -**Cause:** elements hold hard-coded hex values instead of `var()`. - -**Diagnosis:** in DevTools → Inspector → Computed, look for values that read like -`#rrggbb` or `rgb(...)` instead of `var(--sf-color-*)`. - -**Fix:** re-style the element by picking a colour from a SLASHED palette, not the -colour wheel. - ---- - -### Everything is colourless / grey - -**Cause:** the SLASHED CSS bundle isn't loaded on the page. - -**Diagnosis:** - -```js -// In the console: -document.querySelector('link[href*="slashed"]') -// Should return a element. null = bundle not loaded. -``` - -**Fix:** - -1. Check the SLASHED Bricks plugin is active. -2. Admin → SLASHED → Bundle — confirm a bundle is set. -3. Check no other plugin is blocking the CSS. - ---- - -### Tokens are the defaults, not yours - -**Cause:** the token file wasn't imported, or the import failed. - -**Diagnosis:** Admin → SLASHED → Colors — the colours should show your overrides. -If everything is the default (purple), the file didn't reach the database. - -**Fix:** repeat the token import (Phase 3, Step 2). - ---- - -### `sf-*` classes don't work (no styling) - -**Cause:** the CSS bundle doesn't include those classes (e.g. Essential was used -instead of Optimal/Full). - -**Diagnosis:** - -```js -// Check whether the CSS rule exists: -[...document.styleSheets].flatMap(s => [...s.cssRules]).find(r => r.selectorText?.includes('sf-cluster')) -// undefined = the rule isn't in the bundle -``` - -**Fix:** Admin → SLASHED → Bundle → switch to Optimal or Full. - ---- - -### Dark mode doesn't work - -**Cause:** Bricks uses `data-brx-theme` instead of SLASHED's `data-theme`. The -plugin bridges the two attributes automatically, but only when the CSS is loaded. - -**Check** that the loaded CSS contains this rule: - -```css -[data-brx-theme="dark"] { color-scheme: dark; --sf-is-dark: 1 } -``` - -If it's missing → check the plugin version (requires the current release or newer). - ---- - -## Quick cheatsheet - -```text -Before designing: - ✅ Colour → from a SLASHED palette group - ✅ Custom CSS → var(--sf-*) - ✅ Layout → sf-* class - ❌ Colour → hex from the colour wheel - ❌ Custom CSS → px / hex literal - ❌ Colour → Bricks Global Colors - -Export (from this site): - 1. Admin → SLASHED → Design Tokens → Export / Import → Download token file - 2. Bricks → Export template - -Import (on the new site): - 1. Admin → SLASHED → Design Tokens → Export / Import → Import token file - 2. Bricks → Import template - 3. Reload the page - 4. Verify in the editor -``` diff --git a/docs/rebemer.md b/docs/rebemer.md deleted file mode 100644 index c44f0dd5..00000000 --- a/docs/rebemer.md +++ /dev/null @@ -1,803 +0,0 @@ -# reBEMer — design doc - -> A subtree-scoped BEM class manager for the Bricks Builder structure -> panel, shipped as part of **SLASHED for Bricks**. - -Status: **v1 partial implementation in progress** — see §0 Implementation status. -License: MIT, same as the host plugin. - ---- - -## 0. Implementation status - -Section bodies describe the full v1 design; this table tracks what has shipped -in code. Spec-only rows (§7, §8, §10, §12, §13) describe accurate design that -lands in follow-up PRs. - -| Capability | Status | Where | -|---|---|---| -| Badge injection in structure panel (§3.1) | ✅ shipped | `editor-app/src/main.js` + `BemBadge.svelte` | -| Panel mount + close (§3.2) | ✅ shipped | `editor-app/src/main.js` + `BemPanel.svelte` | -| Add / Rename / Replace / Add Modifier modes (§3.3) | ✅ shipped | `editor-app/src/lib/apply.js` | -| Class-family picker in dedicated Rename & Replace (§3.3) | ✅ shipped | `Row.svelte` family ` elements. - · .is-success → general component feedback, not tied to a form field. - Use on containers, banners, step indicators. - They share the same visual treatment because both represent a "positive" - state, but consuming them interchangeably is a semantic error. */ - .is-valid, - .is-success { - --sf-field-border-color: var(--sf-color-success); - --sf-field-text-color: var(--sf-color-success-strong); - } - - /* NOTE: .is-invalid and .is-error share identical CSS values by design. - · .is-invalid → form-field validation failure (maps to aria-invalid="true"). - · .is-error → general component error state (failed save, network error). */ - .is-invalid, - .is-error { - --sf-field-border-color: var(--sf-color-error); - --sf-field-text-color: var(--sf-color-error-strong); - } - - .is-warning { - --sf-field-border-color: var(--sf-color-warning); - --sf-field-text-color: var(--sf-color-warning-strong); - } - - .is-info { - --sf-field-border-color: var(--sf-color-info); - --sf-field-text-color: var(--sf-color-info-strong); - } - - .is-danger { - --sf-field-border-color: var(--sf-color-danger); - --sf-field-text-color: var(--sf-color-danger-strong); - } - - /* ============================================================ - POSITION / STICKINESS - ============================================================ */ - - .is-sticky { - position: sticky; - inset-block-start: var(--sf-sticky-offset, 0); /* logical: respects writing-mode */ - z-index: var(--sf-z-low); - } - - .is-pinned { - position: sticky; - inset-block-start: 0; - z-index: var(--sf-z-mid); - } - - .is-fixed { - /* Intentionally bare — position only. Consumers must supply placement - (inset, inset-block-start, etc.) and z-index via a modifier or inline style. - Example:
*/ - position: fixed; - } - - .is-fullscreen { - /* App-managed fullscreen overlay. For the native Fullscreen API, - prefer the `:fullscreen` pseudo-class; this is the JS-toggled - equivalent for elements not entered via that API. */ - position: fixed; - inset: 0; - z-index: var(--sf-z-max); - } - - .is-resizable { - resize: both; - overflow: auto; - min-inline-size: var(--sf-space-xl, 3.55rem); /* prevent collapsing to zero on drag */ - min-block-size: var(--sf-space-xl, 3.55rem); - } - - /* ============================================================ - OVERFLOW / CLIPPING - ============================================================ */ - - .is-clipped { - overflow: hidden !important; - } - - .is-scrollable { - overflow: auto; - overscroll-behavior: contain; - } - - .is-truncated { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - /* ============================================================ - DRAG & DROP - ============================================================ */ - - .is-dragging { - opacity: var(--sf-opacity-50); - cursor: grabbing; - } - - .is-drop-target { - outline: var(--sf-focus-ring-width) dashed var(--sf-color-action); - outline-offset: var(--sf-focus-ring-offset); - } - - .is-draggable { - cursor: grab; - } - - /* ============================================================ - OVERLAY / BACKDROP - ============================================================ */ - - .is-overlay { - position: absolute; - inset: 0; - } - - /* ============================================================ - FOCUS / INTERACTION MODIFIERS - ============================================================ */ - - .is-clickable { - cursor: pointer; - } - - .is-unselectable { - user-select: none; - } - - .is-focused { - /* Programmatic / JS-driven focus indicator, independent of - :focus-visible. Normal weight so consumers can restyle. */ - outline: var(--sf-focus-ring-width) var(--sf-focus-ring-style, solid) var(--sf-focus-ring-color); - outline-offset: var(--sf-focus-ring-offset); - } - - /* ============================================================ - EMPTY STATE - ============================================================ */ - - /* NOTE: :empty only matches elements with NO child nodes — including no - whitespace. Server-rendered HTML often includes space between tags, which - prevents the match. When toggling via JS, call element.textContent = "" - before adding this class (or use element.innerHTML = "" to clear all nodes - including whitespace text nodes). */ - .is-empty:empty { - display: none; - } - - /* ============================================================ - KEYFRAMES (used by states above) - - Spinner and shimmer keyframes live in core/motion.css alongside - the rest of the framework's @keyframes. They are referenced from - here by name (sf-spin, sf-shimmer) and are inert until applied. - If you load core/states.css without core/motion.css, define them - in your own CSS or copy them from motion.css. - ============================================================ */ - -} - -/* ─── core/motion.css ─── */ -/* SLASHED — core/motion.css - @layer slashed.motion - - Extends the base interactive transition (defined in base.css) - with a broader property set and adds scroll/view-transition - behaviour. Optional and independently loadable. - - Reduced-motion handling is intentionally NOT duplicated here: - accessibility.css (a higher cascade layer, shipped in the - 'essential' bundle) neutralises animation/transition via - property-level !important. The no-preference wrapper below is - a defensive opt-in stance — animations apply only when the - user has not requested their absence — and remains correct - even if motion.css is served without accessibility.css. - ─────────────────────────────────────────────────── */ - -@layer slashed.motion { - - @media (prefers-reduced-motion: no-preference) { - - html { - scroll-behavior: smooth; - } - - /* Keyboard navigation must not be hijacked by smooth scroll. */ - html:focus-within { - scroll-behavior: auto; - } - - /* Broadens the base.css link transition to all interactive - elements and the full set of animatable UI properties. */ - a, button, input, select, textarea, summary { - transition-property: - color, background-color, border-color, text-decoration-color, - fill, stroke, opacity, box-shadow, transform, filter, - backdrop-filter; - transition-duration: var(--sf-duration-fast); - transition-timing-function: var(--sf-ease-out); - } - - @supports (view-transition-name: none) { - ::view-transition-old(root), - ::view-transition-new(root) { - animation-duration: var(--sf-duration-normal); - animation-timing-function: var(--sf-ease-out); - } - } - - /* Animation presets — opt-in classes. Scoped to no-preference so - reduced-motion users never trigger them (accessibility.css also - neutralises animation-duration as a hard guarantee). These are - DECORATIVE: under reduced motion they simply don't run, so never - rely on one (e.g. .sf-fade-out) as the sole way to show/hide - content — drive real state with hidden/display/JS instead. */ - .sf-fade-in { animation: var(--sf-animation-fade-in); } - .sf-fade-out { animation: var(--sf-animation-fade-out); } - .sf-slide-in-up { animation: var(--sf-animation-slide-in-up); } - .sf-slide-in-down { animation: var(--sf-animation-slide-in-down); } - .sf-slide-in-left { animation: var(--sf-animation-slide-in-left); } - .sf-slide-in-right { animation: var(--sf-animation-slide-in-right); } - .sf-scale-up { animation: var(--sf-animation-scale-up); } - .sf-scale-down { animation: var(--sf-animation-scale-down); } - - /* Scroll-driven entrance effects — element animates into view - as it enters the viewport. Uses CSS scroll-driven animations - via animation-timeline: view(). Range tokens - (--sf-scroll-timeline-range-{start,end}, declared in - core/tokens.css) control where the animation starts and ends - along the scroll progress. - - Example: -
- - Reduced-motion: gated by the no-preference wrapper above, so - these are inert when the user opts out. The core animation - (translate/opacity) still composes correctly with reduced-motion - neutralisation in accessibility.css if a consumer accidentally - writes them outside the gate. - - Browser support — animation-timeline ships in Chrome/Edge 115+. - Firefox keeps it behind a flag (the scroll-driven-animations - preference) and Safari has not shipped it (tracked at WebKit - bug 264057); on those engines the @supports block below is - skipped, so the rule falls back to a one-shot time-driven - animation playing once at --sf-duration-slow. - animation-duration is set unconditionally because without it - a non-supporting engine would default to 0s and snap to the - end state instantly — there'd be no visible entrance at all. - Where animation-timeline is supported the timeline drives - progress and the duration is ignored. */ - .sf-entrance--fade, - .sf-entrance--fade-up, - .sf-entrance--fade-down, - .sf-entrance--fade-left, - .sf-entrance--fade-right, - .sf-entrance--scale-up { - animation-duration: var(--sf-duration-slow); - animation-timing-function: var(--sf-ease-out); - animation-fill-mode: both; - } - @supports (animation-timeline: view()) { - .sf-entrance--fade, - .sf-entrance--fade-up, - .sf-entrance--fade-down, - .sf-entrance--fade-left, - .sf-entrance--fade-right, - .sf-entrance--scale-up { - animation-timeline: view(); - animation-range: var(--sf-scroll-timeline-range-start, entry 0%) - var(--sf-scroll-timeline-range-end, cover 30%); - } - } - .sf-entrance--fade { animation-name: sf-fade-in; } - .sf-entrance--fade-up { animation-name: sf-slide-in-up; } - .sf-entrance--fade-down { animation-name: sf-slide-in-down; } - .sf-entrance--fade-left { animation-name: sf-slide-in-left; } - .sf-entrance--fade-right { animation-name: sf-slide-in-right; } - .sf-entrance--scale-up { animation-name: sf-scale-up; } - - } - - /* Keyframes — inert until referenced, so they live outside the - no-preference wrapper (consumers may reference them at will). - Spinner and shimmer are used by .is-loading / .is-skeleton in - core/states.css; the rest power the .sf-* animation classes - above. */ - @keyframes sf-fade-in { from { opacity: 0; } to { opacity: 1; } } - @keyframes sf-fade-out { from { opacity: 1; } to { opacity: 0; } } - @keyframes sf-slide-in-up { from { opacity: 0; translate: 0 1rem; } to { opacity: 1; translate: 0 0; } } - @keyframes sf-slide-in-down { from { opacity: 0; translate: 0 -1rem; } to { opacity: 1; translate: 0 0; } } - @keyframes sf-slide-in-left { from { opacity: 0; translate: 1rem 0; } to { opacity: 1; translate: 0 0; } } - @keyframes sf-slide-in-right { from { opacity: 0; translate: -1rem 0; } to { opacity: 1; translate: 0 0; } } - @keyframes sf-scale-up { from { opacity: 0; scale: 0.92; } to { opacity: 1; scale: 1; } } - @keyframes sf-scale-down { from { opacity: 1; scale: 1; } to { opacity: 0; scale: 0.92; } } - - @keyframes sf-spin { to { rotate: 360deg; } } - @keyframes sf-shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } - - /* Notification ping (expanding fading ring), cursor blink, gentle float. - Keyframes only — apply via your own BEM/component rules or the - --sf-animation-ping / -blink / -float presets. No utility classes - ship for these (by design). */ - @keyframes sf-ping { 75%, 100% { scale: 2; opacity: 0; } } - @keyframes sf-blink { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } } - @keyframes sf-float { 0%, 100% { translate: 0 0; } 50% { translate: 0 -0.5rem; } } - - /* @property color interpolation — animates --sf-color-primary-light smoothly - because it is registered with syntax:"" (see @property in tokens.css). - Without that registration the browser cannot interpolate between color values - and the animation will snap at 50% with no intermediate frames. - Gated behind @supports so unsupporting engines simply skip it. */ - @supports (@property --x { syntax: ""; inherits: false; initial-value: white; }) { - @keyframes sf-color-pulse { - 0% { --sf-color-primary-light: var(--sf-color-primary-light); } - 50% { --sf-color-primary-light: oklch(from var(--sf-color-primary-light) calc(l + 0.25) c h); } - 100% { --sf-color-primary-light: var(--sf-color-primary-light); } - } - - @media (prefers-reduced-motion: no-preference) { - .sf-color-pulse { - animation: var(--sf-animation-color-pulse); - } - } - } - - /* @starting-style — entry animations for elements appearing in the - top layer (dialog, [popover]) or via display:none → block. SLASHED - ships no global @starting-style rules because the from-state is - element/component specific. Consumers opt in per component, e.g.: - dialog[open] { opacity: 1; @starting-style { opacity: 0; } } - Tracked for the future components layer. */ - -} - -/* ─── core/accessibility.css ─── */ -/* SLASHED — core/accessibility.css - @layer slashed.accessibility - - Critical a11y rules use !important to survive unlayered CSS and - third-party resets (unlayered normal beats any layered normal). - Protection is SELECTIVE — only rules whose override is a genuine - user-facing barrier are hardened. Rules that are legitimately - customised (touch targets, skip-link position, contrast boost, - cursor) are left normal on purpose. - - Audit note: reset.css/base.css contain no !important on outline, - animation-* or transition-*, so the reversed important-layer order - cannot let a lower layer defeat these rules. - ─────────────────────────────────────────────────── */ - -@layer slashed.accessibility { - - /* ---------------------------------------------------------- - Focus — keyboard visibility is WCAG 2.4.7, hard barrier. - Ring definition lives here (not base.css) so all focus - logic is in one high-priority layer. Hardened. - ---------------------------------------------------------- */ - :focus:not(:focus-visible) { - outline: none; - } - - :focus-visible { - outline: var(--sf-focus-ring-width) var(--sf-focus-ring-style, solid) var(--sf-focus-ring-color) !important; - outline-offset: var(--sf-focus-ring-offset) !important; - } - - /* ---------------------------------------------------------- - Focus-shadow opt-in — switches the focus indicator from the - default `outline` ring to a `box-shadow` ring (the composite - --sf-focus-ring-shadow token). Useful when the outline clips - awkwardly on rounded corners or overflow:hidden containers, - where box-shadow follows border-radius. Opt-in per element: - - - The base :focus-visible outline is hardened with !important - (WCAG 2.4.7), so this opt-in must also use !important to take - over — it replaces one always-visible ring with another, so - the keyboard-focus guarantee is preserved, not weakened. - outline:none here only removes the now-redundant outline ring - in favour of the box-shadow ring on the same element. - ---------------------------------------------------------- */ - .sf-focus-shadow:focus-visible { - outline: none !important; - box-shadow: var(--sf-focus-ring-shadow) !important; - } - /* Forced-colors (Windows High Contrast) drops box-shadow entirely, which - would leave this element with NO focus indicator. Restore a system-colour - outline ring there so the WCAG 2.4.7 guarantee holds in that mode too. */ - @media (forced-colors: active) { - .sf-focus-shadow:focus-visible { - outline: var(--sf-focus-ring-width) solid !important; - outline-offset: var(--sf-focus-ring-offset) !important; - box-shadow: none !important; - } - } - - /* ---------------------------------------------------------- - Reduced motion. Token override helps components that read - duration tokens in other contexts; the property-level - !important block is the real guarantee and works - independently of tokens (and of unlayered token overrides). - ---------------------------------------------------------- */ - @media (prefers-reduced-motion: reduce) { - :root { - --sf-duration-instant: 0.01ms; - --sf-duration-fast: 0.01ms; - --sf-duration-normal: 0.01ms; - --sf-duration-slow: 0.01ms; - --sf-duration-slower: 0.01ms; - } - - *, - *::before, - *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - scroll-behavior: auto !important; - } - } - - /* Manual motion opt-out — same suppression as the OS preference above, - but consumer-toggled (e.g. a site-level "reduce motion" switch). Works - regardless of the OS setting. */ - .no-motion, - .no-motion *, - .no-motion *::before, - .no-motion *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - scroll-behavior: auto !important; - } - - /* ---------------------------------------------------------- - High contrast — enhancement, not a barrier. Left normal: - if overridden the focus ring still exists, just thinner. - ---------------------------------------------------------- */ - @media (prefers-contrast: more) { - :root { - --sf-focus-ring-width: 3px; - /* Push derived reading-text colours toward the extremes (see the - --sf-contrast-bias knob in tokens.css). */ - --sf-contrast-bias: 0.06; - } - - hr { - border-block-start-width: var(--sf-border-width-2); - } - } - - /* ---------------------------------------------------------- - Reduced transparency. - ---------------------------------------------------------- */ - @media (prefers-reduced-transparency: reduce) { - ::backdrop { - background-color: var(--sf-color-neutral); - opacity: 1; - } - } - - /* ---------------------------------------------------------- - Reduced data — Save-Data / metered-connection preference. - Enhancement, not a barrier (left normal, no !important): we - only neutralise the framework's paint-heaviest *decorative* - output via its own master knobs, so layout and legibility are - untouched. --sf-shadow-strength: 0 zeroes every box/text/drop - shadow and the glow in one step (they all derive their alpha - from it); the blur tokens collapse so backdrop/filter blurs - stop compositing expensive layers. Progressive enhancement — - engines without prefers-reduced-data simply ignore this block. - ---------------------------------------------------------- */ - @media (prefers-reduced-data: reduce) { - :root { - --sf-shadow-strength: 0; - --sf-blur-xs: 0px; - --sf-blur-s: 0px; - --sf-blur-m: 0px; - --sf-blur-l: 0px; - --sf-blur-xl: 0px; - } - } - - /* ---------------------------------------------------------- - Minimum touch targets — WCAG 2.5.5 has documented - exceptions; dense desktop-first UIs legitimately go below - 44px with other hit-area strategies. Left normal ON PURPOSE - so it stays a recommendation, not an unbreakable rule. - ---------------------------------------------------------- */ - @media (pointer: coarse) { - button, - input[type="button"], - input[type="submit"], - input[type="reset"], - select, - summary { - min-block-size: var(--sf-touch-target); - min-inline-size: var(--sf-touch-target); - } - /* Checkboxes and radios are replaced elements — no display change needed. - The UA renders them as a fixed small square; min-size expands the - hit area without touching layout. */ - input[type="checkbox"], - input[type="radio"] { - min-block-size: var(--sf-touch-target); - min-inline-size: var(--sf-touch-target); - } - } - - /* ---------------------------------------------------------- - Disabled states — cosmetic, left normal. - ---------------------------------------------------------- */ - [disabled], - [aria-disabled="true"] { - cursor: not-allowed; - } - - /* ---------------------------------------------------------- - Screen-reader-only. Atomic contract: a partial override - either exposes hidden content visually or hides intended - content. All-or-nothing, so the whole set is hardened. - ---------------------------------------------------------- */ - .sr-only, - .sr-only-focusable:not(:focus, :focus-within) { - position: absolute !important; - width: 1px !important; - height: 1px !important; - padding: 0 !important; - margin: -1px !important; - overflow: hidden !important; /* fallback for Safari <16 */ - overflow: clip !important; - clip-path: inset(50%) !important; - white-space: nowrap !important; - border: 0 !important; - } - - /* ---------------------------------------------------------- - Skip link — appearance belongs to the integrator's design. - Left normal so position/colour can be themed freely. - ---------------------------------------------------------- */ - .skip-link { - position: absolute; - inset-block-start: -100%; - inset-inline-start: var(--sf-space-m); - z-index: var(--sf-z-max); - padding: var(--sf-space-s) var(--sf-space-m); - background: var(--sf-color-bg); - color: var(--sf-color-text); - border: var(--sf-border-width-1) solid var(--sf-color-border); - border-radius: var(--sf-radius-m); - transition: inset-block-start var(--sf-duration-fast) var(--sf-ease-out); - } - .skip-link:focus { - inset-block-start: var(--sf-space-m); - } - - - /* ---------------------------------------------------------- - Focus-parent — an a11y *pattern*, not a state. Lives in the - accessibility layer (was previously in states.css; moved in - v0.3.0) so that all focus-related a11y rules share one home - and survive third-party resets at the same priority as the - focus-visible ring above. Cannot be hardened with !important - because the outline shorthand is consumer-customisable. - Canonical name: .sf-focus-parent (prefixed). - ---------------------------------------------------------- */ - /* Uses --sf-focus-ring-color (same token as :focus-visible) so the ring - is visually consistent whether focus lands on the parent or a child. */ - .sf-focus-parent:focus-within { - outline: var(--sf-focus-ring-width) var(--sf-focus-ring-style, solid) var(--sf-focus-ring-color); - outline-offset: var(--sf-focus-ring-offset); - } - - /* ---------------------------------------------------------- - Clickable-parent — the card-with-link a11y pattern. - Applied to the card container. Two modes: - - Automatic (single primary link): -
-

Title

-

Description

- -
- - Explicit (multiple links — add [data-overlay-link] to the primary): -
-

Title

-

Description

- Category ← lifted automatically - ← lifted automatically -
- - Mechanic: the link's ::after fills the card via position:absolute; - inset:0. It is contained by .sf-clickable-parent (position:relative) - only if the link itself has no positioned ancestor between it and the - card — so no element containing the overlay link should be given - position:relative. Flow children are therefore intentionally left at - position:static; they sit below the overlay in stacking order. - - Trade-off: card text is NOT user-selectable by mouse. A JavaScript - click-delegate could restore selectability while preserving - full-card navigation. - - Key features: - • [data-overlay-link] explicit mode — reliable multi-link cards - • `:focus-within` card ring — always included, no extra recipe - • --sf-clickable-overlay-z — escape parent stacking contexts - • Wider interactive-element exceptions (summary, video, audio…) - - Tune --sf-clickable-overlay-z to escape a parent stacking context. - [data-no-overlay] opts any descendant above the overlay. - ---------------------------------------------------------- */ - .sf-clickable-parent { - --sf-clickable-overlay-z: 1; - position: relative; - cursor: pointer; - } - - /* Overlay: the link's ::after fills the containing card. - Automatic mode — every :any-link gets the overlay; works reliably - for single-link cards. Secondary links that also receive ::after are - lifted to position:relative (interactive rule below), which scopes - their ::after to themselves and keeps them independently clickable. - Explicit mode — only [data-overlay-link] fills the card. */ - .sf-clickable-parent:not(:has([data-overlay-link])) a:where(:any-link)::after, - .sf-clickable-parent [data-overlay-link]::after { - content: ""; - position: absolute; - inset: 0; - z-index: var(--sf-clickable-overlay-z); - pointer-events: auto; - } - - /* Interactive descendants beat the overlay and stay functional. - Explicit mode: every non-designated :any-link is lifted — robust at - any nesting depth, no :first-of-type fragility. - Automatic mode: links that are :not(:first-of-type) among siblings - are lifted. Sibling-scoped — use explicit mode for cards where - secondary links live at different nesting depths. - [data-no-overlay] is the per-element consumer escape hatch. */ - .sf-clickable-parent :is( - button, input, textarea, select, - summary, video, audio, - [role="button"], [data-no-overlay] - ), - .sf-clickable-parent:has([data-overlay-link]) a:where(:any-link):not([data-overlay-link]), - .sf-clickable-parent:not(:has([data-overlay-link])) a:where(:any-link):not(:first-of-type) { - position: relative; - z-index: calc(var(--sf-clickable-overlay-z) + 1); - } - - /* Card-level focus ring — keyboard users see which card is active. - The focus ring is always included without requiring a separate - recipe. Suppress with outline:none - if the card design provides its own focus indicator. */ - .sf-clickable-parent:focus-within { - outline: var(--sf-focus-ring-width) var(--sf-focus-ring-style, solid) var(--sf-focus-ring-color); - outline-offset: var(--sf-focus-ring-offset); - } - - /* ---------------------------------------------------------- - Forced colors (Windows High Contrast). Outline-based focus - already survives; we only (a) adopt the system Highlight - colour for the ring and (b) drop decorative shadows that - forced-colors would otherwise flatten into solid slabs. - Shadow removal is zero-specificity so any rule can win it back. - ---------------------------------------------------------- */ - @media (forced-colors: active) { - :root { - --sf-focus-ring-color: Highlight; - } - - :focus-visible { - outline-color: Highlight !important; - } - - :where(*) { - box-shadow: none; - text-shadow: none; - } - - /* Form controls otherwise lose their visible boundary in forced-colors - (their token-driven border colour is replaced by the system canvas). - Re-assert a system-coloured border so fields stay distinguishable. */ - input, - select, - textarea { - border: var(--sf-border-width-1) solid; - } - } - -} - -/* ─── core/print.css ─── */ -/* SLASHED — core/print.css - @layer slashed.print - - Print rules and @page definition. Print tokens (--sf-print-*) - are declared in core/tokens.css. - - Color contract (Phase 3): - authored colour is preserved by default. Browsers apply their own - ink-saving heuristics via the default `print-color-adjust: economy`, - so semantic colours (badges, status pills, , syntax highlighting) - reach paper intact. Two opt-in classes give consumers control: - - .print-color-exact → force every descendant to print colours - verbatim (for colour-coded data that loses - meaning when the browser flattens it). - .print-no-color → force everything to ink-on-paper: - transparent backgrounds + system text colour. - The legacy blanket reset, now opt-in. - - `!important` is reserved for selectors whose semantics require - defeating consumer-authored CSS: - - `.no-print` and the hide-list (nav, aside, button, …) — these - are content negative-space and must vanish from print regardless - of authored display rules. - - `details > summary` — disclosure widget carries no print value. - - The two opt-in colour classes above — when the consumer marks a - region with them they are explicitly asking to override authored - colour, so `!important` is the contract. - ─────────────────────────────────────────────────── */ - -@layer slashed.print { - - /* Screen default: print-only content is hidden until the page is printed. */ - .print-only { - display: none; - } - - @media print { - - @page { - /* var() is not valid inside @page — hardcode values here. - Consumers who need a different page size must override @page directly. */ - size: a4 portrait; - margin: 2cm; - } - - body { - font-size: var(--sf-print-base-size, 11pt); - } - - /* Decorative shadows are paper-noise. No `!important` — an authored - shadow that explicitly survives into print is the consumer's call. */ - *, - *::before, - *::after { - box-shadow: none; - text-shadow: none; - } - - /* Expand URLs after link text — exclude fragment, javascript:, mailto:, - tel: links (already readable or noisy on paper) and .sf-link-external - (which has its own ::after marker). */ - a[href]:not([href^="#"], [href^="javascript:" i], [href^="mailto:" i], [href^="tel:" i], .sf-link-external)::after { - content: " (" attr(href) ")"; - font-size: 90%; - word-break: break-all; - } - - abbr[title]::after { - content: " (" attr(title) ")"; - } - - img, svg, canvas, figure, table, pre, blockquote { - break-inside: avoid; - /* video is excluded — it is already hidden in print (see hide list below) */ - } - - thead { - display: table-header-group; - } - - tr { - break-inside: avoid; - } - - h1, h2, h3, h4, h5, h6 { - break-after: avoid; - /* orphans/widows on single-line headings have no effect — set on p below */ - } - - p { - orphans: 3; - widows: 3; - } - - pre, blockquote { - border: var(--sf-border-width-1) solid var(--sf-color-border--strong); - padding: var(--sf-space-s); - } - - /* Hide non-essential interactive and decorative elements. - `!important` is justified: these elements are content negative-space - in print, and consumer-authored display rules must not bring them - back accidentally. */ - nav, aside, button, input, select, textarea, - audio, video, dialog, [popover], .no-print { - display: none !important; - } - - /* Reveal print-only content (overrides the screen default above). */ - .print-only { - display: revert !important; - } - - /* Preserve
content regardless of open state. Only - `summary` keeps `!important` — the disclosure chevron carries no - print value and the consumer should not be able to bring it back - inadvertently. The other two rules win against the UA stylesheet - on layered-author > UA precedence alone. */ - details { - display: block; - } - details > summary { - display: none !important; - } - details:not([open]) > :not(summary) { - display: block; - } - - /* ── Opt-in colour treatment ─────────────────────────────────── */ - - /* Force colour-faithful rendering. Use on regions where a tinted - background or coloured text encodes meaning (status pills, - severity callouts, syntax highlighting, charts). */ - .print-color-exact, - .print-color-exact * { - print-color-adjust: exact !important; - -webkit-print-color-adjust: exact !important; - } - - /* Opt back into the legacy blanket reset: transparent backgrounds - and system text colour everywhere inside the marked subtree. - Use on regions where ink-saving is the contract (corporate - boilerplate forms, append-to-existing-document templates). */ - .print-no-color, - .print-no-color *, - .print-no-color *::before, - .print-no-color *::after { - background: transparent !important; - color: CanvasText !important; - } - - } - -} diff --git a/plugins/SLASHED-for-WP/dist/slashed.full.css b/plugins/SLASHED-for-WP/dist/slashed.full.css deleted file mode 100644 index 26160d82..00000000 --- a/plugins/SLASHED-for-WP/dist/slashed.full.css +++ /dev/null @@ -1,5873 +0,0 @@ -/* SLASHED v0.5.21 — slashed.full.css */ - -/* ─── core/layers.css ─── */ -/* SLASHED — core/layers.css - CSS cascade layer order. Load this file first, always. - ─────────────────────────────────────────────────── */ - -@layer - slashed.tokens, - slashed.reset, - slashed.base, - slashed.forms, - slashed.layout, - slashed.components, - slashed.macros, - slashed.utilities, - slashed.states, - slashed.themes, - slashed.motion, - slashed.accessibility, - slashed.print, - slashed.legacy, - slashed.overrides; - -/* ─── core/tokens.color-fallbacks.css ─── */ -/* SLASHED — core/tokens.color-fallbacks.css (@layer slashed.tokens) - HSL-based fallbacks for browsers without light-dark() / oklch(from…) support. - Light mode only — dark mode is handled by modern CSS exclusively. - Customise by overriding the --sf-*-h / --sf-*-s / --sf-*-l channel variables; - the entire derived token tree recomputes automatically, no build step required. */ - -@layer slashed.tokens { - :root { - /* ── Source channels — override to apply brand colours ──────────────── - Values are approximate HSL equivalents of the OKLCH source tokens. */ - - --sf-primary-h: 226; --sf-primary-s: 99%; --sf-primary-l: 47%; - --sf-secondary-h: 221; --sf-secondary-s: 45%; --sf-secondary-l: 12%; - --sf-tertiary-h: 268; --sf-tertiary-s: 84%; --sf-tertiary-l: 39%; - --sf-action-h: 200; --sf-action-s: 100%; --sf-action-l: 32%; - --sf-neutral-h: 217; --sf-neutral-s: 11%; --sf-neutral-l: 42%; - --sf-base-h: 211; --sf-base-s: 27%; --sf-base-l: 95%; - --sf-success-h: 134; --sf-success-s: 100%; --sf-success-l: 24%; - --sf-warning-h: 42; --sf-warning-s: 100%; --sf-warning-l: 45%; - --sf-error-h: 352; --sf-error-s: 93%; --sf-error-l: 38%; - --sf-info-h: 200; --sf-info-s: 100%; --sf-info-l: 30%; - --sf-danger-h: 340; --sf-danger-s: 100%; --sf-danger-l: 35%; - - /* ── Brand resolved ───────────────────────────────────────────────────── */ - - --sf-color-primary: hsl(var(--sf-primary-h) var(--sf-primary-s) var(--sf-primary-l)); - --sf-color-secondary: hsl(var(--sf-secondary-h) var(--sf-secondary-s) var(--sf-secondary-l)); - --sf-color-tertiary: hsl(var(--sf-tertiary-h) var(--sf-tertiary-s) var(--sf-tertiary-l)); - --sf-color-action: hsl(var(--sf-action-h) var(--sf-action-s) var(--sf-action-l)); - --sf-color-neutral: hsl(var(--sf-neutral-h) var(--sf-neutral-s) var(--sf-neutral-l)); - --sf-color-base: hsl(var(--sf-base-h) var(--sf-base-s) var(--sf-base-l)); - - /* ── Status resolved ──────────────────────────────────────────────────── */ - - --sf-color-success: hsl(var(--sf-success-h) var(--sf-success-s) var(--sf-success-l)); - --sf-color-warning: hsl(var(--sf-warning-h) var(--sf-warning-s) var(--sf-warning-l)); - --sf-color-error: hsl(var(--sf-error-h) var(--sf-error-s) var(--sf-error-l)); - --sf-color-info: hsl(var(--sf-info-h) var(--sf-info-s) var(--sf-info-l)); - --sf-color-danger: hsl(var(--sf-danger-h) var(--sf-danger-s) var(--sf-danger-l)); - - /* ── Status strong (darker variant) ──────────────────────────────────── */ - - --sf-color-success-strong: hsl(var(--sf-success-h) var(--sf-success-s) calc(var(--sf-success-l) - 8%)); - --sf-color-warning-strong: hsl(var(--sf-warning-h) var(--sf-warning-s) calc(var(--sf-warning-l) - 15%)); - --sf-color-error-strong: hsl(var(--sf-error-h) var(--sf-error-s) calc(var(--sf-error-l) - 6%)); - --sf-color-info-strong: hsl(var(--sf-info-h) var(--sf-info-s) calc(var(--sf-info-l) - 8%)); - --sf-color-danger-strong: hsl(var(--sf-danger-h) var(--sf-danger-s) calc(var(--sf-danger-l) - 8%)); - - /* ── Surfaces (base-derived) ──────────────────────────────────────────── */ - - --sf-color-bg: hsl(var(--sf-base-h) var(--sf-base-s) calc(var(--sf-base-l) + 2%)); - --sf-color-inset: hsl(var(--sf-base-h) var(--sf-base-s) calc(var(--sf-base-l) - 3%)); - --sf-color-raised: hsl(var(--sf-base-h) var(--sf-base-s) calc(var(--sf-base-l) + 4%)); - /* Inverse surface — dark in light-mode fallback (modern flips lightness - via oklch). Overlay — the page bg at 0.9 alpha for scrim/backdrop use. */ - --sf-color-inverse: hsl(var(--sf-neutral-h) 18% 14%); - --sf-color-overlay: hsl(var(--sf-base-h) var(--sf-base-s) calc(var(--sf-base-l) + 2%) / 0.9); - - /* ── Text (neutral-derived) ───────────────────────────────────────────── */ - - --sf-color-text: hsl(var(--sf-neutral-h) 15% 12%); - --sf-color-text--secondary: hsl(var(--sf-neutral-h) 10% 35%); - --sf-color-heading: hsl(var(--sf-neutral-h) 20% 8%); - - /* ── Text-on-color ──────────────────────────────────────────────────────── - Modern CSS auto-picks black/white per background luminance via the - sign() formula in tokens.css. That formula needs oklch relative color, - so on this fallback tier we hardcode the choice from each channel's - known lightness: white on the dark brand/status fills, dark text on the - light ones (base/surface) and on warning (perceptually light yellow). - Without these, every ungated `.sf-surface--*` rule would keep its - background but inherit unreadable body text. */ - --sf-color-text--on-primary: hsl(0 0% 100%); - --sf-color-text--on-secondary: hsl(0 0% 100%); - --sf-color-text--on-tertiary: hsl(0 0% 100%); - --sf-color-text--on-action: hsl(0 0% 100%); - --sf-color-text--on-neutral: hsl(0 0% 100%); - --sf-color-text--on-base: hsl(var(--sf-neutral-h) 15% 12%); - --sf-color-text--on-surface: hsl(var(--sf-neutral-h) 15% 12%); - --sf-color-text--on-success: hsl(0 0% 100%); - --sf-color-text--on-warning: hsl(var(--sf-neutral-h) 15% 12%); - --sf-color-text--on-error: hsl(0 0% 100%); - --sf-color-text--on-info: hsl(0 0% 100%); - --sf-color-text--on-danger: hsl(0 0% 100%); - --sf-color-text--on-inverse: hsl(0 0% 100%); - - /* ── Borders (neutral-derived) ────────────────────────────────────────── */ - - --sf-color-border: hsl(var(--sf-neutral-h) 8% 85%); - --sf-color-border--subtle: hsl(var(--sf-neutral-h) 5% 91%); - --sf-color-border--strong: hsl(var(--sf-neutral-h) 14% 70%); - - /* ── Links (action-derived) ───────────────────────────────────────────── */ - - --sf-color-link: hsl(var(--sf-action-h) var(--sf-action-s) calc(var(--sf-action-l) - 5%)); - - /* ── Selection ────────────────────────────────────────────────────────── */ - - --sf-color-selection-bg: hsl(var(--sf-action-h) var(--sf-action-s) var(--sf-action-l) / 0.28); - - /* ── Shadows (neutral-derived) ────────────────────────────────────────── */ - - --sf-shadow-none: none; - --sf-shadow-xs: 0 1px 2px 0 - hsl(var(--sf-neutral-h) 20% 10% / 0.06); - --sf-shadow-s: 0 1px 3px 0 hsl(var(--sf-neutral-h) 20% 10% / 0.10), - 0 1px 2px -1px hsl(var(--sf-neutral-h) 20% 10% / 0.10); - --sf-shadow-m: 0 4px 6px -1px hsl(var(--sf-neutral-h) 20% 10% / 0.10), - 0 2px 4px -2px hsl(var(--sf-neutral-h) 20% 10% / 0.06); - --sf-shadow-l: 0 10px 15px -3px hsl(var(--sf-neutral-h) 20% 10% / 0.10), - 0 4px 6px -4px hsl(var(--sf-neutral-h) 20% 10% / 0.06); - --sf-shadow-xl: 0 20px 25px -5px hsl(var(--sf-neutral-h) 20% 10% / 0.10), - 0 8px 10px -6px hsl(var(--sf-neutral-h) 20% 10% / 0.10); - --sf-shadow-2xl: 0 25px 50px -12px hsl(var(--sf-neutral-h) 20% 10% / 0.25); - --sf-shadow-inner: inset 0 2px 4px 0 hsl(var(--sf-neutral-h) 20% 10% / 0.10); - - /* ── Text shadows (neutral-derived) ───────────────────────────────────── */ - - --sf-text-shadow-none: none; - --sf-text-shadow-s: 0 1px 2px hsl(var(--sf-neutral-h) 20% 10% / 0.18); - --sf-text-shadow-m: 0 2px 4px hsl(var(--sf-neutral-h) 20% 10% / 0.24); - --sf-text-shadow-l: 0 4px 8px hsl(var(--sf-neutral-h) 20% 10% / 0.30); - - /* ── Drop shadows (neutral-derived) ───────────────────────────────────── */ - - --sf-drop-shadow-s: drop-shadow(0 1px 2px hsl(var(--sf-neutral-h) 20% 10% / 0.18)); - --sf-drop-shadow-m: drop-shadow(0 4px 6px hsl(var(--sf-neutral-h) 20% 10% / 0.24)); - --sf-drop-shadow-l: drop-shadow(0 8px 16px hsl(var(--sf-neutral-h) 20% 10% / 0.30)); - } -} - -/* ─── core/tokens.css ─── */ -/* ============================================================ - SLASHED — core/tokens.css - @layer slashed.tokens - - ARCHITECTURE — 3 layers: - 1. @property source tokens — animatable, 6 brand + 5 status (-light only); -dark is optional - 2. Resolved semantic tokens — light-dark() with auto-derivation of dark from light (relative color syntax) - 3. Derived tokens — relative color syntax (auto-contrast, tints/shades) - - THEMING — override just the 6 -light tokens. Dark mode auto-adapts via - relative color syntax — zero extra work. For full control over dark mode, - optionally override the -dark tokens (unregistered custom properties). - - Quick branding (6 tokens): - :root { - --sf-color-primary-light: oklch(0.47 0.27 264); - --sf-color-secondary-light: oklch(0.22 0.04 264); - --sf-color-tertiary-light: oklch(0.42 0.22 295); - --sf-color-action-light: oklch(0.50 0.22 235); - --sf-color-neutral-light: oklch(0.52 0.025 260); - --sf-color-base-light: oklch(0.96 0.006 250); - } - Full per-mode control (add -dark overrides): - :root { - --sf-color-primary-dark: oklch(0.75 0.18 264); - } - - BEM CONSUMER API — most tokens are intentionally unused by the - framework itself. Tokens such as --sf-shadow-*, --sf-blur-*, --sf-gap, - --sf-gradient-* etc. are meant for use in your own BEM classes: - .card { box-shadow: var(--sf-shadow-m); gap: var(--sf-gap); } - "Not used internally" does NOT mean dead code: every token is exercised - in docs/demo.html and validated by the regression suite (tests/), so an - unreferenced token is never untested or orphaned. - - RELATION TO tokens.layout.css — the spacing aliases in the "Spacing - aliases" section are the source of truth for --sf-space-gap and - --sf-space-content in tokens.layout.css. Overriding --sf-gap changes the - gaps across all layout primitives (cluster, grid, sidebar, etc.). - - PUBLIC API vs INTERNAL (token-API freeze contract) - ───────────────────────────────────────────────── - Tokens are classified into three stability tiers (PUBLIC, - PUBLIC-ADVANCED, INTERNAL). - PUBLIC and PUBLIC-ADVANCED share the same SemVer guarantee - (removal/rename = major bump after freeze). The distinction is - documentation-level: ADVANCED tokens are powerful but niche — they - live in a separate "Advanced" section in docs. - - PUBLIC — everyday knobs, prominently documented: - · brand source tokens (--sf-color-{primary,secondary,tertiary,action,neutral,base}-light) - · status source tokens (--sf-color-{success,warning,error,info,danger}-light) - · resolved semantic colors (--sf-color-{primary,...,danger}, surfaces, text, borders, links, interactive bg, selection, mark, --sf-color-dim) - · numeric shade scales (--sf-color-{family}-{50..950}) — available for the 6 brand families - (primary/secondary/tertiary/action/neutral/base) via optional/tokens.palette.css only; - status families (success/warning/error/info/danger) intentionally have no numeric scale - · semantic shade aliases (--sf-color-{primary,secondary,tertiary,action,neutral,base}-{superlight,xlight,lighter,darker,xdark,superdark,hover,active,subtle,muted,ghost}) — auto-adapts to dark mode; brand families only (status families use the triplets below) - · text-on-color (--sf-color-text--on-*) - · code colors (--sf-color-code-bg, --sf-color-code-text) - · status triplets (--sf-color-{success,...,danger}-{subtle,strong,muted}) - · gradients (--sf-gradient-*) - · shadows (--sf-shadow-{none,xs,s,m,l,xl,2xl,inner}) - · text/drop shadows (--sf-text-shadow-*, --sf-drop-shadow-*) - · blur (--sf-blur-*) - · opacity (--sf-opacity-{0,10,25,50,75,100}, --sf-opacity-disabled, --sf-state-pending-opacity) - · spacing (--sf-space-{none,px,gutter,2xs..4xl}) - · font families (--sf-font-{body,heading,display,mono,humanist,geometric,slab}) - · font weights (--sf-font-weight-{thin..black,body,heading,display}) - · font sizes (--sf-text-{2xs..4xl}, --sf-text-display-{s,m,l}) - · leading (--sf-leading-{tight,snug,normal,relaxed}) - · tracking (--sf-tracking-{tight,normal,wide,wider,widest}) - · numeric figures (--sf-font-numeric) - · icon sizes (--sf-icon-{xs,s,m,l,xl,2xl}) - · UI sizes (--sf-size-{xs,s,m,l,xl}) - · containers (--sf-container-{narrow,prose,default,wide,full}) - · ratios (--sf-ratio-{square,video,cinema,portrait,golden,4-3,3-2}) - · border widths (--sf-border-width-{hairline,1,2,3,4}) - · border styles (--sf-border-style, --strong, --soft, --dotted) - · border shorthands (--sf-border, --sf-border-subtle, --sf-border-strong) - · divider (--sf-divider-{width,style,color,gap}) - · radius (--sf-radius-{none,xs,s,m,l,xl,2xl,3xl,4xl,full,pill}) - · duration (--sf-duration-{none,instant,fast,normal,slow,slower}) - · easing (--sf-ease-{linear,out,in,in-out,spring,elastic,bounce,overshoot}) - · transitions (--sf-transition-{all,colors,transform,opacity,shadow,fast,slow,enter,exit,overlay}) - · animations (--sf-animation-{fade-in,fade-out,slide-in-*,scale-*,color-pulse,ping,blink,float,spin,shimmer}) - · animation delays (--sf-animation-delay-{1..5}) - · z-index (--sf-z-{below,base,raised,low,mid,high,top,max}) - · header/sticky (--sf-header-height{,-mobile,-desktop}, --sf-sticky-offset{,-mobile,-desktop}) - · touch target (--sf-touch-target) - · focus ring (--sf-focus-ring-{width,offset,style,color}) - · caret (--sf-caret-color) - · scrollbar (--sf-scrollbar-{thumb,track}) - · BEM aliases (--sf-gap, --sf-content-gap, --sf-component-pad, --sf-field-block, - --sf-field-required-marker, --sf-link-external-marker) - · section padding (--sf-section-pad, --sf-section-pad--{xs,s,m,l,xl,2xl}) - · typography aliases (--sf-body-*, --sf-code-font-size, --sf-heading-*, --sf-h{1..6}-*) - · link geometry (--sf-link-underline-offset, --sf-link-underline-thickness) - · current-font-weight (--sf-current-font-weight) - · color scheme (--sf-color-scheme) - · color anchors (--sf-color-white, --sf-color-black) - · stroke (--sf-stroke-{thin,regular,bold,heavy}) - · object fit (--sf-object-fit, --sf-object-position) - · multi-column (--sf-col-width-{s,m,l}, --sf-col-rule-width-{s,m,l}) - · layout tokens — all of tokens.layout.css (per-primitive gaps, sidebar, grid, bento, etc.) - · macro tokens — all of tokens.macros.css (flow-space, line-clamp, aspect, scroll-shadow-size) - · palette tokens — all of optional/tokens.palette.css - · scoped override tokens (--sf-icon-size, --sf-bento-cols, --sf-bento-row, - --sf-color-code-block-bg, --sf-color-code-block-text, - --sf-field-border-color, --sf-field-text-color) - - PUBLIC-ADVANCED — stable (same SemVer guarantee) but niche/powerful: - · --sf-lumlocker (LumLocker lightness override) - · --sf-contrast-bias, --sf-contrast-threshold (text-contrast tuning) - · --sf-shadow-strength, --sf-shadow-color, --sf-shadow-glow-color (shadow internals) - · --sf-font-features, --sf-font-variation, --sf-optical-sizing (OpenType) - · --sf-space-scale, --sf-text-scale, --sf-text-display-scale, - --sf-radius-scale, --sf-motion-scale (global multipliers) - · --sf-scroll-timeline-range-{start,end} (scroll-driven animation) - · --sf-mask-scrim-{start,end} (edge-fade masks) - · --sf-safe-{top,bottom,left,right} (safe-area insets) - · --sf-perspective-{near,normal,far} (3D transforms) - · --sf-truncate-suffix (ellipsis character override) - · --sf-radius-outer (concentric radius helper) - · --sf-is-active, --sf-is-current, --sf-is-pressed, --sf-is-open (state flags) - · --sf-focus-ring-shadow (composite focus shadow) - · --sf-print-{page-margin,page-size,base-size} (print tokens) - - INTERNAL — implementation detail, may change without a major bump: - · --sf-is-dark (mode flag — set via data-theme, not directly) - - CANONICAL-SOURCE ALIASES (intentional synonym hops, kept by design): - · --sf-space-gap → --sf-gap (layout primitives read this name) - · --sf-space-content → --sf-content-gap (stack / prose rhythm) - · --sf-section-pad → --sf-section-pad--m (unsuffixed = the -m default) - These are deliberate two-name contracts: override the canonical token - (right side) to move everything downstream. The alias graph is otherwise - ≤3 hops (per-primitive → layout-system → canonical source), giving three - override levels: local (style="--sf-cluster-gap"), all-primitives - (--sf-space-gap), or global (--sf-gap). - ============================================================ */ - -@layer slashed.tokens { - - /* ============================================================ - @property — BRAND COLORS (-light source tokens, animatable) - Override these 6 tokens to rebrand — dark mode auto-derives. - Optionally set --sf-color-X-dark (no @property) for full control. - ============================================================ */ - - @property --sf-color-primary-light { syntax: ""; inherits: true; initial-value: oklch(0.47 0.27 264); } - @property --sf-color-secondary-light { syntax: ""; inherits: true; initial-value: oklch(0.22 0.04 264); } - @property --sf-color-tertiary-light { syntax: ""; inherits: true; initial-value: oklch(0.42 0.22 295); } - @property --sf-color-action-light { syntax: ""; inherits: true; initial-value: oklch(0.50 0.22 235); } - @property --sf-color-neutral-light { syntax: ""; inherits: true; initial-value: oklch(0.52 0.025 260); } - /* initial-value is 0.96 (not 0.99) so the +0.02/+0.04 surface offsets produce - visually distinct levels in light mode. At 0.99 all lighter surfaces clamp - to pure white and become indistinguishable. 0.96 gives: bg=0.98, inset=0.94, - raised=1.0→white — all distinct. Consumers using near-1.0 values should rely - on shadows rather than lightness for elevation. */ - @property --sf-color-base-light { syntax: ""; inherits: true; initial-value: oklch(0.96 0.006 250); } - - /* @property — STATUS COLORS (-light source tokens) */ - - @property --sf-color-success-light { syntax: ""; inherits: true; initial-value: oklch(0.50 0.16 145); } - @property --sf-color-warning-light { syntax: ""; inherits: true; initial-value: oklch(0.75 0.17 80); } - @property --sf-color-error-light { syntax: ""; inherits: true; initial-value: oklch(0.50 0.20 25); } - @property --sf-color-info-light { syntax: ""; inherits: true; initial-value: oklch(0.48 0.18 235); } - @property --sf-color-danger-light { syntax: ""; inherits: true; initial-value: oklch(0.48 0.22 12); } - - /* Plain :root mirrors — character-for-character identical to each @property - initial-value above. Engines lacking @property support read the token value - from here. Do NOT edit these independently from the initial-value strings. */ - :root { - --sf-color-primary-light: oklch(0.47 0.27 264); - --sf-color-secondary-light: oklch(0.22 0.04 264); - --sf-color-tertiary-light: oklch(0.42 0.22 295); - --sf-color-action-light: oklch(0.50 0.22 235); - --sf-color-neutral-light: oklch(0.52 0.025 260); - --sf-color-base-light: oklch(0.96 0.006 250); - --sf-color-success-light: oklch(0.50 0.16 145); - --sf-color-warning-light: oklch(0.75 0.17 80); - --sf-color-error-light: oklch(0.50 0.20 25); - --sf-color-info-light: oklch(0.48 0.18 235); - --sf-color-danger-light: oklch(0.48 0.22 12); - } - - /* Mode flag — drives formula direction for non-color dark overrides. - Set by themes.css via [data-theme="dark"] and the prefers-color-scheme - media query. Do not set directly. */ - @property --sf-is-dark { syntax: ""; inherits: true; initial-value: 0; } - - @property --sf-is-active { syntax: ""; inherits: true; initial-value: 0; } - @property --sf-is-current { syntax: ""; inherits: true; initial-value: 0; } - @property --sf-is-pressed { syntax: ""; inherits: true; initial-value: 0; } - @property --sf-is-open { syntax: ""; inherits: true; initial-value: 0; } - - /* ============================================================ - DERIVED SEMANTIC TOKENS - ============================================================ */ - - :root { - - /* ---------------------------------------------------------- - Color scheme - ---------------------------------------------------------- */ - --sf-color-scheme: light dark; - - /* ---------------------------------------------------------- - LumLocker — OKLCH L value used by the - :root[data-lumlocker] override in core/themes.css. - Locks all 5 brand colors (primary, secondary, tertiary, - action, neutral) to one shared lightness while keeping their - individual hue and chroma. Base is excluded — it must remain - near-white in light mode for the page background. - Override to change the locked lightness: - :root { --sf-lumlocker: 0.55 } - ---------------------------------------------------------- */ - --sf-lumlocker: 0.65; - - /* ---------------------------------------------------------- - Resolved color tokens — auto-switch via light-dark(). - Components reference these. Override the -light / -dark - source tokens above to customise; or override these directly - with your own light-dark() expression. - ---------------------------------------------------------- */ - /* Surface alias (plain var — no IACVT, stays ungated) */ - --sf-color-surface: var(--sf-color-base); - - /* ---------------------------------------------------------- - Text — plain var aliases (ungated) - ---------------------------------------------------------- */ - --sf-color-text--muted: var(--sf-color-neutral); - - /* ---------------------------------------------------------- - Text-on-color — plain var aliases (ungated) - ---------------------------------------------------------- */ - --sf-color-text--on-base: var(--sf-color-text); - --sf-color-text--on-surface: var(--sf-color-text--on-base); /* compat alias → base */ - --sf-color-text--on-inverse: var(--sf-color-text--inverse); - - /* ---------------------------------------------------------- - Borders — plain var aliases (ungated) - ---------------------------------------------------------- */ - --sf-color-border--focus: var(--sf-color-action); - - /* ---------------------------------------------------------- - Links — plain var aliases and geometry (ungated) - ---------------------------------------------------------- */ - --sf-color-link--disabled: var(--sf-color-text--disabled); - /* Underline geometry — consumed by a:link in core/base.css and by - the .sf-link--* macros. `auto` defers to the font's own metrics; - set an explicit length (e.g. 0.08em) for a uniform rule. */ - --sf-link-underline-offset: 0.15em; - --sf-link-underline-thickness: auto; - - /* ---------------------------------------------------------- - Interactive states — plain var aliases (ungated) - ---------------------------------------------------------- */ - --sf-color-bg--disabled: var(--sf-color-inset); - --sf-color-code-bg: var(--sf-color-inset); - - /* ---------------------------------------------------------- - Selection & backdrop — static values (ungated) - ---------------------------------------------------------- */ - --sf-color-selection-text: inherit; - --sf-color-mark-text: inherit; - --sf-color-dim: oklch(0 0 0 / 0.5); - --sf-color-white: oklch(100% 0 0); - --sf-color-black: oklch(0% 0 0); - - /* ---------------------------------------------------------- - Scrollbar - ---------------------------------------------------------- */ - --sf-scrollbar-thumb: var(--sf-color-neutral); - --sf-scrollbar-track: transparent; - - /* ---------------------------------------------------------- - Shadow — strength scales up in dark mode via --sf-is-dark - ---------------------------------------------------------- */ - --sf-shadow-strength: calc(0.08 + var(--sf-is-dark) * 0.17); - --sf-shadow-glow-color: var(--sf-color-primary); - --sf-opacity-disabled: 0.45; - --sf-state-pending-opacity: 0.7; - - /* ---------------------------------------------------------- - Focus / form colors - ---------------------------------------------------------- */ - --sf-focus-ring-color: var(--sf-color-action); - --sf-focus-ring-shadow: 0 0 0 var(--sf-focus-ring-offset) var(--sf-color-bg), - 0 0 0 calc(var(--sf-focus-ring-offset) + var(--sf-focus-ring-width)) var(--sf-focus-ring-color); - --sf-caret-color: var(--sf-color-action); - - } - /* ── @supports gate: light-dark() ── */ - @supports (color: light-dark(white, black)) { - :root { - - /* ---------------------------------------------------------- - Resolved color tokens — auto-switch via light-dark(). - Dark auto-derivation formula (brand + status): - clamp(0.65, 0.95 - l*0.5, 0.88) lightens dark-mode value relative to the light source. - Surface inverts: clamp(0.16, 1.18 - l, 0.24) — near-white flips to near-dark. - Override any --sf-color-X-dark to take full per-mode control. */ - --sf-color-primary: light-dark(var(--sf-color-primary-light), var(--sf-color-primary-dark, oklch(from var(--sf-color-primary-light) clamp(0.65, calc(0.95 - l * 0.5), 0.88) calc(c * 0.9) h))); - --sf-color-secondary: light-dark(var(--sf-color-secondary-light), var(--sf-color-secondary-dark, oklch(from var(--sf-color-secondary-light) clamp(0.65, calc(0.95 - l * 0.5), 0.88) calc(c * 0.9) h))); - --sf-color-tertiary: light-dark(var(--sf-color-tertiary-light), var(--sf-color-tertiary-dark, oklch(from var(--sf-color-tertiary-light) clamp(0.65, calc(0.95 - l * 0.5), 0.88) calc(c * 0.9) h))); - --sf-color-action: light-dark(var(--sf-color-action-light), var(--sf-color-action-dark, oklch(from var(--sf-color-action-light) clamp(0.65, calc(0.95 - l * 0.5), 0.88) calc(c * 0.9) h))); - --sf-color-neutral: light-dark(var(--sf-color-neutral-light), var(--sf-color-neutral-dark, oklch(from var(--sf-color-neutral-light) clamp(0.65, calc(0.95 - l * 0.5), 0.88) calc(c * 0.9) h))); - --sf-color-base: light-dark(var(--sf-color-base-light), var(--sf-color-base-dark, oklch(from var(--sf-color-base-light) clamp(0.16, calc(1.18 - l), 0.24) calc(c * 0.5) h))); - - --sf-color-success: light-dark(var(--sf-color-success-light), var(--sf-color-success-dark, oklch(from var(--sf-color-success-light) clamp(0.65, calc(0.95 - l * 0.5), 0.88) calc(c * 0.9) h))); - --sf-color-warning: light-dark(var(--sf-color-warning-light), var(--sf-color-warning-dark, oklch(from var(--sf-color-warning-light) clamp(0.65, calc(0.95 - l * 0.5), 0.88) calc(c * 0.9) h))); - --sf-color-error: light-dark(var(--sf-color-error-light), var(--sf-color-error-dark, oklch(from var(--sf-color-error-light) clamp(0.65, calc(0.95 - l * 0.5), 0.88) calc(c * 0.9) h))); - --sf-color-info: light-dark(var(--sf-color-info-light), var(--sf-color-info-dark, oklch(from var(--sf-color-info-light) clamp(0.65, calc(0.95 - l * 0.5), 0.88) calc(c * 0.9) h))); - --sf-color-danger: light-dark(var(--sf-color-danger-light), var(--sf-color-danger-dark, oklch(from var(--sf-color-danger-light) clamp(0.65, calc(0.95 - l * 0.5), 0.88) calc(c * 0.9) h))); - - /* ---------------------------------------------------------- - Text — light and dark require opposite formula directions, - so each side of light-dark() references its source token. - ---------------------------------------------------------- */ - --sf-color-text: light-dark( - oklch(from var(--sf-color-neutral-light) clamp(0.05, calc(l - 0.4 - var(--sf-contrast-bias)), 0.35) c h), - oklch(from var(--sf-color-neutral) clamp(0.70, calc(l + 0.25 + var(--sf-contrast-bias)), 1) c h) - ); - --sf-color-text--secondary: light-dark( - oklch(from var(--sf-color-neutral-light) clamp(0.15, calc(l - 0.25 - var(--sf-contrast-bias)), 0.45) c h), - oklch(from var(--sf-color-neutral) clamp(0.55, calc(l + 0.1 + var(--sf-contrast-bias)), 0.90) c h) - ); - --sf-color-text--placeholder: light-dark( - oklch(from var(--sf-color-neutral-light) clamp(0.45, calc(l + 0.15), 0.75) c h), - oklch(from var(--sf-color-neutral) clamp(0.35, calc(l - 0.1), 0.65) c h) - ); - --sf-color-text--disabled: light-dark( - oklch(from var(--sf-color-neutral-light) clamp(0.55, calc(l + 0.25), 0.82) c h), - oklch(from var(--sf-color-neutral) clamp(0.25, calc(l - 0.2), 0.55) c h) - ); - --sf-color-text--inverse: light-dark( - oklch(from var(--sf-color-neutral-light) clamp(0.85, calc(l + 0.4), 0.98) c h), - oklch(from var(--sf-color-neutral) clamp(0.05, calc(l - 0.4), 0.35) c h) - ); - --sf-color-heading: light-dark( - oklch(from var(--sf-color-neutral-light) clamp(0.05, calc(l - 0.4 - var(--sf-contrast-bias)), 0.35) c h), - oklch(from var(--sf-color-neutral) clamp(0.70, calc(l + 0.25 + var(--sf-contrast-bias)), 1) c h) - ); - - /* ---------------------------------------------------------- - Borders — direction-dependent, reference source tokens. - Light: borders are lighter than neutral (l + offset). - Dark: borders are darker than neutral (l - offset). - ---------------------------------------------------------- */ - --sf-color-border: light-dark( - oklch(from var(--sf-color-neutral-light) clamp(0.70, calc(l + 0.35), 0.95) 0.005 h), - oklch(from var(--sf-color-neutral) clamp(0.25, calc(l - 0.3), 0.55) 0.005 h) - ); - --sf-color-border--subtle: light-dark( - oklch(from var(--sf-color-neutral-light) clamp(0.75, calc(l + 0.4), 0.97) 0.005 h), - oklch(from var(--sf-color-neutral) clamp(0.20, calc(l - 0.38), 0.45) 0.005 h) - ); - --sf-color-border--strong: light-dark( - oklch(from var(--sf-color-neutral-light) clamp(0.55, calc(l + 0.1), 0.85) 0.02 h), - oklch(from var(--sf-color-neutral) clamp(0.38, calc(l - 0.1), 0.65) 0.02 h) - ); - - /* ---------------------------------------------------------- - Links — derived from --sf-color-action (auto-adapts) - ---------------------------------------------------------- */ - /* Links keep the action hue but CLAMP lightness toward a contrast-safe - band — a ceiling in light mode, a floor in dark mode — so link text - clears WCAG AA (4.5:1) on the page background for the default palette - AND the large majority of brand overrides (a fixed offset alone only - fixed the default). Lightness dominates OKLCH contrast but doesn't - fully determine it: very high-chroma hues (saturated yellow/green) can - still fall short even at a safe lightness — those remain the consumer's - responsibility (see docs/theming.md → "Link contrast"). - - NOTE: the consumer-facing :link rule in core/base.css uses - `a:link` (not bare `a`) so its specificity matches WebKit's UA - stylesheet `a:link { color: -webkit-link }`. Without that - pseudo-class the framework's `--sf-color-link` value is - silently ignored on unvisited anchors in WebKit and - `-webkit-link` (≈ #00728f in dark mode) paints instead — - regardless of how this token is computed. PR #73 and several - follow-up attempts mistakenly chased the formula. */ - --sf-color-link: light-dark( - oklch(from var(--sf-color-action) clamp(0, min(l - 0.07, 0.48), 1) c h), - oklch(from var(--sf-color-action) clamp(0.68, l, 1) c h) - ); - --sf-color-link--hover: light-dark( - oklch(from var(--sf-color-action) clamp(0, min(l - 0.15, 0.40), 1) c h), - oklch(from var(--sf-color-action) clamp(0, max(l + 0.10, 0.68), 1) c h) - ); - --sf-color-link--active: light-dark( - oklch(from var(--sf-color-action) clamp(0, min(l - 0.21, 0.34), 1) c h), - oklch(from var(--sf-color-action) clamp(0, max(l + 0.15, 0.74), 1) c h) - ); - /* Visited link — 60° hue shift from action gives clearly distinct - chroma without colliding with default tertiary (h ≈ 310). Same - lightness clamp as the base link. If your brand puts action and - tertiary close in hue, override directly: - :root { --sf-color-link--visited: #6b46c1; } */ - --sf-color-link--visited: light-dark( - oklch(from var(--sf-color-action) clamp(0, min(l - 0.07, 0.48), 1) c calc(h + 60)), - oklch(from var(--sf-color-action) clamp(0.68, l, 1) c calc(h + 60)) - ); - - /* ---------------------------------------------------------- - Selection - ---------------------------------------------------------- */ - --sf-color-selection-bg: light-dark( - oklch(from var(--sf-color-action-light) l c h / 0.28), - oklch(from var(--sf-color-action-light) clamp(0.62, calc(0.93 - l * 0.4), 0.78) c h / 0.55) - ); - - /* ---------------------------------------------------------- - Status triplets — strong (direction-dependent, uses light-dark) - error = form validation, input errors - danger = destructive actions (delete, remove) - ---------------------------------------------------------- */ - --sf-color-success-strong: light-dark( - oklch(from var(--sf-color-success-light) calc(l - 0.15) c h), - oklch(from var(--sf-color-success) clamp(0.70, calc(l + 0.15), 1) c h) - ); - --sf-color-warning-strong: light-dark( - oklch(from var(--sf-color-warning-light) calc(l - 0.25) c h), - oklch(from var(--sf-color-warning) clamp(0.70, calc(l + 0.05), 1) c h) - ); - --sf-color-error-strong: light-dark( - oklch(from var(--sf-color-error-light) calc(l - 0.1) c h), - oklch(from var(--sf-color-error) clamp(0.70, calc(l + 0.15), 1) c h) - ); - --sf-color-info-strong: light-dark( - oklch(from var(--sf-color-info-light) calc(l - 0.1) c h), - oklch(from var(--sf-color-info) clamp(0.70, calc(l + 0.15), 1) c h) - ); - --sf-color-danger-strong: light-dark( - oklch(from var(--sf-color-danger-light) calc(l - 0.1) c h), - oklch(from var(--sf-color-danger) clamp(0.70, calc(l + 0.15), 1) c h) - ); - - } - } - /* ── @supports gate: oklch(from …) relative color + color-mix() ── */ - @supports (color: oklch(from red l c h)) { - :root { - - /* ---------------------------------------------------------- - Surfaces — derived from --sf-color-base (auto-adapts) - ---------------------------------------------------------- */ - --sf-color-bg: oklch(from var(--sf-color-base) calc(l + 0.02) c h); - --sf-color-inset: oklch(from var(--sf-color-base) calc(l - 0.02) c h); - --sf-color-raised: oklch(from var(--sf-color-base) calc(l + 0.04) c h); - --sf-color-overlay: oklch(from var(--sf-color-base) l c h / 0.9); - --sf-color-inverse: oklch(from var(--sf-color-base) calc(1 - l) c h); - - /* ---------------------------------------------------------- - Code text — auto-contrasts with --sf-color-code-bg. - ---------------------------------------------------------- */ - /* Inline-code text colour. Auto-contrasts with --sf-color-code-bg using - the same luminance formula as --sf-color-text--on-*. This prevents - invisible text when code appears inside inverted-colour containers - (e.g. sf-imposter with a dark background where inherited colour is - near-white). Override with a tint value if desired. */ - --sf-color-code-text: oklch(from var(--sf-color-code-bg) clamp(0.1, sign(var(--sf-contrast-threshold) - l) * 999, 0.95) 0 0); - - /* ---------------------------------------------------------- - Text-on-color — sign(var(--sf-contrast-threshold) - l) * 999 is mode-agnostic: - auto-selects light or dark text based on background luminance. - Uses resolved tokens — works for any user-supplied color. - - CONTRAST NOTE: Guarantees ≥ 3:1 (WCAG AA Large Text / UI). - Colors in the mid-luminance range (L ≈ 0.52–0.67) may not - reach 4.5:1 (AA Normal Text) regardless of threshold chosen — - this is a mathematical limitation of any binary black/white - decision. The SLASHED configurator will warn users when their - brand colors fall in this range. For body text on colored - backgrounds, override --sf-color-text--on-* manually. - ---------------------------------------------------------- */ - --sf-color-text--on-primary: oklch(from var(--sf-color-primary) clamp(0.1, sign(var(--sf-contrast-threshold) - l) * 999, 0.95) 0 0); - --sf-color-text--on-secondary: oklch(from var(--sf-color-secondary) clamp(0.1, sign(var(--sf-contrast-threshold) - l) * 999, 0.95) 0 0); - --sf-color-text--on-tertiary: oklch(from var(--sf-color-tertiary) clamp(0.1, sign(var(--sf-contrast-threshold) - l) * 999, 0.95) 0 0); - --sf-color-text--on-action: oklch(from var(--sf-color-action) clamp(0.1, sign(var(--sf-contrast-threshold) - l) * 999, 0.95) 0 0); - --sf-color-text--on-neutral: oklch(from var(--sf-color-neutral) clamp(0.1, sign(var(--sf-contrast-threshold) - l) * 999, 0.95) 0 0); - --sf-color-text--on-success: oklch(from var(--sf-color-success) clamp(0.1, sign(var(--sf-contrast-threshold) - l) * 999, 0.95) 0 0); - --sf-color-text--on-warning: oklch(from var(--sf-color-warning) clamp(0.1, sign(var(--sf-contrast-threshold) - l) * 999, 0.95) 0 0); - --sf-color-text--on-error: oklch(from var(--sf-color-error) clamp(0.1, sign(var(--sf-contrast-threshold) - l) * 999, 0.95) 0 0); - --sf-color-text--on-info: oklch(from var(--sf-color-info) clamp(0.1, sign(var(--sf-contrast-threshold) - l) * 999, 0.95) 0 0); - --sf-color-text--on-danger: oklch(from var(--sf-color-danger) clamp(0.1, sign(var(--sf-contrast-threshold) - l) * 999, 0.95) 0 0); - - /* ---------------------------------------------------------- - Border alpha tokens - ---------------------------------------------------------- */ - --sf-color-border--disabled: oklch(from var(--sf-color-border--subtle) l 0 h / 0.5); - --sf-color-border--translucent: oklch(from var(--sf-color-neutral) l c h / 0.15); - - /* ---------------------------------------------------------- - Link alpha token - ---------------------------------------------------------- */ - --sf-color-link--underline: oklch(from var(--sf-color-action) l c h / 0.3); - - /* ---------------------------------------------------------- - Interactive states — derived from --sf-color-neutral (auto-adapts) - ---------------------------------------------------------- */ - --sf-color-bg--hover: oklch(from var(--sf-color-neutral) l c h / 0.08); - --sf-color-bg--active: oklch(from var(--sf-color-neutral) l c h / 0.12); - --sf-color-bg--selected: oklch(from var(--sf-color-action) l c h / 0.1); - --sf-color-bg--focus: oklch(from var(--sf-color-action) l c h / 0.06); - - /* ---------------------------------------------------------- - Selection & mark - ---------------------------------------------------------- */ - --sf-color-mark-bg: oklch(from var(--sf-color-warning) l c h / 0.25); - - /* ---------------------------------------------------------- - Status triplets — subtle/muted (alpha variants) - bg/border use same formula both modes - ---------------------------------------------------------- */ - --sf-color-success-subtle: oklch(from var(--sf-color-success) l c h / 0.12); - --sf-color-success-muted: oklch(from var(--sf-color-success) l c h / 0.3); - - --sf-color-warning-subtle: oklch(from var(--sf-color-warning) l c h / 0.12); - --sf-color-warning-muted: oklch(from var(--sf-color-warning) l c h / 0.3); - - --sf-color-error-subtle: oklch(from var(--sf-color-error) l c h / 0.1); - --sf-color-error-muted: oklch(from var(--sf-color-error) l c h / 0.3); - - --sf-color-info-subtle: oklch(from var(--sf-color-info) l c h / 0.1); - --sf-color-info-muted: oklch(from var(--sf-color-info) l c h / 0.3); - - --sf-color-danger-subtle: oklch(from var(--sf-color-danger) l c h / 0.1); - --sf-color-danger-muted: oklch(from var(--sf-color-danger) l c h / 0.3); - - /* ---------------------------------------------------------- - Gradients — derived from brand colors (auto-adapt) - ---------------------------------------------------------- */ - --sf-gradient-primary: linear-gradient(in oklch 135deg, var(--sf-color-primary), oklch(from var(--sf-color-primary) calc(l - 0.08) c h)); - --sf-gradient-secondary: linear-gradient(in oklch 135deg, var(--sf-color-secondary), oklch(from var(--sf-color-secondary) calc(l - 0.08) c h)); - --sf-gradient-tertiary: linear-gradient(in oklch 135deg, var(--sf-color-tertiary), oklch(from var(--sf-color-tertiary) calc(l - 0.08) c h)); - --sf-gradient-brand: linear-gradient(in oklch 135deg, var(--sf-color-primary), oklch(from var(--sf-color-primary) l c calc(h + 30))); - --sf-gradient-surface: linear-gradient(in oklab 180deg, var(--sf-color-surface), var(--sf-color-bg)); - - --sf-gradient-fade--r: linear-gradient(in oklch to right, transparent, var(--sf-color-bg)); - --sf-gradient-fade--l: linear-gradient(in oklch to left, transparent, var(--sf-color-bg)); - --sf-gradient-fade--t: linear-gradient(in oklch to top, transparent, var(--sf-color-bg)); - --sf-gradient-fade--b: linear-gradient(in oklch to bottom, transparent, var(--sf-color-bg)); - - /* ---------------------------------------------------------- - Shadow glow - ---------------------------------------------------------- */ - --sf-shadow-glow: 0 0 15px 2px oklch(from var(--sf-shadow-glow-color) l c h / clamp(0, calc(var(--sf-shadow-strength) * 2), 0.7)); - - } - } - - /* ============================================================ - SEMANTIC COLOUR ALIASES — shade, interactive + transparency - - Shade aliases (superlight → superdark) use color-mix(in oklab) - toward --sf-color-surface (tints) or --sf-color-text (shades), - matching the formula used by optional/tokens.palette.css. - They are surface-aware and correct on any background. - - Surface family uses an inverted mix direction for tints: - text mixes into surface (not color into surface) so the ramp - reads light→dark regardless of the base surface value. - - Alpha aliases (subtle/muted/ghost) use oklch relative-color - syntax to preserve hue and chroma while varying only alpha. - - When optional/tokens.palette.css is also loaded it overrides - these aliases to point at the named scale steps it introduces - (-50…-950, -a5…-a95) for the 6 brand colour families — same - computed color, but overriding a step then propagates to its alias. - ============================================================ */ - - /* ── @supports gate: shade ramps + alpha aliases ── */ - @supports (color: oklch(from red l c h)) { - :root { - - /* ─── primary ──────────────────────────────────────────── */ - - --sf-color-primary-superlight: color-mix(in oklab, var(--sf-color-primary) 4%, var(--sf-color-surface)); - --sf-color-primary-xlight: color-mix(in oklab, var(--sf-color-primary) 20%, var(--sf-color-surface)); - --sf-color-primary-lighter: color-mix(in oklab, var(--sf-color-primary) 65%, var(--sf-color-surface)); - --sf-color-primary-darker: color-mix(in oklab, var(--sf-color-primary) 82%, var(--sf-color-text)); - --sf-color-primary-xdark: color-mix(in oklab, var(--sf-color-primary) 38%, var(--sf-color-text)); - --sf-color-primary-superdark: color-mix(in oklab, var(--sf-color-primary) 8%, var(--sf-color-text)); - --sf-color-primary-hover: var(--sf-color-primary-darker); - --sf-color-primary-active: var(--sf-color-primary-xdark); - --sf-color-primary-subtle: oklch(from var(--sf-color-primary) l c h / 0.10); - --sf-color-primary-muted: oklch(from var(--sf-color-primary) l c h / 0.30); - --sf-color-primary-ghost: oklch(from var(--sf-color-primary) l c h / 0.05); - - /* ─── secondary ────────────────────────────────────────── */ - - --sf-color-secondary-superlight: color-mix(in oklab, var(--sf-color-secondary) 4%, var(--sf-color-surface)); - --sf-color-secondary-xlight: color-mix(in oklab, var(--sf-color-secondary) 20%, var(--sf-color-surface)); - --sf-color-secondary-lighter: color-mix(in oklab, var(--sf-color-secondary) 65%, var(--sf-color-surface)); - --sf-color-secondary-darker: color-mix(in oklab, var(--sf-color-secondary) 82%, var(--sf-color-text)); - --sf-color-secondary-xdark: color-mix(in oklab, var(--sf-color-secondary) 38%, var(--sf-color-text)); - --sf-color-secondary-superdark: color-mix(in oklab, var(--sf-color-secondary) 8%, var(--sf-color-text)); - --sf-color-secondary-hover: var(--sf-color-secondary-darker); - --sf-color-secondary-active: var(--sf-color-secondary-xdark); - --sf-color-secondary-subtle: oklch(from var(--sf-color-secondary) l c h / 0.10); - --sf-color-secondary-muted: oklch(from var(--sf-color-secondary) l c h / 0.30); - --sf-color-secondary-ghost: oklch(from var(--sf-color-secondary) l c h / 0.05); - - /* ─── tertiary ──────────────────────────────────────────── */ - - --sf-color-tertiary-superlight: color-mix(in oklab, var(--sf-color-tertiary) 4%, var(--sf-color-surface)); - --sf-color-tertiary-xlight: color-mix(in oklab, var(--sf-color-tertiary) 20%, var(--sf-color-surface)); - --sf-color-tertiary-lighter: color-mix(in oklab, var(--sf-color-tertiary) 65%, var(--sf-color-surface)); - --sf-color-tertiary-darker: color-mix(in oklab, var(--sf-color-tertiary) 82%, var(--sf-color-text)); - --sf-color-tertiary-xdark: color-mix(in oklab, var(--sf-color-tertiary) 38%, var(--sf-color-text)); - --sf-color-tertiary-superdark: color-mix(in oklab, var(--sf-color-tertiary) 8%, var(--sf-color-text)); - --sf-color-tertiary-hover: var(--sf-color-tertiary-darker); - --sf-color-tertiary-active: var(--sf-color-tertiary-xdark); - --sf-color-tertiary-subtle: oklch(from var(--sf-color-tertiary) l c h / 0.10); - --sf-color-tertiary-muted: oklch(from var(--sf-color-tertiary) l c h / 0.30); - --sf-color-tertiary-ghost: oklch(from var(--sf-color-tertiary) l c h / 0.05); - - /* ─── action ────────────────────────────────────────────── */ - - --sf-color-action-superlight: color-mix(in oklab, var(--sf-color-action) 4%, var(--sf-color-surface)); - --sf-color-action-xlight: color-mix(in oklab, var(--sf-color-action) 20%, var(--sf-color-surface)); - --sf-color-action-lighter: color-mix(in oklab, var(--sf-color-action) 65%, var(--sf-color-surface)); - --sf-color-action-darker: color-mix(in oklab, var(--sf-color-action) 82%, var(--sf-color-text)); - --sf-color-action-xdark: color-mix(in oklab, var(--sf-color-action) 38%, var(--sf-color-text)); - --sf-color-action-superdark: color-mix(in oklab, var(--sf-color-action) 8%, var(--sf-color-text)); - --sf-color-action-hover: var(--sf-color-action-darker); - --sf-color-action-active: var(--sf-color-action-xdark); - --sf-color-action-subtle: oklch(from var(--sf-color-action) l c h / 0.10); - --sf-color-action-muted: oklch(from var(--sf-color-action) l c h / 0.30); - --sf-color-action-ghost: oklch(from var(--sf-color-action) l c h / 0.05); - - /* ─── neutral ───────────────────────────────────────────── */ - - --sf-color-neutral-superlight: color-mix(in oklab, var(--sf-color-neutral) 4%, var(--sf-color-surface)); - --sf-color-neutral-xlight: color-mix(in oklab, var(--sf-color-neutral) 20%, var(--sf-color-surface)); - --sf-color-neutral-lighter: color-mix(in oklab, var(--sf-color-neutral) 65%, var(--sf-color-surface)); - --sf-color-neutral-darker: color-mix(in oklab, var(--sf-color-neutral) 82%, var(--sf-color-text)); - --sf-color-neutral-xdark: color-mix(in oklab, var(--sf-color-neutral) 38%, var(--sf-color-text)); - --sf-color-neutral-superdark: color-mix(in oklab, var(--sf-color-neutral) 8%, var(--sf-color-text)); - --sf-color-neutral-hover: var(--sf-color-neutral-darker); - --sf-color-neutral-active: var(--sf-color-neutral-xdark); - --sf-color-neutral-subtle: oklch(from var(--sf-color-neutral) l c h / 0.10); - --sf-color-neutral-muted: oklch(from var(--sf-color-neutral) l c h / 0.30); - --sf-color-neutral-ghost: oklch(from var(--sf-color-neutral) l c h / 0.05); - - /* ─── base ────────────────────────────────────────────── */ - - --sf-color-base-superlight: color-mix(in oklab, var(--sf-color-text) 4%, var(--sf-color-base)); - --sf-color-base-xlight: color-mix(in oklab, var(--sf-color-text) 20%, var(--sf-color-base)); - --sf-color-base-lighter: color-mix(in oklab, var(--sf-color-text) 65%, var(--sf-color-base)); - --sf-color-base-darker: color-mix(in oklab, var(--sf-color-base) 82%, var(--sf-color-text)); - --sf-color-base-xdark: color-mix(in oklab, var(--sf-color-base) 38%, var(--sf-color-text)); - --sf-color-base-superdark: color-mix(in oklab, var(--sf-color-base) 8%, var(--sf-color-text)); - --sf-color-base-hover: var(--sf-color-base-darker); - --sf-color-base-active: var(--sf-color-base-xdark); - --sf-color-base-subtle: oklch(from var(--sf-color-base) l c h / 0.10); - --sf-color-base-muted: oklch(from var(--sf-color-base) l c h / 0.30); - --sf-color-base-ghost: oklch(from var(--sf-color-base) l c h / 0.05); - - } - } - - /* ============================================================ - NON-COLOR DESIGN TOKENS - ============================================================ */ - - :root { - - /* ---------------------------------------------------------- - Scale multipliers - ---------------------------------------------------------- */ - --sf-space-scale: 1; - --sf-text-scale: 1; - --sf-text-display-scale: 1; - --sf-radius-scale: 1; - --sf-motion-scale: 1; - - /* ---------------------------------------------------------- - Font families - ---------------------------------------------------------- */ - --sf-font-body: system-ui, -apple-system, sans-serif; - --sf-font-heading: var(--sf-font-body); - --sf-font-display: var(--sf-font-heading); - --sf-font-mono: ui-monospace, monospace; - - /* Optional system-font stacks (zero-cost, no @import) */ - --sf-font-humanist: "Seravek", "Gill Sans Nova", "Ubuntu", "Calibri", "DejaVu Sans", source-sans-pro, sans-serif; - --sf-font-geometric: "Avenir", "Montserrat", "Corbel", "URW Gothic", source-sans-pro, sans-serif; - --sf-font-slab: "Rockwell", "Rockwell Nova", "Roboto Slab", "DejaVu Serif", "Sitka Small", serif; - - /* Opt-in OpenType / variable-font controls (token-only, not applied by the framework) */ - --sf-font-features: normal; /* e.g. "cv11", "ss01" */ - --sf-font-variation: normal; /* e.g. "wght" 450, "opsz" 32 */ - --sf-optical-sizing: auto; - - /* ---------------------------------------------------------- - Font weights - ---------------------------------------------------------- */ - /* Numeric scale */ - --sf-font-weight-thin: 100; - --sf-font-weight-extralight: 200; - --sf-font-weight-light: 300; - --sf-font-weight-normal: 400; - --sf-font-weight-medium: 500; - --sf-font-weight-semibold: 600; - --sf-font-weight-bold: 700; - --sf-font-weight-extrabold: 800; - --sf-font-weight-black: 900; - - /* Semantic aliases — reference the numeric scale */ - --sf-font-weight-body: var(--sf-font-weight-normal); - --sf-font-weight-heading: var(--sf-font-weight-semibold); - --sf-font-weight-display: var(--sf-font-weight-bold); - - /* State token — consumed by .is-current in states.css. - Override to match your nav design (e.g. semibold for thin fonts). */ - --sf-current-font-weight: var(--sf-font-weight-bold); - - /* ---------------------------------------------------------- - Font sizes — fluid - ---------------------------------------------------------- */ - --sf-text-2xs: calc(clamp(0.51rem, calc(0.00029629629629629656 * (100vw - 22.5rem) + 0.51rem), 0.53rem) * var(--sf-text-scale)); - --sf-text-xs: calc(clamp(0.64rem, calc(0.0008888888888888881 * (100vw - 22.5rem) + 0.64rem), 0.7rem) * var(--sf-text-scale)); - --sf-text-s: calc(clamp(0.8rem, calc(0.002074074074074073 * (100vw - 22.5rem) + 0.8rem), 0.94rem) * var(--sf-text-scale)); - --sf-text-m: calc(clamp(1rem, calc(0.003703703703703704 * (100vw - 22.5rem) + 1rem), 1.25rem) * var(--sf-text-scale)); - --sf-text-l: calc(clamp(1.25rem, calc(0.006222222222222221 * (100vw - 22.5rem) + 1.25rem), 1.67rem) * var(--sf-text-scale)); - --sf-text-xl: calc(clamp(1.56rem, calc(0.00977777777777778 * (100vw - 22.5rem) + 1.56rem), 2.22rem) * var(--sf-text-scale)); - --sf-text-2xl: calc(clamp(1.95rem, calc(0.014962962962962963 * (100vw - 22.5rem) + 1.95rem), 2.96rem) * var(--sf-text-scale)); - --sf-text-3xl: calc(clamp(2.44rem, calc(0.022370370370370374 * (100vw - 22.5rem) + 2.44rem), 3.95rem) * var(--sf-text-scale)); - --sf-text-4xl: calc(clamp(3.05rem, calc(0.03274074074074074 * (100vw - 22.5rem) + 3.05rem), 5.26rem) * var(--sf-text-scale)); - - --sf-text-display-s: calc(clamp(2.4rem, calc(0.00888888888888889 * (100vw - 22.5rem) + 2.4rem), 3rem) * var(--sf-text-display-scale)); - --sf-text-display-m: calc(clamp(3rem, calc(0.014814814814814815 * (100vw - 22.5rem) + 3rem), 4rem) * var(--sf-text-display-scale)); - --sf-text-display-l: calc(clamp(3.75rem, calc(0.023407407407407408 * (100vw - 22.5rem) + 3.75rem), 5.33rem) * var(--sf-text-display-scale)); - - /* ---------------------------------------------------------- - Line heights & letter spacing - ---------------------------------------------------------- */ - --sf-leading-tight: 1.1; - --sf-leading-snug: 1.3; - --sf-leading-normal: 1.5; - --sf-leading-relaxed: 1.625; - - --sf-tracking-tight: -0.025em; - --sf-tracking-normal: 0; - --sf-tracking-wide: 0.025em; - --sf-tracking-wider: 0.05em; - --sf-tracking-widest: 0.1em; - - /* Numeric figure style — tabular-nums gives every digit a fixed - advance width so numbers line up in vertical columns (price - lists, totals, invoices, dashboards). Consumed by number inputs - in optional/forms.css and available to BEM components, e.g. - `.price { font-variant-numeric: var(--sf-font-numeric); }`. - Universal browser support. */ - --sf-font-numeric: tabular-nums; - - /* ---------------------------------------------------------- - Icon sizes — em-based by design. - Icons are sized in em so they scale with the surrounding - text: an icon in a button at --sf-text-m renders at 1rem; - moved into a heading at --sf-text-2xl, the same icon scales - up automatically. - Global scaling flows transitively through --sf-text-scale - (parent text grows → em-icons grow) — there is no separate - --sf-icon-scale knob by design. clamp() is intentionally - absent here: making icons fluid INDEPENDENTLY of context - would break the relationship to surrounding text. - Capped at --2xl (4em ≈ 64px on default text); above that the - element ceases to be an icon and becomes an illustration. - ---------------------------------------------------------- */ - --sf-icon-xs: 0.875em; - --sf-icon-s: 1em; - --sf-icon-m: 1.5em; - --sf-icon-l: 2em; - --sf-icon-xl: 3em; - --sf-icon-2xl: 4em; - - /* ---------------------------------------------------------- - Spacing — fixed - ---------------------------------------------------------- */ - --sf-space-none: 0; - --sf-space-px: 1px; - --sf-space-gutter: var(--sf-space-l); - - /* Spacing — fluid */ - --sf-space-2xs: calc(clamp(0.51rem, calc(0.004888888888888888 * (100vw - 22.5rem) + 0.51rem), 0.84rem) * var(--sf-space-scale)); - --sf-space-xs: calc(clamp(0.64rem, calc(0.007259259259259258 * (100vw - 22.5rem) + 0.64rem), 1.13rem) * var(--sf-space-scale)); - --sf-space-s: calc(clamp(0.8rem, calc(0.01037037037037037 * (100vw - 22.5rem) + 0.8rem), 1.5rem) * var(--sf-space-scale)); - --sf-space-m: calc(clamp(1rem, calc(0.014814814814814815 * (100vw - 22.5rem) + 1rem), 2rem) * var(--sf-space-scale)); - --sf-space-l: calc(clamp(1.25rem, calc(0.021037037037037035 * (100vw - 22.5rem) + 1.25rem), 2.67rem) * var(--sf-space-scale)); - --sf-space-xl: calc(clamp(1.56rem, calc(0.029481481481481477 * (100vw - 22.5rem) + 1.56rem), 3.55rem) * var(--sf-space-scale)); - --sf-space-2xl: calc(clamp(1.95rem, calc(0.04133333333333333 * (100vw - 22.5rem) + 1.95rem), 4.74rem) * var(--sf-space-scale)); - --sf-space-3xl: calc(clamp(2.44rem, calc(0.057333333333333326 * (100vw - 22.5rem) + 2.44rem), 6.31rem) * var(--sf-space-scale)); - --sf-space-4xl: calc(clamp(3.05rem, calc(0.07955555555555556 * (100vw - 22.5rem) + 3.05rem), 8.42rem) * var(--sf-space-scale)); - - /* ---------------------------------------------------------- - UI sizes - ---------------------------------------------------------- */ - --sf-size-xs: 1.5rem; - --sf-size-s: 2rem; - --sf-size-m: 2.5rem; - --sf-size-l: 2.75rem; - --sf-size-xl: 3.5rem; - - /* ---------------------------------------------------------- - Containers - ---------------------------------------------------------- */ - --sf-container-narrow: 38rem; - --sf-container-prose: 65ch; - --sf-container-default: 75rem; - --sf-container-wide: 90rem; - --sf-container-full: 100%; - - /* ---------------------------------------------------------- - Aspect ratios - ---------------------------------------------------------- */ - --sf-ratio-square: 1; - --sf-ratio-video: 16 / 9; - --sf-ratio-cinema: 21 / 9; - --sf-ratio-4-3: 4 / 3; - --sf-ratio-3-2: 3 / 2; - --sf-ratio-portrait: 3 / 4; - --sf-ratio-golden: 1.618 / 1; - - /* ---------------------------------------------------------- - Border widths - --sf-border-width-hairline: device-pixel-dependent. On 1x DPR - displays (Windows non-Retina) the browser will round 0.5px up - to 1px, so it visually matches --sf-border-width-1. On 2x+ - DPR (macOS/iOS Retina, most modern phones) it renders as a - true half-pixel hairline. Use it for subtle separators where - 1px on low-DPR is acceptable. - ---------------------------------------------------------- */ - --sf-border-width-hairline: 0.5px; - --sf-border-width-1: 1px; - --sf-border-width-2: 2px; - --sf-border-width-3: 3px; - --sf-border-width-4: 4px; - - /* ---------------------------------------------------------- - Border styles — paired with --sf-border-width-* and - --sf-color-border*. Consumer BEM: - .my-card { border: var(--sf-border-width-1) var(--sf-border-style) var(--sf-color-border); } - Override per element to switch decorative styles without - rewriting the rule. - ---------------------------------------------------------- */ - --sf-border-style: solid; /* default */ - --sf-border-style-soft: dashed; - --sf-border-style-dotted: dotted; - - /* ---------------------------------------------------------- - Border shorthands — convenience tokens composing width, style, - and color into a single value for the common cases. - subtle / strong vary COLOR CONTRAST only — all three use - --sf-border-width-1 (1px). This matches the IBM Carbon and - GitHub Primer convention where subtle/strong name a color axis, - not a weight axis. For a thicker border compose manually: - border: var(--sf-border-width-2) var(--sf-border-style) var(--sf-color-border--strong) - Usage: border: var(--sf-border); - border-bottom: var(--sf-border-subtle); - ---------------------------------------------------------- */ - --sf-border: var(--sf-border-width-1) var(--sf-border-style) var(--sf-color-border); - --sf-border-subtle: var(--sf-border-width-1) var(--sf-border-style) var(--sf-color-border--subtle); - --sf-border-strong: var(--sf-border-width-1) var(--sf-border-style) var(--sf-color-border--strong); - - /* ---------------------------------------------------------- - Divider — global separator system. Drives `hr` (base) and - the `.sf-divider` layout primitive. - ---------------------------------------------------------- */ - --sf-divider-width: var(--sf-border-width-1); - --sf-divider-style: solid; - --sf-divider-color: var(--sf-color-border); - --sf-divider-gap: var(--sf-space-m); - - /* ---------------------------------------------------------- - Border radius - --sf-radius-full: intentionally not scaled by --sf-radius-scale. - A pill/circle is a topological constant, not a relative size: - any --sf-radius-scale: 0 ("sharp" design system) should still - render pill buttons as pills. 9999px is a magic-number convention - large enough to clip to half the shorter edge for any realistic UI. - ---------------------------------------------------------- */ - --sf-radius-none: 0; - --sf-radius-xs: calc(2px * var(--sf-radius-scale)); - --sf-radius-s: calc(4px * var(--sf-radius-scale)); - --sf-radius-m: calc(8px * var(--sf-radius-scale)); - --sf-radius-l: calc(12px * var(--sf-radius-scale)); - --sf-radius-xl: calc(16px * var(--sf-radius-scale)); - --sf-radius-2xl: calc(24px * var(--sf-radius-scale)); - --sf-radius-3xl: calc(32px * var(--sf-radius-scale)); - --sf-radius-4xl: calc(48px * var(--sf-radius-scale)); - --sf-radius-full: 9999px; - /* Pill — semantic alias of --sf-radius-full. Reach for this name - on rectangles you want capsule-shaped (badges, tags, chips); use - --sf-radius-full when you mean "as round as topologically possible". */ - --sf-radius-pill: var(--sf-radius-full); - /* Concentric helper — outer radius for a container that wraps - content padded by --sf-component-pad with inner radius m. - Keeps inner elements visually proportional inside a padded box. - Override per use: - .my-card { border-radius: var(--sf-radius-outer); } - Maths: outer = inner + padding ⇒ aligned curves at corners. */ - --sf-radius-outer: calc(var(--sf-radius-m) + var(--sf-component-pad)); - - /* ---------------------------------------------------------- - Shadows — BEM consumer API - Use in your own classes: .card { box-shadow: var(--sf-shadow-m); } - ---------------------------------------------------------- */ - - - /* ---------------------------------------------------------- - Blur & opacity - ---------------------------------------------------------- */ - --sf-blur-xs: 4px; - --sf-blur-s: 8px; - --sf-blur-m: 16px; - --sf-blur-l: 32px; - --sf-blur-xl: 48px; - - /* 3D perspective */ - --sf-perspective-near: 500px; - --sf-perspective-normal: 1000px; - --sf-perspective-far: 2000px; - - --sf-opacity-0: 0; - --sf-opacity-10: 0.1; - --sf-opacity-25: 0.25; - --sf-opacity-50: 0.5; - --sf-opacity-75: 0.75; - --sf-opacity-100: 1; - - /* ---------------------------------------------------------- - Motion & easing - ---------------------------------------------------------- */ - --sf-duration-none: 0ms; - --sf-duration-instant: calc(100ms * var(--sf-motion-scale)); - --sf-duration-fast: calc(150ms * var(--sf-motion-scale)); - --sf-duration-normal: calc(250ms * var(--sf-motion-scale)); - --sf-duration-slow: calc(400ms * var(--sf-motion-scale)); - --sf-duration-slower: calc(600ms * var(--sf-motion-scale)); - - --sf-ease-linear: linear; - --sf-ease-out: cubic-bezier(0.25, 0, 0.15, 1); - --sf-ease-in: cubic-bezier(0.5, 0, 0.75, 0.25); - --sf-ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); - --sf-ease-spring: linear(0, 0.5, 1.1, 0.95, 1.02, 1); - --sf-ease-elastic: linear(0, 0.3, 1.2, 0.9, 1.05, 1); - --sf-ease-bounce: linear(0, 0.35 18%, 1 32%, 0.86 42%, 1.02 56%, 0.98 72%, 1); - --sf-ease-overshoot: linear(0, 0.6 30%, 1.08 55%, 0.98 75%, 1); - - /* ---------------------------------------------------------- - Animation presets — pair keyframes (core/motion.css) with - duration + easing. Use directly: animation: var(--sf-animation-fade-in); - ---------------------------------------------------------- */ - --sf-animation-fade-in: sf-fade-in var(--sf-duration-normal) var(--sf-ease-out) both; - --sf-animation-fade-out: sf-fade-out var(--sf-duration-normal) var(--sf-ease-in) both; - --sf-animation-slide-in-up: sf-slide-in-up var(--sf-duration-normal) var(--sf-ease-out) both; - --sf-animation-slide-in-down: sf-slide-in-down var(--sf-duration-normal) var(--sf-ease-out) both; - --sf-animation-slide-in-left: sf-slide-in-left var(--sf-duration-normal) var(--sf-ease-out) both; - --sf-animation-slide-in-right: sf-slide-in-right var(--sf-duration-normal) var(--sf-ease-out) both; - --sf-animation-scale-up: sf-scale-up var(--sf-duration-normal) var(--sf-ease-overshoot) both; - --sf-animation-scale-down: sf-scale-down var(--sf-duration-normal) var(--sf-ease-in) both; - --sf-animation-color-pulse: sf-color-pulse var(--sf-duration-slow) var(--sf-ease-in-out) infinite; - --sf-animation-ping: sf-ping var(--sf-duration-slow) var(--sf-ease-out) infinite; - --sf-animation-blink: sf-blink calc(1s * var(--sf-motion-scale)) steps(1, end) infinite; - --sf-animation-float: sf-float calc(3s * var(--sf-motion-scale)) var(--sf-ease-in-out) infinite; - --sf-animation-spin: sf-spin var(--sf-duration-slower) linear infinite; - --sf-animation-shimmer: sf-shimmer calc(1.5s * var(--sf-motion-scale)) var(--sf-ease-in-out) infinite; - - /* Stagger delays — apply to successive items for entrance choreography - (e.g. style="animation-delay: var(--sf-animation-delay-2)"). Tokens - only; SLASHED ships no .sf-stagger utility class (by design — apply via token presets or your own BEM rules). */ - --sf-animation-delay-1: calc(75ms * var(--sf-motion-scale)); - --sf-animation-delay-2: calc(150ms * var(--sf-motion-scale)); - --sf-animation-delay-3: calc(225ms * var(--sf-motion-scale)); - --sf-animation-delay-4: calc(300ms * var(--sf-motion-scale)); - --sf-animation-delay-5: calc(375ms * var(--sf-motion-scale)); - - /* ---------------------------------------------------------- - Scroll-driven animation range — consumer API. Attach these - to your own scroll-timeline via animation-range. Not consumed - by the framework itself; intended for BEM component authors. - ---------------------------------------------------------- */ - --sf-scroll-timeline-range-start: entry 0%; - --sf-scroll-timeline-range-end: cover 30%; - - /* ---------------------------------------------------------- - Mask scrim — edge-fade stops for scroll reels / overflow. - Use in a mask-image gradient to fade content near an edge: - mask-image: linear-gradient(to right, - transparent 0, #000 var(--sf-mask-scrim-start), - #000 calc(100% - var(--sf-mask-scrim-end)), transparent 100%); - ---------------------------------------------------------- */ - --sf-mask-scrim-start: var(--sf-space-l); - --sf-mask-scrim-end: var(--sf-space-l); - - /* ---------------------------------------------------------- - Z-index - ---------------------------------------------------------- */ - --sf-z-below: -1; - --sf-z-base: 0; - --sf-z-raised: 1; - --sf-z-low: 10; - --sf-z-mid: 100; - --sf-z-high: 500; - --sf-z-top: 900; - --sf-z-max: 9999; - - /* Semantic z-index aliases — map UI roles onto the numeric ladder - above so component authors target intent, not magic integers. - Every role that can overlap another on the scroll plane gets its - OWN rung, ordered: raised < sticky < fixed < dropdown < toast. - Notably --sf-z-dropdown sits ABOVE sticky and fixed so a navbar - menu clears the sticky header it opens from (and any fixed chrome). - Modal / popover / tooltip are intentionally absent: SLASHED's - dialog & popover components render in the browser TOP LAYER (native - / [popover]), which is ordered above the page and its - ::backdrop independently of z-index — a z token there would be dead. - The --sf-z-overlay fallback is provided only for non-top-layer - implementations that still need to beat everything else. */ - --sf-z-sticky: var(--sf-z-low); /* 10 sticky headers / sidebars */ - --sf-z-fixed: var(--sf-z-mid); /* 100 fixed app chrome (navbar, FAB) */ - --sf-z-dropdown: var(--sf-z-high); /* 500 menus — clear sticky + fixed */ - --sf-z-toast: var(--sf-z-top); /* 900 transient notifications */ - --sf-z-overlay: var(--sf-z-max); /* 9999 non-top-layer modal/popover fallback */ - - /* ---------------------------------------------------------- - Layout & a11y - ---------------------------------------------------------- */ - /* Header height fluidly interpolates between the mobile and desktop - tokens across the framework viewport range (22.5rem → 90rem), the - same range used by the fluid type/space scales. Override either - endpoint to retune; set both equal for a fixed height. The slope - (0.0222…) matches the default 3.5rem→5rem delta; clamp() keeps the - result bounded to the endpoints even when they are overridden. - This mirrors the clamp the WP plugin's CSS generator emits. */ - --sf-header-height-mobile: 3.5rem; - --sf-header-height-desktop: 5rem; - --sf-header-height: clamp( - var(--sf-header-height-mobile), - calc(0.022222222222222223 * (100vw - 22.5rem) + var(--sf-header-height-mobile)), - var(--sf-header-height-desktop)); - --sf-sticky-offset-mobile: var(--sf-header-height-mobile); - --sf-sticky-offset-desktop: var(--sf-header-height-desktop); - --sf-sticky-offset: clamp( - var(--sf-sticky-offset-mobile), - calc(0.022222222222222223 * (100vw - 22.5rem) + var(--sf-sticky-offset-mobile)), - var(--sf-sticky-offset-desktop)); - --sf-focus-ring-width: 2px; - --sf-focus-ring-offset: 2px; - --sf-focus-ring-style: solid; - --sf-touch-target: var(--sf-size-l); - /* Global text-contrast knob. Nudges derived reading-text colours - toward the extremes (darker in light mode, lighter in dark mode). - Positive = more contrast; 0 = no change. Consumed by - --sf-color-text, --sf-color-text--secondary, --sf-color-heading. */ - --sf-contrast-bias: 0; - /* Lightness crossover for text-on-color auto-contrast. Colours with - L above this value get dark text; below get light text. Default 0.6 - guarantees ≥ 3:1 for most colours. Shift down (e.g. 0.55) if your - brand sits in the 0.52-0.60 range and you prefer white text on it. */ - --sf-contrast-threshold: 0.6; - - --sf-safe-top: env(safe-area-inset-top, 0px); - --sf-safe-bottom: env(safe-area-inset-bottom, 0px); - --sf-safe-left: env(safe-area-inset-left, 0px); - --sf-safe-right: env(safe-area-inset-right, 0px); - - /* ---------------------------------------------------------- - Print - ---------------------------------------------------------- */ - --sf-print-page-margin: 2cm; - --sf-print-page-size: a4; - --sf-print-base-size: 11pt; - - /* ---------------------------------------------------------- - Stroke widths - ---------------------------------------------------------- */ - --sf-stroke-thin: 1px; - --sf-stroke-regular: 1.5px; - --sf-stroke-bold: 2px; - --sf-stroke-heavy: 3px; - - /* ---------------------------------------------------------- - Object fit / position — global defaults for replaced elements. - Applied in core/base.css on img and video. Override per element - via inline style="--sf-object-position: top" or in BEM CSS. - ---------------------------------------------------------- */ - --sf-object-fit: cover; - --sf-object-position: 50% 50%; - - /* ---------------------------------------------------------- - CSS multi-column — tokens for the `columns` layout property. - column-width is ch-based (character units) so columns maintain - a comfortable reading measure regardless of font-size. - ---------------------------------------------------------- */ - --sf-col-width-s: 16ch; - --sf-col-width-m: 24ch; - --sf-col-width-l: 32ch; - --sf-col-rule-width-s: var(--sf-border-width-1); - --sf-col-rule-width-m: var(--sf-border-width-2); - --sf-col-rule-width-l: var(--sf-border-width-3); - - } - - /* ── @supports gate: shadow tokens ── */ - @supports (color: oklch(from red l c h)) { - :root { - - /* Shadow tint — near-black derived from the neutral; the tint simply - inherits the neutral's own chroma/hue, so a colourless neutral yields - colourless shadows. Forced dark in both modes; --sf-shadow-strength - handles dark-mode intensity. Override to a brand colour for explicitly - tinted shadows — keep it dark or you get bright slabs: - oklch(from var(--sf-color-primary) 0.15 c h). */ - --sf-shadow-color: oklch(from var(--sf-color-neutral) 0.15 c h); - - --sf-shadow-none: none; - --sf-shadow-xs: 0 1px 2px 0 oklch(from var(--sf-shadow-color) l c h / clamp(0, calc(var(--sf-shadow-strength) * 0.5), 0.7)); - --sf-shadow-s: 0 1px 2px 0 oklch(from var(--sf-shadow-color) l c h / clamp(0, calc(var(--sf-shadow-strength) * 0.5), 0.7)), - 0 2px 6px 0 oklch(from var(--sf-shadow-color) l c h / clamp(0, var(--sf-shadow-strength), 0.7)); - --sf-shadow-m: 0 1px 3px 0 oklch(from var(--sf-shadow-color) l c h / clamp(0, calc(var(--sf-shadow-strength) * 0.5), 0.7)), - 0 4px 12px 0 oklch(from var(--sf-shadow-color) l c h / clamp(0, calc(var(--sf-shadow-strength) * 2), 0.7)); - --sf-shadow-l: 0 2px 4px 0 oklch(from var(--sf-shadow-color) l c h / clamp(0, calc(var(--sf-shadow-strength) * 0.5), 0.7)), - 0 8px 24px 0 oklch(from var(--sf-shadow-color) l c h / clamp(0, calc(var(--sf-shadow-strength) * 3), 0.7)), - 0 16px 48px 0 oklch(from var(--sf-shadow-color) l c h / clamp(0, calc(var(--sf-shadow-strength) * 2), 0.7)); - --sf-shadow-xl: 0 2px 8px 0 oklch(from var(--sf-shadow-color) l c h / clamp(0, calc(var(--sf-shadow-strength) * 0.5), 0.7)), - 0 12px 36px 0 oklch(from var(--sf-shadow-color) l c h / clamp(0, calc(var(--sf-shadow-strength) * 3.5), 0.7)), - 0 24px 72px 0 oklch(from var(--sf-shadow-color) l c h / clamp(0, calc(var(--sf-shadow-strength) * 2.5), 0.7)); - --sf-shadow-2xl: 0 4px 12px 0 oklch(from var(--sf-shadow-color) l c h / clamp(0, calc(var(--sf-shadow-strength) * 0.6), 0.7)), - 0 20px 60px 0 oklch(from var(--sf-shadow-color) l c h / clamp(0, calc(var(--sf-shadow-strength) * 4), 0.7)), - 0 40px 100px -8px oklch(from var(--sf-shadow-color) l c h / clamp(0, calc(var(--sf-shadow-strength) * 5), 0.7)); - --sf-shadow-inner: inset 0 2px 4px 0 oklch(from var(--sf-shadow-color) l c h / clamp(0, calc(var(--sf-shadow-strength) * 2), 0.7)); - - /* ---------------------------------------------------------- - Text shadows — share --sf-shadow-color / --sf-shadow-strength - ---------------------------------------------------------- */ - --sf-text-shadow-none: none; - --sf-text-shadow-s: 0 1px 2px oklch(from var(--sf-shadow-color) l c h / clamp(0, calc(var(--sf-shadow-strength) * 1.5), 0.7)); - --sf-text-shadow-m: 0 2px 4px oklch(from var(--sf-shadow-color) l c h / clamp(0, calc(var(--sf-shadow-strength) * 2), 0.7)); - --sf-text-shadow-l: 0 4px 8px oklch(from var(--sf-shadow-color) l c h / clamp(0, calc(var(--sf-shadow-strength) * 2.5), 0.7)); - - /* Drop shadows — for filter: drop-shadow() (respects alpha edges) */ - --sf-drop-shadow-s: drop-shadow(0 1px 2px oklch(from var(--sf-shadow-color) l c h / clamp(0, calc(var(--sf-shadow-strength) * 1.5), 0.7))); - --sf-drop-shadow-m: drop-shadow(0 4px 6px oklch(from var(--sf-shadow-color) l c h / clamp(0, calc(var(--sf-shadow-strength) * 2), 0.7))); - --sf-drop-shadow-l: drop-shadow(0 8px 16px oklch(from var(--sf-shadow-color) l c h / clamp(0, calc(var(--sf-shadow-strength) * 2.5), 0.7))); - - } - } - - /* ============================================================ - TYPOGRAPHY ALIASES - ============================================================ */ - - :root { - --sf-body-font-size: var(--sf-text-m); - --sf-body-line-height: var(--sf-leading-normal); - --sf-body-font-weight: var(--sf-font-weight-body); - --sf-body-font-family: var(--sf-font-body); - --sf-body-color: var(--sf-color-text); - --sf-body-text-wrap: pretty; - --sf-body-strong-weight: var(--sf-font-weight-bold); - --sf-body-em-style: italic; - - --sf-code-font-size: 0.875em; - - --sf-heading-font-family: var(--sf-font-heading); - --sf-heading-color: var(--sf-color-heading); - --sf-heading-text-wrap: balance; - - --sf-h1-size: var(--sf-text-4xl); - --sf-h1-line-height: var(--sf-leading-tight); - --sf-h1-font-weight: var(--sf-font-weight-heading); - --sf-h1-letter-spacing: var(--sf-tracking-tight); - - --sf-h2-size: var(--sf-text-3xl); - --sf-h2-line-height: var(--sf-leading-tight); - --sf-h2-font-weight: var(--sf-font-weight-heading); - --sf-h2-letter-spacing: var(--sf-tracking-tight); - - --sf-h3-size: var(--sf-text-2xl); - --sf-h3-line-height: var(--sf-leading-snug); - --sf-h3-font-weight: var(--sf-font-weight-heading); - --sf-h3-letter-spacing: var(--sf-tracking-normal); - - --sf-h4-size: var(--sf-text-xl); - --sf-h4-line-height: var(--sf-leading-snug); - --sf-h4-font-weight: var(--sf-font-weight-heading); - --sf-h4-letter-spacing: var(--sf-tracking-normal); - - --sf-h5-size: var(--sf-text-l); - --sf-h5-line-height: var(--sf-leading-normal); - --sf-h5-font-weight: var(--sf-font-weight-heading); - --sf-h5-letter-spacing: var(--sf-tracking-normal); - - --sf-h6-size: var(--sf-text-m); - --sf-h6-line-height: var(--sf-leading-normal); - --sf-h6-font-weight: var(--sf-font-weight-heading); - --sf-h6-letter-spacing: var(--sf-tracking-wide); - - /* ---------------------------------------------------------- - Spacing aliases — BEM consumer API - Use in your own BEM classes: .nav { gap: var(--sf-gap); } - Source of truth for tokens.layout.css: - --sf-space-gap → var(--sf-gap) (all layout primitives) - --sf-space-content → var(--sf-content-gap) (stack, prose) - ---------------------------------------------------------- */ - --sf-gap: var(--sf-space-m); /* gap between BEM elements */ - --sf-content-gap: var(--sf-space-s); /* rhythm within a component */ - --sf-component-pad: var(--sf-space-m); /* standard component padding */ - --sf-field-block: var(--sf-space-l); /* form-field block spacing */ - --sf-field-required-marker: " *"; - /* Leading space is baked into the default so consumers can fully - disable the indicator with `--sf-link-external-marker: ""` — - without the space a bare "" still left a stray space after the - link text. Override with your own glyph (include leading space - if you want one): `:root { --sf-link-external-marker: " →" }`. */ - --sf-link-external-marker: " \2197"; /* " ↗" — consumed by .sf-link-external */ - - /* ---------------------------------------------------------- - Section padding - ---------------------------------------------------------- */ - --sf-section-pad: var(--sf-section-pad--m); - --sf-section-pad--xs: var(--sf-space-xl); - --sf-section-pad--s: var(--sf-space-2xl); - --sf-section-pad--m: var(--sf-space-3xl); - --sf-section-pad--l: var(--sf-space-4xl); - --sf-section-pad--xl: calc(var(--sf-space-4xl) * 1.5); - --sf-section-pad--2xl: calc(var(--sf-space-4xl) * 2); - - /* ---------------------------------------------------------- - Transition shorthands - - --sf-transition-all transitions ALL properties — convenient but - a performance footgun (forces the browser to watch every computed - value). It is the ONLY token here that uses `all`; reach for it - only as a deliberate escape hatch. Every other token below names - an explicit property set. - - The tempo presets (fast / slow / enter / exit) transition a curated - "common interactive + entrance" set — color, background-color, - border-color, box-shadow, opacity, transform, filter — covering what - buttons, cards, links, and entering elements realistically animate, - without watching every property. They differ from each other only in - duration + easing. (Changed in this release: they previously used - `all`, contradicting the warning above — see CHANGELOG.) - ---------------------------------------------------------- */ - --sf-transition-all: all var(--sf-duration-normal) var(--sf-ease-out); - --sf-transition-colors: - color var(--sf-duration-normal) var(--sf-ease-out), - background-color var(--sf-duration-normal) var(--sf-ease-out), - border-color var(--sf-duration-normal) var(--sf-ease-out), - text-decoration-color var(--sf-duration-normal) var(--sf-ease-out), - fill var(--sf-duration-normal) var(--sf-ease-out), - stroke var(--sf-duration-normal) var(--sf-ease-out); - --sf-transition-transform: transform var(--sf-duration-normal) var(--sf-ease-out); - --sf-transition-opacity: opacity var(--sf-duration-normal) var(--sf-ease-out); - --sf-transition-shadow: box-shadow var(--sf-duration-normal) var(--sf-ease-out); - - --sf-transition-fast: - color var(--sf-duration-fast) var(--sf-ease-out), - background-color var(--sf-duration-fast) var(--sf-ease-out), - border-color var(--sf-duration-fast) var(--sf-ease-out), - box-shadow var(--sf-duration-fast) var(--sf-ease-out), - opacity var(--sf-duration-fast) var(--sf-ease-out), - transform var(--sf-duration-fast) var(--sf-ease-out), - filter var(--sf-duration-fast) var(--sf-ease-out); - --sf-transition-slow: - color var(--sf-duration-slow) var(--sf-ease-in-out), - background-color var(--sf-duration-slow) var(--sf-ease-in-out), - border-color var(--sf-duration-slow) var(--sf-ease-in-out), - box-shadow var(--sf-duration-slow) var(--sf-ease-in-out), - opacity var(--sf-duration-slow) var(--sf-ease-in-out), - transform var(--sf-duration-slow) var(--sf-ease-in-out), - filter var(--sf-duration-slow) var(--sf-ease-in-out); - --sf-transition-enter: - color var(--sf-duration-normal) var(--sf-ease-out), - background-color var(--sf-duration-normal) var(--sf-ease-out), - border-color var(--sf-duration-normal) var(--sf-ease-out), - box-shadow var(--sf-duration-normal) var(--sf-ease-out), - opacity var(--sf-duration-normal) var(--sf-ease-out), - transform var(--sf-duration-normal) var(--sf-ease-out), - filter var(--sf-duration-normal) var(--sf-ease-out); - --sf-transition-exit: - color var(--sf-duration-fast) var(--sf-ease-in), - background-color var(--sf-duration-fast) var(--sf-ease-in), - border-color var(--sf-duration-fast) var(--sf-ease-in), - box-shadow var(--sf-duration-fast) var(--sf-ease-in), - opacity var(--sf-duration-fast) var(--sf-ease-in), - transform var(--sf-duration-fast) var(--sf-ease-in), - filter var(--sf-duration-fast) var(--sf-ease-in); - - /* Top-layer transition building block (dialog, [popover]). - transition-behavior: allow-discrete keeps the discrete `display` - and `overlay` properties in the transition so entry animations can - fire and exit animations finish before the element leaves the top - layer. Compose it into a component transition alongside the visual - properties: - transition: opacity var(--sf-duration-normal) var(--sf-ease-out), - translate var(--sf-duration-normal) var(--sf-ease-out), - var(--sf-transition-overlay); - The matching @starting-style rules are element-specific and ship with - the dialog/popover components (slashed.components). Progressive - enhancement — unsupporting engines simply show/hide instantly. - Chrome/Edge/Safari and Firefox (2025+) support allow-discrete. */ - --sf-transition-overlay: - overlay var(--sf-duration-normal) allow-discrete, - display var(--sf-duration-normal) allow-discrete; - } - -} - -/* ─── core/tokens.layout.css ─── */ -/* ============================================================ - SLASHED — core/tokens.layout.css - Semantic tokens for layout primitives. - Load before: core/layout.css - Layer: slashed.tokens - Prefix: --sf- - - DEPENDENCIES — requires tokens.css (loaded earlier). - Spacing aliases inherit from the BEM consumer API in tokens.css: - --sf-space-gap → var(--sf-gap) from tokens.css - --sf-space-content → var(--sf-content-gap) from tokens.css - Override --sf-gap or --sf-content-gap (the canonical source) to change - the gaps across all layout primitives at once. --sf-space-gap and - --sf-space-content are CANONICAL-SOURCE ALIASES — a second public name - kept for layout-primitive readability, not a redundant indirection. - - PUBLIC API — every --sf-*-gap / --sf-*-padding / --sf-*-min / etc. here - is a per-primitive override knob (e.g. style="--sf-cluster-gap: …"). - No INTERNAL tokens live in this file. - - Each layout primitive (cluster, grid, sidebar…) has its own - tokens (--sf-cluster-gap etc.) for local overrides. - ============================================================ */ - -@layer slashed.tokens { - :root { - - /* ---------------------------------------------------------- - Shared spacing aliases - ---------------------------------------------------------- */ - --sf-space-gap: var(--sf-gap); /* component-level gap */ - --sf-space-content: var(--sf-content-gap); /* within-component rhythm */ - - /* ---------------------------------------------------------- - Stack - ---------------------------------------------------------- */ - --sf-stack-gap: var(--sf-space-content); - - /* ---------------------------------------------------------- - Gap - ---------------------------------------------------------- */ - --sf-gap-size: var(--sf-space-gap); - - /* ---------------------------------------------------------- - Box - ---------------------------------------------------------- */ - --sf-box-padding: var(--sf-space-m); - --sf-box-border-width: 0; - --sf-box-border-color: var(--sf-color-border); - - /* ---------------------------------------------------------- - Center - ---------------------------------------------------------- */ - --sf-center-max: var(--sf-container-default); - --sf-center-gutter: var(--sf-space-gutter); - - /* ---------------------------------------------------------- - Cluster - ---------------------------------------------------------- */ - --sf-cluster-gap: var(--sf-space-gap); - --sf-cluster-align: center; - --sf-cluster-justify: flex-start; - - /* ---------------------------------------------------------- - Sidebar - ---------------------------------------------------------- */ - --sf-sidebar-gap: var(--sf-space-gap); - --sf-sidebar-min-width: 50%; /* content column minimum */ - --sf-sidebar-width: 18rem; /* sidebar panel default size */ - - /* ---------------------------------------------------------- - Switcher - ---------------------------------------------------------- */ - --sf-switcher-threshold: 30rem; - --sf-switcher-gap: var(--sf-space-gap); - - /* ---------------------------------------------------------- - Grid (auto-fill, breakpoint-free) - ---------------------------------------------------------- */ - --sf-grid-min: 16rem; - --sf-grid-gap: var(--sf-space-gap); - - --sf-grid-min-xs: 10rem; - --sf-grid-min-s: 13rem; - --sf-grid-min-m: 16rem; - --sf-grid-min-l: 20rem; - --sf-grid-min-xl: 24rem; - --sf-grid-min-2xl: 28rem; - - /* ---------------------------------------------------------- - Equal columns (fixed, non-responsive) - ---------------------------------------------------------- */ - --sf-equal-cols: 2; - --sf-equal-gap: var(--sf-space-gap); - - /* ---------------------------------------------------------- - Cover - ---------------------------------------------------------- */ - --sf-cover-min-height: 100dvh; - --sf-cover-padding: var(--sf-section-pad); - - /* ---------------------------------------------------------- - Frame - ---------------------------------------------------------- */ - --sf-frame-ratio: 16 / 9; /* literal , valid in aspect-ratio */ - - /* ---------------------------------------------------------- - Icon (boxed variant) — extras for .sf-icon--boxed. - The naked .sf-icon sizing tokens (--sf-icon-xs..xl) live in - core/tokens.css alongside the typography scale. - ---------------------------------------------------------- */ - --sf-icon-box-pad: 0.5em; - --sf-icon-box-radius: var(--sf-radius-s); - --sf-icon-box-bg: var(--sf-color-inset); - --sf-icon-box-border: var(--sf-border-width-1) solid var(--sf-color-border); - - /* ---------------------------------------------------------- - Reel - ---------------------------------------------------------- */ - --sf-reel-item-width: max-content; - --sf-reel-gap: var(--sf-space-gap); - --sf-reel-height: auto; - - /* ---------------------------------------------------------- - Imposter (centered absolute/fixed overlay) - ---------------------------------------------------------- */ - --sf-imposter-margin: var(--sf-space-m); - - /* ---------------------------------------------------------- - Bento grid - ---------------------------------------------------------- */ - --sf-bento-cols-default: 3; - --sf-bento-row-default: 10rem; - --sf-bento-row-compact: 6rem; - --sf-bento-row-tall: 16rem; - --sf-bento-gap: var(--sf-space-gap); - - /* ---------------------------------------------------------- - Content grid (breakout pattern) - ---------------------------------------------------------- */ - --sf-breakout-width: var(--sf-container-wide); - --sf-content-width: var(--sf-container-default); - - /* ---------------------------------------------------------- - Prose - ---------------------------------------------------------- */ - --sf-prose-paragraph: var(--sf-space-content); - - /* ---------------------------------------------------------- - Alternate (zigzag) - ---------------------------------------------------------- */ - --sf-alternate-gap: var(--sf-space-content); - --sf-alternate-inner-gap: var(--sf-space-gap); - - } -} - -/* ─── core/tokens.macros.css ─── */ -/* ============================================================ - SLASHED — core/tokens.macros.css - Semantic tokens for macros (recipes / patterns). - Load before: core/macros.css - Layer: slashed.tokens - Prefix: --sf- - - DEPENDENCIES — requires tokens.css (loaded earlier). - Macro tokens reference --sf-space-* / --sf-radius-* / etc. - - PUBLIC API — every token here is a per-recipe override knob - (e.g. style="--sf-line-clamp: 5"). No INTERNAL tokens live in - this file. - - Architectural parallel — these tokens stand alongside - tokens.layout.css: same prefix, same shape, same status - (essential, public, override-friendly). - ============================================================ */ - -@layer slashed.tokens { - :root { - - /* ---------------------------------------------------------- - Flow — distance between flow children (lobotomized owl). - Consumed by .sf-flow > * + *. - ---------------------------------------------------------- */ - --sf-flow-space: var(--sf-space-content); - - /* ---------------------------------------------------------- - Line clamp — default line count for .sf-line-clamp-N. - Override per element: style="--sf-line-clamp: 5". - The fixed-N variants .sf-line-clamp-2 / -3 don't read this - token (they hardcode the value), so the default only - applies to .sf-line-clamp-N callers. - ---------------------------------------------------------- */ - --sf-line-clamp: 3; - - /* ---------------------------------------------------------- - Truncate — ellipsis character for .sf-truncate. - Stays as a token so consumers can swap to ASCII "..." or a - localised character without rewriting the rule. The CSS - text-overflow: ellipsis property doesn't read it directly; - it's exposed for consumer-built variants. - ---------------------------------------------------------- */ - --sf-truncate-suffix: "\2026"; - - /* ---------------------------------------------------------- - Aspect ratio — default for .sf-aspect (generic ratio container, - distinct from layout's .sf-frame which has its own --sf-frame-ratio). - ---------------------------------------------------------- */ - --sf-aspect: 16 / 9; - - /* ---------------------------------------------------------- - Prose — per-instance override knobs for .sf-prose. - All values default to system spacing/color tokens so prose - stays in sync with the global scale unless overridden. - - Override per-element: - style="--sf-prose-marker-color: var(--sf-color-secondary)" - ---------------------------------------------------------- */ - --sf-prose-heading-gap: var(--sf-space-s); - --sf-prose-list-gap: var(--sf-space-xs); - --sf-prose-block-margin: var(--sf-space-m); - --sf-prose-media-margin: var(--sf-space-m); - --sf-prose-media-radius: var(--sf-radius-m); - --sf-prose-figure-margin: var(--sf-space-l); - --sf-prose-marker-color: var(--sf-color-primary); - --sf-prose-figcaption-size: var(--sf-text-s); - - /* ---------------------------------------------------------- - Scroll shadow / overflow fade — mask gradient size. - Used by .sf-scroll-shadow (top+bottom) and .sf-overflow-fade - (end-edge horizontal fade). - ---------------------------------------------------------- */ - --sf-scroll-shadow-size: 2rem; - - /* ---------------------------------------------------------- - Content visibility — placeholder size for .sf-content-auto. - Feeds contain-intrinsic-size so offscreen sections reserve - space before their first render, keeping the scrollbar and - scroll position stable. Override per element: - style="--sf-content-intrinsic-size: 800px". - ---------------------------------------------------------- */ - --sf-content-intrinsic-size: 500px; - - /* ---------------------------------------------------------- - Scrim — darkening overlay for text-over-image legibility, - consumed by the .sf-scrim macro. The gradient is composed - from a direction + a color stop so consumers can retune - either independently: - style="--sf-scrim-color: oklch(0 0 0 / 0.75)" - style="--sf-scrim-direction: to top right" - --sf-scrim-gradient is the final composed value; override it - directly for a multi-stop or radial scrim. - ---------------------------------------------------------- */ - --sf-scrim-color: oklch(0 0 0 / 0.55); - --sf-scrim-direction: to top; - --sf-scrim-gradient: linear-gradient(var(--sf-scrim-direction), var(--sf-scrim-color), transparent); - /* Text-on-image legibility WITHOUT darkening the picture — a soft - shadow halo behind glyphs, consumed by .sf-text-protect. */ - --sf-scrim-text-shadow: 0 1px 3px oklch(0 0 0 / 0.6); - - } -} - -/* ─── core/reset.css ─── */ -/* SLASHED — core/reset.css - @layer slashed.reset - - Lightweight normalize: corrects cross-browser inconsistencies and - sets sensible modern baselines. Deliberately does NOT zero global - margins/padding, strip list markers, or change form-element font - inheritance — those are component-level concerns. This keeps the - reset composable with any third-party UI on the same page (WP admin - bar, page-builder chrome, cookie banners, etc.) without element- - specific exclusions. - - A heavier "full reset" (zeroing all margins/padding, stripping list - markers, etc.) is intentionally NOT shipped — it would conflict with - the third-party UI this reset is designed to coexist with. Note that - core/base.css already sets the most useful of those defaults on its - own terms (media as display:block with capped width, form controls - inheriting font/color). Author any further reset in your own CSS. - ─────────────────────────────────────────────────── */ - -@layer slashed.reset { - - /* Box-sizing — near-universal baseline, safe to set globally. - Does not affect layout of third-party UI that explicitly - overrides it; UA agents and most WP/builder stylesheets do not - set box-sizing, so this wins by default but harmlessly. */ - *, *::before, *::after { - box-sizing: border-box; - } - - html { - /* stylelint-disable-next-line property-no-vendor-prefix -- Safari <17: unprefixed text-size-adjust only landed in Safari 17 */ - -webkit-text-size-adjust: 100%; - text-size-adjust: 100%; - scroll-padding-top: var(--sf-header-height, 5rem); - scrollbar-gutter: stable; - color-scheme: var(--sf-color-scheme, light dark); - hanging-punctuation: first last; - } - - /* interpolate-size enables animations to/from intrinsic sizing - keywords (auto, min-content, max-content, fit-content). Shipped: - Chrome 129 (Aug 2024), Safari 18 (Sep 2024). Firefox: behind a - flag as of 2026-Q2. Wrapped in @supports so unsupporting engines - get clean progressive enhancement, not an unknown-declaration. */ - @supports (interpolate-size: allow-keywords) { - html { interpolate-size: allow-keywords; } - } - - body { - min-height: 100dvh; - min-inline-size: 320px; - } - - /* Normalize button text-transform — Chrome/Edge capitalize UA rule. */ - button { - text-transform: none; - } - - /* is a block element per spec; Safari ≤16 renders it inline. */ - search { - display: block; - } - - /* summary cursor: normalize across browsers. */ - summary { - cursor: pointer; - } - - /* table: collapse borders (UA default varies). */ - table { - border-collapse: collapse; - } - - /* [hidden] polyfill — flex/grid containers ignore the attribute in - some older engines without this. */ - [hidden] { - display: none !important; - } - - /* dialog/popover: normalize padding/border differences. - background uses Canvas (UA system color) so the surface adapts to - color-scheme and forced-colors without hardcoding a hex value. */ - dialog, [popover] { - padding: 0; - border: none; - background: Canvas; - color: inherit; - } - - /* fieldset: min-width:0 prevents overflow in flex/grid containers - (genuine browser bug, not an opinion). */ - fieldset { - min-width: 0; - } - - [inert] { - cursor: default; - } - -} - -/* ─── core/base.css ─── */ -/* SLASHED — core/base.css - @layer slashed.base - ─────────────────────────────────────────────────── */ - -@layer slashed.base { - - /* Dark/light mode lives in core/themes.css (slashed.themes layer). - The baseline color-scheme: var(--sf-color-scheme, light dark) is - set on html in core/reset.css. */ - - body { - margin: 0; /* UA default is 8px */ - font-family: var(--sf-body-font-family, var(--sf-font-body)); - font-size: var(--sf-body-font-size, var(--sf-text-m)); - line-height: var(--sf-body-line-height, var(--sf-leading-normal)); - font-weight: var(--sf-body-font-weight, var(--sf-font-weight-body)); - color: var(--sf-body-color, var(--sf-color-text)); - background-color: var(--sf-color-bg, #fff); - } - - h1, h2, h3, h4, h5, h6 { - margin: 0; /* UA adds top/bottom margins */ - font-family: var(--sf-heading-font-family, var(--sf-font-heading)); - line-height: var(--sf-leading-tight); - color: var(--sf-heading-color, var(--sf-color-heading)); - text-wrap: var(--sf-heading-text-wrap); - overflow-wrap: break-word; - text-rendering: optimizelegibility; - scroll-margin-top: calc(var(--sf-header-height, 5rem) + var(--sf-space-m)); - } - - h1 { font-size: var(--sf-h1-size); line-height: var(--sf-h1-line-height); font-weight: var(--sf-h1-font-weight); letter-spacing: var(--sf-h1-letter-spacing); } - h2 { font-size: var(--sf-h2-size); line-height: var(--sf-h2-line-height); font-weight: var(--sf-h2-font-weight); letter-spacing: var(--sf-h2-letter-spacing); } - h3 { font-size: var(--sf-h3-size); line-height: var(--sf-h3-line-height); font-weight: var(--sf-h3-font-weight); letter-spacing: var(--sf-h3-letter-spacing); } - h4 { font-size: var(--sf-h4-size); line-height: var(--sf-h4-line-height); font-weight: var(--sf-h4-font-weight); letter-spacing: var(--sf-h4-letter-spacing); } - h5 { font-size: var(--sf-h5-size); line-height: var(--sf-h5-line-height); font-weight: var(--sf-h5-font-weight); letter-spacing: var(--sf-h5-letter-spacing); } - h6 { font-size: var(--sf-h6-size); line-height: var(--sf-h6-line-height); font-weight: var(--sf-h6-font-weight); letter-spacing: var(--sf-h6-letter-spacing); } - - p { - margin: 0; /* UA adds margin-block: 1em */ - text-wrap: var(--sf-body-text-wrap); - overflow-wrap: break-word; - orphans: 2; - widows: 2; - } - - b, strong { - font-weight: var(--sf-body-strong-weight); - } - - em { - font-style: var(--sf-body-em-style); - } - - small { - font-size: var(--sf-text-s, 0.875em); - } - - mark { - background-color: var(--sf-color-mark-bg, yellow); - color: var(--sf-color-mark-text, black); - border-radius: var(--sf-radius-xs); - padding-inline: 0.2em; - } - - abbr[title] { - text-decoration: underline dotted; - cursor: help; - } - - kbd, samp { - font-family: var(--sf-font-mono); - font-size: inherit; - } - - sub, sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; - } - sup { top: -0.5em; } - sub { bottom: -0.25em; } - - /* `a:link` (specificity 0,1,1) rather than bare `a` (0,0,1) so the - author rule wins over WebKit's UA stylesheet rule - `a:link { color: -webkit-link; }`. The bare-`a` form silently - loses to the UA `:link` rule on unvisited anchors in WebKit, - which paints `-webkit-link` (≈ #00728f in dark mode) regardless - of whatever `--sf-color-link` resolves to — producing axe's - 3.12 : 1 dark-mode contrast failure that survived four attempts - to "fix" the link colour formula (PR #73, then three iterations - of PR #76). The colour formula was never the problem; the - selector specificity was. Hover / visited / active rules already - used pseudo-classes so they had matching specificity and were - never affected. */ - a:link { - color: var(--sf-color-link); - text-decoration-color: var(--sf-color-link--underline); - text-underline-offset: var(--sf-link-underline-offset); - text-decoration-thickness: var(--sf-link-underline-thickness); - text-underline-position: from-font; - transition: - color var(--sf-duration-fast) var(--sf-ease-out), - text-decoration-color var(--sf-duration-fast) var(--sf-ease-out); - } - a:hover { color: var(--sf-color-link--hover); text-decoration-color: currentcolor; } - a:visited { color: var(--sf-color-link--visited); } - a:active { color: var(--sf-color-link--active); } - a:not([class]) { text-decoration-skip-ink: auto; } - - a, code { - overflow-wrap: break-word; - } - - code { - font-family: var(--sf-font-mono); - font-size: var(--sf-code-font-size); - padding: 0.125em 0.3em; - border-radius: var(--sf-radius-xs); - background-color: var(--sf-color-code-bg); - color: var(--sf-color-code-text, inherit); - } - - pre { - font-family: var(--sf-font-mono); - font-size: var(--sf-text-s); - padding: var(--sf-space-m); - border-radius: var(--sf-radius-m); - background: var(--sf-color-code-block-bg, var(--sf-color-code-bg)); - color: var(--sf-color-code-block-text, inherit); - overflow-x: auto; - tab-size: 2; - hyphens: none; - } - pre code { - background: none; - color: inherit; - padding: 0; - font-size: inherit; - border-radius: 0; - } - - /* Media: block display prevents the UA inline baseline gap and - caps width to the container. Intentionally NOT in reset.css — - third-party UI (WP admin bar, builder chrome) uses inline SVG - icons that must remain inline. */ - img, picture, video, canvas { - display: block; - max-inline-size: 100%; - block-size: auto; - } - - /* object-fit only applies to replaced elements with intrinsic dimensions */ - img, video { - object-fit: var(--sf-object-fit); - object-position: var(--sf-object-position); - } - - iframe, embed, object { - display: block; - max-inline-size: 100%; - block-size: auto; - border: none; - } - - hr { - border: none; - border-block-start: var(--sf-divider-width) var(--sf-divider-style) var(--sf-divider-color); - } - - :target { - scroll-margin-top: calc(var(--sf-header-height, 5rem) + var(--sf-space-m)); - } - - ::selection { - background: var(--sf-color-selection-bg); - color: var(--sf-color-selection-text); - text-shadow: none; - } - - th, caption { - text-align: start; - } - - input, textarea { - caret-color: var(--sf-caret-color); - } - - input, progress { - accent-color: var(--sf-color-action); - } - - ::backdrop { - background-color: var(--sf-color-dim); - } - - /* Form elements: normalize font/color/cursor behaviour. - UA stylesheets intentionally break font/color inheritance for - form controls; these rules restore conventional inheritance. - Scoped to base.css (not reset.css) so third-party form UI that - ships its own unlayered stylesheet retains cascade priority. */ - button, input, select, textarea { - font: inherit; - color: inherit; - letter-spacing: inherit; - color-scheme: inherit; - line-height: inherit; - } - button { - cursor: pointer; - } - a, button, input, select, textarea, summary { - touch-action: manipulation; - } - textarea { - resize: vertical; - } - - optgroup { font: inherit; } - ::placeholder { opacity: 1; color: var(--sf-color-text--placeholder, currentcolor); } - - /* Minimal rich-block defaults — just enough to distinguish from

. - Full styling lives in .sf-prose (core/layout.css). */ - blockquote { - border-inline-start: var(--sf-border-width-3) var(--sf-border-style) var(--sf-color-border); - padding-inline-start: var(--sf-space-m); - } - - dt { - font-weight: var(--sf-font-weight-heading); - } - - dd { - margin-inline-start: var(--sf-space-m); - } - - table { - text-align: start; - } - - th { - font-weight: var(--sf-font-weight-heading); - } - - td, th { - padding: var(--sf-space-2xs) var(--sf-space-xs); - border-block-end: var(--sf-border-width-hairline) var(--sf-border-style) var(--sf-color-border--subtle); - } - -} - -/* ─── core/themes.css ─── */ -/* SLASHED — core/themes.css - @layer slashed.themes - - Theme switching: token reassignments only. No new selectors, - no new properties — strictly remaps existing tokens based on - system preference and explicit data-theme attributes. - - Cascade position is significant. slashed.themes sits ABOVE - slashed.{states, utilities, components, layout, base} (see - core/layers.css), so a theme's token reassignment cannot be - accidentally beaten by a component or utility rule of equal - specificity. Only consumer code in slashed.overrides (or - unlayered CSS) can override a theme. - - USAGE - → follows OS preference - → whole page dark -

→ only this section dark -
→ only this section light - - The base color-scheme: var(--sf-color-scheme, light dark) - declaration lives in core/reset.css and provides the baseline. - This layer flips it. - - MULTI-BRAND — scope a palette to any subtree by re-declaring the 6 - source tokens under a selector (e.g. [data-brand="x"]). See - optional/theme-example.css and docs/theming.md. - - THEME TRANSITION — toggling [data-theme] via document.startViewTransition() - animates through the ::view-transition(root) rules in core/motion.css. - For a plain cross-fade of colours, opt into a transition class on - (example in optional/theme-example.css) — never transition every colour - globally and permanently (perf footgun). - ─────────────────────────────────────────────────── */ - -@layer slashed.themes { - - /* OS preference — only when no data-theme is set on :root. - A section-level [data-theme] still wins via the explicit - selectors below (higher specificity within the same layer). */ - @media (prefers-color-scheme: dark) { - :root:not([data-theme]) { - color-scheme: dark; - --sf-is-dark: 1; - } - } - - /* Explicit theme — works on :root (whole page) or any element - (section-level theming). */ - [data-theme="light"] { color-scheme: light; --sf-is-dark: 0; } - [data-theme="dark"] { color-scheme: dark; --sf-is-dark: 1; } - - - /* LumLocker — opt-in via [data-lumlocker] on :root. - Locks the 5 brand colors (primary, secondary, tertiary, action, - neutral) to one shared OKLCH L value (--sf-lumlocker, - default 0.65 in core/tokens.css), keeping each color's own hue - and chroma. Useful when a brand picker produces colors with - wildly different L values that look mismatched in weight. - - Base is excluded by design — it is the source token for the - page-surface ladder, and locking it would distort the derived page - background and related surface levels. - - Default state: OFF. Without the attribute, every brand keeps its - own L. Add :root[data-lumlocker] (or set via JS) to enable. - - Per-color escape: override --sf-color-X under - :root[data-lumlocker] to opt that one color out: - :root[data-lumlocker] { --sf-color-primary: #yourColor; } - - The dark-side input falls back to -light when -dark isn't - explicitly set, so this works even without per-mode overrides. - - Requires CSS relative color syntax (Chrome 119+, Safari 16.4+, - Firefox 128+). Wrapped in @supports — unsupporting engines skip - the block silently; brand colors remain unmodified. */ - @supports (color: oklch(from red l c h)) { - :root[data-lumlocker] { - --sf-color-primary: light-dark( - oklch(from var(--sf-color-primary-light) var(--sf-lumlocker) c h), - oklch(from var(--sf-color-primary-dark, var(--sf-color-primary-light)) var(--sf-lumlocker) c h) - ); - --sf-color-secondary: light-dark( - oklch(from var(--sf-color-secondary-light) var(--sf-lumlocker) c h), - oklch(from var(--sf-color-secondary-dark, var(--sf-color-secondary-light)) var(--sf-lumlocker) c h) - ); - --sf-color-tertiary: light-dark( - oklch(from var(--sf-color-tertiary-light) var(--sf-lumlocker) c h), - oklch(from var(--sf-color-tertiary-dark, var(--sf-color-tertiary-light)) var(--sf-lumlocker) c h) - ); - --sf-color-action: light-dark( - oklch(from var(--sf-color-action-light) var(--sf-lumlocker) c h), - oklch(from var(--sf-color-action-dark, var(--sf-color-action-light)) var(--sf-lumlocker) c h) - ); - --sf-color-neutral: light-dark( - oklch(from var(--sf-color-neutral-light) var(--sf-lumlocker) c h), - oklch(from var(--sf-color-neutral-dark, var(--sf-color-neutral-light)) var(--sf-lumlocker) c h) - ); - } - } /* end @supports (color: oklch(from red l c h)) */ - - /* ───────────────────────────────────────────────────────────────── - SECTION-LEVEL THEMING — full token re-declaration - ───────────────────────────────────────────────────────────────── - `light-dark()` resolves when a property is *declared* (on :root), - not when it is *inherited*. Setting `color-scheme: dark` on a - child element changes native UI controls and scrollbars but does - NOT cause any inherited custom-property `light-dark()` value to - re-evaluate — those values were already baked when :root computed - them. - - The blocks below fix this by re-declaring every mode-sensitive - token directly on [data-theme] elements, using the explicit - light or dark formula rather than light-dark(). This makes - headers, sections, cards, or any subtree with [data-theme] fully - independent of the root's colour mode. - - Requires relative colour syntax (Chrome 119+, Safari 16.4+, - Firefox 128+). Engines that don't support it skip the whole - @supports block and fall back to the :root values — a graceful - degradation with no visible breakage on older browsers. On :root - itself these rules are redundant (light-dark() already resolves - correctly) but harmless. - - Manual per-token overrides still apply — a consumer-set - [data-theme="dark"] { --sf-color-primary: red } placed in - slashed.overrides will win over these re-declarations. */ - - @supports (color: oklch(from red l c h)) { - - /* ── [data-theme="dark"] — explicit dark values ──────────── */ - [data-theme="dark"] { - - /* Brand — dark formula or explicit -dark override */ - --sf-color-primary: var(--sf-color-primary-dark, oklch(from var(--sf-color-primary-light) clamp(0.65, calc(0.95 - l * 0.5), 0.88) calc(c * 0.9) h)); - --sf-color-secondary: var(--sf-color-secondary-dark, oklch(from var(--sf-color-secondary-light) clamp(0.65, calc(0.95 - l * 0.5), 0.88) calc(c * 0.9) h)); - --sf-color-tertiary: var(--sf-color-tertiary-dark, oklch(from var(--sf-color-tertiary-light) clamp(0.65, calc(0.95 - l * 0.5), 0.88) calc(c * 0.9) h)); - --sf-color-action: var(--sf-color-action-dark, oklch(from var(--sf-color-action-light) clamp(0.65, calc(0.95 - l * 0.5), 0.88) calc(c * 0.9) h)); - --sf-color-neutral: var(--sf-color-neutral-dark, oklch(from var(--sf-color-neutral-light) clamp(0.65, calc(0.95 - l * 0.5), 0.88) calc(c * 0.9) h)); - --sf-color-base: var(--sf-color-base-dark, oklch(from var(--sf-color-base-light) clamp(0.16, calc(1.18 - l), 0.24) calc(c * 0.5) h)); - --sf-color-surface: var(--sf-color-base); - - /* Status — dark formula or explicit -dark override */ - --sf-color-success: var(--sf-color-success-dark, oklch(from var(--sf-color-success-light) clamp(0.65, calc(0.95 - l * 0.5), 0.88) calc(c * 0.9) h)); - --sf-color-warning: var(--sf-color-warning-dark, oklch(from var(--sf-color-warning-light) clamp(0.65, calc(0.95 - l * 0.5), 0.88) calc(c * 0.9) h)); - --sf-color-error: var(--sf-color-error-dark, oklch(from var(--sf-color-error-light) clamp(0.65, calc(0.95 - l * 0.5), 0.88) calc(c * 0.9) h)); - --sf-color-info: var(--sf-color-info-dark, oklch(from var(--sf-color-info-light) clamp(0.65, calc(0.95 - l * 0.5), 0.88) calc(c * 0.9) h)); - --sf-color-danger: var(--sf-color-danger-dark, oklch(from var(--sf-color-danger-light) clamp(0.65, calc(0.95 - l * 0.5), 0.88) calc(c * 0.9) h)); - - /* Text — dark formula (references --sf-color-neutral re-declared above) */ - --sf-color-text: oklch(from var(--sf-color-neutral) clamp(0.70, calc(l + 0.25 + var(--sf-contrast-bias)), 1) c h); - --sf-color-text--secondary: oklch(from var(--sf-color-neutral) clamp(0.55, calc(l + 0.1 + var(--sf-contrast-bias)), 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) clamp(0.05, calc(l - 0.4), 0.35) c h); - --sf-color-heading: oklch(from var(--sf-color-neutral) clamp(0.70, calc(l + 0.25 + var(--sf-contrast-bias)), 1) c h); - - /* Borders — dark formula */ - --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); - - /* Links — dark formula */ - --sf-color-link: oklch(from var(--sf-color-action) clamp(0.68, l, 1) c h); - --sf-color-link--hover: oklch(from var(--sf-color-action) clamp(0, max(l + 0.10, 0.68), 1) c h); - --sf-color-link--active: oklch(from var(--sf-color-action) clamp(0, max(l + 0.15, 0.74), 1) c h); - --sf-color-link--visited: oklch(from var(--sf-color-action) clamp(0.68, l, 1) c calc(h + 60)); - - /* Selection */ - --sf-color-selection-bg: oklch(from var(--sf-color-action-light) clamp(0.62, calc(0.93 - l * 0.4), 0.78) c h / 0.55); - - /* Status — strong variants (dark: push lightness up) */ - --sf-color-success-strong: oklch(from var(--sf-color-success) clamp(0.70, calc(l + 0.15), 1) c h); - --sf-color-warning-strong: oklch(from var(--sf-color-warning) clamp(0.70, calc(l + 0.05), 1) c h); - --sf-color-error-strong: oklch(from var(--sf-color-error) clamp(0.70, calc(l + 0.15), 1) c h); - --sf-color-info-strong: oklch(from var(--sf-color-info) clamp(0.70, calc(l + 0.15), 1) c h); - --sf-color-danger-strong: oklch(from var(--sf-color-danger) clamp(0.70, calc(l + 0.15), 1) c h); - } - - /* ── [data-theme="light"] — explicit light values ─────────── */ - [data-theme="light"] { - - /* Brand — explicit light values */ - --sf-color-primary: var(--sf-color-primary-light); - --sf-color-secondary: var(--sf-color-secondary-light); - --sf-color-tertiary: var(--sf-color-tertiary-light); - --sf-color-action: var(--sf-color-action-light); - --sf-color-neutral: var(--sf-color-neutral-light); - --sf-color-base: var(--sf-color-base-light); - --sf-color-surface: var(--sf-color-base); - - /* Status — explicit light values */ - --sf-color-success: var(--sf-color-success-light); - --sf-color-warning: var(--sf-color-warning-light); - --sf-color-error: var(--sf-color-error-light); - --sf-color-info: var(--sf-color-info-light); - --sf-color-danger: var(--sf-color-danger-light); - - /* Text — light formula */ - --sf-color-text: oklch(from var(--sf-color-neutral-light) clamp(0.05, calc(l - 0.4 - var(--sf-contrast-bias)), 0.35) c h); - --sf-color-text--secondary: oklch(from var(--sf-color-neutral-light) clamp(0.15, calc(l - 0.25 - var(--sf-contrast-bias)), 0.45) c h); - --sf-color-text--placeholder: oklch(from var(--sf-color-neutral-light) clamp(0.45, calc(l + 0.15), 0.75) c h); - --sf-color-text--disabled: oklch(from var(--sf-color-neutral-light) clamp(0.55, calc(l + 0.25), 0.82) c h); - --sf-color-text--inverse: oklch(from var(--sf-color-neutral-light) clamp(0.85, calc(l + 0.4), 0.98) c h); - --sf-color-heading: oklch(from var(--sf-color-neutral-light) clamp(0.05, calc(l - 0.4 - var(--sf-contrast-bias)), 0.35) c h); - - /* Borders — light formula */ - --sf-color-border: oklch(from var(--sf-color-neutral-light) clamp(0.70, calc(l + 0.35), 0.95) 0.005 h); - --sf-color-border--subtle: oklch(from var(--sf-color-neutral-light) clamp(0.75, calc(l + 0.4), 0.97) 0.005 h); - --sf-color-border--strong: oklch(from var(--sf-color-neutral-light) clamp(0.55, calc(l + 0.1), 0.85) 0.02 h); - - /* Links — light formula */ - --sf-color-link: oklch(from var(--sf-color-action) clamp(0, min(l - 0.07, 0.48), 1) c h); - --sf-color-link--hover: oklch(from var(--sf-color-action) clamp(0, min(l - 0.15, 0.40), 1) c h); - --sf-color-link--active: oklch(from var(--sf-color-action) clamp(0, min(l - 0.21, 0.34), 1) c h); - --sf-color-link--visited: oklch(from var(--sf-color-action) clamp(0, min(l - 0.07, 0.48), 1) c calc(h + 60)); - - /* Selection */ - --sf-color-selection-bg: oklch(from var(--sf-color-action-light) l c h / 0.28); - - /* Status — strong variants (light: push lightness down) */ - --sf-color-success-strong: oklch(from var(--sf-color-success-light) calc(l - 0.15) c h); - --sf-color-warning-strong: oklch(from var(--sf-color-warning-light) calc(l - 0.25) c h); - --sf-color-error-strong: oklch(from var(--sf-color-error-light) calc(l - 0.1) c h); - --sf-color-info-strong: oklch(from var(--sf-color-info-light) calc(l - 0.1) c h); - --sf-color-danger-strong: oklch(from var(--sf-color-danger-light) calc(l - 0.1) c h); - } - - } /* end @supports (color: oklch(from red l c h)) */ - -} - -/* ─── core/layout.css ─── */ -/* ============================================================ - SLASHED — core/layout.css - ★ de facto required for most projects - Layout primitives + structural patterns - Requires: core/tokens.css, core/tokens.layout.css - ============================================================ */ - -@layer slashed.layout { - - /* -- Section ------------------------------------------------- - Page section with vertical padding. Size modifiers - override the scoped token. - ---------------------------------------------------------- */ - - .sf-section { padding-block: var(--sf-section-pad); } - .sf-section--xs { padding-block: var(--sf-section-pad--xs); } - .sf-section--s { padding-block: var(--sf-section-pad--s); } - .sf-section--m { padding-block: var(--sf-section-pad--m); } - .sf-section--l { padding-block: var(--sf-section-pad--l); } - .sf-section--xl { padding-block: var(--sf-section-pad--xl); } - .sf-section--2xl { padding-block: var(--sf-section-pad--2xl); } - - .sf-section-group > .sf-section + .sf-section { padding-block-start: 0; } - - /* Collapse modifier — when two adjacent sections share this modifier, - they redistribute padding 50/50 across the boundary so total spacing - stays one full padding gap (instead of two). Works regardless of - background color: each section always has half its padding on - the boundary side, so content never touches a color edge. - - Use when sections are siblings outside a .sf-section-group wrapper - and may share OR differ in background. - - Note: uses --sf-section-pad (the canonical alias = m by default). - For other sizes, set --sf-section-pad locally on each section: -
- ---------------------------------------------------------- */ - .sf-section--collapse:has(+ .sf-section--collapse) { - padding-block-end: calc(var(--sf-section-pad) / 2); - } - .sf-section--collapse + .sf-section--collapse { - padding-block-start: calc(var(--sf-section-pad) / 2); - } - - /* -- Divider ------------------------------------------------- - Standalone separator primitive. Token-driven; pairs with the - `--sf-divider-*` tokens and mirrors the `hr` element default. - ---------------------------------------------------------- */ - .sf-divider { - border: none; - border-block-start: var(--sf-divider-width) var(--sf-divider-style) var(--sf-divider-color); - margin-block: var(--sf-divider-gap); - } - .sf-divider--vertical { - border-block-start: none; - border-inline-start: var(--sf-divider-width) var(--sf-divider-style) var(--sf-divider-color); - margin-block: 0; - margin-inline: var(--sf-divider-gap); - align-self: stretch; - } - /* Style modifiers — retune the line itself via the same tokens the - base rule reads, so they apply to horizontal and vertical alike. */ - .sf-divider--soft { --sf-divider-color: var(--sf-color-border--subtle); } - .sf-divider--strong { --sf-divider-color: var(--sf-color-border--strong); } - .sf-divider--dashed { --sf-divider-style: dashed; } - .sf-divider--dotted { --sf-divider-style: dotted; } - /* Gradient divider — a hairline that fades out at both ends. Replaces - the border with a masked background so it can't use border-style; - height tracks --sf-divider-width. Horizontal only. */ - .sf-divider--gradient { - border: none; - block-size: var(--sf-divider-width); - background: linear-gradient(to right, transparent, var(--sf-divider-color), transparent); - } - - /* -- Container ----------------------------------------------- - Max-width container, centered with side gutters. Establishes - a named inline-size container `sf-layout` so children can - target it precisely with `@container sf-layout (...)`. - The framework's own `.sf-bento` and `.sf-grid-*` rules use - anonymous `@container` queries on purpose — they react to - the nearest inline-size ancestor, which lets a consumer - embed them inside any container without rewiring. - ---------------------------------------------------------- */ - - .sf-container { - container: sf-layout / inline-size; - width: 100%; - max-width: var(--sf-container-default); - margin-inline: auto; - padding-inline: var(--sf-space-gutter); - } - .sf-container--narrow { max-width: var(--sf-container-narrow); } - .sf-container--prose { max-width: var(--sf-container-prose); } - .sf-container--wide { max-width: var(--sf-container-wide); } - .sf-container--full { max-width: var(--sf-container-full); } - - /* -- Stack --------------------------------------------------- - Flex column with even vertical spacing between children. - Override: style="--sf-stack-gap: 2rem" - ---------------------------------------------------------- */ - - .sf-stack { - display: flex; - flex-direction: column; - gap: var(--sf-stack-gap); - } - .sf-stack--xs { gap: var(--sf-space-xs); } - .sf-stack--s { gap: var(--sf-space-s); } - .sf-stack--m { gap: var(--sf-space-m); } - .sf-stack--l { gap: var(--sf-space-l); } - .sf-stack--xl { gap: var(--sf-space-xl); } - .sf-stack--2xl { gap: var(--sf-space-2xl); } - - .sf-stack--center { align-items: center; } - .sf-stack--end { align-items: flex-end; } - .sf-stack--stretch { align-items: stretch; } - - /* -- Gap ---------------------------------------------------- - Injects gap into any existing flex or grid container without - imposing a display type or axis. Unlike .sf-stack (flex - column) or .sf-cluster (flex-wrap row), .sf-gap is layout- - agnostic — use it on elements that already have their own - display context. - Override: style="--sf-gap-size: 2rem" - ---------------------------------------------------------- */ - .sf-gap { gap: var(--sf-gap-size); } - .sf-gap--xs { gap: var(--sf-space-xs); } - .sf-gap--s { gap: var(--sf-space-s); } - .sf-gap--m { gap: var(--sf-space-m); } - .sf-gap--l { gap: var(--sf-space-l); } - .sf-gap--xl { gap: var(--sf-space-xl); } - .sf-gap--2xl { gap: var(--sf-space-2xl); } - - /* -- Box ----------------------------------------------------- - Isolated unit with padding and an optional border. - Outline instead of border — doesn't disturb the box model when toggled. - Override: style="--sf-box-padding: 1rem; --sf-box-border-width: 1px" - ---------------------------------------------------------- */ - - .sf-box { - padding: var(--sf-box-padding); - outline: var(--sf-box-border-width) solid var(--sf-box-border-color); - } - - /* -- Center -------------------------------------------------- - Centers content with a max-inline-size and side gutters. - content-box: max-inline-size applies to content only; padding is additive. - Override: style="--sf-center-max: 60rem" - ---------------------------------------------------------- */ - - .sf-center { - box-sizing: content-box; - margin-inline: auto; - max-inline-size: var(--sf-center-max); - padding-inline: var(--sf-center-gutter); - } - - .sf-center--intrinsic { - display: flex; - flex-direction: column; - align-items: center; - } - - /* -- Cluster ------------------------------------------------- - Flex-wrap group of variable-width items. - Override: style="--sf-cluster-gap: 0.5rem; --sf-cluster-justify: center" - ---------------------------------------------------------- */ - - .sf-cluster { - display: flex; - flex-wrap: wrap; - gap: var(--sf-cluster-gap); - align-items: var(--sf-cluster-align); - justify-content: var(--sf-cluster-justify); - } - .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); } - .sf-cluster--2xl { gap: var(--sf-space-2xl); } - - .sf-cluster--no-wrap { flex-wrap: nowrap; } - .sf-cluster--center { justify-content: center; } - .sf-cluster--end { justify-content: flex-end; } - .sf-cluster--between { justify-content: space-between; } - - /* -- Sidebar ------------------------------------------------- - Two-column layout: first-child = sidebar, last-child = main content. - Collapses when the viewport is too narrow (flex-basis trick). - Override: style="--sf-sidebar-width: 20rem" - ---------------------------------------------------------- */ - - .sf-sidebar { - display: flex; - flex-wrap: wrap; - gap: var(--sf-sidebar-gap); - } - - .sf-sidebar > :first-child { - flex-basis: var(--sf-sidebar-width); - flex-grow: 1; - } - - .sf-sidebar > :last-child { - flex-basis: 0; - flex-grow: 999; - min-inline-size: var(--sf-sidebar-min-width); - } - - .sf-sidebar--right > :first-child { - flex-basis: 0; - flex-grow: 999; - min-inline-size: var(--sf-sidebar-min-width); - } - - .sf-sidebar--right > :last-child { - flex-basis: var(--sf-sidebar-width); - flex-grow: 1; - } - - .sf-sidebar--narrow { --sf-sidebar-width: 12rem; } - .sf-sidebar--wide { --sf-sidebar-width: 26rem; } - - /* -- Switcher ------------------------------------------------ - Switches between a column (narrow viewport) and a row (wide). - Uses calc((threshold - 100%) * 999) — zero media queries. - Override: style="--sf-switcher-threshold: 40rem" - ---------------------------------------------------------- */ - - .sf-switcher { - display: flex; - flex-wrap: wrap; - gap: var(--sf-switcher-gap); - } - - .sf-switcher > * { - flex-grow: 1; - flex-basis: calc((var(--sf-switcher-threshold) - 100%) * 999); - } - - .sf-switcher--no-wrap { flex-wrap: nowrap; overflow-x: auto; } - .sf-switcher--vertical { flex-direction: column; } - - /* -- Grid ---------------------------------------------------- - Auto-fill CSS grid with no breakpoints. .sf-grid--fit switches to auto-fit. - auto-fill: empty tracks preserved, last item doesn't stretch. - auto-fit (.sf-grid--fit): empty tracks collapsed, items stretch to fill the gap. - Override: style="--sf-grid-min: 12rem; --sf-grid-gap: 1rem" - ---------------------------------------------------------- */ - - .sf-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(min(var(--sf-grid-min), 100%), 1fr)); - gap: var(--sf-grid-gap); - } - - .sf-grid--fit { - grid-template-columns: repeat(auto-fit, minmax(min(var(--sf-grid-min), 100%), 1fr)); - } - - .sf-grid--xs { --sf-grid-min: var(--sf-grid-min-xs); } - .sf-grid--s { --sf-grid-min: var(--sf-grid-min-s); } - .sf-grid--m { --sf-grid-min: var(--sf-grid-min-m); } - .sf-grid--l { --sf-grid-min: var(--sf-grid-min-l); } - .sf-grid--xl { --sf-grid-min: var(--sf-grid-min-xl); } - .sf-grid--2xl { --sf-grid-min: var(--sf-grid-min-2xl); } - - /* Dense packing — let later items backfill earlier gaps (as .sf-bento - does). Applies to any grid, including the fixed-column .sf-grid-N. */ - .sf-grid--dense { grid-auto-flow: dense; } - - /* -- Icon ---------------------------------------------------- - Inline icon sizing primitive. Uses em-based --sf-icon-* tokens - so an icon scales with its surrounding text. Defaults to -m. - ---------------------------------------------------------- */ - .sf-icon { - display: inline-block; - inline-size: var(--sf-icon-size, var(--sf-icon-m)); - block-size: var(--sf-icon-size, var(--sf-icon-m)); - flex-shrink: 0; - vertical-align: text-bottom; - fill: currentcolor; - } - .sf-icon--xs { --sf-icon-size: var(--sf-icon-xs); } - .sf-icon--s { --sf-icon-size: var(--sf-icon-s); } - .sf-icon--m { --sf-icon-size: var(--sf-icon-m); } - .sf-icon--l { --sf-icon-size: var(--sf-icon-l); } - .sf-icon--xl { --sf-icon-size: var(--sf-icon-xl); } - .sf-icon--2xl { --sf-icon-size: var(--sf-icon-2xl); } - - /* Boxed variant — wraps the icon in a padded, bordered, optionally - coloured frame. The declared inline-/block-size is the OUTER size - (icon + 2× pad); border-box ensures padding is subtracted inward so - the glyph content area stays at exactly --sf-icon-size. - */ - .sf-icon--boxed { - inline-size: calc(var(--sf-icon-size, var(--sf-icon-m)) + var(--sf-icon-box-pad) * 2); - block-size: calc(var(--sf-icon-size, var(--sf-icon-m)) + var(--sf-icon-box-pad) * 2); - padding: var(--sf-icon-box-pad); - border: var(--sf-icon-box-border); - border-radius: var(--sf-icon-box-radius); - background: var(--sf-icon-box-bg); - box-sizing: border-box; /* padding is contained within the declared outer size */ - } - - /* -- Cover --------------------------------------------------- - Full-height flex column. A .sf-cover__center child is - vertically centered via margin-block: auto. - Override: style="--sf-cover-min-height: 50dvh" - ---------------------------------------------------------- */ - - .sf-cover { - display: flex; - flex-direction: column; - min-height: var(--sf-cover-min-height); - padding-block: var(--sf-cover-padding); - } - - .sf-cover > .sf-cover__center { - margin-block: auto; - } - - .sf-cover--min { min-height: 50dvh; } - /* --max intentionally sets min-height: 0 to override .sf-cover's token-driven - minimum — the cover may collapse below the default min if content is short. - Pair with padding-block or a min-content guard if collapsing is undesirable. */ - .sf-cover--max { min-block-size: 0; max-block-size: 100dvh; } - .sf-cover--padding-s { padding-block: var(--sf-space-xl); } - .sf-cover--padding-l { padding-block: var(--sf-space-4xl); } - - /* -- Frame --------------------------------------------------- - Aspect-ratio container. Media children: fill + cover. - Override: style="--sf-frame-ratio: 4 / 3" - ---------------------------------------------------------- */ - - .sf-frame { - aspect-ratio: var(--sf-frame-ratio); - overflow: hidden; - display: flex; - justify-content: center; - align-items: center; - } - - .sf-frame > img, - .sf-frame > video { - inline-size: 100%; - block-size: 100%; - object-fit: cover; - } - - .sf-frame--square { aspect-ratio: var(--sf-ratio-square); } - .sf-frame--video { aspect-ratio: var(--sf-ratio-video); } - .sf-frame--cinema { aspect-ratio: var(--sf-ratio-cinema); } - .sf-frame--portrait { aspect-ratio: var(--sf-ratio-portrait); } - .sf-frame--4-3 { aspect-ratio: var(--sf-ratio-4-3); } - .sf-frame--3-2 { aspect-ratio: var(--sf-ratio-3-2); } - .sf-frame--golden { aspect-ratio: var(--sf-ratio-golden); } - - /* -- Reel ---------------------------------------------------- - Horizontal scroll with scroll-snap. Each child snaps to start. - Override: style="--sf-reel-gap: 1rem; --sf-reel-item-width: 280px" - ---------------------------------------------------------- */ - - .sf-reel { - display: flex; - block-size: var(--sf-reel-height); - gap: var(--sf-reel-gap); - overflow-x: auto; - scroll-snap-type: x mandatory; - scroll-padding-inline: var(--sf-reel-gap); - overscroll-behavior-x: contain; - scrollbar-width: thin; - } - - .sf-reel > * { - flex: 0 0 var(--sf-reel-item-width); - scroll-snap-align: start; - } - - /* -- Imposter ------------------------------------------------ - Element centered absolutely within its parent. - The parent must have position: relative. - ---------------------------------------------------------- */ - - .sf-imposter { - position: absolute; - inset-block-start: 50%; - left: 50%; /* physical: translate() uses physical x-axis; logical inset-inline-start: 50% maps to right:50% in RTL, which combined with translate(-50%) shifts away from center */ - translate: -50% -50%; - } - - .sf-imposter--fixed { - position: fixed; - } - - .sf-imposter--contain { - max-inline-size: calc(100% - var(--sf-imposter-margin) * 2); - max-block-size: calc(100% - var(--sf-imposter-margin) * 2); - overflow: auto; - } - - /* -- Alternate (zigzag / media-object) ----------------------- - Alternating two-column layouts (container-query responsive). - Every other row reverses its children's order. - - Establishes a named container `sf-alternate` and binds the - responsive query to that name — so a `.sf-alternate` nested - inside another container (e.g. `.sf-bento`) still flips on - its OWN width, not the outer container's. - ---------------------------------------------------------- */ - - .sf-alternate { - display: grid; - gap: var(--sf-alternate-gap); - container: sf-alternate / inline-size; - } - - .sf-alternate > * { - display: grid; - gap: var(--sf-alternate-inner-gap); - align-items: center; - } - - @container sf-alternate (min-width: 48em) { - .sf-alternate > * { - grid-template-columns: 1fr 1fr; - } - .sf-alternate > :nth-child(even) > :first-child { order: 2; } - .sf-alternate > :nth-child(even) > :last-child { order: 1; } - } - - /* -- Pancake (sticky footer) --------------------------------- - Grid with three rows: header / main(1fr) / footer. - ---------------------------------------------------------- */ - - .sf-pancake { - display: grid; - grid-template-rows: auto 1fr auto; - min-height: 100dvh; - } - - /* -- Fixed column grids (container-query responsive) --------- - Responsive fixed-column grids with no media queries. - - IMPORTANT: These classes use anonymous @container queries, so they - react to the nearest ancestor that has a container context - (container-type: inline-size). Without a container ancestor the - queries never match and the grid stays single-column at all widths. - Wrap in .sf-container (or any element with container-type: inline-size). - ---------------------------------------------------------- */ - - .sf-grid-1, .sf-grid-2, .sf-grid-3, .sf-grid-4, .sf-grid-6 { - display: grid; - gap: var(--sf-grid-gap); - } - - @container (min-width: 30em) { - .sf-grid-4 { grid-template-columns: repeat(2, 1fr); } - .sf-grid-6 { grid-template-columns: repeat(3, 1fr); } - } - - @container (min-width: 48em) { - .sf-grid-2 { grid-template-columns: repeat(2, 1fr); } - .sf-grid-3 { grid-template-columns: repeat(3, 1fr); } - .sf-grid-4 { grid-template-columns: repeat(4, 1fr); } - .sf-grid-6 { grid-template-columns: repeat(6, 1fr); } - } - - /* -- Ratio grids --------------------------------------------- - Two-column ratio grids, container-query responsive. - ---------------------------------------------------------- */ - - .sf-grid-1-2, .sf-grid-2-1, - .sf-grid-1-3, .sf-grid-3-1 { - display: grid; - gap: var(--sf-grid-gap); - } - - @container (min-width: 48em) { - .sf-grid-1-2 { grid-template-columns: 1fr 2fr; } - .sf-grid-2-1 { grid-template-columns: 2fr 1fr; } - .sf-grid-1-3 { grid-template-columns: 1fr 3fr; } - .sf-grid-3-1 { grid-template-columns: 3fr 1fr; } - } - - /* -- Equal columns ------------------------------------------ - Fixed N-column grid. Column count NEVER collapses — unlike - .sf-grid-N which uses container queries to stack at small - sizes. Use when the layout must remain N columns at all - widths; the caller is responsible for overflow handling. - Override: style="--sf-equal-cols: 3" - ---------------------------------------------------------- */ - .sf-equal { - display: grid; - grid-template-columns: repeat(var(--sf-equal-cols), 1fr); - gap: var(--sf-equal-gap); - } - .sf-equal--2 { --sf-equal-cols: 2; } - .sf-equal--3 { --sf-equal-cols: 3; } - .sf-equal--4 { --sf-equal-cols: 4; } - .sf-equal--6 { --sf-equal-cols: 6; } - - /* -- Content grid (breakout pattern) ------------------------- - Full-width grid that lets items break out to breakout or full - width. Children default to the content column. - ---------------------------------------------------------- */ - - .sf-content-grid { - display: grid; - grid-template-columns: - [full-start] - minmax(var(--sf-space-gutter), 1fr) - [breakout-start] - minmax(0, calc((var(--sf-breakout-width) - var(--sf-content-width)) / 2)) - [content-start] - min(var(--sf-content-width), 100% - var(--sf-space-gutter) * 2) - [content-end] - minmax(0, calc((var(--sf-breakout-width) - var(--sf-content-width)) / 2)) - [breakout-end] - minmax(var(--sf-space-gutter), 1fr) - [full-end]; - } - - .sf-content-grid > * { grid-column: content; } - .sf-content-grid > .sf-breakout { grid-column: breakout; } - .sf-content-grid > .sf-full-bleed { grid-column: full; } - - /* -- Bento grid ---------------------------------------------- - Free-form grid with auto-flow dense, responsive via CQ. - Override: style="--sf-bento-cols: 4; --sf-bento-row: 12rem" - ---------------------------------------------------------- */ - - .sf-bento { - display: grid; - grid-template-columns: repeat(var(--sf-bento-cols, var(--sf-bento-cols-default)), 1fr); - grid-auto-rows: minmax(var(--sf-bento-row, var(--sf-bento-row-default)), auto); - grid-auto-flow: dense; - gap: var(--sf-bento-gap); - } - - @container (max-width: 29.99em) { - .sf-bento { grid-template-columns: 1fr; } /* single column at smallest container sizes */ - } - - @container (min-width: 30em) and (max-width: 47.99em) { - .sf-bento { grid-template-columns: repeat(2, 1fr); } - } - - .sf-bento--2 { --sf-bento-cols: 2; } - .sf-bento--4 { --sf-bento-cols: 4; } - .sf-bento--compact { --sf-bento-row: var(--sf-bento-row-compact); } - .sf-bento--tall { --sf-bento-row: var(--sf-bento-row-tall); } - - /* -- Subgrid ------------------------------------------------- - A grid child that inherits its parent's tracks. - ---------------------------------------------------------- */ - - .sf-subgrid { display: grid; grid-template-columns: subgrid; } - .sf-subgrid-rows { display: grid; grid-template-rows: subgrid; } - -} - -/* ─── core/macros.css ─── */ -/* ============================================================ - SLASHED — core/macros.css - ★ shipped in essential bundle - Macro-classes (recipes / patterns). - Requires: core/tokens.css, core/tokens.macros.css - Layer: slashed.macros - - Macro-classes vs layout primitives: - · Layout primitive = answers "where do my children go?" - (e.g. .sf-stack, .sf-grid, .sf-cluster) - · Macro-class = answers "what does this element DO or - LOOK LIKE?" (e.g. .sf-prose, .sf-flow, .sf-truncate) - - Cascade position: - layout → components → macros → utilities → states → … - Macros may compose with primitives and components, but a - single-property utility still wins on the same selector. - - PUBLIC API — every class here is part of the public surface. - Tokens consumed by these classes are listed in - core/tokens.macros.css. - ============================================================ */ - -@layer slashed.macros { - - /* -- Prose --------------------------------------------------- - Long-form text column with automatic vertical rhythm. - .sf-not-prose resets styles inside a prose block. - ---------------------------------------------------------- */ - - .sf-prose { - max-width: var(--sf-container-prose); - overflow-wrap: break-word; - } - - .sf-prose > * + * { margin-block-start: var(--sf-prose-paragraph); } - .sf-prose :is(h2, h3, h4) + * { margin-block-start: var(--sf-prose-heading-gap); } - .sf-prose li + li { margin-block-start: var(--sf-prose-list-gap); } - .sf-prose :is(blockquote, pre) { margin-block: var(--sf-prose-block-margin); } - .sf-prose blockquote { font-style: italic; } - .sf-prose img { margin-block: var(--sf-prose-media-margin); border-radius: var(--sf-prose-media-radius); } - .sf-prose :is(ul, ol) { padding-inline-start: 1.25em; } - .sf-prose ul { list-style: disc; } - .sf-prose ol { list-style: decimal; } - .sf-prose ::marker { color: var(--sf-prose-marker-color); } - .sf-prose figure { margin-block: var(--sf-prose-figure-margin); } - .sf-prose figcaption { font-size: var(--sf-prose-figcaption-size); color: var(--sf-color-text--muted); margin-block-start: var(--sf-prose-list-gap); } - .sf-prose table { margin-block: var(--sf-prose-block-margin); display: block; overflow-x: auto; width: 100%; max-width: 100%; } - .sf-prose video { margin-block: var(--sf-prose-media-margin); border-radius: var(--sf-prose-media-radius); } - - /* .sf-not-prose resets — specificity 0-2-1 intentionally exceeds the - general 0-2-0 budget because these rules must override the .sf-prose - child selectors above (also 0-2-0). This is a documented exception. */ - .sf-prose .sf-not-prose > * + * { margin-block-start: 0; } - .sf-prose .sf-not-prose li + li { margin-block-start: 0; } - /* `revert` in a cascade layer rolls back past all layers to the UA - stylesheet — restoring browser-default list styling. In the flat - bundle (layers stripped), `revert` falls back to the UA stylesheet - directly. Both paths produce the same result: native list bullets. */ - .sf-prose .sf-not-prose :is(ul,ol) { list-style: revert; padding-inline-start: revert; } - .sf-prose .sf-not-prose ::marker { color: inherit; } - .sf-prose .sf-not-prose img { margin-block: 0; border-radius: 0; } - .sf-prose .sf-not-prose figure { margin-block: 0; } - .sf-prose .sf-not-prose figcaption { font-size: inherit; color: inherit; margin-block-start: 0; } - .sf-prose .sf-not-prose table { margin-block: 0; display: revert; overflow-x: revert; max-width: revert; } - .sf-prose .sf-not-prose video { margin-block: 0; border-radius: 0; } - - /* -- Flow ---------------------------------------------------- - Heydon Pickering's "lobotomized owl" — every flow child gets - equal margin-block-start. Useful for content blocks where you - don't know in advance which elements will be present. - Override: style="--sf-flow-space: 2rem" - ---------------------------------------------------------- */ - .sf-flow > * + * { - margin-block-start: var(--sf-flow-space); - } - - /* -- Truncate / line-clamp ----------------------------------- - Single-line ellipsis (.sf-truncate) and N-line clamp - (.sf-line-clamp-2 / -3 / -N). The fixed-count variants - hardcode -webkit-line-clamp; -N reads --sf-line-clamp. - - Note: -webkit-line-clamp is not vendor-locked despite the - prefix — every modern browser implements it as the de-facto - standard. The unprefixed CSS Overflow 4 `line-clamp` exists - but is not yet widely shipped. - ---------------------------------------------------------- */ - .sf-truncate { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - .sf-line-clamp-2, - .sf-line-clamp-3, - .sf-line-clamp-N { - display: -webkit-box; - /* stylelint-disable-next-line property-no-vendor-prefix -- de-facto standard; unprefixed CSS Overflow 4 line-clamp not yet shipped */ - -webkit-box-orient: vertical; - overflow: hidden; - } - /* line-clamp (unprefixed) is not yet shipped in any production browser as of - 2026 — only -webkit-line-clamp is effective. The prefixed form is the de - facto standard across all engines despite the name. */ - /* stylelint-disable-next-line property-no-vendor-prefix -- see above */ - .sf-line-clamp-2 { -webkit-line-clamp: 2; } - /* stylelint-disable-next-line property-no-vendor-prefix -- see above */ - .sf-line-clamp-3 { -webkit-line-clamp: 3; } - .sf-line-clamp-N { - /* stylelint-disable-next-line property-no-vendor-prefix -- see above */ - -webkit-line-clamp: var(--sf-line-clamp); - } - - /* -- Equal height -------------------------------------------- - Force grid/flex children to share the tallest one's height. - Works in flex (default `align-items: stretch`) and is - re-asserted here for cases where a wrapper resets it. - ---------------------------------------------------------- */ - .sf-equal-height { - display: flex; - align-items: stretch; - } - .sf-equal-height > * { - block-size: auto; - flex: 1 1 auto; - } - - /* -- Aspect -------------------------------------------------- - Generic aspect-ratio container. Distinct from layout's - .sf-frame which targets media (object-fit: cover) — .sf-aspect - is content-agnostic. - Override: style="--sf-aspect: 4 / 3" - ---------------------------------------------------------- */ - .sf-aspect { - aspect-ratio: var(--sf-aspect, 16 / 9); /* fallback: widescreen; override via style="--sf-aspect:4/3" */ - } - - /* -- Scroll shadow ------------------------------------------- - Top + bottom mask gradient that reveals when content is - scrolled inside a vertical-scroll container. Pure CSS, - no JS. Combine with overflow-y: auto on the same element. - Override: style="--sf-scroll-shadow-size: 3rem" - ---------------------------------------------------------- */ - .sf-scroll-shadow { - overflow-y: auto; - /* stylelint-disable-next-line property-no-vendor-prefix -- required for Safari < 15.4 */ - -webkit-mask-image: - linear-gradient(to bottom, - transparent 0, - black var(--sf-scroll-shadow-size), - black calc(100% - var(--sf-scroll-shadow-size)), - transparent 100%); - mask-image: - linear-gradient(to bottom, - transparent 0, - black var(--sf-scroll-shadow-size), - black calc(100% - var(--sf-scroll-shadow-size)), - transparent 100%); - } - - /* -- Scroll snap --------------------------------------------- - Vertical scroll-snap container for vertically stacked sections. - Pairs with consumer-supplied scroll-snap-align on children. - For horizontal snap, use the .sf-reel layout primitive. - ---------------------------------------------------------- */ - .sf-scroll-snap { - overflow-y: auto; - scroll-snap-type: y mandatory; - overscroll-behavior-y: contain; - } - .sf-scroll-snap > * { - scroll-snap-align: start; - } - - /* -- Overflow fade ------------------------------------------- - End-edge horizontal fade for clipped inline content - (e.g. a row of tags overflowing a card). Pure mask, so it - respects the element's actual background. - ---------------------------------------------------------- */ - .sf-overflow-fade { - overflow: hidden; - /* stylelint-disable-next-line property-no-vendor-prefix -- required for Safari < 15.4 */ - -webkit-mask-image: linear-gradient( - to right, - black 0, - black calc(100% - var(--sf-scroll-shadow-size)), - transparent 100% - ); - mask-image: linear-gradient( - to right, - black 0, - black calc(100% - var(--sf-scroll-shadow-size)), - transparent 100% - ); - } - - /* -- No tap highlight ---------------------------------------- - Suppresses the WebKit/Android grey tap-highlight overlay on - interactive elements where it conflicts with the framework's - own .is-active / hover treatment. - ---------------------------------------------------------- */ - .sf-no-tap-highlight { - /* stylelint-disable-next-line property-no-vendor-prefix -- no unprefixed equivalent */ - -webkit-tap-highlight-color: transparent; - } - - /* -- Surface ------------------------------------------------- - Sets background + auto-flipped foreground in one class. - Combines --sf-color-X (resolved semantic, mode-aware) with - the matching --sf-color-text--on-X (auto-contrast) so text - legibility is automatic without consumer math. - - 11 variants cover all brand + status + inverse colors. Base is - excluded — base IS the page surface; use plain - background: var(--sf-color-bg) if you need it explicitly. - - Composes naturally with .sf-section / .sf-card / .sf-stack: -
- - Contextual color cascade (in @supports block below): - Re-declares the eight core semantic tokens on the surface element - so every descendant that reads var(--sf-color-text), --heading, - --border etc. automatically gets the surface-appropriate value - via inheritance — no extra classes needed on children. - Links inherit the surface text color (underline provides the - affordance); override --sf-color-link in slashed.overrides - if you want a branded link hue on a specific surface. - ---------------------------------------------------------- */ - .sf-surface--primary { background: var(--sf-color-primary); color: var(--sf-color-text--on-primary); } - .sf-surface--secondary { background: var(--sf-color-secondary); color: var(--sf-color-text--on-secondary); } - .sf-surface--tertiary { background: var(--sf-color-tertiary); color: var(--sf-color-text--on-tertiary); } - .sf-surface--action { background: var(--sf-color-action); color: var(--sf-color-text--on-action); } - .sf-surface--neutral { background: var(--sf-color-neutral); color: var(--sf-color-text--on-neutral); } - .sf-surface--inverse { background: var(--sf-color-inverse); color: var(--sf-color-text--on-inverse); } - .sf-surface--success { background: var(--sf-color-success); color: var(--sf-color-text--on-success); } - .sf-surface--warning { background: var(--sf-color-warning); color: var(--sf-color-text--on-warning); } - .sf-surface--error { background: var(--sf-color-error); color: var(--sf-color-text--on-error); } - .sf-surface--info { background: var(--sf-color-info); color: var(--sf-color-text--on-info); } - .sf-surface--danger { background: var(--sf-color-danger); color: var(--sf-color-text--on-danger); } - - @supports (color: oklch(from red l c h)) { - .sf-surface--primary { - --sf-color-text: var(--sf-color-text--on-primary); - --sf-color-heading: var(--sf-color-text--on-primary); - --sf-color-text--secondary: oklch(from var(--sf-color-text--on-primary) l c h / 0.70); - --sf-color-text--placeholder: oklch(from var(--sf-color-text--on-primary) l c h / 0.45); - --sf-color-text--disabled: oklch(from var(--sf-color-text--on-primary) l c h / 0.30); - --sf-color-border: oklch(from var(--sf-color-text--on-primary) l c h / 0.20); - --sf-color-border--subtle: oklch(from var(--sf-color-text--on-primary) l c h / 0.12); - --sf-color-border--strong: oklch(from var(--sf-color-text--on-primary) l c h / 0.35); - } - .sf-surface--secondary { - --sf-color-text: var(--sf-color-text--on-secondary); - --sf-color-heading: var(--sf-color-text--on-secondary); - --sf-color-text--secondary: oklch(from var(--sf-color-text--on-secondary) l c h / 0.70); - --sf-color-text--placeholder: oklch(from var(--sf-color-text--on-secondary) l c h / 0.45); - --sf-color-text--disabled: oklch(from var(--sf-color-text--on-secondary) l c h / 0.30); - --sf-color-border: oklch(from var(--sf-color-text--on-secondary) l c h / 0.20); - --sf-color-border--subtle: oklch(from var(--sf-color-text--on-secondary) l c h / 0.12); - --sf-color-border--strong: oklch(from var(--sf-color-text--on-secondary) l c h / 0.35); - } - .sf-surface--tertiary { - --sf-color-text: var(--sf-color-text--on-tertiary); - --sf-color-heading: var(--sf-color-text--on-tertiary); - --sf-color-text--secondary: oklch(from var(--sf-color-text--on-tertiary) l c h / 0.70); - --sf-color-text--placeholder: oklch(from var(--sf-color-text--on-tertiary) l c h / 0.45); - --sf-color-text--disabled: oklch(from var(--sf-color-text--on-tertiary) l c h / 0.30); - --sf-color-border: oklch(from var(--sf-color-text--on-tertiary) l c h / 0.20); - --sf-color-border--subtle: oklch(from var(--sf-color-text--on-tertiary) l c h / 0.12); - --sf-color-border--strong: oklch(from var(--sf-color-text--on-tertiary) l c h / 0.35); - } - .sf-surface--action { - --sf-color-text: var(--sf-color-text--on-action); - --sf-color-heading: var(--sf-color-text--on-action); - --sf-color-text--secondary: oklch(from var(--sf-color-text--on-action) l c h / 0.70); - --sf-color-text--placeholder: oklch(from var(--sf-color-text--on-action) l c h / 0.45); - --sf-color-text--disabled: oklch(from var(--sf-color-text--on-action) l c h / 0.30); - --sf-color-border: oklch(from var(--sf-color-text--on-action) l c h / 0.20); - --sf-color-border--subtle: oklch(from var(--sf-color-text--on-action) l c h / 0.12); - --sf-color-border--strong: oklch(from var(--sf-color-text--on-action) l c h / 0.35); - } - .sf-surface--neutral { - --sf-color-text: var(--sf-color-text--on-neutral); - --sf-color-heading: var(--sf-color-text--on-neutral); - --sf-color-text--secondary: oklch(from var(--sf-color-text--on-neutral) l c h / 0.70); - --sf-color-text--placeholder: oklch(from var(--sf-color-text--on-neutral) l c h / 0.45); - --sf-color-text--disabled: oklch(from var(--sf-color-text--on-neutral) l c h / 0.30); - --sf-color-border: oklch(from var(--sf-color-text--on-neutral) l c h / 0.20); - --sf-color-border--subtle: oklch(from var(--sf-color-text--on-neutral) l c h / 0.12); - --sf-color-border--strong: oklch(from var(--sf-color-text--on-neutral) l c h / 0.35); - } - .sf-surface--inverse { - --sf-color-text: var(--sf-color-text--on-inverse); - --sf-color-heading: var(--sf-color-text--on-inverse); - --sf-color-text--secondary: oklch(from var(--sf-color-text--on-inverse) l c h / 0.70); - --sf-color-text--placeholder: oklch(from var(--sf-color-text--on-inverse) l c h / 0.45); - --sf-color-text--disabled: oklch(from var(--sf-color-text--on-inverse) l c h / 0.30); - --sf-color-border: oklch(from var(--sf-color-text--on-inverse) l c h / 0.20); - --sf-color-border--subtle: oklch(from var(--sf-color-text--on-inverse) l c h / 0.12); - --sf-color-border--strong: oklch(from var(--sf-color-text--on-inverse) l c h / 0.35); - } - .sf-surface--success { - --sf-color-text: var(--sf-color-text--on-success); - --sf-color-heading: var(--sf-color-text--on-success); - --sf-color-text--secondary: oklch(from var(--sf-color-text--on-success) l c h / 0.70); - --sf-color-text--placeholder: oklch(from var(--sf-color-text--on-success) l c h / 0.45); - --sf-color-text--disabled: oklch(from var(--sf-color-text--on-success) l c h / 0.30); - --sf-color-border: oklch(from var(--sf-color-text--on-success) l c h / 0.20); - --sf-color-border--subtle: oklch(from var(--sf-color-text--on-success) l c h / 0.12); - --sf-color-border--strong: oklch(from var(--sf-color-text--on-success) l c h / 0.35); - } - .sf-surface--warning { - --sf-color-text: var(--sf-color-text--on-warning); - --sf-color-heading: var(--sf-color-text--on-warning); - --sf-color-text--secondary: oklch(from var(--sf-color-text--on-warning) l c h / 0.70); - --sf-color-text--placeholder: oklch(from var(--sf-color-text--on-warning) l c h / 0.45); - --sf-color-text--disabled: oklch(from var(--sf-color-text--on-warning) l c h / 0.30); - --sf-color-border: oklch(from var(--sf-color-text--on-warning) l c h / 0.20); - --sf-color-border--subtle: oklch(from var(--sf-color-text--on-warning) l c h / 0.12); - --sf-color-border--strong: oklch(from var(--sf-color-text--on-warning) l c h / 0.35); - } - .sf-surface--error { - --sf-color-text: var(--sf-color-text--on-error); - --sf-color-heading: var(--sf-color-text--on-error); - --sf-color-text--secondary: oklch(from var(--sf-color-text--on-error) l c h / 0.70); - --sf-color-text--placeholder: oklch(from var(--sf-color-text--on-error) l c h / 0.45); - --sf-color-text--disabled: oklch(from var(--sf-color-text--on-error) l c h / 0.30); - --sf-color-border: oklch(from var(--sf-color-text--on-error) l c h / 0.20); - --sf-color-border--subtle: oklch(from var(--sf-color-text--on-error) l c h / 0.12); - --sf-color-border--strong: oklch(from var(--sf-color-text--on-error) l c h / 0.35); - } - .sf-surface--info { - --sf-color-text: var(--sf-color-text--on-info); - --sf-color-heading: var(--sf-color-text--on-info); - --sf-color-text--secondary: oklch(from var(--sf-color-text--on-info) l c h / 0.70); - --sf-color-text--placeholder: oklch(from var(--sf-color-text--on-info) l c h / 0.45); - --sf-color-text--disabled: oklch(from var(--sf-color-text--on-info) l c h / 0.30); - --sf-color-border: oklch(from var(--sf-color-text--on-info) l c h / 0.20); - --sf-color-border--subtle: oklch(from var(--sf-color-text--on-info) l c h / 0.12); - --sf-color-border--strong: oklch(from var(--sf-color-text--on-info) l c h / 0.35); - } - .sf-surface--danger { - --sf-color-text: var(--sf-color-text--on-danger); - --sf-color-heading: var(--sf-color-text--on-danger); - --sf-color-text--secondary: oklch(from var(--sf-color-text--on-danger) l c h / 0.70); - --sf-color-text--placeholder: oklch(from var(--sf-color-text--on-danger) l c h / 0.45); - --sf-color-text--disabled: oklch(from var(--sf-color-text--on-danger) l c h / 0.30); - --sf-color-border: oklch(from var(--sf-color-text--on-danger) l c h / 0.20); - --sf-color-border--subtle: oklch(from var(--sf-color-text--on-danger) l c h / 0.12); - --sf-color-border--strong: oklch(from var(--sf-color-text--on-danger) l c h / 0.35); - } - } /* end @supports (color: oklch(from red l c h)) — surface contextual cascade */ - - /* -- Surface shadow tint ------------------------------------- - Sets background + auto-flipped foreground in one class. Shadows - re-derive from surface background for in-family cast shadows. - Re-derive --sf-shadow-color from each surface's OWN background - so box-shadow / text-shadow / drop-shadow on a colored surface - read as an in-family cast shadow rather than a neutral-grey slab. - Mirrors the default formula (oklch …0.15 c h) but seeds it with - the surface background instead of --sf-color-neutral. Completes - the contextual cascade so elevated children (cards, popovers) - elevated ON a surface inherit a correctly tinted shadow. - Inverse is intentionally omitted: its background is the page text - color, so the neutral default already matches. */ - @supports (color: oklch(from red l c h)) { - .sf-surface--primary { --sf-shadow-color: oklch(from var(--sf-color-primary) 0.15 c h); } - .sf-surface--secondary { --sf-shadow-color: oklch(from var(--sf-color-secondary) 0.15 c h); } - .sf-surface--tertiary { --sf-shadow-color: oklch(from var(--sf-color-tertiary) 0.15 c h); } - .sf-surface--action { --sf-shadow-color: oklch(from var(--sf-color-action) 0.15 c h); } - .sf-surface--neutral { --sf-shadow-color: oklch(from var(--sf-color-neutral) 0.15 c h); } - .sf-surface--success { --sf-shadow-color: oklch(from var(--sf-color-success) 0.15 c h); } - .sf-surface--warning { --sf-shadow-color: oklch(from var(--sf-color-warning) 0.15 c h); } - .sf-surface--error { --sf-shadow-color: oklch(from var(--sf-color-error) 0.15 c h); } - .sf-surface--info { --sf-shadow-color: oklch(from var(--sf-color-info) 0.15 c h); } - .sf-surface--danger { --sf-shadow-color: oklch(from var(--sf-color-danger) 0.15 c h); } - } - - /* -- Scrim --------------------------------------------------- - Darkening overlay for text placed over a background image, so - the text clears contrast without dimming the whole picture. - Apply to a positioned wrapper that holds the image + text; the - scrim paints as a ::before gradient between them. The wrapper - must establish a stacking/positioning context, so the macro - sets position: relative and isolation: isolate itself. - - Two layouts work: - (a) image as a CSS background on the wrapper (simplest): -
-
-
- (b) image as a child element (use object-fit to fill, and - position the content over it): -
- -
-
- In (b) the image is left in the background layer (media children - are NOT lifted) so the scrim darkens it; the content is lifted. - - The gradient defaults to bottom-anchored (text sits at the - bottom). Retune via tokens: - style="--sf-scrim-direction: to top right" - style="--sf-scrim-color: oklch(0 0 0 / 0.75)" - or replace the whole gradient with --sf-scrim-gradient. - - Direction modifiers cover the common anchors without inline - styles. Stacking order is image (background layer) → scrim - (::before) → content, so the image MUST sit below the scrim: - media children (img/picture/video/svg) are intentionally NOT - lifted, only the text/content children are. (Lifting every - child would promote the image above the scrim and the overlay - would never darken it.) - ---------------------------------------------------------- */ - .sf-scrim { - position: relative; - isolation: isolate; - } - .sf-scrim::before { - content: ""; - position: absolute; - inset: 0; - z-index: 0; - background: var(--sf-scrim-gradient); - pointer-events: none; - } - /* Lift content above the scrim, but leave media below it so the - gradient actually darkens the picture. */ - .sf-scrim > :not(img, picture, video, svg, canvas) { - position: relative; - z-index: 1; - } - .sf-scrim--top { --sf-scrim-direction: to bottom; } /* text anchored at the top */ - .sf-scrim--bottom { --sf-scrim-direction: to top; } /* text anchored at the bottom (default) */ - .sf-scrim--full { --sf-scrim-gradient: var(--sf-scrim-color); } /* even wash over the whole image */ - - /* -- Text protect -------------------------------------------- - Lighter-weight alternative to .sf-scrim: protects legibility - of text over a busy image WITHOUT a darkening layer, using a - soft shadow halo behind the glyphs. Apply directly to the text - element sitting over the image. -

Over a photo

- ---------------------------------------------------------- */ - .sf-text-protect { - text-shadow: var(--sf-scrim-text-shadow); - } - - /* -- Text gradient ------------------------------------------- - Fills text with a gradient. Defaults to --sf-gradient-primary - (auto-adapts to brand + dark mode). Override the background-image - per-instance for any other gradient or radial: -

- - Selection on gradient text inherits the clipping — that's a - known browser limitation across all engines, accepted as-is. - - `background-clip: text` and `color: transparent` are required - together — without clipping, transparent text is invisible. - - WebKit only added the standard, unprefixed `background-clip: text` - in Safari 18; the framework floor is Safari 17.5, where the value - is recognised ONLY through `-webkit-background-clip`. The prefixed - declaration is therefore required, or gradient headings render as - invisible (transparent fill, unclipped background) on Safari - 17.5–17.x. The unprefixed property is listed last so Chrome 123+, - Firefox 128+ and Safari 18+ use the standard form, while Safari - 17.5–17.x fall back to the prefixed one (last valid wins per - browser). Firefox has never needed the prefix. - ---------------------------------------------------------- */ - .sf-text-gradient { - background-image: var(--sf-gradient-primary); - /* stylelint-disable-next-line property-no-vendor-prefix -- Safari <18 only supports background-clip:text via the -webkit- prefix; floor is Safari 17.5 */ - -webkit-background-clip: text; - background-clip: text; - color: transparent; - } - - /* -- External link indication -------------------------------- - Adds an external-link glyph after the link text. Opt-in per - element via class — for global auto-detection, write your - own rule in unlayered CSS: - a[rel~="external"]:not(.no-external)::after { - content: var(--sf-link-external-marker); - } - - Marker is tokenised so consumers can swap to " →", " ⤴", or - unset (empty string fully disables it — the default token - value bakes the leading space in, so `""` leaves nothing - after the link): - :root { --sf-link-external-marker: "" } - ---------------------------------------------------------- */ - .sf-link-external::after { - content: var(--sf-link-external-marker); - display: inline-block; - font-size: 0.85em; - text-decoration: none; - } - - /* -- Content visibility -------------------------------------- - Skip rendering (layout + paint) for offscreen content until it - scrolls near the viewport — a large initial-render win on long - pages (product grids, long articles). contain-intrinsic-size - reserves a placeholder box so the scrollbar and scroll position - stay stable before a section is first rendered; the `auto` - keyword then caches each section's last-rendered size. Override - the placeholder per element: - style="--sf-content-intrinsic-size: 800px" - - Unsupported engines (e.g. Safari < 18) simply ignore both - declarations and render normally — pure progressive enhancement. - - Deliberately NOT paired with `will-change`: that pre-creates - compositing layers and usually HURTS performance when applied - broadly. `will-change` belongs on JS, set only while an element - is actively about to animate. - ---------------------------------------------------------- */ - .sf-content-auto { - content-visibility: auto; - contain-intrinsic-size: auto var(--sf-content-intrinsic-size, 500px); - } - - /* -- Tabular figures ----------------------------------------- - Fixed-width digits so numbers align in vertical columns - (price lists, totals, invoices, dashboards). Reads the public - --sf-font-numeric token. Universal browser support. - ---------------------------------------------------------- */ - .sf-tabular-nums { - font-variant-numeric: var(--sf-font-numeric, tabular-nums); - } - - /* -- Link variants ------------------------------------------- - Opt-in link treatments composed from the link tokens. They - don't change link COLOR (that stays the auto-contrast - --sf-color-link); they only adjust the underline affordance. - - .sf-link--subtle — underline hidden until hover/focus. Use - for dense link lists (nav, footers) where a permanent - underline is visually noisy. The hover underline keeps the - affordance for the moment of interaction. - .sf-link--reverse — the literal inverse: underlined by - default, underline removed on hover/focus. Use where the - resting state should read as an obvious link and hover is - the "active" de-emphasis. - - All four rules are gated on :link/:visited (like base a:link) - so the resting and hover halves stay symmetric — the classes - are meant for real anchors with an href. - ---------------------------------------------------------- */ - .sf-link--subtle:link, - .sf-link--subtle:visited { - text-decoration-line: none; - } - .sf-link--subtle:is(:link, :visited):hover, - .sf-link--subtle:is(:link, :visited):focus-visible { - text-decoration-line: underline; - text-decoration-color: currentcolor; - } - - .sf-link--reverse:link, - .sf-link--reverse:visited { - text-decoration-line: underline; - } - .sf-link--reverse:is(:link, :visited):hover, - .sf-link--reverse:is(:link, :visited):focus-visible { - text-decoration-line: none; - } - -} - -/* ─── core/states.css ─── */ -/* SLASHED — core/states.css - @layer slashed.states - - Stateful classes toggled by JS or ARIA attributes. - Prefix: .is-* (exclusive to this layer — never used in utilities). - - These are NOT utility classes. They describe dynamic, - runtime states that change in response to user interaction - or application logic. Utilities describe static intent; - states describe current condition. - - Design principles: - ─ Generic states only — component-specific overrides belong - in slashed.components (e.g. .sf-modal.is-open). - ─ Tokens first — every value references a design token. - ─ Low specificity — single class, no nesting. - ─ Minimal property sets — states set the MINIMUM needed - to communicate the state. Components layer on top. - ─────────────────────────────────────────────────── */ - -@layer slashed.states { - - /* ============================================================ - VISIBILITY - ============================================================ */ - - .is-hidden { - display: none !important; - } - - .is-invisible { - visibility: hidden; - } - - .is-visible { - visibility: visible; - } - - /* ============================================================ - INTERACTIVITY — DISABLED / READONLY - ============================================================ */ - - .is-disabled { - opacity: var(--sf-opacity-disabled); - pointer-events: none; - /* cursor: not-allowed is intentional even though pointer-events:none means it never renders - on this element itself. The CSS value IS inherited by child elements that have - pointer-events:auto re-enabled, and it is part of the documented .is-disabled API surface. */ - cursor: not-allowed; - user-select: none; - } - - .is-readonly { - pointer-events: none; - user-select: none; - } - - /* ============================================================ - LOADING / ASYNC FEEDBACK - ============================================================ */ - - .is-loading { - color: transparent !important; - pointer-events: none; - position: relative; - } - - .is-loading::after { - content: ""; - position: absolute; - inset-block-start: 50%; - inset-inline-start: 50%; - inline-size: 1em; - block-size: 1em; - margin-block-start: -0.5em; - margin-inline-start: -0.5em; - border: var(--sf-border-width-2) solid var(--sf-color-border--strong); - border-block-start-color: var(--sf-color-action); - border-radius: var(--sf-radius-full); - /* Reduced-motion: accessibility.css kills animation-duration via - !important on *, *::before, *::after — so this spinner stops - even without a local reduced-motion wrapper. */ - animation: var(--sf-animation-spin); - } - - .is-busy { - cursor: progress; - } - - .is-pending { - /* Optimistic UI — request in flight but content still visible/usable - (unlike .is-loading, which masks content with a spinner). */ - opacity: var(--sf-state-pending-opacity); - cursor: progress; - } - - .is-skeleton { - color: transparent !important; - background: linear-gradient( - 90deg, - var(--sf-color-inset) 25%, - var(--sf-color-raised) 50%, - var(--sf-color-inset) 75% - ); - background-size: 200% 100%; - animation: var(--sf-animation-shimmer); - border-radius: var(--sf-radius-s); - pointer-events: none; - user-select: none; - } - - /* ============================================================ - ACTIVE / SELECTED / CURRENT — navigation & toggling - ============================================================ */ - - .is-active { - /* Generic active indicator — components add visual specifics. - Sets a scoped token for components to consume. */ - --sf-is-active: 1; - } - - .is-selected { - background-color: var(--sf-color-bg--selected); - } - - .is-current { - /* Navigation: marks the current page/route. - Components style via .sf-nav .is-current etc. - Weight is tokenised so consumers can override without specificity battles: - :root { --sf-current-font-weight: var(--sf-font-weight-semibold); } */ - --sf-is-current: 1; - font-weight: var(--sf-current-font-weight, var(--sf-font-weight-bold)); - } - - .is-highlighted { - background-color: var(--sf-color-bg--focus); - } - - .is-pressed { - /* Toggle buttons (aria-pressed="true"). Components add specifics. */ - --sf-is-pressed: 1; - background-color: var(--sf-color-bg--selected); - } - - /* ============================================================ - OPEN / CLOSED — disclosure widgets, modals, dropdowns - - .is-open / .is-collapsed — a thing is shown vs hidden (modal, - dropdown, drawer). .is-expanded — maps to aria-expanded on a - disclosure/accordion trigger. They share the --sf-is-open flag - but signal different intent to consumers. - ============================================================ */ - - .is-open { - --sf-is-open: 1; - } - - .is-collapsed { - --sf-is-open: 0; - } - - .is-expanded { - --sf-is-open: 1; - } - - /* ============================================================ - VALIDATION / FEEDBACK - - Overlap semantics (see docs/states.md): - · .is-invalid / .is-valid — form-field validation (maps to - aria-invalid). Use on inputs. - · .is-error / .is-success — general component feedback, not tied - to a single field (a failed save, a completed step). - · .is-danger — destructive-action context (a delete button), NOT - a validation result. Distinct from .is-error. - ============================================================ */ - - /* NOTE: .is-valid and .is-success share identical CSS values by design. - The semantic distinction is intentional (see docs/states.md): - · .is-valid → form-field validation result (maps to aria-invalid="false"). - Use on elements. - · .is-success → general component feedback, not tied to a form field. - Use on containers, banners, step indicators. - They share the same visual treatment because both represent a "positive" - state, but consuming them interchangeably is a semantic error. */ - .is-valid, - .is-success { - --sf-field-border-color: var(--sf-color-success); - --sf-field-text-color: var(--sf-color-success-strong); - } - - /* NOTE: .is-invalid and .is-error share identical CSS values by design. - · .is-invalid → form-field validation failure (maps to aria-invalid="true"). - · .is-error → general component error state (failed save, network error). */ - .is-invalid, - .is-error { - --sf-field-border-color: var(--sf-color-error); - --sf-field-text-color: var(--sf-color-error-strong); - } - - .is-warning { - --sf-field-border-color: var(--sf-color-warning); - --sf-field-text-color: var(--sf-color-warning-strong); - } - - .is-info { - --sf-field-border-color: var(--sf-color-info); - --sf-field-text-color: var(--sf-color-info-strong); - } - - .is-danger { - --sf-field-border-color: var(--sf-color-danger); - --sf-field-text-color: var(--sf-color-danger-strong); - } - - /* ============================================================ - POSITION / STICKINESS - ============================================================ */ - - .is-sticky { - position: sticky; - inset-block-start: var(--sf-sticky-offset, 0); /* logical: respects writing-mode */ - z-index: var(--sf-z-low); - } - - .is-pinned { - position: sticky; - inset-block-start: 0; - z-index: var(--sf-z-mid); - } - - .is-fixed { - /* Intentionally bare — position only. Consumers must supply placement - (inset, inset-block-start, etc.) and z-index via a modifier or inline style. - Example:
*/ - position: fixed; - } - - .is-fullscreen { - /* App-managed fullscreen overlay. For the native Fullscreen API, - prefer the `:fullscreen` pseudo-class; this is the JS-toggled - equivalent for elements not entered via that API. */ - position: fixed; - inset: 0; - z-index: var(--sf-z-max); - } - - .is-resizable { - resize: both; - overflow: auto; - min-inline-size: var(--sf-space-xl, 3.55rem); /* prevent collapsing to zero on drag */ - min-block-size: var(--sf-space-xl, 3.55rem); - } - - /* ============================================================ - OVERFLOW / CLIPPING - ============================================================ */ - - .is-clipped { - overflow: hidden !important; - } - - .is-scrollable { - overflow: auto; - overscroll-behavior: contain; - } - - .is-truncated { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - /* ============================================================ - DRAG & DROP - ============================================================ */ - - .is-dragging { - opacity: var(--sf-opacity-50); - cursor: grabbing; - } - - .is-drop-target { - outline: var(--sf-focus-ring-width) dashed var(--sf-color-action); - outline-offset: var(--sf-focus-ring-offset); - } - - .is-draggable { - cursor: grab; - } - - /* ============================================================ - OVERLAY / BACKDROP - ============================================================ */ - - .is-overlay { - position: absolute; - inset: 0; - } - - /* ============================================================ - FOCUS / INTERACTION MODIFIERS - ============================================================ */ - - .is-clickable { - cursor: pointer; - } - - .is-unselectable { - user-select: none; - } - - .is-focused { - /* Programmatic / JS-driven focus indicator, independent of - :focus-visible. Normal weight so consumers can restyle. */ - outline: var(--sf-focus-ring-width) var(--sf-focus-ring-style, solid) var(--sf-focus-ring-color); - outline-offset: var(--sf-focus-ring-offset); - } - - /* ============================================================ - EMPTY STATE - ============================================================ */ - - /* NOTE: :empty only matches elements with NO child nodes — including no - whitespace. Server-rendered HTML often includes space between tags, which - prevents the match. When toggling via JS, call element.textContent = "" - before adding this class (or use element.innerHTML = "" to clear all nodes - including whitespace text nodes). */ - .is-empty:empty { - display: none; - } - - /* ============================================================ - KEYFRAMES (used by states above) - - Spinner and shimmer keyframes live in core/motion.css alongside - the rest of the framework's @keyframes. They are referenced from - here by name (sf-spin, sf-shimmer) and are inert until applied. - If you load core/states.css without core/motion.css, define them - in your own CSS or copy them from motion.css. - ============================================================ */ - -} - -/* ─── core/motion.css ─── */ -/* SLASHED — core/motion.css - @layer slashed.motion - - Extends the base interactive transition (defined in base.css) - with a broader property set and adds scroll/view-transition - behaviour. Optional and independently loadable. - - Reduced-motion handling is intentionally NOT duplicated here: - accessibility.css (a higher cascade layer, shipped in the - 'essential' bundle) neutralises animation/transition via - property-level !important. The no-preference wrapper below is - a defensive opt-in stance — animations apply only when the - user has not requested their absence — and remains correct - even if motion.css is served without accessibility.css. - ─────────────────────────────────────────────────── */ - -@layer slashed.motion { - - @media (prefers-reduced-motion: no-preference) { - - html { - scroll-behavior: smooth; - } - - /* Keyboard navigation must not be hijacked by smooth scroll. */ - html:focus-within { - scroll-behavior: auto; - } - - /* Broadens the base.css link transition to all interactive - elements and the full set of animatable UI properties. */ - a, button, input, select, textarea, summary { - transition-property: - color, background-color, border-color, text-decoration-color, - fill, stroke, opacity, box-shadow, transform, filter, - backdrop-filter; - transition-duration: var(--sf-duration-fast); - transition-timing-function: var(--sf-ease-out); - } - - @supports (view-transition-name: none) { - ::view-transition-old(root), - ::view-transition-new(root) { - animation-duration: var(--sf-duration-normal); - animation-timing-function: var(--sf-ease-out); - } - } - - /* Animation presets — opt-in classes. Scoped to no-preference so - reduced-motion users never trigger them (accessibility.css also - neutralises animation-duration as a hard guarantee). These are - DECORATIVE: under reduced motion they simply don't run, so never - rely on one (e.g. .sf-fade-out) as the sole way to show/hide - content — drive real state with hidden/display/JS instead. */ - .sf-fade-in { animation: var(--sf-animation-fade-in); } - .sf-fade-out { animation: var(--sf-animation-fade-out); } - .sf-slide-in-up { animation: var(--sf-animation-slide-in-up); } - .sf-slide-in-down { animation: var(--sf-animation-slide-in-down); } - .sf-slide-in-left { animation: var(--sf-animation-slide-in-left); } - .sf-slide-in-right { animation: var(--sf-animation-slide-in-right); } - .sf-scale-up { animation: var(--sf-animation-scale-up); } - .sf-scale-down { animation: var(--sf-animation-scale-down); } - - /* Scroll-driven entrance effects — element animates into view - as it enters the viewport. Uses CSS scroll-driven animations - via animation-timeline: view(). Range tokens - (--sf-scroll-timeline-range-{start,end}, declared in - core/tokens.css) control where the animation starts and ends - along the scroll progress. - - Example: -
- - Reduced-motion: gated by the no-preference wrapper above, so - these are inert when the user opts out. The core animation - (translate/opacity) still composes correctly with reduced-motion - neutralisation in accessibility.css if a consumer accidentally - writes them outside the gate. - - Browser support — animation-timeline ships in Chrome/Edge 115+. - Firefox keeps it behind a flag (the scroll-driven-animations - preference) and Safari has not shipped it (tracked at WebKit - bug 264057); on those engines the @supports block below is - skipped, so the rule falls back to a one-shot time-driven - animation playing once at --sf-duration-slow. - animation-duration is set unconditionally because without it - a non-supporting engine would default to 0s and snap to the - end state instantly — there'd be no visible entrance at all. - Where animation-timeline is supported the timeline drives - progress and the duration is ignored. */ - .sf-entrance--fade, - .sf-entrance--fade-up, - .sf-entrance--fade-down, - .sf-entrance--fade-left, - .sf-entrance--fade-right, - .sf-entrance--scale-up { - animation-duration: var(--sf-duration-slow); - animation-timing-function: var(--sf-ease-out); - animation-fill-mode: both; - } - @supports (animation-timeline: view()) { - .sf-entrance--fade, - .sf-entrance--fade-up, - .sf-entrance--fade-down, - .sf-entrance--fade-left, - .sf-entrance--fade-right, - .sf-entrance--scale-up { - animation-timeline: view(); - animation-range: var(--sf-scroll-timeline-range-start, entry 0%) - var(--sf-scroll-timeline-range-end, cover 30%); - } - } - .sf-entrance--fade { animation-name: sf-fade-in; } - .sf-entrance--fade-up { animation-name: sf-slide-in-up; } - .sf-entrance--fade-down { animation-name: sf-slide-in-down; } - .sf-entrance--fade-left { animation-name: sf-slide-in-left; } - .sf-entrance--fade-right { animation-name: sf-slide-in-right; } - .sf-entrance--scale-up { animation-name: sf-scale-up; } - - } - - /* Keyframes — inert until referenced, so they live outside the - no-preference wrapper (consumers may reference them at will). - Spinner and shimmer are used by .is-loading / .is-skeleton in - core/states.css; the rest power the .sf-* animation classes - above. */ - @keyframes sf-fade-in { from { opacity: 0; } to { opacity: 1; } } - @keyframes sf-fade-out { from { opacity: 1; } to { opacity: 0; } } - @keyframes sf-slide-in-up { from { opacity: 0; translate: 0 1rem; } to { opacity: 1; translate: 0 0; } } - @keyframes sf-slide-in-down { from { opacity: 0; translate: 0 -1rem; } to { opacity: 1; translate: 0 0; } } - @keyframes sf-slide-in-left { from { opacity: 0; translate: 1rem 0; } to { opacity: 1; translate: 0 0; } } - @keyframes sf-slide-in-right { from { opacity: 0; translate: -1rem 0; } to { opacity: 1; translate: 0 0; } } - @keyframes sf-scale-up { from { opacity: 0; scale: 0.92; } to { opacity: 1; scale: 1; } } - @keyframes sf-scale-down { from { opacity: 1; scale: 1; } to { opacity: 0; scale: 0.92; } } - - @keyframes sf-spin { to { rotate: 360deg; } } - @keyframes sf-shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } - - /* Notification ping (expanding fading ring), cursor blink, gentle float. - Keyframes only — apply via your own BEM/component rules or the - --sf-animation-ping / -blink / -float presets. No utility classes - ship for these (by design). */ - @keyframes sf-ping { 75%, 100% { scale: 2; opacity: 0; } } - @keyframes sf-blink { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } } - @keyframes sf-float { 0%, 100% { translate: 0 0; } 50% { translate: 0 -0.5rem; } } - - /* @property color interpolation — animates --sf-color-primary-light smoothly - because it is registered with syntax:"" (see @property in tokens.css). - Without that registration the browser cannot interpolate between color values - and the animation will snap at 50% with no intermediate frames. - Gated behind @supports so unsupporting engines simply skip it. */ - @supports (@property --x { syntax: ""; inherits: false; initial-value: white; }) { - @keyframes sf-color-pulse { - 0% { --sf-color-primary-light: var(--sf-color-primary-light); } - 50% { --sf-color-primary-light: oklch(from var(--sf-color-primary-light) calc(l + 0.25) c h); } - 100% { --sf-color-primary-light: var(--sf-color-primary-light); } - } - - @media (prefers-reduced-motion: no-preference) { - .sf-color-pulse { - animation: var(--sf-animation-color-pulse); - } - } - } - - /* @starting-style — entry animations for elements appearing in the - top layer (dialog, [popover]) or via display:none → block. SLASHED - ships no global @starting-style rules because the from-state is - element/component specific. Consumers opt in per component, e.g.: - dialog[open] { opacity: 1; @starting-style { opacity: 0; } } - Tracked for the future components layer. */ - -} - -/* ─── core/accessibility.css ─── */ -/* SLASHED — core/accessibility.css - @layer slashed.accessibility - - Critical a11y rules use !important to survive unlayered CSS and - third-party resets (unlayered normal beats any layered normal). - Protection is SELECTIVE — only rules whose override is a genuine - user-facing barrier are hardened. Rules that are legitimately - customised (touch targets, skip-link position, contrast boost, - cursor) are left normal on purpose. - - Audit note: reset.css/base.css contain no !important on outline, - animation-* or transition-*, so the reversed important-layer order - cannot let a lower layer defeat these rules. - ─────────────────────────────────────────────────── */ - -@layer slashed.accessibility { - - /* ---------------------------------------------------------- - Focus — keyboard visibility is WCAG 2.4.7, hard barrier. - Ring definition lives here (not base.css) so all focus - logic is in one high-priority layer. Hardened. - ---------------------------------------------------------- */ - :focus:not(:focus-visible) { - outline: none; - } - - :focus-visible { - outline: var(--sf-focus-ring-width) var(--sf-focus-ring-style, solid) var(--sf-focus-ring-color) !important; - outline-offset: var(--sf-focus-ring-offset) !important; - } - - /* ---------------------------------------------------------- - Focus-shadow opt-in — switches the focus indicator from the - default `outline` ring to a `box-shadow` ring (the composite - --sf-focus-ring-shadow token). Useful when the outline clips - awkwardly on rounded corners or overflow:hidden containers, - where box-shadow follows border-radius. Opt-in per element: - - - The base :focus-visible outline is hardened with !important - (WCAG 2.4.7), so this opt-in must also use !important to take - over — it replaces one always-visible ring with another, so - the keyboard-focus guarantee is preserved, not weakened. - outline:none here only removes the now-redundant outline ring - in favour of the box-shadow ring on the same element. - ---------------------------------------------------------- */ - .sf-focus-shadow:focus-visible { - outline: none !important; - box-shadow: var(--sf-focus-ring-shadow) !important; - } - /* Forced-colors (Windows High Contrast) drops box-shadow entirely, which - would leave this element with NO focus indicator. Restore a system-colour - outline ring there so the WCAG 2.4.7 guarantee holds in that mode too. */ - @media (forced-colors: active) { - .sf-focus-shadow:focus-visible { - outline: var(--sf-focus-ring-width) solid !important; - outline-offset: var(--sf-focus-ring-offset) !important; - box-shadow: none !important; - } - } - - /* ---------------------------------------------------------- - Reduced motion. Token override helps components that read - duration tokens in other contexts; the property-level - !important block is the real guarantee and works - independently of tokens (and of unlayered token overrides). - ---------------------------------------------------------- */ - @media (prefers-reduced-motion: reduce) { - :root { - --sf-duration-instant: 0.01ms; - --sf-duration-fast: 0.01ms; - --sf-duration-normal: 0.01ms; - --sf-duration-slow: 0.01ms; - --sf-duration-slower: 0.01ms; - } - - *, - *::before, - *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - scroll-behavior: auto !important; - } - } - - /* Manual motion opt-out — same suppression as the OS preference above, - but consumer-toggled (e.g. a site-level "reduce motion" switch). Works - regardless of the OS setting. */ - .no-motion, - .no-motion *, - .no-motion *::before, - .no-motion *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - scroll-behavior: auto !important; - } - - /* ---------------------------------------------------------- - High contrast — enhancement, not a barrier. Left normal: - if overridden the focus ring still exists, just thinner. - ---------------------------------------------------------- */ - @media (prefers-contrast: more) { - :root { - --sf-focus-ring-width: 3px; - /* Push derived reading-text colours toward the extremes (see the - --sf-contrast-bias knob in tokens.css). */ - --sf-contrast-bias: 0.06; - } - - hr { - border-block-start-width: var(--sf-border-width-2); - } - } - - /* ---------------------------------------------------------- - Reduced transparency. - ---------------------------------------------------------- */ - @media (prefers-reduced-transparency: reduce) { - ::backdrop { - background-color: var(--sf-color-neutral); - opacity: 1; - } - } - - /* ---------------------------------------------------------- - Reduced data — Save-Data / metered-connection preference. - Enhancement, not a barrier (left normal, no !important): we - only neutralise the framework's paint-heaviest *decorative* - output via its own master knobs, so layout and legibility are - untouched. --sf-shadow-strength: 0 zeroes every box/text/drop - shadow and the glow in one step (they all derive their alpha - from it); the blur tokens collapse so backdrop/filter blurs - stop compositing expensive layers. Progressive enhancement — - engines without prefers-reduced-data simply ignore this block. - ---------------------------------------------------------- */ - @media (prefers-reduced-data: reduce) { - :root { - --sf-shadow-strength: 0; - --sf-blur-xs: 0px; - --sf-blur-s: 0px; - --sf-blur-m: 0px; - --sf-blur-l: 0px; - --sf-blur-xl: 0px; - } - } - - /* ---------------------------------------------------------- - Minimum touch targets — WCAG 2.5.5 has documented - exceptions; dense desktop-first UIs legitimately go below - 44px with other hit-area strategies. Left normal ON PURPOSE - so it stays a recommendation, not an unbreakable rule. - ---------------------------------------------------------- */ - @media (pointer: coarse) { - button, - input[type="button"], - input[type="submit"], - input[type="reset"], - select, - summary { - min-block-size: var(--sf-touch-target); - min-inline-size: var(--sf-touch-target); - } - /* Checkboxes and radios are replaced elements — no display change needed. - The UA renders them as a fixed small square; min-size expands the - hit area without touching layout. */ - input[type="checkbox"], - input[type="radio"] { - min-block-size: var(--sf-touch-target); - min-inline-size: var(--sf-touch-target); - } - } - - /* ---------------------------------------------------------- - Disabled states — cosmetic, left normal. - ---------------------------------------------------------- */ - [disabled], - [aria-disabled="true"] { - cursor: not-allowed; - } - - /* ---------------------------------------------------------- - Screen-reader-only. Atomic contract: a partial override - either exposes hidden content visually or hides intended - content. All-or-nothing, so the whole set is hardened. - ---------------------------------------------------------- */ - .sr-only, - .sr-only-focusable:not(:focus, :focus-within) { - position: absolute !important; - width: 1px !important; - height: 1px !important; - padding: 0 !important; - margin: -1px !important; - overflow: hidden !important; /* fallback for Safari <16 */ - overflow: clip !important; - clip-path: inset(50%) !important; - white-space: nowrap !important; - border: 0 !important; - } - - /* ---------------------------------------------------------- - Skip link — appearance belongs to the integrator's design. - Left normal so position/colour can be themed freely. - ---------------------------------------------------------- */ - .skip-link { - position: absolute; - inset-block-start: -100%; - inset-inline-start: var(--sf-space-m); - z-index: var(--sf-z-max); - padding: var(--sf-space-s) var(--sf-space-m); - background: var(--sf-color-bg); - color: var(--sf-color-text); - border: var(--sf-border-width-1) solid var(--sf-color-border); - border-radius: var(--sf-radius-m); - transition: inset-block-start var(--sf-duration-fast) var(--sf-ease-out); - } - .skip-link:focus { - inset-block-start: var(--sf-space-m); - } - - - /* ---------------------------------------------------------- - Focus-parent — an a11y *pattern*, not a state. Lives in the - accessibility layer (was previously in states.css; moved in - v0.3.0) so that all focus-related a11y rules share one home - and survive third-party resets at the same priority as the - focus-visible ring above. Cannot be hardened with !important - because the outline shorthand is consumer-customisable. - Canonical name: .sf-focus-parent (prefixed). - ---------------------------------------------------------- */ - /* Uses --sf-focus-ring-color (same token as :focus-visible) so the ring - is visually consistent whether focus lands on the parent or a child. */ - .sf-focus-parent:focus-within { - outline: var(--sf-focus-ring-width) var(--sf-focus-ring-style, solid) var(--sf-focus-ring-color); - outline-offset: var(--sf-focus-ring-offset); - } - - /* ---------------------------------------------------------- - Clickable-parent — the card-with-link a11y pattern. - Applied to the card container. Two modes: - - Automatic (single primary link): -
-

Title

-

Description

- -
- - Explicit (multiple links — add [data-overlay-link] to the primary): -
-

Title

-

Description

- Category ← lifted automatically - ← lifted automatically -
- - Mechanic: the link's ::after fills the card via position:absolute; - inset:0. It is contained by .sf-clickable-parent (position:relative) - only if the link itself has no positioned ancestor between it and the - card — so no element containing the overlay link should be given - position:relative. Flow children are therefore intentionally left at - position:static; they sit below the overlay in stacking order. - - Trade-off: card text is NOT user-selectable by mouse. A JavaScript - click-delegate could restore selectability while preserving - full-card navigation. - - Key features: - • [data-overlay-link] explicit mode — reliable multi-link cards - • `:focus-within` card ring — always included, no extra recipe - • --sf-clickable-overlay-z — escape parent stacking contexts - • Wider interactive-element exceptions (summary, video, audio…) - - Tune --sf-clickable-overlay-z to escape a parent stacking context. - [data-no-overlay] opts any descendant above the overlay. - ---------------------------------------------------------- */ - .sf-clickable-parent { - --sf-clickable-overlay-z: 1; - position: relative; - cursor: pointer; - } - - /* Overlay: the link's ::after fills the containing card. - Automatic mode — every :any-link gets the overlay; works reliably - for single-link cards. Secondary links that also receive ::after are - lifted to position:relative (interactive rule below), which scopes - their ::after to themselves and keeps them independently clickable. - Explicit mode — only [data-overlay-link] fills the card. */ - .sf-clickable-parent:not(:has([data-overlay-link])) a:where(:any-link)::after, - .sf-clickable-parent [data-overlay-link]::after { - content: ""; - position: absolute; - inset: 0; - z-index: var(--sf-clickable-overlay-z); - pointer-events: auto; - } - - /* Interactive descendants beat the overlay and stay functional. - Explicit mode: every non-designated :any-link is lifted — robust at - any nesting depth, no :first-of-type fragility. - Automatic mode: links that are :not(:first-of-type) among siblings - are lifted. Sibling-scoped — use explicit mode for cards where - secondary links live at different nesting depths. - [data-no-overlay] is the per-element consumer escape hatch. */ - .sf-clickable-parent :is( - button, input, textarea, select, - summary, video, audio, - [role="button"], [data-no-overlay] - ), - .sf-clickable-parent:has([data-overlay-link]) a:where(:any-link):not([data-overlay-link]), - .sf-clickable-parent:not(:has([data-overlay-link])) a:where(:any-link):not(:first-of-type) { - position: relative; - z-index: calc(var(--sf-clickable-overlay-z) + 1); - } - - /* Card-level focus ring — keyboard users see which card is active. - The focus ring is always included without requiring a separate - recipe. Suppress with outline:none - if the card design provides its own focus indicator. */ - .sf-clickable-parent:focus-within { - outline: var(--sf-focus-ring-width) var(--sf-focus-ring-style, solid) var(--sf-focus-ring-color); - outline-offset: var(--sf-focus-ring-offset); - } - - /* ---------------------------------------------------------- - Forced colors (Windows High Contrast). Outline-based focus - already survives; we only (a) adopt the system Highlight - colour for the ring and (b) drop decorative shadows that - forced-colors would otherwise flatten into solid slabs. - Shadow removal is zero-specificity so any rule can win it back. - ---------------------------------------------------------- */ - @media (forced-colors: active) { - :root { - --sf-focus-ring-color: Highlight; - } - - :focus-visible { - outline-color: Highlight !important; - } - - :where(*) { - box-shadow: none; - text-shadow: none; - } - - /* Form controls otherwise lose their visible boundary in forced-colors - (their token-driven border colour is replaced by the system canvas). - Re-assert a system-coloured border so fields stay distinguishable. */ - input, - select, - textarea { - border: var(--sf-border-width-1) solid; - } - } - -} - -/* ─── core/print.css ─── */ -/* SLASHED — core/print.css - @layer slashed.print - - Print rules and @page definition. Print tokens (--sf-print-*) - are declared in core/tokens.css. - - Color contract (Phase 3): - authored colour is preserved by default. Browsers apply their own - ink-saving heuristics via the default `print-color-adjust: economy`, - so semantic colours (badges, status pills, , syntax highlighting) - reach paper intact. Two opt-in classes give consumers control: - - .print-color-exact → force every descendant to print colours - verbatim (for colour-coded data that loses - meaning when the browser flattens it). - .print-no-color → force everything to ink-on-paper: - transparent backgrounds + system text colour. - The legacy blanket reset, now opt-in. - - `!important` is reserved for selectors whose semantics require - defeating consumer-authored CSS: - - `.no-print` and the hide-list (nav, aside, button, …) — these - are content negative-space and must vanish from print regardless - of authored display rules. - - `details > summary` — disclosure widget carries no print value. - - The two opt-in colour classes above — when the consumer marks a - region with them they are explicitly asking to override authored - colour, so `!important` is the contract. - ─────────────────────────────────────────────────── */ - -@layer slashed.print { - - /* Screen default: print-only content is hidden until the page is printed. */ - .print-only { - display: none; - } - - @media print { - - @page { - /* var() is not valid inside @page — hardcode values here. - Consumers who need a different page size must override @page directly. */ - size: a4 portrait; - margin: 2cm; - } - - body { - font-size: var(--sf-print-base-size, 11pt); - } - - /* Decorative shadows are paper-noise. No `!important` — an authored - shadow that explicitly survives into print is the consumer's call. */ - *, - *::before, - *::after { - box-shadow: none; - text-shadow: none; - } - - /* Expand URLs after link text — exclude fragment, javascript:, mailto:, - tel: links (already readable or noisy on paper) and .sf-link-external - (which has its own ::after marker). */ - a[href]:not([href^="#"], [href^="javascript:" i], [href^="mailto:" i], [href^="tel:" i], .sf-link-external)::after { - content: " (" attr(href) ")"; - font-size: 90%; - word-break: break-all; - } - - abbr[title]::after { - content: " (" attr(title) ")"; - } - - img, svg, canvas, figure, table, pre, blockquote { - break-inside: avoid; - /* video is excluded — it is already hidden in print (see hide list below) */ - } - - thead { - display: table-header-group; - } - - tr { - break-inside: avoid; - } - - h1, h2, h3, h4, h5, h6 { - break-after: avoid; - /* orphans/widows on single-line headings have no effect — set on p below */ - } - - p { - orphans: 3; - widows: 3; - } - - pre, blockquote { - border: var(--sf-border-width-1) solid var(--sf-color-border--strong); - padding: var(--sf-space-s); - } - - /* Hide non-essential interactive and decorative elements. - `!important` is justified: these elements are content negative-space - in print, and consumer-authored display rules must not bring them - back accidentally. */ - nav, aside, button, input, select, textarea, - audio, video, dialog, [popover], .no-print { - display: none !important; - } - - /* Reveal print-only content (overrides the screen default above). */ - .print-only { - display: revert !important; - } - - /* Preserve
content regardless of open state. Only - `summary` keeps `!important` — the disclosure chevron carries no - print value and the consumer should not be able to bring it back - inadvertently. The other two rules win against the UA stylesheet - on layered-author > UA precedence alone. */ - details { - display: block; - } - details > summary { - display: none !important; - } - details:not([open]) > :not(summary) { - display: block; - } - - /* ── Opt-in colour treatment ─────────────────────────────────── */ - - /* Force colour-faithful rendering. Use on regions where a tinted - background or coloured text encodes meaning (status pills, - severity callouts, syntax highlighting, charts). */ - .print-color-exact, - .print-color-exact * { - print-color-adjust: exact !important; - -webkit-print-color-adjust: exact !important; - } - - /* Opt back into the legacy blanket reset: transparent backgrounds - and system text colour everywhere inside the marked subtree. - Use on regions where ink-saving is the contract (corporate - boilerplate forms, append-to-existing-document templates). */ - .print-no-color, - .print-no-color *, - .print-no-color *::before, - .print-no-color *::after { - background: transparent !important; - color: CanvasText !important; - } - - } - -} - -/* ─── optional/tokens.palette.css ─── */ -/* ============================================================ - SLASHED — optional/tokens.palette.css - Optional module. Auto-generates a full numeric scale, alpha - variants, and semantic + functional aliases for the 6 brand - color tokens using color-mix(in oklab). No build step required. - oklab (Cartesian) is used instead of oklch (polar) to prevent - hue drift when mixing colors with distant hues (e.g. yellow - mixed toward a blue-ish base would sweep through greens in oklch). - - Status colors keep their subtle/muted/strong triplets in - core/tokens.css — they are NOT duplicated here. - - This is a deliberate design choice: brand colours need a full 50-950 - ramp for decorative/compositional use (backgrounds, borders, hover - states at arbitrary intensities). Status colours are functional — you - need a subtle background, a strong readable text, and a muted accent. - A full numeric scale for errors/warnings would add ~185 tokens that - most consumers never use. If you need --sf-color-error-300, replicate - the brand-palette formula: - --sf-color-error-300: color-mix(in oklab, var(--sf-color-error) 40%, var(--sf-color-surface)); - - Load after core/tokens.css: - - Token families (per brand color): - -50 … -950 numeric scale. 500 = the base color. - Tints (50-400) mix toward --sf-color-surface; shades - (600-950) mix toward --sf-color-text. Both auto-adapt - to light/dark. (base inverts the mix direction.) - -a5 … -a95 alpha variants (mixed with transparent). - shade aliases superlight/xlight/lighter/darker/xdark/superdark - → numeric steps via var() (one override propagates). - functional aliases hover/active/subtle/muted/ghost → other aliases. - ============================================================ */ - -@layer slashed.tokens { - :root { - - /* ---------------------------------------------------------- - primary - ---------------------------------------------------------- */ - --sf-color-primary-50: color-mix(in oklab, var(--sf-color-primary) 4%, var(--sf-color-surface)); - --sf-color-primary-100: color-mix(in oklab, var(--sf-color-primary) 8%, var(--sf-color-surface)); - --sf-color-primary-200: color-mix(in oklab, var(--sf-color-primary) 20%, var(--sf-color-surface)); - --sf-color-primary-300: color-mix(in oklab, var(--sf-color-primary) 40%, var(--sf-color-surface)); - --sf-color-primary-400: color-mix(in oklab, var(--sf-color-primary) 65%, var(--sf-color-surface)); - --sf-color-primary-500: var(--sf-color-primary); - --sf-color-primary-600: color-mix(in oklab, var(--sf-color-primary) 82%, var(--sf-color-text)); - --sf-color-primary-700: color-mix(in oklab, var(--sf-color-primary) 62%, var(--sf-color-text)); - --sf-color-primary-800: color-mix(in oklab, var(--sf-color-primary) 38%, var(--sf-color-text)); - --sf-color-primary-900: color-mix(in oklab, var(--sf-color-primary) 18%, var(--sf-color-text)); - --sf-color-primary-950: color-mix(in oklab, var(--sf-color-primary) 8%, var(--sf-color-text)); - - --sf-color-primary-a5: color-mix(in oklab, var(--sf-color-primary) 5%, transparent); - --sf-color-primary-a10: color-mix(in oklab, var(--sf-color-primary) 10%, transparent); - --sf-color-primary-a20: color-mix(in oklab, var(--sf-color-primary) 20%, transparent); - --sf-color-primary-a30: color-mix(in oklab, var(--sf-color-primary) 30%, transparent); - --sf-color-primary-a40: color-mix(in oklab, var(--sf-color-primary) 40%, transparent); - --sf-color-primary-a50: color-mix(in oklab, var(--sf-color-primary) 50%, transparent); - --sf-color-primary-a60: color-mix(in oklab, var(--sf-color-primary) 60%, transparent); - --sf-color-primary-a70: color-mix(in oklab, var(--sf-color-primary) 70%, transparent); - --sf-color-primary-a80: color-mix(in oklab, var(--sf-color-primary) 80%, transparent); - --sf-color-primary-a90: color-mix(in oklab, var(--sf-color-primary) 90%, transparent); - --sf-color-primary-a95: color-mix(in oklab, var(--sf-color-primary) 95%, transparent); - - --sf-color-primary-superlight: var(--sf-color-primary-50); - --sf-color-primary-xlight: var(--sf-color-primary-200); - --sf-color-primary-lighter: var(--sf-color-primary-400); - --sf-color-primary-darker: var(--sf-color-primary-600); - --sf-color-primary-xdark: var(--sf-color-primary-800); - --sf-color-primary-superdark: var(--sf-color-primary-950); - - --sf-color-primary-hover: var(--sf-color-primary-darker); - --sf-color-primary-active: var(--sf-color-primary-xdark); - --sf-color-primary-subtle: var(--sf-color-primary-a10); - --sf-color-primary-muted: var(--sf-color-primary-a30); - --sf-color-primary-ghost: var(--sf-color-primary-a5); - - - /* ---------------------------------------------------------- - secondary - ---------------------------------------------------------- */ - --sf-color-secondary-50: color-mix(in oklab, var(--sf-color-secondary) 4%, var(--sf-color-surface)); - --sf-color-secondary-100: color-mix(in oklab, var(--sf-color-secondary) 8%, var(--sf-color-surface)); - --sf-color-secondary-200: color-mix(in oklab, var(--sf-color-secondary) 20%, var(--sf-color-surface)); - --sf-color-secondary-300: color-mix(in oklab, var(--sf-color-secondary) 40%, var(--sf-color-surface)); - --sf-color-secondary-400: color-mix(in oklab, var(--sf-color-secondary) 65%, var(--sf-color-surface)); - --sf-color-secondary-500: var(--sf-color-secondary); - --sf-color-secondary-600: color-mix(in oklab, var(--sf-color-secondary) 82%, var(--sf-color-text)); - --sf-color-secondary-700: color-mix(in oklab, var(--sf-color-secondary) 62%, var(--sf-color-text)); - --sf-color-secondary-800: color-mix(in oklab, var(--sf-color-secondary) 38%, var(--sf-color-text)); - --sf-color-secondary-900: color-mix(in oklab, var(--sf-color-secondary) 18%, var(--sf-color-text)); - --sf-color-secondary-950: color-mix(in oklab, var(--sf-color-secondary) 8%, var(--sf-color-text)); - - --sf-color-secondary-a5: color-mix(in oklab, var(--sf-color-secondary) 5%, transparent); - --sf-color-secondary-a10: color-mix(in oklab, var(--sf-color-secondary) 10%, transparent); - --sf-color-secondary-a20: color-mix(in oklab, var(--sf-color-secondary) 20%, transparent); - --sf-color-secondary-a30: color-mix(in oklab, var(--sf-color-secondary) 30%, transparent); - --sf-color-secondary-a40: color-mix(in oklab, var(--sf-color-secondary) 40%, transparent); - --sf-color-secondary-a50: color-mix(in oklab, var(--sf-color-secondary) 50%, transparent); - --sf-color-secondary-a60: color-mix(in oklab, var(--sf-color-secondary) 60%, transparent); - --sf-color-secondary-a70: color-mix(in oklab, var(--sf-color-secondary) 70%, transparent); - --sf-color-secondary-a80: color-mix(in oklab, var(--sf-color-secondary) 80%, transparent); - --sf-color-secondary-a90: color-mix(in oklab, var(--sf-color-secondary) 90%, transparent); - --sf-color-secondary-a95: color-mix(in oklab, var(--sf-color-secondary) 95%, transparent); - - --sf-color-secondary-superlight: var(--sf-color-secondary-50); - --sf-color-secondary-xlight: var(--sf-color-secondary-200); - --sf-color-secondary-lighter: var(--sf-color-secondary-400); - --sf-color-secondary-darker: var(--sf-color-secondary-600); - --sf-color-secondary-xdark: var(--sf-color-secondary-800); - --sf-color-secondary-superdark: var(--sf-color-secondary-950); - - --sf-color-secondary-hover: var(--sf-color-secondary-darker); - --sf-color-secondary-active: var(--sf-color-secondary-xdark); - --sf-color-secondary-subtle: var(--sf-color-secondary-a10); - --sf-color-secondary-muted: var(--sf-color-secondary-a30); - --sf-color-secondary-ghost: var(--sf-color-secondary-a5); - - - /* ---------------------------------------------------------- - tertiary - ---------------------------------------------------------- */ - --sf-color-tertiary-50: color-mix(in oklab, var(--sf-color-tertiary) 4%, var(--sf-color-surface)); - --sf-color-tertiary-100: color-mix(in oklab, var(--sf-color-tertiary) 8%, var(--sf-color-surface)); - --sf-color-tertiary-200: color-mix(in oklab, var(--sf-color-tertiary) 20%, var(--sf-color-surface)); - --sf-color-tertiary-300: color-mix(in oklab, var(--sf-color-tertiary) 40%, var(--sf-color-surface)); - --sf-color-tertiary-400: color-mix(in oklab, var(--sf-color-tertiary) 65%, var(--sf-color-surface)); - --sf-color-tertiary-500: var(--sf-color-tertiary); - --sf-color-tertiary-600: color-mix(in oklab, var(--sf-color-tertiary) 82%, var(--sf-color-text)); - --sf-color-tertiary-700: color-mix(in oklab, var(--sf-color-tertiary) 62%, var(--sf-color-text)); - --sf-color-tertiary-800: color-mix(in oklab, var(--sf-color-tertiary) 38%, var(--sf-color-text)); - --sf-color-tertiary-900: color-mix(in oklab, var(--sf-color-tertiary) 18%, var(--sf-color-text)); - --sf-color-tertiary-950: color-mix(in oklab, var(--sf-color-tertiary) 8%, var(--sf-color-text)); - - --sf-color-tertiary-a5: color-mix(in oklab, var(--sf-color-tertiary) 5%, transparent); - --sf-color-tertiary-a10: color-mix(in oklab, var(--sf-color-tertiary) 10%, transparent); - --sf-color-tertiary-a20: color-mix(in oklab, var(--sf-color-tertiary) 20%, transparent); - --sf-color-tertiary-a30: color-mix(in oklab, var(--sf-color-tertiary) 30%, transparent); - --sf-color-tertiary-a40: color-mix(in oklab, var(--sf-color-tertiary) 40%, transparent); - --sf-color-tertiary-a50: color-mix(in oklab, var(--sf-color-tertiary) 50%, transparent); - --sf-color-tertiary-a60: color-mix(in oklab, var(--sf-color-tertiary) 60%, transparent); - --sf-color-tertiary-a70: color-mix(in oklab, var(--sf-color-tertiary) 70%, transparent); - --sf-color-tertiary-a80: color-mix(in oklab, var(--sf-color-tertiary) 80%, transparent); - --sf-color-tertiary-a90: color-mix(in oklab, var(--sf-color-tertiary) 90%, transparent); - --sf-color-tertiary-a95: color-mix(in oklab, var(--sf-color-tertiary) 95%, transparent); - - --sf-color-tertiary-superlight: var(--sf-color-tertiary-50); - --sf-color-tertiary-xlight: var(--sf-color-tertiary-200); - --sf-color-tertiary-lighter: var(--sf-color-tertiary-400); - --sf-color-tertiary-darker: var(--sf-color-tertiary-600); - --sf-color-tertiary-xdark: var(--sf-color-tertiary-800); - --sf-color-tertiary-superdark: var(--sf-color-tertiary-950); - - --sf-color-tertiary-hover: var(--sf-color-tertiary-darker); - --sf-color-tertiary-active: var(--sf-color-tertiary-xdark); - --sf-color-tertiary-subtle: var(--sf-color-tertiary-a10); - --sf-color-tertiary-muted: var(--sf-color-tertiary-a30); - --sf-color-tertiary-ghost: var(--sf-color-tertiary-a5); - - - /* ---------------------------------------------------------- - action - ---------------------------------------------------------- */ - --sf-color-action-50: color-mix(in oklab, var(--sf-color-action) 4%, var(--sf-color-surface)); - --sf-color-action-100: color-mix(in oklab, var(--sf-color-action) 8%, var(--sf-color-surface)); - --sf-color-action-200: color-mix(in oklab, var(--sf-color-action) 20%, var(--sf-color-surface)); - --sf-color-action-300: color-mix(in oklab, var(--sf-color-action) 40%, var(--sf-color-surface)); - --sf-color-action-400: color-mix(in oklab, var(--sf-color-action) 65%, var(--sf-color-surface)); - --sf-color-action-500: var(--sf-color-action); - --sf-color-action-600: color-mix(in oklab, var(--sf-color-action) 82%, var(--sf-color-text)); - --sf-color-action-700: color-mix(in oklab, var(--sf-color-action) 62%, var(--sf-color-text)); - --sf-color-action-800: color-mix(in oklab, var(--sf-color-action) 38%, var(--sf-color-text)); - --sf-color-action-900: color-mix(in oklab, var(--sf-color-action) 18%, var(--sf-color-text)); - --sf-color-action-950: color-mix(in oklab, var(--sf-color-action) 8%, var(--sf-color-text)); - - --sf-color-action-a5: color-mix(in oklab, var(--sf-color-action) 5%, transparent); - --sf-color-action-a10: color-mix(in oklab, var(--sf-color-action) 10%, transparent); - --sf-color-action-a20: color-mix(in oklab, var(--sf-color-action) 20%, transparent); - --sf-color-action-a30: color-mix(in oklab, var(--sf-color-action) 30%, transparent); - --sf-color-action-a40: color-mix(in oklab, var(--sf-color-action) 40%, transparent); - --sf-color-action-a50: color-mix(in oklab, var(--sf-color-action) 50%, transparent); - --sf-color-action-a60: color-mix(in oklab, var(--sf-color-action) 60%, transparent); - --sf-color-action-a70: color-mix(in oklab, var(--sf-color-action) 70%, transparent); - --sf-color-action-a80: color-mix(in oklab, var(--sf-color-action) 80%, transparent); - --sf-color-action-a90: color-mix(in oklab, var(--sf-color-action) 90%, transparent); - --sf-color-action-a95: color-mix(in oklab, var(--sf-color-action) 95%, transparent); - - --sf-color-action-superlight: var(--sf-color-action-50); - --sf-color-action-xlight: var(--sf-color-action-200); - --sf-color-action-lighter: var(--sf-color-action-400); - --sf-color-action-darker: var(--sf-color-action-600); - --sf-color-action-xdark: var(--sf-color-action-800); - --sf-color-action-superdark: var(--sf-color-action-950); - - --sf-color-action-hover: var(--sf-color-action-darker); - --sf-color-action-active: var(--sf-color-action-xdark); - --sf-color-action-subtle: var(--sf-color-action-a10); - --sf-color-action-muted: var(--sf-color-action-a30); - --sf-color-action-ghost: var(--sf-color-action-a5); - - - /* ---------------------------------------------------------- - neutral - ---------------------------------------------------------- */ - --sf-color-neutral-50: color-mix(in oklab, var(--sf-color-neutral) 4%, var(--sf-color-surface)); - --sf-color-neutral-100: color-mix(in oklab, var(--sf-color-neutral) 8%, var(--sf-color-surface)); - --sf-color-neutral-200: color-mix(in oklab, var(--sf-color-neutral) 20%, var(--sf-color-surface)); - --sf-color-neutral-300: color-mix(in oklab, var(--sf-color-neutral) 40%, var(--sf-color-surface)); - --sf-color-neutral-400: color-mix(in oklab, var(--sf-color-neutral) 65%, var(--sf-color-surface)); - --sf-color-neutral-500: var(--sf-color-neutral); - --sf-color-neutral-600: color-mix(in oklab, var(--sf-color-neutral) 82%, var(--sf-color-text)); - --sf-color-neutral-700: color-mix(in oklab, var(--sf-color-neutral) 62%, var(--sf-color-text)); - --sf-color-neutral-800: color-mix(in oklab, var(--sf-color-neutral) 38%, var(--sf-color-text)); - --sf-color-neutral-900: color-mix(in oklab, var(--sf-color-neutral) 18%, var(--sf-color-text)); - --sf-color-neutral-950: color-mix(in oklab, var(--sf-color-neutral) 8%, var(--sf-color-text)); - - --sf-color-neutral-a5: color-mix(in oklab, var(--sf-color-neutral) 5%, transparent); - --sf-color-neutral-a10: color-mix(in oklab, var(--sf-color-neutral) 10%, transparent); - --sf-color-neutral-a20: color-mix(in oklab, var(--sf-color-neutral) 20%, transparent); - --sf-color-neutral-a30: color-mix(in oklab, var(--sf-color-neutral) 30%, transparent); - --sf-color-neutral-a40: color-mix(in oklab, var(--sf-color-neutral) 40%, transparent); - --sf-color-neutral-a50: color-mix(in oklab, var(--sf-color-neutral) 50%, transparent); - --sf-color-neutral-a60: color-mix(in oklab, var(--sf-color-neutral) 60%, transparent); - --sf-color-neutral-a70: color-mix(in oklab, var(--sf-color-neutral) 70%, transparent); - --sf-color-neutral-a80: color-mix(in oklab, var(--sf-color-neutral) 80%, transparent); - --sf-color-neutral-a90: color-mix(in oklab, var(--sf-color-neutral) 90%, transparent); - --sf-color-neutral-a95: color-mix(in oklab, var(--sf-color-neutral) 95%, transparent); - - --sf-color-neutral-superlight: var(--sf-color-neutral-50); - --sf-color-neutral-xlight: var(--sf-color-neutral-200); - --sf-color-neutral-lighter: var(--sf-color-neutral-400); - --sf-color-neutral-darker: var(--sf-color-neutral-600); - --sf-color-neutral-xdark: var(--sf-color-neutral-800); - --sf-color-neutral-superdark: var(--sf-color-neutral-950); - - --sf-color-neutral-hover: var(--sf-color-neutral-darker); - --sf-color-neutral-active: var(--sf-color-neutral-xdark); - --sf-color-neutral-subtle: var(--sf-color-neutral-a10); - --sf-color-neutral-muted: var(--sf-color-neutral-a30); - --sf-color-neutral-ghost: var(--sf-color-neutral-a5); - - - /* ---------------------------------------------------------- - base — inverted mix direction (V-shaped ramp by design): - tints mix --sf-color-text INTO base, shades mix base INTO text. - 50 = lightest base, 950 = darkest. See architecture.md. - ---------------------------------------------------------- */ - --sf-color-base-50: color-mix(in oklab, var(--sf-color-text) 4%, var(--sf-color-base)); - --sf-color-base-100: color-mix(in oklab, var(--sf-color-text) 8%, var(--sf-color-base)); - --sf-color-base-200: color-mix(in oklab, var(--sf-color-text) 20%, var(--sf-color-base)); - --sf-color-base-300: color-mix(in oklab, var(--sf-color-text) 40%, var(--sf-color-base)); - --sf-color-base-400: color-mix(in oklab, var(--sf-color-text) 65%, var(--sf-color-base)); - --sf-color-base-500: var(--sf-color-base); - --sf-color-base-600: color-mix(in oklab, var(--sf-color-base) 82%, var(--sf-color-text)); - --sf-color-base-700: color-mix(in oklab, var(--sf-color-base) 62%, var(--sf-color-text)); - --sf-color-base-800: color-mix(in oklab, var(--sf-color-base) 38%, var(--sf-color-text)); - --sf-color-base-900: color-mix(in oklab, var(--sf-color-base) 18%, var(--sf-color-text)); - --sf-color-base-950: color-mix(in oklab, var(--sf-color-base) 8%, var(--sf-color-text)); - - --sf-color-base-a5: color-mix(in oklab, var(--sf-color-base) 5%, transparent); - --sf-color-base-a10: color-mix(in oklab, var(--sf-color-base) 10%, transparent); - --sf-color-base-a20: color-mix(in oklab, var(--sf-color-base) 20%, transparent); - --sf-color-base-a30: color-mix(in oklab, var(--sf-color-base) 30%, transparent); - --sf-color-base-a40: color-mix(in oklab, var(--sf-color-base) 40%, transparent); - --sf-color-base-a50: color-mix(in oklab, var(--sf-color-base) 50%, transparent); - --sf-color-base-a60: color-mix(in oklab, var(--sf-color-base) 60%, transparent); - --sf-color-base-a70: color-mix(in oklab, var(--sf-color-base) 70%, transparent); - --sf-color-base-a80: color-mix(in oklab, var(--sf-color-base) 80%, transparent); - --sf-color-base-a90: color-mix(in oklab, var(--sf-color-base) 90%, transparent); - --sf-color-base-a95: color-mix(in oklab, var(--sf-color-base) 95%, transparent); - - --sf-color-base-superlight: var(--sf-color-base-50); - --sf-color-base-xlight: var(--sf-color-base-200); - --sf-color-base-lighter: var(--sf-color-base-400); - --sf-color-base-darker: var(--sf-color-base-600); - --sf-color-base-xdark: var(--sf-color-base-800); - --sf-color-base-superdark: var(--sf-color-base-950); - - --sf-color-base-hover: var(--sf-color-base-darker); - --sf-color-base-active: var(--sf-color-base-xdark); - --sf-color-base-subtle: var(--sf-color-base-a10); - --sf-color-base-muted: var(--sf-color-base-a30); - --sf-color-base-ghost: var(--sf-color-base-a5); - - } -} - -/* ─── optional/tokens.sizes-extended.css ─── */ -/* ============================================================ - SLASHED — optional/tokens.sizes-extended.css - Optional module. Extends the core spacing and typography scales - with two additions: - - 1. BRIDGE VARIABLES — fluid clamp() values that span between two - non-adjacent scale steps. Where --sf-space-l compresses within - its own range, --sf-space-l-to-m starts at l's max value on - wide viewports and arrives at m's min on narrow viewports — - a larger range of motion for contexts that need it. - - 2. PER-TEXT-SIZE SUB-PROPERTIES — individual override knobs for - line-height, font-weight, letter-spacing, and max-width on - each text size step. The defaults encode typographic convention - (large text = tight leading; small text = relaxed leading) but - every value is a token authors can override. - - Load after core/tokens.css: - - Token families: - --sf-space-{A}-to-{B} spacing bridge (A > B) - --sf-text-{A}-to-{B} text bridge (A > B) - --sf-text-{size}-line-height per-size line-height - --sf-text-{size}-font-weight per-size font-weight - --sf-text-{size}-letter-spacing per-size letter-spacing - --sf-text-{size}-max-width per-size max-width - ============================================================ */ - -@layer slashed.tokens { - :root { - - /* ---------------------------------------------------------- - Spacing bridges - Full descending matrix: --sf-space-{larger}-to-{smaller} - Each token is a fluid clamp spanning from the larger step's - max value down to the smaller step's min value. - Respects --sf-space-scale like all fluid spacing tokens. - ---------------------------------------------------------- */ - - --sf-space-4xl-to-3xl: calc(clamp(2.44rem, calc(0.0885925925925926 * (100vw - 22.5rem) + 2.44rem), 8.42rem) * var(--sf-space-scale)); - --sf-space-4xl-to-2xl: calc(clamp(1.95rem, calc(0.0958518518518519 * (100vw - 22.5rem) + 1.95rem), 8.42rem) * var(--sf-space-scale)); - --sf-space-4xl-to-xl: calc(clamp(1.56rem, calc(0.10162962962963 * (100vw - 22.5rem) + 1.56rem), 8.42rem) * var(--sf-space-scale)); - --sf-space-4xl-to-l: calc(clamp(1.25rem, calc(0.106222222222222 * (100vw - 22.5rem) + 1.25rem), 8.42rem) * var(--sf-space-scale)); - --sf-space-4xl-to-m: calc(clamp(1rem, calc(0.109925925925926 * (100vw - 22.5rem) + 1rem), 8.42rem) * var(--sf-space-scale)); - --sf-space-4xl-to-s: calc(clamp(0.8rem, calc(0.112888888888889 * (100vw - 22.5rem) + 0.8rem), 8.42rem) * var(--sf-space-scale)); - --sf-space-4xl-to-xs: calc(clamp(0.64rem, calc(0.115259259259259 * (100vw - 22.5rem) + 0.64rem), 8.42rem) * var(--sf-space-scale)); - --sf-space-4xl-to-2xs: calc(clamp(0.51rem, calc(0.117185185185185 * (100vw - 22.5rem) + 0.51rem), 8.42rem) * var(--sf-space-scale)); - - --sf-space-3xl-to-2xl: calc(clamp(1.95rem, calc(0.0645925925925926 * (100vw - 22.5rem) + 1.95rem), 6.31rem) * var(--sf-space-scale)); - --sf-space-3xl-to-xl: calc(clamp(1.56rem, calc(0.0703703703703704 * (100vw - 22.5rem) + 1.56rem), 6.31rem) * var(--sf-space-scale)); - --sf-space-3xl-to-l: calc(clamp(1.25rem, calc(0.074962962962963 * (100vw - 22.5rem) + 1.25rem), 6.31rem) * var(--sf-space-scale)); - --sf-space-3xl-to-m: calc(clamp(1rem, calc(0.0786666666666667 * (100vw - 22.5rem) + 1rem), 6.31rem) * var(--sf-space-scale)); - --sf-space-3xl-to-s: calc(clamp(0.8rem, calc(0.0816296296296296 * (100vw - 22.5rem) + 0.8rem), 6.31rem) * var(--sf-space-scale)); - --sf-space-3xl-to-xs: calc(clamp(0.64rem, calc(0.084 * (100vw - 22.5rem) + 0.64rem), 6.31rem) * var(--sf-space-scale)); - --sf-space-3xl-to-2xs: calc(clamp(0.51rem, calc(0.0859259259259259 * (100vw - 22.5rem) + 0.51rem), 6.31rem) * var(--sf-space-scale)); - - --sf-space-2xl-to-xl: calc(clamp(1.56rem, calc(0.0471111111111111 * (100vw - 22.5rem) + 1.56rem), 4.74rem) * var(--sf-space-scale)); - --sf-space-2xl-to-l: calc(clamp(1.25rem, calc(0.0517037037037037 * (100vw - 22.5rem) + 1.25rem), 4.74rem) * var(--sf-space-scale)); - --sf-space-2xl-to-m: calc(clamp(1rem, calc(0.0554074074074074 * (100vw - 22.5rem) + 1rem), 4.74rem) * var(--sf-space-scale)); - --sf-space-2xl-to-s: calc(clamp(0.8rem, calc(0.0583703703703704 * (100vw - 22.5rem) + 0.8rem), 4.74rem) * var(--sf-space-scale)); - --sf-space-2xl-to-xs: calc(clamp(0.64rem, calc(0.0607407407407407 * (100vw - 22.5rem) + 0.64rem), 4.74rem) * var(--sf-space-scale)); - --sf-space-2xl-to-2xs: calc(clamp(0.51rem, calc(0.0626666666666667 * (100vw - 22.5rem) + 0.51rem), 4.74rem) * var(--sf-space-scale)); - - --sf-space-xl-to-l: calc(clamp(1.25rem, calc(0.0340740740740741 * (100vw - 22.5rem) + 1.25rem), 3.55rem) * var(--sf-space-scale)); - --sf-space-xl-to-m: calc(clamp(1rem, calc(0.0377777777777778 * (100vw - 22.5rem) + 1rem), 3.55rem) * var(--sf-space-scale)); - --sf-space-xl-to-s: calc(clamp(0.8rem, calc(0.0407407407407407 * (100vw - 22.5rem) + 0.8rem), 3.55rem) * var(--sf-space-scale)); - --sf-space-xl-to-xs: calc(clamp(0.64rem, calc(0.0431111111111111 * (100vw - 22.5rem) + 0.64rem), 3.55rem) * var(--sf-space-scale)); - --sf-space-xl-to-2xs: calc(clamp(0.51rem, calc(0.045037037037037 * (100vw - 22.5rem) + 0.51rem), 3.55rem) * var(--sf-space-scale)); - - --sf-space-l-to-m: calc(clamp(1rem, calc(0.0247407407407407 * (100vw - 22.5rem) + 1rem), 2.67rem) * var(--sf-space-scale)); - --sf-space-l-to-s: calc(clamp(0.8rem, calc(0.0277037037037037 * (100vw - 22.5rem) + 0.8rem), 2.67rem) * var(--sf-space-scale)); - --sf-space-l-to-xs: calc(clamp(0.64rem, calc(0.0300740740740741 * (100vw - 22.5rem) + 0.64rem), 2.67rem) * var(--sf-space-scale)); - --sf-space-l-to-2xs: calc(clamp(0.51rem, calc(0.032 * (100vw - 22.5rem) + 0.51rem), 2.67rem) * var(--sf-space-scale)); - - --sf-space-m-to-s: calc(clamp(0.8rem, calc(0.0177777777777778 * (100vw - 22.5rem) + 0.8rem), 2rem) * var(--sf-space-scale)); - --sf-space-m-to-xs: calc(clamp(0.64rem, calc(0.0201481481481481 * (100vw - 22.5rem) + 0.64rem), 2rem) * var(--sf-space-scale)); - --sf-space-m-to-2xs: calc(clamp(0.51rem, calc(0.0220740740740741 * (100vw - 22.5rem) + 0.51rem), 2rem) * var(--sf-space-scale)); - - --sf-space-s-to-xs: calc(clamp(0.64rem, calc(0.0127407407407407 * (100vw - 22.5rem) + 0.64rem), 1.5rem) * var(--sf-space-scale)); - --sf-space-s-to-2xs: calc(clamp(0.51rem, calc(0.0146666666666667 * (100vw - 22.5rem) + 0.51rem), 1.5rem) * var(--sf-space-scale)); - - --sf-space-xs-to-2xs: calc(clamp(0.51rem, calc(0.00918518518518518 * (100vw - 22.5rem) + 0.51rem), 1.13rem) * var(--sf-space-scale)); - - /* ---------------------------------------------------------- - Text bridges - Full descending matrix: --sf-text-{larger}-to-{smaller} - No --sf-text-scale multiplier — consistent with how the - base text tokens are defined in core/tokens.css. - ---------------------------------------------------------- */ - - --sf-text-4xl-to-3xl: clamp(2.44rem, calc(0.0417777777777778 * (100vw - 22.5rem) + 2.44rem), 5.26rem); - --sf-text-4xl-to-2xl: clamp(1.95rem, calc(0.049037037037037 * (100vw - 22.5rem) + 1.95rem), 5.26rem); - --sf-text-4xl-to-xl: clamp(1.56rem, calc(0.0548148148148148 * (100vw - 22.5rem) + 1.56rem), 5.26rem); - --sf-text-4xl-to-l: clamp(1.25rem, calc(0.0594074074074074 * (100vw - 22.5rem) + 1.25rem), 5.26rem); - --sf-text-4xl-to-m: clamp(1rem, calc(0.0631111111111111 * (100vw - 22.5rem) + 1rem), 5.26rem); - --sf-text-4xl-to-s: clamp(0.8rem, calc(0.0660740740740741 * (100vw - 22.5rem) + 0.8rem), 5.26rem); - --sf-text-4xl-to-xs: clamp(0.64rem, calc(0.0684444444444444 * (100vw - 22.5rem) + 0.64rem), 5.26rem); - --sf-text-4xl-to-2xs: clamp(0.51rem, calc(0.0703703703703704 * (100vw - 22.5rem) + 0.51rem), 5.26rem); - - --sf-text-3xl-to-2xl: clamp(1.95rem, calc(0.0296296296296296 * (100vw - 22.5rem) + 1.95rem), 3.95rem); - --sf-text-3xl-to-xl: clamp(1.56rem, calc(0.0354074074074074 * (100vw - 22.5rem) + 1.56rem), 3.95rem); - --sf-text-3xl-to-l: clamp(1.25rem, calc(0.04 * (100vw - 22.5rem) + 1.25rem), 3.95rem); - --sf-text-3xl-to-m: clamp(1rem, calc(0.0437037037037037 * (100vw - 22.5rem) + 1rem), 3.95rem); - --sf-text-3xl-to-s: clamp(0.8rem, calc(0.0466666666666667 * (100vw - 22.5rem) + 0.8rem), 3.95rem); - --sf-text-3xl-to-xs: clamp(0.64rem, calc(0.049037037037037 * (100vw - 22.5rem) + 0.64rem), 3.95rem); - --sf-text-3xl-to-2xs: clamp(0.51rem, calc(0.050962962962963 * (100vw - 22.5rem) + 0.51rem), 3.95rem); - - --sf-text-2xl-to-xl: clamp(1.56rem, calc(0.0207407407407407 * (100vw - 22.5rem) + 1.56rem), 2.96rem); - --sf-text-2xl-to-l: clamp(1.25rem, calc(0.0253333333333333 * (100vw - 22.5rem) + 1.25rem), 2.96rem); - --sf-text-2xl-to-m: clamp(1rem, calc(0.029037037037037 * (100vw - 22.5rem) + 1rem), 2.96rem); - --sf-text-2xl-to-s: clamp(0.8rem, calc(0.032 * (100vw - 22.5rem) + 0.8rem), 2.96rem); - --sf-text-2xl-to-xs: clamp(0.64rem, calc(0.0343703703703704 * (100vw - 22.5rem) + 0.64rem), 2.96rem); - --sf-text-2xl-to-2xs: clamp(0.51rem, calc(0.0362962962962963 * (100vw - 22.5rem) + 0.51rem), 2.96rem); - - --sf-text-xl-to-l: clamp(1.25rem, calc(0.0143703703703704 * (100vw - 22.5rem) + 1.25rem), 2.22rem); - --sf-text-xl-to-m: clamp(1rem, calc(0.0180740740740741 * (100vw - 22.5rem) + 1rem), 2.22rem); - --sf-text-xl-to-s: clamp(0.8rem, calc(0.021037037037037 * (100vw - 22.5rem) + 0.8rem), 2.22rem); - --sf-text-xl-to-xs: clamp(0.64rem, calc(0.0234074074074074 * (100vw - 22.5rem) + 0.64rem), 2.22rem); - --sf-text-xl-to-2xs: clamp(0.51rem, calc(0.0253333333333333 * (100vw - 22.5rem) + 0.51rem), 2.22rem); - - --sf-text-l-to-m: clamp(1rem, calc(0.00992592592592592 * (100vw - 22.5rem) + 1rem), 1.67rem); - --sf-text-l-to-s: clamp(0.8rem, calc(0.0128888888888889 * (100vw - 22.5rem) + 0.8rem), 1.67rem); - --sf-text-l-to-xs: clamp(0.64rem, calc(0.0152592592592593 * (100vw - 22.5rem) + 0.64rem), 1.67rem); - --sf-text-l-to-2xs: clamp(0.51rem, calc(0.0171851851851852 * (100vw - 22.5rem) + 0.51rem), 1.67rem); - - --sf-text-m-to-s: clamp(0.8rem, calc(0.00666666666666667 * (100vw - 22.5rem) + 0.8rem), 1.25rem); - --sf-text-m-to-xs: clamp(0.64rem, calc(0.00903703703703704 * (100vw - 22.5rem) + 0.64rem), 1.25rem); - --sf-text-m-to-2xs: clamp(0.51rem, calc(0.010962962962963 * (100vw - 22.5rem) + 0.51rem), 1.25rem); - - --sf-text-s-to-xs: clamp(0.64rem, calc(0.00444444444444444 * (100vw - 22.5rem) + 0.64rem), 0.94rem); - --sf-text-s-to-2xs: clamp(0.51rem, calc(0.00637037037037037 * (100vw - 22.5rem) + 0.51rem), 0.94rem); - - --sf-text-xs-to-2xs: clamp(0.51rem, calc(0.00281481481481481 * (100vw - 22.5rem) + 0.51rem), 0.7rem); - - /* ---------------------------------------------------------- - Per-text-size sub-properties - Override knobs for each body text size step. Defaults encode - standard typographic conventions; override any value globally - here or locally via CSS custom property on a scoped element. - - These tokens are NOT auto-applied — they are opt-in composable - overrides. Wire them up in your own rules: - .my-element { - font-size: var(--sf-text-xl); - line-height: var(--sf-text-xl-line-height); - font-weight: var(--sf-text-xl-font-weight); - letter-spacing: var(--sf-text-xl-letter-spacing); - max-width: var(--sf-text-xl-max-width); - } - ---------------------------------------------------------- */ - - --sf-text-2xs-line-height: var(--sf-leading-relaxed); - --sf-text-2xs-font-weight: var(--sf-font-weight-body); - --sf-text-2xs-letter-spacing: var(--sf-tracking-normal); - --sf-text-2xs-max-width: 55ch; - - --sf-text-xs-line-height: var(--sf-leading-relaxed); - --sf-text-xs-font-weight: var(--sf-font-weight-body); - --sf-text-xs-letter-spacing: var(--sf-tracking-normal); - --sf-text-xs-max-width: 60ch; - - --sf-text-s-line-height: var(--sf-leading-relaxed); - --sf-text-s-font-weight: var(--sf-font-weight-body); - --sf-text-s-letter-spacing: var(--sf-tracking-normal); - --sf-text-s-max-width: 65ch; - - --sf-text-m-line-height: var(--sf-leading-normal); - --sf-text-m-font-weight: var(--sf-font-weight-body); - --sf-text-m-letter-spacing: var(--sf-tracking-normal); - --sf-text-m-max-width: 65ch; - - --sf-text-l-line-height: var(--sf-leading-normal); - --sf-text-l-font-weight: var(--sf-font-weight-body); - --sf-text-l-letter-spacing: var(--sf-tracking-normal); - --sf-text-l-max-width: none; - - --sf-text-xl-line-height: var(--sf-leading-snug); - --sf-text-xl-font-weight: var(--sf-font-weight-body); - --sf-text-xl-letter-spacing: var(--sf-tracking-normal); - --sf-text-xl-max-width: none; - - --sf-text-2xl-line-height: var(--sf-leading-snug); - --sf-text-2xl-font-weight: var(--sf-font-weight-heading); - --sf-text-2xl-letter-spacing: var(--sf-tracking-normal); - --sf-text-2xl-max-width: none; - - --sf-text-3xl-line-height: var(--sf-leading-tight); - --sf-text-3xl-font-weight: var(--sf-font-weight-heading); - --sf-text-3xl-letter-spacing: var(--sf-tracking-tight); - --sf-text-3xl-max-width: none; - - --sf-text-4xl-line-height: var(--sf-leading-tight); - --sf-text-4xl-font-weight: var(--sf-font-weight-heading); - --sf-text-4xl-letter-spacing: var(--sf-tracking-tight); - --sf-text-4xl-max-width: none; - - } -} - -/* ─── optional/forms.css ─── */ -/* ============================================================ - SLASHED — optional/forms.css - @layer slashed.forms - - Classless form styling — opt-in element-level defaults for - ,