Design tokens
Every CSS variable a Tortus site exposes — color, type, spacing, motion, loader, navigation, and widget skins.
Every published Tortus site (and the builder canvas) carries a full set of CSS custom properties emitted from the org's brand. Reference them in any Custom Code block or converted section, and your work stays on‑brand and consistent, and re‑skins automatically if the broker changes their brand.
Source of truth:
@tortus/design-tokens(toModularCssVars). Override any value per‑site via the brand'sdesign_tokens— tell us the values and every page picks them up.
Color
| Variable | Default | |
|---|---|---|
--color-primary | #27323a | primary brand |
--color-secondary | #4a5f7f | secondary |
--color-accent | #27323a | accent / CTAs |
--color-primary-rgb · --color-secondary-rgb · --color-accent-rgb | "R G B" | triplets for color-mix() / opacity |
--color-background | #FFFFFF | page background |
--color-background-alt | #F5F5F5 | alt surface |
--color-text | #1A1A1A | body text |
--color-text-secondary | #666666 | muted text |
--color-text-inverse | #FFFFFF | on‑dark text |
--color-border | #E5E5E5 | borders |
--color-success / --color-warning / --color-error / --color-info | #10B981 / #F59E0B / #EF4444 / #3B82F6 | status |
Background is page/section only — it does not color buttons. A block's
background (the backgroundColor role, or --color-background) sets the surface
behind the content. Button fills come from --color-accent (the "CTAs" color) or
the block's own button-style controls (buttonColor / buttonColorCustom).
Typography
| Variable | Default |
|---|---|
--font-family-heading | 'Playfair Display', 'Cormorant', Georgia, serif |
--font-family-body | 'Inter', 'SF Pro Display', system-ui, sans-serif |
--font-size-xs … --font-size-5xl | 0.75rem → 4rem |
--font-weight-light … --font-weight-bold | 300 → 700 |
--line-height-tight / -normal / -relaxed | 1.25 / 1.5 / 1.75 |
--letter-spacing-tight / -normal / -wide | -0.025em / 0 / 0.1em |
Spacing · radius · shadow
| Group | Variables | Range |
|---|---|---|
| Spacing | --space-0 … --space-120 | 0 → 30rem |
| Radius | --radius-none / -sm / -md / -lg / -xl / -full | 0 → 9999px |
| Shadow | --shadow-none / -sm / -md / -lg / -xl / -2xl | flat → deep |
Motion
Durations + easing curves + scroll/parallax/hover primitives. Animate against these so motion is consistent and on‑brand.
| Variable | Default | Use for |
|---|---|---|
--motion-duration-instant | 0ms | snap states |
--motion-duration-fast | 150ms | hovers, toggles |
--motion-duration-normal | 300ms | most transitions |
--motion-duration-slow | 500ms | section reveals |
--motion-duration-slower | 800ms | hero / preloader |
--motion-easing-standard | cubic-bezier(0.4, 0, 0.2, 1) | general UI |
--motion-easing-decelerate | cubic-bezier(0, 0, 0.2, 1) | enter / reveal‑in |
--motion-easing-accelerate | cubic-bezier(0.4, 0, 1, 1) | exit / fade‑out |
--motion-easing-emphasized | cubic-bezier(0.2, 0, 0, 1) | expressive |
--motion-easing-spring | cubic-bezier(0.34, 1.56, 0.64, 1) | playful overshoot |
--motion-parallax-depth | 0.15 | parallax translate multiplier |
--motion-scroll-reveal-distance | 24px | reveal‑in rise distance |
--motion-hover-lift | -4px | translateY on hover |
.reveal { opacity: 0; transform: translateY(var(--motion-scroll-reveal-distance));
transition: opacity var(--motion-duration-slow) var(--motion-easing-decelerate),
transform var(--motion-duration-slow) var(--motion-easing-decelerate); }
.reveal.in-view { opacity: 1; transform: none; }
.card:hover { transform: translateY(var(--motion-hover-lift));
transition: transform var(--motion-duration-fast) var(--motion-easing-standard); }Art direction motion vars
Two extra motion vars are set per site by the art direction + motion level
the site was built with. Unlike the tokens above they don't come from
@tortus/design-tokens — they're injected as a managed :root{…} block in the
site's Custom CSS at build time. Read them in your own motion so a parallax or
reveal you hand‑write moves at the SAME intensity as the rest of the site, and
re‑tunes for free if the direction or motion level changes.
| Variable | Set by | Use for |
|---|---|---|
--parallax-speed | art direction × motion level | the parallax drift multiplier the platform's hero/showcase parallax uses — multiply your own scroll‑linked transforms by it so they match (Bold drives hard, Editorial drifts, Minimal near‑still; Still motion → 0) |
--tr-dur | art direction | scroll‑reveal / entrance duration (ms) — drop straight into a transition |
/* a reveal that lasts exactly as long as the site's direction intends */
.reveal { opacity: 0; transform: translateY(var(--motion-scroll-reveal-distance));
transition: opacity var(--tr-dur, 760ms) var(--motion-easing-decelerate),
transform var(--tr-dur, 760ms) var(--motion-easing-decelerate); }
.reveal.in-view { opacity: 1; transform: none; }Both carry safe fallbacks, so a block that reads them still works on a site with no direction set (the editor, or a legacy site).
Loader / preloader
The page‑load + route‑transition spinner. Colors follow the brand by default.
| Variable | Default |
|---|---|
--loader-spinner-color | var(--color-accent) |
--loader-spinner-track-color | var(--color-border) |
--loader-spinner-size | 40px |
--loader-spinner-thickness | 3px |
--loader-spinner-duration | 0.8s |
--loader-spinner-easing | linear |
--loader-overlay-color | var(--color-background) |
--loader-overlay-blur | 8px |
.preloader { position: fixed; inset: 0; display: grid; place-items: center;
background: var(--loader-overlay-color); backdrop-filter: blur(var(--loader-overlay-blur)); }
.preloader__spinner { width: var(--loader-spinner-size); height: var(--loader-spinner-size);
border: var(--loader-spinner-thickness) solid var(--loader-spinner-track-color);
border-top-color: var(--loader-spinner-color); border-radius: 50%;
animation: spin var(--loader-spinner-duration) var(--loader-spinner-easing) infinite; }
@keyframes spin { to { transform: rotate(360deg); } }Widget skins
Backend‑fed widgets follow your brand for backgrounds/type/text already; these are the small extra accents you can recolor.
| Variable | Default | Drives |
|---|---|---|
--rating-star-filled | #FBBF24 | filled review stars |
--rating-star-empty | #D1D5DB | empty review stars |
--instagram-overlay | rgba(0, 0, 0, 0.6) | hover scrim over Instagram tiles |
:root { --rating-star-filled: var(--color-accent); } /* brand-colored stars */Navigation dropdown
The hover sub‑menu on the site navigation reads its own token group, derived from
the core palette (so a brand change re‑skins it for free) but namespaced so you
can restyle the menu surface independently of the page background. Every
NavigationHeader variant reads these, so it's a one‑place change.
| Variable | Default | Drives |
|---|---|---|
--nav-dropdown-bg | var(--color-background) (#FFFFFF) | dropdown surface |
--nav-dropdown-fg | var(--color-text) (#1A1A1A) | menu item text |
--nav-dropdown-hover-bg | rgba(<primary-rgb> / 0.06) | hovered item background |
--nav-dropdown-border | var(--color-border) (#E5E5E5) | dropdown border |
--nav-dropdown-shadow | var(--shadow-xl) | dropdown elevation |
/* a darker, tighter sub-menu without touching the page background */
:root {
--nav-dropdown-bg: var(--color-primary);
--nav-dropdown-fg: var(--color-text-inverse);
--nav-dropdown-hover-bg: rgba(var(--color-accent-rgb) / 0.14);
}Cookie banner
The consent banner reads its own --banner-* token group so a broker can re‑skin
the banner chrome without touching the page. Every value is a per‑site override
(organizations.settings.cookie_banner.tokens); blank inherits the platform
default below (and, for --banner-primary-bg + --banner-rail, the brand accent
when one is set). The legal copy is fixed by compliance — these govern only the
chrome. Value spec: @tortus/design-tokens (complianceBannerTokens).
| Variable | Default | Drives |
|---|---|---|
--banner-surface | #ffffff | card background |
--banner-ink | #27323a | title / heading text |
--banner-muted | #5b6770 | body / description text |
--banner-border | rgba(39,50,58,0.10) | card border |
--banner-rail | #6fd8ba | thin brand rail along the top edge |
--banner-accent | #6fd8ba | category toggles / links / focus |
--banner-primary-bg | #27323a | "Accept all" button background |
--banner-primary-ink | #ffffff | "Accept all" button text |
--banner-radius | 0.75rem | card corner radius |
--banner-button-radius | 0.5rem | button corner radius |
/* a tighter, brand-forward banner */
:root {
--banner-primary-bg: var(--color-accent);
--banner-rail: var(--color-accent);
--banner-radius: 20px;
--banner-button-radius: 999px;
}For deeper restyling than these tokens allow, the banner also exposes the
.tortus-cookie-banner / .tortus-cookie-accept / .tortus-cookie-reject class
hooks and a scoped Custom CSS field — see
Cookie banner.
The design page
In the builder, the Design surface is where you mark up a page and drop in
{{tortus:…}} modules (compliant search/listings, plus backend‑fed team, blog,
reviews, Instagram, new developments, and the market surfaces — neighborhood
index, published reports, live market data, neighborhood report, and place
momentum). Those modules render our data inside your
markup; the tokens above style everything around (and, where noted, within) them.
See the Designer handbook for the module reference,
the compliance rules, and the full workflow.

