Back to home
Design system

The system this site is built from

The verno/dev tokens, rules and components, in the open and in full. Not a showcase: the same values and the same imports the production bundle is built from.

Values are read from src/styles/_variables.sass. The buttons, cards and eyebrows below are neither screenshots nor copied markup — they are the same components used on every other page.

Principles

Five decisions everything else follows from. Each one cost either a measurement or a rebuild.

  1. 01

    One hue, committed

    Everything accented is evergreen, hue ≈162°. Even ::selection and the scrollbar are re-coloured. Exactly two exceptions: the signal colours and the deliberate macOS traffic lights in the terminal.

  2. 02

    Physically motivated glass

    Light strikes from the top left: a catchlight in the corner, brighter top and left edges, a shadowed bottom-right lip. The effect reads as light behaviour, not decoration.

  3. 03

    Dark-first

    Dark values live on :root; the light theme overrides them via [data-theme="light"]. Theme-invariant tokens are declared once.

  4. 04

    Contrast is measured, not assumed

    Accent text is tuned against the worst realistic backdrop — glow over glass — not against the bare background. The measured ratio is recorded in a comment beside the token.

  5. 05

    Effects are priced in frame time

    The frame-time cost was not backdrop-filter but blurred shadows repeated across 15–20 cards: 35 ms versus 17 ms. Repeating cards use the no-blur recipe; full glass is reserved for singleton surfaces.

Colour

One hue across the interface and two themes. The light theme is not a lightened copy of the dark one but a set of values in its own right.

Evergreen

Brand, hue ≈162°

  • 50#F4FBF7Text on accent fills (--accent-ink)
  • 300#4CBE9CHeading gradient start, dark theme
  • 400#42AE8ESmall accent text, dark theme (--accent-text)
  • 500#2E8B6FThe bright accent: icons, borders, glows (--accent-bright)
  • 600#2C7E66Heading gradient start, light theme
  • 650#1E7358Light-theme meta text — the greenest shade that still passes AA
  • 700#256B56Primary button fill, ::selection, skip link (--accent-btn)
  • 800#1A5F4CSmall accent text, light theme (--accent-text)
  • 900#1A4D3EDeep base accent (--accent)
  • 950#14402DHeading gradient end, light theme

Neutral

Green-tinted — never pure grey

  • 50#ECF2EFDark-theme ink (--ink)
  • 100#E7ECE6Light-theme bg — deliberately deeper than white so glass can show
  • 200#DCE3DALight-theme deep bg (--bg-deep)
  • 300#B9C4BEDark-theme descriptions (--ink-soft)
  • 400#8B9891Dark-theme muted (--muted)
  • 600#4E5B54Light-theme muted (--muted)
  • 700#35423BLight-theme descriptions (--ink-soft)
  • 900#191D1BDark-theme bg — green charcoal, not a pitch-black void
  • 950#111413Dark-theme deep bg (--bg-deep)
  • 975#0B140FLight-theme ink (--ink)

Signal

The only non-evergreen colours in the interface

  • ok#17C46BSuccess: submitted form, availability status
  • error#FF8A7AField error — soft coral, legible on dark glass

Semantic tokens

What components actually reference. The name states a role, not a shade: the colour behind it changes with the theme and, for the accent tokens, at runtime to match the platform of whichever project is open.

Accent

Theme-invariant: declared once on :root. useAccentHover rewrites exactly these when the page re-tints to a project platform — which is why components may only reference var(--accent-*), never a resolved hex.

TokenDarkLight
--accent#1A4D3E#1A4D3E
--accent-bright#2E8B6F#2E8B6F
--accent-btn#256B56#256B56
--accent-ink#F4FBF7#F4FBF7
--accent-glowrgba(46, 139, 111, 0.28)rgba(46, 139, 111, 0.28)
--ok#17C46B#17C46B
--ok-glowrgba(23, 196, 107, 0.18)rgba(23, 196, 107, 0.18)

Text and background

TokenDarkLight
--bg#191D1B#E7ECE6
--bg-deep#111413#DCE3DA
--ink#ECF2EF#0B140F
--ink-soft#B9C4BE#35423B
--muted#8B9891#4E5B54
--accent-text#42AE8E#1A5F4C
--heading-grad-a#4CBE9C#2C7E66
--heading-grad-b#2E8B6F#14402D

Surfaces and borders

Alpha values differ per theme by design: dark glass is white-tinted at 7–12 %, light glass at 45–68 %, because a pane barely darker than white on a near-white page is invisible.

TokenDarkLight
--surface-glassrgba(255, 255, 255, 0.07)rgba(255, 255, 255, 0.45)
--surface-glass-strongrgba(255, 255, 255, 0.12)rgba(255, 255, 255, 0.68)
--surface-dockrgba(16, 24, 20, 0.94)rgba(252, 253, 252, 0.96)
--border-glassrgba(255, 255, 255, 0.14)rgba(20, 32, 25, 0.16)
--border-glass-strongrgba(255, 255, 255, 0.26)rgba(20, 32, 25, 0.28)
--border-glass-litrgba(255, 255, 255, 0.3)rgba(255, 255, 255, 0.92)

