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
HCPs · Creative
→
_campaign_data.md
PM fills weekly
structured fields
structured fields
→
Claude Code
Reads data + standard
Updates index.html
Updates index.html
→
PM QA
Browser review
+ auto pre-check
+ auto pre-check
→
Admin Deploy
./deploy.sh
Cloudflare Pages
Cloudflare Pages
→
Live Portal
eq5-hub.pages.dev
Cloudflare Access
Cloudflare Access
Roles at a glance
🧑💼 Project Manager
Fill data file
Paste all exported fields into
_campaign_data.md for each active clientRun 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
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
Chart 1
Impressions Over Time
Section #performance · Line chart · Color: #0071E3
Chart 2
CTR by Week
Section #performance · Bar chart · Color: #F2842A
Chart 3
Channel Split
Section #audience · Doughnut · Cutout 68%
LinkedIn
Meta
Chart 4
Specialty Breakdown
Section #audience · Horizontal bar · indexAxis: 'y'
Chart 5
Delivery Pacing
Section #delivery · Single H-bar per brand · Progress style
Skin
66%
Lung
52%
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 area —
aspect-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
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.
Brand palette — portal.css & hub.css
--brand-blue
#3079E9 · hub.css
#0071E3 · portal.css
#0071E3 · portal.css
Primary actions, links, chart line, buttons
--brand-cyan
#00D3D3 · hub.css
#00C4C4 · portal.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
Regeneron
Equals 5 · NPI-Matched Social · Libtayo Portfolio
Campaign Performance
Impressions, CTR, and HCP reach over the active flight.
1,352,418
Impressions Delivered
3 · Audience & Reach
Active · NPI-Matched · June 2026
Monospace — code & file paths
hub/clients/regeneron/_campaign_data.md
--brand-blue: #3079E9
Menlo · Monaco · Courier New · system monospace
--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
.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 Name | Section | Chart.js type | Color | When used |
|---|---|---|---|---|---|
| C1 | Impressions Trend id: chart-impressions | #performance | line | #0071E3 | All active campaigns — required |
| C2 | CTR Trend id: chart-ctr | #performance | bar | #F2842A | All active campaigns — required |
| C3 | Channel Split id: chart-channel-split | #audience | doughnut | Palette order — blue first | 2+ channels — required |
| C4 | Specialty Breakdown id: chart-specialty | #audience | bar + indexAxis:'y' | Blue 80% | Always — specialty % only, no HCP names |
| C5 | Delivery Pacing Bar id: chart-pacing | #delivery | bar + indexAxis:'y' | Blue fill | One bar per brand/program |
| C6 | NPI Match Rate id: chart-npi-match | #audience | bar (single H) | Green | When 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
| Chart | Height | Notes |
|---|---|---|
| C1 Impressions Trend | 200px | responsive:true, maintainAspectRatio:false |
| C2 CTR Trend | 160px | Side-by-side with C1 when space allows |
| C3 Channel Split | 160×160 fixed | Not responsive — inline with legend list |
| C4 Specialty Breakdown | (N × 28) + 40px | Dynamic: 5 specialties = 180px |
| C5 Pacing Bar | 60px per brand | Stack vertically for multi-brand |
File & folder naming
kebab-case
Client slugs and hub routes —
novo-nordisk, libtayo-skin. Also shell scripts: deploy.sh, build-client-page.sh.snake_case
Python scripts and team folder names —
pull_account_status.py, ron_scalici/.UPPER_SNAKE_CASE
Process runbooks and canonical docs —
WEEKLY_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 assets —
libtayo-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 layout —
style="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> blockColor 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.