--- version: alpha name: ledgerline-design description: "A near-black financial ledger built around a #0a0e14 canvas and #eef1f6 text, with two accents: an emerald #1fae7a for growth and confirmation, a muted gold #d4af6a for the one number that needs a second look. IBM Plex Mono carries every literal figure at weight 600; IBM Plex Sans carries the surrounding prose. The system reads as a bank statement that trusts you with real numbers, not a growth-hacked budgeting app." --- ## Brand Positioning Ledgerline is for people who want to see the actual number, not a mood about the number. It exists because most personal-finance apps bury the balance under gamification, badges, streaks, a cartoon mascot cheering a saved dollar. Ledgerline's bet: the most reassuring thing a finance product can do is state the figure precisely and let it change in front of you, the way a real ledger line does when a new entry posts. Every visual decision below serves that one bet. If a component doesn't help someone read their own numbers faster, or doesn't come from a real banking interaction (statement, transaction table, form, status), it doesn't belong in this system. colors: accent: "#1fae7a" accent-alt: "#d4af6a" ink: "#eef1f6" ink-muted: "#8b96a5" canvas: "#0a0e14" surface: "#10151d" surface-raised: "#161d27" border: "#1f2731" focus-ring: "#1fae7a" ## Logo / Wordmark System The mark is a rounded-square ledger card containing a miniature sparkline, an ascending path in emerald ending in a single gold dot, literally the balance chart the homepage draws in. It is not a generic icon, it's a 1:1 miniature of the `.spark` component. ```svg ``` - **Lockup**: mark, then a `0.5rem` gap, then the wordmark "ledgerline" in mono/600, mark always left of the wordmark, never stacked or reversed. - **Minimum size**: mark not smaller than `16px` square, below that the sparkline path and dot merge into a blob. Wordmark not smaller than `0.8rem`. - **Clearspace**: empty margin equal to the mark's own height (22px at default size) on all four sides of the lockup. - **Don't**: recolor the mark for a light background, it only ever renders on `--panel`/`--bg`. Don't animate the mark itself, the sparkline motion belongs to the homepage's balance card, not the nav. Don't swap the dot to emerald, gold marks "the current point," and reusing green there would make it indistinguishable from the line. Don't stretch or rotate it. ## Color System ### Roles | Role | Token | Value | Used for | |---|---|---|---| | Canvas | `--bg` | `#0a0e14` | Page background | | Surface | `--panel` | `#10151d` | Panels, nav, cards, table header | | Surface raised | `--surface-raised` | `#161d27` | Hover state on a surface (input focus bg, row hover) | | Border | `--border` | `#1f2731` | Panel edges, dividers, input outlines, table rules | | Ink | `--ink` | `#eef1f6` | Primary text | | Ink muted | `--dim` | `#8b96a5` | Secondary text, timestamps, helper text, placeholders | | Growth / confirm | `--accent` | `#1fae7a` | Sparkline line, incoming amounts, primary button fill, success status, focus ring | | Attention | `--accent-alt` | `#d4af6a` | Sparkline endpoint dot, sync-error state, the one over-budget category bar | There is deliberately no separate "danger red." A bank statement doesn't need a fire-alarm color to say a category ran over, it needs the reader's attention, not their anxiety. Gold does that without implying something broke. A genuine failure (a lost bank connection) is communicated with the word "failed" in body copy, not a red fill. ### Contrast (computed from the hex values above via WCAG relative luminance) | Pairing | Ratio | Passes | |---|---|---| | `--ink` text on `--bg` | ~17.1:1 | AAA | | `--dim` text on `--bg` | ~6.5:1 | AA (body text) | | `--accent` text on `--bg` (sparkline, incoming amounts) | ~6.8:1 | AA (body text) | | `--accent-alt` text on `--bg` (attention states) | ~9.3:1 | AAA | | `--bg` label on `--accent` fill (primary button) | ~6.8:1 | AA | | `--ink` on `--panel` | ~16.2:1 | AAA | | `--ink` on `--surface-raised` | ~15.0:1 | AAA | The rule this table exists to enforce: a button or badge filled with `--accent` always labels itself in `--bg`, never in white or `--ink`, both of those undershoot AA at this saturation. typography: display: fontFamily: IBM Plex Mono fontWeight: 600 body: fontFamily: IBM Plex Sans fontWeight: 400 ## Type Scale | Token | Size | Weight | Family | Line-height | Letter-spacing | Used for | |---|---|---|---|---|---|---| | `--text-display` | `clamp(2.25rem, 5.5vw, 4rem)` | 600 | Mono | 1.05 | 0 | Hero H1 only | | `--text-h2` | `1.5rem` | 600 | Mono | 1.2 | 0 | Section headings | | `--text-balance` | `clamp(2rem, 6vw, 3.25rem)` | 600 | Mono | 1.1 | 0 | The balance figure, the one number on the page that gets its own scale step | | `--text-body` | `1rem` | 400 | Sans | 1.6 | 0 | Paragraph copy | | `--text-small` | `0.85rem` | 400 | Sans | 1.5 | 0.01em | Footer, nav utility links, labels, helper/error text | | `--text-figure` | `0.9rem` | 400 (tabular-nums) | Mono | 1.5 | 0 | Table amounts, category totals, timestamps | Rule for mono vs. sans: mono is reserved for the hero headline, section headings, the balance figure, and anything that is literally a number meant to be compared digit-by-digit (table amounts use `font-variant-numeric: tabular-nums` for this reason). Body copy, nav labels, button labels, and form labels are sans. A page that put every word in mono would read like a terminal, not a statement, the split keeps it a statement. ## 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 One radius scale: `--radius: 10px` for panels, cards, inputs, and table containers. `--radius-pill: 999px` for buttons, category-bar tracks, and status badges. Nothing else rounds. ## Iconography No icon library. The only non-text glyphs are the logo mark and one Unicode status glyph, `✓` (synced/success), set in mono at the same size as the text beside it. Negative and positive amounts are distinguished by a literal `+`/`−` prefix and color, not an arrow icon, matching how a real statement prints. If a future page needs more than that one glyph, the fallback is Phosphor (line style, `1.5` stroke), that decision just hasn't been needed yet. ## Motion Principles Scroll drives an odometer-style balance that rolls digit by digit from zero to its real value while a sparkline beneath it draws in stroke-by-stroke on the same timeline, an endpoint dot fading in once the line finishes. Category bars fill to their real width on the same scroll-in pattern. These are the only scripted animations on the page, everything else (buttons, inputs, table rows) uses only `:hover`/`:active`/`:focus-visible` CSS transitions. Each reveal plays once per view (`toggleActions: play none none reverse`), never replays on every scroll pass, and fully honors `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 balance panel, the panel is the hero, the headline lives inside its toolbar. 2. No two-button CTA row floating free of the panel, the primary action sits inside the same widget as the balance and sparkline. 3. No decorative toolbar chrome, the "Synced" badge is the same real status DESIGN.md already documents, previously built but never shown on the homepage. 4. No `width`/`height` transitions where `transform: scaleX()` does the same job without triggering layout, this pass's fix for the category-bar fill. ## Voice & Tone precise, calm, plainspoken about numbers, no hype. In practice: - **Button label**: "Open an account", not "Start your money journey today" - **Error message**: "Sync failed. Retry.", not "Uh-oh! We couldn't reach your bank" - **Empty state**: "No transactions yet this month.", not "Nothing here yet, add your first transaction!" - **Success status**: "Synced 1m ago", not "You're all synced up! 💰" ## 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 balance count-up, sparkline reveal, and category-bar fill all 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: incoming vs. outgoing amounts pair color with a literal `+`/`−` prefix, form errors pair the border color with an inline message, sync status pairs the `✓` glyph with a word. ## Components ### Navigation Single-line bar, `64px` tall, `--panel` background with a `--border` bottom hairline, sticky to the top. Left: a small "back to gallery" utility link in `--text-small`/`--dim`. Center-left: the mark + wordmark lockup. Right: an Activity link plus one primary button. Collapses to just the lockup and primary button under `480px`, no hamburger, there are only two destinations. ### Buttons Three variants, one radius (`--radius-pill`), one accent: - **Primary**: `--accent` fill, `--bg` label, used once per view for the single real action ("Open an account"). - **Secondary**: transparent fill, `1px solid --border`, `--ink` label. Retry actions, form submits that aren't the primary conversion. - **Ghost**: text-only, `--dim` label, `--ink` on hover. Tertiary actions like "see it in action." All variants define `:hover` (brightness/border shift), `:active` (`transform: scale(0.98)`), `:disabled` (`opacity: .4`, `pointer-events: none`), and `:focus-visible` (accent outline). ### Forms Label above input, `--text-small`/`--dim`. Input: `--panel` background, `1px solid --border`, `--radius`, `--ink` text, `--dim` placeholder. On `:focus`, border becomes `--accent` and the focus ring applies. On error, the border becomes `--accent-alt` and helper text explains what to fix in plain language, never "invalid input." ### Balance card A `.panel` containing the balance label, the mono balance figure, and the `.spark` sparkline. The one component on the page allowed a scale step above the type table's normal maximum, because it's the one number the whole product exists to show. ### Category bars A label, a `--radius-pill` track filled to a percentage in `--accent` (on-plan) or `--accent-alt` (over budget), and a right-aligned tabular-nums amount. Reused wherever a spending category needs a relative-size comparison at a glance. The fill is a full-width element scaled with `transform: scaleX()` from a `left` origin, not an animated `width`, so the reveal stays on the compositor instead of triggering layout on every frame. The fill element needs an explicit `display: block`, it's a ``, and `width`/`transform` scaling don't apply to non-replaced inline elements, a real bug this pass caught: the bars never actually rendered a visible fill before this fix. ### Table Header row in `--panel`/`--dim` sans labels, body rows in `--bg`, one `--border` rule between rows, row hover to `--surface-raised`. Amount cells are mono/tabular-nums with a literal `+`/`−` prefix, never a bare number. ### 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. - **Error**: an inline `--accent-alt` message with a retry action, never a silent failure. - **Success / status badge**: `--radius-pill`, `1px solid --accent`, `--accent` text on transparent fill, not filled, filled would compete with the primary button's exclusive use of a solid `--accent` fill. ### Footer One row: product name/tagline on the left, real page links on the right (Activity, back to gallery). No invented legal or compliance text. ## Do's and Don'ts ### Do - Let the balance figure and its sparkline be the only oversized, scale-breaking element on any page. - Reserve `--accent`/`--accent-alt` for genuinely semantic uses, incoming vs. attention, never purely decorative. - Pair every amount with a literal `+`/`−` prefix, not color alone. - Use `--bg` as the label color on any `--accent`-filled surface. - Set tabular-nums on every column of numbers so digits align. ### Don't - Don't add a third accent color, gamification badges, streak counters, or a mascot, this is a statement, not a rewards program. - Don't replay the balance count-up or sparkline draw on every scroll pass, reveal once per view, like a statement posting. - Don't introduce a red "danger" color, route genuine failures to plain-language copy in `--accent-alt` instead. - Don't stack more than one primary button in the same view. - Don't fill a status badge solid, badges stay outlined so the primary button remains the only solid-fill element on screen. ## Known Constraints - Two routes now exist (statement home, activity), still no account-settings or multi-account switcher, no responsive breakpoint table beyond a single-column mobile collapse, no authenticated states (the "Open an account" flow is not wired to anything real, this is a concept demo).