Skip to main content
ENKO

Tokens

Design tokens are design decisions stored as variables. They separate structure from visual style, so switching themes or updating a brand color cascades across every component without touching individual files.

Senco defines 306 tokens in ds.css, organized into six layers. Those 306 names produce 497 --ds-* declarations once the dark theme and the prototype scope redefine them, plus 41 framework bridge variables — 537 declarations in total. Three numbers, three different questions; tokens.json reports all of them. Every layer references the one below it and never skips downward — that single rule is what makes a brand change, a theme switch, or a platform swap a one-file edit instead of a search-and-replace.

Token layers

#LayerContainsExample
1BaseRaw values. No meaning attached.--ds-accent-600: #0F62FE
2SemanticIntent-based aliases. What a value is for.--ds-primary: var(--ds-accent-600)
3StateTransformations of semantic tokens for interaction.--ds-primary-hover: var(--ds-accent-700)
4ComponentScoped to one component. Composes semantic + state.--ds-actionbtn-primary-bg: var(--ds-primary)
5PlatformResolves theme, motion preference, and viewport..ds-layout.ds-dark { --ds-primary: #458DFF }
6BridgeMaps DS tokens onto the consuming framework.--primary: var(--ds-primary)

Full dependency chain:

Figma variable → Base → Semantic → State → Component → Platform → Tailwind/shadcn

Why six layers

The system originally ran on four layers — Base → Semantic → Component → Tailwind. That structure worked until three things broke it.

ProblemSymptomLayer added
Hover and focus values were hard-coded into each component tokenChanging the focus ring meant editing 14 component blocksState
Dark mode was patched per-componentA new component shipped light-only and nobody noticed until QAPlatform
Framework variables were mixed into the semantic layerThe DS could not be read without knowing TailwindBridge (formalized)

The State and Platform layers do not introduce new values. They introduce rules about how values change — which is exactly the part that was previously duplicated by hand.

Layer rules

  • A token may only reference the layer directly below it, or any layer further down. It may never reference the layer above.
  • Components never reference Base tokens. If a component needs a raw value, the missing piece is a semantic token, not an exception.
  • The State layer defines transformations, not new palettes. If a hover value is not derivable from its semantic parent, the semantic token is wrong.
  • The Platform layer only ever remaps existing tokens. It never defines a token that exists nowhere else.
  • The Bridge layer is the only layer allowed to know the name "Tailwind" or "shadcn". Everything below it is framework-agnostic.

Naming convention

--ds-{group}-{role}-{variant}-{state}
     │       │       │         └── hover, active, selected, disabled
     │       │       └── sm / md / lg / primary / secondary
     │       └── bg, text, border, icon, shadow, radius
     └── palette group, semantic role, or component name

Every token is prefixed --ds-. The prefix is what keeps the system from colliding with Tailwind's own variables when both are live in the same scope.

Figma variables → Base tokens

Figma variable collections map 1:1 onto the Base layer. The MCP server returns variable names with the collection path intact, so the mapping is mechanical:

Figma variableBase token
Color/Accent/600--ds-accent-600
Spacing/4--ds-space-4
Radius/lg--ds-radius-lg
Type/Size/base--ds-text-base

Semantic and above are authored in code, not in Figma. Figma owns raw values; the DS owns meaning. This is why a renamed Figma style never breaks a component — it only ever invalidates one Base token.

Base tokens

Raw values with no attached meaning. These exist only to be referenced by higher layers.

Color

Seven palette groups, each spanning 10 steps from 900 (darkest) to 50 (lightest).

GroupRangePurpose
--ds-primary-*#111827 → #F8FAFCDark anchors — headings, text, structure
--ds-accent-*#001D6C → #F5F8FFBrand blue — interactive elements
--ds-info-*#1E3A5F → #F0F7FFMuted blue — informational, distinct from brand
--ds-gray-*#111827 → #F9FAFBText hierarchy, borders, surfaces
--ds-green-*#14532D → #F0FAF3Success
--ds-yellow-*#713F12 → #FDF8EAWarning
--ds-red-*#7F1D1D → #FDF3F3Error, destructive

All 70 steps, as declared. Every value the rest of this system derives — a hover one step darker, a border on the 400 step, an icon on the 700 — is resolvable from this table without opening ds.css.

Group90080070060050040030020010050
primary#111827#1E293B#334155#475569#64748B#94A3B8#CBD5E1#E2E8F0#F1F5F9#F8FAFC
accent#001D6C#002D9C#0043CE#0F62FE#4589FF#78A9FF#A6C8FF#D0E2FF#E8F0FE#F5F8FF
info#1E3A5F#1E4D8C#2563A0#3B82B8#60A5D4#93C5E8#BAD9F1#DBEAFE#EFF6FF#F0F7FF
gray#111827#1F2937#374151#4B5563#636971#727780#D1D5DB#E5E7EB#F3F4F6#F9FAFB
green#14532D#166534#15803D#22915A#4AAD6E#6EC98E#A3DFBA#C6EDCF#E2F5E7#F0FAF3
yellow#713F12#854D0E#A16207#C07C14#D4952C#E5B34A#F0CE70#F5DEA0#FAF0D2#FDF8EA
red#7F1D1D#922525#A93434#BB4A4A#D46060#E08888#EBADAD#F5D0D0#FAE8E8#FDF3F3

Plus --ds-white (#FFFFFF) and --ds-hero-gradient (linear-gradient(180deg, #0A4ABF, #0F62FE)), the one gradient in the system and a marketing-only surface.

Two gray steps are deliberately off-ramp from the usual Tailwind values: gray-500 (#636971, normally #6B7280) and gray-400 (#727780, normally #9CA3AF). Both were darkened until muted text and placeholder text clear 4.5:1 on white. See Color for the full contrast table.

Spacing

4px base unit, 10 steps. Every step is a multiple of 4 — there are no half-steps, and no arbitrary values.

TokenrempxTypical use
--ds-space-10.25rem4pxBadge dot-to-label
--ds-space-20.5rem8pxInline gaps, icon margins
--ds-space-30.75rem12pxTransaction row internal padding
--ds-space-41rem16pxForm field gaps, mobile edge
--ds-space-51.5rem24pxCard padding, content padding
--ds-space-62rem32pxSection spacing, desktop margin
--ds-space-72.5rem40pxLarge section dividers
--ds-space-83rem48pxMajor section breaks
--ds-space-94rem64pxPage-level spacing
--ds-space-106rem96pxHero spacing

Typography

Minor third scale (1.200), snapped to the 4px grid.

TokenrempxRole
--ds-text-2xs0.625rem10pxDense metadata
--ds-text-xs0.75rem12pxCaptions, helper text
--ds-text-sm0.875rem14pxLabels, secondary body
--ds-text-base1rem16pxBody
--ds-text-lg1.125rem18pxLead paragraph
--ds-text-xl1.375rem22pxH3
--ds-text-2xl1.75rem28pxH2
--ds-text-3xl2.25rem36pxH1
--ds-text-display2.5rem40pxBalance figures, hero numbers
--ds-text-page-titleclamp(1.75rem, 1.25rem + 1.5vw, 2.063rem)28→33pxFluid page title

Families and weights:

TokenValue
--ds-font-sanssystem-ui, -apple-system, "Segoe UI", sans-serif
--ds-font-monoui-monospace, "SF Mono", "Cascadia Code", monospace
--ds-font-regular400
--ds-font-medium500
--ds-font-semibold600
--ds-font-bold700
--ds-tracking-tight-0.02em — Display and H1 only

System fonts are a deliberate constraint, not a placeholder. Senco ships to users on metered and unreliable connections; a webfont is a render-blocking request that buys nothing the system needs. It also sidesteps the Figma MCP limitation that custom typefaces do not survive the pipeline.

Shape and elevation

TokenValueApplied to
--ds-radius-none0Full-bleed sections
--ds-radius-sm0.25rem (4px)Small chips
--ds-radius-md0.375rem (6px)Buttons, inputs
--ds-radius-lg0.5rem (8px)Cards, dialogs
--ds-radius-xl0.75rem (12px)Modals, sheets, form cards
--ds-radius-2xl1rem (16px)Large cards, bottom sheets
--ds-radius-full9999pxPills, avatars
--ds-border-width1pxDefault border
--ds-border-width-focus2pxFocus outline
TokenValue
--ds-shadow-xs0 1px 2px rgba(0,0,0,0.03)
--ds-shadow-sm0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.03)
--ds-shadow-md0 4px 6px rgba(0,0,0,0.05), 0 2px 4px rgba(0,0,0,0.03)
--ds-shadow-lg0 10px 15px rgba(0,0,0,0.06), 0 4px 6px rgba(0,0,0,0.03)
--ds-shadow-xl0 20px 25px rgba(0,0,0,0.08), 0 8px 10px rgba(0,0,0,0.03)

Shadow opacity tops out at 8%. Elevation in Senco signals layering, not drama — a modal should read as above the page, not as a different material.

Motion

TokenValueUsed for
--ds-duration-instant50msState feedback on press
--ds-duration-fast100msHover, focus
--ds-duration-normal180msDefault transition
--ds-duration-slow300msSheets, dialogs
--ds-duration-slower500msFull-screen transitions
--ds-ease-defaultcubic-bezier(0.4, 0, 0.2, 1)Everything, unless specified
--ds-ease-incubic-bezier(0.4, 0, 1, 1)Exits
--ds-ease-outcubic-bezier(0, 0, 0.2, 1)Entrances
--ds-ease-bouncecubic-bezier(0.34, 1.56, 0.64, 1)Confirmation only
--ds-transitionvar(--ds-duration-normal) var(--ds-ease-default)Composite shorthand

Sizing

TokenValueNotes
--ds-size-touch48pxMinimum touch target — WCAG 2.2 SC 2.5.8
--ds-size-icon20pxDefault icon
--ds-size-icon-sm16pxInline with text-sm
--ds-size-icon-lg24pxStandalone / nav icons
--ds-size-avatar36pxContact avatar
--ds-size-appbar48pxMobile app bar
--ds-size-bottomnav52pxBottom tab bar
--ds-size-list-item60pxTransaction row
--ds-input-height48pxDefault control height
--ds-input-height-sm36pxDense control height

Z-index

A fixed six-step scale. No component may invent a z-index outside it.

TokenValue
--ds-z-dropdown100
--ds-z-sticky200
--ds-z-overlay300
--ds-z-modal400
--ds-z-popover500
--ds-z-toast600

Toast sits above modal on purpose: a transfer failure must be readable while a confirmation dialog is still open.

Color tokens
SwatchTokenCSS varValue
bg--ds-bg
fg--ds-fg
heading--ds-heading
muted--ds-muted
border--ds-border
surface--ds-surface
white--ds-white
on-accent--ds-on-accent
primary--ds-primary
primary-hover--ds-primary-hover
primary-light--ds-primary-light
primary-900--ds-primary-900
link--ds-link
focus--ds-focus
overlay--ds-overlay
disabled-bg--ds-disabled-bg
disabled-text--ds-disabled-text
success-bg--ds-success-bg
success-border--ds-success-border
success-text--ds-success-text
warning-bg--ds-warning-bg
warning-border--ds-warning-border
warning-text--ds-warning-text
error-bg--ds-error-bg
error-border--ds-error-border
error-text--ds-error-text
info-bg--ds-info-bg
info-border--ds-info-border
info-text--ds-info-text
Spacing tokens
TokenCSS varValueVisual
space-1--ds-space-1
space-2--ds-space-2
space-3--ds-space-3
space-4--ds-space-4
space-5--ds-space-5
space-6--ds-space-6
space-7--ds-space-7
space-8--ds-space-8
space-9--ds-space-9
space-10--ds-space-10
Typography tokens
TokenCSS varResolved value
font-sans--ds-font-sans
font-mono--ds-font-mono

Font sizes

font-size-xs11pxThe quick brown fox
font-size-caption12pxThe quick brown fox
font-size-sm14pxThe quick brown fox
font-size-base16pxThe quick brown fox
font-size-lg18pxThe quick brown fox
font-size-h419pxThe quick brown fox
font-size-h323pxThe quick brown fox
font-size-h228pxThe quick brown fox
font-size-h128-33pxThe quick brown fox
font-size-display40pxThe quick brown fox
Radius tokens
TokenCSS varValueSample
radius-none--ds-radius-none
radius-sm--ds-radius-sm
radius-md--ds-radius-md
radius-lg--ds-radius-lg
radius-xl--ds-radius-xl

Semantic tokens

Intent-based aliases. Components reference this layer, not the Base layer. This indirection is what makes theme switching possible.

Color

TokenLight valuePurpose
--ds-bg{white}Page background
--ds-surface{gray-100}Card / section fill
--ds-fg{gray-700}Body text
--ds-heading{primary-900}Headings, strong text
--ds-muted{gray-500}Secondary text, hints, labels
--ds-border{gray-200}Dividers, input borders
--ds-primary{accent-600}Brand, primary actions
--ds-primary-light{accent-50}Tinted backgrounds
--ds-link{accent-600}Inline links
--ds-focus{accent-600}Focus ring source color
--ds-placeholder{gray-400}Placeholder text in fields
--ds-overlaycolor-mix(in srgb, {gray-900} 10%, transparent)Modal and sheet backdrop

On-colors

Text and icon colors used on filled surfaces. These exist so that no component has to guess whether white text is legible.

TokenValueUsed on
--ds-on-accent#FFFFFF light, #0F172A darkLabels and indicators on --ds-primary fills
--ds-on-surface{gray-900}Light surfaces
--ds-on-error#FFFFFFred-600 destructive fills

Feedback

Each feedback state is a four-token set — background, border, text, icon — so a notice never has to compose its own palette.

StateBackgroundBorderTextIcon
Info--ds-info-bg--ds-info-border--ds-info-text--ds-info-icon
Success--ds-success-bg--ds-success-border--ds-success-text--ds-success-icon
Warning--ds-warning-bg--ds-warning-border--ds-warning-text--ds-warning-icon
Error--ds-error-bg--ds-error-border--ds-error-text--ds-error-icon
Tip--ds-tip-bg--ds-tip-border--ds-tip-text—

Tip is accent-based rather than info-based. Info is a system message; a tip is Senco talking. Keeping them on different hues prevents the two from blurring together on a busy transfer screen.

Spacing and typography aliases

Semantic spacing names the relationship, not the size. These are conventions in use, not declared variables — the relationship is documented so it stays consistent, and formalizing it as tokens is open work:

ConventionResolves toMeaning
Inline gap--ds-space-2Between icon and its label
Field gap--ds-space-4Between form fields
Section gap--ds-space-6Between page sections
Card padding--ds-space-5Inside a card

State tokens

The State layer defines how a semantic token transforms under interaction. It holds no independent palette — every state value is derived from its semantic parent by a documented rule.

Transformation rules

StateRuleExample
HoverOne step darker on the base ramp (600 → 700)--ds-primary → --ds-primary-hover
Hover (surface)Elevate one shadow step--ds-shadow-xs → --ds-shadow-sm
Focus, bordered controlBorder → --ds-focus, plus a 3px halo at 20% opacity, no offset--ds-focus-ring as box-shadow
Focus, borderless control3px solid --ds-focus outline, 2px offsetoutline: var(--ds-focus-ring-width) solid var(--ds-focus) + outline-offset: var(--ds-focus-ring-offset)
ActiveSame value as hover, no elevation changePressed state reads as "held", not "lifted"
SelectedBorder → --ds-primary, background → --ds-primary-lightRadio card selection
DisabledBackground {gray-300}, text {gray-500}--ds-disabled-bg / --ds-disabled-text

Which focus form applies is decided by one question: does the control have a visible border at rest? Inputs, selects, input groups, and bordered cards use the bordered form. Buttons, links, toggle items, tabs, switches, list rows, and scroll regions use the outline. The indicator that must meet 3:1 is the border or the outline — the 20% halo is decoration and measures about 1.33:1 against white.

Because each rule is expressed once, the hover behavior of a button, a contact card, and a sidebar item are provably the same thing rather than three similar-looking implementations.

Implemented state tokens

TokenValue
--ds-primary-hovervar(--ds-accent-700)
--ds-disabled-bgvar(--ds-gray-300)
--ds-disabled-textvar(--ds-gray-500)
--ds-focus-ring-colorvar(--ds-focus)
--ds-focus-ring-width3px
--ds-focus-ring-opacity20%
--ds-focus-ring-offset2px — used only by the outline form; a box-shadow has no offset
--ds-focus-ring0 0 0 var(--ds-focus-ring-width) color-mix(in srgb, var(--ds-focus-ring-color) var(--ds-focus-ring-opacity), transparent)
--ds-focus-ring-errorSame formula, sourced from --ds-red-600

The focus ring is built with color-mix() rather than a fixed rgba value. That is what lets dark mode remap --ds-focus and get a correct ring for free — the transformation survives the theme switch, because the transformation is the token.

State pairs in component tokens

Component tokens that carry state name it explicitly and in pairs:

--ds-actionbtn-primary-bg:          var(--ds-primary);
--ds-actionbtn-primary-bg-hover:    var(--ds-primary-hover);
--ds-actionbtn-primary-shadow:      var(--ds-shadow-sm);
--ds-actionbtn-primary-shadow-hover: var(--ds-shadow-md);

A component token with a -hover variant but no base variant is a bug. Review catches this as an unpaired state.

Utility prefixes

In markup, states are applied with Tailwind's prefix syntax, which resolves through the Bridge layer:

StateCSS prefixExample
Hoverhover:hover:bg-primary-hover
Focusfocus-visible:focus-visible:ring-focus
Activeactive:active:bg-primary-hover
Disabledaria-disabled:aria-disabled:bg-disabled — controls here use aria-disabled, so disabled: never matches

Focus uses focus-visible:, not focus:. A mouse click on a button should not leave a ring behind; a Tab key press must.

Component tokens

Scoped to a single component. Component tokens compose semantic and state tokens, and are the only place a component-specific decision is allowed to live.

Buttons — three components, three token sets

--ds-actionbtn-* does not style the ordinary button. It styles the dashboard action tile: a 72px-tall card with a 40px circular icon above its label. Reading it as the generic button is what produced a 6px-versus-9999px radius contradiction across three files, so the ownership is stated explicitly here.

ComponentRadiusHeightToken set
Button (default)--ds-radius-md (6px)--ds-size-touch (48px)No component block — composes semantic + state
Pill button--ds-radius-full--ds-pillbtn-h (48px)--ds-pillbtn-*
Action tile--ds-actionbtn-radius (full)--ds-actionbtn-min-h (72px)--ds-actionbtn-*

Action tile

TokenValue
--ds-actionbtn-primary-bgvar(--ds-primary)
--ds-actionbtn-primary-bg-hovervar(--ds-primary-hover)
--ds-actionbtn-primary-textvar(--ds-on-accent)
--ds-actionbtn-primary-icon-bgrgba(255,255,255,0.15)
--ds-actionbtn-primary-shadowvar(--ds-shadow-sm)
--ds-actionbtn-primary-shadow-hovervar(--ds-shadow-md)
--ds-actionbtn-secondary-bgvar(--ds-bg)
--ds-actionbtn-secondary-bg-hovervar(--ds-surface)
--ds-actionbtn-secondary-textvar(--ds-heading)
--ds-actionbtn-secondary-bordervar(--ds-border)
--ds-actionbtn-secondary-border-hovervar(--ds-accent-200)
--ds-actionbtn-secondary-icon-bgvar(--ds-accent-50)
--ds-actionbtn-secondary-icon-colorvar(--ds-primary)
--ds-actionbtn-secondary-shadowvar(--ds-shadow-xs)
--ds-actionbtn-secondary-shadow-hovervar(--ds-shadow-sm)
--ds-actionbtn-radiusvar(--ds-radius-full)
--ds-actionbtn-icon-size2.5rem
--ds-actionbtn-icon-radiusvar(--ds-radius-full)
--ds-actionbtn-min-h4.5rem
--ds-actionbtn-label-sizevar(--ds-text-sm)
--ds-actionbtn-label-weightvar(--ds-font-semibold)

Pill button

TokenValue
--ds-pillbtn-h3rem
--ds-pillbtn-px1.5rem
--ds-pillbtn-font-sizevar(--ds-text-sm)
--ds-pillbtn-font-weightvar(--ds-font-semibold)
--ds-pillbtn-primary-bgvar(--ds-primary)
--ds-pillbtn-primary-textvar(--ds-on-accent)
--ds-pillbtn-secondary-bgvar(--ds-bg)
--ds-pillbtn-secondary-textvar(--ds-primary)
--ds-pillbtn-secondary-bordervar(--ds-primary)

Button (default) has no component block. It composes --ds-primary / --ds-primary-hover / --ds-on-accent / --ds-border directly with --ds-radius-md. The danger variant has no component tokens either, and currently resolves to --ds-red-600 — a Base reference, which this layer's own rules forbid. Recorded in Known drift below.

Input

TokenValue
--ds-input-height48px
--ds-radiocard-bordervar(--ds-border)
--ds-radiocard-border-hovervar(--ds-gray-400)
--ds-radiocard-border-selectedvar(--ds-primary)
--ds-radiocard-bg-selectedvar(--ds-primary-light)
--ds-fileupload-dropzone-bordervar(--ds-gray-300)
--ds-fileupload-dropzone-border-activevar(--ds-primary)
--ds-multiselect-chip-bgvar(--ds-gray-100)
--ds-multiselect-chip-remove-hovervar(--ds-red-600)

Notice / Feedback

Feedback components consume the semantic feedback sets directly — they define no component tokens of their own. This is intentional: a notice that needed its own colors would mean the semantic set was incomplete.

Senco components

Product-specific components carry the largest component-token blocks, because they encode domain decisions that no generic library would have:

ComponentToken groupNotable decision
Status badge.snc-badge-*Six statuses; the only place background, text, and dot come from three different ramps
Status timeline--ds-timeline-*Completed steps are green, active is brand blue, future is gray-300
Bottom tab bar--ds-bottomtab-*Badge is red-600 on white — reads at a glance in sunlight
Camera capture--ds-camera-*Fixed #111827 chrome, not a theme token — the viewfinder is dark in both themes
QR scanner--ds-qr-*Same fixed-dark rule; scan line uses accent-400 for contrast on black
Receipt view--ds-receipt-*Paper metaphor with a tear edge; header and footer use gray-50
Contact card--ds-contactcard-*Hover raises shadow, never changes border color

Camera and QR surfaces are the one documented exception to theme remapping. A viewfinder inverts in light mode for no benefit and costs the user their frame of reference, so those tokens are hard values by design. The exception is recorded here rather than discovered in the CSS.

Platform tokens

The Platform layer resolves environment. It never invents values — it only remaps tokens that already exist, which is why adding a theme cannot break a component.

Theme resolution

Dark mode is a remap of the semantic layer, applied at .ds-layout.ds-dark:

TokenLightDark
--ds-bg#FFFFFF#0F172A
--ds-surface{gray-100}#1E293B
--ds-fg{gray-700}#CBD5E1
--ds-heading{primary-900}#F1F5F9
--ds-muted{gray-500}#94A3B8
--ds-border{gray-200}#334155
--ds-primary#0F62FE#458DFF
--ds-primary-hover#0043CE#3079FF
--ds-primary-light{accent-50}#0F2847
--ds-on-accent#FFFFFF#0F172A
--ds-placeholder{gray-400}#8391A6
--ds-overlay{gray-900} at 10%{gray-900} at 30%

Brand blue lightens in dark mode. #0F62FE on #0F172A measures 3.57:1 — under the 4.5:1 bar for text. #458DFF measures 5.53:1 and clears it. The base palette is untouched; only the semantic alias moves.

Lightening the primary fill has a consequence handled in this same layer: --ds-on-accent remaps to #0F172A, because a white label on #458DFF measures 3.23:1. The button component does not change — it still reads --ds-on-accent. See Themes.

Reduced motion

A single platform block neutralizes animation system-wide — components do not each implement their own guard:

@media (prefers-reduced-motion: reduce) {
  .ds-layout *,
  .ds-layout *::before,
  .ds-layout *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

Viewport resolution

Breakpoints are Platform-layer inputs, not component decisions. A component reads a container token; the Platform layer decides which container applies. See Responsive breakpoint tokens below.

Scoped platform targets

ScopePurpose
.ds-layoutDocumentation site — full DS token set
.ds-layout.ds-darkDark theme remap
.senco-prototype-scopeDevice-frame prototypes — pins light values so the portfolio's own dark theme cannot leak into a mobile mockup

The prototype scope is a real platform target, not a workaround. A prototype frame represents the Senco app, which has its own theme state independent of the page hosting it.

Density — defined, not implemented

The layer reserves compact and cozy density variants, remapping --ds-space-* and control heights. Neither is implemented in ds.css today. Senco's screens are single-task and mobile-first, so density has never earned its cost. The slot stays documented so that if a desktop transaction table ever needs it, it lands in the Platform layer rather than as per-component overrides.

Component library integration

The Bridge layer — the sixth and outermost. Senco builds on shadcn (base-nova style). A CSS variable bridge maps shadcn and Tailwind variables onto Senco semantic tokens inside the .ds-layout scope.

shadcn variableMaps to
--background--ds-bg
--foreground--ds-fg
--card--ds-white
--card-foreground--ds-heading
--popover--ds-white
--primary--ds-primary
--primary-foreground--ds-on-accent
--secondary--ds-white
--secondary-foreground--ds-primary
--muted--ds-surface
--muted-foreground--ds-muted
--accent--ds-surface
--destructive--ds-red-600
--border--ds-border
--input--ds-border
--ring--ds-primary
--radius0.5rem

Sidebar and chart bridges follow the same pattern (--sidebar-*, --chart-1 … --chart-5).

This bridge lives in ds.css, so shadcn components pick up Senco branding without per-component overrides.

The bridge depends on inheritance

The bridge is declared once, on the scope root, and every component reads it by inheritance. Anything that re-declares the same framework variables on descendants — a host application's theme written as .theme * rather than .theme — silently replaces the Senco values with the host's, and nothing looks broken except the colours. When the system is embedded in a host with such a rule, the scope re-asserts inheritance for exactly the variables the bridge defines:

.ds-scope.host-theme *:not(.prototype-scope) {
  --primary: inherit;
  --primary-foreground: inherit;
  /* …every variable the bridge declares */
}

Verification is a single check: a primary button inside the scope must render --ds-primary in light mode and the dark value after the theme switches. If it renders the same colour in both themes, the bridge is not reaching it.

Why the bridge is a layer, not a detail

Everything below layer 6 is technology-agnostic. Moving Senco from Tailwind to CSS-in-JS, or to a native platform, means rewriting one file — the bridge — and nothing else. The DS does not know what renders it, and that is the property the layer exists to protect.

Timing and threshold values

Durations that are not animation. A spec that says a value is "debounced", "throttled", or "has a timeout" has described a requirement and withheld the only part a developer cannot derive — and two developers will pick different numbers, which for a rate quote means two different prices on screen.

These are system defaults. A product surface may override one, but it may not leave it unstated.

ConstantValueApplies toWhy this number
Input settle300msCurrency conversion recalculation, search filteringBelow ~200ms it fires mid-word; above ~500ms the result feels detached from the typing. 300ms is the interval at which a pause reads as "finished typing"
Announcement throttle, routine30sRate refresh announcementsMatches the dashboard rate poll, so the announcement and the visual update are the same event rather than two
Announcement throttle, progress10% or 1s, whichever is less frequentUpload and verification progressAnnouncing every percent is unusable; announcing only at the end is uninformative
Announcement debounce, validation500msPassword strength, async field checksLong enough that a fast typist is not interrupted per character
Quote validity, fiat60sFX rate held between quote and confirmationLong enough to read a fee breakdown without rushing, short enough that the rate is still real
Quote validity, crypto15sStablecoin conversion rateCrypto pairs move inside a fiat window; holding one for 60s means quoting a price the desk cannot honour
Request timeout, default10sAny request whose spinner a user is watchingPast 10s on a slow connection a user has already decided the app is broken; the failure state is more useful than more waiting
Request timeout, upload30sKYC document uploadSized for the payload, not the attention span — this is the longest legitimate operation in the product
Submit re-enableWhen the caller sets loading back to false — on response or on timeoutEvery submitting controlThe caller owns the request, so the caller owns the release. The caller sets loading to true synchronously in the press handler, before any await, which is what stops a second tap in the same frame

Rate expiry is visible before it happens

A quote does not expire silently at zero. The remaining validity is shown from the halfway mark, and expiry replaces the figure with a refresh action rather than a stale number. See Amount input and Transfer flow.

Numeric display precision

ValuePrecisionExample
Fiat amountThe currency's minor unit250.00 USD, 412,500 NGN, 3,200 JPY
Crypto amount8 decimal places, trailing zeros trimmed0.0042 BTC
FX rateExactly 6 significant figures — minimumSignificantDigits: 6, maximumSignificantDigits: 61 USD = 1,650.00 NGN, 1 USD = 0.923145 EUR
FeeThe currency's minor unit, never rounded up3.50 USD

"Full precision" previously stood in for the FX rate row. Six significant figures is what makes a rate comparable across corridors — a fixed decimal count either truncates 0.923145 or pads 1,650.00 with noise.

Currency minor units

The minor unit drives amount precision, and three examples are not a contract. The system takes minor units from ISO 4217, via Intl.NumberFormat, with two exceptions it must carry itself:

CaseHandling
ISO 4217 currenciesIntl.NumberFormat(locale, { style: "currency", currency }).resolvedOptions()
Crypto assetsNot in ISO 4217 — carried in a product-owned table; 8 decimals for BTC, 6 for USDC/USDT
Display roundingNever rounds a fee up or a received amount up

Known gaps — not yet tokenized

None of the values above exist as --ds-* declarations in ds.css. They are a contract stated here and implemented in component logic. Promoting them to tokens (--ds-timing-settle, --ds-timing-quote-fiat) is open work; until then this table is the single source and a component that picks its own number is a defect.

Responsive breakpoint tokens

TokenValueApplies to
--ds-bp-mobile600pxMobile portrait and below
--ds-bp-tablet1024pxTablet portrait to landscape
--ds-bp-desktop1440pxStandard desktop and above

Container widths resolved at each breakpoint:

TokenValue
--ds-container-xs320px
--ds-container-sm480px
--ds-container-md640px
--ds-container-lg960px
--ds-container-xl1200px
--ds-container-max1400px

Rules

  • Never hard-code raw values in component styles.
  • Always reference the layer directly below. Never skip upward.
  • Base tokens exist only to be referenced by higher layers.
  • Components never reference Base tokens. A missing semantic token is the real problem.
  • State tokens must be derivable from their semantic parent by a documented transformation.
  • Every component token carrying -hover must have an unprefixed base counterpart.
  • Platform overrides only remap. A token introduced inside a theme block is a bug.
  • All interactive elements define hover, focus-visible, and disabled states.
  • shadcn components inherit Senco tokens via the bridge — do not override individually.

Adding a token

  1. Does an existing semantic token cover it? If yes, stop.
  2. If the value is raw and reusable, add it to Base and alias it in Semantic.
  3. If it only varies by interaction, it belongs in State as a transformation.
  4. If it applies to exactly one component, add it to that component's block — and only after 1–3 have failed.
  5. Record the decision. A token whose reason is not written down gets duplicated within a quarter.

Deprecating a token

Removal is a two-step change, never one. Mark the token deprecated in ds.css with its replacement named inline, leave it aliased for one release, then delete. An immediate delete turns into a hunt across 29 spec files and every prototype.

Known drift

ItemStatus
--ds-focus-offsetRemoved — superseded by --ds-focus-ring-offset
Dark theme overrides three Base tokens.ds-layout.ds-dark redefines --ds-accent-50, -100, and -200, which the rules above forbid. They are the tints behind --ds-primary-light and the sidebar's active row; in dark mode those need a dark tint, and remapping the palette step was the shortest path. The correct fix is a semantic tint token that each theme points somewhere different, leaving the ramp alone. Until then, "one brand blue means one thing everywhere" is true of accent-600 and not of the low end of the ramp. --ds-white is also remapped and that one is intended — it is a semantic token wearing a colour's name
Neutral feedback setNo --ds-gray-bg / -text / -border / -icon exists. The Cancelled status reaches past the semantic layer to --ds-gray-100 / -600 / -500 directly — the one sanctioned Base reference in a component, pending a neutral set
--ds-tip-iconThe Tip feedback set has bg, border, and text but no icon token; Tip currently ships without one
Danger button tokensNo --ds-actionbtn-danger-* or equivalent block exists. The danger variant resolves to --ds-red-600, a Base reference this layer forbids
Link hover / destructive--ds-link has no -hover pair and reuses --ds-primary-hover. .link-destructive is a class with no --ds-link-destructive token
--ds-space-0Used as a value, not declared as a variable
Line-height tokensRatios are specified per role in Typography; no --ds-leading-* tokens exist
Status badge compact spacing3px / 10px padding and a 6px gap sit off the 4px scale; kept because they fit the 60px transaction row
Density variantsDocumented above, not implemented
Semantic spacing aliasesDocumented above; several are conventions in use rather than declared variables in ds.css

Drift is listed rather than hidden. An agent reading this file needs to know which statements are contracts and which are intent — an undocumented gap is what produces confidently wrong generated code.