diff --git a/.github/workflows/brand-guard.yml b/.github/workflows/brand-guard.yml new file mode 100644 index 00000000..8d1b158f --- /dev/null +++ b/.github/workflows/brand-guard.yml @@ -0,0 +1,22 @@ +name: Brand guard + +on: + push: + branches: [main] + pull_request: # run on all PRs regardless of target branch, matching ci.yml + +permissions: + contents: read + +concurrency: + group: brand-guard-${{ github.ref }} + cancel-in-progress: true + +jobs: + brand-guard: + name: Brand invariants + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Check brand invariants + run: node scripts/brand-guard.mjs diff --git a/BRANDING.md b/BRANDING.md new file mode 100644 index 00000000..0a281af9 --- /dev/null +++ b/BRANDING.md @@ -0,0 +1,74 @@ +# BodyMaps branding — how to style this app + +The visual identity comes from the BodyMaps brand pack (v1.0, July 2026). The +tokens live in `PanTS-Demo/src/styles/brand.css` and are available globally — +use them instead of hex literals for any app-chrome color or font. + +CI runs `node scripts/brand-guard.mjs` (the **Brand guard** workflow) plus +`src/test/brand.test.ts` in the normal test suite. Both fail on regressions of +the rules below. + +## Colors + +| Token | Value | Use for | +|---|---|---| +| `--ink` / `--ink-2` / `--ink-3` | `#0F172A` / `#1B2436` / `#2A344A` | **The main color.** Headings, body text, primary button fills, active nav states, focus borders, toggles-on; `--ink-2/3` for hover/elevation on ink fills | +| `--paper` / `--paper-2` / `--paper-3` | `#FFFFFF` / `#F7F7F4` / `#ECEBE5` | Page ground, off-white bands, cards | +| `--rule` | `#E2E1DA` | Hairline dividers, card borders (instead of shadows) | +| `--accent` | `#E76F51` | Vivid orange: decorative bands/graphics, accents and CTA fills on **dark** surfaces (pair with ink text) | +| `--accent-deep` | `#C2532F` | Orange **text only** on light: links, eyebrow numbers, stat values. AA on `--paper` (#FFFFFF) **only** — on `paper-2`/`paper-3`/`#fafafa` use `--accent-deep-hover` instead. Not for button fills — primary fills are ink | +| `--accent-tint` | `#F08A6F` | Small orange text on **dark** (7.3:1 on ink) | +| `--muted` / `--muted-2` | `#5A6175` / `#6B7285` | Secondary / tertiary text on light | +| `--accent-deep-hover` | `#A8431F` | Hover state for accent-deep fills/links (app-local, not a pack token) | +| `--teal` | `#0B4F6C` | **Reserved — do not use** | + +Ink is the main color; orange is strictly an accent and scarce by design: one +accent word per heading, one band per composition, small highlights only. +Never vivid `--accent` for normal-size text on white, and never orange as the +dominant fill color of a page. + +Functional colors are **not** brand colors: segmentation palettes, organ mesh +colors, CT windowing grays, success/error states stay as they are. + +## Typography + +IBM Plex Sans 400/500/600 for everything; IBM Plex Mono 400/500 for labels, +eyebrows, and numeric metadata. Self-hosted from `PanTS-Demo/public/fonts/` +via `@font-face` in `brand.css` — never load fonts from an external host. +Use `var(--font-sans)` / `var(--font-mono)`. Weights above 600 are not shipped +(`font-synthesis: none` means they silently render as the nearest real weight, +so don't declare them). The viewer's `--vp-font` / `--vp-mono` alias these +tokens. + +## Logo + +`PanTS-Demo/public/bodymaps-logo.svg` is the adopted mark (two ink bars + +orange agreement band) and the only sanctioned mark. Never rotate, stretch, +recolor, round, clip, or add effects to it; minimum size 16px; keep roughly one +bar-height of clear space. Favicons (`favicon.svg`, PNG fallbacks, +`apple-touch-icon.png`) and the OG share image are the brand-pack exports — +replace them only with new pack exports. + +## Naming + +"BodyMaps" is the product/company; "PanTS" and "CancerVerse" name datasets and +case IDs only. Title format: `BodyMaps — ` (em dash). + +## Deferred surfaces (do not copy their style) + +These files still carry pre-brand colors/fonts and are explicitly allowlisted +in `scripts/brand-guard.mjs` because they are under active development in +open PRs — they get migrated in a follow-up, not piecemeal: +`UploadPage.css/.tsx`, `VisualizationPage.css/.tsx`, `AnnotationToolbar.*`, +`FlyoutPrimitives.css`, the `segmentation/` components, `ToolWalkthrough.css`, +`MeasurementPanel.css`, `AISidebar.css`, `Preview.tsx`, and (pre-registered +for an in-flight branch) `SharePatientCard.tsx`. Their "Space Grotesk" / +"JetBrains Mono" declarations render as IBM Plex via an alias `@font-face` +shim in `brand.css` until migrated. Don't add files to an allowlist to +silence the guard — use the tokens. + +The guard's scan scope is `PanTS-Demo/` only, by decision: repo-root apps +(`analytics/`) and `flask-server/` are out of scope for now. The Flask PDF +report (`flask-server/api/utils.py` header drawing plus its template) is a +known deferred brand surface *outside* the guard's reach — migrate it when +that file's in-flight PRs land. diff --git a/PanTS-Demo/index.html b/PanTS-Demo/index.html index 0e60586f..709e4f2a 100644 --- a/PanTS-Demo/index.html +++ b/PanTS-Demo/index.html @@ -2,13 +2,21 @@ - + + + + + + + + + + + - - - - + + BodyMaps — CT Segmentation Platform diff --git a/PanTS-Demo/public/apple-touch-icon.png b/PanTS-Demo/public/apple-touch-icon.png new file mode 100644 index 00000000..a6a0e959 Binary files /dev/null and b/PanTS-Demo/public/apple-touch-icon.png differ diff --git a/PanTS-Demo/public/bodymaps-logo.svg b/PanTS-Demo/public/bodymaps-logo.svg index d380d0ba..186e78f9 100644 --- a/PanTS-Demo/public/bodymaps-logo.svg +++ b/PanTS-Demo/public/bodymaps-logo.svg @@ -1,13 +1,5 @@ - - - - - - - - - - - - + + + + diff --git a/PanTS-Demo/public/favicon-16.png b/PanTS-Demo/public/favicon-16.png new file mode 100644 index 00000000..487e6975 Binary files /dev/null and b/PanTS-Demo/public/favicon-16.png differ diff --git a/PanTS-Demo/public/favicon-32.png b/PanTS-Demo/public/favicon-32.png new file mode 100644 index 00000000..8f49c1ff Binary files /dev/null and b/PanTS-Demo/public/favicon-32.png differ diff --git a/PanTS-Demo/public/favicon.svg b/PanTS-Demo/public/favicon.svg new file mode 100644 index 00000000..c95d361b --- /dev/null +++ b/PanTS-Demo/public/favicon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/PanTS-Demo/public/fonts/ibm-plex-mono-v20-latin-ext-normal-400.woff2 b/PanTS-Demo/public/fonts/ibm-plex-mono-v20-latin-ext-normal-400.woff2 new file mode 100644 index 00000000..e4f67deb Binary files /dev/null and b/PanTS-Demo/public/fonts/ibm-plex-mono-v20-latin-ext-normal-400.woff2 differ diff --git a/PanTS-Demo/public/fonts/ibm-plex-mono-v20-latin-ext-normal-500.woff2 b/PanTS-Demo/public/fonts/ibm-plex-mono-v20-latin-ext-normal-500.woff2 new file mode 100644 index 00000000..743dae39 Binary files /dev/null and b/PanTS-Demo/public/fonts/ibm-plex-mono-v20-latin-ext-normal-500.woff2 differ diff --git a/PanTS-Demo/public/fonts/ibm-plex-mono-v20-latin-normal-400.woff2 b/PanTS-Demo/public/fonts/ibm-plex-mono-v20-latin-normal-400.woff2 new file mode 100644 index 00000000..52b6c759 Binary files /dev/null and b/PanTS-Demo/public/fonts/ibm-plex-mono-v20-latin-normal-400.woff2 differ diff --git a/PanTS-Demo/public/fonts/ibm-plex-mono-v20-latin-normal-500.woff2 b/PanTS-Demo/public/fonts/ibm-plex-mono-v20-latin-normal-500.woff2 new file mode 100644 index 00000000..3308bce2 Binary files /dev/null and b/PanTS-Demo/public/fonts/ibm-plex-mono-v20-latin-normal-500.woff2 differ diff --git a/PanTS-Demo/public/fonts/ibm-plex-sans-v23-latin-ext-normal-400.woff2 b/PanTS-Demo/public/fonts/ibm-plex-sans-v23-latin-ext-normal-400.woff2 new file mode 100644 index 00000000..ee8d682c Binary files /dev/null and b/PanTS-Demo/public/fonts/ibm-plex-sans-v23-latin-ext-normal-400.woff2 differ diff --git a/PanTS-Demo/public/fonts/ibm-plex-sans-v23-latin-ext-normal-500.woff2 b/PanTS-Demo/public/fonts/ibm-plex-sans-v23-latin-ext-normal-500.woff2 new file mode 100644 index 00000000..7dcfc144 Binary files /dev/null and b/PanTS-Demo/public/fonts/ibm-plex-sans-v23-latin-ext-normal-500.woff2 differ diff --git a/PanTS-Demo/public/fonts/ibm-plex-sans-v23-latin-ext-normal-600.woff2 b/PanTS-Demo/public/fonts/ibm-plex-sans-v23-latin-ext-normal-600.woff2 new file mode 100644 index 00000000..f427db43 Binary files /dev/null and b/PanTS-Demo/public/fonts/ibm-plex-sans-v23-latin-ext-normal-600.woff2 differ diff --git a/PanTS-Demo/public/fonts/ibm-plex-sans-v23-latin-normal-400.woff2 b/PanTS-Demo/public/fonts/ibm-plex-sans-v23-latin-normal-400.woff2 new file mode 100644 index 00000000..e5ebabbd Binary files /dev/null and b/PanTS-Demo/public/fonts/ibm-plex-sans-v23-latin-normal-400.woff2 differ diff --git a/PanTS-Demo/public/fonts/ibm-plex-sans-v23-latin-normal-500.woff2 b/PanTS-Demo/public/fonts/ibm-plex-sans-v23-latin-normal-500.woff2 new file mode 100644 index 00000000..07ac9a9f Binary files /dev/null and b/PanTS-Demo/public/fonts/ibm-plex-sans-v23-latin-normal-500.woff2 differ diff --git a/PanTS-Demo/public/fonts/ibm-plex-sans-v23-latin-normal-600.woff2 b/PanTS-Demo/public/fonts/ibm-plex-sans-v23-latin-normal-600.woff2 new file mode 100644 index 00000000..36cd7428 Binary files /dev/null and b/PanTS-Demo/public/fonts/ibm-plex-sans-v23-latin-normal-600.woff2 differ diff --git a/PanTS-Demo/public/jhu_logo.jpg b/PanTS-Demo/public/jhu_logo.jpg deleted file mode 100644 index f3d64457..00000000 Binary files a/PanTS-Demo/public/jhu_logo.jpg and /dev/null differ diff --git a/PanTS-Demo/public/og-image-1200x630.png b/PanTS-Demo/public/og-image-1200x630.png new file mode 100644 index 00000000..717ffccf Binary files /dev/null and b/PanTS-Demo/public/og-image-1200x630.png differ diff --git a/PanTS-Demo/public/vite.svg b/PanTS-Demo/public/vite.svg deleted file mode 100644 index e7b8dfb1..00000000 --- a/PanTS-Demo/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/PanTS-Demo/src/assets/fonts/Inter-Italic-VariableFont_opsz,wght.ttf b/PanTS-Demo/src/assets/fonts/Inter-Italic-VariableFont_opsz,wght.ttf deleted file mode 100644 index 43ed4f5e..00000000 Binary files a/PanTS-Demo/src/assets/fonts/Inter-Italic-VariableFont_opsz,wght.ttf and /dev/null differ diff --git a/PanTS-Demo/src/assets/fonts/Inter-VariableFont_opsz_wght.ttf b/PanTS-Demo/src/assets/fonts/Inter-VariableFont_opsz_wght.ttf deleted file mode 100644 index e31b51e3..00000000 Binary files a/PanTS-Demo/src/assets/fonts/Inter-VariableFont_opsz_wght.ttf and /dev/null differ diff --git a/PanTS-Demo/src/assets/react.svg b/PanTS-Demo/src/assets/react.svg deleted file mode 100644 index 6c87de9b..00000000 --- a/PanTS-Demo/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/PanTS-Demo/src/components/AuthButton.module.css b/PanTS-Demo/src/components/AuthButton.module.css index 0b402bac..ff51e189 100644 --- a/PanTS-Demo/src/components/AuthButton.module.css +++ b/PanTS-Demo/src/components/AuthButton.module.css @@ -1,31 +1,31 @@ -/* Account control for both nav bars. Monochrome to match the BodyMaps header; - the JHU Heritage Blue (#002D72) is reused for the primary "Sign in" action to - match the landing page's active tab / primary button. */ +/* Account control for both nav bars. Ink primary per BRANDING.md; + ink fill on the primary "Sign in" action to match the landing + page's active tab / primary button. */ .signInBtn { padding: 8px 20px; border-radius: 22px; - font-family: 'Space Grotesk', sans-serif; + font-family: "IBM Plex Sans", system-ui, sans-serif; font-size: 12px; font-weight: 600; letter-spacing: 0.06em; cursor: pointer; - background: #002d72; + background: var(--ink); color: #ffffff; - border: 1px solid #002d72; + border: 1px solid var(--ink); transition: background 0.2s ease, transform 0.2s ease; white-space: nowrap; } .signInBtn:hover { - background: #00399a; - border-color: #00399a; + background: var(--ink-2); + border-color: var(--ink-2); transform: translateY(-1px); } /* ── Signed-in trigger ── */ .accountRoot { position: relative; - font-family: 'Space Grotesk', sans-serif; + font-family: "IBM Plex Sans", system-ui, sans-serif; } .accountTrigger { @@ -50,13 +50,13 @@ height: 26px; flex-shrink: 0; border-radius: 50%; - background: #002d72; + background: var(--ink); color: #ffffff; display: flex; align-items: center; justify-content: center; font-size: 12px; - font-weight: 700; + font-weight: 600; } .accountEmail { @@ -98,7 +98,7 @@ color: #111111; } .menuEmail { - font-family: 'JetBrains Mono', monospace; + font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 11px; color: #8f8f8f; margin-top: 2px; @@ -115,7 +115,7 @@ border: none; background: transparent; border-radius: 8px; - font-family: 'Space Grotesk', sans-serif; + font-family: "IBM Plex Sans", system-ui, sans-serif; font-size: 13px; color: #111111; cursor: pointer; diff --git a/PanTS-Demo/src/components/AuthModal.css b/PanTS-Demo/src/components/AuthModal.css index b0e0bdb8..d634fd15 100644 --- a/PanTS-Demo/src/components/AuthModal.css +++ b/PanTS-Demo/src/components/AuthModal.css @@ -1,6 +1,5 @@ -/* Global auth popup. Light / monochrome to match the app, World Labs layout. */ +/* Global auth popup. Light surface, ink primary per BRANDING.md. */ -@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap'); .authm-backdrop { position: fixed; @@ -12,7 +11,7 @@ justify-content: center; padding: 5vh 5vw; animation: authm-fade 0.15s ease; - font-family: 'Space Grotesk', sans-serif; + font-family: "IBM Plex Sans", system-ui, sans-serif; } @keyframes authm-fade { @@ -59,13 +58,12 @@ .authm-logo { width: 40px; height: 40px; - border-radius: 11px; margin-bottom: 12px; } .authm-title { font-size: 22px; - font-weight: 700; + font-weight: 600; color: #111111; margin: 0 0 22px; } @@ -83,7 +81,7 @@ background: #ffffff; border: 1px solid rgba(0, 0, 0, 0.14); color: #111111; - font-family: 'Space Grotesk', sans-serif; + font-family: "IBM Plex Sans", system-ui, sans-serif; font-size: 14px; font-weight: 500; cursor: pointer; @@ -104,7 +102,7 @@ width: 100%; margin: 14px 0; color: #b0b0b0; - font-family: 'JetBrains Mono', monospace; + font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 11px; } .authm-divider::before, @@ -122,17 +120,17 @@ width: 100%; padding: 12px 16px; border-radius: 10px; - background: #002d72; + background: var(--ink); color: #ffffff; - border: 1px solid #002d72; - font-family: 'Space Grotesk', sans-serif; + border: 1px solid var(--ink); + font-family: "IBM Plex Sans", system-ui, sans-serif; font-size: 14px; font-weight: 600; cursor: pointer; transition: background 0.15s; } .authm-email-btn:hover { - background: #00399a; + background: var(--ink-2); } /* Email form */ @@ -157,35 +155,35 @@ border-radius: 10px; border: 1px solid rgba(0, 0, 0, 0.12); background: #fafafa; - font-family: 'Space Grotesk', sans-serif; + font-family: "IBM Plex Sans", system-ui, sans-serif; font-size: 14px; color: #111111; transition: border-color 0.15s, background 0.15s; } .authm-input:focus { outline: none; - border-color: #002d72; + border-color: var(--ink); background: #ffffff; } .authm-error { - font-family: 'JetBrains Mono', monospace; + font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 12px; color: #ef4444; } .authm-submit { padding: 12px 16px; border-radius: 10px; - background: #002d72; + background: var(--ink); color: #ffffff; - border: 1px solid #002d72; - font-family: 'Space Grotesk', sans-serif; + border: 1px solid var(--ink); + font-family: "IBM Plex Sans", system-ui, sans-serif; font-size: 14px; font-weight: 600; cursor: pointer; transition: background 0.2s; } .authm-submit:hover:not(:disabled) { - background: #00399a; + background: var(--ink-2); } .authm-submit:disabled { opacity: 0.6; @@ -195,7 +193,7 @@ background: none; border: none; padding: 2px; - font-family: 'Space Grotesk', sans-serif; + font-family: "IBM Plex Sans", system-ui, sans-serif; font-size: 12px; color: #6a6a6a; cursor: pointer; @@ -219,7 +217,7 @@ text-decoration: underline; } .authm-fineprint a:hover { - color: #002d72; + color: var(--accent-deep); } .authm-toggle { @@ -237,7 +235,7 @@ padding: 0; font: inherit; font-weight: 600; - color: #002d72; + color: var(--accent-deep); cursor: pointer; text-decoration: underline; } diff --git a/PanTS-Demo/src/components/BatchDetailsModal.css b/PanTS-Demo/src/components/BatchDetailsModal.css index 567e4d1c..ad6d4b04 100644 --- a/PanTS-Demo/src/components/BatchDetailsModal.css +++ b/PanTS-Demo/src/components/BatchDetailsModal.css @@ -1,4 +1,4 @@ -/* Batch details popup. Monochrome + JHU Heritage Blue, matching the app. */ +/* Batch details popup. Ink/paper neutrals + BodyMaps accent, matching the app. */ .bdm-backdrop { position: fixed; @@ -27,7 +27,7 @@ border-radius: 16px; box-shadow: 0 24px 60px rgba(0, 0, 0, 0.28); overflow: hidden; - font-family: 'Space Grotesk', sans-serif; + font-family: "IBM Plex Sans", system-ui, sans-serif; } .bdm-header { @@ -40,11 +40,11 @@ } .bdm-title { font-size: 18px; - font-weight: 700; + font-weight: 600; color: #111111; } .bdm-subtitle { - font-family: 'JetBrains Mono', monospace; + font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 12px; color: #8f8f8f; margin-top: 2px; @@ -57,17 +57,17 @@ .bdm-download-all { padding: 8px 16px; border-radius: 8px; - background: #002d72; + background: var(--ink); color: #ffffff; - border: 1px solid #002d72; - font-family: 'Space Grotesk', sans-serif; + border: 1px solid var(--ink); + font-family: "IBM Plex Sans", system-ui, sans-serif; font-size: 12px; font-weight: 600; cursor: pointer; transition: background 0.15s; } .bdm-download-all:hover { - background: #00399a; + background: var(--ink-2); } .bdm-close { display: flex; @@ -81,7 +81,7 @@ color: #6a6a6a; cursor: pointer; transition: background 0.15s, color 0.15s; - font-family: 'Space Grotesk', sans-serif; + font-family: "IBM Plex Sans", system-ui, sans-serif; font-size: 22px; line-height: 1; padding: 0 0 2px; @@ -121,7 +121,7 @@ white-space: nowrap; } .bdm-row-sub { - font-family: 'JetBrains Mono', monospace; + font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 11px; color: #6a6a6a; margin-top: 2px; @@ -142,7 +142,7 @@ background: transparent; border: 1px solid rgba(0, 0, 0, 0.12); color: #111111; - font-family: 'Space Grotesk', sans-serif; + font-family: "IBM Plex Sans", system-ui, sans-serif; font-size: 11px; cursor: pointer; transition: background 0.15s, border-color 0.15s; diff --git a/PanTS-Demo/src/components/Header/Header.module.css b/PanTS-Demo/src/components/Header/Header.module.css index ee5327fb..0bbed815 100644 --- a/PanTS-Demo/src/components/Header/Header.module.css +++ b/PanTS-Demo/src/components/Header/Header.module.css @@ -5,7 +5,7 @@ position: sticky; top: 0; z-index: var(--z-sidebar); - font-family: 'Space Grotesk', sans-serif; + font-family: "IBM Plex Sans", system-ui, sans-serif; } /* ── Nav bar ── */ @@ -30,12 +30,11 @@ width: 38px; height: 38px; display: block; - border-radius: 11px; } .logoTitle { font-size: 18px; - font-weight: 700; - color: #111111; + font-weight: 600; + color: var(--ink); letter-spacing: -0.02em; line-height: 1.1; } @@ -58,7 +57,7 @@ border: 1px solid transparent; color: #6a6a6a; transition: color 0.2s ease, background 0.2s ease; - font-family: 'Space Grotesk', sans-serif; + font-family: "IBM Plex Sans", system-ui, sans-serif; background: transparent; letter-spacing: 0.08em; text-decoration: none; @@ -68,9 +67,9 @@ color: #111111; } .tabPillActive { - background: #002D72; + background: var(--ink); color: #ffffff; - border-color: #002D72; + border-color: var(--ink); cursor: default; } .tabPillActive:hover { @@ -172,10 +171,10 @@ } .drawerTitle { font-size: 16px; - font-weight: 700; + font-weight: 600; color: #111111; letter-spacing: -0.01em; - font-family: 'Space Grotesk', sans-serif; + font-family: "IBM Plex Sans", system-ui, sans-serif; } .drawerClose { display: flex; @@ -215,7 +214,7 @@ border: none; text-align: left; cursor: pointer; - font-family: 'Space Grotesk', sans-serif; + font-family: "IBM Plex Sans", system-ui, sans-serif; letter-spacing: 0.03em; transition: color 0.15s, background 0.15s; min-height: 44px; @@ -226,11 +225,11 @@ background: rgba(0, 0, 0, 0.04); } .mobileTabActive { - color: #002D72; - background: rgba(0, 45, 114, 0.07); + color: var(--ink); + background: rgba(15, 23, 42, 0.06); } .mobileTabActive:hover { - color: #002D72; + color: var(--ink); } .drawerFooter { diff --git a/PanTS-Demo/src/components/ProcessingSummaryBar.tsx b/PanTS-Demo/src/components/ProcessingSummaryBar.tsx index 2c3ec05b..b8131b0d 100644 --- a/PanTS-Demo/src/components/ProcessingSummaryBar.tsx +++ b/PanTS-Demo/src/components/ProcessingSummaryBar.tsx @@ -35,7 +35,7 @@ const ProcessingSummaryBar: React.FC = ({ running, done, statusLabel, tit cy={SIZE / 2} r={r} fill="none" - stroke="rgba(0,45,114,0.12)" + stroke="rgba(15,23,42,0.12)" strokeWidth={STROKE} /> = ({ running, done, statusLabel, tit cy={SIZE / 2} r={r} fill="none" - stroke="#002D72" + stroke="#C2532F" strokeWidth={STROKE} strokeLinecap="round" strokeDasharray={circ} diff --git a/PanTS-Demo/src/components/ScrollToTopButton/ScrollToTopButton.module.css b/PanTS-Demo/src/components/ScrollToTopButton/ScrollToTopButton.module.css index adeb7a41..5139a0c2 100644 --- a/PanTS-Demo/src/components/ScrollToTopButton/ScrollToTopButton.module.css +++ b/PanTS-Demo/src/components/ScrollToTopButton/ScrollToTopButton.module.css @@ -14,7 +14,7 @@ border: none; cursor: pointer; - background: #002D72; + background: var(--ink); color: #ffffff; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18); @@ -31,7 +31,7 @@ } .btn:hover { - background: #00399a; + background: var(--ink-2); } .btn:active { diff --git a/PanTS-Demo/src/components/SiteFooter/SiteFooter.module.css b/PanTS-Demo/src/components/SiteFooter/SiteFooter.module.css new file mode 100644 index 00000000..b63d5591 --- /dev/null +++ b/PanTS-Demo/src/components/SiteFooter/SiteFooter.module.css @@ -0,0 +1,47 @@ +.footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px 24px; + flex-wrap: wrap; + width: 100%; + box-sizing: border-box; + padding: 10px 32px; + border-top: 1px solid var(--rule); + background: var(--paper); + font-family: "IBM Plex Sans", system-ui, sans-serif; + font-size: 12px; + line-height: 1.4; + color: var(--muted); + flex: none; + position: relative; + z-index: 1; +} + +.tagline { + letter-spacing: 0.01em; +} + +.partner { + font-family: "IBM Plex Mono", ui-monospace, monospace; + font-size: 11px; + letter-spacing: 0.04em; +} + +.link { + color: var(--accent-deep); + font-weight: 500; + text-decoration: underline; /* non-color affordance at rest (WCAG 1.4.1) */ + text-underline-offset: 2px; +} +.link:hover { + color: var(--accent-deep-hover); +} + +@media (max-width: 640px) { + .footer { + justify-content: center; + text-align: center; + padding: 8px 20px; + } +} diff --git a/PanTS-Demo/src/components/SiteFooter/index.tsx b/PanTS-Demo/src/components/SiteFooter/index.tsx new file mode 100644 index 00000000..3885c770 --- /dev/null +++ b/PanTS-Demo/src/components/SiteFooter/index.tsx @@ -0,0 +1,25 @@ +import styles from "./SiteFooter.module.css"; + +/** Slim site-wide footer: company line + commercial-partnership pointer to the main site. */ +function SiteFooter() { + return ( +
+
+ BodyMaps — the intelligence layer for medical imaging AI. + + + For commercial use, please visit{" "} + + thebodymaps.com + + + + ); +} + +export default SiteFooter; diff --git a/PanTS-Demo/src/components/UpgradeDialog.css b/PanTS-Demo/src/components/UpgradeDialog.css index 4bc8df06..60c34ed9 100644 --- a/PanTS-Demo/src/components/UpgradeDialog.css +++ b/PanTS-Demo/src/components/UpgradeDialog.css @@ -2,7 +2,6 @@ (AuthModal.css) so a plan wall doesn't look like it came from a different product than the sign-in it sits next to. */ -@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap'); .upg-backdrop { position: fixed; @@ -24,7 +23,7 @@ border-radius: 18px; box-shadow: 0 24px 60px rgba(0, 0, 0, 0.2); padding: 30px 28px 24px; - font-family: 'Space Grotesk', sans-serif; + font-family: "IBM Plex Sans", system-ui, sans-serif; color: #111111; } @@ -46,7 +45,7 @@ .upg-title { font-size: 19px; - font-weight: 700; + font-weight: 600; margin: 0 0 8px; padding-right: 20px; line-height: 1.3; @@ -68,7 +67,7 @@ } .upg-plan-name { font-size: 14px; - font-weight: 700; + font-weight: 600; margin-bottom: 10px; } .upg-plan-points { @@ -94,7 +93,7 @@ width: 5px; height: 5px; border-radius: 50%; - background: #002d72; + background: var(--ink); } .upg-actions { @@ -107,8 +106,8 @@ width: 100%; padding: 11px; border-radius: 10px; - background: #002d72; - border: 1px solid #002d72; + background: var(--ink); + border: 1px solid var(--ink); color: #ffffff; font-family: inherit; font-size: 14px; @@ -117,7 +116,7 @@ transition: background 0.15s; } .upg-primary:hover { - background: #00399a; + background: var(--ink-2); } .upg-secondary { width: 100%; diff --git a/PanTS-Demo/src/index.css b/PanTS-Demo/src/index.css index 1d534430..fec857a7 100644 --- a/PanTS-Demo/src/index.css +++ b/PanTS-Demo/src/index.css @@ -1,11 +1,12 @@ @import "tailwindcss"; +@import "./styles/brand.css"; #root { width: 100vw; } :root { - font-family: "Inter"; + font-family: "IBM Plex Sans", system-ui, sans-serif; line-height: 1.5; font-weight: 400; @@ -16,18 +17,9 @@ /* ── Breakpoints: sm: 640px | md: 768px | lg: 1024px | xl: 1280px ── */ - /* ── Global palette ── */ - --bg: #0e1a2b; - --panel: #12243b; - --panel-soft: #132a45; - --ink: #e8eff9; - --sub: #a9b7cc; - --line: #243a57; - --brand: #111111; - --brand-ink: #fff; - --accent: #111111; - --good: #16a34a; - --bad: #ef4444; + /* Color/font tokens live in styles/brand.css (BodyMaps brand pack). */ + + /* ── Layout ── */ --control-h: 44px; --header-h: 70px; @@ -48,19 +40,19 @@ --vp-text-dim: rgba(255, 255, 255, 0.5); --vp-text-faint: rgba(255, 255, 255, 0.36); --vp-hover: rgba(255, 255, 255, 0.1); - --vp-accent: #6ea8fe; - --vp-font: "Space Grotesk", system-ui, sans-serif; - --vp-mono: "JetBrains Mono", ui-monospace, monospace; + --vp-accent: var(--accent); + --vp-font: var(--font-sans); + --vp-mono: var(--font-mono); } a { font-weight: 500; - color: #111111; + color: var(--accent-deep); text-decoration: inherit; } a:hover { - color: #000000; + color: var(--accent-deep-hover); } body { diff --git a/PanTS-Demo/src/routes/ComparePage.css b/PanTS-Demo/src/routes/ComparePage.css index a32aa95d..eeaec596 100644 --- a/PanTS-Demo/src/routes/ComparePage.css +++ b/PanTS-Demo/src/routes/ComparePage.css @@ -2,7 +2,7 @@ min-height: 100vh; background: #08090b; color: #f4f5f7; - font-family: "Space Grotesk", system-ui, sans-serif; + font-family: "IBM Plex Sans", system-ui, sans-serif; padding: 20px clamp(16px, 5vw, 64px) 64px; box-sizing: border-box; } @@ -62,14 +62,14 @@ .cmp__viewerlink { padding: 8px 12px; border-radius: 8px; - background: #2563eb; - color: #fff; + background: var(--accent); + color: var(--ink); text-decoration: none; font-size: 12px; font-weight: 600; white-space: nowrap; } -.cmp__viewerlink:hover { background: #1d4ed8; } +.cmp__viewerlink:hover { background: var(--accent-tint); } .cmp__cases { display: grid; @@ -106,19 +106,19 @@ justify-content: center; font-size: 10px; color: rgba(255, 255, 255, 0.35); - font-family: "Space Mono", monospace; + font-family: "IBM Plex Mono", ui-monospace, monospace; } .cmp-case__meta { display: flex; flex-direction: column; gap: 6px; } .cmp-case__id { font-size: 15px; font-weight: 600; } .cmp-case__demo { font-size: 12px; color: rgba(255, 255, 255, 0.62); - font-family: "Space Mono", monospace; + font-family: "IBM Plex Mono", ui-monospace, monospace; } .cmp-case__open { margin-top: auto; font-size: 12px; - color: #7aa2ff; + color: var(--accent-tint); text-decoration: none; } .cmp-case__open:hover { text-decoration: underline; } @@ -148,7 +148,7 @@ .cmp__rowgroup:last-child { border-bottom: none; } .cmp__row--head { background: rgba(255, 255, 255, 0.05); - font-family: "Space Mono", monospace; + font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; @@ -163,7 +163,7 @@ font-size: 16px; } .cmp__cell { - font-family: "Space Mono", monospace; + font-family: "IBM Plex Mono", ui-monospace, monospace; color: rgba(255, 255, 255, 0.9); display: flex; flex-direction: column; @@ -222,13 +222,13 @@ display: grid; grid-template-columns: 1.3fr 1fr 1fr 1fr; gap: 18px; - font-family: "Space Mono", monospace; + font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 13px; } .cmp__detail-row span:not(:first-child) { text-align: right; color: rgba(255, 255, 255, 0.85); } .cmp__detail-row span:first-child { color: rgba(255, 255, 255, 0.55); - font-family: "Space Grotesk", system-ui, sans-serif; + font-family: "IBM Plex Sans", system-ui, sans-serif; } .cmp__detail-row--head span:not(:first-child) { color: rgba(255, 255, 255, 0.42); diff --git a/PanTS-Demo/src/routes/CompareViewerPage.css b/PanTS-Demo/src/routes/CompareViewerPage.css index 3ed97a38..b8b549ba 100644 --- a/PanTS-Demo/src/routes/CompareViewerPage.css +++ b/PanTS-Demo/src/routes/CompareViewerPage.css @@ -4,7 +4,7 @@ flex-direction: column; background: #08090b; color: #f4f5f7; - font-family: "Space Grotesk", system-ui, sans-serif; + font-family: "IBM Plex Sans", system-ui, sans-serif; overflow: hidden; /* Re-declare the single viewer's design tokens so its vp-* classes (reused in the @@ -18,9 +18,9 @@ --vp-text-dim: rgba(255, 255, 255, 0.5); --vp-text-faint: rgba(255, 255, 255, 0.36); --vp-hover: rgba(255, 255, 255, 0.1); - --vp-accent: #6ea8fe; - --vp-font: "Space Grotesk", system-ui, sans-serif; - --vp-mono: "JetBrains Mono", ui-monospace, monospace; + --vp-accent: var(--accent); + --vp-font: "IBM Plex Sans", system-ui, sans-serif; + --vp-mono: "IBM Plex Mono", ui-monospace, monospace; } .cmv__bar { @@ -47,7 +47,7 @@ .cmv__home:hover { background: rgba(255, 255, 255, 0.08); } .cmv__title { font-size: 16px; font-weight: 600; margin: 0; } .cmv__ids { - font-family: "Space Mono", monospace; + font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 13px; color: rgba(255, 255, 255, 0.5); font-weight: 400; @@ -70,8 +70,8 @@ cursor: pointer; } .cmv__gear:hover { background: rgba(255, 255, 255, 0.1); } -/* Active = subtle blue tint (not a white fill) so the light icon stays visible. */ -.cmv__gear.is-active { background: rgba(37, 99, 235, 0.3); border-color: #2563eb; } +/* Active = subtle accent tint (not a white fill) so the light icon stays visible. */ +.cmv__gear.is-active { background: rgba(231, 111, 81, 0.3); border-color: var(--accent); } /* Body = settings sidebar + the viewport grid. */ .cmv__body { flex: 1; display: flex; min-height: 0; } @@ -95,11 +95,11 @@ cursor: pointer; user-select: none; } -.cmv__toggle input { accent-color: #2563eb; width: 15px; height: 15px; } +.cmv__toggle input { accent-color: var(--accent); width: 15px; height: 15px; } .cmv__msg { margin: auto; color: rgba(255, 255, 255, 0.55); font-size: 14px; } .cmv__msg code { - font-family: "Space Mono", monospace; + font-family: "IBM Plex Mono", ui-monospace, monospace; background: rgba(255, 255, 255, 0.08); padding: 2px 6px; border-radius: 5px; @@ -132,11 +132,11 @@ top: 8px; left: 10px; z-index: 2; - font-family: "Space Mono", monospace; + font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 12px; font-weight: 600; - color: #fff; - background: rgba(37, 99, 235, 0.8); + color: var(--ink); + background: rgba(231, 111, 81, 0.9); padding: 3px 8px; border-radius: 6px; pointer-events: none; @@ -146,7 +146,7 @@ bottom: 8px; left: 10px; z-index: 2; - font-family: "Space Mono", monospace; + font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; diff --git a/PanTS-Demo/src/routes/Homepage/components/CaseGrid/CaseGrid.module.css b/PanTS-Demo/src/routes/Homepage/components/CaseGrid/CaseGrid.module.css index 079ffc8f..932cbcec 100644 --- a/PanTS-Demo/src/routes/Homepage/components/CaseGrid/CaseGrid.module.css +++ b/PanTS-Demo/src/routes/Homepage/components/CaseGrid/CaseGrid.module.css @@ -1,7 +1,7 @@ .emptyState { padding: 48px 0; text-align: center; - font-family: 'JetBrains Mono', monospace; + font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 13px; color: rgba(0, 0, 0, 0.5); } diff --git a/PanTS-Demo/src/routes/Homepage/components/CompareTray/CompareTray.module.css b/PanTS-Demo/src/routes/Homepage/components/CompareTray/CompareTray.module.css index 26b9e9c5..4cc7500d 100644 --- a/PanTS-Demo/src/routes/Homepage/components/CompareTray/CompareTray.module.css +++ b/PanTS-Demo/src/routes/Homepage/components/CompareTray/CompareTray.module.css @@ -21,7 +21,7 @@ } .compareTrayIds { - font-family: 'JetBrains Mono', monospace; + font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 12px; } @@ -38,7 +38,7 @@ border: 1px solid rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.06); color: #ffffff; - font-family: 'JetBrains Mono', monospace; + font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 12px; outline: none; } @@ -68,7 +68,7 @@ } .compareTrayCompareBtn { - background: #2563eb; + background: var(--ink); border: none; color: #ffffff; border-radius: 8px; @@ -78,6 +78,6 @@ font-weight: 600; } .compareTrayCompareBtn:disabled { - background: rgba(37, 99, 235, 0.4); + background: rgba(15, 23, 42, 0.4); cursor: not-allowed; } diff --git a/PanTS-Demo/src/routes/Homepage/components/FilterPanel/FilterPanel.module.css b/PanTS-Demo/src/routes/Homepage/components/FilterPanel/FilterPanel.module.css index 6ef12e09..0d59c727 100644 --- a/PanTS-Demo/src/routes/Homepage/components/FilterPanel/FilterPanel.module.css +++ b/PanTS-Demo/src/routes/Homepage/components/FilterPanel/FilterPanel.module.css @@ -8,9 +8,9 @@ } .filterLabel { - font-family: 'Space Grotesk', sans-serif; + font-family: "IBM Plex Sans", system-ui, sans-serif; font-size: 12px; - font-weight: 700; + font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: rgba(0, 0, 0, 0.75); @@ -18,7 +18,7 @@ .facetsLoading { align-self: center; - font-family: 'JetBrains Mono', monospace; + font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 11px; color: rgba(0, 0, 0, 0.35); } @@ -26,7 +26,7 @@ .pill { padding: 7px 16px; border-radius: 8px; - font-family: 'Space Grotesk', sans-serif; + font-family: "IBM Plex Sans", system-ui, sans-serif; font-size: 13px; font-weight: 600; cursor: pointer; @@ -37,14 +37,14 @@ outline: none; } .pillActive { - border-color: #002D72; - background: #002D72; + border-color: var(--ink); + background: var(--ink); color: #ffffff; } .countBadge { margin-left: 7px; - font-family: 'JetBrains Mono', monospace; + font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 10px; opacity: 0.6; } diff --git a/PanTS-Demo/src/routes/Homepage/components/LibraryHeader/LibraryHeader.module.css b/PanTS-Demo/src/routes/Homepage/components/LibraryHeader/LibraryHeader.module.css index c27e36b0..5eca63ce 100644 --- a/PanTS-Demo/src/routes/Homepage/components/LibraryHeader/LibraryHeader.module.css +++ b/PanTS-Demo/src/routes/Homepage/components/LibraryHeader/LibraryHeader.module.css @@ -1,5 +1,5 @@ .sectionHeader { - font-family: 'Space Grotesk', sans-serif; + font-family: "IBM Plex Sans", system-ui, sans-serif; font-size: 11px; font-weight: 600; letter-spacing: 0.12em; @@ -21,7 +21,7 @@ cursor: pointer; text-transform: none; letter-spacing: 0.04em; - font-family: 'JetBrains Mono', monospace; + font-family: "IBM Plex Mono", ui-monospace, monospace; display: flex; align-items: center; gap: 6px; @@ -39,7 +39,7 @@ } .matchTotal { - font-family: 'JetBrains Mono', monospace; + font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 11px; font-weight: 400; letter-spacing: 0.04em; diff --git a/PanTS-Demo/src/routes/Homepage/components/Pagination/Pagination.module.css b/PanTS-Demo/src/routes/Homepage/components/Pagination/Pagination.module.css index 3f654d64..f6166808 100644 --- a/PanTS-Demo/src/routes/Homepage/components/Pagination/Pagination.module.css +++ b/PanTS-Demo/src/routes/Homepage/components/Pagination/Pagination.module.css @@ -10,7 +10,7 @@ .pagerBtn { padding: 8px 16px; border-radius: 8px; - font-family: 'Space Grotesk', sans-serif; + font-family: "IBM Plex Sans", system-ui, sans-serif; font-size: 13px; font-weight: 600; border: 1px solid rgba(0, 0, 0, 0.12); @@ -26,7 +26,7 @@ } .pagerInfo { - font-family: 'JetBrains Mono', monospace; + font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 12px; color: rgba(0, 0, 0, 0.55); min-width: 120px; @@ -45,7 +45,7 @@ padding: 8px 10px; border-radius: 8px; border: 1px solid rgba(0, 0, 0, 0.12); - font-family: 'JetBrains Mono', monospace; + font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 12px; color: #111111; background: #ffffff; diff --git a/PanTS-Demo/src/routes/Homepage/components/ResultsSummary/ResultsSummary.module.css b/PanTS-Demo/src/routes/Homepage/components/ResultsSummary/ResultsSummary.module.css index fd03eec9..90c79554 100644 --- a/PanTS-Demo/src/routes/Homepage/components/ResultsSummary/ResultsSummary.module.css +++ b/PanTS-Demo/src/routes/Homepage/components/ResultsSummary/ResultsSummary.module.css @@ -7,7 +7,7 @@ } .resultsText { - font-family: 'JetBrains Mono', monospace; + font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 12px; color: rgba(0, 0, 0, 0.55); } @@ -16,7 +16,7 @@ display: flex; align-items: center; gap: 6px; - font-family: 'JetBrains Mono', monospace; + font-family: "IBM Plex Mono", ui-monospace, monospace; font-size: 12px; color: rgba(0, 0, 0, 0.45); background: transparent; diff --git a/PanTS-Demo/src/routes/Homepage/components/SearchBar/SearchBar.module.css b/PanTS-Demo/src/routes/Homepage/components/SearchBar/SearchBar.module.css index 35fe9ac1..a4285b21 100644 --- a/PanTS-Demo/src/routes/Homepage/components/SearchBar/SearchBar.module.css +++ b/PanTS-Demo/src/routes/Homepage/components/SearchBar/SearchBar.module.css @@ -5,7 +5,7 @@ border: 1px solid rgba(0, 0, 0, 0.08); border-radius: 8px; color: #111111; - font-family: 'Space Grotesk', sans-serif; + font-family: "IBM Plex Sans", system-ui, sans-serif; font-size: 13px; outline: none; min-width: 0; @@ -21,7 +21,7 @@ border: 1px solid rgba(0, 0, 0, 0.08); border-radius: 8px; color: #6a6a6a; - font-family: 'Space Grotesk', sans-serif; + font-family: "IBM Plex Sans", system-ui, sans-serif; font-size: 13px; outline: none; cursor: pointer; @@ -32,10 +32,10 @@ } .filterBadge { - background: #002D72; + background: var(--ink); color: #ffffff; font-size: 10px; - font-weight: 700; + font-weight: 600; border-radius: 999px; min-width: 16px; height: 16px; @@ -47,11 +47,11 @@ .searchBtn { padding: 10px 32px; - background: #002D72; + background: var(--ink); border: none; border-radius: 8px; color: #ffffff; - font-family: 'Space Grotesk', sans-serif; + font-family: "IBM Plex Sans", system-ui, sans-serif; font-size: 13px; font-weight: 600; cursor: pointer; diff --git a/PanTS-Demo/src/routes/Homepage/index.tsx b/PanTS-Demo/src/routes/Homepage/index.tsx index e41b5fab..ffb2e09c 100644 --- a/PanTS-Demo/src/routes/Homepage/index.tsx +++ b/PanTS-Demo/src/routes/Homepage/index.tsx @@ -7,6 +7,7 @@ import ResultsSummary from "./components/ResultsSummary"; import CaseGrid from "./components/CaseGrid"; import Pagination from "./components/Pagination"; import CompareTray from "./components/CompareTray"; +import SiteFooter from "../../components/SiteFooter"; import styles from "./Homepage.module.css"; import { PER_PAGE } from "./constants"; @@ -14,7 +15,7 @@ export default function Homepage() { const dash = useDashboard(); return ( -
+
+
); } diff --git a/PanTS-Demo/src/routes/UploadPage.css b/PanTS-Demo/src/routes/UploadPage.css index 241a3d09..02661bac 100644 --- a/PanTS-Demo/src/routes/UploadPage.css +++ b/PanTS-Demo/src/routes/UploadPage.css @@ -1,7 +1,8 @@ /* ===== UploadPage.css ===== - Redesigned to match the BodyMaps landing page aesthetic. - Color palette: #ffffff / #f5f5f5 / #111111 (monochrome) - Fonts: Space Grotesk, JetBrains Mono + DEFERRED pre-brand surface: still monochrome + Space Grotesk while + in-flight PRs on this file land. Do not copy this vocabulary — new + styling uses the BodyMaps tokens (ink/paper/accent, IBM Plex); see + BRANDING.md at the repo root. */ @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap'); @@ -15,6 +16,8 @@ font-family: 'Space Grotesk', sans-serif; position: relative; overflow-x: hidden; + display: flex; + flex-direction: column; } /* ── Ambient glow orbs ── */ @@ -46,6 +49,9 @@ /* ── Main content area ── */ .upload-main { + flex: 1; + width: 100%; + box-sizing: border-box; max-width: 1152px; margin: 0 auto; padding: 36px 24px 60px; diff --git a/PanTS-Demo/src/routes/UploadPage.tsx b/PanTS-Demo/src/routes/UploadPage.tsx index d53f1255..6dc8abdc 100644 --- a/PanTS-Demo/src/routes/UploadPage.tsx +++ b/PanTS-Demo/src/routes/UploadPage.tsx @@ -102,6 +102,7 @@ import { type PendingUpload, } from "../helpers/pendingUploads"; import { postWithRetry, resolveResumeStart } from "../helpers/chunkUpload"; +import SiteFooter from "../components/SiteFooter"; const parseApiResponse = async (res: Response): Promise => { const contentType = res.headers.get("content-type") || ""; @@ -2055,6 +2056,7 @@ const UploadPage: React.FC = () => { ); })()}
+ ); }; diff --git a/PanTS-Demo/src/styles/brand.css b/PanTS-Demo/src/styles/brand.css new file mode 100644 index 00000000..baed0231 --- /dev/null +++ b/PanTS-Demo/src/styles/brand.css @@ -0,0 +1,162 @@ +/* ────────────────────────────────────────────────────────────────────── + BodyMaps brand layer — tokens + self-hosted fonts. + Palette block is synced verbatim from the BodyMaps brand pack + (colors/bodymaps-colors.css, v1.0). Do not hand-edit values here; + see BRANDING.md at the repo root for the rules and how to use them. + ────────────────────────────────────────────────────────────────────── */ + +/* ── IBM Plex Sans 400/500/600 + IBM Plex Mono 400/500 (self-hosted) ── */ +@font-face { + font-family: "IBM Plex Sans"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("/fonts/ibm-plex-sans-v23-latin-normal-400.woff2") format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: "IBM Plex Sans"; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url("/fonts/ibm-plex-sans-v23-latin-normal-500.woff2") format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: "IBM Plex Sans"; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url("/fonts/ibm-plex-sans-v23-latin-normal-600.woff2") format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: "IBM Plex Sans"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("/fonts/ibm-plex-sans-v23-latin-ext-normal-400.woff2") format("woff2"); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1EFF, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: "IBM Plex Sans"; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url("/fonts/ibm-plex-sans-v23-latin-ext-normal-500.woff2") format("woff2"); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1EFF, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: "IBM Plex Sans"; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url("/fonts/ibm-plex-sans-v23-latin-ext-normal-600.woff2") format("woff2"); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1EFF, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: "IBM Plex Mono"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("/fonts/ibm-plex-mono-v20-latin-normal-400.woff2") format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: "IBM Plex Mono"; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url("/fonts/ibm-plex-mono-v20-latin-normal-500.woff2") format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: "IBM Plex Mono"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("/fonts/ibm-plex-mono-v20-latin-ext-normal-400.woff2") format("woff2"); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1EFF, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: "IBM Plex Mono"; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url("/fonts/ibm-plex-mono-v20-latin-ext-normal-500.woff2") format("woff2"); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1EFF, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +/* ── Alias shim: retired families render as IBM Plex ── + Deferred surfaces (see BRANDING.md) still declare "Space Grotesk" / + "JetBrains Mono", but those fonts are no longer loaded from anywhere. + These aliases map them onto the self-hosted Plex faces so rendering is + deterministic (never dependent on which page's CSS loaded first). + Delete this block when the deferred-surface migration lands. */ +@font-face { + font-family: "Space Grotesk"; + font-style: normal; + font-weight: 100 499; + font-display: swap; + src: url("/fonts/ibm-plex-sans-v23-latin-normal-400.woff2") format("woff2"); +} +@font-face { + font-family: "Space Grotesk"; + font-style: normal; + font-weight: 500 599; + font-display: swap; + src: url("/fonts/ibm-plex-sans-v23-latin-normal-500.woff2") format("woff2"); +} +@font-face { + font-family: "Space Grotesk"; + font-style: normal; + font-weight: 600 900; + font-display: swap; + src: url("/fonts/ibm-plex-sans-v23-latin-normal-600.woff2") format("woff2"); +} +@font-face { + font-family: "JetBrains Mono"; + font-style: normal; + font-weight: 100 499; + font-display: swap; + src: url("/fonts/ibm-plex-mono-v20-latin-normal-400.woff2") format("woff2"); +} +@font-face { + font-family: "JetBrains Mono"; + font-style: normal; + font-weight: 500 900; + font-display: swap; + src: url("/fonts/ibm-plex-mono-v20-latin-normal-500.woff2") format("woff2"); +} + +/* ── BodyMaps color tokens (brand pack v1.0, verbatim) ── + Two-tone accent system: --accent (vivid) for decorative bands, graphics, + and text on dark surfaces; --accent-deep for orange TEXT and filled CTAs + on light surfaces (WCAG AA: 4.59:1 on white). */ +:root { + /* Core */ + --ink: #0F172A; /* primary dark — hero, headers, footer, body text */ + --ink-2: #1B2436; /* elevation 1 on dark surfaces */ + --ink-3: #2A344A; /* elevation 2 on dark surfaces */ + --paper: #FFFFFF; /* page default */ + --paper-2: #F7F7F4; /* off-white bands, card backgrounds */ + --paper-3: #ECEBE5; /* deeper paper tone */ + --rule: #E2E1DA; /* hairline dividers, card borders */ + + /* Accent (the "agreement band" orange) */ + --accent: #E76F51; /* vivid — bands, graphics, dark-surface text (AA large) */ + --accent-deep: #C2532F; /* deep — orange text & CTA fills on light (AA normal) */ + --accent-tint: #F08A6F; /* tint — small accent text on dark (7.3:1 on ink) */ + + /* Text grays */ + --muted: #5A6175; /* secondary/body text on light (6.2:1) */ + --muted-2: #6B7285; /* tertiary text on light (4.8:1) */ + + /* Reserved */ + --teal: #0B4F6C; /* defined in the system; unused in the current build */ + + /* App-local derived values (not pack tokens) */ + --accent-deep-hover: #A8431F; /* darkened accent-deep for hover states */ + --font-sans: "IBM Plex Sans", system-ui, sans-serif; + --font-mono: "IBM Plex Mono", ui-monospace, monospace; +} diff --git a/PanTS-Demo/src/test/brand.test.ts b/PanTS-Demo/src/test/brand.test.ts new file mode 100644 index 00000000..8a213531 --- /dev/null +++ b/PanTS-Demo/src/test/brand.test.ts @@ -0,0 +1,47 @@ +/** + * Brand invariants — locks the BodyMaps identity assets and tokens so a + * merge or refactor cannot silently revert them (this happened once: + * a brand-alignment commit was undone by an unrelated upstream merge). + * If one of these fails, see BRANDING.md at the repo root. + */ +import { describe, expect, it } from "vitest"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +// cwd is PanTS-Demo because npm runs scripts from the package dir and CI +// sets working-directory accordingly (vitest itself does not chdir) +const read = (rel: string) => readFileSync(resolve(process.cwd(), rel), "utf8"); + +describe("brand identity", () => { + it("index.html carries the favicon set, theme color, description, and og tags", () => { + const html = read("index.html"); + expect(html).toContain('href="/favicon.svg"'); + expect(html).toContain('rel="apple-touch-icon"'); + expect(html).toContain('name="theme-color" content="#0F172A"'); + expect(html).toContain('name="description"'); + expect(html).toContain('property="og:image"'); + expect(html).not.toMatch(/fonts\.(googleapis|gstatic)\.com/); + }); + + it("the logo file is the adopted mark (two ink bars + orange band)", () => { + const svg = read("public/bodymaps-logo.svg"); + expect(svg).toContain("#E76F51"); // agreement band + expect(svg).toContain("#0F172A"); // ink bars + expect(svg).not.toContain("#27379b"); // invented navy crosshair + }); + + it("brand tokens match the brand pack", () => { + const css = read("src/styles/brand.css"); + expect(css).toContain("--accent: #E76F51"); + expect(css).toContain("--accent-deep: #C2532F"); + expect(css).toContain("--ink: #0F172A"); + expect(css).toContain('--font-sans: "IBM Plex Sans"'); + }); + + it("the viewer accent seam points at the brand accent", () => { + const css = read("src/index.css"); + expect(css).toContain("--vp-accent: var(--accent)"); + expect(css).toContain("--vp-font: var(--font-sans)"); + expect(css).toContain("--vp-mono: var(--font-mono)"); + }); +}); diff --git a/scripts/brand-guard.mjs b/scripts/brand-guard.mjs new file mode 100644 index 00000000..f1857621 --- /dev/null +++ b/scripts/brand-guard.mjs @@ -0,0 +1,167 @@ +#!/usr/bin/env node +/** + * brand-guard.mjs — BodyMaps brand invariants for the PanTS-Demo frontend. + * + * Run: node scripts/brand-guard.mjs (from the repo root) + * Docs: BRANDING.md (repo root) + * + * Dependency-free by design so it never touches package.json. + * Each rule bans a pattern that the brand alignment PR fully eradicated, + * except in the per-rule ALLOW list — the explicit register of deferred + * files (surfaces under active development that will be migrated in a + * follow-up). Allow entries are rename-tolerant: a file matches if its + * path OR its basename equals an entry's, so moving a deferred file into + * a folder does not turn CI red. Do not add new files to an ALLOW list + * to silence a failure; fix the color/font instead (see BRANDING.md). + */ + +import { execSync } from "node:child_process"; +import { readFileSync, existsSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + +const ROOT = fileURLToPath(new URL("..", import.meta.url)); +const TEXT_EXT = /\.(css|tsx|ts|jsx|js|mjs|html|json|md|yml|yaml|svg)$/; + +const RULES = [ + { + name: "no-external-font-hosts", + pattern: /fonts\.(googleapis|gstatic)\.com/i, + why: "Fonts are self-hosted from PanTS-Demo/public/fonts (IBM Plex).", + allow: [ + "PanTS-Demo/src/routes/UploadPage.css", + // Pre-registered: added by in-flight branches (feat/share-patient-card + // family); migrate to self-hosted Plex when that work lands. + "PanTS-Demo/src/routes/SharePatientCard.tsx", + ], + }, + { + name: "no-off-brand-font-names", + // Tolerates JSX-escaped quotes: fontFamily: "\"Space Grotesk\", ..." + pattern: /["'](Space Grotesk|JetBrains Mono|Space Mono)\\?["']/, + why: 'Use var(--font-sans) / var(--font-mono) or "IBM Plex Sans" / "IBM Plex Mono".', + allow: [ + "PanTS-Demo/src/components/AIAssistant/AISidebar.css", + "PanTS-Demo/src/components/segmentation/MaskingSelect.css", + "PanTS-Demo/src/components/segmentation/SegmentEffectPanel.css", + "PanTS-Demo/src/components/segmentation/SegmentsPopup.css", + "PanTS-Demo/src/components/segmentation/SliceAnchorPickerUI.tsx", + "PanTS-Demo/src/components/viewer/AnnotationToolbar.css", + "PanTS-Demo/src/components/viewer/AnnotationToolbar.tsx", + "PanTS-Demo/src/components/viewer/FlyoutPrimitives.css", + "PanTS-Demo/src/components/walkthrough/ToolWalkthrough.css", + "PanTS-Demo/src/routes/UploadPage.css", + "PanTS-Demo/src/routes/UploadPage.tsx", + // Alias @font-face shim: maps the retired families onto IBM Plex so + // deferred surfaces render deterministically until migrated. + "PanTS-Demo/src/styles/brand.css", + ], + }, + { + name: "no-inter-font", + // Matches CSS font-family, JSX fontFamily inline styles, and Google + // Fonts family= query params. + pattern: /(font-family|fontFamily)\s*:\s*[^;\n]{0,60}\bInter\b|family=Inter/, + why: "The brand typeface is IBM Plex Sans (see BRANDING.md).", + allow: [ + // Pre-registered for the in-flight share-patient-card branches. + "PanTS-Demo/src/routes/SharePatientCard.tsx", + ], + }, + { + name: "no-invented-mark", + pattern: /#27379b/i, + why: "The navy crosshair mark was never an approved logo. Use public/bodymaps-logo.svg (adopted mark).", + allow: ["PanTS-Demo/src/test/brand.test.ts"], // the invariant test asserts this hex is absent + }, + { + name: "no-jhu-blue", + // Hex forms plus rgb()/rgba() triplet of #002D72. + pattern: /#002d72|#00399a|#68ace5|rgba?\(\s*0\s*,\s*45\s*,\s*114/i, + why: "JHU Heritage/Spirit Blue is not a BodyMaps color. CTAs/links on light: var(--accent-deep); accents on dark: var(--accent) / var(--accent-tint).", + allow: [ + "PanTS-Demo/src/components/segmentation/SegmentEffectPanel.css", + "PanTS-Demo/src/components/segmentation/SegmentsPopup.css", + "PanTS-Demo/src/components/segmentation/SegmentsPopup.tsx", + "PanTS-Demo/src/components/segmentation/SliceAnchorPickerUI.tsx", + "PanTS-Demo/src/components/viewer/AnnotationToolbar.css", + "PanTS-Demo/src/components/viewer/AnnotationToolbar.tsx", + "PanTS-Demo/src/components/viewer/FlyoutPrimitives.css", + "PanTS-Demo/src/routes/UploadPage.css", + "PanTS-Demo/src/routes/UploadPage.tsx", + "PanTS-Demo/src/routes/VisualizationPage.tsx", + ], + }, + { + name: "no-off-brand-blue-accents", + pattern: /#6ea8fe|#2563eb|#1d4ed8|#7aa2ff/i, + why: "Blue accent families are retired. Use the accent tokens (see BRANDING.md).", + allow: [ + "PanTS-Demo/src/components/MeasurementPanel/MeasurementPanel.css", + "PanTS-Demo/src/components/Preview.tsx", + "PanTS-Demo/src/components/walkthrough/ToolWalkthrough.css", + "PanTS-Demo/src/routes/VisualizationPage.css", + ], + }, + { + name: "reserved-teal", + pattern: /#0b4f6c/i, + why: "Teal #0B4F6C is reserved and unused per the brand book.", + allow: ["PanTS-Demo/src/styles/brand.css"], + }, +]; + +// Head-metadata invariants for the app shell. +const INDEX_HTML = "PanTS-Demo/index.html"; +const HEAD_MUST_CONTAIN = [ + ['rel="icon" type="image/svg+xml" href="/favicon.svg"', "brand favicon link"], + ['rel="apple-touch-icon"', "apple-touch-icon link"], + ['name="theme-color" content="#0F172A"', "ink theme-color"], + ['name="description"', "meta description"], + ['property="og:image"', "og:image tag"], +]; + +let files; +try { + files = execSync("git ls-files -- PanTS-Demo", { cwd: ROOT, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }) + .split("\n") + .filter((f) => f && TEXT_EXT.test(f) && !f.includes("node_modules")); +} catch (err) { + console.error("brand-guard: requires a git checkout (git ls-files failed)."); + console.error(String(err.stderr || err.message).trim().split("\n")[0]); + process.exit(1); +} + +const basename = (p) => p.slice(p.lastIndexOf("/") + 1); +const isAllowed = (rule, f) => + rule.allow.includes(f) || rule.allow.some((a) => basename(a) === basename(f)); + +let failures = 0; + +for (const rule of RULES) { + for (const f of files) { + if (isAllowed(rule, f)) continue; + const path = ROOT + f; + if (!existsSync(path)) continue; + const lines = readFileSync(path, "utf8").split("\n"); + lines.forEach((line, i) => { + if (rule.pattern.test(line)) { + failures++; + console.error(`FAIL [${rule.name}] ${f}:${i + 1}\n ${line.trim().slice(0, 120)}\n → ${rule.why}`); + } + }); + } +} + +const indexHtml = readFileSync(ROOT + INDEX_HTML, "utf8"); +for (const [needle, label] of HEAD_MUST_CONTAIN) { + if (!indexHtml.includes(needle)) { + failures++; + console.error(`FAIL [head-metadata] ${INDEX_HTML}: missing ${label} (expected to contain: ${needle})`); + } +} + +if (failures) { + console.error(`\nbrand-guard: ${failures} violation(s). See BRANDING.md at the repo root for the tokens and rules.`); + process.exit(1); +} +console.log(`brand-guard: OK (${files.length} files checked, ${RULES.length} rules + head metadata)`);