Admin · Portal System
Portal Playbook & Style Guide

Weekly update process, copy-ready prompts, document map, design tokens, chart naming conventions, and component rules — all in one place.

📋
How the system works
Weekly cadence — 4 steps, 2 roles
A PM collects data, fills a structured file, and runs a Claude Code prompt to rebuild the portal. An admin reviews and deploys. No code writing required — Claude handles the HTML.
Data flow
📊
Ad Platform
Impressions · CTR
HCPs · Creative
📄
_campaign_data.md
PM fills weekly
structured fields
🤖
Claude Code
Reads data + standard
Updates index.html
PM QA
Browser review
+ auto pre-check
🚀
Admin Deploy
./deploy.sh
Cloudflare Pages
🌐
Live Portal
eq5-hub.pages.dev
Cloudflare Access
Roles at a glance
🧑‍💼 Project Manager
1 Export data from ad platform Before filling the data file
KPI Strip — cumulative totals
impressions_delivered Total impressions to date (integer)
ctr_current_period Weekly avg CTR as float (e.g. 0.31)
hcps_reached Unique NPI-matched physicians (integer)
contracted_impressions Total contracted — used to compute pacing %
npi_match_rate NPI match % (integer — Regeneron: required)
Weekly trend — one row per completed week
weekly_labels Short date strings for X-axis (e.g. "5/12")
weekly_impressions Impressions delivered that week (integer)
weekly_ctr CTR that week as float (e.g. 0.34)
weekly_clicks Clicks that week (integer)
All four arrays must be the same length. Add one new row each week — never replace prior rows.
Audience — channel & specialty breakdown
channel_names List of channels (e.g. LinkedIn, Meta)
channel_percentages % per channel as integers — must sum to 100
specialty_breakdown.name Specialty label — no HCP names
specialty_breakdown.pct % of audience for that specialty (integer)
Ad creative — one entry per live ad unit
creative_units.label Human name (e.g. "LinkedIn Feed — Single Image")
creative_units.size Dimensions string (e.g. 1200×628)
creative_units.image_file Filename only — place file in assets/ads/
creative_units.impressions Impressions for this ad unit (integer)
creative_units.clicks Clicks for this ad unit (integer)
creative_units.ctr CTR for this ad unit as float (e.g. 0.38)
Fill data file
Paste all exported fields into _campaign_data.md for each active client
Run build prompt
Paste Build Prompt into Claude Code — one client at a time
QA page
Run auto pre-check prompt + manual browser review
🔐 Admin
Review QA sign-off
Confirm PM completed checklist for all active clients
Run deploy prompt
Paste Deploy Prompt into Claude Code
Verify live
Spot-check 2 live portal URLs after deploy
Update registry
Run registry update prompt — set Last Updated date
Active clients this cycle
Client Brands Owner Status Access
AstraZenecaCalquence · TagrissoChad H.ActiveInternal
DeerfieldDanziten · Opzelura · AryntaChris C.ActiveInternal
Health Monitor NetworkWegovy · Ozempic + moreBea L.ActiveInternal
IncyteJakafi · Niktimvo + moreChad H.ActiveInternal
Novo NordiskWegovy · Ozempic · NexlizetChad H.ActiveInternal
RegeneronLibtayo Skin · Libtayo LungChad H.ActiveInternal
SumitomoOAB Academy · Gemtesa + moreGreg R.ActiveInternal
HealioSelf-Service SOWChris C.ActiveInternal
Per-client folder anatomy
Every active client has this structure
Files are color-coded by who owns them and how often they change. Yellow = PM fills weekly. Green = Claude updates. Red = never rendered in HTML.
hub/clients/{client-slug}/ │ ├── README.md Claude reads first↳ Client brief: brands, platform notes, preferences, overrides │ ├── _campaign_data.md PM fills weekly↳ All metrics: KPIs, weekly trend arrays, channel split, specialty, creative units │ ├── _internal.md never rendered↳ Deal values, pipeline stage, probabilities — internal only │ ├── index.html Claude updates↳ Client-facing portal — rebuilt from data file each week │ ├── access.md↳ Cloudflare Access allowlist mirror — edit when adding/removing client emails │ ├── css/ │ └── portal.css ← styles (don't edit weekly) │ ├── assets/ │ └── ads/ │ ├── README.md ← asset tracker + naming rules │ ├── {brand}-{channel}-1200x628.png PM adds │ └── {brand}-{channel}-1080x1080.png │ └── programs/ └── {brand-slug}/ └── index.html ← brand-level reporting page
System-wide standards (read by Claude on every build)
internal/campaign_processes/ │ ├── PORTAL_PAGE_STANDARD.md Claude reads↳ Section order · Chart types · Ad display rules · CSS variables This is the canonical standard — client README.md overrides take precedence │ ├── CAMPAIGN_DATA_TEMPLATE.md↳ Field spec for _campaign_data.md — copy this when onboarding a new client │ ├── CLIENT_PORTAL_WEEKLY_UPDATE.md↳ Full runbook: steps, who does what, deploy instructions, rollback │ └── CLICKUP_WEEKLY_PORTAL_UPDATE.md ↳ ClickUp-ready task template with all prompts inline — copy each week
Claude's read order on every page build
1
hub/clients/{slug}/README.md
Client brief — brands, platform, quirks, overrides
2
hub/clients/{slug}/_campaign_data.md
Current week's numbers and creative unit list
3
internal/campaign_processes/PORTAL_PAGE_STANDARD.md
Section order, chart types, ad display rules
4
hub/clients/{slug}/index.html
The page to update — modified last, not first
Per-client files
📋 README.md
hub/clients/{slug}/README.md
Client brief. Brands, flight dates, platform notes, client preferences, asset inventory, contacts. Claude reads this before touching anything. Overrides take precedence over the page standard.
📊 _campaign_data.md
hub/clients/{slug}/_campaign_data.md
Structured YAML-style data file. All weekly metrics, trend arrays, channel split, specialty breakdown, and per-ad-unit creative performance. PM fills this each Tuesday before running the build prompt.
🔒 _internal.md
hub/clients/{slug}/_internal.md
Internal-only deal notes: pipeline stage, probability %, CRM deal IDs, rep action items, competitive context. Never rendered in HTML. Never referenced in the build prompt.
🌐 index.html
hub/clients/{slug}/index.html
The client-facing portal page. Claude rebuilds this from the data file and page standard each week. Never contains deal values, pipeline language, or HCP names.
🖼️ assets/ads/
hub/clients/{slug}/assets/ads/*.png
Ad creative images. Follow naming convention: {brand}-{channel}-{width}x{height}.png. PM places files here before running the build prompt. Claude renders a "Creative pending" placeholder if a file is missing.
🔑 access.md
hub/clients/{slug}/access.md
Version-controlled mirror of the Cloudflare Access allowlist for this client's portal. Edit when adding or removing client email addresses. Must match the live policy in the Cloudflare dashboard.
System-wide process files
📐 PORTAL_PAGE_STANDARD.md
internal/campaign_processes/
Canonical standard for all portal builds. Defines section order, Chart.js config templates for all 5 chart types, ad unit card HTML pattern, CSS variable usage, and date display rules.
📝 CAMPAIGN_DATA_TEMPLATE.md
internal/campaign_processes/
Blank _campaign_data.md template. Copy this when onboarding a new client. Contains all field names, types, and examples. Source of truth for what data the PM needs to collect.
📖 CLIENT_PORTAL_WEEKLY_UPDATE.md
internal/campaign_processes/
Full runbook with every step, role assignments, all three prompts (build / QA / deploy), Cloudflare dev preview instructions, and rollback procedure.
CLICKUP_WEEKLY_PORTAL_UPDATE.md
internal/campaign_processes/
ClickUp task template. 7 tasks across 4 groups with copy-paste prompts embedded. Create a new instance each Tuesday. Covers data export → build → QA → deploy → notify → registry update.
Three prompts run the whole system
Copy each prompt into Claude Code exactly as written. Replace the pink placeholders with the client name and slug. The rest is handled by Claude.
Prompt 1 — Page Build (PM · runs after data file is filled)
PM Page Build Prompt — paste into Claude Code
You are updating the client-facing campaign portal for [CLIENT NAME]. Source files: - Campaign data: hub/clients/[CLIENT-SLUG]/_campaign_data.md - Portal page: hub/clients/[CLIENT-SLUG]/index.html - Existing CSS: hub/clients/[CLIENT-SLUG]/css/portal.css Instructions: 1. Read _campaign_data.md fully before making any changes. 2. Update hub/clients/[CLIENT-SLUG]/index.html with the latest data. 3. Follow PORTAL_PAGE_STANDARD.md (internal/campaign_processes/) for section order, chart types, and creative display rules. 4. Data rules: - Never show individual HCP names — use Specialty only. - Do not show deal values, pipeline stage, or probability anywhere on the page. - All metrics are current as of the update_date in _campaign_data.md. 5. Charts: use Chart.js 4.4.2 (CDN). Match chart type per section in the standard. 6. Ad creative: follow the Ad Unit Display Standard in PORTAL_PAGE_STANDARD.md. 7. After editing, report: a) Which sections were updated b) Any data fields that were empty or ambiguous c) Confirmation the page passes the HCP anonymization rule Do NOT deploy. Do NOT run ./deploy.sh. Dev build only.
Prompt 2 — Automated QA Pre-Check (PM · runs after build, before manual review)
QA Automated Pre-Check Prompt — paste into Claude Code
Run a pre-deploy QA check on hub/clients/[CLIENT-SLUG]/index.html. Check and report on each of the following: 1. All chart canvas IDs exist and have matching Chart.js instantiation blocks. 2. All img src paths in the creative section resolve to files that exist in assets/ads/. 3. No individual HCP names appear anywhere in the HTML. (Search for: "Dr.", "MD", "NPI:", and name-like strings in table cells.) 4. No internal hub links appear. (Grep for /rates/, /team/, /competitive/, /tools/, /partners/ in href attributes.) 5. No deal $ values appear. (Grep for $ followed by digits in visible text nodes.) 6. The update_date shown on the page matches the update_date in _campaign_data.md. 7. The <title> tag matches the page header h1. Report: PASS or FAIL for each check. List any failures with the line number. Do not make any edits — report only.
Prompt 3 — Deploy (Admin only · after PM QA sign-off)
Admin Deploy Prompt — paste into Claude Code
The PM has approved all dev builds for this week's portal update. 1. Confirm the following files were modified in this session: - hub/clients/astrazeneca/index.html - hub/clients/deerfield/index.html - hub/clients/healio/index.html - hub/clients/health-monitor-network/index.html - hub/clients/incyte/index.html - hub/clients/novo-nordisk/index.html - hub/clients/regeneron/index.html - hub/clients/sumitomo/index.html 2. Run the deploy: CLOUDFLARE_API_TOKEN=<token> ./deploy.sh 3. After deploy completes, report: - Deploy exit code - Any files that were NOT in the modified list above - Confirmation the deploy log shows all 8 portal files uploaded Do not modify any files. Deploy only what was built and QA'd.
Prompt 4 — Registry Update (Admin · after deploy)
Admin Registry Update Prompt
Update hub/clients/REGISTRY.md. For each of the following clients, set the Last Updated column to today's date ([TODAY'S DATE, e.g. June 5, 2026]): - AstraZeneca - Deerfield - Healio - Health Monitor Network - Incyte - Novo Nordisk - Regeneron - Sumitomo Do not change any other columns.
Section anatomy — required order
1
#overview — Page Header + KPI Strip
4 stat cards (5 for some clients)
Required
2
#performance — Campaign Performance
Line chart + Bar chart
Required
3
#delivery — Delivery & Pacing
Horizontal pacing bar
Required
4
#audience — Audience & Reach
Donut + Horizontal bar
Required
5
#creative — Ad Creative & Engagement
Ad unit card grid
Required
6
#programs — Programs / Brands
Card grid (no chart)
Multi-brand only
7
#team — Your EQ5 Team
Contact cards
Required
Chart type reference
KPI strip — standard 4-card layout
Impressions
1.35M
Cumulative
CTR
0.31%
Current period
HCPs Reached
8,240
NPI-matched
Pacing
61%
Of contracted
Some clients use 5 cards (e.g. Regeneron adds NPI Match Rate). Always check the client's README.md for overrides.
Non-negotiable rules
🚫
No individual HCP names — ever
Specialty breakdown and engagement tables show specialty only. No NPI number, name, email, or hospital affiliation.
🚫
No deal values or pipeline language
No $ amounts, no pipeline stage names ("Proposal Sent", "Monitoring"), no probability percentages.
🚫
No internal hub navigation links
Portal pages must be self-contained. No links to /rates/, /team/, /competitive/, other client slugs, or any internal hub page.
Ad unit card — anatomy
Example card
Ad Creative
1200 × 628
LinkedIn Feed  ·  1200×628
368K
Impressions
0.38%
CTR
1,398
Clicks
What each part does
A
Image areaaspect-ratio:16/9, object-fit:contain, padding 12px. Never crops creative. Square ads use 1/1.
B
Label row — placement name + dimensions. Source: creative_units[].label + .size in data file.
C
Metrics row — Impressions · CTR (orange) · Clicks. 3-column grid. Source: .impressions, .ctr, .clicks.
!
If the image file is missing from assets/ads/, Claude renders a "Creative pending" div instead — never a broken img tag.
File naming convention
libtayo-skin-linkedin-1200x628.png libtayo-lung-meta-1080x1080.png wegovy-linkedin-1200x628.png
brand-slugKebab-case brand name
channellinkedin · meta · programmatic
WxH1200x628 · 1080x1080 · 300x250
Standard ad sizes
Size Placement Aspect ratio CSS
1200×628LinkedIn Feed · Meta Feed16/9aspect-ratio:16/9
1080×1080LinkedIn Square · Meta Square1/1aspect-ratio:1/1
1080×1920Instagram Stories · Meta Stories9/16aspect-ratio:9/16
300×250Display / Programmatic6/5aspect-ratio:300/250
Card grid layout
Grid: repeat(auto-fill, minmax(280px, 1fr))
Ad card
Ad card
Ad card
Ad card
Cards reflow automatically — 3 per row on desktop, 2 on tablet, 1 on mobile. Gap: 16px.
You've reviewed the full playbook
The ClickUp template has all prompts pre-loaded. Start there each Tuesday.
← Admin Home
Brand palette — portal.css & hub.css
--brand-blue
#3079E9 · hub.css
#0071E3 · portal.css
Primary actions, links, chart line, buttons
--brand-cyan
#00D3D3 · hub.css
#00C4C4 · portal.css
Gradient endpoint, accent, teal elements
--brand-navy
#0A1020
Dark hero backgrounds, dark cards, nav
--gradient
-28deg, blue → cyan
Brand gradient — logo, hero accents
Chart accent palette — use in this order, never invent new colors
Chart Blue
#0071E3
1st series · impressions · primary metric
Chart Orange
#F2842A
2nd series · CTR · secondary metric
Chart Teal
#00C4C4
3rd series · 3rd channel
Chart Purple
#8B5CF6
4th series
Chart Green
#10B981
5th series · positive delta
Surfaces
--bg
#F5F5F7
Page background
--surface
#FFFFFF
Card background
--surface-2
#F5F5F7
Table row hover, inner card tint
--surface-3
#EBEBED
Pressed state, deep inset
Text
--text-primary
#1D1D1F
Headings, values, primary copy
--text-secondary
#6E6E73
Body copy, card descriptions
--text-muted
#86868B
Labels, captions, metadata, dates
Status colors
--green
#30A46C
Active, delivered, success, on-track pacing
--orange
#D97706
Warning, paused, behind pacing — also CTR on KPI cards
--red
#DC2626
Error, critical, blocked
--gold
#B45309
Pipeline, estimated, pending approval
Typefaces
Hub (hub.css)
Inter
Google Fonts · 400–900 · UI-optimized geometric
Portal (portal.css)
SF Pro / System
System stack — SF Pro on Mac, Segoe on Windows
Type scale — portal pages
Page Title
48px · 900 · -0.03em
.page-title
Regeneron
Page Subtitle
15px · 400 · normal
.page-subtitle
Equals 5 · NPI-Matched Social · Libtayo Portfolio
Card Title
17px · 600 · -0.022em
.card-title
Campaign Performance
Card Description
14px · 400 · 1.5 lh
.card-desc
Impressions, CTR, and HCP reach over the active flight.
KPI Value
28px · 700 · -0.022em
.kpi-value
1,352,418
KPI Label
11px · 600 · 0.06em · UPPER
.kpi-label
Impressions Delivered
Section Label
11px · 600 · 0.08em · UPPER
.section-divider-label
3 · Audience & Reach
Micro / Badge
10–11px · 600–700 · 0.04em
.badge
Active · NPI-Matched · June 2026
Monospace — code & file paths
hub/clients/regeneron/_campaign_data.md
--brand-blue: #3079E9
Menlo · Monaco · Courier New · system monospace
KPI Card
KPI Card
.kpi-card
Always in a 4-up grid inside .kpi-strip. Value color variants: .green .blue .cyan .gold
Impressions
1.35M
Cumulative to date
CTR
0.31%
Current period
Badges
Status Badge
.badge .badge-{color}
Never use color alone — always pair with a text label.
ActivePausedPipelineLiveBlockedComplete
Buttons
Primary
.btn .btn-primary
One per view. Deploy, submit, confirm.
Secondary
.btn .btn-secondary
Supporting actions — back, download.
Outline
.btn .btn-outline
Low-emphasis tertiary on white backgrounds.
Content Card
Card
.card
Primary content container. Contains .card-title and optionally .card-desc.
Campaign Performance
Impressions and CTR trend across the active flight.
Section Divider
Section Divider
.section-divider
.section-divider-label
Precedes every major section. Format: "{N} · {Name}"
4 · Audience & Reach
Executive Snapshot (collapsed by default)
Exec Snap
.exec-snap.collapsed
TYPE A portals only. Must load collapsed — never open on load.
Partnership Snapshot
▸ Expand
$560K
Won 2026
2
Active Programs
Official chart names — use these exact terms
In prompts, ClickUp tasks, Slack, and code reviews — always use the canonical name and canvas ID below. This is the shared vocabulary.
#Canonical NameSectionChart.js typeColorWhen used
C1Impressions Trend
id: chart-impressions
#performanceline#0071E3All active campaigns — required
C2CTR Trend
id: chart-ctr
#performancebar#F2842AAll active campaigns — required
C3Channel Split
id: chart-channel-split
#audiencedoughnutPalette order — blue first2+ channels — required
C4Specialty Breakdown
id: chart-specialty
#audiencebar + indexAxis:'y'Blue 80%Always — specialty % only, no HCP names
C5Delivery Pacing Bar
id: chart-pacing
#deliverybar + indexAxis:'y'Blue fillOne bar per brand/program
C6NPI Match Rate
id: chart-npi-match
#audiencebar (single H)GreenWhen NPI data available — prominent for Regeneron
Chart.js global defaults — include once per page
// Always include before any new Chart(...) call Chart.defaults.font.family = '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; Chart.defaults.color = '#6B7280'; Chart.defaults.plugins.legend.display = false; // most charts hide legend
Canvas height conventions
ChartHeightNotes
C1 Impressions Trend200pxresponsive:true, maintainAspectRatio:false
C2 CTR Trend160pxSide-by-side with C1 when space allows
C3 Channel Split160×160 fixedNot responsive — inline with legend list
C4 Specialty Breakdown(N × 28) + 40pxDynamic: 5 specialties = 180px
C5 Pacing Bar60px per brandStack vertically for multi-brand
File & folder naming
kebab-case
Client slugs and hub routesnovo-nordisk, libtayo-skin. Also shell scripts: deploy.sh, build-client-page.sh.
snake_case
Python scripts and team folder namespull_account_status.py, ron_scalici/.
UPPER_SNAKE_CASE
Process runbooks and canonical docsWEEKLY_RUNBOOK.md, PORTAL_PAGE_STANDARD.md.
_underscore prefix
Internal-only files — never rendered_internal.md, _campaign_data.md. Signals "never goes into client HTML."
{brand}-{ch}-{WxH}
Ad creative assetslibtayo-skin-linkedin-1200x628.png. Always: brand slug · channel · dimensions · ext.
CSS class naming — BEM-lite
.block
Component root.card, .kpi-strip, .exec-snap, .page-header.
.block-element
Child of component.card-title, .kpi-value, .section-divider-label. Always prefixed with block name.
.block.modifier
State or variant.kpi-value.green, .badge.badge-orange, .exec-snap.collapsed.
Inline styles
Max 2 properties for one-off layoutstyle="margin-top:24px" is OK. More than 2 properties belongs in a class.
Chart canvas IDs
chart-impressions
C1 — Impressions Trend. Multi-brand suffix: chart-impressions-skin.
chart-ctr
C2 — CTR Trend bar chart.
chart-channel-split
C3 — Channel Split doughnut.
chart-specialty
C4 — Specialty Breakdown horizontal bar.
chart-pacing
C5 — Delivery Pacing Bar. One chart with multiple datasets preferred over separate canvases.
chart-npi-match
C6 — NPI Match Rate single horizontal bar.
Section anchor IDs — portal pages
#overview
Page header + KPI strip. Always first.
#performance
C1 Impressions Trend + C2 CTR Trend.
#delivery
C5 Delivery Pacing Bar.
#audience
C3 Channel Split + C4 Specialty Breakdown + C6 NPI Match Rate.
#creative
Ad unit card grid.
#programs
Multi-brand summary cards — multi-brand clients only.
#team
EQ5 contact cards. Always last.
Data field names — _campaign_data.md
impressions_delivered
Cumulative total impressions. Integer. KPI strip Card 1.
ctr_current_period
CTR for the reporting period (weekly avg). Float, e.g. 0.31 → displays as "0.31%". KPI Card 2.
hcps_reached
Unique NPI-matched physicians reached. Integer. Never call this "users" or "people." KPI Card 3.
campaign_pacing_pct
Impressions delivered ÷ contracted. Integer %. KPI Card 4.
specialty_breakdown
C4 data. List of {name, pct}. Specialty names only — never HCP names or NPI numbers.
creative_units
Ad unit array. Each entry needs: label, size, image_file, impressions, clicks, ctr.
Non-negotiable — content rules
🚫
No individual HCP names — ever
Specialty breakdowns show Specialty only. No NPI number, physician name, email, or hospital. Applies to all portal types.
🚫
No deal values or pipeline language on client-facing pages
No $ amounts, no pipeline stage names, no probability %. These belong in _internal.md only.
🚫
No internal hub links on portal pages
No links to /rates/, /team/, /competitive/, other client slugs. Navigation back to the hub via the top nav only.
🚫
Exec Snapshot must always load collapsed
Every TYPE A portal uses class="exec-snap collapsed". Never open on page load — the block may be screen-shared.
Do / Don't — charts
✓ Do
Use var(--brand-blue) for the primary series on every chart
Set responsive:true, maintainAspectRatio:false on line and bar charts
Use canonical canvas IDs so QA scripts can find them
Include Chart.js globals once per page before any new Chart()
Use borderRadius:4 on bar charts
Label Y-axis with units: v => v.toLocaleString() for impressions
✗ Don't
Invent chart types outside C1–C6
Hard-code hex colors in chart configs
Show legend when only one series — default is display:false
Use chart labels that could identify an HCP
Use beginAtZero:true on the impressions trend line
Load Chart.js more than once per page
Do / Don't — CSS & components
✓ Do
Use CSS variables for all colors — var(--brand-blue) not #3079E9
Use .badge.badge-green for status — never a raw colored div
Follow section order: overview → performance → delivery → audience → creative → programs → team
Use absolute dates — June 5, 2026 not "last week"
✗ Don't
Write more than 2 inline style properties — use a class
Use relative dates ("this week") — they go stale
Skip .section-divider between major sections
Duplicate CSS from portal.css in a page <style> block
Color usage rules
CTR values
Always orange — var(--orange) on KPI cards and C2 chart bars. This is the team-wide convention.
Pacing thresholds
≥75% → green. 50–74% → default text. <50% → orange. Never red — it's not an error state.
HCPs Reached
Default var(--text-primary). Never orange — that's reserved for CTR.
Status badges
Active → green · Paused → orange · Complete → muted · Blocked → red · Pipeline → blue · Live access → cyan.
Dark sections
Background #0A1020. Full-opacity white for headings, rgba(255,255,255,.5) for subtext. Never pure black.