ContentFlow Design Guide

ContentFlow Brand and Product Design Guide

Brand Core

WHAT CONTENTFLOW IS, AND ISN'T YET

What ContentFlow is

ContentFlow is the orchestration layer for in-app content. It lets marketing and product teams publish campaigns, dynamic blocks, banners, journeys, and targeted experiences inside an app without waiting for app releases or engineering cycles.

One-line positioning

ContentFlow helps consumer apps ship in-app campaigns, dynamic blocks, and journeys in seconds.

Expanded positioning

ContentFlow is a B2B SaaS control plane for banks, fintechs, insurers, marketplaces, and consumer apps in Saudi Arabia and MENA. Teams can create content, target audiences, launch campaigns, manage localization, run approvals, analyze performance, and integrate once (REST API today, SDKs in preview) so changes go live without code.

Brand promise

Move in-app content at marketing speed.

Product pillars (shipped)

  1. Dynamic Blocks: reusable in-app content units rendered in the host app at runtime. Integration status: REST API today, SDKs in preview.
  2. Campaigns: targeted deployments with scheduling, approvals, A/B testing, analytics, and lifecycle states.
  3. Journeys: multi-step user flows for onboarding, engagement, win-back, and seasonal campaigns.
  4. Audiences: segmentation by lifecycle, behavior, product usage, location, consent, and attributes.
  5. Localization: Arabic and English content management with a right-to-left content flag (the host app renders the RTL layout), approval-gated string publishing.
  6. Analytics: campaign analytics, cohorts, experiments, drilldowns.
  7. Messaging Channels: Push, WhatsApp, SMS, Popup, In-App (Twilio-backed where applicable).
  8. Developers: REST API, docs, API keys, sandbox, operational status. SDKs are in preview, see Section 15.

In preview