Glass optics and atmosphere

TokenDarkLight
--glass-specularrgba(255, 255, 255, 0.09)rgba(255, 255, 255, 0.6)
--glass-grad-argba(255, 255, 255, 0.06)rgba(255, 255, 255, 0.5)
--glass-grad-brgba(46, 139, 111, 0.05)rgba(46, 139, 111, 0.07)
--glass-highlightrgba(255, 255, 255, 0.35)rgba(255, 255, 255, 0.9)
--glass-inner-shadowrgba(0, 0, 0, 0.22)rgba(20, 35, 28, 0.12)
--shadow-colorrgba(1, 4, 3, 0.65)rgba(20, 35, 28, 0.18)
--atmo-glowrgba(46, 139, 111, 0.09)rgba(46, 139, 111, 0.13)
--atmo-lightrgba(255, 255, 255, 0.04)rgba(255, 255, 255, 0.5)

Contrast

The page computes these ratios from the values in the table above — they are not transcribed from a report and cannot drift away from the tokens.

PairWhereDarkLight
--ink on --bgBody text15.02:1AAA15.64:1AAA
--ink-soft on --bgDescriptions9.49:1AAA8.80:1AAA
--muted on --bgCaptions, section ledes5.68:1AA5.95:1AA
--accent-text on --bgSmall accent text6.23:1AA6.30:1AA
--accent-ink on --accent-btnPrimary button, text selection6.03:1AA6.03:1AA
--accent-bright on --bgLarge text and icons only4.09:1AA3.48:1AA

Only solid pairs appear here, where the ratio is exact. Glass surfaces are translucent, and their stacks — glow over glass, the worst case on the site — were measured by hand, with the result recorded in a comment beside the token definition.

Typography

Three faces, and both text faces carry full Cyrillic and Latin in one file — no glyph mismatch between the Russian and English versions.

  • Headings carry the page$font-displayGolos Text600, letter-spacing −0.01em, line-height 1.1–1.2h1–h4
  • Body text, buttons and form fields$font-bodyManrope400/600, 16/1.5Copy, buttons, inputs
  • EYEBROW / 01 — MONO 12PX$font-monoJetBrains Mono600, uppercase, letter-spacing 0.06–0.08emEyebrows, section indices, terminal

Static scale

Every step is a multiple of 4. After consolidation some roles landed on the same value — the names stay so the code speaks in roles, not pixels.

  • The quick brown fox jumps over the lazy dog$text-xs12pxMono footnotes, indices, field errors
  • The quick brown fox jumps over the lazy dog$text-sm12pxEyebrows, mono chips, captions
  • The quick brown fox jumps over the lazy dog$text-base-sm16pxChips, dense UI
  • The quick brown fox jumps over the lazy dog$text-base16pxText CTAs, secondary copy
  • The quick brown fox jumps over the lazy dog$text-md16pxBody copy — same as body
  • The quick brown fox jumps over the lazy dog$text-lg20pxSection lede
  • The quick brown fox jumps over the lazy dog$text-xl20pxCard titles on mobile
  • The quick brown fox jumps over the lazy dog$text-2xl28pxColumn headings
  • The quick brown fox jumps over the lazy dog$text-3xl32pxAccent column headings

Fluid scale

Headings are set with clamp: a floor, a growth rate and a ceiling.

  • Home hero (h1)clamp(40px, 5.2vw, 72px)
  • Page titleclamp(36px, 5vw, 64px)
  • Case-study titleclamp(28px, 4.4vw, 52px)
  • Section heading (h2)clamp(28px, 3.4vw, 44px)
  • Block titleclamp(24px, 2.8vw, 36px)
  • Card titleclamp(20px, 2.2vw, 28px)

Line height moves with the role — 1.1 for large headings, 1.2 for the smaller ones. A single tight value breaks at card size once a title wraps: on a two-line name the descenders nearly touch the following line.

Spacing and layout

One scale for the whole site. New margins and padding come from it and nowhere else.

  • $space-18px
  • $space-212px
  • $space-320px
  • $space-432px
  • $space-540px
  • $space-660px
  • $space-780px
  • $space-8120px

Layout

Container 1200px
Padding 28px, 16px on mobile. The --narrow modifier drops to 760px for long-form text
Section rhythm 80 / 60 px
Default and .section--tight. Tablet 68px, mobile 48px
Section head 640px
Bottom margin 40px, hairline rule, mono /0N index at the bottom right
Scroll clearance 104px
scroll-padding-top on html is the single mechanism. scroll-margin-top on sections stacks additively and is therefore banned

Breakpoints

All max-width. The nav breakpoint is deliberately separate from the tablet one: “does the primary nav fit” is a question about 887 pixels of layout, not about a class of device.

$bp-mobile 620px
Phone
$bp-nav 930px
“Does the primary nav fit” — a different question from “is this a tablet”
$bp-tablet 980px
Tablet
$bp-laptop 1080px
Intermediate step so 621–980 is not one undifferentiated bucket

Shape and glass

