--- version: alpha name: northbound-design description: "A cool graphite dashboard canvas (#0f1115) with raised panel surfaces (#171a21), signal blue (#5b8def) as the primary accent and a single warm amber (#f5a623) reserved for growth/warning numbers. Inter Tight carries headlines and every number at weight 700; Inter carries body copy and labels. The system reads as enterprise-trustworthy, precise, numerate, unhurried." --- ## Brand Positioning Northbound is for the finance lead who opens one dashboard before the stand-up and needs the number to be right the first time, not "delightful." It exists because most revenue dashboards borrow consumer-product chrome (confetti on a good month, cartoon mascots, a chart that bounces like a toy) that undermines the one thing a finance tool has to earn: trust that the number on screen is the real number. Northbound's bet: a dashboard should feel like it was drawn by an instrument, not a designer showing off, so the chart draws itself once, calmly, and the numbers count up the way a real ledger would round them, with commas and a currency prefix, never a gimmick. Every visual decision below serves that bet. If a component doesn't help someone trust a number faster, or doesn't come from a real SaaS-dashboard interaction (nav, form, table, status), it doesn't belong in this system. colors: accent: "#5b8def" accent-alt: "#f5a623" ink: "#f2f3f5" ink-muted: "#8a8f9a" canvas: "#0f1115" surface: "#171a21" surface-raised: "#1d212b" border: "#262b35" focus-ring: "#5b8def" ## Logo / Wordmark System The mark is a self-drawing trend line inside a rounded-square panel, a literal miniature of the chart component the product is built around: a line that only ever climbs up and to the right, with a single point marking the current, live value. It is not a compass or an arrow glyph, it's the same path shape the hero chart draws, frozen at four points. ```svg ``` - **Lockup**: mark, then a `0.5rem` gap, then the wordmark "Northbound" in Inter Tight/700, mark always left of the wordmark, never stacked or reversed. - **Minimum size**: mark not smaller than `18px` square, below that the climb-and-point shape reads as a blob rather than a line. Wordmark not smaller than `0.85rem`. - **Clearspace**: empty margin equal to the mark's own height (22px at default size) on all four sides of the lockup, nothing else docks closer, including the nav's edge padding. - **Don't**: recolor the mark for a light or busy background, it only ever renders on `--panel`/`--bg`. Don't stretch it non-uniformly. Don't rotate it. Don't invert the line's direction, it climbs up-right, never down, that slope is the brand's entire promise. Don't use the mark alone as a decorative bullet or list marker. ## Color System ### Roles | Role | Token | Value | Used for | |---|---|---|---| | Canvas | `--bg` | `#0f1115` | Page background | | Surface | `--panel` | `#171a21` | Panels, nav, cards, chart panel, table header | | Surface raised | `--surface-raised` | `#1d212b` | Hover state on a surface (row hover, input focus bg) | | Border | `--border` | `#262b35` | Panel edges, dividers, input outlines, table rules | | Ink | `--ink` | `#f2f3f5` | Primary text | | Ink muted | `--dim` | `#8a8f9a` | Secondary text, timestamps, helper text, placeholders, card labels | | Signal | `--accent` | `#5b8def` | Chart line, primary button fill, focus ring, "live" status, links | | Warning | `--accent-alt` | `#f5a623` | Growth/change emphasis, "needs attention" status, form invalid state | There is deliberately no third accent for "error" or "critical." Northbound only ever surfaces two temperatures to a finance lead: on track (`--accent`, calm blue) and needs a look (`--accent-alt`, warm amber). A sync failure, a stalled data source, or a form mistake all route to amber with a plain-language word next to it, never to an invented red. This is the brand's constraint, not a gap, a dashboard that reaches for alarm-red every time something is merely late is exactly the "consumer flash" this system exists to avoid. ### Contrast (computed from the hex values above via WCAG relative luminance) | Pairing | Ratio | Passes | |---|---|---| | `--ink` text on `--bg` | ~17:1 | AAA | | `--dim` text on `--bg` | ~5.8:1 | AA (body text) | | `--ink` text on `--panel` | ~15.7:1 | AAA | | `--dim` text on `--panel` | ~5.4:1 | AA (body text) | | `--dim` text on `--surface-raised` (row hover) | ~5.0:1 | AA (body text) | | `--accent` text on `--bg` (links, live status) | ~5.85:1 | AA | | `--accent-alt` text on `--bg` (warning status) | ~9.3:1 | AAA | | `--bg` label on `--accent` fill (primary button) | ~5.85:1 | AA | | `--bg` label on `--accent-alt` fill (warning badge) | ~9.3:1 | AAA | | `--ink` label on `--accent` fill | ~2.9:1 | Fail, never do this | | white label on `--accent-alt` fill | ~2.0:1 | Fail, never do this | The one rule this table exists to enforce: a button or badge filled with `--accent` or `--accent-alt` always labels itself in `--bg`, never in white or `--ink`. Both alternatives measurably fail AA against these fills. typography: display: fontFamily: Inter Tight fontWeight: 700 body: fontFamily: Inter fontWeight: 400 ## Type Scale | Token | Size | Weight | Family | Line-height | Letter-spacing | Used for | |---|---|---|---|---|---|---| | `--text-display` | `clamp(2.5rem, 6vw, 4.5rem)` | 700 | Inter Tight | 1.05 | -0.02em | Hero H1 only | | `--text-h2` | `1.75rem` | 700 | Inter Tight | 1.2 | -0.01em | Section headings | | `--text-h3` | `1.125rem` | 600 | Inter Tight | 1.3 | 0 | Card/table group titles | | `--text-stat` | `clamp(1.5rem, 3vw, 1.8rem)` | 700 | Inter Tight | 1.1 | -0.01em | Dashboard stat values, count-up numbers, tabular-nums on | | `--text-body` | `1rem` | 400 | Inter | 1.6 | 0 | Paragraph copy | | `--text-small` | `0.85rem` | 400 | Inter | 1.5 | 0.01em | Nav links, footer, helper/error text, timestamps | | `--text-label` | `0.8rem` | 500 | Inter | 1.4 | 0.04em, uppercase | Card labels, table column headers | Rule for Inter Tight vs. Inter: Inter Tight is reserved for the hero headline, section headings, card/table titles, and anything that is literally a number representing a metric (stat values, table figures, count-up totals), with `font-variant-numeric: tabular-nums` on every numeral so columns compare at a glance. Everything meant to be read as a sentence, a nav link, a button label, a form label, or a footer line is Inter. If it's a moment where the number itself is the product, it's Inter Tight, if it's copy explaining or framing that number, it's Inter. ## Spacing Scale Base unit `8px`. Tokens in use: `--space-2: 0.5rem`, `--space-3: 1rem`, `--space-4: 1.5rem`, `--space-6: 2rem`, `--space-8: 3rem`, `--space-12: 4rem`. ## Shape Two radii, each with a documented, consistently-applied job, not a mixed system: - `--radius-lg: 16px` for exactly one surface, the hero chart panel, the single largest and most important panel on the page earns the softest, roomiest corner. - `--radius: 12px` for everything else that is a container, stat cards, the table wrap, form inputs, status panels. - `--radius-pill: 999px` for every interactive chip, buttons and badges. Nothing rounds outside these three values. ## Iconography No icon library. The only non-text glyphs on the page are the logo mark and a single status dot (`--accent` for live/synced, `--accent-alt` for needs attention), always paired with a plain-language word, never used decoratively in a list or nav. This matches how a real ops dashboard signals state and avoids importing an icon set for one glyph. If a future page needs more than a two-state status dot, the fallback is Phosphor (line style, `1.5` stroke), not hand-drawn SVG paths, that decision just hasn't been needed yet. ## Motion Principles An animated line chart draws itself stroke-by-stroke as you scroll, dashboard cards flipping in with counting numbers. This is the only scripted scroll animation on the page, everything else (buttons, inputs, table rows) uses only `:hover`/`:active`/`:focus-visible` CSS transitions. The chart draw is a scroll-scrubbed stroke reveal (`power2.out`, tied to the panel entering view once), the card entrance is a one-shot staggered pop, the two intentionally use different easing so a continuous "the line is being measured" motion never reads the same as a one-time "the cards arrived" motion. Both play once per view (`toggleActions: play none none reverse`) and both, along with the count-up tweens, fully honor `prefers-reduced-motion` by rendering the end state immediately with no animation. ### Never-list (written before the impeccable-pass rebuild) 1. No centered marketing headline sitting above the chart panel, the dashboard widget is the hero, the headline lives inside its toolbar. 2. No two-button CTA row floating free of the dashboard chrome, the primary action sits inside the same widget as the chart and cards. 3. No stat cards in a separate full-width section disconnected from the chart, nest them in the same panel, like a real dashboard widget. 4. No decorative window-chrome, the panel toolbar is real: a live-sync status dot and a time-range label, not decorative dots. 5. No neon or saturated colors beyond the documented signal blue and amber. 6. No component wall, exactly one new proof component (a Sources status list), not a stacked gallery of every pattern. ## Voice & Tone calm, precise, numbers-first. In practice: - **Button label**: "Book a walkthrough", not "Get started for free today" - **Error / edge-case message**: "Sync paused 4 minutes ago. Reconnect your source.", not "Uh-oh! Something went wrong on our end" - **Empty state**: "No revenue synced yet.", not "Nothing here yet! Connect a source to get the party started" ## Accessibility Baseline - Focus ring: `2px solid var(--accent)`, `2px` offset, applied via `:focus-visible` on every interactive element (links, buttons, inputs), never removed without a visible replacement. - Reduced motion: the chart draw, the card entrance, and every count-up tween check `prefers-reduced-motion: reduce` and render fully resolved with no animation when it's set. - Minimum tap target: `44px` in the smallest dimension for every button and input on touch viewports. - Color is never the only signal: status pairs the dot with a word ("Live synced" / "Needs attention"), table status cells pair color with plain-language text, form errors pair an amber border with an inline message, not just a colored outline. ## Components ### Navigation Single-line bar, `68px` tall, `--panel` background with a `--border` bottom hairline, sticky to the top. Left: the mark + wordmark lockup. Center-right: a gallery utility link plus two page links (Home, Reports). Right: one primary button. No dropdown, no hamburger at this page count, the link group hides below `640px` rather than collapsing into a menu, the lockup and primary action always stay. ### Buttons Three variants, one radius (`--radius-pill`), two accents, only one fill per view: - **Primary**: `--accent` fill, `--bg` label, used once per view for the single real action ("Book a walkthrough"). - **Secondary**: transparent fill, `1px solid --border`, `--ink` label. Same-page anchor jumps and secondary navigation like "View reports." - **Ghost**: text-only, `--dim` label, `--ink` on hover. Tertiary actions. All variants define `:hover` (brightness/border shift), `:active` (`transform: scale(0.98)`), `:disabled` (`opacity: .4`, `pointer-events: none`), and `:focus-visible` (accent outline). Disabled state never relies on color alone, opacity carries it too. ### Hero signature interaction A single dashboard widget is the hero: a real toolbar (live-sync status dot, "This quarter" range label), the page headline as the widget's caption instead of a centered marketing H1, an SVG line chart with `stroke-dasharray`/`stroke-dashoffset` that draws itself in as the panel scrolls into view, three stat cards nested in the same panel flipping in with a staggered pop and counting up formatted like a real dashboard (commas, a `$` prefix on MRR), and the primary CTA row inside the widget's own footer, not a separate floating hero block. ### Cards / flat panels A flat `.panel`: `--panel` background, `1px solid --border`, `--radius` (or `--radius-lg` for the chart panel only), no shadow, no gradient. Used for stat cards, the table wrap, and status states. Elevation is communicated by the border, never a drop shadow, matching the brand's instrument-not-showroom read. ### Data table Header row in `--panel`/`--text-label`, body rows in `--bg`, one `--border` rule between rows, row hover to `--surface-raised`. Numeric columns are right-aligned, Inter Tight, `tabular-nums`, formatted with commas and a `$` prefix on currency columns, exactly the way a real revenue table is read. Status cells pair the dot with a plain-language word. ### Status / feedback states - **Loading**: a skeleton shaped like the real content (bars matching text line-heights), not a spinner. - **Empty**: a plain sentence in `--dim`, no illustration, per Voice & Tone above. - **Needs attention**: an inline `--accent-alt` message with a reconnect/retry action, this is Northbound's only "something's off" state, there is no red. - **Live synced**: `--radius-pill` badge, `1px solid var(--accent)`, `--accent` text on transparent fill (outlined, not filled, so the primary button remains the only solid-blue element on screen). ### Sources status list A bordered row group, `.sources`, proving the live/needs-attention status dot outside the toolbar: one row per billing data source, name on the left, a status dot plus plain-language timestamp on the right, `--accent-alt` on the row whose source needs attention. Lives on the homepage, directly below the dashboard widget. ### Forms Label above input, `--text-small`/`--dim`. Input: `--panel` background, `1px solid --border`, `--radius`, `--ink` text, `--dim` placeholder (never placeholder-as-label). On `:focus`, border becomes `--accent` and the focus ring applies. Helper text sits below in `--text-small`/`--dim`. On error, the border becomes `--accent-alt` and the helper text is replaced with an amber message, the same amber the "needs attention" status uses, reused here because it's the brand's one genuinely warning-semantic color, not a decorative third accent. ### Footer One row: product name/tagline on the left, real page links on the right (Reports, back to gallery). No invented legal text, no links to pages that don't exist. ## Do's and Don'ts ### Do - Let the chart line be the hero, draw it with intent, not as decoration. - Format counted numbers the way a real dashboard would (commas, a currency prefix on MRR) so the count-up reads as trustworthy, not gimmicky. - Keep panels calm, one signal blue, one warning amber, nothing louder. - Right-align and `tabular-nums` every numeric table or card value so columns compare at a glance. - Use `--bg` as the label color on any `--accent`- or `--accent-alt`-filled surface, the computed contrast table exists to enforce this. - Route every "something's wrong" state to the amber "needs attention" semantic, paired with a plain-language word, never a bare color. ### Don't - Don't reuse the same easing for the chart draw and the card entrance, differentiate a scroll-scrubbed reveal from a one-shot stagger. - Don't clutter the dashboard mock with more than 3 stat cards. - Don't use neon or saturated colors, this is enterprise trust, not consumer flash. - Don't invent a third accent color for "error" or "critical", amber already carries that job. - Don't label an `--accent`- or `--accent-alt`-filled button or badge in white or `--ink`, both measurably fail AA against these fills. - Don't stack more than one primary button in the same view, one conversion action per screen. - Don't float the headline above the chart in a separate centered hero block, it lives inside the dashboard widget's own toolbar. ## Known Constraints - Two routes now exist (marketing home, reports), still no docs/pricing pages, no authenticated app shell (the "Book a walkthrough" and "Invite a teammate" flows are not wired to anything real, this is a concept demo), no responsive breakpoint table beyond a single-column mobile collapse. The page ships one fixed dark canvas by design, not a light/dark toggle, consistent with an always-on ops dashboard a finance lead leaves open all day.