/* =========================================================================
FOLIO — a Satori UI design system
=========================================================================
An editorial look for products that publish: reports, research, finance,
long reads. Warm paper, ink-black text and buttons, one vermilion, serif
italic for the things a page names and the numbers it is about, hairline
rules in place of shadows, corners that are nearly square.
HOW IT IS LAYERED
The foundation is plain shadcn/ui. Nothing in components/ui is edited.
This file does three things on top of it:
1. Defines the Folio vocabulary as `--ds-*` custom properties, scoped
to `.ds-folio` so several systems can live on one page.
2. Bridges that vocabulary onto the shadcn tokens (`--primary`,
`--border`, `--radius` …) so every primitive re-skins untouched.
3. Restyles what tokens cannot express (ruled tables, the printed
focus outline, the square switch, small-caps labels) by `data-slot`.
SCOPE
Put `ds-folio` on <html> to theme a whole app. To theme a subtree, put
it on the subtree AND on anything that portals out of it
(DialogContent, SelectContent, DropdownMenuContent, TooltipContent):
portals render on <body>, outside the scope.
Every scope block is also written as `html.ds-folio`, so the system wins
on <html> over the project's own `:root` and `.dark` blocks, which come
later in globals.css.
WHAT THE SKIN OWNS
The cosmetic layer (section 07) sets colour, border, radius, shadow,
type and motion on each primitive, never padding, gap or width.
FONTS
Instrument Serif (italic) for titles and figures, Geist for reading and
interface, Geist Mono for labels and data. Expose them as
--font-instrument-serif / --font-geist-sans / --font-geist-mono
(next/font does this), or make the families available by name.
========================================================================= */
/* -------------------------------------------------------------------------
01 VOCABULARY — LIGHT
Ink does the work: text, rules and the primary button. Vermilion marks
what is live (links, focus, checked controls, progress) and is the one
emphasised surface a page gets. Paper is warm, never grey.
------------------------------------------------------------------------- */
html.ds-folio,
.ds-folio {
/* Fonts */
--ds-font-display:
var(--font-instrument-serif, "Instrument Serif"), "Iowan Old Style", "Palatino Linotype", Georgia, serif;
--ds-font-sans: var(--font-geist-sans, "Geist"), ui-sans-serif, system-ui, sans-serif;
--ds-font-mono: var(--font-geist-mono, "Geist Mono"), ui-monospace, SFMono-Regular, Menlo, monospace;
/* Ramps */
--ds-paper-50: oklch(0.992 0.005 85);
--ds-paper-100: oklch(0.975 0.009 85);
--ds-paper-200: oklch(0.948 0.012 82);
--ds-paper-300: oklch(0.9 0.014 80);
--ds-ink-400: oklch(0.62 0.012 70);
--ds-ink-500: oklch(0.52 0.012 65);
--ds-ink-700: oklch(0.36 0.01 60);
--ds-ink-900: oklch(0.2 0.008 60);
--ds-vermilion-500: oklch(0.6 0.19 33);
--ds-vermilion-600: oklch(0.54 0.19 32);
/* Black ink: the primary button and every rule. */
--ds-black: var(--ds-ink-900);
--ds-black-hover: var(--ds-ink-700);
--ds-on-black: var(--ds-paper-50);
/* Vermilion: the PAINT (--ds-red) and the TYPE (--ds-red-ink), which is a
step darker so links pass AA on paper. */
--ds-red: var(--ds-vermilion-500);
--ds-red-ink: var(--ds-vermilion-600);
--ds-on-red: var(--ds-paper-50);
--ds-red-tint: oklch(0.95 0.03 40);
/* Status, as tints behind words. Success is a muted green so it never
competes with the vermilion. */
--ds-success: oklch(0.55 0.11 150);
--ds-success-tint: color-mix(in oklab, var(--ds-success) 13%, transparent);
--ds-success-on-tint: oklch(0.42 0.09 150);
--ds-success-edge: color-mix(in oklab, var(--ds-success) 35%, transparent);
--ds-info: oklch(0.52 0.1 245);
--ds-info-tint: color-mix(in oklab, var(--ds-info) 12%, transparent);
--ds-info-on-tint: oklch(0.42 0.09 245);
--ds-info-edge: color-mix(in oklab, var(--ds-info) 35%, transparent);
--ds-warning: oklch(0.72 0.14 75);
--ds-warning-tint: color-mix(in oklab, var(--ds-warning) 18%, transparent);
--ds-warning-on-tint: oklch(0.45 0.1 65);
--ds-warning-edge: color-mix(in oklab, var(--ds-warning) 45%, transparent);
--ds-danger: oklch(0.52 0.19 28);
--ds-danger-tint: color-mix(in oklab, var(--ds-danger) 12%, transparent);
--ds-danger-on-tint: oklch(0.45 0.17 28);
--ds-danger-edge: color-mix(in oklab, var(--ds-danger) 35%, transparent);
/* Surfaces: the page is paper; cards are a lighter sheet laid on it. */
--ds-bg-canvas: var(--ds-paper-100);
--ds-bg-surface: var(--ds-paper-50);
--ds-bg-muted: var(--ds-paper-200);
--ds-bg-raised: var(--ds-paper-50);
--ds-text-primary: var(--ds-ink-900);
--ds-text-secondary: var(--ds-ink-700);
--ds-text-muted: var(--ds-ink-500);
/* Rules. A hairline of ink, and a full-strength rule for the line under a
table head or a masthead. */
--ds-rule: color-mix(in oklab, var(--ds-ink-900) 13%, transparent);
--ds-rule-strong: var(--ds-ink-900);
--ds-input: color-mix(in oklab, var(--ds-ink-900) 24%, transparent);
/* Focus is printed, not glowing: a 2px vermilion outline, offset. */
--ds-focus: var(--ds-red);
--ds-scrim: color-mix(in oklab, var(--ds-ink-900) 55%, transparent);
/* Shape: nearly square. Tags are square; nothing is a pill but an avatar. */
--ds-radius-tag: 0px;
--ds-radius: 2px;
--ds-radius-sheet: 3px;
/* Elevation exists only for things that float. */
--ds-elev-float: 0 1px 0 var(--ds-rule), 0 12px 32px -12px rgb(40 30 20 / 0.25);
/* Motion: quick and plain. */
--ds-ease: cubic-bezier(0.2, 0, 0, 1);
--ds-dur-hover: 120ms;
/* ---- The shadcn bridge -------------------------------------------------- */
--background: var(--ds-bg-canvas);
--foreground: var(--ds-text-primary);
--card: var(--ds-bg-surface);
--card-foreground: var(--ds-text-primary);
--popover: var(--ds-bg-raised);
--popover-foreground: var(--ds-text-primary);
--primary: var(--ds-black);
--primary-foreground: var(--ds-on-black);
--secondary: var(--ds-bg-muted);
--secondary-foreground: var(--ds-text-primary);
--muted: var(--ds-bg-muted);
--muted-foreground: var(--ds-text-muted);
--accent: var(--ds-bg-muted);
--accent-foreground: var(--ds-text-primary);
--destructive: var(--ds-danger);
--border: var(--ds-rule);
--input: var(--ds-input);
--ring: var(--ds-red);
--radius: var(--ds-radius);
--chart-1: var(--ds-black);
--chart-2: var(--ds-red);
--chart-3: var(--ds-ink-400);
--chart-4: var(--ds-info);
--chart-5: var(--ds-paper-300);
--sidebar: var(--ds-bg-canvas);
--sidebar-foreground: var(--ds-text-primary);
--sidebar-primary: var(--ds-black);
--sidebar-primary-foreground: var(--ds-on-black);
--sidebar-accent: var(--ds-bg-muted);
--sidebar-accent-foreground: var(--ds-text-primary);
--sidebar-border: var(--ds-rule);
--sidebar-ring: var(--ds-red);
/* ---- Base typography on the scope ------------------------------------ */
font-family: var(--ds-font-sans);
font-size: 15px;
line-height: 1.55;
color: var(--ds-text-primary);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-feature-settings: "ss01", "cv11";
}
/* -------------------------------------------------------------------------
02 VOCABULARY — DARK (the night edition)
Paper and ink swap: the primary button becomes a paper fill with ink
text, rules become paper at low alpha, vermilion lifts to stay legible.
------------------------------------------------------------------------- */
html.ds-folio:where(.dark, .dark *):not(:where(.dark .light, .dark .light *)),
.ds-folio:where(.dark, .dark *):not(:where(.dark .light, .dark .light *)) {
--ds-black: var(--ds-paper-100);
--ds-black-hover: var(--ds-paper-300);
--ds-on-black: var(--ds-ink-900);
--ds-red: oklch(0.66 0.18 35);
--ds-red-ink: oklch(0.74 0.15 38);
--ds-on-red: oklch(0.18 0.01 40);
--ds-red-tint: oklch(0.3 0.07 35);
--ds-success-on-tint: oklch(0.8 0.1 150);
--ds-info-on-tint: oklch(0.8 0.08 245);
--ds-warning-on-tint: oklch(0.85 0.12 80);
--ds-danger: oklch(0.65 0.18 28);
--ds-danger-on-tint: oklch(0.8 0.12 28);
--ds-success-tint: color-mix(in oklab, var(--ds-success) 22%, transparent);
--ds-info-tint: color-mix(in oklab, var(--ds-info) 25%, transparent);
--ds-danger-tint: color-mix(in oklab, var(--ds-danger) 22%, transparent);
--ds-bg-canvas: oklch(0.165 0.008 60);
--ds-bg-surface: oklch(0.2 0.008 60);
--ds-bg-muted: oklch(0.255 0.009 60);
--ds-bg-raised: oklch(0.22 0.008 60);
--ds-text-primary: var(--ds-paper-100);
--ds-text-secondary: var(--ds-paper-300);
--ds-text-muted: oklch(0.7 0.012 75);
--ds-rule: color-mix(in oklab, var(--ds-paper-100) 14%, transparent);
--ds-rule-strong: var(--ds-paper-100);
--ds-input: color-mix(in oklab, var(--ds-paper-100) 26%, transparent);
--ds-scrim: rgb(0 0 0 / 0.6);
--ds-elev-float: 0 0 0 1px var(--ds-rule), 0 16px 40px -12px rgb(0 0 0 / 0.7);
}
/* -------------------------------------------------------------------------
04 TAILWIND THEME
One addition, `font-editorial`: the display face, for anything a page
names or counts. (Not `font-display`: the site already has that class.)
------------------------------------------------------------------------- */
@theme inline {
--font-editorial: var(--ds-font-display);
}
/* -------------------------------------------------------------------------
05 UTILITIES
------------------------------------------------------------------------- */
/* Status tints for the shadcn Badge: `<Badge variant="outline"
className="tint-success">`. Defined identically in every Satori UI
system (each sets its own --ds-* values), so markup moves between them.
The state is always named in words: hue is the second signal. */
@utility tint-success {
background-color: var(--ds-success-tint);
color: var(--ds-success-on-tint);
border-color: var(--ds-success-edge);
}
@utility tint-info {
background-color: var(--ds-info-tint);
color: var(--ds-info-on-tint);
border-color: var(--ds-info-edge);
}
@utility tint-warning {
background-color: var(--ds-warning-tint);
color: var(--ds-warning-on-tint);
border-color: var(--ds-warning-edge);
}
@utility tint-danger {
background-color: var(--ds-danger-tint);
color: var(--ds-danger-on-tint);
border-color: var(--ds-danger-edge);
}
/* -------------------------------------------------------------------------
06 BASE BEHAVIOUR INSIDE THE SCOPE
------------------------------------------------------------------------- */
.ds-folio :is(code, kbd, pre, samp) {
font-family: var(--ds-font-mono);
}
.ds-folio ::selection {
background-color: color-mix(in oklab, var(--ds-red) 22%, transparent);
}
.ds-folio :is(input, textarea)::placeholder {
color: var(--ds-text-muted);
}
.ds-folio table {
font-variant-numeric: tabular-nums lining-nums;
}
/* Focus is a printed outline, never a glow, and never removed. */
:where(.ds-folio, .ds-folio *):focus-visible {
outline: 2px solid var(--ds-focus);
outline-offset: 2px;
box-shadow: none;
}
.ds-folio svg.lucide {
stroke-width: 1.6;
}
/* -------------------------------------------------------------------------
07 COSMETIC LAYER — the shadcn primitives, by data-slot
Unlayered on purpose so it beats the classes the primitives carry. It
sets skin only (colour, border, radius, shadow, type, motion), never
padding, gap or width. Things that move set `translate`, not `transform`.
------------------------------------------------------------------------- */
/* Every focusable primitive: the printed outline, and no ring utility. */
:is(
[data-slot="button"],
[data-slot="input"],
[data-slot="textarea"],
[data-slot="select-trigger"],
[data-slot="checkbox"],
[data-slot="radio-group-item"],
[data-slot="switch"],
[data-slot="tabs-trigger"]
):where(.ds-folio, .ds-folio *):focus-visible {
outline: 2px solid var(--ds-focus);
outline-offset: 2px;
box-shadow: none;
}
/* Button: square-ish, ink. Hover lightens the ink; nothing moves. */
[data-slot="button"]:where(.ds-folio, .ds-folio *) {
border-radius: var(--ds-radius);
font-weight: 500;
letter-spacing: -0.005em;
box-shadow: none;
transition:
background-color var(--ds-dur-hover) var(--ds-ease),
border-color var(--ds-dur-hover) var(--ds-ease),
color var(--ds-dur-hover) var(--ds-ease);
}
[data-slot="button"][data-variant="default"]:where(.ds-folio, .ds-folio *):hover {
background-color: var(--ds-black-hover);
}
[data-slot="button"][data-variant="outline"]:where(.ds-folio, .ds-folio *) {
border-color: var(--ds-rule-strong);
background-color: transparent;
}
[data-slot="button"][data-variant="outline"]:where(.ds-folio, .ds-folio *):hover {
background-color: var(--ds-black);
color: var(--ds-on-black);
}
[data-slot="button"][data-variant="secondary"]:where(.ds-folio, .ds-folio *):hover,
[data-slot="button"][data-variant="ghost"]:where(.ds-folio, .ds-folio *):hover {
background-color: var(--ds-bg-muted);
color: var(--ds-text-primary);
}
[data-slot="button"][data-variant="destructive"]:where(.ds-folio, .ds-folio *) {
background-color: var(--ds-danger);
color: var(--ds-paper-50);
}
[data-slot="button"][data-variant="link"]:where(.ds-folio, .ds-folio *) {
color: var(--ds-red-ink);
text-decoration-line: underline;
text-decoration-thickness: 1px;
text-underline-offset: 3px;
}
/* Badge: a square tag in small caps. */
[data-slot="badge"]:where(.ds-folio, .ds-folio *) {
border-radius: var(--ds-radius-tag);
font-family: var(--ds-font-mono);
font-size: 10.5px;
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
}
[data-slot="badge"][data-variant="default"]:where(.ds-folio, .ds-folio *) {
border-color: transparent;
background-color: var(--ds-black);
color: var(--ds-on-black);
}
[data-slot="badge"][data-variant="outline"]:where(.ds-folio, .ds-folio *):not([class*="tint-"]) {
border-color: var(--ds-rule-strong);
color: var(--ds-text-primary);
}
/* Card: a sheet with a hairline edge and no shadow. Titles are serif italic. */
[data-slot="card"]:where(.ds-folio, .ds-folio *) {
border-radius: var(--ds-radius);
border-color: var(--ds-rule);
box-shadow: none;
}
[data-slot="card-title"]:where(.ds-folio, .ds-folio *) {
font-family: var(--ds-font-display);
font-style: italic;
font-weight: 400;
font-size: 23px;
line-height: 1.15;
letter-spacing: -0.005em;
}
[data-slot="card-description"]:where(.ds-folio, .ds-folio *) {
font-size: 13.5px;
line-height: 1.45;
}
/* Fields: an underline-weight edge on paper. Focus is the outline. */
:is([data-slot="input"], [data-slot="textarea"], [data-slot="select-trigger"]):where(.ds-folio, .ds-folio *) {
border-radius: var(--ds-radius);
border-color: var(--ds-input);
background-color: var(--ds-bg-surface);
box-shadow: none;
font-size: 15px;
transition: border-color var(--ds-dur-hover) var(--ds-ease);
}
:is([data-slot="input"], [data-slot="textarea"], [data-slot="select-trigger"]):where(.ds-folio, .ds-folio *):hover {
border-color: color-mix(in oklab, var(--ds-text-primary) 45%, transparent);
}
:is([data-slot="input"], [data-slot="textarea"], [data-slot="select-trigger"]):where(.ds-folio, .ds-folio *):focus-visible {
border-color: var(--ds-rule-strong);
}
:is([data-slot="input"], [data-slot="textarea"], [data-slot="select-trigger"]):where(.ds-folio, .ds-folio *)[aria-invalid="true"] {
border-color: var(--ds-danger);
box-shadow: inset 3px 0 0 var(--ds-danger);
}
[data-slot="label"]:where(.ds-folio, .ds-folio *) {
font-size: 13.5px;
font-weight: 500;
}
/* Selection controls: square box, round radio, vermilion when on. */
[data-slot="checkbox"]:where(.ds-folio, .ds-folio *) {
border-radius: 1px;
border-color: var(--ds-rule-strong);
background-color: var(--ds-bg-surface);
box-shadow: none;
}
[data-slot="checkbox"]:where(.ds-folio, .ds-folio *):is([data-state="checked"], [data-state="indeterminate"]) {
border-color: var(--ds-red);
background-color: var(--ds-red);
color: var(--ds-on-red);
}
[data-slot="radio-group-item"]:where(.ds-folio, .ds-folio *) {
border-color: var(--ds-rule-strong);
background-color: var(--ds-bg-surface);
box-shadow: none;
}
[data-slot="radio-group-item"][data-state="checked"]:where(.ds-folio, .ds-folio *) {
border-color: var(--ds-red);
}
[data-slot="radio-group-item"][data-state="checked"]:where(.ds-folio, .ds-folio *) svg {
fill: var(--ds-red);
color: var(--ds-red);
}
/* Switch: square track and thumb, like a printed toggle. 36×20 with a 14px
thumb inset 2px; travel is 36 - 2·2 border-box - 14 - 2·2 = 14px. */
[data-slot="switch"]:where(.ds-folio, .ds-folio *) {
height: 20px;
width: 36px;
border-radius: 2px;
border: 1px solid var(--ds-rule-strong);
background-color: transparent;
box-shadow: none;
padding: 2px;
transition: background-color var(--ds-dur-hover) var(--ds-ease);
}
[data-slot="switch"][data-state="checked"]:where(.ds-folio, .ds-folio *) {
border-color: var(--ds-red);
background-color: var(--ds-red);
}
[data-slot="switch-thumb"]:where(.ds-folio, .ds-folio *) {
width: 14px;
height: 14px;
border-radius: 1px;
background-color: var(--ds-black);
box-shadow: none;
translate: 0 0;
transition: translate var(--ds-dur-hover) var(--ds-ease);
}
[data-slot="switch-thumb"][data-state="checked"]:where(.ds-folio, .ds-folio *) {
background-color: var(--ds-on-red);
translate: 16px 0;
}
/* Tabs: ruled, not boxed. The track is a rule underneath; the active tab
is ink on paper with the rule turned vermilion under it. */
[data-slot="tabs-list"][data-variant="default"]:where(.ds-folio, .ds-folio *) {
border-radius: 0;
background-color: transparent;
box-shadow: inset 0 -1px 0 var(--ds-rule);
padding: 0;
}
[data-slot="tabs-trigger"]:where(.ds-folio, .ds-folio *) {
border-radius: 0;
border: 0;
font-size: 13.5px;
font-weight: 500;
color: var(--ds-text-muted);
transition: color var(--ds-dur-hover) var(--ds-ease), box-shadow var(--ds-dur-hover) var(--ds-ease);
}
[data-slot="tabs-trigger"]:where(.ds-folio, .ds-folio *):hover {
color: var(--ds-text-primary);
}
[data-slot="tabs-list"] [data-slot="tabs-trigger"][data-state="active"]:where(.ds-folio, .ds-folio *) {
background-color: transparent;
color: var(--ds-text-primary);
box-shadow: inset 0 -2px 0 var(--ds-red);
}
[data-slot="tabs-list"][data-variant="line"] [data-slot="tabs-trigger"]:where(.ds-folio, .ds-folio *)::after {
background-color: var(--ds-red);
}
/* Floating surfaces: paper sheets with a hairline and a drop. */
:is([data-slot="select-content"], [data-slot="dropdown-menu-content"], [data-slot="dropdown-menu-sub-content"]):where(.ds-folio, .ds-folio *) {
border-radius: var(--ds-radius);
border-color: var(--ds-rule);
background-color: var(--ds-bg-raised);
box-shadow: var(--ds-elev-float);
}
:is([data-slot="select-item"], [data-slot="dropdown-menu-item"], [data-slot="dropdown-menu-checkbox-item"], [data-slot="dropdown-menu-radio-item"], [data-slot="dropdown-menu-sub-trigger"]):where(.ds-folio, .ds-folio *) {
border-radius: 1px;
font-size: 14px;
}
:is([data-slot="select-item"], [data-slot="dropdown-menu-item"], [data-slot="dropdown-menu-checkbox-item"], [data-slot="dropdown-menu-radio-item"], [data-slot="dropdown-menu-sub-trigger"]):where(.ds-folio, .ds-folio *):is(:focus, [data-highlighted], [data-state="open"]) {
background-color: var(--ds-black);
color: var(--ds-on-black);
}
:is([data-slot="select-item"], [data-slot="dropdown-menu-item"]):where(.ds-folio, .ds-folio *):is(:focus, [data-highlighted]) svg {
color: var(--ds-on-black);
}
:is([data-slot="select-label"], [data-slot="dropdown-menu-label"]):where(.ds-folio, .ds-folio *) {
font-family: var(--ds-font-mono);
font-size: 10.5px;
font-weight: 500;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--ds-text-muted);
}
:is([data-slot="select-separator"], [data-slot="dropdown-menu-separator"]):where(.ds-folio, .ds-folio *) {
background-color: var(--ds-rule);
}
/* Tooltip: an ink slip with paper text, square, no arrow. */
[data-slot="tooltip-content"]:where(.ds-folio, .ds-folio *) {
border-radius: 1px;
background-color: var(--ds-black);
color: var(--ds-on-black);
font-size: 12.5px;
}
[data-slot="tooltip-content"]:where(.ds-folio, .ds-folio *) > svg {
display: none;
}
/* Dialog: a sheet with a heavy top rule, serif italic title. */
[data-slot="dialog-content"]:where(.ds-folio, .ds-folio *) {
border-radius: var(--ds-radius-sheet);
border-color: var(--ds-rule);
border-top: 3px solid var(--ds-rule-strong);
background-color: var(--ds-bg-raised);
box-shadow: var(--ds-elev-float);
}
[data-slot="dialog-overlay"]:where(.ds-folio, .ds-folio *) {
background-color: var(--ds-scrim);
}
[data-slot="dialog-title"]:where(.ds-folio, .ds-folio *) {
font-family: var(--ds-font-display);
font-style: italic;
font-weight: 400;
font-size: 26px;
line-height: 1.1;
}
[data-slot="dialog-description"]:where(.ds-folio, .ds-folio *) {
font-size: 14px;
}
/* Table: set like a printed table. Small-caps heads over a full ink rule,
hairlines between rows, no hover wash beyond a faint paper step. */
[data-slot="table"]:where(.ds-folio, .ds-folio *) {
font-size: 14px;
}
[data-slot="table-header"] [data-slot="table-row"]:where(.ds-folio, .ds-folio *) {
border-bottom: 1px solid var(--ds-rule-strong);
}
[data-slot="table-head"]:where(.ds-folio, .ds-folio *) {
font-family: var(--ds-font-mono);
font-size: 10.5px;
font-weight: 500;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--ds-text-primary);
}
[data-slot="table-row"]:where(.ds-folio, .ds-folio *) {
border-color: var(--ds-rule);
transition: background-color var(--ds-dur-hover) var(--ds-ease);
}
[data-slot="table-body"] [data-slot="table-row"]:where(.ds-folio, .ds-folio *):hover {
background-color: color-mix(in oklab, var(--ds-bg-muted) 55%, transparent);
}
[data-slot="table-row"][data-state="selected"]:where(.ds-folio, .ds-folio *) {
background-color: var(--ds-red-tint);
}
/* Smaller parts */
[data-slot="skeleton"]:where(.ds-folio, .ds-folio *) {
border-radius: 1px;
background-color: var(--ds-bg-muted);
}
[data-slot="progress"]:where(.ds-folio, .ds-folio *) {
height: 4px;
border-radius: 0;
background-color: var(--ds-rule);
}
[data-slot="progress-indicator"]:where(.ds-folio, .ds-folio *) {
background-color: var(--ds-red);
}
[data-slot="alert"]:where(.ds-folio, .ds-folio *) {
border-radius: var(--ds-radius);
border-color: var(--ds-rule);
border-left: 3px solid var(--ds-rule-strong);
background-color: var(--ds-bg-surface);
font-size: 14px;
}
[data-slot="kbd"]:where(.ds-folio, .ds-folio *) {
border-radius: 1px;
background-color: var(--ds-bg-muted);
font-family: var(--ds-font-mono);
font-size: 11px;
color: var(--ds-text-secondary);
}
[data-slot="avatar-fallback"]:where(.ds-folio, .ds-folio *) {
background-color: var(--ds-bg-muted);
color: var(--ds-text-secondary);
font-family: var(--ds-font-display);
font-style: italic;
font-size: 14px;
}
[data-slot="separator"]:where(.ds-folio, .ds-folio *) {
background-color: var(--ds-rule);
}
/* -------------------------------------------------------------------------
08 BLOCK CONTRACT VALUES
What the shared ds-* utilities in section 09 read, said in Folio's
vocabulary. A block written against the contract becomes a Folio page:
paper chrome with a ruled edge, serif italic titles and figures,
small-caps mono labels, one vermilion sheet.
------------------------------------------------------------------------- */
html.ds-folio,
.ds-folio {
--ds-corner: round;
--ds-ink: var(--ds-red-ink);
--ds-ink-tint: var(--ds-red-tint);
--ds-emphasis: var(--ds-red);
--ds-on-emphasis: var(--ds-on-red);
--ds-emphasis-shadow: none;
--ds-quiet-fill: var(--ds-bg-muted);
/* Shell: one sheet of paper, the sidebar separated by a rule. */
--ds-shell-canvas: var(--ds-bg-canvas);
--ds-shell-panel: var(--ds-bg-canvas);
--ds-shell-edge: var(--ds-rule-strong);
--ds-shell-radius: var(--ds-radius-sheet);
--ds-shell-inset: 0px;
--ds-panel-radius: 0px;
--ds-panel-ring: none;
--ds-sidebar-bg: transparent;
--ds-sidebar-blur: none;
--ds-sidebar-edge: var(--ds-rule);
--ds-topbar-bg: transparent;
--ds-topbar-blur: none;
--ds-nav-active-bg: var(--ds-black);
--ds-nav-active-edge: transparent;
--ds-nav-active-fg: var(--ds-on-black);
/* Shape */
--ds-r-control: var(--ds-radius);
--ds-r-surface: var(--ds-radius);
--ds-surface-edge: var(--ds-rule);
--ds-surface-shadow: none;
/* Type */
--ds-text-size: 15px;
--ds-text-leading: 1.55;
--ds-small-size: 13.5px;
--ds-small-leading: 1.45;
--ds-nav-size: 14px;
--ds-title-font: var(--ds-font-display);
--ds-title-size: 38px;
--ds-title-leading: 1.05;
--ds-title-tracking: -0.01em;
--ds-title-weight: 400;
--ds-subtitle-size: 15px;
--ds-subtitle-leading: 1.5;
--ds-stat-font: var(--ds-font-display);
--ds-stat-size: 44px;
--ds-stat-leading: 1;
--ds-stat-tracking: -0.01em;
--ds-stat-weight: 400;
--ds-label-font: var(--ds-font-mono);
--ds-label-size: 10.5px;
--ds-label-leading: 1.35;
--ds-label-tracking: 0.1em;
--ds-label-weight: 500;
--ds-label-case: uppercase;
--ds-label-color: var(--ds-text-muted);
--ds-meta-font: var(--ds-font-mono);
--ds-meta-size: 11.5px;
--ds-meta-tracking: 0;
--ds-meta-color: var(--ds-text-muted);
}
/* The display face is set in italic wherever the contract uses it. */
.ds-folio :is(.ds-title, .ds-stat, .ds-mark) {
font-style: italic;
}
@media (prefers-reduced-motion: reduce) {
.ds-folio *,
.ds-folio *::before,
.ds-folio *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* <block-contract> --------------------------------------------------------
09 BLOCK CONTRACT (shared — identical in every Satori UI system)
Blocks (components/satori-ui/blocks) render in whichever system scopes
them. They read shadcn's tokens, the tint-* utilities and the ds-*
utilities below, never a system's own names. Each utility reads only
--ds-* variables, with a shadcn fallback, and each system sets those
variables in its own section 08. This section must stay byte for byte
the same in every system file: `node design-systems/check-contract.mjs`
fails if two copies drift.
The utilities set skin only. Layout (flex, padding, gaps, widths) stays
in the block's markup, so a className still wins. The one exception is
ds-shell-panel's margin: whether the content panel is inset on the
canvas is part of a system's look, not of the block.
------------------------------------------------------------------------- */
/* App frame: the outer edge and the ground the sidebar sits on. */
@utility ds-shell {
border: 1px solid var(--ds-shell-edge, var(--border));
border-radius: var(--ds-shell-radius, var(--radius));
corner-shape: var(--ds-corner, round);
background-color: var(--ds-shell-canvas, var(--background));
color: var(--foreground);
}
@utility ds-shell-sidebar {
background-color: var(--ds-sidebar-bg, transparent);
backdrop-filter: var(--ds-sidebar-blur, none);
-webkit-backdrop-filter: var(--ds-sidebar-blur, none);
border-right: 1px solid var(--ds-sidebar-edge, transparent);
}
@utility ds-shell-panel {
margin: var(--ds-shell-inset, 0) var(--ds-shell-inset, 0) var(--ds-shell-inset, 0) 0;
border-radius: var(--ds-panel-radius, 0);
corner-shape: var(--ds-corner, round);
background-color: var(--ds-shell-panel, var(--background));
box-shadow: var(--ds-panel-ring, none);
}
@utility ds-shell-topbar {
background-color: var(--ds-topbar-bg, transparent);
backdrop-filter: var(--ds-topbar-blur, none);
-webkit-backdrop-filter: var(--ds-topbar-blur, none);
border-bottom: 1px solid var(--border);
}
/* Navigation rows. The current page is marked with aria-current="page". */
@utility ds-nav-item {
border: 1px solid transparent;
border-radius: var(--ds-r-control, calc(var(--radius) - 2px));
corner-shape: var(--ds-corner, round);
color: var(--muted-foreground);
font-size: var(--ds-nav-size, 0.875rem);
font-weight: 500;
transition:
background-color var(--ds-dur-hover, 150ms) ease,
color var(--ds-dur-hover, 150ms) ease;
&:hover {
background-color: var(--ds-quiet-fill, var(--accent));
color: var(--foreground);
}
&[aria-current="page"] {
background-color: var(--ds-nav-active-bg, var(--accent));
border-color: var(--ds-nav-active-edge, transparent);
color: var(--ds-nav-active-fg, var(--foreground));
}
}
/* The product's mark: the one place the emphasis colour appears in chrome. */
@utility ds-mark {
border-radius: var(--ds-r-control, calc(var(--radius) - 2px));
corner-shape: var(--ds-corner, round);
background-color: var(--ds-emphasis, var(--primary));
color: var(--ds-on-emphasis, var(--primary-foreground));
font-family: var(--ds-title-font, inherit);
font-weight: 600;
}
/* Type. The roles a block needs that shadcn has no name for. Blocks set
copy with ds-text and ds-small rather than text-sm / text-xs, because a
system may re-point Tailwind's steps (Skita draws at 110%). */
@utility ds-text {
font-size: var(--ds-text-size, 0.875rem);
line-height: var(--ds-text-leading, 1.5);
}
@utility ds-small {
font-size: var(--ds-small-size, 0.8125rem);
line-height: var(--ds-small-leading, 1.45);
}
@utility ds-title {
font-family: var(--ds-title-font, inherit);
font-size: var(--ds-title-size, 1.5rem);
line-height: var(--ds-title-leading, 1.25);
letter-spacing: var(--ds-title-tracking, -0.01em);
font-weight: var(--ds-title-weight, 600);
text-transform: var(--ds-title-case, none);
}
@utility ds-subtitle {
font-size: var(--ds-subtitle-size, 0.875rem);
line-height: var(--ds-subtitle-leading, 1.5);
color: var(--muted-foreground);
}
@utility ds-stat {
font-family: var(--ds-stat-font, var(--ds-title-font, inherit));
font-size: var(--ds-stat-size, 1.875rem);
line-height: var(--ds-stat-leading, 1.2);
letter-spacing: var(--ds-stat-tracking, -0.02em);
font-weight: var(--ds-stat-weight, 600);
font-variant-numeric: tabular-nums;
}
/* Data about the content: KPI labels, group headings, column captions. */
@utility ds-label {
font-family: var(--ds-label-font, inherit);
font-size: var(--ds-label-size, 0.6875rem);
line-height: var(--ds-label-leading, 1.3);
letter-spacing: var(--ds-label-tracking, 0.06em);
font-weight: var(--ds-label-weight, 500);
text-transform: var(--ds-label-case, uppercase);
font-variant-numeric: tabular-nums;
color: var(--ds-label-color, var(--muted-foreground));
}
/* A small value: timestamps, IDs, amounts, lengths. */
@utility ds-meta {
font-family: var(--ds-meta-font, inherit);
font-size: var(--ds-meta-size, 0.75rem);
letter-spacing: var(--ds-meta-tracking, 0);
font-variant-numeric: tabular-nums;
color: var(--ds-meta-color, var(--muted-foreground));
}
@utility ds-link {
color: var(--ds-ink, var(--primary));
font-weight: 500;
text-underline-offset: 3px;
&:hover {
text-decoration-line: underline;
}
}
@utility ds-ink {
color: var(--ds-ink, var(--primary));
}
/* Surfaces a block draws itself (a KPI tile, a plan, an empty state). */
@utility ds-tile {
border: 1px solid var(--ds-surface-edge, var(--border));
border-radius: var(--ds-r-surface, var(--radius));
corner-shape: var(--ds-corner, round);
background-color: var(--card);
color: var(--card-foreground);
box-shadow: var(--ds-surface-shadow, none);
}
/* The one emphasised surface a view gets. Labels and meta inside it take
the label colour, so ds-label and ds-meta read on the fill. */
@utility ds-tile-emphasis {
border: 1px solid transparent;
border-radius: var(--ds-r-surface, var(--radius));
corner-shape: var(--ds-corner, round);
background-color: var(--ds-emphasis, var(--primary));
color: var(--ds-on-emphasis, var(--primary-foreground));
box-shadow: var(--ds-emphasis-shadow, none);
--ds-label-color: currentColor;
--ds-meta-color: currentColor;
--muted-foreground: color-mix(in oklab, currentColor 75%, transparent);
}
/* An icon in a wash of the action colour. */
@utility ds-well {
border-radius: var(--ds-r-control, calc(var(--radius) - 2px));
corner-shape: var(--ds-corner, round);
background-color: var(--ds-ink-tint, var(--muted));
color: var(--ds-ink, var(--primary));
}
/* A row or card that highlights on hover with the system's quiet fill. */
@utility ds-hover {
transition: background-color var(--ds-dur-hover, 150ms) ease;
&:hover {
background-color: var(--ds-quiet-fill, var(--accent));
}
}
/* </block-contract> ------------------------------------------------------- */