Three radii and four surface recipes. Choosing a recipe is choosing a frame-time cost, not a taste.

  • $radius-sm12pxInputs, chips, skip link
  • $radius20pxGlass cards and panels
  • $radius-pill999pxButtons, scrollbar thumb

Surface recipes

The cards below demonstrate themselves: each one is built with the recipe it describes.

  • glass-effect

    Singleton surfaces: header, terminal, contact form, side menu

    Catchlight + white-to-evergreen wash + translucent fill; lit top and left edges; backdrop-filter: blur(20px) saturate(160%); ambient shadow, contact shadow and two inset lips.

  • glass-card--no-blur

    Repeating cards in grids and carousels

    The same face without backdrop-filter and without the blurred shadow — inset lips only. That blurred shadow repeated across 15–20 cards is what cost 35 ms per frame versus 17 ms.

  • glass-puck

    Icon containers repeated in grids

    A gradient, a 1px border and one small shadow. No backdrop-filter — the icon sits in the material, not on a flat tinted box.

  • cursor-sheen

    Interactive cards

    A soft radial accent pool follows the pointer inside the card: 7 % in dark, 10 % in light. useCursorSheen writes the coordinates straight into style, with no React state. The card itself never moves.

Where backdrop-filter is unsupported, every recipe falls back to the opaque --surface-glass-strong — the panel stays readable, it simply stops being glass.

Motion

One law of movement across Sass and Framer Motion — the same four numbers in both places.

Curvecubic-bezier(0.16, 1, 0.3, 1)

A sharp start and a long settle: the element nearly arrives within the first third of the duration and spends the rest coming to rest. That is why the interface reads as fast even at a quarter of a second.

Durations

$dur-fast 0.2s
Micro-interactions, Framer button hover
$dur-base 0.25s
Button colours, field borders, cursor sheen
$dur-slow 0.6s
Large reveals

Rules

Press scale(0.97)
The only touch feedback there is: the native tap highlight is disabled globally because it painted a rectangle over pill buttons
Button hover y: −2
Framer Motion; tap is scale 0.96
Cards no movement
The lift and skew were removed on 13.09.2026. A card hover is its glow and the cursor sheen, nothing more
Theme switch 0.3s
Only the background travels; text colour snaps. Cross-fading both sends them through a grey-on-grey midpoint of ~1.1:1 where the text briefly vanishes
Eyebrow pulse 2.6s
A ring expands and fades on an infinite loop
Page transition 0.35s
Sections settle into the plane over 0.7s

Components

Live, not captured: everything below is imported from the same files the rest of the site uses. Hover and press them — the states are real.

Button

Three variants, four states. Pill, Manrope 600, 16px, padding 15×28.

primary
ghost
on-accent
VariantDefaultHoverActiveDisabled
primary--accent-btn / --accent-ink6px glow ring + soft haloscale(0.97)opacity 0.6, cursor: progress
ghostWhite 4% + blur(20px), --border-glass-strong borderBorder and text go --accent-brightscale(0.97)opacity 0.6, cursor: progress
on-accent--bg / --ink--bg-deepscale(0.97)opacity 0.6, cursor: progress

The pressed state is scale(0.97) on all three variants: the native tap highlight is disabled globally because it painted a rectangle over a pill button and its shape cannot be styled. That makes touch feedback the component’s own job.

GlassCard

One surface, three modes. They differ in frame-time cost, not in appearance.

  • <GlassCard />

    Full glass: backdrop-filter and soft shadows. For singleton surfaces.

  • noBlur

    No blur and no blurred shadow, inset lips only. For grids and carousels.

  • hoverLift

    Accent border and glow on hover. The card itself never moves.

Eyebrow and section head

The two patterns that run through every page. The head index is a CSS counter: sections number themselves by document order.

.eyebrow Available for projects
.section__head

Section heading

A 640px subtitle with a hairline rule and a mono index on the right.

Form field

Radius 12, glass fill, accent border on focus.

The 16px text size is not decorative: below it, Safari on iOS zooms the page when the field takes focus. The focus ring stays separate from the accent border — suppressing the outline outright is not an option.

Accessibility

Rules that cannot be broken without breaking the system. The first four are about colour, the last two about focus and motion.

  1. Small accent text uses --accent-text only. --accent-bright never carries text below large-text size: 3.97:1 under --accent-ink and 3.48:1 on the light background.
  2. A new accent-on-surface combination is measured against the worst stack it will sit on — glow over glass — not against bare --bg.
  3. Headings may take the brighter gradient stops only while the clamp floor and weight 600 keep them in WCAG large-text territory (≥18.66px bold → a 3:1 threshold).
  4. The measured ratio goes into a comment beside the token definition — that is how all of _variables.sass is written.
  5. The focus ring is 2px var(--accent-text) at a 3px offset with no border-radius override: a forced radius restyled the element itself while it held focus, squaring off circular carousel dots.
  6. Every motion effect must fall under the global prefers-reduced-motion kill switch — it zeroes animations, transitions and smooth scrolling, while MotionConfig reducedMotion="user" stops what Framer drives through inline styles.

Want a system like this?

If a project already has a mockup or a live site, the system can be extracted from what exists and the work can run on it from there.

Get a scope & estimate