Design system
Components
Reusable UI primitives that compose every page — buttons, inputs, cards, badges, navigation patterns. Each will be documented with markup, modifiers, states, and live examples.
Links
Every <a> on the site that isn’t a button. Five distinct idioms ship today — an inline prose link (blue, underlined), a navigation link with an animated accent bar, a muted text-swap link for chrome regions, a heading link for blog titles, and a soft CTA for read-more affordances. The base a rule in global/reset.css sets the dark-mode default (blue, no underline at rest, underline on hover); every recipe below either inherits that or opts out.
Link copy rule. Link text is words only — never trailing arrows, chevrons, ornaments, or other glyphs (no →, », •, etc.). The colour and underline carry the affordance; arrows are reserved for the button and CTA recipes, where the icon is positioned as a sibling element with its own spacing and animation, not embedded as a character in the label.
1 · Inline prose link
The link recipe used inside long-form text — blog post bodies and legal pages. Blue colour, persistent underline (set by the component, not the global hover), darker-blue hover. Two near-identical implementations differ only in hover colour and underline-offset; consolidating them into a shared .prose a rule is a sync candidate.
color: var(--color-blue);
text-decoration: underline;
text-underline-offset: 2px; /* 0.2em on legal */
transition: color var(--transition-fast);
&:hover {
color: var(--color-blue-dark); /* var(--color-green) on legal */
}
Base reset
a
Naked link styling from global/reset.css — blue, no underline at rest, underline appears on hover.
Foundation for every other link recipe. Defined in src/css/global/reset.css:34: color: var(--color-blue), text-decoration: none, hover adds text-decoration: underline. Used by: any <a> that doesn’t opt into a more specific recipe — e.g. the inline links inside the contact-form fine print rendered by src/contact.njk.
Blog body link
.blog-post__content a
Read the blog index for the latest posts on contextual targeting.
src/css/blog/blog.css:170. Blue with a persistent 2 px underline-offset; hover deepens to --color-blue-dark. Underline persists on hover so the affordance stays stable. Used by: the body of every blog post — rendered through src/_includes/blog-post.njk, applied to any link inside the markdown body. See it live on any post in /blog/ (e.g. IAB taxonomy: which version is right?).
Legal body link
.legal__content a
See our privacy policy for details on data handling.
src/css/legal.css:68. Same blue base with a wider 0.2em underline-offset, but hover swaps to --color-green instead of darker blue — the only place on the site where a content link goes green on hover. Used by: a single page today — /privacy/ (template src/privacy.njk). Any future legal/policy page should reuse the .legal__content wrapper to inherit this recipe.
Sync candidate. Two implementations of the same idiom that differ only in underline-offset and hover colour. Lift to a shared .prose a rule, pick one hover colour (blue-dark is the more common pattern), and let legal opt into green via a modifier if the green hover is intentional.
2 · Animated accent-bar nav link
The header navigation idiom — a centred 2 px blue bar under the link that expands left-and-right to fill the link’s padding box on hover. Used for the main nav and its dropdown sub-items, otherwise unique to the header. Two implementations duplicate the recipe with only padding and font-size differing.
position: relative;
padding: var(--space-3) var(--space-4);
color: var(--color-text);
text-decoration: none;
transition: color var(--transition-fast);
&::after {
content: "";
position: absolute;
left: 50%;
right: 50%;
bottom: 0;
height: 2px;
background: var(--color-blue);
transition: left var(--transition-fast), right var(--transition-fast);
}
&:hover::after { left: var(--space-4); right: var(--space-4); }
Header main link
.site-header__link
src/css/shared/header.css:151. Base font-size, mobile height 2.75 rem for touch comfort. The accent bar lives in ::after; the parent supplies the relative positioning context. Hover removes the global text-decoration: underline so only the bar animates. Used by: every top-level item in the site nav — src/_includes/header.njk. Visible on every page; hover any nav item on the landing page to see the bar expand.
Header dropdown link
.site-header__dropdown-link
src/css/shared/header.css:234. Smaller padding (space-2 space-4) and font-size: sm — otherwise byte-for-byte identical to the main link recipe. Used by: the Solutions and Industries dropdowns in src/_includes/header.njk. Hover “Solutions” in the header to expose the dropdown and see the recipe in context (links into /solutions/contextual-audiences/, /solutions/bid-enrichment/, etc.).
Sync candidate. The accent-bar pseudo-element rule is duplicated. Promote to a shared selector list (.site-header__link, .site-header__dropdown-link) and keep only the size-specific properties on the per-element rules.
3 · Muted text-swap link
The chrome / supporting-region recipe — muted base colour, brightens on hover, no underline. Same shape across the footer, the team cards, and the design-system TOC, just with different muted/active colour pairs depending on what the surrounding surface needs.
Footer link
.site-footer__link
src/css/shared/footer.css:51. font-size: sm, color: var(--color-grey-400), hover lifts to --color-white. Used by: every link in the footer columns — product links, company links, and the legal row — rendered from src/_includes/footer.njk. Visible on every page; scroll to the footer of the landing page.
Footer social icon link
.site-footer__social-link
src/css/shared/footer.css:85. Flex-centred icon container, color: var(--color-grey-400) driving the SVG via currentColor, hover to --color-white. Used by: the LinkedIn / X / GitHub icon row in src/_includes/footer.njk. Visible in the footer of every page. Reach for this whenever a new social icon needs the same understated treatment.
Team card link
.team-card__link
src/css/contact/contact.css:238. Inline-flex with a leading icon, font-size: sm, color: var(--color-text-muted), hover swaps to --color-blue. Used by: the team-member cards on /contact/ (template src/contact.njk) for email + LinkedIn affordances on each card.
All three share the “muted base, brighter hover, no underline” recipe. Worth a shared .link-muted primitive with modifiers (--white, --blue) for the active colour.
4 · Heading link
A heading-sized link that swaps colour on hover instead of adding an underline. Used once today — the post-list titles on the blog index.
Blog post title
.post-list a:not(.post-list__read-more)
src/css/blog/blog.css:62. font-size: xl, weight 600, color: var(--color-heading) — reads as a heading at rest. Hover swaps to --color-blue; the global :hover underline is suppressed because the colour change carries the affordance. Excludes the “read more” link via :not() so only the title gets heading treatment. Used by: the blog index /blog/ (template src/blog.njk), as the post-title link on every row.
5 · Soft CTA / read-more
Small-format affordances that point somewhere but aren’t buttons — a blue inline call-to-action, weight 600, no underline, colour shift on hover. Two production instances today.
Read more
.post-list__read-more
src/css/blog/blog.css:101. font-size: sm, weight 600, color: var(--color-blue). Aligned to the right of each blog-list row beside the title; deepens to --color-blue-dark on hover. Used by: every row of the blog index /blog/ (template src/blog.njk), paired with the heading-link recipe above on the same row.
Announcement bar link
.announcement-bar__link
src/css/shared/announcement.css:59. Lives inside the dismissable top banner rendered from src/_includes/header.njk. color: var(--color-white), weight 600, nowrap, no hover treatment defined — the banner’s own background and dismiss affordance carry the interactivity. Used by: the site-wide announcement bar at the top of every page (until dismissed); currently linking out to the latest whitepaper / event CTA. Always paired with body copy on the same line.
Inputs
Two production forms cover all of Neuwo's input needs: the contact form on /contact/ (text, email, select, textarea) and the API-demo request panel on /api-demo/ (monospace text + JSON textarea, with an animated error state). Both share a single chrome — 1 px glass border, --radius-lg corners, --glass-bg fill, and a 3 px brand-blue glow ring on focus — and differ only in font and a couple of body-specific tweaks.
The recipe
The shared base. Every input on the site reaches for these values; component-level rules add at most font-family, min-height, and the select chevron on top.
width: 100%;
padding: var(--space-3) var(--space-4);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
color: var(--color-text);
transition:
border-color var(--transition-fast),
box-shadow var(--transition-fast);
/* Focus */
border-color: var(--color-blue);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-blue) 18%, transparent);
/* Placeholder */
color: var(--color-grey-400);
Labels above each input use the Typography › Form label role: Noto Sans 500 / sm, sentence case. Both .contact-form__label and .api-demo__field-label are local aliases of that role.
Roles
Four control types share the chrome above. Click into any field to see the focus glow.
Text input
.contact-form__input
The default single-line input. Used for name, email, and any short free-text field. Body font (Noto Sans), base size, comfortable padding. The same rule covers type="text", type="email", type="url", and any other text-shaped input.
- Used by
- Contact form — Name, Email
Select
select.contact-form__input
Same chrome as the text input, with the native chevron suppressed (appearance: none) and a custom inline-SVG chevron painted via background-image at the right edge. The unselected state shows the placeholder colour via :invalid, mirroring text-input placeholder treatment.
- Used by
- Contact form — Role
Textarea
.contact-form__textarea
A multi-line text input. min-height: 8rem so it reads as a body block at rest, resize: vertical so users can grow it but not shrink it horizontally. The API-demo variant sets resize: none instead because its container handles sizing.
- Used by
- Contact form — Message
Monospace input + textarea
.api-demo__input · .api-demo__textarea
The API-demo variant. Same chrome, smaller font (sm) in ui-monospace, "JetBrains Mono" so URLs and JSON read as code rather than prose. Used for the endpoint URL field and the request-body editor on the API demo.
- Used by
- API demo — REST panel
States
Four states cover every interaction beat: rest, focus, placeholder (typed-but-empty), and error (validation failure on the API demo). The error state additionally fires a 0.45 s shake animation (api-demo-input-shake) once on class application — sharp horizontal jitter to mark "this is wrong."
Sync candidate. The contact form's focus glow uses a hard-coded rgba(10, 165, 195, 0.12) while the API-demo variant uses color-mix(in srgb, var(--color-blue) 18%, transparent). Same intent, two slightly different alphas (12 % vs 18 %) and one expressed in raw RGB instead of the brand token. Promote a --shadow-focus (or similar) and consume it from both.
Selection controls
Four primitives for user choices: checkbox for multi-select, radio for single-select from a group, toggle for an immediate on/off action, and segmented control for switching between a small set of mutually exclusive views. Checkbox, radio, and toggle are new components in src/css/global/forms.css following the same input chrome as the contact form. The segmented control is an existing production pattern from src/css/api-demo/api-demo.css.
Checkbox & Radio
Both controls use .form-check as the clickable row wrapper, a plain <input> with appearance: none for the visual box, and .form-check__label for the text. The only differences between them are border-radius (square vs full-pill) and the checked indicator (a white tick vs a white dot). Focus ring, disabled opacity, and transition are shared.
/* Shared chrome */
background: var(--glass-bg);
border: 1px solid var(--glass-border);
width: 1.125rem;
height: 1.125rem;
transition: background, border-color, box-shadow -- var(--transition-fast);
/* Checked */
background-color: var(--color-blue);
border-color: var(--color-blue);
/* Focus */
border-color: var(--color-blue);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-blue) 18%, transparent);
/* Disabled */
opacity: 0.35;
Checkbox
.form-check · input[type="checkbox"].form-check__input
border-radius: var(--radius-sm) gives the square-with-rounded-corners shape. The checked indicator is an inline SVG tick (white, 2.5 px stroke) painted via background-image at 0.8 rem size so it sits clearly centred in the box without touching the edges. Wrap the <input> and label text in a <label class="form-check"> so the full row is clickable. For groups use a <fieldset> + <legend>.
Radio
.form-check · input[type="radio"].form-check__input
border-radius: var(--radius-full) makes the control circular. The checked indicator is a centred white dot (SVG circle at r=3.5) rather than a tick, so the filled circle reads as a selection mark inside the empty ring. The same .form-check wrapper and .form-check__label work identically — only the type attribute and name grouping differ. Ensure all options in a single-choice group share the same name.
Toggle / Switch
A native <input type="checkbox" role="switch"> drives the state; the visible track and thumb are pure CSS via a sibling .form-toggle__track element and its ::after pseudo-element. The input is visually hidden but stays in the tab order, so keyboard and screen-reader users interact with the native control while sighted users see the custom track.
Toggle
.form-toggle · .form-toggle__input · .form-toggle__track
Track & thumb. Track is 3 rem × 1.75 rem, full-pill radius, glass fill + a subtle inset shadow at rest so the channel looks recessed. The thumb (::after) is a 1.25 rem circle, 0.25 rem inset on each side, light grey with a drop shadow so it reads as a raised element on top of the track. Vertical centering uses top: 50%; transform: translateY(-50%) so the 1 px border never shifts it. On :checked the track picks up a 22 % blue tint + 45 % blue border; the thumb slides right by 1.125 rem (inner track − two 4 px offsets − thumb = 18 px) and turns solid blue with a soft glow shadow matching the site’s hover-glow language. Focus ring appears on the track via the :focus-visible + .form-toggle__track sibling selector.
Semantics. Use role="switch" on the hidden checkbox so screen readers announce it as a switch (on/off) rather than a checkbox (checked/unchecked). Always provide a visible .form-toggle__label or an aria-label describing what the toggle controls, not its current state — the state is communicated by aria-checked automatically.
When to reach for it. Use a toggle for an immediate action with binary outcome — enabling a feature, turning notifications on, activating a setting. If the action requires confirmation or has a delayed effect, use a checkbox in a form with a submit button instead.
Segmented control
A compact pill-wrapped group of buttons for switching between a small set of mutually exclusive views. The active option gets a solid blue fill; all others are transparent. Unlike the segmented tabs documented under Navigation, this variant uses role="group" — it controls a visual preference (how content is displayed) rather than navigating between content panels, so tab semantics would be wrong.
View toggle
.api-demo__view-toggle · .api-demo__view
Mechanics. The container (.api-demo__view-toggle) is an inline-flex pill with 0.25 rem padding and a 0.125 rem gap between buttons, giving a barely-there channel between options. Individual buttons are transparent with xs semibold text in muted colour; the active button gets a solid --color-blue background and white text — no glow shadow, no border, just the fill. This is intentionally lighter than the segmented tabs treatment, which carries a full glow ring.
When to reach for it. 2–3 options where the change is purely visual (table vs card, visual vs JSON, grid vs list). The compact xs size means it sits inline in a panel header without dominating it. Don’t use this for more than 3 options or for choices that navigate to a new section — reach for segmented tabs or pill tabs instead.
Cards
Cards on the site fall into a dozen-or-so functional categories — feature cards, stat cards, profile cards, demo cards, and so on. Each is a different content shape sitting on one of the surfaces from Foundations › Surfaces. We document them one category at a time.
1 · Feature / capability cards
A grid-cell card that promotes one feature or capability — title + body, with one of several decorators: an audience tag + top accent bar + CTA, a big step number + watermark icon, or a coloured icon panel. All sit on standard glass (recipe B) and render inside a grid of related cards. The decorator is what distinguishes one variant from another; the underlying surface is the same recipe with small drift on radius and hover lift. The same recipe also rotates to a horizontal layout in .team-card (Contact › Meet the team), where the decorator is a circular portrait and the body sits to its right.
Solutions card
.solutions__card · .solutions__card--featured
A glass-surface card whose signature is the 3 px .solutions__accent bar at the top. The bar holds a static blue→green gradient at rest; on hover it slides the gradient across (200% background size, 0.6 s ease) so the colour shift reads as a directional cue. Body layout: tag eyebrow + title + paragraph + small button. Hover lift is −2 px with a subtle blue glow shadow.
The --featured modifier replaces the top accent with an always-on rotating conic-gradient border (12 s linear, blue→green→blue), drops the glass background for an opaque --color-surface-solid inner, swaps the secondary outline button for the primary solid, and adds a .solutions__badge “New” pill alongside the audience tag. At ≥48 rem the card spans two columns of the parent grid so it reads as the lead feature.
- Used by
- Landing › Solutions grid
Feature tile
.why-grid__card · .card-grid__card
Two production classes that share the same surface recipe — standard glass (recipe B), 1.5 rem padding, --radius-lg, and a hover lift of −2 px with shadow-md and a brighter border. They differ only in content shape and parent context. Bare (.why-grid__card) carries title + description and lives in the auto-scrolling Why-Neuwo rail; the companion class .why-carousel__item pins width to 20 rem so every card aligns. Numbered (.card-grid__card) carries an absolute SVG watermark icon (positioned bottom-right at opacity: 0.25), a big --color-grey-200 step number, a title, and body copy; lives in static 3- or 5-column grids on every solution-page process section.
Worth flagging for a sync: consolidation: the two share virtually identical surface CSS but drifted on transition speed (--transition-fast on .why-grid__card vs --transition-base on .card-grid__card). The position: relative; overflow: hidden on .card-grid__card exists only to clip the absolute watermark icon — the bare variant doesn’t need it.
- Used by
- Classification API › Why Neuwo carousel — bare
- Classification API › How it works — numbered
- Auto-Tagging API process — numbered
- Monetisation Platform process — numbered
- Bid Enrichment process — numbered,
.card-grid--5
Info card
.api-demo-info__card
Decorator is a 2.75 rem coloured icon panel at the top of the card body — --radius-lg with a color-mix background at 14% of the icon colour. Three colour variants ship: default blue (on --shield), green on --tag, and blue-light on --layers — a row of three reads as related but distinct. Cards live in a .api-demo-info__grid wrapper that’s 1-up below 48 rem and 3-up above.
Drifts vs the other Category 1 cards: uses --radius-2xl (1.5 rem) instead of --radius-lg/xl, and a deeper hover lift of −0.25 rem with shadow-lg. The --shield modifier inherits the default blue with no extra rule of its own. All three are sync: normalisation candidates.
- Used by
- API demo › What you get back
2 · Stats panel
A composite card that headlines a section with live-feeling metrics. Header (pulsing blue dot + label with a coloured suffix), a grid of stat cells (large green number + small description), and an optional footer line. Lighter glass surface (recipe C) with the masked grid backdrop on each cell. In production each number animates in via count-up.js when the panel scrolls into view; the specimens below are static.
Stats panel
.stats-panel · .stats-panel--row
Default 2×2 grid stacks the cells in a square; used on the Classification API hero. The --row modifier flips the grid to a single 4-column row at ≥48 rem and shifts the cell dividers from horizontal to vertical; used on the Bid Enrichment and Monetisation Platform heroes. Cell numbers use --color-green; the .accent spans inside numbers (<, +, etc.) inherit the same green for prefix or suffix decoration. The header label has an inline span that shifts to --color-blue for live-status copy.
- Used by
- Classification API hero — default
- Bid Enrichment hero —
--row - Monetisation Platform hero —
--row
3 · Live demo cards
Cards that act as a working demo of a system state — an analysis result, a data flow, a pipeline stage. They share a recipe of: header (status dot + label + meta), several internal sections of structured data, and a skeleton-loader overlay that swaps for the live content while a JS-driven sequence plays. Surface is recipe D (glass with interior blob field). Members differ in body content, blob colour, and what the sequence demonstrates.
Signal-split card
.signal-split__card
A four-section analysis card: header (blue dot + label + latency), Segments (named scores with a horizontal bar driven by an inline --w custom property), Content (tier-key + tier-value chips), Audiences (chip cluster), and a Brand safety footer with a shield icon and score. Surface is recipe D — glass with a green blob top-right and a blue blob bottom-left. The specimen above shows the post-analysis state; in production the card boots with .is-loading, which displays a sibling .signal-split__skeleton overlay (still in the DOM here, just hidden) until JS removes the class on a sequenced timer.
Contextual-audiences process card
.ca-process__card
A two-section deal-pipeline card: Contextual Audiences (icon + label, then a cluster of .ca-process__tag chips numbered 1–14 so each picks up a different blue/green/grey shade and a different staggered enter timing), and Deal ID Delivered (icon + label + a green “Live” badge, then the deal ID and meta stats with shield + clock icons). Surface is the same recipe D as .signal-split__card, with three differences worth noting: the border is tinted green (color-mix(--color-green 25%)), there’s an outer green halo (0 0 1.5rem at 8% green), and on hover both intensify to 40% / 15%. The skeleton overlay shows four lines (one fewer pair than signal-split). Same .is-loading mechanism — specimen omits the class so the analysed state shows.
CTAs
Two production CTA recipes close out almost every page on the site. The banner CTA is the workhorse — a flat solid-surface bar with a passing scan-line shimmer and a pulsing primary button, used at the foot of every solution page and the API demo. The final CTA is the landing-page closer — the same solid-surface inner, but framed by a gradient border that comes alive on hover with a rotating conic gradient and a glow halo. They share design intent (one strong call to action with motion that doesn’t demand attention) but diverge sharply on chrome.
1 · Banner CTA
Two-column grid (headline + sub on the left, button(s) on the right) on an opaque --color-surface-solid bar with --radius-lg corners. A diagonal scan-line passes left-to-right every 5.5 s, and the primary button radiates a pulse ring on the same cadence so the two motions sync. Hover the button and the whole banner picks up a soft blue glow via :has(). Below 40 rem the grid collapses to a single centred column.
Solution-page banner
.cta-banner
Background is the same opaque --color-surface-solid as the inner of the final CTA — CTAs lean on this surface so the action stands clear of the surrounding glass panels. The scan-line is a 20%-wide skewed gradient strip absolutely positioned and translated from left: -30% to left: 130%, paused for the first 45% of the cycle then sweeping across; the matching button pulse is a box-shadow ring expanding from 0 to 12 px in the same first 23% then fading. The shared 5.5 s cycle is what makes the bar feel like one orchestrated motion rather than two unrelated animations.
API-demo wrapper. The same banner ships under an .api-demo-info__cta wrapper that adds margin-top: var(--space-10); visually identical otherwise. Worth flagging for sync: consolidation: the entire .cta-banner rule set is duplicated between solutions.css and api-demo.css (the latter ports it locally with renamed keyframes api-demo-cta-scan/api-demo-cta-pulse to avoid pulling in solutions.css). Lifting the banner into a shared file removes the duplicate.
Dead code. A .floating-cta rule lives in monetisation-platform.css with no markup that uses it — another sync: cleanup candidate.
- Used by
- Classification API
- Auto-Tagging API
- Monetisation Platform
- Bid Enrichment
- Contextual Audiences
- API demo — under
.api-demo-info__ctawrapper
2 · Final CTA
A single hero card that closes the landing page. Three layers stack inside a --radius-2xl shell: an outer 1 px border, a blurred glow halo, and an inner panel on the same --color-surface-solid as the banner. At rest the border is the standard glass border; on hover it fills with a rotating conic gradient (blue→green→blue, 360° in 3 s) and the halo behind it fades up to opacity: 0.4 with a 12 px blur. While idle, the inner runs a passing shimmer scan-line and the button radiates a pulse ring — the same 5.5 s cadence as the banner CTA, so the two recipes feel like cousins.
Landing-page final CTA
.final-cta__card
Built on the @property --final-cta-angle custom property so the conic gradient can animate around the card without re-painting. The card itself has padding: 1px and a background that swaps from --glass-border at rest to a conic gradient on hover — the inner panel sits on top with border-radius: calc(--radius-2xl - 1px), leaving a hair-thin border that becomes the spinning ring. The .final-cta__border sibling is a separate, larger, blurred element absolutely positioned behind the card to provide the halo — cheaper than blurring the card itself.
The title accent (.final-cta__title-accent) reuses --gradient-accent with background-clip: text; the button is a stock .btn.btn--outline.btn--lg.btn--wide with two extra pseudo-element rules for the idle and hover pulses. prefers-reduced-motion is fully respected: the spin, halo, shimmer, and button pulses all suspend.
- Used by
- Landing › Final CTA
Tables
A single tabular pattern carries the comparison surface on the site — the "Neuwo vs LLM APIs" matrix on Classification API. The recipe is a glass-bg wrapper with horizontal scroll on overflow, a tinted header band, status dots in three colours (good / limited / bad), and a highlighted "Neuwo" column with a 2 px green left rule and a soft green tint that picks the eye out at a glance.
1 · Comparison table
Used for vendor comparisons and feature matrices. Header row sits on a 6 % blue tint; body rows brighten to 3 % blue on hover. The Neuwo column gains a thicker rule and tint in both header and body so the reader's eye lands there first. Status dots (.ct-icon--good / --limited / --bad) are 0.5 rem circles with a matching glow, sized to read inline with text without dominating it.
Neuwo vs LLM APIs
.comparison-table-wrap · .comparison-table
| Factor | Neuwo | OpenAI / Azure | Google Cloud NLP |
|---|---|---|---|
| Response time | <80ms Edge / <200ms REST | 200–2,000ms | 100–500ms |
| Pricing model | Per document or fixed monthly fee | Per token (unpredictable) | Per unit (variable) |
| Classification consistency | Deterministic | Probabilistic (varies) | Mostly consistent |
| EU data residency | Guaranteed | Optional / complex | Optional / complex |
| Custom taxonomy / fine-tuning | Managed for you | Expensive, complex | Limited |
| Private / on-premise deployment | Available | Limited options | Not available |
Mechanics. The wrap is a glass-bg container with overflow-x: auto so the table scrolls horizontally on narrow viewports rather than collapsing. min-width: 36rem on the table itself prevents the columns from squeezing. Headers stay on a single line via white-space: nowrap; body cells line-wrap at line-height: 1.5.
Highlighted Neuwo column. .comparison-table__neuwo applies green text + 4 % green background + a 2 px green-30 % left rule on body cells, and a different treatment in the header (heading-coloured text + 10 % blue background + 2 px solid blue left rule). Two slightly different treatments because the header is a label, the body cells are values.
Status dot semantics. Three categorical colours: green for "good", amber (#e5a50a) for "limited", red (#e5534b) for "bad". Each dot has a soft matching glow via box-shadow, sized to read inline. The amber and red are raw hex rather than tokens — candidate for promotion to --color-warning and --color-danger if those status colours show up elsewhere.
Badges
Four production recipes cover every inline-label need today. Tag is the page-level eyebrow pill (“For Publishers”, “For Developers”), with a green --new variant for inline highlights. Founder badge is a solid surface pill with a pulsing green status dot, used once on the about page. Process tag is a small content chip with a fourteen-step colour decay, animated through the contextual-audiences pipeline. Live deal badge is a tiny pulsing label paired with the deal-ID display. Two of them — .tag and .founder__badge — share the global eyebrow typography (uppercase, xs, 600); the contextual-audiences pair uses the heading family in mixed case so the chip content reads naturally.
1 · Tag (eyebrow pill)
The page-level kicker that sits above solution-page hero titles. Pill-shaped, full radius, low-opacity blue tint with a slightly stronger blue border, eyebrow typography. .tag--new swaps the blue tints for green and is used inline (after the base tag) to flag a new offer. In production the base .tag carries a margin-bottom and a negative margin-left so it tucks back to the page gutter; the specimens below reset those margins so the pill renders cleanly in card flow.
display: inline-block;
color: var(--color-blue);
background: rgba(10, 165, 195, 0.08);
border: 1px solid rgba(10, 165, 195, 0.2);
padding: var(--space-1) var(--space-3);
border-radius: var(--radius-full);
/* Typography inherited via the .eyebrow group:
font-family: var(--font-heading);
font-size: var(--font-size-xs);
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase; */
Tag — default (blue)
.tag
The hero kicker on every solution page. Sits above the page title at the page gutter, then the title clears it with built-in margin-bottom. Always paired with .eyebrow-family typography via the shared rule in global/eyebrow.css.
Tag — new (green)
.tag.tag--new
Use as a sibling of the base .tag to flag a newly-released offer (currently only the monetisation-platform hero). Applies a left margin so it sits cleanly next to its base sibling. Don’t use .tag--new alone — it’s designed as a modifier that follows a base tag, not as a standalone label.
Sync candidate. The .tag recipe still uses raw rgba(10, 165, 195, 0.08) and rgba(10, 165, 195, 0.2) instead of color-mix(in srgb, var(--color-blue) N%, transparent) — worth promoting to tokens so a brand-blue change propagates here automatically.
2 · Dot-prefixed pill
A pill with a small leading dot. Two production variants ship today, both from about.css: the founder badge uses a solid surface, eyebrow typography, and a single green status dot; the team values pill uses a blue-tinted glass fill, heading-font sentence case, and a blue→green gradient dot. Different surfaces and typography roles, but the same idiom — label preceded by a tiny coloured marker — which makes them worth documenting side by side.
Founder badge
.founder__badge · .founder__badge-dot
Used once on the about page as an overlay caption on the founder photo. Eyebrow typography, surface fill, glass border, full pill radius, shadow-md. The dot is 0.4 rem green with a 30 % green 3 px halo — reads as a “present” status indicator. In production it’s absolutely positioned to the bottom-centre of the photo; the specimen above resets that to render in flow. If a second team-card surface needs it, lift the rule out of about.css into a shared .status-badge with the photo-positioning kept on a thin .founder__badge--photo modifier.
Team values pill
.team-block__values li · .team-block__values-dot
- Trust
- Collaboration
- Equality
Used once on the about page as the team-values list. Heading font at --font-size-sm, weight 600, sentence case, on a 6 % blue glass fill with a 22 % blue border. The dot is 0.4 rem with the brand linear-gradient(135deg, blue, green) — same accent gradient that runs through the buttons and headings, just shrunk to a marker. The list itself is a flex row with --space-3 gap; the specimen above left-aligns the centred production layout. Reach for this when you need a short list of equally-weighted labels and the founder badge’s status dot would over-promise (no “present/active” meaning here).
3 · Process tag
Small content chip used by the contextual-audiences live demo card to render IAB topics inside the pipeline animation. Heading font, mixed case (so Sports reads as content rather than a label), pill radius, glass-border + faint text-tinted fill at rest. Fourteen numeric variants --1 through --14 drive a deliberate colour decay across the pipeline: blue for the first five (incoming), green for the next four (matched), then a controlled fade through low-opacity green and grey for the final five (dropped). The numbers are positional — whichever five tags happen to be first get the blue treatment, regardless of content.
Process tag — full decay
.ca-process__tag.ca-process__tag--{1…14}
Phase rules. 1–5 get a 10 % blue fill + 25 % blue border; 6–9 swap to the same percentages on green; 10–11 fade to 5 %/12 % green at 60 % opacity; 12–13 step further down to text-tinted greys at the same opacity; 14 falls to 35 % opacity. The base .ca-process__tag alone (no number) renders the rest state — faint text-tint fill on glass border, full-colour heading text. Reach for the numbered variants only when you’re building a multi-tag flow that wants the same blue→green→dim narrative; for a single tag use the base class.
4 · Live deal badge
A tiny green “Live” pill used inside the contextual-audiences deal-pipeline card. Eyebrow-style typography (0.625 rem, 700 weight, 0.08em letter-spacing, uppercase), 12 % green fill, full pill radius, 2.5 s ease-in-out radial-pulse animation that rings out from 20 % green to transparent. Honours prefers-reduced-motion indirectly — if you turn the pulse off, drop the animation declaration. Pairs with .ca-process__deal-id, the monospace deal-ID chip beside it.
Live deal badge + ID
.ca-process__deal-badge · .ca-process__deal-id
The badge announces the active state, the ID chip carries the payload — together they form the “deal delivered” row of the contextual-audiences process card. The pulse runs continuously; if you reuse this pattern for a state that should freeze on completion, gate the animation behind a .is-pulsing modifier and remove the class once the deal lands. The ID chip uses Outfit at --font-size-sm with 0.04em letter-spacing on a faint green tint — close cousin of the monospace input, but as a read-only chip rather than an editable field.
Feedback
Three patterns ship today, covering the three phases of an asynchronous interaction. Skeleton placeholders fill the layout while content hasn’t arrived yet (used on the bid-enrichment and contextual-audiences live-demo cards). The scan-bar loader runs while the API demo is mid-request. Inline status reports the final result of a form submission. There are no toast/banner alerts, no progress bars, and no spinners — the patterns below are the canonical set.
1 · Skeleton
Globally available primitive in shared/skeleton.css. A 9 % text-tinted block with a 10 % text-tinted gradient sweep that travels left-to-right on a 1.6 s linear loop. Three modifiers cover the most common shapes; for anything else apply .skeleton alone and size it via a layout class. Honours prefers-reduced-motion by halting the sweep and dropping opacity to 0.4.
<span class="skeleton skeleton--line"></span>
<span class="skeleton skeleton--bar"></span>
<span class="skeleton skeleton--chip"></span>
Line
.skeleton.skeleton--line
0.625 rem tall, full-pill radius. The default text-line placeholder — stack a few at varying widths to mimic a paragraph. This is the variant the Foundations and Animations specimens already use to illustrate the pattern.
Bar
.skeleton.skeleton--bar
A thinner sibling at 0.5 rem. Use for scoreboards and bar-chart placeholders — rows of horizontal indicators where the height should feel quieter than copy.
Chip
.skeleton.skeleton--chip
1.5 rem tall, full-pill radius, display: inline-block so chips sit side-by-side. Use for tag/badge clusters — the contextual-audiences card uses the same idea on its bespoke skeleton overlay.
For full-card overlays where the placeholders animate in and fade out as content arrives, see the live-demo cards — .signal-split__skeleton and .ca-process__skeleton are bespoke layout wrappers that compose the shared .skeleton primitive with absolute positioning and an .is-loading opacity gate on the parent card.
2 · Loader
A horizontal scan bar paired with a pulsing dot and an uppercase status label. Currently scoped to the API demo as .api-demo__loading; lift to a global .loader recipe when a second consumer arrives. The scan bar sweeps a 35 %-width blue-to-green gradient across a 12 rem track on a 1.8 s ease-in-out loop; the dot pulses opacity from 0.75 to 1 on a 1.4 s ease-in-out loop.
Scan-bar loader
.api-demo__loading · .api-demo__loading-scan · .api-demo__loading-pulse
Classifying
Where it lives. The empty state of the API-demo response panel swaps to this loader during the request and back to the result panel when the response arrives. Why it works here. The scan bar sets a clear “something is happening” rhythm without committing to a percent-complete claim the API can’t deliver, and the brand-gradient sweep keeps it from feeling like a generic spinner. Wrap the whole thing in aria-live="polite" so screen readers announce the in-progress state, and decorate the visual elements with aria-hidden="true". Honours prefers-reduced-motion by halting both animations.
When to reach for it. Async operations longer than ~250 ms where the user is waiting on a single result and there’s no progressive payload to skeleton out. For multi-card or list payloads, prefer skeleton placeholders. For instantaneous local state changes, no indicator is needed.
3 · Inline status
A single text line that announces the outcome of a form submission. Currently scoped to the contact form as .contact-form__status; promote when a second form needs the same pattern. Three states, switched via data-state on the element.
Status line
.contact-form__status · [data-state="success" | "error"]
Thanks — we’ll be in touch within one business day.
Something went wrong. Please try again or email us directly.
min-height: 1.25 rem reserves space for the message so the layout doesn’t shift when text appears. Default colour is --color-text-muted; data-state="success" switches to --color-green, data-state="error" to --color-red (the latter falls back to a hex because --color-red isn’t in variables.css yet — promote to a real token when a second consumer needs an error colour). role="status" + aria-live="polite" makes screen readers announce the message without interrupting the user. Place it adjacent to the submit button so the result lands in the user’s field of view.