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)
- Dynamic Blocks: reusable in-app content units rendered in the host app at runtime. Integration status: REST API today, SDKs in preview.
- Campaigns: targeted deployments with scheduling, approvals, A/B testing, analytics, and lifecycle states.
- Journeys: multi-step user flows for onboarding, engagement, win-back, and seasonal campaigns.
- Audiences: segmentation by lifecycle, behavior, product usage, location, consent, and attributes.
- Localization: Arabic and English content management with a right-to-left content flag (the host app renders the RTL layout), approval-gated string publishing.
- Analytics: campaign analytics, cohorts, experiments, drilldowns.
- Messaging Channels: Push, WhatsApp, SMS, Popup, In-App (Twilio-backed where applicable).
- 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.
Parked behind beta flags ("Soon")
- AI Insights (
ai-insightsfeature flag): recommendations, copy generation, segment discovery, campaign draft creation. Visible to beta testers only; shows a "Soon" pill to everyone else. - Marketplace (
marketplacefeature flag): integrations directory. Same beta-gated treatment.
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).
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.
#571FE4For 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.
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.
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.
Historical exploration files
CF logoFrame 54.png: wordmark / lockup explorationCF logoFrame 55.pngthroughCF logoFrame 63.png: logo exploration / icon mark variantsCF 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 explorationCF 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
16px in the browser tab: the one sanctioned exception below 24px, because the browser chrome sets the size, not us.
App sidebar
30px beside the wordmark on --ink. This is a full-size context; never shrink it.
Landing nav
28px on --void, left-aligned, with the violet CTA as the only other colour in the bar.
Social avatar
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
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.
dir="rtl", mark on the right of the word. The mark itself never mirrors, only the arrangement flips.
| Variant | Permitted contexts | Banned contexts |
|---|---|---|
| Primary | Default everywhere, except brand-violet fields, where the inverted mark is required instead | Brand-violet fields only; not banned elsewhere |
| Mark only | Small badges, dense UI, favicon-adjacent chrome | First-impression surfaces that need the full lockup |
| Inverted | Brand violet fields, controlled photography | White or light backgrounds |
| Monochrome black | Single-colour print, engraving, stamping on light stock | Any on-screen surface |
| Monochrome white | Single-colour stamping or foil on dark stock | Any on-screen surface |
| Full lockup (EN) | Nav bars, covers, anywhere the wordmark needs to read on its own | Spaces under about 120px wide |
| Arabic lockup | Any RTL surface: ar.html, Arabic app shell, Arabic decks | Mirroring the mark itself; only the layout direction flips |
| Stacked wordmark | Dense vertical contexts, square social tiles, splash screens where a single-line wordmark would run too wide | Breaking 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.
Flow
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.
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
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'sindex.css. - (c) Dev (
dev.contentflow.click): a Linear-derived docs identity, Inter, near-black#08090abackground, 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;
}
#04050B#070912#EDEFF7#98A1B9#858DA2#571FE4#8B7CFF#BDB2FF#8B7CFF#E8B04B#FF5D5DTranslucent border tokens (not chip-able as solid swatches):
| Token | Value |
|---|---|
| --panel | rgba(9,11,20,.94) |
| --line | rgba(148,158,190,.14) |
| --line-2 | rgba(148,158,190,.28) |
Light-theme overrides
#F7F8FC#FFFFFF#10131D#4B5466#5A6374#6654E8#571FE4No --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);
}
#0B0D12#13161E#191D28#1F2533#262C3A#333B4D#E8EAF0#9AA1B2#646C7E#571FE4#2BD389#F5B547Light-theme overrides
#F5F6F9#FFFFFF#F1F3F8#E7EAF1#E5E8EF#D3D8E2#171A22#586072#8B92A2#5B57E0#179A63#C0851CNote 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 */
}
#08090a#0f1011#141516#191a1b#f7f8f8#d0d6e0#8a8f98#62666d#7170ff#828fff#a5a0ff#4cb782#d9a76a#eb5757#0f1011#d0d6e0#4cb782#b48eff#6ea8fe#d9a76a#6b7078Light-theme overrides (partial)
#fff#1b1c1f#5e6ad2Full 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-themepre-paint via an inline head script to avoid a flash: landing checks a?theme=param, thenlocalStorage.cf_tl_theme, thenprefers-color-scheme; app uses aThemeProviderkeyed ondata-theme, not a media query; dev site useslocalStorage.cf-theme. - App light theme shifts its action-indigo token from
#571FE4to#5B57E0. This is a mutable UI token, not the brand/logo violet: the logo itself always stays#571FE4in 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.htmldo 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).
| Row | Class | Block format | What it looks like |
|---|---|---|---|
| hero | .ac-hero | Card block | one full card: image, headline, body and a CTA |
| car | .ac-car | Carousel block | a 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-ban | Banner block | inline banner strip, icon plus one line |
| mini / mini_cta | .ac-mini | Card block | compact card, no image; mini_cta adds a CTA |
| toast | .ac-toast | Toast block | confirmation strip |
| list | .ac-list | Card block | card holding 2-4 record rows: title, sub, trailing value |
| stat | .ac-stat | Card block | card with a status label, a progress bar and a caption |
.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 (
--voidto--inkto--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#productsaurora 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.
| Role | Landing | App | Dev |
|---|---|---|---|
| Page canvas | |||
| Raised surface | |||
| Hairline border | |||
| Primary text | |||
| Secondary text | |||
| Action color | |||
| Warning | |||
| Error | |||
| Success |
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
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
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
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
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
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
Delta E 7.3: close, but separately tuned against different dark canvases.
Secondary text, light theme
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.
| Surface | Token or file | Status |
|---|---|---|
| App | --background, --foreground, --card, --popover, --primary, --secondary, --muted, --accent, --destructive, --border, --input, --ring, plus *-foreground variants | 19 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-2 | Dead, #141516 |
| Dev | --bg-3 | Dead, #191a1b |
| Dev | --purple | Dead, alias of --accent |
| App | WebApp4/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, oramberare 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 useoverlay. - Solid action colors use
action-primary; alpha tints useaction-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)
| Surface | Old name | Proposed 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 |
| App | shadcn --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.
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.
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.
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 onar.html). - Roles: Figtree carries nearly everything (headings, buttons, labels, body). Bricolage is reserved for the single accent word inside a heading (the
.itspan, weight 750, coloredvar(--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.
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.
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 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.
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.
--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.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:
- No literal single objects as a motif: a product's dithered scene is built from a small system (nodes, clusters, a lattice), never one static icon rendered large.
- Motion stays calm and consistent; random per-frame jitter reads as a rendering bug, not texture, and gets rejected on review.
- Composition balance matters: weight and motion should read evenly across the card, not cluster in one corner.
Visibility floors against the ambient plasma base (roughly .13 to .27 intensity):
| Layer | Minimum 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 use the logo as a hero motif (rejected direction, see Section 3).
- Never build a hero that rotates through all eight products, that duplicates the dedicated products section and was rejected for it. The hero tells one story.
- Never propose a monochrome, sparse, "quiet" redesign of a shipped surface without checking this section first.
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.
Design SystemFigtree
Geometric
flow
June2026
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:
| Group | Items |
|---|---|
| Workspace | Overview, Campaigns (/campaigns), Analytics (/analytics), Audience (/audience), Reports (/reports), Approvals (/approvals, hidden from nav unless the admin view or user.canApprove is true) |
| Build | Dynamic 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:
| State | Unlocked | Render |
|---|---|---|
| beta | user.betaTester or viewAs === 'tester' | indigo .beta-pill reading "Beta" |
| beta | locked | grey .soon-pill reading "Soon" |
| released | n/a | no pill at all |
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.
Below the nav
SidebarTrysetup checklist, with items quoted verbatim from the live UI: "Connect SDK", "Create a segment", "Set up messaging", "Invite your team." ("Connect SDK" here is existing shipped UI copy describing the integration step, not a status claim; new copy about integration status uses "REST API today, SDKs in preview," Section 11.) Hidden for showcase workspaces; items disappear viauseSetupSignalsas they're completed.SidebarTeam"My team": up to 6 members with.tm-dotpresence indicators.SidebarUpgrade"Go Enterprise" card, admin-only, dismissible per session per tier.
App UI principles
- Dashboard first. Data, status, and next action must be visible quickly.
- Preview is truth. The device mockup should show exactly what the end user receives, with finished content and no placeholder bars (Section 5.4).
- Arabic is first-class. RTL is full mirroring, not only text direction.
- No-code, but not simplistic. Marketers can publish, but enterprise controls are present.
- 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. - Honest empty states, always. No fabricated data outside
isShowcaseworkspaces; 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).
| Attribute | Value |
|---|---|
| viewBox | 0 0 24 24 |
| fill | none |
| stroke | currentColor |
| stroke-width | 2 |
| caps / joins | round |
| default size | 16px |
This set replaced 68 pictographic color emojis across the app; never reintroduce emoji in product UI.
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):
- ChannelTag: WhatsApp = green, Push = sky, In-App = violet, SMS = amber, Popup = coral, Banner = slate, Email = sky, Video = violet, fallback slate.
- PriorityTag:
priorityLevel >= 7= High (coral),>= 4= Medium (indigo), else Low (green), renders nothing if unset. - SegmentTag:
.ctag.outline, rendersnullfor raw UUIDs, never shows an unresolved id to a user.
.ctag color variants:
--green / --green-soft--indigo / --indigo-soft--amber#E5746C / rgba(.14)#9B7BF0 / rgba(.16)#4DA6F0 / rgba(.15)--text-dim / --surface-3Coral, violet, and sky are one-off hardcoded values, not tokens.
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).
.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
- Toast: fixed bottom-center,
--surface-2background, 1px--line-bright, radius 11px, slide/fade 0.3s, success uses green icon, error uses coral icon (ToastContext). - Modals:
.modal/.modal-box,.modal-wideat 780px,cf-pop-inkeyframes at 0.14 to 0.16s (opacity + translateY 6px + scale 0.985), reduced-motion respected, rendered through a portal (AppModal.tsx) to escape the topbar's backdrop-filter.
.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:
- The product is called Sensors.
- Enabling a string turns it into an active sensor; the string itself is the sensor, not a separate object you configure elsewhere.
- Saved views (the older "trackers" term) are optional groupings of enabled sensors around one product question, a Surface, a Journey, or a Copy watch. They help investigation but are not required to start observing.
Metrics vocabulary is fixed: Views, Interactions, Engagement rate, Reach, Live now. Do not substitute synonyms.
| Pass | PR | Description |
|---|---|---|
| v1 | #291 | Initial shipped console, beta-gated |
| v1.1 | #297 | Sensor-first console and docs reframe |
| v1.2 | #303 | Honesty-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:
- 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.
- 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.
- 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 needsdata-cf-t), then report picks and a silent-sensor alert toggle ("Tell me if this sensor goes silent for 60 minutes"). - 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."
- 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.
- 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.
.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
- Explain the pain: marketing moves fast externally, but in-app content is stuck.
- Show the solution: dynamic blocks, campaigns, journeys, targeting, analytics.
- Prove it is built for KSA/MENA: Arabic, RTL, PDPL, Hijri, enterprise workflows.
- Drive the CTA: request access, book a demo, or view the product.
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.
| Metric | Value |
|---|---|
| World size | 6800 x 4200px |
| Track height | 1120vh |
| Per-hop segment (SEG_VH) | 1.7 viewport-heights |
| Exhibit fit | ~86% viewport width / 78% viewport height |
| Exhibits | 7 |
| 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.
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.
#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.
| Motion | Duration |
|---|---|
| Hovers | 0.25 to 0.35s |
| Reveals | 0.5 to 0.9s |
| Progress fills | 1.1 to 1.3s |
| FAQ accordion | 0.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.
| Property | Value |
|---|---|
| Card width | flex:0 0 min(400px,84vw) |
| Card height | 520px |
| Radius | 26px |
| Snap | snap-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:
| # | Product | Page | Motif |
|---|---|---|---|
| 1 | Dynamic Blocks | /products/dynamic-blocks.html | A cube assembled from chunks, visible internal seams, two smaller satellite cubes drifting nearby. |
| 2 | Campaigns | /products/campaigns.html | A cell-tower lattice mast broadcasting arc volleys, receiver points flaring as waves reach them. |
| 3 | Journeys | /products/journeys.html | Animated routes. |
| 4 | Sensors | links to dev.contentflow.click | A field of roughly 9 sensor nodes over a faint connecting mesh, each waking in turn with a pulse ring. |
| 5 | Localization | /products/localization.html | A globe. |
| 6 | Audiences | /products/audiences.html | 112 dots drifting slowly between 4 clusters, each cluster with its own marker shape. |
| 7 | Analytics | /products/analytics.html | A chart. |
| 8 | Messaging Channels | /products/messaging-channels.html | A 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.
Pricing
The Pro plan card (.plan.hot) exists in the markup but is display:none, hidden site-wide by deliberate choice.
PDPL and consent
- Cookie toast (
.cookie): fixed bottom-left,min(330px),rgba(6,8,15,.94)background, 1px--line-2, radius 16px, solid-indigo Accept and outline Decline. - PDPL chip:
.b5-pill.b5-p2reading "PDPL-ready controls" with a lock/shield icon; a certifications-grid cell (.ccell) with a "PDPL" mono badge and the tag "Controls ready."
Navigation and dead CSS
The live nav has exactly 5 links: Pricing, Developers, Book a demo, Open app, and Arabic (العربية).
.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.
Pages
| Page | Role |
|---|---|
index.html | The main docs page, titled "ContentFlow Docs, SDK, API, CLI." All-docs-as-homepage. |
docs.html | A 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:
| Group | Items |
|---|---|
| Get Started | Overview, Quickstart, Auth, Core concepts |
| SDK | Install & init, Sync & render, Localization, Sensors |
| CLI | Cards-as-code, Block types |
| REST API | Overview, Blocks, Audience, Strings, Campaigns, Webhooks |
| Learn | Wiki |
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.
Components
.term: terminal blocks with a fake macOS title bar and syntax-colored spans using the--c-*tokens from Section 5.3.
#ec6a5e#f4bf4f#61c554.note: callouts,.tip(green),.warn(amber),.info(purple-ink)..cardgrids: 32px accent-tint icon chips, used for the block-type gallery (banner, card, tile, hero, carousel, list, popup, tooltip-toast, panel)..ep: API endpoint rows with a method pill and a mono.path.
| Method pill | Color |
|---|---|
.m.get | green |
.m.post | purple |
.m.put | amber |
.m.del | red |
- Plain hairline tables for reference data.
.tag: an eyebrow pill with a leading dot, used for section labels like "Developer platform, API v1" and "SDK Preview, REST today." For any new copy, the canonical phrase is "REST API today, SDKs in preview" (Section 11).
CF Flap
Do
A small canvas-based easter egg game, "CF Flap," sits at the bottom ofindex.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
- Ship in-app content without a release.
- Campaigns, journeys, and dynamic blocks in one place.
- Your app's internal campaign engine.
- Build once. Publish every day.
- Move in-app content at marketing speed.
- Target the right user, in the right screen, at the right moment.
Example product copy
- "Create a campaign, choose an audience, publish to the app."
- "ContentFlow renders dynamic blocks at runtime, so content can change without an app update."
- "Arabic and English content stay managed in one place; the host app renders the right-to-left layout."
The SDK wording rule
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.
- Keep product terms as-is in Arabic copy: SDK, API, block, campaign (بلوك، حملة). Do not force-translate technical vocabulary that Saudi/MENA product teams already use in English.
- Preserve numbers and plan limits exactly; never round or paraphrase a figure when translating.
- Arabic remaps all font roles to IBM Plex Sans Arabic (Section 6, Section 9).
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
- Arabic uses IBM Plex Sans Arabic everywhere (Section 6).
- RTL is implemented primarily through CSS logical properties (
inset-inline-start,margin-inline-start, and equivalents), not physical left/right properties. This is how the landing page achieves RTL with almost no duplicated rules. [dir=rtl]overrides should be rare exceptions, not the default mechanism. Landing currently has three shared[dir=rtl]rules plus one Arabic-only extra (.pw2-go svgmirroring) that should be promoted into the shared set rather than left as a one-off drift (Section 19).- Do not use tracked uppercase styles in Arabic.
- Numeric analytics can stay left-to-right where needed, but labels and layout should respect RTL.
- Arabic copy should feel Saudi/MENA business-friendly, per the Thmanyh standard in Section 11, not literal translated English.
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
- Saudi PDPL consent enforcement
- Arabic/English localization workflow with approval gating
- Banking/fintech/insurance readiness
- SAMA customer-scoped compliance posture, PDPL-ready with SDAIA pending
Component Rules
REAL CLASSES, USED VERBATIM
WebApp4/src/index.css. Use them verbatim in mockups and new UI.Buttons
.btn-ghost:font-size:13px; font-weight:600; color:var(--text); background:var(--surface-2); border:1px solid var(--line-bright); padding:8px 14px; border-radius:8px, hover background--surface-3..btn-ghost.del:hoverturns text/border to#E5746C..btn-ghost.on-seluses--indigo-softbackground and--indigotext/border for a selected state..btn-pub:color:#fff; background:var(--indigo); border:none; padding:8px 16px; border-radius:8px, hover#6B66F5.
.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.- Destructive: neutral until hover, then coral/red border and text.
- Disabled: reduce opacity, do not change layout.
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 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).
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
| Recipe | Selectors | Background | Radius | Font-size |
|---|---|---|---|---|
| General | input[type=text/number], textarea, select.fld | --surface | --radius-sm | 14px |
| Modal/row (denser) | input.f, textarea.f, select.f | --ink | 9px | 13.5px |
General recipe focus state: border --indigo plus background --surface-2.
- Error state:
.field.invalidborder#F5736B,.field-err11px#F5736B. - Keep labels short; use helper text only where needed.
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
- Use indigo and green as primary data colors; avoid rainbow palettes.
- Follow the
AreaChartconventions in Section 8: hand-rolled SVG, JetBrains Mono ticks, honest empty state, no fake data to fill a blank chart. - Keep legends compact.
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.
- 1. Badge: a small pill naming the card's own kind (
card,rule,btn,tile,banner,tip), pinned to the preview corner. - 2. Title: the human-readable card name.
- 3. Identifier: the machine key, e.g.
#discovery_card, plus a placement chip. - 4. Status: a colored dot plus label (Live, New, Draft), never color alone, per Section 16.
- 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
| Type | Purpose |
|---|---|
| Discovery Card | Surfaces a new feature or offer on the home screen. |
| Benefits Screen | Lists eligibility and perks for a finance product. |
| Action Button | A single wallet or top-bar call to action. |
| Promo Banner | Home-screen seasonal or campaign banner. |
| Insurance Tile | Compact cross-sell tile inside the Explore tab. |
| Onboarding Tooltip | First-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.
viewBox="0 0 256 256", never rescaledfill="currentColor"on the<svg>,stroke:none- tone layer = the same path at
opacity=".2" - one API:
<Icon name size className style /> - an unknown
namerenders the name as text, so a typo is visible rather than silent
chevron-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.▲, ▼, •, ✓ 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.
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.
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.
.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.
.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.
.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.
| Fill token | Paired "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 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.--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.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.
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)
- "Checkout page" (Surface saved view)
- "Onboarding journey" (Journey saved view)
- "Summer launch message" (Copy watch saved view)
- "A language comparison" (deferred template, disabled in the wizard, marked "Later")
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:
| Method | Path | Description |
|---|---|---|
| GET | /api/v1/sdk/strings | Localized strings, with an ETag that changes when sensors update |
| POST | /api/v1/sdk/events | Signal emission, requires X-CF-Key, X-Tenant-Id, X-CF-Device, and consent:true |
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
| Type | Fields |
|---|---|
string_impression | key, locale, source (usage or viewport), optional namespace/path |
string_interaction | key, locale, optional namespace/path |
Consent and dedup rules
- Session-scoped dedup per key.
- A viewport signal upgrades a usage signal without double-counting.
- Interactions are not deduped.
- Paths are normalized: query strings and fragments stripped, ids redacted to
:id, lowercased, capped at 128 characters.
Developer UI rules
- Code snippets use JetBrains Mono.
- API keys should be masked by default.
- Region and locale should be explicit.
- Connection state should be visible.
- Install wizard should be step-based and copy-friendly.
Accessibility and Responsiveness
READABLE, FOCUSABLE, RESPONSIVE
Accessibility
- Maintain readable contrast in both light and dark modes. This guide's own chrome text (body, muted, faint, table text, captions) has been verified at 4.5:1 or better in both themes. Several documented small-text tokens from the shipped app, landing, and dev-site palettes fall short of that in their stated uses; they are reproduced verbatim here because they are facts about what is shipped, not this guide's own styling, see Section 19, item 10 for the full list and Section 8/Section 13 for where
.app-demorenders them live. - Do not rely on color alone for status; use labels and dots together (Section 13).
- Focus states must be visible; the app uses an indigo border/ring on focus.
- Buttons and nav items should have enough tap area.
- Avoid tiny text for primary actions.
- Respect
prefers-reduced-motioneverywhere motion exists (Section 9's calm() pattern on landing; app modals respect it too).
Responsive behavior
- Sidebar becomes a drawer on mobile (app).
- Product grids collapse from 4 to 3 to 2 to 1 columns; use
repeat(N,minmax(0,1fr)), not plain1fr, to avoid fixed-width children inflating the layout. - Tables scroll horizontally on small screens.
- Device mockup scales to 0.88 on viewports below 560px; its palette follows the page theme, except the two pinned-dark instances inside the drag-to-compare widget (Section 5.4).
- Landing's canvas tour is desktop-only; mobile gets the linear re-parented page (Section 9). The dev-site sidebar collapses to a static top block below 900px (Section 10).
Brand Asset Index
WHERE THE SOURCE FILES LIVE
Source files used for this edition
landing-site/(index.html,ar.html,plans.html,contact.html,pitch.html) in the production monorepo: landing reference, contentflow.click.WebApp4/src/(index.css,App.tsx,Sidebar.tsx,Icon.tsx,Tags.tsx,AreaChart.tsx,lib/features.ts,components/sensors/*): app reference, app.contentflow.click, including the shipped, beta-gated Sensors console (PRs #291, #297, #303).dev-site/(index.html,docs.html,wiki.html,assets/styles.css,copy.js): dev-site reference, dev.contentflow.click.- The original "Sensors, ContentFlow" design artifact: the source for the
.sns-*CSS's original.tk-*/.wz-*/.dt-*classes, now shipped and iterated on twice since (v1.1, v1.2). Still in preview (beta-gated), but no longer just a design artifact. - Obsidian knowledge base (hard rules, rejected directions, copy standards).
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)
| Series | Files |
|---|---|
| CF logoFrame | CF logoFrame 54.png through CF logoFrame 63.png |
| CF LogoScreenshot | Six 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
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.
.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.#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.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).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..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.[dir=rtl] overrides are rare exceptions, not the default mechanism (Section 12). Arabic remaps all font roles to IBM Plex Sans Arabic.isShowcase workspaces.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.
| # | Issue | Detail |
|---|---|---|
| 1 | Dead 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 mismatch | plans.html/contact.html use green #35D07F for --live, while index.html uses violet #8B7CFF. Same token name, two different meanings. |
| 3 | plans.html/contact.html are dark-only | They carry their own duplicated, trimmed :root block with no light theme and no toggle, unlike the rest of the landing site. |
| 4 | ar.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. |
| 5 | Two parallel ghost-button conventions in the app | .btn-ghost and .btn.ghost both exist and do similar things. Needs consolidation onto one. |
| 6 | Icon.tsx dead entries | The PATHS object still contains star and sparkles paths left over from the pre-icon-system era. Should be deleted, not just left unused. |
| 7 | Stats-strip users value | Fetched from the public-stats API but has no render slot, dead data being pulled for nothing. |
| 8 | Sensors preview framing gaps | The 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. |
| 9 | Stale stop-counter placeholder on landing | The #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. |
| 10 | Faint-text-tier contrast gap in documented tokens | Several 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. |
| 11 | Logo corner-radius inconsistency in production | landing-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 Dev | App'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). |
| 13 | 15 dead color tokens plus an unimported stylesheet | 12 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). |
| 14 | Shipped star/sparkle glyphs violate the guide's own guardrail | The 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.