Sensors (Runtime Product Intelligence): every localized string is itself a dormant sensor, remotely enabled with no app release to emit consent-aware signals (Views, Interactions, Engagement rate, Reach, Live now), powering rules, segments, and journeys. Roadmap: Observe, Relate, Act, Learn. Shipped behind the sensors beta flag (PR #291, UX passes PR #297 and PR #303), but not generally available.

Always describe Sensors as "in preview"See Section 8's Sensors subsection and Section 10 for the docs-side treatment.

Parked behind beta flags ("Soon")

  • AI Insights (ai-insights feature flag): recommendations, copy generation, segment discovery, campaign draft creation. Visible to beta testers only; shows a "Soon" pill to everyone else.
  • Marketplace (marketplace feature flag): integrations directory. Same beta-gated treatment.
Do not present as available todayAI Insights and Marketplace exist in code behind lib/features.ts, state 'beta', unlocked only for user.betaTester or viewAs === 'tester'.

Brand Personality

HOW THE BRAND SHOULD FEEL

ContentFlow should feel:

  • Sharp: built for enterprise teams, not toy dashboards.
  • Fast: launch, update, test, and measure without delay.
  • Bilingual: Arabic and English are equal; the Arabic locale carries a right-to-left flag, and the host app renders the RTL layout.
  • Local: Saudi and MENA-ready, with Hijri, PDPL, consent, seasonal targeting, and Arabic-first needs.
  • Tactile: content feels modular, draggable, editable, and alive.
  • Quietly intelligent: AI appears as guidance and next-best-action, not as generic chatbot noise (and only where it's actually shipped; see Section 1's parked list).
AvoidMaking the brand feel generic AI SaaS, over-glassy, crypto-like, or too playful for enterprise buyers. Avoid making it feel flat-minimal or sparse either; see Section 7 for the crucial distinction between flat surfaces and flat minimalism.

Logos

ONE MARK, FIXED, NEVER REDRAWN

Canonical mark

The ContentFlow logo is fixed, not a direction to explore further: a #571FE4 rounded square (corner radius rx=36 on a 216px canvas, exactly one sixth of the rendered size at any scale, the canonical value) containing a white eight-arrow pinwheel. An exact canonical SVG exists; never approximate, redraw, or reinvent it. The canonical mark's exact violet (#571FE4) and exact pinwheel never vary, in any theme. This is distinct from the app's mutable action-indigo token, which does shift in the app's light theme; see Section 5.4. The mark itself is fixed, but production does not yet render it consistently: rx=36 is correct here and on the dev site, while other production instances currently ship different corner radii, see Section 19 for the audit.

Brand violet#571FE4

For the mark built to scale inside real contexts (favicon, sidebar, landing nav, avatar, app icon, share card), see Section 03A. For the full set of sanctioned colour and lockup variants, see Section 03B.

Size ladder

The icon-only mark holds up from 24px upward. Below that, the eight-arrow pinwheel compresses into a grey smear at typical screen density; treat 24px as the minimum for any size ContentFlow controls, in product, web, or app-icon contexts. The one exception is the browser-rendered favicon at 16px, where the browser chrome sets the size, not us; see Section 03A.

16px, too small
24px, minimum
32px
48px
72px
128px

The 16px tile is simulated at typical screen density: the arms fuse and the corner radius reads as a blur rather than a shape. Never ship the icon-only mark below 24px.

Clear space

Keep clear space around the mark equal to one quarter of its own width on every side. No text, icon, or edge may sit inside that margin.

96px mark, 24px dashed clear space on every side: exactly one quarter of the mark's width.

Backgrounds

The primary mark carries its own violet square, so it only reads correctly where that square has somewhere to sit.

Approvedon --void
Approvedon --ink3
Approvedon white
Not approvedon brand violet

On brand violet, the mark's own square disappears into the field behind it. Use the inverted mark instead: see Section 03B.

Never do this

Every distortion below is applied to a real copy of the canonical mark, not a redrawn approximation, so the failure is exact.

Do not

Stretched. Non-uniform scale breaks the square into a rectangle and throws off the pinwheel's radial symmetry.

Educational exhibit, not an asset. Do not export or reuse.

Do not

Rotated. The mark has one fixed orientation; tilting it reads as a rendering error, not a variant.

Educational exhibit, not an asset. Do not export or reuse.

Do not

Recoloured. The violet is exact; a hue shift is never a valid theme, dark or light.

Educational exhibit, not an asset. Do not export or reuse.

Do not

Drop-shadowed. The mark carries no glow, bevel, or shadow of its own; effects are always added by mistake, never by design.

Educational exhibit, not an asset. Do not export or reuse.

Do not

On a busy gradient. Without a solid or blurred container, the mark competes with its background and loses its edges.

Educational exhibit, not an asset. Do not export or reuse.

Never a hero motifThe mark is not a scroll-camera centerpiece or hero illustration: on its own it "doesn't imply anything." See Section 7 and Section 9.

Historical exploration files

  • CF logoFrame 54.png: wordmark / lockup exploration
  • CF logoFrame 55.png through CF logoFrame 63.png: logo exploration / icon mark variants
  • CF LogoScreenshot 2026-06-20 at 2.08.17 PM.png, 2.08.31 PM.png, 2.08.58 PM.png, 2.09.07 PM.png, 4.50.39 PM.png: logo exploration
  • CF LogoScreenshot 2026-06-25 at 10.33.21 PM.png, 10.33.40 PM.png, 10.34.26 PM.png: logo exploration

These exploration files predate the canonical mark above. Treat the canonical SVG as the single source of truth; use the exploration PNGs only as historical reference, not as alternates to ship.

Logo Use Cases

THE MARK, BUILT TO SCALE, IN CONTEXT

Each mock below is real CSS at the size the mark actually ships, not an illustration of the idea. If a context needs something the mark can't do here, it needs a different section, not a new logo.

Favicon

ContentFlow

16px in the browser tab: the one sanctioned exception below 24px, because the browser chrome sets the size, not us.

App sidebar

ContentFlow
Overview
Campaigns
Audience

30px beside the wordmark on --ink. This is a full-size context; never shrink it.

Landing nav

ProductPricingDocs
Get started

28px on --void, left-aligned, with the violet CTA as the only other colour in the bar.

Social avatar

@contentflow

56px, deliberately cropped to a circle for avatar surfaces only: the one sanctioned clipping exception, see Section 18.

App icon

64px with room to spare on a neutral tile. Platform icon masking is applied outside this artwork, never through it.

OG / share card

Move in-app content at marketing speed.

1.91:1, mark top-left, a short line of real brand voice that scales with the card, a violet edge, and nothing invented: no customer names, no fabricated metrics.

Logo Variants

ONE DEFAULT PLUS SEVEN EXCEPTIONS, EIGHT TOTAL

The primary lockup, a violet square with a white pinwheel, is the default. Everything below is a named exception for a specific context, not a free palette.

Primary Correct: everywhere, by default, with one exception. On brand violet, its own container disappears into the field; use the inverted mark there instead. Not banned elsewhere: this is still the baseline every other variant is measured against.
Mark only Correct: small badges and dense UI where a square container would crowd the layout. Banned: as a replacement for the primary lockup in any first-impression surface.
Inverted Correct: on brand violet fields or over photography with a controlled dark area. Banned: on white, light grey, or any surface where white loses contrast.
Monochrome black Correct: single-colour print, engraving, or stamping on light stock. Banned: on screen, where colour is free and the violet should be used instead.
Monochrome white Correct: single-colour stamping or foil on dark stock. Banned: on screen, for the same reason as monochrome black.
ContentFlow
Full lockup Mark and wordmark set in Figtree, vertically centred on the mark's optical middle, not its bounding box. Treat the gap between them as fixed, not a place to tighten kerning.
كونتنت فلو
Arabic lockup Set in IBM Plex Sans Arabic with dir="rtl", mark on the right of the word. The mark itself never mirrors, only the arrangement flips.
ContentFlow logo variants: permitted and banned contexts
VariantPermitted contextsBanned contexts
PrimaryDefault everywhere, except brand-violet fields, where the inverted mark is required insteadBrand-violet fields only; not banned elsewhere
Mark onlySmall badges, dense UI, favicon-adjacent chromeFirst-impression surfaces that need the full lockup
InvertedBrand violet fields, controlled photographyWhite or light backgrounds
Monochrome blackSingle-colour print, engraving, stamping on light stockAny on-screen surface
Monochrome whiteSingle-colour stamping or foil on dark stockAny on-screen surface
Full lockup (EN)Nav bars, covers, anywhere the wordmark needs to read on its ownSpaces under about 120px wide
Arabic lockupAny RTL surface: ar.html, Arabic app shell, Arabic decksMirroring the mark itself; only the layout direction flips
Stacked wordmarkDense vertical contexts, square social tiles, splash screens where a single-line wordmark would run too wideBreaking the line anywhere except between "Content" and "Flow"

Stacked wordmark

The master lockup board also pairs the mark with the wordmark broken across two lines, "Content" over "Flow," rather than set on one line. This is a stacked-line variant of the wordmark itself, not a new mark position: the single-line full lockup above stays the default everywhere it fits.

Content
Flow
Stacked wordmark Correct: dense vertical contexts, square social tiles, splash screens, where a single-line wordmark would run too wide. The line break always falls between "Content" and "Flow," never mid-word.

Source: ContentFlow Figma brand file, node 91:19338, rebuilt as code.

On dark photography

The same board also places the mark over starfields, sky photography, and concentric-ring textures instead of a flat fill, always inside a container, never loose on the busy field itself: the ungrounded failure case is already documented in Section 3's Never do this. Below, a CSS gradient stands in for photography, with the mark set inside a solid dark chip.

on photography, solid container

CSS gradient standing in for photography; the mark sits inside a solid dark chip rather than directly on the busy field. No blur is used here: blur stays reserved for nav chrome only, see Section 7.

Source: ContentFlow Figma brand file, node 91:19338, rebuilt as code.

Rejected Directions

WHAT THE MARK IS NOT

Four mark directions and one wordmark were explored on the brand board and rejected before the canonical pinwheel mark (Section 3) was locked. Each is redrawn here as a simplified vector evocation, not a pixel copy of the Figma render, purely so the reason for rejection is legible at a glance.

Do not

Asterisk / starburst badge Educational exhibit: this tile documents the rejected direction that motivated the guide's own no-star/sparkle guardrail (Section 18), which bans star and sparkle glyphs from production surfaces and distributable assets. Its simplified star-like shape is drawn deliberately, here only, as a non-exportable exhibit.

Educational exhibit, not an asset. Do not export or reuse.

Do not

Hexagon-in-circle badge Reads as a generic security or crypto badge, not a content or flow idea specific to ContentFlow.

Educational exhibit, not an asset. Do not export or reuse.

Do not

Chain-link mark Green broke the one-violet identity, and interlocking links read as integrations or blockchain, not in-app content.

Educational exhibit, not an asset. Do not export or reuse.

Do not

Lightning-chevron mark Generic "fast" iconography used across countless SaaS logos; it does not distinguish ContentFlow from any other speed-branded tool.

Educational exhibit, not an asset. Do not export or reuse.

Do not

Maspel
Old wordmark (pre-rename) The company's previous name, retired when the product repositioned as ContentFlow. Does not appear in current production brand assets; shown here only as a rejection exhibit.

Educational exhibit, not an asset. Do not export or reuse.

Source: ContentFlow Figma brand file, nodes 708:12235, 708:12229, 708:12223, 708:12241, and 708:12196, rebuilt as code.

Product and Brand Imagery

SYSTEM OVER STOCK PEOPLE

ContentFlow imagery should show the product as a system, not stock people. Use product UI, modular blocks, phone previews, bento layouts, and controlled editorial visuals.

Product UI screenshots (reference files)

01-overview.png, 02-campaigns.png, 03-analytics.png, 04-audience.png

Image direction

Do

  • CSS-drawn, fictional product UI recreated from real WebApp4 classes (see Section 13). Real workspace screenshots are also allowed in marketing and Open Graph imagery when the workspace owner approves their use. Keep the image faithful to the product, and use the approved workspace data as shown.
  • Phone-in-hand or phone-on-surface images with soft screen glow.
  • Modular block compositions.
  • Bento grids for feature communication.
  • Abstract diagrams showing ContentFlow as the hub between host app, analytics, CRM, identity, warehouse, and messaging tools.
  • Deep violet fields, faint dot grids, and precise UI overlays. Real product UI should look dense and functional, not like a sparse illustrative sketch.

Do not

  • Generic AI robot imagery.
  • Stock office people.
  • Overused SaaS blob gradients.
  • Random 3D objects that do not explain the product.
  • Dark dashboards with no readable hierarchy.
  • Monochrome, quiet-type, sparse mockups. A full flat-minimal redesign of the landing page (PR #289) was rejected outright as "so basic, so sloppy, too flat, fake mocks." Density and realism are requirements, not decoration.

Color System: Three-Surface Architecture

ONE VIOLET, THREE TOKEN SETS

The brand deliberately spans three surfaces that share one logo and one immutable brand violet, #571FE4 (never varies, in any theme, see Section 3), but each carries its own token set with its own mutable action-color token (see 5.4 below). This is not drift to fix, it is the intended architecture:

  • (a) Landing (contentflow.click): dark-default cinematic canvas tour, Figtree at light display weights, flat hairline surfaces.
  • (b) App (app.contentflow.click): dark-default product UI, Figtree body plus Bricolage Grotesque numerals, its own token set defined in WebApp4's index.css.
  • (c) Dev (dev.contentflow.click): a Linear-derived docs identity, Inter, near-black #08090a background, brighter indigo accent #7170ff. The dev site's palette is intentionally separate from landing and app. Do not "fix" it to match.

All three default to dark. The primary landing page, the app, and the dev site expose a [data-theme="light"] override (see 5.4 below); plans.html and contact.html are documented dark-only exceptions, see Section 19.

5.1 Landing tokens (landing-site/index.html, dark default)

:root {
  --void:#04050B; --board:#070912; --panel:rgba(9,11,20,.94);
  --bone:#EDEFF7; --dim:#98A1B9; --faint:#858DA2;
  --line:rgba(148,158,190,.14); --line-2:rgba(148,158,190,.28);
  --indigo:#571FE4; --iris:#8B7CFF; --glow:#BDB2FF; --live:#8B7CFF;
  --amber:#E8B04B; --red:#FF5D5D;
  --display:"Figtree"; --serif:"Bricolage Grotesque"; --body: system stack;
  --mono:"Figtree" /* kickers, not real monospace */; --code:"JetBrains Mono";
  --arabic:"IBM Plex Sans Arabic";
  --gutter: clamp(20px,4vw,64px); --maxw:1280px;
  --ease: cubic-bezier(.65,.05,0,1); --ease-out: cubic-bezier(.22,1,.36,1);
}

[data-theme="light"] {
  --void:#F7F8FC; --board:#FFFFFF; --panel:rgba(255,255,255,.95);
  --bone:#10131D; --dim:#4B5466; --faint:#5A6374;
  --line:rgba(26,18,54,.12); --line-2:rgba(26,18,54,.22);
  --iris:#6654E8; --glow:#571FE4;
}
void#04050B
board#070912
bone#EDEFF7
dim#98A1B9
faint#858DA2
indigo#571FE4
iris#8B7CFF
glow#BDB2FF
live#8B7CFF
amber#E8B04B
red#FF5D5D

Translucent border tokens (not chip-able as solid swatches):

Landing tokens: additional translucent border tokens (Token, Value)
TokenValue
--panelrgba(9,11,20,.94)
--linergba(148,158,190,.14)
--line-2rgba(148,158,190,.28)

Light-theme overrides

void#F7F8FC
board#FFFFFF
bone#10131D
dim#4B5466
faint#5A6374
iris#6654E8
glow#571FE4

No --radius or --shadow tokens exist on the landing page; radii are hardcoded per component. plans.html and contact.html carry their own duplicated, trimmed :root block, dark-only, no light theme or toggle. Flagged in Section 19.

5.2 App tokens (WebApp4/src/index.css, dark default, color-scheme:dark)

:root {
  --ink:#0B0D12; --surface:#13161E; --surface-2:#191D28; --surface-3:#1F2533;
  --line:#262C3A; --line-bright:#333B4D;
  --text:#E8EAF0; --text-dim:#9AA1B2; --text-faint:#646C7E;
  --indigo:#571FE4; --indigo-soft:rgba(87,31,228,0.16);
  --green:#2BD389; --green-soft:rgba(43,211,137,0.14); --amber:#F5B547;
  --bar-bg:rgba(11,13,18,0.9); --radius:14px; --radius-sm:9px;
  --mono:'JetBrains Mono','IBM Plex Sans Arabic',monospace;
  --label:'Figtree','IBM Plex Sans Arabic',system-ui,sans-serif;
}

[data-theme="light"] {
  --ink:#F5F6F9; --surface:#FFFFFF; --surface-2:#F1F3F8; --surface-3:#E7EAF1;
  --line:#E5E8EF; --line-bright:#D3D8E2;
  --text:#171A22; --text-dim:#586072; --text-faint:#8B92A2;
  --indigo:#5B57E0; --indigo-soft:rgba(91,87,224,0.10);
  --green:#179A63; --green-soft:rgba(23,154,99,0.12); --amber:#C0851C;
  --bar-bg:rgba(245,246,249,0.9);
}
ink#0B0D12
surface#13161E
surface-2#191D28
surface-3#1F2533
line#262C3A
line-bright#333B4D
text#E8EAF0
text-dim#9AA1B2
text-faint#646C7E
indigo#571FE4
green#2BD389
amber#F5B547

Light-theme overrides

ink#F5F6F9
surface#FFFFFF
surface-2#F1F3F8
surface-3#E7EAF1
line#E5E8EF
line-bright#D3D8E2
text#171A22
text-dim#586072
text-faint#8B92A2
indigo#5B57E0
green#179A63
amber#C0851C

Note the light theme's indigo is #5B57E0, not #571FE4. That shift is intentional (see 5.4 below). --label is Figtree, not --mono: labels, tags, and captions read in the body face; --mono is reserved for genuine code, keys, and JSON. A vestigial shadcn/Tailwind token-alias layer exists at the end of the file; it is not the production convention and should not be extended.

5.3 Dev-site tokens (dev-site/assets/styles.css, dark default)

:root {
  --bg:#08090a; --bg-1:#0f1011; --bg-2:#141516; --bg-3:#191a1b;
  --surface: rgba(255,255,255,.04); --surface-2: rgba(255,255,255,.08);
  --line: rgba(255,255,255,.08); --line-2: rgba(255,255,255,.14);
  --ink:#f7f8f8; --ink-2:#d0d6e0; --ink-soft:#8a8f98; --ink-4:#62666d;
  --accent:#7170ff; --accent-hover:#828fff; --accent-tint: rgba(113,112,255,.12);
  --purple-ink:#a5a0ff;
  --ok:#4cb782; --amber:#d9a76a; --red:#eb5757;
  --code-bg:#0f1011; --code-ink:#d0d6e0;
  --c-prompt:#4cb782; --c-kw:#b48eff; --c-id:#6ea8fe; --c-str:#d9a76a; --c-dim:#6b7078;
  --radius:10px; --radius-lg:14px; --nav-h:64px;
  --font:'Inter'; --mono:'JetBrains Mono';
}

[data-theme="light"] {
  --bg:#fff; --ink:#1b1c1f; --accent:#5e6ad2;
  /* full light remap in styles.css follows the same Linear pattern */
}
bg#08090a
bg-1#0f1011
bg-2#141516
bg-3#191a1b
ink#f7f8f8
ink-2#d0d6e0
ink-soft#8a8f98
ink-4#62666d
accent#7170ff
accent-hover#828fff
purple-ink#a5a0ff
ok#4cb782
amber#d9a76a
red#eb5757
code-bg#0f1011
code-ink#d0d6e0
c-prompt#4cb782
c-kw#b48eff
c-id#6ea8fe
c-str#d9a76a
c-dim#6b7078

Light-theme overrides (partial)

bg#fff
ink#1b1c1f
accent#5e6ad2

Full light remap in styles.css follows the same Linear pattern.

5.4 Theme rules (all three surfaces)

  • Dark is the default everywhere. Every surface stamps data-theme pre-paint via an inline head script to avoid a flash: landing checks a ?theme= param, then localStorage.cf_tl_theme, then prefers-color-scheme; app uses a ThemeProvider keyed on data-theme, not a media query; dev site uses localStorage.cf-theme.
  • App light theme shifts its action-indigo token from #571FE4 to #5B57E0. This is a mutable UI token, not the brand/logo violet: the logo itself always stays #571FE4 in every theme (Section 3). Do not "fix" the app's light-theme action token back to #571FE4.
  • Device mockups follow the page theme: a light page shows a light app, a dark page shows the dark app it always ran. The hardware stays dark in both. Two pinned exceptions and the full reasoning are in the canonical device section below.
  • Landing and app both expose a light override; plans.html/contact.html do not (Section 19).

The canonical device (.cfd)

All device mockups on the site use a single component, .cfd. The contract changed on 2026-07-26. The device used to be one frozen artefact, CSS, markup and copy, identical across all 41 instances, which meant every page showed the same banking screen regardless of what that page was arguing. The owner rejected that outcome directly: "you just copy and pasting the same screen, it should be consistent in its own use cases... I want you to double check every screen to be accurate, they shouldn't be the same design." The component is now split into two parts with different rules: a frozen shell, identical everywhere, and a free content region, composed per page out of a documented row vocabulary.

Physical dimensions and ratio

The device is exactly 288 pixels wide and 624 pixels tall, which yields a precise 19.5:9 aspect ratio. This fixed proportion is maintained everywhere the device appears.

Hardware design

Three nested solid fills, no lines or outlines anywhere: a titanium-toned outer band, then black glass, then the display recessed inside. The Dynamic Island pill sits at the top with a camera dot, and a home indicator bar is drawn at the bottom. The operating system status bar is hand-drawn geometry (time, signal, wifi, battery glyphs), never a bitmap.

Scaling and layout

The device scales uniformly via the --cfd-s CSS variable, from its top-left corner. At a 1280px viewport, --cfd-s is 1.0, so the device measures 288 by 624. At 375px and below, it scales to 0.88, measuring 253 by 549. This one size step lives in the CSS at @media (max-width:560px){.cfd{--cfd-s:.88}}. Every instance on the page shares the same scale at the same viewport width. The device is never reflowed to fit its slot; a narrower column gets a smaller device with identical proportions and bezel ratio.

The frozen shell

The shell is frozen and identical on every instance, regardless of page or vertical: the hardware described above, the operating system's status bar (.ac-sb), the app bar with its avatar (.ac-hd, including .ac-act and the .ac-av avatar), and the tab bar (.ac-tabs). Geometry, colors and structure are locked. scripts/check_device_drift.py compares every instance's shell skeleton, tag for tag and class for class, against the canonical shell derived from scripts/cf_device.py; changing a shell tag on any one page is drift, and fails the check.

The free content region

Everything inside .ac-body between the app bar's closing tag and the tab bar is free: each page writes a screen there for its own subject and vertical. It is not free-form HTML, though. It is composed from a documented vocabulary of rows, and scripts/check_device_drift.py rejects anything outside it. Two kinds of row exist.

Block rows stand for a ContentFlow block exactly as the host app would render it, and are wrapped in <div class="ac-r" data-blk="...">. The compare widget's overlay prints that data-blk attribute as the row's badge, so it has to name a real block format, never an invented one, and every content row on every page carries one truthfully; furniture is the only content-region markup allowed to skip it (below).

The content-region row vocabulary: row, class, block format, and what it looks like
RowClassBlock formatWhat it looks like
hero.ac-heroCard blockone full card: image, headline, body and a CTA
car.ac-carCarousel blocka strip of 2 to 8 shorter cards that actually scrolls, the next one peeking at the trailing edge; one card carries a LIVE badge, the rest PREV
ban.ac-banBanner blockinline banner strip, icon plus one line
mini / mini_cta.ac-miniCard blockcompact card, no image; mini_cta adds a CTA
toast.ac-toastToast blockconfirmation strip
list.ac-listCard blockcard holding 2-4 record rows: title, sub, trailing value
stat.ac-statCard blockcard with a status label, a progress bar and a caption
Corrected: hero and carousel used to be the same row.ac-hero and .ac-car used to be one row: a single fixed card badged "Carousel block," which the owner called out directly on 2026-07-27, a carousel that cannot move is not a carousel. .ac-car is the honest fix: a real horizontal strip built with cf_device.py's carousel() helper, wide enough that its scroll width genuinely exceeds its visible box, checked by scripts/check_device_rendered.py. .ac-hero keeps its own row for a single full card that really is one screen's worth of content; it is never badged "Carousel block" again. Every prior edition of this table listing .ac-hero as "Carousel block / Card block" and omitting .ac-car entirely was stale against both scripts/cf_device.py and the live markup: landing-site/index.html's three device screens each stamp data-blk="Card block" on their .ac-hero and data-blk="Carousel block" on their .ac-car strip.

Host furniture goes straight into .ac-body with no .ac-r wrapper and no data-blk badge, because no ContentFlow block produced it; badging it would be a lie told inside a product page. Three furniture rows exist: .ac-greet, the one-line greeting; .ac-bal, the account or summary panel; and .ac-met, a 2-3 tile metric strip.

The content region has a measured budget of 465.4px, the gap between the app bar's own margin and the top of the tab bar at the base 288-by-624 size. A screen's rows have to fit inside that budget, not overflow it.

Media plates (data-img)

A block row's media slot (.cf-card-thumb) can mount a real dithered picture instead of running on its abstract motif canvas alone, selected by a data-img="key" attribute. The attribute route exists because the shell skeleton is byte-checked and fixes .cf-card-thumb's own class list, so there is nowhere to hang a class; data-img is legal precisely because scripts/check_device_drift.py's skeleton comparison drops every attribute on purpose. Exactly two row shapes can carry a plate: .ac-hero's single card, and each card of an .ac-car strip (its own nested .cf-card-thumb, one level deeper under .dc). The two are not equally strict. A page's .ac-hero media slot is the whole story, so leaving it unmarked while the page declares any plate rule at all renders one bare grey rectangle where the rest of the screen shows pictures, and is checked as mandatory. An .ac-car card is not that: the mechanic this row restores deliberately shows a real photo on the LIVE card and a plain motif canvas on PREV ones, so a carousel card's data-img is validated when present (the key still has to resolve) but is never required. Each page declares exactly the plates it uses: scripts/check_device_drift.py derives a page's legal data-img keys straight from that page's own inlined CSS (every .ac-hero .cf-card-thumb[data-img="key"]::after / .ac-car .dc .cf-card-thumb[data-img="key"]::after rule it finds), not from a hardcoded list, so a key with no matching rule on that page fails, and a page that ships no plates at all (for example products/analytics.html, which runs every device screen on its motif canvas alone) is never asked to have any.

A screen belongs to its page and its vertical

A screen must belong to its page and its vertical. A reader who sees only the phone, with none of the surrounding page chrome, should still be able to name the page it came from. The banking screen is not a default to paste onto a fintech, insurance, marketplace or food and beverage page; each vertical gets its own screen, built from the row vocabulary above, showing what that vertical's own end users would actually see. A fifth industry page, food and beverage, is being added alongside the existing four.

Palette follows the page theme

The device palette follows the page's own data-theme, a change from 2026-07-26. A light page renders a light app; a dark page renders the dark app it always ran. The palette used to be pinned dark regardless of the surrounding theme; the owner overruled that directly: "in light mode it will be light, in dark mode it's really dark." In cf_device.py's DEVICE_CSS, the dark values are declared once as --cfd-dk-* custom properties, and the light overrides live under [data-theme="light"] .cfd, remapping the working tokens (--cfd-canvas, --s1/--s2/--s3, --bone/--dim/--faint, --iris and the rest) rather than restating the whole component.

The hardware stays dark in both themes. The titanium band, the black glass and the camera pill are the object, not the app, and a white bezel on a light page would read as a different phone. Only what the display actually shows flips: the app canvas, the ink ladder, the status-bar glyphs and the home indicator, which is exactly the set that flips on the real device. The light variant is a real light app (grey canvas, white cards, dark ink), not the dark screen inverted; its worst-case text contrast, faint tab labels sitting directly on the app canvas, measures 4.76:1.

Two exceptions stay pinned dark in both page themes: the two devices inside the drag-to-compare widget (.op-cmp, on landing-site/products/dynamic-blocks.html and landing-site/products/localization.html). That widget layers two devices as two states of one screen and lets the visitor drag a divider across them; the two halves have to agree about what they are, and half a comparison flipping to white would break the only thing the component is for.

Deck surfaces never set a page theme at all: the journeys pages and the pitch slides carry no data-theme attribute, so the device's dark default fires and they render the dark app, unchanged.

Content honesty

The app screen inside the device is finished content. Zero grey placeholder bars: no wireframe skeletons, no shimmer loaders. The screen carries real UI from the host app (account balances, action buttons, notifications, tab bars, content cards), all rendered in finished detail. A grey stand-in is the exact thing that makes a mockup read as a wireframe instead of a finished product.

Source and drift checking

The canonical source of the device CSS and markup lives in scripts/cf_device.py. Every page that uses a device inlines its own copy of the CSS because the site convention is fully self-contained pages with zero external CSS/JS requests. Two scripts check that inlined copy, at two different levels. scripts/check_device_drift.py checks the source: the inlined CSS block is byte-identical to the canonical one, every instance's shell matches the frozen skeleton, and every content-region row is a documented type from the vocabulary above, with a real data-blk naming every block row. scripts/check_device_rendered.py checks the rendered result in an actual browser: one device size per page, the correct 19.5:9 ratio, hardware present and centered, RTL genuinely mirrored rather than just dir="rtl" set, and the motif canvas actually painted rather than a blank grey slot. The source check catches an edited copy; the rendered check catches a device that is correct in source and still broken on screen, for example a later page rule overriding a .cfd descendant.

When a device is honest

A device mockup is only honest when it surrounds an end-user app screen, a page the user themselves would see in the app. Pages that diagram workspace surfaces (Audience rules, Analytics funnels, Campaign variants, Segment comparisons) have no device, and no phone is drawn around a real desktop portal screenshot. The device represents what end users receive, not what marketers see.

Color rules

  • Indigo is the main action color, not decoration everywhere.
  • Green is only for live, success, and positive signals.
  • Amber is only for review, scheduled, paused, or warning states.
  • Red/coral is only for destructive actions and rejection.
  • Separate surfaces with fill and space, not lines. Step between adjacent tokens on the surface's own tonal ladder (--void to --ink to --ink2/--ink3, or the App/Dev equivalents in 5.5) and let padding and gap carry the rest. A 1px hairline is the exception, earned only where a rule is genuinely the right tool: a persistent chrome boundary that has to hold regardless of theme (a sidebar edge, a dense reference table's row rule), or a single accent bar marking a state (a Do/Do not left edge). It is never the default way to separate a card, tile, or swatch from its background; see Section 7 for the corrected rule and why this guide used to say the opposite.
  • Landing's canvas exhibits use only three accent hues, not one per product: most exhibits use --tint:#8B7CFF, Journeys uses #FFB84E, Audiences uses #FF7AC6. The #products aurora deck is the one deliberate exception: each of the 8 cards keeps its own accent hue for its dithered motif, on a dark card, not a shared uniform accent on light panels (see Section 9).

5.5 The three palettes side by side

The same interface role gets a different token, and usually a visibly different value, on each surface. Dark-theme values shown below; see 5.1 to 5.3 above for each surface's own light-theme overrides.

The three palettes compared by role: Role, Landing, App, Dev
RoleLandingAppDev
Page canvas --void
#04050B
--ink
#0B0D12
--bg
#08090a
Raised surface --board
#070912
--surface
#13161E
--bg-1
#0f1011
Hairline border --line
rgba(148,158,190,.14)
--line
#262C3A
--line
rgba(255,255,255,.08)
Primary text --bone
#EDEFF7
--text
#E8EAF0
--ink
#f7f8f8
Secondary text --dim
#98A1B9
--text-dim
#9AA1B2
--ink-2
#d0d6e0
Action color --indigo
#571FE4
--indigo
#571FE4
--accent
#7170ff
Warning --amber
#E8B04B
--amber
#F5B547
--amber
#d9a76a
Error --red
#FF5D5D
none --red
#eb5757
Success none --green
#2BD389
--ok
#4cb782

App's error row reads "none" because the app has no live error token: there is no live --red token in index.css, and it has no fallback either. The dead legacy shadcn layer declares --destructive: #ef4444, but a zero-reference scan of WebApp4/src confirms nothing references it, see the dead layer covered in 5.8 below.

5.6 Name collisions

Three token names mean opposite or incompatible things depending on which surface declares them. Treating the name as portable across surfaces is the dangerous part.

Do not

App --ink
#0B0D12, page background
Dev --ink
#f7f8f8, primary text

Exact inverted roles under an identical name. Moving code between surfaces can silently turn a foreground color into a background color with no build error.

Do not

App --surface
#13161E, solid layer
Dev --surface
rgba(255,255,255,.04), overlay

Same name, different rendering models: App's is an opaque solid, Dev's is an alpha-composited overlay. Substituting one for the other silently changes contrast and stacking behavior.

Do not

App --accent alias
16% tint of --indigo
Dev --accent
#7170ff, solid CTA

Dev's --accent is the actual saturated brand action color; App's shadcn --accent alias is only a faint tint of a different token. Intensity and foreground-compatibility assumptions are incompatible.

5.7 Near-duplicate values

These are close enough to be visually indistinguishable, which is exactly what makes them easy to mistake for one shared token. They are not: each is independently tuned for its own surface.

Three near-blacks

Landing --panel
rgba(9,11,20,.94)
App --ink
#0b0d12
Dev --bg-1
#0f1011

Within a delta E of 3.5 to 8.4 of each other. Different roles though: a translucent panel, a base canvas, and a card. Landing --panel is the only token of the three that is not a solid color; composited over the landing --void #04050B backdrop it renders as approximately #090b13, which is the solid value used for the delta E comparison above.

Three light canvases

Landing --void
#f7f8fc
App --ink
#f5f6f9
Dev --bg-1
#f8f8f8

Within a delta E of 4.1. Dev's actual light-theme page background is white (--bg:#ffffff); --bg-1 here is a card, not the canvas, so the near-match is coincidental, not a shared role.

Secondary text, dark theme

Landing --dim
#98a1b9
App --text-dim
#9aa1b2

Delta E 7.3: close, but separately tuned against different dark canvases.

Secondary text, light theme

Landing --dim
#4b5466
App --text-dim
#586072

Delta E 5.9: same role, no evidence either value is erroneous.

5.8 Dead tokens

Fifteen declared tokens have zero live references, plus one entire stylesheet that is never imported. None of this has been deleted yet; it is flagged here so nobody extends or copies it forward.

Dead tokens and files: Surface, Token or file, Status
SurfaceToken or fileStatus
App--background, --foreground, --card, --popover, --primary, --secondary, --muted, --accent, --destructive, --border, --input, --ring, plus *-foreground variants19 declared names in the legacy shadcn layer; 12 are confirmed dead by zero-reference scan, but the extraction behind this guide does not isolate which 12. Do not delete by name alone, re-run a zero-reference check first.
Dev--bg-2Dead, #141516
Dev--bg-3Dead, #191a1b
Dev--purpleDead, alias of --accent
AppWebApp4/src/styles/globals.css (entire file)A duplicate shadcn token mapping never imported by any entry point. Production only imports index.css.

5.9 Consolidation proposal (proposed, not applied)

An independent review by GPT-5.6 Sol assessed every finding above and proposed a naming convention and a migration path. Nothing below has been applied to any repository; it is a proposal awaiting the owner's decision.

Sol's verdict, finding by finding: 1 accidental duplication, 3 dangerous name collisions to rename, and 11 deliberate cross-surface differences to keep exactly as they are.

Naming rule

All local color tokens move to --cf-{surface}-{role-family}-{role}, surface being landing, app, or dev. Role families describe function, never appearance:

  • Color-like names such as ink, bone, purple, or amber are not allowed as public token names; appearance is not a role.
  • Background and foreground roles must never share a role family.
  • Solid layers use surface; alpha-composited layers use overlay.
  • Solid action colors use action-primary; alpha tints use action-primary-tint.

The one token promoted to a shared root layer is --cf-core-brand-violet: #571FE4, the stable ContentFlow identity value. Dev deliberately does not alias its action color to it: #7170ff in dark, #5e6ad2 in light, stays Dev-specific.

Renaming the collision tokens (minimum set)

Old name to new name mapping for the three collision tokens
SurfaceOld nameProposed new name
App--ink--cf-app-canvas
Dev--ink--cf-dev-content-primary
App--surface--cf-app-surface-1
Dev--surface--cf-dev-overlay-subtle
Dev--accent--cf-dev-action-primary
Appshadcn --accent alias, if proven live--cf-app-action-primary-tint (delete instead, if proven dead)

Status colors (warning, error, success) are deliberately not proposed for a shared token: contrast against each surface's actual background has not yet been tested, and no status color should be consolidated until it has.

Proposal only, nothing changedThis is GPT-5.6 Sol's independent consolidation proposal, not a decision. No production code in any repository has been touched. It awaits the owner's sign-off before any migration begins.

5.10 Explored, not shipped

An early Figma color-combinations board paired the shipped primary violet with three candidate secondary accents. None reached production; the app, landing, and dev-site token sets above (5.1 to 5.3) still ship with no secondary accent at all. Shown for historical reference only, not as available tokens, and not named as tokens below.

Primary, shipped
#571FE4
Lime, exploration
#C8FF3A
Blue, exploration
#006FFD
Orange, exploration
#FF5A1F
The only shipped accent system is 5.1 to 5.4 aboveLime, blue, and orange were combination explorations on a color board, never wired into any surface's token set. Do not treat them as a pending secondary-accent proposal.

Source: ContentFlow Figma brand file, node 1083:5104, rebuilt as code.

Typography

SURFACE-SPECIFIC TYPE SYSTEMS

Typography is also surface-specific. Do not assume one font stack covers all three.

This scale is aspirational, not a report of what shippedEach subsection below names a small, closed set of sizes and weights per surface: a handful of headings, one body size, one label size. A site-wide count of what actually renders found 32 distinct heading sizes and 95 distinct body-text size/line-height/weight combinations in production, far past what any of these scales describe. This section is not fixed here; it documents the scale each surface is meant to converge on, few steps, each one named, so that drift has a target to be measured against. Treat any number below as the target, not as a claim already true on every page.

6.1 Landing

  • Fonts loaded: Bricolage Grotesque (opsz,wght 12..96,500..800), Figtree (300..900), JetBrains Mono (400..600), IBM Plex Sans Arabic (400;600, wider range on ar.html).
  • Roles: Figtree carries nearly everything (headings, buttons, labels, body). Bricolage is reserved for the single accent word inside a heading (the .it span, weight 750, colored var(--glow)). JetBrains Mono is for real code blocks only.
  • This is a correction from the previous edition, which had Bricolage carrying "landing headlines." It does not; Figtree does, at unusually light display weights.
Ship in-app content
Hero H162pxWeight 380Tracking -.02emLine 1.05
Ship in-app content
Hero H1, mobileclamp(2.7rem,12vw,4.2rem)Weight 390Tracking -.045em
Section heading
Section H2clamp(2rem,5vw,3.8rem)Weight 380
Capabilities
Capabilities H2clamp(1.9rem,3.8vw,3rem)Weight 380
Get started
Get-started H2clamp(2.5rem,5.8vw,5rem)Weight 380
Dynamic Blocks
Card H319 to 29pxWeight 700 to 750
Move in-app content at marketing speed.
Body / sub18pxLine 1.65Color --dim
Product platform
Kickers / micro-labels8.5 to 13pxUppercaseTracking .1em to .3emWeight 600

Signature move: very light variable-font display weights (380/390) on big Figtree headings, paired with negative tracking (-.01 to -.03em) on display, with the mobile hero H1 above at -.045em as the documented extreme, and wide positive tracking (.1 to .18em) on uppercase labels.

6.2 App

Figtree is the body base. Bricolage Grotesque is for headings and big numerals.

Overview
App H134px (27px at <=820px)Weight 600Tracking -.02em
1,033
KPI numbers (.kpi .kn, .bill-plan-price)18 to 34pxWeight 600 to 700Negative tracking

Labels, eyebrows, and table headers use var(--label) (Figtree), 9.5 to 11px, uppercase, letter-spacing .05em to .12em, color --text-faint. Body text 12.5 to 15px. Weights in active use: 300, 400, 500, 600, 700, 750, 800.

Mono is for code onlyJetBrains Mono is strictly for code, keys, and JSON, never for labels or marketing copy (see --mono vs --label in Section 5.2).

RTL: #appShell[dir=rtl] forces IBM Plex Sans Arabic on headings and titles with !important.

6.3 Dev site

Inter (variable, cv01, ss03 features) plus JetBrains Mono for code.

ContentFlow Docs
H1clamp(38px,5.4vw,58px)Weight 545Tracking -.032em
Get Started
H2clamp(22px,2.6vw,29px)Weight 538Tracking -.021em
Install and init
H315.5pxWeight 590
Body copy at 15px with 1.6 line-height.
Body15pxLine 1.6
Do not round the weightsThe non-round variable weights (538, 545, 590) are a deliberate Linear-esque quirk. Do not round them to 500/550/600.

Type behavior across all surfaces

  • Use IBM Plex Sans Arabic for all Arabic copy on landing and app; reset letter-spacing in RTL.
  • Micro labels can use uppercase in English, but not in Arabic.
  • JetBrains Mono is for code, keys, and JSON only, never marketing copy or UI labels, on any surface (the authoritative scope is Section 8, App UI principle 5).

Visual Language

FLAT SURFACES, NOT FLAT MINIMALISM

Flat, not flat-minimal

The shipped look across all three surfaces is flat: solid backgrounds, tonal fill steps, radius, and hover states driven by transform/border-color, not shadows or glow. This flattening (landing PRs #285/#286) is correct and live. Evidence from the landing audit: backdrop-filter appears twice in the whole file, one of them is a comment explicitly saying "no backdrop-filter"; the only real blur is the nav. box-shadow appears 24 times, none of them are decorative glow, they are inset bezels, focus pulses, and resets. As of the pre-2026-07-19 landing audit, that is, before the aurora deck shipped in PR #292, there were zero occurrences of "glass" in the landing CSS, and no glassmorphism surfaces exist anywhere in the app UI. "Aurora" now appears throughout this guide and the landing codebase as the deliberate name of the dithered #products deck (Section 9), not as an accidental drift back toward glass effects.

This is a correction from earlier editions of this guide, which described the flattening itself as "1px hairline borders" and repeated that phrase as house law in three places. No shadow or glow was ever the point; the flattening was about removing decoration, not about drawing an outline around every surface. Read literally, "prefer hairlines" told every page to knock a line around each card instead of just filling it a tone lighter or darker than its container, and pages complied: a direct measurement of this document alone (Playwright, whole document, both themes, phone mockups excluded) found 3,739 hairline edges and 453 boxes carrying a border with no fill behind it before this section was corrected. This guide was the worst offender on the site while teaching the rule that caused it. landing-site/products/analytics.html and landing-site/index.html show what the surfaces actually converged on: analytics.html carries 4 hairline edges in its entire document, index.html carries roughly one hairline edge for every four elements against this guide's roughly one per element pre-correction. The law is fixed above in Section 5's color rules; this section corrects its own two remaining restatements and demonstrates the replacement.

But flat does not mean flat-minimal. A full flat-minimal redesign of the landing page (PR #289) was explicitly rejected:

"so basic, so sloppy, too flat, fake mocks"

Monochrome color, quiet type, and sparse illustrative mockups read as broken, not premium. The approved direction is dense, animated, and built from real-looking, fully-detailed product surfaces, the canvas tour is the benchmark. Keep all three truths in view at once:

Do

Flat surfaces, built from fill and space. Tonal steps between a surface and its container, radius, generous padding, no glass, no glow, no decorative shadow.

Do not

Flat minimalism. Density, motion, and real (or faithfully CSS-recreated) product mockups are required, not optional polish.

Do not

Hairline-everything. A 1px line around every card, tile, and swatch is not "flat," it is a wireframe with the shading removed. Fill the box instead; keep the line only for a genuine rule (Section 5's color rules).

The two patterns below are the same card, one difference each: the left one separates from its page with a tonal step alone, the right one is defined only by a 1px line around an unfilled box, the knockout pattern this correction removes.

Do: tonal fill
Card content sits on --ink2, itself a step up from this box's own --ink, itself a step up from the page's --void. No border anywhere in the stack.
Do not: knockout box
Same background as the box around it. The only thing defining this shape is its own outline, the exact pattern that ran to 453 instances on this page alone.

The one sanctioned blur

Nav blur is the single exception to "no blur." The sanctioned scope is navigation chrome, including this guide's own mobile top bar: landing's fixed header (backdrop-filter: blur(13px)), dev site's sticky nav (blur(18px) saturate(1.4)), and this guide's own mobile .topbar (blur(12px)). Do not extend blur anywhere else.

The dither engine (products deck)

The #products aurora deck (Section 9) and the canvas tour's example-media slots share one Bayer-dither rendering approach, and it now has its own house rules for anyone extending it:

Visibility floors against the ambient plasma base (roughly .13 to .27 intensity):

Visual language: dither engine visibility floors (Layer, Minimum intensity)
LayerMinimum intensity
Ambient plasma base.13 to .27
Story elements (the motif itself)at least .32
Structural elements (rings, grids, connecting lines)at least .45
Highlights (the active/live accent)at least .9

These are dark-surface exceptions to the flat, no-glow rule above; they are a deliberate dark motif system, not a drift back toward glass or gradients.

Never

Never

Motifs from the design file

Two motifs from the brand-exploration board are sanctioned house motifs, rebuilt as code rather than pasted in as images.

Halftone pinwheel

A dithered grid built from the mark's own violet, arranged in four rotationally-symmetric quadrants that echo the pinwheel's arms. Generated as a 12×12 grid of squares at varying opacity, not a bitmap.

Adopted as a sanctioned motif for section dividers and dense background texture, dark surfaces only, per the visibility floors above.

Source: ContentFlow Figma brand file, node 937:337, rebuilt as code.

Quarter-circle poster device

A poster-family exploration builds its layout from one recurring device: a quarter circle anchoring a corner, Figtree display type, and a small fixed accent block. Adopted below as a sanctioned poster motif using this guide's own token palette, not the exploration's own orange and coral.

International
Design System
Figtree
Geometric
Content
flow
12–23
June
2026

A 3:4 poster block: page background, Figtree display numerals, a violet quarter circle anchoring the bottom-right corner, built with the app's own token palette.

Source: ContentFlow Figma brand file, node 1083:5548, rebuilt as code.

app.contentflow.click Product UI

THE OPERATIONAL CONTROL PLANE

App product identity

The app portal is a compact enterprise control plane. It should feel powerful, dense, and operational, but still clean enough for non-technical marketers.

App navigation (Sidebar.tsx / App.tsx)

Two sidebar groups:

App navigation: sidebar groups (Group, Items)
GroupItems
WorkspaceOverview, Campaigns (/campaigns), Analytics (/analytics), Audience (/audience), Reports (/reports), Approvals (/approvals, hidden from nav unless the admin view or user.canApprove is true)
BuildDynamic Blocks (/blocks), Content Library (/content), Messaging (/messaging), Developers (/developers, hidden in body.va-viewer), Localization (/localization), Journeys (/journeys), Sensors (/sensors, in preview), then parked at the bottom: AI Insights (/agent, flag ai-insights) and Marketplace (/marketplace, flag marketplace)

Beta-flag pill system

FEATURES = {
  'ai-insights': 'beta',
  'marketplace': 'beta',
  'moyasar-billing': 'beta',
  'sensors': 'beta'
}
// FeatureState: 'released' | 'beta'

Rendering rule:

App UI: beta-flag pill rendering rules (State, Unlocked, Render)
StateUnlockedRender
betauser.betaTester or viewAs === 'tester'indigo .beta-pill reading "Beta"
betalockedgrey .soon-pill reading "Soon"
releasedn/ano pill at all
Dynamic Blocks Beta Soon

Live demo built from the real .beta-pill / .soon-pill rules in WebApp4/src/index.css. This demo renders the app's real tokens verbatim, including color combinations that fail WCAG AA; see Section 16.

Sensors nonstandard pill, still openSensors is shipped and beta-gated the same way as the rest of this list, but its sidebar entry still uses a nonstandard "New" pill instead of the Beta/Soon system above; verified still true as of PR #303, not yet corrected. Flagged in Section 19, item 8.

Below the nav

Never "Upgrade to Pro"The upgrade card is never labeled "Upgrade to Pro" (Pro is hidden everywhere user-facing, see Section 19).

App UI principles

  1. Dashboard first. Data, status, and next action must be visible quickly.
  2. Preview is truth. The device mockup should show exactly what the end user receives, with finished content and no placeholder bars (Section 5.4).
  3. Arabic is first-class. RTL is full mirroring, not only text direction.
  4. No-code, but not simplistic. Marketers can publish, but enterprise controls are present.
  5. System text is clear. Mono (--mono, JetBrains Mono) is for system identifiers only: IDs, API keys, env tags, code, and JSON. Statuses, counts, captions, and every other UI label read in Figtree via --label (Section 5.2). This is the single authoritative scope for mono; where older copy suggests mono statuses or counts, this rule wins.
  6. Honest empty states, always. No fabricated data outside isShowcase workspaces; real workspaces show real numbers or an honest "not enough data yet" message (see AreaChart below).

Icon system (Icon.tsx)

A single stroke line-icon set of roughly 35 names (bell, eye, lock, image, chart, user, home, megaphone, gift, note, globe, target, cart, warning, bolt, check, clock, keyboard, route, grid, list, chevrons, and more).

App UI: icon system SVG attributes (Attribute, Value)
AttributeValue
viewBox0 0 24 24
fillnone
strokecurrentColor
stroke-width2
caps / joinsround
default size16px

This set replaced 68 pictographic color emojis across the app; never reintroduce emoji in product UI.

Dead entries, never call themThe PATHS object still contains dead star and sparkles path entries left over from the old icon set. Never call them. The no-star rule (Section 18) applies to this component directly.

Tag system (Tags.tsx)

Backs ChannelTag, PriorityTag, SegmentTag, all built on the shared .ctag class (inline-flex, 11px, weight 600, padding 3px 9px, radius 7px):

.ctag color variants:

green--green / --green-soft
indigo--indigo / --indigo-soft
amber--amber
coral#E5746C / rgba(.14)
violet#9B7BF0 / rgba(.16)
sky#4DA6F0 / rgba(.15)
slate--text-dim / --surface-3

Coral, violet, and sky are one-off hardcoded values, not tokens.

WhatsApp Push In-App SMS High priority Banner a1b2c3d4

Live demo built from the real .ctag color recipes in WebApp4/src/index.css. This demo renders the app's real tokens verbatim, including color combinations that fail WCAG AA; see Section 16.

AreaChart conventions

AreaChart.tsx is a hand-rolled inline SVG, no charting library: viewBox="0 0 560 210", indigo gradient area and line, an optional dashed --text-faint previous-period overlay, per-point hit-rects driving hover state with a crosshair and floating tooltip (ink fill, line stroke, JetBrains Mono ticks).

Honest empty stateAll-zero or empty data renders .cc-empty: "No {unit} yet, this chart fills in as events arrive." Never render a fake curve to avoid a blank chart.

Toasts, modals, loading

No skeleton loadersThere is no skeleton or shimmer loading state anywhere in the app. Loading uses .spinner (3px border, indigo top, 0.8s spin) or .explorer-loading, and route-level Suspense falls back to RouteLoading. Do not design a skeleton loader; it does not match the shipped pattern.

Sensors and the data page (in preview)

Sensors is Runtime Product Intelligence: every localization string is itself a dormant sensor. It renders normally and stays quiet until someone enables observation remotely, no app release required. Use these naming rules exactly:

Metrics vocabulary is fixed: Views, Interactions, Engagement rate, Reach, Live now. Do not substitute synonyms.

Business ruleEnabling sensors and managing saved views is free; analytics reads are gated to Pro and Enterprise plans.
Sensors: shipped console passes (Pass, PR, Description)
PassPRDescription
v1#291Initial shipped console, beta-gated
v1.1#297Sensor-first console and docs reframe
v1.2#303Honesty-and-edge-case pass

It remains beta-gated, not generally available: always describe it as "in preview," never as live. See Section 10 for the parallel docs-side language, which must use the same framing.

Two top-level tabs plus detail views, built from existing production primitives (.card, .kpi, .pill, .panel-card, .tbar, .switch, .ctag, .chip, .stepper, .rpt-type-card), plus a .sns-* layer spread across SensorList.tsx, SensorDetail.tsx, TrackerWizard.tsx, TrackerDetail.tsx, charts.tsx, and meta.tsx for what's genuinely new:

  1. Overview (default tab): h1 "Sensors", subhead "Runtime Product Intelligence: turn existing product language into observation points." A dismissible "Turn strings into sensors" intro panel, a "How signals work" panel linking the dev-site integration guide, and the saved-view card grid (the old "Trackers home"): per-type cards, Surface (indigo, e.g. "Checkout page" with Live now / Attention 7d / Engagement KPIs and a sparkline), Journey (violet, e.g. "Onboarding journey" with a 3-bar mini funnel and Reach), Copy watch (amber, e.g. "Summer launch message" with a presence strip), plus a dashed "+ New saved view" card.
  2. Enable sensors tab: the full paginated, searchable list of the workspace's localization strings, each with a dormant/enabled toggle and batch mini metrics; opening one shows sensor detail, per-string engagement plus a signal-status line.
  3. New saved view wizard (3 steps): template picker (Surface / Journey / Copy watch / a disabled "A language comparison" chip marked "Later," deferred), type configuration (name plus a found-strings list, with a note that sensors are existing localization strings, t() usage signals work with no app release, viewport accuracy needs data-cf-t), then report picks and a silent-sensor alert toggle ("Tell me if this sensor goes silent for 60 minutes").
  4. Surface detail (e.g. "Checkout page"): range pills (24h/7d/30d), KPIs (Sessions seeing checkout, Attention/views, Engagement rate, Interactions), top-engaged-copy bars, a 3-step funnel (Saw, Engaged, Converted with counts and percentages), an attention trend chart, and a sensors list with "Add sensors."
  5. Journey detail (e.g. "Onboarding journey"): KPIs (Entered, Completed, Completion rate, Biggest drop), a funnel hero of per-step cards (quoted copy, count, percent, an amber drop-off chip), an attention trend, and a caption clarifying that the ordered journey steps are the sensors and "Converted" is the SDK conversion event in the same session.
  6. Copy watch detail (e.g. "Summer launch message"): KPIs (Displays 30d, Display rate, Reach, Live now), a 90-day presence strip, a placements list, and a quiet "Export CSV" row.

The .sns-* CSS reuses production tokens and typography roles throughout: Bricolage for numerals, Figtree for labels, JetBrains Mono for keys, green/amber for status, the dashed add-new idiom for empty slots.

Extend, do not reinventDo not invent new visual language for Sensors; extend the existing system minimally, exactly as the shipped .sns-* layer does.

contentflow.click Landing Page

THE CANVAS TOUR AND THE AURORA DECK

Landing page identity

The landing site lives at the root domain, contentflow.click (not a tl. subdomain, that was stale). It should sell the product quickly: more editorial and emotional than the app portal, but still grounded in real UI, and it must stay dense and dark by default (Section 7).

Landing page role

The canvas tour (desktop only)

The signature mechanic is a scroll-driven camera over a virtual 6800x4200px world (#world), transform-panned and zoomed; page scroll (#track, 1120vh tall) is the input proxy. Seven exhibits sit at absolute coordinates: Mission brief, Dynamic Block anatomy, Rendered in your app, Campaign spec, Journey map, Localization, and Audience, plus overview waypoints before and after.

Landing page: canvas tour metrics (Metric, Value)
MetricValue
World size6800 x 4200px
Track height1120vh
Per-hop segment (SEG_VH)1.7 viewport-heights
Exhibit fit~86% viewport width / 78% viewport height
Exhibits7
Minimap (#mini)172 x 106px

Each hop is eased with requestAnimationFrame, and only writes the transform on actual movement (a performance guard). A #coords readout shows the current stop and zoom: at runtime it renders "0N / 0" + EXS.length (so "01 / 07" with all seven exhibits present) while an exhibit is active, and "MAP" on the zoomed-out overview waypoints.

Stale placeholder, harmlessThe static markup contains a hardcoded "01 / 06" placeholder that JS overwrites on load; still stale as of this edition (Section 19, item 9).

The tour's 7 example-media placeholders (the device editor scene and similar exhibit slots) have gone through three passes: PR #298 replaced the flat purple gradient with a mini aurora ground (dark base, masked dot grid, indigo glow), PR #301 layered in Bayer-dithered subject imagery as data-URI PNGs (the .cfdi-* classes: sunset, family photo, mug, city, and more) plus an interactive image picker in the scene-02 device editor, in both EN and AR, and PR #306 made the localization and journey slots' images static (no picker) while raising grid density for the journey-flow and publish-dunes motifs. Do not reintroduce the old flat purple placeholder.

Mobile disables the camera entirelyMobile (max-width 767px) disables the camera entirely. Track height collapses to 0; a separate script re-parents #capabilities, #products, #integrate, #pricing, #faq, #getstarted, and the footer into #ground as an ordinary linear page. Never assume the canvas tour renders on mobile, it deliberately does not.

Motion

Two easing tokens: --ease: cubic-bezier(.65,.05,0,1) for camera moves and scroll reveals, --ease-out: cubic-bezier(.22,1,.36,1) for hovers and [data-io] fades.

Landing page: motion durations (Motion, Duration)
MotionDuration
Hovers0.25 to 0.35s
Reveals0.5 to 0.9s
Progress fills1.1 to 1.3s
FAQ accordion0.5s

A nav "Motion: on/off" toggle (#motionBtn, localStorage cf_motion) combines with prefers-reduced-motion into a single calm() check: when either is true, count-ups snap instantly, camera settling is skipped, and a reduced-motion media block kills decorative animation.

The #products section (the aurora deck, 8 cards)

The panel carousel described in earlier editions of this guide was replaced outright (PR #292, live since 2026-07-19; polish through PR #306 on 2026-07-20). The section now sits above the bento capabilities section and is built as .pw2-track, still a horizontal scroll-snap carousel (overflow-x:auto, scroll-snap-type:x mandatory, drag-to-scroll), but each .pw2-panel is now a dark card with a live dithered canvas motif rather than a light app-screenshot mock.

Landing page: aurora deck card properties (Property, Value)
PropertyValue
Card widthflex:0 0 min(400px,84vw)
Card height520px
Radius26px
Snapsnap-centered

A .pw2-dither canvas per card is Bayer-dithered against an ambient plasma field. Card title sits bottom-left (logical start via inset-inline-start) with the ContentFlow pinwheel mark in the logical-start top corner, both RTL-aware. This section is the one deliberate exception to "dark page, light product mocks" (Section 4): it stays dark in both site themes by design, see Section 7's dither engine rules for the motion and visibility rules that govern it.

Eight cards, each with its own accent hue and a system-built (not literal-object) motif:

Landing page: aurora deck, eight product cards (#, Product, Page, Motif)
#ProductPageMotif
1Dynamic Blocks/products/dynamic-blocks.htmlA cube assembled from chunks, visible internal seams, two smaller satellite cubes drifting nearby.
2Campaigns/products/campaigns.htmlA cell-tower lattice mast broadcasting arc volleys, receiver points flaring as waves reach them.
3Journeys/products/journeys.htmlAnimated routes.
4Sensorslinks to dev.contentflow.clickA field of roughly 9 sensor nodes over a faint connecting mesh, each waking in turn with a pulse ring.
5Localization/products/localization.htmlA globe.
6Audiences/products/audiences.html112 dots drifting slowly between 4 clusters, each cluster with its own marker shape.
7Analytics/products/analytics.htmlA chart.
8Messaging Channels/products/messaging-channels.htmlA stacked chat motif with a push-up slot mechanic as messages cycle through.

Below the track: JS-generated quick-jump pills (.pw2-nav) and a progress bar (.pw2-prog).

Stat strip

.stats-strip (in the mobile hero and #ovi) ships hidden by default. Exactly two stats are shown: "Dynamic blocks live" and "Impressions served." It fetches https://app.contentflow.click/api/v1/sdk/public-stats, and only un-hides with a count-up animation on a clean 200 response; any failure keeps the whole strip hidden.

Dead dataA third value, users, is fetched but has no render slot, dead data (Section 19).

Pricing

The Pro plan card (.plan.hot) exists in the markup but is display:none, hidden site-wide by deliberate choice.

Do not re-enable without a decisionDo not re-enable the Pro card without an explicit product decision; the app's own billing UI shows Free/Enterprise only, matching this.

PDPL and consent

Neutral wording onlyStatus wording is neutral, never a green "ok" tint (Section 18).

Navigation and dead CSS

The live nav has exactly 5 links: Pricing, Developers, Book a demo, Open app, and Arabic (العربية).

No products trigger in the navA full mega-menu (.mega, .mega-btn, .mega-grid, .mcol, .mitem, .mwhat-card, .mega-foot) is fully styled in the stylesheet but has no HTML using it anywhere, it is dead, orphaned CSS; still true as of this edition. Do not build against it as if it were live; flagged for removal in Section 19.

An unlisted pitch deck also ships at contentflow.click/sale-deck/ (moved from pitch.html on 2026-08-20; the old URL now redirects) since 2026-07-20 (PRs #308, #309, #310): not in the nav or sitemap, footer-linked ("Pitch deck" / "العرض التقديمي") from both index.html and ar.html.

Arabic (ar.html)

html lang="ar" dir="rtl". The token block is identical to the English page except every font role remaps to IBM Plex Sans Arabic: --display, --serif, --mono, and --code all collapse to the one Arabic family, the whole type system flattens for RTL. RTL is implemented with CSS logical properties throughout (inset-inline-start, margin-inline-start, etc.); only three [dir=rtl] overrides are shared in index.html, plus one Arabic-only extra ([dir=rtl] .pw2-go svg{transform:scaleX(-1)}) that should be promoted to the shared rule set (Section 19).

dev.contentflow.click Developer Site

A LINEAR-DERIVED HOME FOR BUILDERS

Identity

The dev site is a Linear-derived docs identity, deliberately distinct from both landing and app: near-black #08090a background, off-white #f7f8f8 ink, brighter indigo accent #7170ff, and Inter instead of Figtree. It does not import WebApp4's CSS. See Section 5.3 for the full token block and Section 6.3 for the type scale.

Do not unify this paletteDo not unify this palette with landing or app, the separation is intended.

Pages

Developer site: pages (Page, Role)
PageRole
index.htmlThe main docs page, titled "ContentFlow Docs, SDK, API, CLI." All-docs-as-homepage.
docs.htmlA 45-line redirect shim only, not a real page.
wiki.html"ContentFlow Developer Wiki, Guides," covering Concepts, Architecture, Content lifecycle, Glossary, FAQ. Wiki is the "why," docs is the "how."

Layout

Sticky nav, 64px tall, backdrop-filter: blur(18px) saturate(1.4) over a translucent color-mix background, single-letter keyboard hints. Docs grid is 250px sidebar plus a 1fr main column (.doc-main, max 820px). The sidebar is sticky with its own scroll; active section is tracked with an IntersectionObserver. Below 900px width the sidebar collapses to a static block at the top of the page instead of floating.

Sidebar groups:

Developer site: sidebar groups (Group, Items)
GroupItems
Get StartedOverview, Quickstart, Auth, Core concepts
SDKInstall & init, Sync & render, Localization, Sensors
CLICards-as-code, Block types
REST APIOverview, Blocks, Audience, Strings, Campaigns, Webhooks
LearnWiki

The Sensors entry documents an in-preview feature (Section 8's Sensors subsection). The section's copy was reframed sensor-first alongside the v1.1 console work (PR #297): it now leads with the Runtime Product Intelligence line and explains dormant versus enabled sensors directly.

Still-open gapIt still only carries the "SDK Preview, REST today" eyebrow (which describes SDK availability, Section 15) and no line stating that Sensors itself is in preview; that gap is unchanged and still tracked in Section 19, item 8.

Components

macOS dot#ec6a5e
macOS dot#f4bf4f
macOS dot#61c554
Developer site: API method pill colors (Method pill, Color)
Method pillColor
.m.getgreen
.m.postpurple
.m.putamber
.m.delred

CF Flap

Do

A small canvas-based easter egg game, "CF Flap," sits at the bottom of index.html with a localStorage high score. This is an approved playful touch, keep it; it does not need to match the docs' otherwise serious tone.

Brand ties back to the system

Same #571FE4 pinwheel logo in the header as landing and app. Same consent-gated GA4 (G-5H2ZGZBKHL) and Clarity (xminhlruar) in every page head. Footer cross-links to both the app and the marketing site.

The dev site is a full member of the ContentFlow brand family, it just expresses it in its own type and color language, the way a company's engineering blog can look different from its marketing site while still being unmistakably the same company.

Copy System

CLEAR, CONFIDENT, PRODUCT-LED

Voice (English)

Clear, confident, and product-led. No hype. No fake AI magic. The product should sound like it solves a real enterprise bottleneck.

Example headlines

Example product copy

The SDK wording rule

Use the exact phraseUse the exact phrase "REST API today, SDKs in preview" whenever describing developer integration status. Never say "One SDK integration" or "SDK integration available"; native SDK packages are not yet published (Section 15).

Avoid

Do not

  • "Revolutionary"
  • "AI-powered everything"
  • "10x your growth"
  • Generic startup slogans without product meaning
  • Vague words like seamless, effortless, game-changing, unless supported by specifics
  • The em-dash character, anywhere, in any copy or document

Voice (Arabic)

Arabic copy follows a Thmanyh-style standard: a فُصحى (formal Arabic) spine, warmed with light Najdi markers (اللي، إحنا، تخلّي، متى ما، الجاي، تبي، شوف، بس، عشان). Confident, short sentences. Direct second person. No stiff MSA, and no translated-from-English smell.

Bilingual and RTL Rules

ARABIC AND ENGLISH, EQUAL BY DESIGN

Arabic and English must be equal across the system, this is documented and shipped on landing (ar.html) and app (dir=rtl app shell); the dev site's Arabic support is not currently documented and should not be assumed.

Rules

RTL sample

حملة الصيف: خصم على التحويلأنشئ حملة، اختر جمهورًا، انشر إلى التطبيق. النص والتخطيط ينعكسان بالكامل، وليس اتجاه الكتابة فقط

Rendered with dir="rtl", remapped to IBM Plex Sans Arabic, using vocabulary from Section 11 and the shipped product copy example.

Local features to preserve

Component Rules

REAL CLASSES, USED VERBATIM

Use verbatim, don't invent parallel conventionsThese are the real, shipped classes from WebApp4/src/index.css. Use them verbatim in mockups and new UI.

Buttons

Two ghost-button families existA second, parallel ghost-button family exists: .btn.ghost (transparent, 1px --line) plus .btn.sm. This is unconsolidated duplication, not two intentional variants (Section 19). Prefer .btn-ghost for new work until it's cleaned up.

Live demo: real .btn-ghost, .btn-pub, and the parallel .btn.ghost recipes copied from WebApp4/src/index.css. This demo renders the app's real tokens verbatim, including color combinations that fail WCAG AA; see Section 16. These exhibits are rendered inert with the disabled attribute, plus a guide-only override that suppresses the production disabled-state dimming so the color recipes stay legible; in the live app, disabled controls dim per the rule above.

Pills, cards, tags

.pill: font-size:13px; font-weight:500; color:var(--text-dim); background:transparent; border:1px solid var(--line); padding:7px 14px; border-radius:20px, hover border --line-bright.

.pill.active inverts, it does not turn indigo.pill.active inverts to background:var(--text); color:var(--ink); border-color:var(--text), a high-contrast text/ink swap, not an indigo fill. Do not assume active pills turn indigo.

.card: background:var(--surface); border:1px solid var(--line); border-radius:var(--radius), hover border-bright plus translateY(-2px). .kpi (18px padding), .panel-card (20px padding), and .md-row share this same recipe, it is the de facto card primitive across the app.

.ctag tag system: see Section 8 for the full color map (ChannelTag, PriorityTag, SegmentTag).

.chip family: .chip, .seg, .chip-toggle, .chip.health.warn/.chip.health.bad, .chip-mini.

.status dots: live, draft, review, paused, rejected, test, each a colored dot plus label, never color alone (Section 16).

All campaigns Active 7 days 30 days
KPI card
Panel card
Live Draft Review Paused Rejected Test

Live demo: real .pill/.pill.active, .chip-toggle, .kpi/.panel-card, and .status dot recipes copied from WebApp4/src/index.css. This demo renders the app's real tokens verbatim, including color combinations that fail WCAG AA; see Section 16.

Forms

Component rules: form field recipes (Recipe, Selectors, Background, Radius, Font-size)
RecipeSelectorsBackgroundRadiusFont-size
Generalinput[type=text/number], textarea, select.fld--surface--radius-sm14px
Modal/row (denser)input.f, textarea.f, select.f--ink9px13.5px

General recipe focus state: border --indigo plus background --surface-2.

Tables

No generic .table class; tables are scoped per surface: .fmt-table, .perm-table, .nba-table, .coh-table, .l10-table, .ppl-table, .fields-table. Shared shape: uppercase 10 to 10.5px --label headers in --text-faint, bottom-hairline rows, borderless last row, mobile falls back to horizontal scroll.

Charts

Dynamic Blocks card anatomy

Every Dynamic Blocks library card follows the same five-part anatomy. Annotated below on one representative card, recreated at this guide's own token scale to match the real .card, .chip, .status, and .blk-count recipes documented above.

card
Discovery Card
#discovery_cardHome screen
Live 3 blocks
  1. 1. Badge: a small pill naming the card's own kind (card, rule, btn, tile, banner, tip), pinned to the preview corner.
  2. 2. Title: the human-readable card name.
  3. 3. Identifier: the machine key, e.g. #discovery_card, plus a placement chip.
  4. 4. Status: a colored dot plus label (Live, New, Draft), never color alone, per Section 16.
  5. 5. Count: live instance count, e.g. .blk-count.

Numbered callouts are this guide's own annotation layer, not shipped chrome: the real card never shows them.

Source: ContentFlow Figma brand file, node 725:318, rebuilt as code.

Card types

Dynamic Blocks card anatomy: the six designed card types and their purpose
TypePurpose
Discovery CardSurfaces a new feature or offer on the home screen.
Benefits ScreenLists eligibility and perks for a finance product.
Action ButtonA single wallet or top-bar call to action.
Promo BannerHome-screen seasonal or campaign banner.
Insurance TileCompact cross-sell tile inside the Explore tab.
Onboarding TooltipFirst-session contextual hint anchored to a control.

Product Iconography

DUOTONE, ONE SOURCE, TWO STROKED EXCEPTIONS

Convention

Every product icon comes from Phosphor's duotone weight (MIT), vendored into a single file: WebApp4/src/components/shared/Icon.tsx. Nothing is hand-inlined at a call site any more. The grid is Phosphor's native viewBox="0 0 256 256"do not rescale the path data to 24×24, the coordinates will not survive it.

Both tones come from currentColor: the solid shapes take it directly, and the tone layer carries opacity=".2". That is the whole reason this set was chosen over a flat multi-colour one — an icon follows the user's picked accent and flips with the theme, where baked hex fills could do neither.

The two stroked exceptions: dropdown chevronschevron-down and chevron-up are the only stroked icons in the file. Phosphor's CaretDown/CaretUp duotone is a solid triangle plus a tone layer, which at the 11–18px a dropdown affordance actually renders reads as a heavy black wedge rather than a chevron. They ship as an open stroked path, round caps, stroke-width="24" on the 256 grid so the weight still matches the filled icons beside them.

stroke and fill are presentation attributes on the path, which beat the svg's inherited stroke:none — an inherited value loses to any declaration on the element itself. Do not hoist them onto the parent style; the icons will vanish.
Never draw a mark as a unicode characterA literal , , , or in the text takes the font's shape and weight, not the icon set's, and can fall back to a different glyph entirely depending on the platform's font stack. KPI deltas use DeltaIcon (arrow-up / arrow-down / minus). Status and required-field dots are drawn in CSS as a border-radius:50% span, inheriting currentColor. Sort markers use the chevron.

No tinted square behind a mark; a boxless mark is sized to roughly 72% of its slot. No star or sparkle shape, ever — see the guardrails.

Canonical set

All 59 icons, generated directly from the shipped PATHS map rather than redrawn, so this gallery cannot drift from the app.

analytics
approvals
arrow-down
arrow-up
bank
bell
bolt
calendar
campaigns
card
cart
chart
check
minus
chevron-down
chevron-up
circle-dashed
clock
close
comment
dashboard
developers
edit
eye
flag
gear
gift
globe
grid
heart
help
home
image
insights
keyboard
leaf
list
lock
mail
marketplace
megaphone
menu
moon
note
phone
play
plus
reports
route
search
sensors
steps
sun
target
trend-down
upload
user
warning
window

Source: WebApp4/src/components/shared/Icon.tsx, path data emitted verbatim from the shipped map.

History: what this replaced

Until 2026-07-29 the set was stroke-only — 24×24, fill="none", stroke-width={2}, round caps — hand-inlined into Sidebar.tsx and TopBar.tsx. That convention is gone: both files now use <Icon> exclusively, and only four stray 24×24 inline SVGs remain anywhere in the app against 242 <Icon> usages.

An earlier revision of this section carried a table arguing that production's stroked glyphs beat Figma's filled ones. That reasoning is superseded: the shipped set is duotone by deliberate choice, for the theming reason above. Figma's sidebar icons remain non-canonical, but the argument is no longer stroke-versus-fill.

Design System Decisions

THE RULE, THE WHY, THE TOKEN

Section 13 lists the real shipped classes. This section documents the decisions behind them, as short rules a developer can apply directly: what to do, why, and which token or class carries it. These apply to new UI work across all three surfaces, not only the classes already named above.

Tags and chips

Always a pill. A tag or chip never squares off; its radius is always border-radius: var(--radius-pill, 999px), the same token behind .pill, .chip, .chip-toggle, and .ctag (Section 13).

Always a single line. display:inline-flex; align-items:center; gap:4px; white-space:nowrap. A tag never wraps to a second line, and an icon inside one sits beside its label, never stacked above it. Rectangular is fine, just not as a tag: a table cell, a card, a panel, an input, a code block, anything roughly 32px tall or more reads as a surface, not a tag, and can take a squarer radius instead.

Segment: Riyadh region Priority 30 days Live

Live demo: real .chip, .ctag, .chip-toggle, and .status recipes copied from WebApp4/src/index.css, all sharing the same pill radius and single-line layout.

Buttons

Three sizes, never a fourth. .btn-sm (28px), .btn-md (34px, the default), and .btn-lg (40px) are the only heights a button ships at. All three share one base recipe: display:inline-flex; align-items:center; justify-content:center; gap:6px; white-space:nowrap; border-radius:var(--radius-sm); border:1px solid transparent.

Never override size with inline padding at the call sitePick .btn-sm/.btn-md/.btn-lg and stop there. A one-off style={{padding:'8px 14px'}} next to a real size class quietly forks a fourth height that only that one button has, and it stops tracking the ladder if the three real sizes are ever retuned.

It also cannot do the job people reach for it to do. Inline padding changes neither a fixed height nor a font-size, which is the other half of any size gap. 98 buttons in the app carried an inline padding override that was trying, and failing, to make two mismatched buttons agree.

The .fa-* form-action pair

.fa-pub (primary) and .fa-draft (secondary) predate the ladder above. They are pinned to the .btn-lg metrics now — via min-height, not height, so a long translation grows the button instead of clipping it. Before that, .fa-draft was 40px at --t-bd while .fa-pub had no height at all, so its size floated with its content and line-height and never matched the button beside it.

Two buttons in one row take the same size, alwaysWhen a row needs both families at one size, add .btn-sm/.btn-md/.btn-lg to both buttons. The compound selectors (.fa-pub.btn-md, .btn-ghost.btn-lg, …) exist precisely so this works regardless of source order — a bare .btn-md cannot win, being equal specificity to .fa-pub and earlier in the file.

Find these by scanning source, not the rendered page: nine mixed-size pairs existed, and a sweep of 18 routes surfaced one, because the rest live behind tabs and modals that never render.

Live demo: the real .btn-sm/.btn-md/.btn-lg height ladder from WebApp4/src/index.css.

Surfaces and borders

Never separate two surfaces with a light 1px hairline. Use fill and space instead. A change in background plus a gap reads as two distinct surfaces even at a glance; a hairline only reads up close, and is the first thing to disappear on a dim phone screen or a compressed screenshot. The ramp is --surface / --surface-2 / --surface-3; a child surface takes the next step down from its parent, for example the app card at --surface holding a usage meter at --surface-2.

border:1px solid transparent is deliberate, not dead code. It reserves the 1px of box model that a hover or focus border needs, without drawing a visible line at rest, so a state change never shifts layout by adding a border that was not there before.

A control never takes the same fill as the surface it sits onThis is the trap on the other side of the no-hairline rule. The flat restyle swept .btn-ghost, .sg-ghost and .fa-draft into the same #FFFFFF as .card and .form-card, so on a white card those buttons were white-on-white and read as bare text — Settings' "Change password", the Appearance Dark/Light pair, Localization's import/export row. With the border already gone, the fill was the only thing separating them, and it had been flattened away.

Controls rest at --surface-3, which clears both the white card and the page ink behind it, with --control-hover one step past that. Before adding a class to a flat-restyle selector list, ask whether it is a surface or a control. Same collision hit .inst-actions button at --surface-2 sitting on a .notif.unread row that was also --surface-2.

Floating surfaces separate with a shadow plus a solid fill, never a hairline. Menus, popovers, and modals sit above the page, so they need a different separation cue than a surface sitting inline. .modal/.palette use background:var(--surface) with box-shadow:0 24px 60px rgba(0,0,0,.4) and carry no border at all.

Colour and theming

Every colour placed on a coloured surface must flip with the theme. A hardcoded white icon on an indigo fill reads fine in dark mode and disappears the moment that fill's hex shifts for light mode. Colour ships in paired tokens instead: one for the fill, one for what sits on top of it.

Design system decisions: paired fill and on-fill colour tokens
Fill tokenPaired "on" token
--accent-fill--on-accent
--green--on-green
--amber--on-amber
--indigo--on-indigo
--sky--on-sky
--violet--on-violet
--danger / --danger-strong--on-danger

Never hardcode a status hex inline, and never pin #fff or #000 on a filled surface; both break the moment the theme or the user's picked accent changes. Route through the paired token instead. Contrast floor: text clears 4.5:1 against the ground it sits on; non-text graphics and focus rings clear 3:1.

--indigo and --accent-fill are not interchangeable--indigo is the accent used as FOREGROUND (text, icon, stroke) and must clear 4.5:1 on its own against the page. --accent-fill is the accent used as a FILLED background and always carries its paired --on-accent on top. Never use --indigo as a background-color; never use --accent-fill for text.
Focus rings use --indigo, never --accent-fillThe user's picked accent swaps into --accent-fill across twelve named colours, and white --on-accent text only clears 4.5:1 on two of them. A ring drawn from --accent-fill would measure as low as 1.59:1 on the Yellow accent, well under the 3:1 floor non-text UI needs. --indigo is picked per theme specifically to stay legible against the page background, so it is the ring colour everywhere, independent of whatever accent the user picked.
The trap: an invalid custom property drops the WHOLE declarationCSS custom properties fail atomically. color: var(--token-that-does-not-exist) does not fall back to inherited color; the entire color declaration is invalid and gets dropped, the same for background, border-color, or anything else set from a token. A typo'd or since-renamed token does not degrade gracefully, it silently unstyles the element while every neighbouring rule keeps working. Reference an existing token exactly, or use the fallback syntax already used throughout Section 13's classes, for example var(--radius-pill, 999px).

Icons

Real line icons only. Never a unicode glyph rendered as text (Settings' gear is exactly this mistake today, flagged in Section 13A), never an emoji (Section 18), and never a star or sparkle shape in any form, filled or stroked (Section 18; Section 19, row 14 tracks three shipped violations of this rule).

No tinted square behind an icon. The mark carries its own colour directly and does not need a coloured box to read. .ov-attn-ic is the canonical boxless mark: background:transparent, with color set to --danger/--amber/--indigo/--green depending on severity, no fill of any kind behind the glyph. A tinted-square badge, such as .bill-badge's background:var(--indigo-soft) or .wlt-act.dynamic .ic's solid --accent-fill square, is a shipped older pattern, not the target for new icon work.

Sized to about 72 percent of its slot. .ov-attn-ic is a 26px slot holding a 19px icon, about 73 percent, the ratio a boxless mark should target: enough breathing room around the glyph that it does not crowd its slot, without shrinking the mark down to illegibility.

Progress bars and charts

No gradients in a progress bar, a flat accent fill only. .bill-bar>i{background:var(--accent-fill)}; the warning state swaps to --amber and the critical state to --danger, always a flat colour, never a gradient.

The track sits one ramp step deeper than its container. .bill-meter, the card around a usage meter, is --surface-2; the .bill-bar track inside it is --surface, one step back toward --ink, so the groove reads as recessed rather than as another raised surface stacked on top.

Charts pull colour from one place: chartConfig(). WebApp4/src/components/shared/charts/tokens.ts exports ACCENT_SERIES (['var(--accent-fill)']) for a single-series chart, so it tracks whatever accent the user picked, and CATEGORY_SERIES (--indigo, --sky, --green, --amber, --violet, --danger) for a multi-series chart, deliberately not the accent, because a category colour encodes identity and must stay stable when the user's accent changes. Never a hardcoded hex in chart code; every series colour routes through one of these two exports.

Radius nesting

A child's radius is the parent's radius minus the parent's padding. A tighter inner radius reads as sharing the same curve as its parent instead of fighting it. The app's radius ladder, --radius (14px), --radius-sm (9px), --radius-xs (4px), gives most nested surfaces a ready-made next step down without computing it by hand each time.

A pill stays a pillNever compute an inset radius for a pill-shaped chip the way you would for a card. A pill's radius is var(--radius-pill, 999px) unconditionally, not "parent radius minus padding": that subtraction, applied to an already-999px shape, once returned 0 and squared off the sidebar tags. If either the parent or the child is a pill, skip the nesting math entirely.

Product Data and Example Entities

THE FIXTURES BEHIND EVERY MOCKUP

Dynamic block examples

Discovery Card, Benefits Screen, Action Button, Promo Banner, Insurance Tile, Onboarding Tooltip, Modal, Story/carousel unit.

Campaign examples

Cashback Summer Launch, Ramadan Engagement, Salary Transfer Offer, Card Activation Push, Insurance Renewal Reminder, New User Onboarding, Win-back Lapsed Users.

Audience examples

All users, New users under 30 days, Riyadh region, High-value cardholders, Salary above 3,000 SAR, Inactive 14 days, Premium customers, Users with consent enabled.

Sensors saved-view examples (in preview)

Developer and SDK Direction

REST API TODAY, SDKS IN PREVIEW

Developer promise

Integrate once. Let business teams manage content after that.

Current reality: REST API today, SDKs in preview

The REST API is live:

Developer and SDK direction: REST API endpoints (Method, Path, Description)
MethodPathDescription
GET/api/v1/sdk/stringsLocalized strings, with an ETag that changes when sensors update
POST/api/v1/sdk/eventsSignal emission, requires X-CF-Key, X-Tenant-Id, X-CF-Device, and consent:true
Native SDK packages are not yet publishedDo not describe SDK integration as available today; use the standard phrase from Section 11: "REST API today, SDKs in preview."

Where a code sample is useful, mark it clearly as the target usage pattern, not a shipped package:

// Planned SDK usage (packages not yet published; integrate via REST today)
ContentFlow.start(
  apiKey: "acme_app",   // SDK keys are tenant-scoped: <tenantId>_app,
                        // never a live-mode publishable key in the style other platforms use
  region: .ksa,
  locale: .ar
)

For string usage today, the pattern is cf.t("checkout.title"), which auto-emits usage impressions, plus data-cf-t markup for viewport- and click-based signals. Old SDK versions ignore smart keys safely.

Signal types

Developer and SDK direction: signal types (Type, Fields)
TypeFields
string_impressionkey, locale, source (usage or viewport), optional namespace/path
string_interactionkey, locale, optional namespace/path

Consent and dedup rules

Developer UI rules

Accessibility and Responsiveness

READABLE, FOCUSABLE, RESPONSIVE

Accessibility

Responsive behavior

Brand Asset Index

WHERE THE SOURCE FILES LIVE

Source files used for this edition

Logo and mark assets

Three tiers, from the single source of truth down to files that should never be referenced again.

Canonical vector (in this document)

The exact canonical mark, rendered here directly from this document's own sprite (#cf-mark). This is the single source of truth; every asset below should match it exactly.

Repo assets

Bitmaps cannot be embedded in this document; each is represented below by a labelled placeholder tile, not the actual image.

landing-site/og-image.png

Open Graph share-card image for the marketing site.

WebApp4/public/og-image.png

Open Graph share-card image for the app portal; per the imagery rule in Section 4, it may use an owner-approved real workspace screenshot or a faithful CSS recreation.

WebApp4/public/contentflow-mark.png

A raster copy of the mark used in the app shell instead of the vector; flagged in Section 19.

Exploration files (historical, on Desktop)

Exploration files, historical reference only: Series, Files
SeriesFiles
CF logoFrameCF logoFrame 54.png through CF logoFrame 63.png
CF LogoScreenshotSix files dated June 20 to June 25, 2026

These predate the canonical mark (Section 3) and exist only as historical exploration reference. They are not brand assets and must never be pulled into new work.

Product images

01-overview.png, 02-campaigns.png, 03-analytics.png, 04-audience.png

Real workspace data, never sharedThe app screenshots 01-overview.png and 02-campaigns.png in Desktop/ContentFlow/contentflow-screenshots/ contain real user and workspace data and must never be embedded in shared documents. UI illustrations in this guide are CSS reconstructions instead (Section 13).

Hard Guardrails

NON-NEGOTIABLE FOR PRODUCTION, NO EXCEPTIONS

These are non-negotiable for production surfaces and distributable assets. Any new asset, mockup, or line of copy must pass all of them.

Educational exhibit, not an assetAn educational exhibit is a demonstration inside this guide, visibly framed with the .dont treatment and captioned "Educational exhibit, not an asset. Do not export or reuse.", that exists only to document what is banned. Exhibits are not assets: they may not be exported or reused. Every guardrail below is absolute for production surfaces and distributable assets; exhibits inside this guide are the sole carve-out.
Logo, on production surfaces and distributable assetsExact canonical geometry only, #571FE4 rounded square (rx=36 on a 216px canvas) with a white eight-arrow pinwheel, and only in the sanctioned renderings defined in Section 03B: primary, mark-only, inverted, monochrome black, monochrome white, English lockup, Arabic lockup, and stacked wordmark, one default plus seven exceptions, eight sanctioned renderings total, plus the circular platform-avatar crop, where the platform imposes the mask, not us (Section 03A). Never reinvent, redraw, approximate, or clip it; anything outside this list is banned. Section 3's violation row and Section 03C's rejected-directions gallery redraw distorted and rejected treatments on purpose, as educational exhibits documenting what is banned; they are not assets and may not be exported or reused.
No pictographic emojis anywhereProduct UI, landing, dev site, marketing, documents, and OG imagery alike. In product UI, use the shared Icon.tsx line-icon set; elsewhere use the same line-icon language or plain text. Monochrome geometric glyphs, arrows, checks, and middots remain allowed. One explicit, sanctioned exception by standing decision: country-flag glyphs used as locale indicators (they are technically emoji, and they are the only emoji permitted; nothing else inherits this exception).
No star or sparkle glyphs, ever, on production surfaces and distributable assetsUse the image/media line icon instead. Icon.tsx still contains dead star and sparkles path entries; they must never be called, their presence in the file is not permission to use them. The one educational exhibit that renders a star-like shape (Section 03C) documents this exact ban; it is not an asset and may not be exported or reused.
No em-dash characterAnywhere in product copy or documents. Use commas, colons, parentheses, or the word "to."
Real workspace screenshots require owner approvalMarketing and Open Graph imagery may use real workspace screenshots when the workspace owner has approved their use. Otherwise, use CSS-drawn fictional UI recreated from real WebApp4 classes (Section 13).
Mockups must use real WebApp4 classes.pill, .btn-ghost, .btn-pub, .chip-toggle, and the rest of Section 13, never invented gradients or the landing page's own .seg pill styling.
RTL via CSS logical properties first[dir=rtl] overrides are rare exceptions, not the default mechanism (Section 12). Arabic remaps all font roles to IBM Plex Sans Arabic.
Honest empty states, alwaysNo fabricated data outside isShowcase workspaces.
PDPL chip statuses are neutral"Controls ready" or "In progress," never a green "ok" tint.
"Go Enterprise," never "Upgrade to Pro"Pro is hidden everywhere user-facing.
Commercial Registration line, verbatim, in the footer"ContentFlow Company, Commercial Registration (CR) 7036006992."
SDKs are in preview, not shippedNever claim native SDK packages exist. Use "REST API today, SDKs in preview" (Section 11, Section 15).

Known Inconsistencies to Fix

DOCUMENTED DRIFT, NOT DIRECTION

These are documented drifts between what's live and what a clean system would look like. They are not brand direction, they are bugs to eventually clean up. Listing them here so nobody "fixes" them the wrong way by copying the drift forward.

Known inconsistencies to fix (#, Issue, Detail)
#IssueDetail
1Dead mega-menu CSS on landing.mega, .mega-btn, .mega-grid, .mcol, .mitem, .mwhat-card, .mega-foot are fully styled but no HTML uses them. Orphaned; either wire it up or delete it. Re-checked against the repo on 2026-07-20: still true, no .mega markup exists anywhere in landing-site/index.html.
2--live color mismatchplans.html/contact.html use green #35D07F for --live, while index.html uses violet #8B7CFF. Same token name, two different meanings.
3plans.html/contact.html are dark-onlyThey carry their own duplicated, trimmed :root block with no light theme and no toggle, unlike the rest of the landing site.
4ar.html-only RTL rule[dir=rtl] .pw2-go svg{transform:scaleX(-1)} exists only in the Arabic page instead of the shared rule set. Should be promoted and shared.
5Two parallel ghost-button conventions in the app.btn-ghost and .btn.ghost both exist and do similar things. Needs consolidation onto one.
6Icon.tsx dead entriesThe PATHS object still contains star and sparkles paths left over from the pre-icon-system era. Should be deleted, not just left unused.
7Stats-strip users valueFetched from the public-stats API but has no render slot, dead data being pulled for nothing.
8Sensors preview framing gapsThe sidebar nav pill still says "New" instead of the standard Beta/Soon pill, and the dev-site Sensors docs section still lacks an explicit "Sensors is in preview" statement (its eyebrow covers SDK availability only). Re-checked on 2026-07-20 after v1.1 (PR #297) and v1.2 (PR #303): both gaps are still open, the docs section was reframed sensor-first but neither gap was addressed. Neither is optional cleanup: both violate the "in preview, everywhere, no exceptions" rule and must be corrected before Sensors leaves beta.
9Stale stop-counter placeholder on landingThe #stopN markup hardcodes "01 / 06" but the runtime denominator is EXS.length (currently 7). Harmless (JS overwrites it), but the placeholder should be updated or emptied so it can't flash a wrong count before JS runs. Re-checked on 2026-07-20: still present verbatim.
10Faint-text-tier contrast gap in documented tokensSeveral documented small-text tokens fall below 4.5:1 for their stated uses: app --text-faint:#646C7E on dark surfaces, app light --text-faint:#8B92A2 on white, and dev --ink-4:#62666d on #08090a. These are shipped, documented values (Section 5), reproduced verbatim here, not this guide's own chrome styling; see Section 16. Needs a token-level contrast pass before further small-text use.
11Logo corner-radius inconsistency in productionlanding-site/index.html ships the mark at both rx=0 (nav instances) and rx=46 (other instances); dev-site uses rx=36, which matches the canonical asset; WebApp4 uses a raster contentflow-mark.png instead of the vector at all. rx=36 is canonical (Section 3); the mark itself is fixed, production's rendering of it currently is not.
12--ink name collision between App and DevApp's --ink:#0B0D12 is the page canvas background; Dev's --ink:#f7f8f8 is the primary text color. Exact inverted roles under an identical name (Section 5.6).
1315 dead color tokens plus an unimported stylesheet12 orphaned shadcn tokens in the app's index.css, plus dev-site's --bg-2, --bg-3, and --purple, all confirmed zero-reference; WebApp4/src/styles/globals.css duplicates the shadcn mapping and is never imported by any entry point (Section 5.8).
14Shipped star/sparkle glyphs violate the guide's own guardrailThe sidebar's AI Insights icon (Sidebar.tsx) is a sparkle shape, live sparkle usage also appears at OnboardingFlow.tsx:554, and the Go Enterprise sidebar card uses a star shape (SidebarUpgrade in Sidebar.tsx). All three contradict Section 18's "no star or sparkle glyphs, ever" guardrail. See Section 13A for the icon-level detail. Fix tracked as a separate production task, not applied by this guide.

Final Creative Direction

ONE COMPANY, THREE SURFACES

ContentFlow should look like a modern enterprise SaaS product built for MENA growth teams: dark-violet identity, sharp product UI, bilingual confidence, modular blocks, clean motion, and real dashboard credibility, expressed across three deliberately distinct surfaces that all still read as unmistakably the same company.

The brand is strongest when it shows the actual product: campaigns, blocks, journeys, targeting, analytics, and a live app preview, all dense, all real, never flat-minimal (Section 7). Landing sells the story through a cinematic canvas tour, now fronted by an aurora deck that gives every product its own dark, dithered motif. The app is the operational control plane. The dev site is a lean, Linear-derived home for builders, deliberately its own palette.

Sensors is the newest pillar, Runtime Product Intelligence, shipped and running behind a beta flag: until it reaches general availability, it stays "in preview," everywhere, with no exceptions.