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 the legal / privacy pages on the live site. Blue colour, persistent underline (set by the component, not the global hover), darker-blue hover. Shared rule lives in src/css/shared/prose-links.css and is consumed by both .blog-post__content a and .legal__content a. The specimen below shows the blog-post wrapper; the legal-page wrapper picks up the same rule by the second selector in prose-links.css.
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.
Prose body link
.blog-post__content a, .legal__content a
Read the blog index for the latest posts on contextual targeting, or see our privacy policy for details on data handling.
src/css/shared/prose-links.css. Blue with a persistent 0.2em 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) and the legal/policy pages under the .legal__content wrapper. See it live on any post in /blog/ and on /privacy/.
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. The accent-bar pseudo lives on a shared selector list (.site-header__link, .site-header__dropdown-link); each variant only sets its own size-specific properties.
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.).
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: var(--shadow-focus);
/* 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."
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.
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. The two shared shells below sit underneath every variant and are the only place the glass + border + blur recipe is written out.
0 · Shared shells
Two shells absorb the recipes every card used to write inline. .card is the bare glass surface; add .card--hover-lift for the standard translateY(-0.125 rem) + border lighten + --shadow-md hover. .glass-card is the heavier variant — the same surface but with built-in padding, gap, --shadow-lg, and an optional .glass-card--spots overlay that paints two corner radial spots from the brand tints. The --spots-flipped modifier swaps the two spot colours.
.card + .card--hover-lift
glass + lift
Hover me
The same surface every feature, stat and capability card sits on.
Adopt this for any new tile-style card. The variant-specific CSS files (landing-solutions.css, contextual-audiences.css, etc.) still own the content layout, radius, and accent decoration; the glass + lift recipe is no longer duplicated. Border radius is intentionally not set by .card — pick --radius-lg for small tiles, --radius-xl for hero-sized panels, --radius-2xl for the glass-card variant.
.glass-card + .glass-card--spots
heavy panel
Spot overlay
Two corner radial gradients from --tint-green-20 and --tint-blue-15, with position CSS variables so each consumer can move them.
Used by the feature-spots tiles on the solution pages and any panel that wants the built-in gap rhythm. Override --feature-spot-1-pos / --feature-spot-1-color (and the -2 pair) per consumer; .glass-card--spots-flipped is the shortcut for the most common variant where the two brand colours swap positions.
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 −0.125 rem with shadow-md and a brighter border. Both run on --transition-base. 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. The numbered variant adds position: relative; overflow: hidden 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.
Matches the rest of Category 1: --radius-lg, hover lift of −0.125 rem with --shadow-md. The --shield modifier inherits the default blue with no extra rule of its own.
- 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. The .cta-banner rule and its scan / pulse keyframes live in src/css/shared/cta-banner.css, consumed by both the solution pages and the API demo.
- 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.
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.
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.
5 · Status dot
A standalone visual indicator shared across the site — the small coloured circle with an optional ring. Three production variants ship today: the resting green dot with a soft green halo, the pulse variant that breathes the halo on a 2 s loop, and the glow variant that switches to a smaller blue dot with the blink keyframe. All three live in src/css/shared/status-dot.css with their own status-dot-pulse keyframe; the dot-prefixed pills above (founder badge, team values) inline their own dot CSS but should adopt this shared primitive when they next need a touch.
Status dot — resting
.status-dot
The default. Green core with a single soft halo at 25 % green — the “system is online” indicator. flex-shrink: 0 so it never collapses when used inside a flex row of label content.
Status dot — pulse
.status-dot.status-dot--pulse
The active variant. Halo expands from 4 px / 25 % green to 8 px / 10 % green at the midpoint, then snaps back. The status-dot-pulse keyframe is the only motion this component owns. Honours prefers-reduced-motion (the animation is suppressed) so the visual still reads as “active” without the breath.
Status dot — glow
.status-dot.status-dot--glow
Smaller (0.5 rem) blue variant with a solid colour halo and the shared blink keyframe from global/animations.css. Used inside hero pills where the “live signal” reads better as a blue eye than as a green system-status indicator.
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.
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.