Skita
newFor task boards and tools people keep open all day.
Skita fits a lot on screen without feeling cramped. Most text is monospaced, the palette is grey with a green accent, and controls are compact. It suits project trackers, issue boards and other work tools.
A whole screen
The foundation plus the four Skita extras: a project home with the panel inset on the sidebar's canvas.
Projects /Skita web
Skita webBeta
Week 45. Three features in flight, one due Friday.
Shipped
7
Two more than last week
Open tasks
18
Across three features
In review
4
Waiting on a second pair
Feedback
12
Four new since Monday
| Task | Owner | Due |
|---|---|---|
| SKI-112Onboarding checklist | OL | Today |
| SKI-108Retry Stripe webhooks | JO | Done |
| SKI-104Drag to reorder roadmap | OL | Fri |
| SKI-097Export docs to PDF | AL | — |
Agent
Runs in the background.
Principles
Mono by default
Geist Mono is the body face, so the interface reads like a well-set terminal. Instrument Sans appears only where a page names itself or a number is the point.
One green, two jobs
Green paints the one action and marks work as done. The fill (primary) and the text (primary-ink) split in dark mode, so each stays legible in its role.
Tints, not solids
Status lives behind words as a 10 to 15% tint. Even the destructive button is a red tint. The only solid colour on screen is the primary button.
Rings, not borders
Cards, menus and dialogs carry a ring of the text colour at 10%, which reads the same on white, zinc and black. Shadows are Tailwind's xs to md, used sparingly.
Dense, then a little larger
Compact controls (sm is the everyday button) drawn 10% larger on desktop, and small type that starts at 14px on phones, so it reads at 100% zoom.
Quiet motion
Hover fades over 150ms, a press nudges down 1px, dialogs arrive in 100ms and menus open instantly. Focus is a neutral zinc ring: green is never spent on focus.
Installation
Add the system
pnpm dlx shadcn@latest add @satoriui/skitaOne command installs the shadcn/ui foundation it restyles (21 primitives), the stylesheet at styles/skita.css and the extras under components/satori-ui/skita/. Primitives you already have are left alone.
Import the stylesheet
@import "tailwindcss";@import "tw-animate-css";@import "../styles/skita.css";The file carries its own @theme and @utility blocks, so it has to be imported where Tailwind can see it.
Scope it
<html lang="en" className="ds-skita">Every token lives under .ds-skita. On html it themes the whole app. On a subtree it themes that subtree, and anything that portals out of it (dialogs, menus, tooltips) needs the class too.
Load the fonts
import { Geist_Mono, Instrument_Sans } from "next/font/google";const mono = Geist_Mono({ subsets: ["latin"], variable: "--font-geist-mono" });const heading = Instrument_Sans({ subsets: ["latin"], variable: "--font-instrument-sans" });<body className={`${mono.variable} ${heading.variable}`}>Outside Next.js, @fontsource-variable/geist-mono and @fontsource-variable/instrument-sans work too: the stack also names both families directly.
Teach cn() the type scale
import { clsx, type ClassValue } from "clsx";import { extendTailwindMerge } from "tailwind-merge";const twMerge = extendTailwindMerge({ extend: { classGroups: { "font-size": [{ text: ["2xs", "3xs"] }], }, },});export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs));}Out of the box, tailwind-merge reads text-2xs as a text colour and silently drops it from cn("text-2xs", "text-foreground"). The extras do not depend on this, but your own code will.
Foundations
Colour
Grey for almost everything, with green as the only accent. Status colours appear as soft tints behind words.
Action
- primaryThe primary button, checked controls, the filled tile
- primary-inkGreen as text: links, the active nav row, badges
- primary-tintThe default badge, the active nav row, icon wells
Status
- warningIn progress. Shown as a tint, or an icon colour
- infoIn review. The one tint that keeps an outline
- dangerDestructive and overdue. A tint even on buttons
Surfaces
- canvasThe sidebar and the ground the panel sits on
- panelThe content sheet and cards
- mutedTab tracks, hovers, skeletons, selected rows
- raisedMenus and dialogs
Text and lines
- foregroundBody, headings and table headers
- mutedDescriptions, IDs, timestamps
- borderFields, rows and the panel edge
- ringFocus: neutral, never green
Type
Two faces. Geist Mono carries the interface; Instrument Sans carries page titles and headline numbers. Sizes are quoted at the 110% desktop scale.
Interface
Geist Mono
Every label, field, table, button and card title. Tabular figures come free.
Titles
Instrument Sans
Opt-in with font-heading: page titles and headline numbers, nothing else.
Shape and elevation
One radius and fractions of it. Depth is a 10% ring or one of Tailwind's small shadows, rarely both.
Radii
Elevation
- ringCards, menus, dialogs: the text colour at 10%
- xsThe content panel on the canvas
- smStat tiles
- mdMenus and select lists
Components
Every demo below is the stock shadcn/ui component. Nothing in components/ui was edited: the stylesheet does all of it. The Code tab shows exactly what you would write.
Buttons
sm is the everyday size. Hover fades the fill to 80%, a press nudges down 1px, and destructive is a red tint rather than a solid.
Badges
Square-cornered tags at the smallest step. Every colour is a tint behind a word; status hues come from the tint-* utilities.
Fields
A zinc edge and no fill. Focus is a neutral ring; an error keeps a red ring and replaces the hint. 16px on phones.
Selection controls
A 4px box and a circle, filled green when on. Settings sit in an inset grouped list with a small, flat switch trailing.
Board
Group by
Notifications
Tabs
A muted track with a lifted white trigger for sections, and the line variant for views of the same data.
Cards and lists
Cards take a 10% ring instead of a border. Only one StatTile per row is filled green, so it stands out.
Shipped
7
This week
Docs
12
Two edited today
Members
5
One invite pending
ski-108-retry and links them for you.Project
Only owners can change these.
Table
Headers in the foreground at medium weight, status as an icon colour, a half-muted hover and a muted selected row.
| Task | Feature | Status | |
|---|---|---|---|
| SKI-112Onboarding checklist | Activation | In progress | |
| SKI-110Invite by link | Members | In review | |
| SKI-108Retry Stripe webhooks | Billing | Done | |
| SKI-104Drag to reorder roadmap | Roadmap | To do |
Dialog, menu and tooltip
Floating surfaces share one radius and a ring. Menus open instantly, the dialog fades in over 100ms, the tooltip is inverted.
Loading
The cascade for work measured in minutes, a skeleton for content on its way, a 4px bar for a known amount.
Agent is planning the week
Reading 18 open tasks
Extras
The components this system adds on top of the foundation. They install to components/satori-ui/skita/ and read the same tokens as everything else.
StatTile
A KPI tile with the number in Instrument Sans. One filled, green tile per row at most.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081import * as React from "react";import { cn } from "@/lib/utils";/*** A KPI tile.** At most one filled tile per row: green is the one colour Skita spends on* emphasis, and two filled tiles means neither is the point.** The label is uppercase at the 3xs step because it is data *about* the* number rather than the number itself. The number is the one place inside* a card that switches to the heading face. The caption explains the number* and never repeats it: "Across four projects" under 34, not "34 tasks".** Inner elements join their classes with plain strings rather than `cn()`:* an unconfigured tailwind-merge reads `text-3xs` as a colour and drops it* when `text-muted-foreground` follows.*/export function StatTile({label,value,caption,icon,filled = false,className,...props}: React.ComponentProps<"div"> & {label: string;value: string;caption?: string;icon?: React.ReactNode;filled?: boolean;}) {return (<divdata-slot="stat-tile"data-filled={filled || undefined}className={cn("flex flex-col gap-3 rounded-lg p-5 shadow-sm",filled ? "bg-primary text-primary-foreground" : "border border-border bg-card",className,)}{...props}><div className="flex items-start justify-between gap-3"><pclassName={`text-3xs font-medium uppercase tracking-wider tabular-nums ${filled ? "text-primary-foreground/70" : "text-muted-foreground"}`}>{label}</p>{icon && (<spanaria-hidden="true"className={`flex size-8 shrink-0 items-center justify-center rounded-md [&_svg]:size-4 ${filled ? "bg-primary-foreground/12 text-primary-foreground" : "bg-primary/10 text-primary-ink"}`}>{icon}</span>)}</div><div><pclassName={`font-heading text-xl font-medium tabular-nums ${filled ? "text-primary-foreground" : "text-foreground"}`}>{value}</p>{caption && (<p className={`mt-1 text-xs ${filled ? "text-primary-foreground/75" : "text-muted-foreground"}`}>{caption}</p>)}</div></div>);}
ListGroup
An inset grouped list for settings and feeds: lines between rows only, the heading outside the surface.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495import * as React from "react";import { cn } from "@/lib/utils";/*** An inset grouped list.** A titled group of rows on one rounded surface, with lines *between* rows* only, never above the first or below the last: that detail is what* separates it from a stack of bordered divs. The heading sits outside the* surface, in uppercase at the 3xs step, because it labels the group rather* than living in it.** Classes on elements that carry `text-2xs` / `text-3xs` are plain strings,* not `cn()`, so an unconfigured tailwind-merge cannot drop them.*/export function ListGroup({title,footnote,className,children,...props}: React.ComponentProps<"section"> & {title?: string;footnote?: string;}) {return (<section className={cn("flex flex-col gap-2", className)} {...props}>{title && (<h3 className="px-1 text-3xs font-medium uppercase tracking-wider tabular-nums text-muted-foreground">{title}</h3>)}{/* `grow`, not `flex-1`: it fills a stretched grid row, so two groupsside by side share a bottom edge, but keeps its own height whenthere is nothing to fill. */}<div className="grow overflow-hidden rounded-lg border border-border bg-card">{children}</div>{footnote && <p className="px-4 text-2xs text-muted-foreground">{footnote}</p>}</section>);}/*** One row: leading glyph, stacked text, trailing value or control.** Rows are 12px tall in padding rather than Apple's 18px. Density is* balanced, not airy: a settings screen still has to scan in one screenful.*/export function ListRow({leading,inset,title,description,trailing,className,children,...props}: Omit<React.ComponentProps<"div">, "title"> & {leading?: React.ReactNode;/*** Reserve the leading column without drawing anything in it. Set it on the* rows of a group where some other row has a `leading` glyph, or their text* starts 44px further left and the group gets a ragged edge.*/inset?: boolean;title: React.ReactNode;description?: React.ReactNode;trailing?: React.ReactNode;}) {return (<divdata-slot="list-row"className={cn(// The line lives on the row and is dropped on the last one, so a// group never ends on a stray hairline."flex items-center gap-3 border-b border-border px-4 py-3 last:border-b-0",className,)}{...props}>{leading ? (<span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary-ink [&_svg]:size-4">{leading}</span>) : inset ? (<span aria-hidden="true" className="size-8 shrink-0" />) : null}<div className="min-w-0 flex-1"><div className="text-xs/relaxed font-medium text-foreground">{title}</div>{description && <div className="mt-0.5 text-xs text-muted-foreground">{description}</div>}{children}</div>{trailing && <div className="shrink-0">{trailing}</div>}</div>);}
CascadeLoader
Four squares and a travelling pulse, for work measured in minutes. Paints in currentColor.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647import * as React from "react";import { cn } from "@/lib/utils";/*** Delay per square, in cycle order, NOT in DOM order.** The grid fills top-left, top-right, bottom-left, bottom-right, so these run* the pulse clockwise: TL, TR, BR, BL. Sorting them to 0 / 0.2 / 0.4 / 0.6* would draw a Z, which looks like the obvious fix and is not one.*/const DELAYS = ["0s", "0.2s", "0.6s", "0.4s"];/*** A four-square cascade, for work that takes minutes rather than moments.** A spinner says "wait" and turns at the same rate whether a request is one* second or four minutes from returning. This says "something is* progressing": the pulse travels, so a glance shows the page is alive* without implying it is nearly done. Use a spinner inside buttons; use this* where an agent, an import or a render is running.** Squares paint in `currentColor`, so it takes the colour of whatever it sits* in. Size it with one class: cells are fractions and the gap a percentage.* The keyframes (`ds-skita-cascade`) live in skita.css.*/export function CascadeLoader({className,label = "Loading",...props}: React.ComponentProps<"div"> & { label?: string }) {return (<divrole="status"aria-label={label}className={cn("grid size-[23px] grid-cols-2 grid-rows-2 gap-[13%]", className)}{...props}>{DELAYS.map((delay) => (<spankey={delay}className="rounded-[2px] bg-current animate-[ds-skita-cascade_1.6s_ease-in-out_infinite] motion-reduce:animate-none motion-reduce:opacity-40"style={{ animationDelay: delay }}/>))}</div>);}
PageHeader
Every screen opens with a title in the heading face, a subtitle and its actions on the same row.
123456789101112131415161718192021222324252627282930313233343536373839import type { ReactNode } from "react";import { cn } from "@/lib/utils";/*** Every screen opens with a title and a subtitle.** Page-level actions sit on the title row, never in the top bar: an action* belongs next to the thing it acts on. The title is one of the few places* Skita switches to the heading face. Titles are sentence case.*/export function PageHeader({title,subtitle,actions,badge,className,}: {title: string;subtitle?: string;actions?: ReactNode;/** Beside the title: a Beta tag, not a status. */badge?: ReactNode;className?: string;}) {return (<div className={cn("mb-6 flex flex-wrap items-end justify-between gap-x-6 gap-y-3", className)}><div className="min-w-0"><h1 className="flex items-center gap-2.5 font-heading text-lg font-medium text-foreground">{title}{badge}</h1>{subtitle && (<p className="mt-1 max-w-[68ch] text-xs/relaxed text-muted-foreground">{subtitle}</p>)}</div>{actions && <div className="flex shrink-0 items-center gap-2">{actions}</div>}</div>);}
Stylesheet
The whole system is this one file. Read it top to bottom: the vocabulary, the dark theme, the palettes, the Tailwind theme, the utilities, and then the cosmetic layer over each primitive.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003/* =========================================================================SKITA — a Satori UI design system=========================================================================A dense, monospaced workspace look: zinc neutrals, one green, Geist Monoas the body face and Instrument Sans for headings. Hairline ringsinstead of borders, a neutral focus ring, tinted (never solid) statuscolour, and menus that open instantly.HOW IT IS LAYEREDThe foundation is plain shadcn/ui. Nothing in components/ui is edited.This file does four things on top of it:1. Defines the Skita vocabulary as `--ds-*` custom properties, scopedto `.ds-skita` so several systems can live on one page.2. Bridges that vocabulary onto the shadcn tokens (`--primary`,`--border`, `--radius` …). Skita has no second semantic layer:product code reads the shadcn names (bg-primary,text-muted-foreground) plus one addition, text-primary-ink.3. Sets the scale. Skita is drawn 10% larger than Tailwind's defaultson desktop and with larger small type on phones. That is done byre-pointing Tailwind's own `--spacing` and `--text-*` variablesinside the scope, so utilities keep working and nothing isoverridden.4. Restyles the details tokens cannot express (the tinted destructivebutton, the 1px press, the hairline ring, the small switch, instantmenus) by `data-slot`.SCOPEPut `ds-skita` on <html> to theme a whole app. To theme a subtree, putit 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-skita`. This file has tobe imported at the top of globals.css, so the project's own `:root` and`.dark` token blocks come after it; the extra type selector is what letsthe system win on <html> instead of losing on source order.WHAT THE SKIN OWNSThe cosmetic layer (section 07) sets colour, border, radius, shadow,type and motion on each primitive. It leaves layout alone (padding,gap, width), so a className like `gap-0 pt-0` on a Card still works.Density comes from the size props: Skita's everyday button is`size="sm"`.FONTSGeist Mono for everything that is not a heading, Instrument Sans forheadings. Expose them as --font-geist-mono / --font-instrument-sans(next/font does this), or make "Geist Mono" and "Instrument Sans"available by name (@fontsource-variable works).========================================================================= *//* -------------------------------------------------------------------------01 VOCABULARY — LIGHTZinc for everything, green for the one action and for "done". Statushues appear only as tints behind words.------------------------------------------------------------------------- */html.ds-skita,.ds-skita {/* Fonts */--ds-font-mono:var(--font-geist-mono, "Geist Mono Variable"), "Geist Mono", ui-monospace, SFMono-Regular,Menlo, Consolas, monospace;--ds-font-heading:var(--font-instrument-sans, "Instrument Sans Variable"), "Instrument Sans", ui-sans-serif,system-ui, sans-serif;/* Ramps the semantic layer points at (Tailwind's zinc and green) */--ds-zinc-50: oklch(0.985 0 0);--ds-zinc-100: oklch(0.967 0.001 286.375);--ds-zinc-200: oklch(0.92 0.004 286.32);--ds-zinc-400: oklch(0.705 0.015 286.067);--ds-zinc-500: oklch(0.552 0.016 285.938);--ds-zinc-800: oklch(0.274 0.006 286.033);--ds-zinc-900: oklch(0.21 0.006 285.885);--ds-zinc-950: oklch(0.141 0.005 285.823);--ds-green-50: oklch(0.982 0.018 155.826);--ds-green-700: oklch(0.527 0.154 150.069);--ds-green-800: oklch(0.448 0.119 151.328);/* The green pair. --ds-primary is the PAINT (buttons, checked controls,the filled tile). --ds-primary-ink is the TYPE colour (links, theactive nav row, badges). They are the same in light mode and split indark, where the fill is too dark to read as text. */--ds-primary: var(--ds-green-700);--ds-primary-hover: color-mix(in oklab, var(--ds-primary) 80%, transparent);--ds-on-primary: var(--ds-green-50);--ds-primary-ink: var(--ds-primary);--ds-primary-tint: color-mix(in oklab, var(--ds-primary) 10%, transparent);/* Surfaces. The canvas is the sidebar's ground; the content panel sitson it as a white, hairline-ringed sheet. */--ds-bg-canvas: var(--ds-zinc-50);--ds-bg-panel: oklch(1 0 0);--ds-bg-muted: var(--ds-zinc-100);--ds-bg-raised: oklch(1 0 0);--ds-text-primary: var(--ds-zinc-950);--ds-text-secondary: oklch(0.21 0.006 285.885);--ds-text-muted: var(--ds-zinc-500);/* Lines. Cards and floating surfaces take a ring of the text colour at10%, not a border colour, so they read the same on any ground. */--ds-border: var(--ds-zinc-200);--ds-input: var(--ds-zinc-200);--ds-hairline: color-mix(in oklab, var(--ds-text-primary) 10%, transparent);--ds-highlight: color-mix(in oklab, var(--ds-text-primary) 10%, transparent);/* Focus is neutral: a zinc edge plus a 2px ring at 30%. Green is neverspent on focus. */--ds-ring: var(--ds-zinc-400);--ds-focus-ring: 0 0 0 2px color-mix(in oklab, var(--ds-ring) 30%, transparent);--ds-focus-ring-danger: 0 0 0 2px color-mix(in oklab, var(--ds-danger) 20%, transparent);/* Status: tints behind words. `-edge` is the badge outline; most aretransparent, info keeps a line so it does not read as a disabled grey. */--ds-danger: oklch(0.577 0.245 27.325);--ds-success: var(--ds-primary);--ds-success-tint: color-mix(in oklab, var(--ds-primary) 15%, transparent);--ds-success-on-tint: var(--ds-primary-ink);--ds-success-edge: transparent;--ds-warning: oklch(0.769 0.188 70.08);--ds-warning-tint: color-mix(in oklab, var(--ds-warning) 15%, transparent);--ds-warning-on-tint: oklch(0.555 0.163 48.998);--ds-warning-edge: transparent;--ds-info: oklch(0.685 0.169 237.323);--ds-info-tint: color-mix(in oklab, var(--ds-info) 10%, transparent);--ds-info-on-tint: oklch(0.443 0.11 240.79);--ds-info-edge: color-mix(in oklab, oklch(0.588 0.158 241.966) 40%, transparent);--ds-danger-tint: color-mix(in oklab, var(--ds-danger) 10%, transparent);--ds-danger-on-tint: var(--ds-danger);--ds-danger-edge: transparent;/* Shape: one radius, and the rest are fractions of it. */--ds-radius: 0.625rem;--ds-radius-xs: 2px;--ds-radius-box: 4px;--ds-radius-sm: calc(var(--ds-radius) * 0.6);--ds-radius-md: calc(var(--ds-radius) * 0.8);--ds-radius-lg: var(--ds-radius);--ds-radius-xl: calc(var(--ds-radius) * 1.4);/* Elevation is Tailwind's own shadow-xs / sm / md. A surface takes aring or a shadow, rarely both. */--ds-elev-ring: 0 0 0 1px var(--ds-hairline);--ds-elev-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05);--ds-elev-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);--ds-elev-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);--ds-scrim: rgb(0 0 0 / 0.8);/* Motion: short and plain. 100ms in for overlays, no springs. */--ds-ease: cubic-bezier(0, 0, 0.2, 1);--ds-dur-fast: 100ms;--ds-dur-hover: 150ms;--ds-dur-slide: 200ms;/* ---- The shadcn bridge -------------------------------------------------Every token the foundation reads, mapped onto the vocabulary above.This is what re-skins components/ui without editing a single file. */--background: var(--ds-bg-panel);--foreground: var(--ds-text-primary);--card: var(--ds-bg-panel);--card-foreground: var(--ds-text-primary);--popover: var(--ds-bg-raised);--popover-foreground: var(--ds-text-primary);--primary: var(--ds-primary);--primary-foreground: var(--ds-on-primary);--primary-ink: var(--ds-primary-ink);--secondary: var(--ds-bg-muted);--secondary-foreground: var(--ds-text-secondary);--muted: var(--ds-bg-muted);--muted-foreground: var(--ds-text-muted);--accent: var(--ds-bg-muted);--accent-foreground: var(--ds-text-secondary);--destructive: var(--ds-danger);--border: var(--ds-border);--input: var(--ds-input);--ring: var(--ds-ring);--radius: var(--ds-radius);--chart-1: var(--ds-zinc-200);--chart-2: var(--ds-zinc-500);--chart-3: oklch(0.442 0.017 285.786);--chart-4: oklch(0.37 0.013 285.805);--chart-5: var(--ds-zinc-800);--sidebar: var(--ds-bg-canvas);--sidebar-foreground: var(--ds-text-primary);--sidebar-primary: oklch(0.627 0.194 149.214);--sidebar-primary-foreground: var(--ds-on-primary);--sidebar-accent: var(--ds-bg-muted);--sidebar-accent-foreground: var(--ds-text-secondary);--sidebar-border: var(--ds-border);--sidebar-ring: var(--ds-ring);/* ---- Scale (phones) ----------------------------------------------------Small type starts large enough to read on a phone. Section 03 brings itback to the compact desktop scale from 40rem up. */--text-xs: 0.875rem;--text-2xs: 0.8125rem;--text-3xs: 0.75rem;/* ---- Base typography on the scope ---------------------------------- */font-family: var(--ds-font-mono);font-size: var(--text-xs);line-height: 1.625;color: var(--ds-text-primary);-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;}/* -------------------------------------------------------------------------02 VOCABULARY — DARKThe sidebar steps UP from the panel instead of down. The green filldarkens and the green ink lightens, so each stays legible in its role.Honours the host's `.dark`.------------------------------------------------------------------------- */html.ds-skita:where(.dark, .dark *):not(:where(.dark .light, .dark .light *)),.ds-skita:where(.dark, .dark *):not(:where(.dark .light, .dark .light *)) {--ds-primary: var(--ds-green-800);--ds-primary-ink: oklch(0.8 0.16 152);--ds-primary-tint: color-mix(in oklab, var(--ds-green-700) 18%, transparent);--ds-bg-canvas: var(--ds-zinc-900);--ds-bg-panel: var(--ds-zinc-950);--ds-bg-muted: var(--ds-zinc-800);--ds-bg-raised: var(--ds-zinc-900);--ds-text-primary: var(--ds-zinc-50);--ds-text-secondary: var(--ds-zinc-50);--ds-text-muted: var(--ds-zinc-400);--ds-border: oklch(1 0 0 / 10%);--ds-input: oklch(1 0 0 / 15%);--ds-ring: var(--ds-zinc-500);--ds-danger: oklch(0.704 0.191 22.216);--ds-success-tint: color-mix(in oklab, var(--ds-green-700) 22%, transparent);--ds-warning-on-tint: oklch(0.828 0.189 84.429);--ds-info-on-tint: oklch(0.828 0.111 230.318);--ds-info-edge: color-mix(in oklab, oklch(0.746 0.16 232.661) 40%, transparent);--ds-danger-tint: color-mix(in oklab, var(--ds-danger) 20%, transparent);--sidebar-primary: oklch(0.723 0.219 149.579);}/* -------------------------------------------------------------------------03 SCALE — DESKTOPSkita's own app sets the root to 110% from 40rem up because the stockscale read small. A scoped stylesheet cannot touch the root, so itscales Tailwind's spacing, type and radius variables by the same 10%instead. Utilities still read those variables, so `h-7`, `px-3` and`text-xs` all land where they do in Skita, and a className still wins.------------------------------------------------------------------------- */@media (width >= 40rem) {html.ds-skita,.ds-skita {--spacing: 0.275rem;--ds-radius: 0.6875rem;--text-3xs: 0.6875rem;--text-2xs: 0.75625rem;--text-xs: 0.825rem;--text-sm: 0.9625rem;--text-base: 1.1rem;--text-lg: 1.2375rem;--text-xl: 1.375rem;--text-2xl: 1.65rem;}}/* -------------------------------------------------------------------------04 TAILWIND THEMETwo type steps below `xs`, the heading face, and green as text. Everyname here either exists only in Skita or is defined exactly as otherSatori UI systems define it, so two systems can share a build.------------------------------------------------------------------------- */@theme inline {--font-heading: var(--ds-font-heading);--color-primary-ink: var(--primary-ink);}@theme {--text-2xs: 0.6875rem;--text-2xs--line-height: calc(1 / 0.6875);--text-3xs: 0.625rem;--text-3xs--line-height: calc(0.875 / 0.625);}/* -------------------------------------------------------------------------05 UTILITIES------------------------------------------------------------------------- *//* Status tints for the shadcn Badge: `<Badge variant="outline"className="tint-success">`. Defined identically in every Satori UIsystem (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);}/* The cascade loader's pulse (extras/cascade-loader): each square spendsmost of the cycle dim and peaks a third of the way through, so foursquares read as one travelling pulse. */@keyframes ds-skita-cascade {0%,70%,100% {opacity: 0.1;transform: scale(0.85);}35% {opacity: 1;transform: scale(1);}}/* -------------------------------------------------------------------------06 BASE BEHAVIOUR INSIDE THE SCOPEHeadings are NOT switched to Instrument Sans here: Skita opts in with`font-heading` on page titles and big numbers only, and everything else,card and dialog titles included, stays in the mono body face.------------------------------------------------------------------------- */@layer base {.ds-skita :is(code, pre, samp, kbd) {font-family: var(--ds-font-mono);}}.ds-skita ::selection {background-color: var(--ds-primary);color: var(--ds-on-primary);}.ds-skita :is(input, textarea)::placeholder {color: var(--ds-text-muted);}:where(.ds-skita, .ds-skita *) {outline-color: color-mix(in oklab, var(--ds-ring) 50%, transparent);}.ds-skita table {font-variant-numeric: tabular-nums;}/* -------------------------------------------------------------------------07 COSMETIC LAYER — the shadcn primitives, by data-slotThese rules are unlayered on purpose: they have to beat the utilityclasses the primitives already carry. That also means they beat aclassName you pass, so they stick to skin (colour, border, radius,shadow, type, motion) and never set padding, gap or width.Tailwind v4 moves things with the `translate` property, not`transform`. Anything here that moves an element sets `translate`, orit would stack on top of the primitive's own offset.------------------------------------------------------------------------- *//* Shared focus: a zinc edge and a 2px ring at 30%. */: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-skita, .ds-skita *):focus-visible {border-color: var(--ds-ring);outline: none;box-shadow: var(--ds-focus-ring);}/* Button: 0.8× radius, 12px medium mono. Press nudges the button down 1pxrather than shrinking it. Hover fades the fill to 80%. */[data-slot="button"]:where(.ds-skita, .ds-skita *) {border-radius: var(--ds-radius-md);font-size: var(--text-xs);line-height: 1.625;font-weight: 500;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),translate var(--ds-dur-fast) var(--ds-ease);}[data-slot="button"]:where(.ds-skita, .ds-skita *):active:not(:disabled, [aria-haspopup]) {translate: 0 1px;}[data-slot="button"][data-variant="default"]:where(.ds-skita, .ds-skita *):hover {background-color: var(--ds-primary-hover);}[data-slot="button"][data-variant="outline"]:where(.ds-skita, .ds-skita *) {border-color: var(--ds-border);background-color: transparent;}[data-slot="button"][data-variant="outline"]:where(.ds-skita, .ds-skita *):where(.dark *, .dark):not(:where(.dark .light, .dark .light *)) {background-color: color-mix(in oklab, var(--ds-input) 30%, transparent);}[data-slot="button"][data-variant="outline"]:where(.ds-skita, .ds-skita *):is(:hover, [aria-expanded="true"]) {background-color: color-mix(in oklab, var(--ds-input) 50%, transparent);color: var(--ds-text-primary);}[data-slot="button"][data-variant="secondary"]:where(.ds-skita, .ds-skita *):hover {background-color: color-mix(in oklch, var(--ds-bg-muted), var(--ds-text-primary) 5%);}[data-slot="button"][data-variant="ghost"]:where(.ds-skita, .ds-skita *):is(:hover, [aria-expanded="true"]) {background-color: var(--ds-bg-muted);color: var(--ds-text-primary);}/* Destructive is a tint, never a solid red: deleting should lookdeliberate, not loud. */[data-slot="button"][data-variant="destructive"]:where(.ds-skita, .ds-skita *) {background-color: var(--ds-danger-tint);color: var(--ds-danger);}[data-slot="button"][data-variant="destructive"]:where(.ds-skita, .ds-skita *):hover {background-color: color-mix(in oklab, var(--ds-danger) 20%, transparent);}[data-slot="button"][data-variant="destructive"]:where(.ds-skita, .ds-skita *):focus-visible {border-color: color-mix(in oklab, var(--ds-danger) 40%, transparent);box-shadow: var(--ds-focus-ring-danger);}[data-slot="button"][data-variant="link"]:where(.ds-skita, .ds-skita *) {color: var(--ds-primary-ink);}/* Badge: a small square-cornered tag at the 3xs step, tinted rather thansolid. The default badge is the green tint; destructive is the red one. */[data-slot="badge"]:where(.ds-skita, .ds-skita *) {border-radius: var(--ds-radius-sm);font-size: var(--text-3xs);line-height: var(--text-3xs--line-height);font-weight: 500;}[data-slot="badge"][data-variant="default"]:where(.ds-skita, .ds-skita *) {border-color: transparent;background-color: var(--ds-primary-tint);color: var(--ds-primary-ink);}[data-slot="badge"][data-variant="outline"]:where(.ds-skita, .ds-skita *):not([class*="tint-"]) {border-color: var(--ds-border);color: var(--ds-text-muted);}[data-slot="badge"][data-variant="destructive"]:where(.ds-skita, .ds-skita *) {border-color: transparent;background-color: var(--ds-danger-tint);color: var(--ds-danger);}/* Card: one radius, a 10% ring in place of a border, no shadow. Titles stayin the mono body face; only page-level headings switch to Instrument. */[data-slot="card"]:where(.ds-skita, .ds-skita *) {border-radius: var(--ds-radius-lg);border-color: var(--ds-hairline);box-shadow: none;font-size: var(--text-xs);line-height: 1.625;}[data-slot="card-title"]:where(.ds-skita, .ds-skita *) {font-size: var(--text-sm);line-height: 1.4;font-weight: 500;}[data-slot="card-description"]:where(.ds-skita, .ds-skita *) {font-size: var(--text-xs);line-height: 1.625;}/* Fields: 0.8× radius, a zinc edge, no fill in light mode. 12px type ondesktop, 16px on phones so iOS never zooms the page on focus. */:is([data-slot="input"], [data-slot="textarea"], [data-slot="select-trigger"]):where(.ds-skita, .ds-skita *) {border-radius: var(--ds-radius-md);border-color: var(--ds-border);background-color: transparent;box-shadow: none;font-size: 1rem;line-height: 1.625;transition:border-color var(--ds-dur-hover) var(--ds-ease),box-shadow var(--ds-dur-hover) var(--ds-ease);}@media (width >= 40rem) {:is([data-slot="input"], [data-slot="textarea"], [data-slot="select-trigger"]):where(.ds-skita, .ds-skita *) {font-size: var(--text-xs);}}:is([data-slot="input"], [data-slot="textarea"], [data-slot="select-trigger"]):where(.ds-skita, .ds-skita *):where(.dark *, .dark):not(:where(.dark .light, .dark .light *)) {background-color: color-mix(in oklab, var(--ds-input) 30%, transparent);}[data-slot="select-trigger"]:where(.ds-skita, .ds-skita *) {background-color: color-mix(in oklab, var(--ds-input) 20%, transparent);}:is([data-slot="input"], [data-slot="textarea"], [data-slot="select-trigger"]):where(.ds-skita, .ds-skita *)[aria-invalid="true"] {border-color: var(--ds-danger);box-shadow: var(--ds-focus-ring-danger);}[data-slot="label"]:where(.ds-skita, .ds-skita *) {font-size: var(--text-xs);font-weight: 500;}/* Selection controls: a 4px box and a circle, both filled green when on. */[data-slot="checkbox"]:where(.ds-skita, .ds-skita *) {border-radius: var(--ds-radius-box);border-color: var(--ds-input);box-shadow: none;}[data-slot="checkbox"]:where(.ds-skita, .ds-skita *):is([data-state="checked"], [data-state="indeterminate"]) {border-color: var(--ds-primary);background-color: var(--ds-primary);color: var(--ds-on-primary);}[data-slot="radio-group-item"]:where(.ds-skita, .ds-skita *) {border-color: var(--ds-input);box-shadow: none;}[data-slot="radio-group-item"][data-state="checked"]:where(.ds-skita, .ds-skita *) {border-color: var(--ds-primary);background-color: var(--ds-primary);}[data-slot="radio-group-item"][data-state="checked"]:where(.ds-skita, .ds-skita *) svg {fill: var(--ds-on-primary);color: var(--ds-on-primary);}/* Switch: small and flat, 30×18 with a 15px thumb that reaches both ends.Green when on, no shadow. Travel is the inner width (30 - 2px ofborder) minus the thumb, set as `translate` so it replaces theprimitive's own offset. */[data-slot="switch"]:where(.ds-skita, .ds-skita *) {height: 18px;width: 30px;box-shadow: none;transition: background-color var(--ds-dur-hover) var(--ds-ease);}[data-slot="switch"][data-state="checked"]:where(.ds-skita, .ds-skita *) {background-color: var(--ds-primary);}[data-slot="switch"][data-size="sm"]:where(.ds-skita, .ds-skita *) {height: 15px;width: 26px;}[data-slot="switch-thumb"]:where(.ds-skita, .ds-skita *) {width: 15px;height: 15px;translate: 0 0;transition: translate var(--ds-dur-hover) var(--ds-ease);}[data-slot="switch-thumb"][data-state="checked"]:where(.ds-skita, .ds-skita *) {translate: 13px 0;}[data-slot="switch"][data-size="sm"] [data-slot="switch-thumb"]:where(.ds-skita, .ds-skita *) {width: 12px;height: 12px;}[data-slot="switch"][data-size="sm"] [data-slot="switch-thumb"][data-state="checked"]:where(.ds-skita, .ds-skita *) {translate: 12px 0;}/* Tabs: a muted track and a lifted white trigger with no shadow; in darkmode the trigger is a faint fill with an edge. */[data-slot="tabs-list"][data-variant="default"]:where(.ds-skita, .ds-skita *) {border-radius: var(--ds-radius-lg);background-color: var(--ds-bg-muted);}[data-slot="tabs-trigger"]:where(.ds-skita, .ds-skita *) {border-radius: var(--ds-radius-md);font-size: var(--text-xs);font-weight: 500;}[data-slot="tabs-list"][data-variant="default"] [data-slot="tabs-trigger"][data-state="active"]:where(.ds-skita, .ds-skita *) {background-color: var(--ds-bg-panel);color: var(--ds-text-primary);box-shadow: none;}[data-slot="tabs-list"][data-variant="default"] [data-slot="tabs-trigger"][data-state="active"]:where(.ds-skita, .ds-skita *):where(.dark *, .dark):not(:where(.dark .light, .dark .light *)) {border-color: var(--ds-input);background-color: color-mix(in oklab, var(--ds-input) 30%, transparent);}/* Floating surfaces: one radius, a 10% ring, shadow-md. Menus open andclose instantly: a menu is a tool, not a reveal. Items highlight withthe text colour at 10%, and a destructive item only turns red at rest. */:is([data-slot="select-content"], [data-slot="dropdown-menu-content"], [data-slot="dropdown-menu-sub-content"]):where(.ds-skita, .ds-skita *) {border-radius: var(--ds-radius-lg);border-color: var(--ds-hairline);background-color: var(--ds-bg-raised);box-shadow: var(--ds-elev-md);animation: none;}: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-skita, .ds-skita *) {border-radius: var(--ds-radius-md);font-size: var(--text-xs);line-height: 1.625;}: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-skita, .ds-skita *):is(:focus, [data-highlighted], [data-state="open"]) {background-color: var(--ds-highlight);color: var(--ds-text-primary);}[data-slot="dropdown-menu-item"][data-variant="destructive"]:where(.ds-skita, .ds-skita *):is(:focus, [data-highlighted]) {background-color: var(--ds-highlight);color: var(--ds-text-primary);}[data-slot="dropdown-menu-item"][data-variant="destructive"]:where(.ds-skita, .ds-skita *):is(:focus, [data-highlighted]) svg {color: var(--ds-text-primary);}:is([data-slot="select-label"], [data-slot="dropdown-menu-label"]):where(.ds-skita, .ds-skita *) {font-size: var(--text-xs);font-weight: 400;color: var(--ds-text-muted);}:is([data-slot="select-separator"], [data-slot="dropdown-menu-separator"]):where(.ds-skita, .ds-skita *) {background-color: color-mix(in oklab, var(--ds-text-primary) 5%, transparent);}[data-slot="dropdown-menu-shortcut"]:where(.ds-skita, .ds-skita *) {font-size: var(--text-3xs);letter-spacing: 0.1em;}/* Tooltip: inverted, the text colour as the fill. The arrow stays. */[data-slot="tooltip-content"]:where(.ds-skita, .ds-skita *) {border-radius: var(--ds-radius-md);background-color: var(--ds-text-primary);color: var(--ds-bg-panel);font-size: var(--text-xs);line-height: 1.5;}[data-slot="tooltip-content"]:where(.ds-skita, .ds-skita *) > svg {background-color: var(--ds-text-primary);fill: var(--ds-text-primary);}[data-slot="tooltip-content"]:where(.ds-skita, .ds-skita *) [data-slot="kbd"] {background-color: color-mix(in oklab, var(--ds-bg-panel) 20%, transparent);color: var(--ds-bg-panel);}/* Dialog: 1.4× radius, a ring rather than a border, a heavy blurred scrim,in over 100ms. The title is mono at the sm step. */[data-slot="dialog-content"]:where(.ds-skita, .ds-skita *) {border-radius: var(--ds-radius-xl);border-color: var(--ds-hairline);background-color: var(--ds-bg-raised);box-shadow: none;font-size: var(--text-xs);line-height: 1.625;animation-duration: var(--ds-dur-fast);}[data-slot="dialog-overlay"]:where(.ds-skita, .ds-skita *) {background-color: var(--ds-scrim);backdrop-filter: blur(4px);-webkit-backdrop-filter: blur(4px);animation-duration: var(--ds-dur-fast);}[data-slot="dialog-title"]:where(.ds-skita, .ds-skita *) {font-family: var(--ds-font-mono);font-size: var(--text-sm);line-height: 1.4;font-weight: 500;}[data-slot="dialog-description"]:where(.ds-skita, .ds-skita *) {font-size: var(--text-xs);line-height: 1.625;}[data-slot="dialog-close"]:where(.ds-skita, .ds-skita *) {border-radius: var(--ds-radius-sm);}/* Table: 12px, headers in the foreground at medium weight (not muted, notuppercase), rows on a border with a half-muted hover. */[data-slot="table"]:where(.ds-skita, .ds-skita *) {font-size: var(--text-xs);}[data-slot="table-head"]:where(.ds-skita, .ds-skita *) {font-weight: 500;color: var(--ds-text-primary);}[data-slot="table-row"]:where(.ds-skita, .ds-skita *) {border-color: var(--ds-border);transition: background-color var(--ds-dur-hover) var(--ds-ease);}[data-slot="table-row"]:where(.ds-skita, .ds-skita *):hover {background-color: color-mix(in oklab, var(--ds-bg-muted) 50%, transparent);}[data-slot="table-row"][data-state="selected"]:where(.ds-skita, .ds-skita *) {background-color: var(--ds-bg-muted);}[data-slot="table-footer"]:where(.ds-skita, .ds-skita *) {background-color: color-mix(in oklab, var(--ds-bg-muted) 50%, transparent);}/* Smaller parts */[data-slot="skeleton"]:where(.ds-skita, .ds-skita *) {border-radius: var(--ds-radius-md);background-color: var(--ds-bg-muted);}[data-slot="progress"]:where(.ds-skita, .ds-skita *) {height: 4px;border-radius: var(--ds-radius-md);background-color: var(--ds-bg-muted);}[data-slot="progress-indicator"]:where(.ds-skita, .ds-skita *) {background-color: var(--ds-primary);}[data-slot="alert"]:where(.ds-skita, .ds-skita *) {border-radius: var(--ds-radius-lg);border-color: var(--ds-border);background-color: var(--ds-bg-panel);font-size: var(--text-xs);line-height: 1.625;}[data-slot="alert-title"]:where(.ds-skita, .ds-skita *) {font-weight: 500;}[data-slot="kbd"]:where(.ds-skita, .ds-skita *) {border-radius: var(--ds-radius-xs);background-color: var(--ds-bg-muted);font-family: ui-sans-serif, system-ui, sans-serif;font-size: 0.625rem;font-weight: 500;color: var(--ds-text-muted);}[data-slot="avatar-fallback"]:where(.ds-skita, .ds-skita *) {background-color: var(--ds-bg-muted);color: var(--ds-text-muted);}[data-slot="separator"]:where(.ds-skita, .ds-skita *) {background-color: var(--ds-border);}/* -------------------------------------------------------------------------08 BLOCK CONTRACT VALUESWhat the shared ds-* utilities in section 09 read, said in Skita'svocabulary. A block written against the contract becomes a Skita screen:a white panel inset on the zinc canvas, mono everywhere, Instrument Sansfor titles and numbers, one green tile.------------------------------------------------------------------------- */html.ds-skita,.ds-skita {--ds-corner: round;--ds-ink: var(--ds-primary-ink);--ds-ink-tint: var(--ds-primary-tint);--ds-emphasis: var(--ds-primary);--ds-on-emphasis: var(--ds-on-primary);--ds-emphasis-shadow: var(--ds-elev-sm);--ds-quiet-fill: var(--ds-bg-muted);/* Shell: the sidebar is the canvas; content is a ringed sheet inset on it. */--ds-shell-canvas: var(--ds-bg-canvas);--ds-shell-panel: var(--ds-bg-panel);--ds-shell-edge: var(--ds-border);--ds-shell-radius: var(--ds-radius-xl);--ds-shell-inset: calc(var(--spacing) * 2);--ds-panel-radius: var(--ds-radius-lg);--ds-panel-ring: 0 0 0 1px var(--ds-border), var(--ds-elev-xs);--ds-sidebar-bg: transparent;--ds-sidebar-blur: none;--ds-sidebar-edge: transparent;--ds-topbar-bg: transparent;--ds-topbar-blur: none;--ds-nav-active-bg: var(--ds-primary-tint);--ds-nav-active-edge: transparent;--ds-nav-active-fg: var(--ds-primary-ink);/* Shape */--ds-r-control: var(--ds-radius-md);--ds-r-surface: var(--ds-radius-lg);--ds-surface-edge: var(--ds-border);--ds-surface-shadow: var(--ds-elev-xs);/* Type: Instrument Sans for titles and numbers, mono for the rest. */--ds-text-size: var(--text-xs);--ds-text-leading: 1.625;--ds-small-size: var(--text-xs);--ds-small-leading: 1.5;--ds-nav-size: var(--text-2xs);--ds-title-font: var(--ds-font-heading);--ds-title-size: var(--text-lg);--ds-title-leading: 1.4;--ds-title-tracking: 0;--ds-title-weight: 500;--ds-subtitle-size: var(--text-xs);--ds-subtitle-leading: 1.625;--ds-stat-font: var(--ds-font-heading);--ds-stat-size: var(--text-xl);--ds-stat-leading: 1.3;--ds-stat-tracking: 0;--ds-stat-weight: 500;--ds-label-font: var(--ds-font-mono);--ds-label-size: var(--text-3xs);--ds-label-leading: 1.4;--ds-label-tracking: 0.05em;--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: var(--text-2xs);--ds-meta-tracking: 0;--ds-meta-color: var(--ds-text-muted);}/* <block-contract> --------------------------------------------------------09 BLOCK CONTRACT (shared — identical in every Satori UI system)Blocks (components/satori-ui/blocks) render in whichever system scopesthem. 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 thosevariables in its own section 08. This section must stay byte for bytethe 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) staysin the block's markup, so a className still wins. The one exception isds-shell-panel's margin: whether the content panel is inset on thecanvas 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 setcopy with ds-text and ds-small rather than text-sm / text-xs, because asystem 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 takethe 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> ------------------------------------------------------- */@media (prefers-reduced-motion: reduce) {.ds-skita *,.ds-skita *::before,.ds-skita *::after {animation-duration: 0.01ms !important;animation-iteration-count: 1 !important;transition-duration: 0.01ms !important;}}