← Back to app

User Guide & Privacy Everything you need to know about using Roastfolio and how your data is protected.

Getting Started

Creating an account

  1. Click Sign Up on the login page and enter your e-mail address and a password (minimum 6 characters).
  2. Check your inbox for a verification code from Roastfolio and enter it when prompted.
  3. Choose a display name (nickname) — this is the only personal detail stored beyond your e-mail.
  4. You are now logged in. Your account and all data are tied to your e-mail address.

Password recovery: If you forget your password, click Forgot password? on the login page. A reset code will be sent to your verified e-mail address.

Creating your first wallet

After logging in, open the Wallets tab. This is your main portfolio management screen. To add a wallet:

Name wallets after the brokerage or account they represent (e.g. IKE, IKZE, XTB, Binance). Each wallet tracks one account independently.

Summary is always the first entry and is computed automatically as the aggregate of all your wallets. It is read-only — you cannot add transactions to it directly.

Holdings

Holdings are created automatically when you record a BUY transaction. There is no separate "add holding" form. Each BUY transaction adds to (or creates) the matching position in your wallet.

The holdings table shows each position with live price data, current value, today's change in PLN and %, and allocation percentage. On mobile, holdings display as stacked cards. On desktop, they appear in a table with sortable columns.

To edit or delete a position, tap the ✏️ or 🗑️ icons on the holding row. To record a trade against an existing holding directly, use the Buy or Sell chip buttons on the holding card.

Cash holding: any holding without a ticker (named e.g. Cash or Konto) is treated as a cash position. Its value is used to calculate available cash for BUY transactions.

Recording transactions

Transactions are recorded in a slide-up panel (bottom sheet) that opens without leaving the holdings view. To open it:

Inside the transaction panel, select the type using the buttons at the top:

TypeWhat it does
BuyAdds units to an existing holding or creates a new one. Decreases cash balance.
SellRemoves units from a holding. Increases cash balance.
DepositAdds cash to the wallet without buying anything. Increases cash balance. Counts toward the monthly contribution streak.
WithdrawalRemoves cash from the wallet. Decreases cash balance. Breaks the withdrawal-free streak.

For Buy and Sell, start by searching for the asset. Two sources are supported:

Enter the quantity (number of units) and trade value (total cash impact in PLN). Commission and a free-text comment are optional under Advanced options. A live review summary appears before you submit.

The panel has two tabs — Trade (new transaction) and History (past transactions and daily value snapshots for the selected wallet). On mobile, swipe down from the handle at the top to dismiss the panel.

Auto-add cash: if a BUY transaction would exceed your available cash balance, you can enable Automatically add missing cash before a buy. This records a DEPOSIT for the shortfall automatically before applying the buy.

All-Time High (ATH) management

Roastfolio tracks the All-Time High (ATH) portfolio value for each wallet and for the overall portfolio. ATH is used on the dashboard gauge to show how close you are to your peak. By default, ATH is computed automatically from your daily value snapshots.

To view or edit ATH for a wallet, open the Wallets tab and select the wallet from the list. In the wallet overview bar you will see an All-Time High (ATH) row showing the current peak value, its date, and whether it was set automatically or manually.

Tap the ✏️ icon to expand the ATH editor:

Auto mode: click Auto (reset from history) to discard any manual override and let the system recalculate ATH from your historical daily snapshots. Auto mode is the default for all new wallets.

Manual ATH does not permanently disable automatic updates — if a new daily snapshot exceeds the stored ATH, the system will still update it automatically. Manual ATH only sets the current floor.

Summary wallet — selecting Summary in the wallet list shows and lets you edit the ATH for the aggregated total portfolio value.

Asset Analysis

The Analysis screen provides deep-dive research tools for any stock, ETF, or crypto asset. Use it to explore detailed price history, financial statements, and earnings data before making investment decisions.

Opening the Analysis screen

There are multiple ways to access the Analysis screen:

Quick tip: Cash holdings are not clickable as they have no market data.

Search and selection

At the top of the Analysis screen is a search bar where you can enter any ticker symbol or company name (e.g. AAPL, CDR.WA, Meta, BTC-USD). Results appear instantly from Yahoo Finance's global database. Holdings you already own are marked with a ✓ Owned badge.

Use arrow keys to navigate the dropdown and press Enter to select an asset. The analysis loads automatically with a smooth animated loader.

Price history chart

The main chart shows the asset's price history with the following features:

Fundamental properties

Below the chart, a grid displays key fundamental metrics fetched from Yahoo Finance:

MetricDescription
Sector & IndustryBusiness classification (e.g. Technology / Software)
Market CapTotal market capitalization (formatted as B/M/T)
P/E RatioTrailing and forward price-to-earnings ratios
Dividend YieldAnnual dividend as percentage of current price
52-Week High/LowPeak and bottom prices over the last year

All metrics display "—" when data is unavailable (common for small-cap stocks, crypto, or newly listed assets).

Cash Flow Statement

For companies with available financial data, the Analysis screen shows a detailed Cash Flow Statement covering the last 5 years. This section includes:

Cash flow data is particularly useful for evaluating a company's ability to generate cash, fund operations, and pay dividends.

Income Statement

The Income Statement section displays profit-and-loss data for the same 5-year period:

This data helps assess profitability, revenue growth, and operational efficiency over time.

Data availability: Financial statements are pulled from Yahoo Finance. Large-cap stocks typically have complete historical data. Small-cap stocks, ETFs, crypto, and international assets may have limited or missing financial data. If no data is available, the Cash Flow and Income Statement sections will be hidden automatically.

Best practices for analysis

Privacy & Security

How authentication works

Roastfolio uses AWS Cognito — Amazon's managed identity service — for all authentication. Your password is never stored or even seen by the application code. The login flow uses the industry-standard SRP (Secure Remote Password) protocol: only a cryptographic proof is exchanged, not the plaintext password.

After login, Cognito issues a signed JWT token (JSON Web Token). Every API call includes this token in the Authorization header. The backend Lambda function validates the token's signature against Cognito's public key before processing any request. If the token is invalid, expired, or missing, the request is rejected with HTTP 401.

Token expiry: Access tokens expire after 24 hours. Refresh tokens expire after 30 days. When your session expires you will be redirected to the login page.

Your data — strict per-user isolation

Every item stored in the database (portfolios, holdings, transactions, snapshots) uses your Cognito user ID (sub) as the primary partition key. This is a unique, non-guessable UUID assigned at signup.

The backend code enforces that your token's user ID matches the requested data's partition key on every read and write. It is technically impossible for one logged-in user to access another user's data through the application's API — even if they know the other user's e-mail or ID.

Infrastructure security

🔒
HTTPS everywhere
All traffic between your browser and the app is encrypted with TLS 1.2+. CloudFront enforces HTTPS and redirects plain HTTP requests.
🌐
CloudFront CDN
Static files are served through CloudFront. The underlying S3 bucket is private — it cannot be accessed directly, only via CloudFront.
🗄️
Encrypted at rest
All DynamoDB tables are encrypted at rest using AWS-managed keys (AES-256). The S3 bucket uses server-side encryption.
♻️
Point-in-time recovery
Every DynamoDB table has PITR enabled — data can be restored to any second in the last 35 days in case of accidental deletion or corruption.
🔑
Least-privilege IAM
Each Lambda function has an IAM role granting only the exact DynamoDB actions it needs (e.g. the snapshot function cannot modify user profiles).
🚫
No public S3 access
The S3 bucket has all public-access settings blocked. A bucket policy allows only the CloudFront distribution (via OAC) to read objects.

Developer access policy

As the developer and sole operator of Roastfolio, I have administrative access to the AWS account that hosts your data. This is technically unavoidable for a solo-operated SaaS. Here is how I limit and account for that access:

What I have access toHow it is controlled
DynamoDB tables (read/write) IAM — separate dev and prod accounts; no routine prod access
CloudWatch logs (Lambda logs) Logs contain request metadata, not portfolio values
AWS CloudTrail All API calls to prod DynamoDB are audit-logged with timestamp and identity
Your password Never stored anywhere — handled entirely by AWS Cognito
Your JWT tokens Never logged or stored — they expire and are validated in-memory only

Your data is protected by AWS IAM access controls and all access to the production database is audit-logged via AWS CloudTrail. I do not have routine access to production data. Any access I make for support or maintenance purposes is recorded in the audit log and I commit to not reading, copying, or using your portfolio data for any purpose other than operating the service.

— Michał Bardadyn, developer of Roastfolio

Important transparency note: Roastfolio is not a zero-knowledge application. Because portfolio calculations (live prices, gain/loss, benchmarks) happen on the server, the Lambda function processes your holdings data in plaintext. If you require cryptographic guarantees that no one — including the operator — can ever read your data, a server-side architecture like this cannot provide that. The controls described above are operational and policy-based, not mathematical.

Data encryption

All data is encrypted at rest (DynamoDB server-side encryption, S3 SSE) and in transit (TLS via CloudFront and API Gateway). The encryption keys are AWS-managed (AES-256). As the account owner I have administrative access to these keys, which is consistent with the operational access model described above.

Audit logging

AWS CloudTrail is enabled on the AWS account and records every API call made to DynamoDB, Lambda, and S3, including the identity making the call, the timestamp, and the source IP. These logs are stored in S3 and cannot be altered retroactively without detection. In practice, this means that any access to your data — including by the developer — leaves an immutable record.

Frequently Asked Questions

Can the developer see my portfolio data?

Technically yes — as the AWS account administrator I can query the DynamoDB tables directly. Practically: I do not do this in the normal course of operating the service, and any time I do (e.g. to investigate a bug you reported) it is recorded in CloudTrail. I have committed above not to read or use your data beyond what is necessary to run the service.

If this level of trust is not sufficient for you, I recommend not storing sensitive financial data in any cloud-hosted service without client-side encryption.

How is my data backed up?

All DynamoDB tables have Point-in-Time Recovery (PITR) enabled. This means AWS continuously backs up your data and it can be restored to any point within the last 35 days. In the event of accidental data loss I can restore a table backup to recover your portfolios and transactions.

How do I delete my account?

Go to Settings → Account → Delete account. This will permanently delete your Cognito account, all your portfolios, holdings, transactions, and snapshots from DynamoDB. The deletion is irreversible. Because DynamoDB PITR retains backups for up to 35 days after deletion, residual copies may exist in backups during that window and are then permanently purged.

🏆 Milestones & Achievements

What is this?

Roastfolio tracks your investing habits over time and rewards consistency with streaks, milestones, and badges. The goal is to make the boring middle of long-term investing feel like it's going somewhere — because it is.

Everything is calculated automatically from your transaction history. There is nothing to opt into: the moment you make your first deposit, the system starts tracking. The only things that require manual setup are the monthly deposit goal and retirement plan, which unlock additional streak and milestone types.

Roastfolio's achievement philosophy: badges reward real financial behavior — depositing money, staying invested, avoiding withdrawals — not artificial engagement like logging in every day or watching animations. The tone is sarcastic but the mechanic is sound.

Where to find it

Achievements surface in three places:

Streaks

A streak is a consecutive run of a specific behavior, measured in months or trading days. Streaks reset if you miss a period — but your personal best is always saved and visible in the achievements screen as a "former streak" record.

Streak What it measures Unit Resets when Requires setup?
Deposit streak Consecutive calendar months with at least one DEPOSIT transaction. Buy orders without a new cash deposit do not count. Months Any month passes with no deposit recorded (3-day grace into the next month) No
Active days Total count of distinct calendar days on which any transaction (Buy, Sell, Deposit, Dividend) was recorded. This is a proxy for portfolio engagement — more active portfolio management accumulates faster. Days Does not reset — it is a cumulative total, not a consecutive run No
Withdrawal-free streak Consecutive calendar months without any WITHDRAWAL transaction. Sell orders are not counted — reinvesting proceeds is a valid decision. Months Any WITHDRAWAL transaction is recorded No
Goal streak Consecutive months where total deposits meet or exceed your self-set monthly deposit goal. Changing your goal mid-streak does not reset the counter. Months Any month where deposits fall below the goal Yes — set monthly goal
Green months Consecutive calendar months where your portfolio's market performance is positive — calculated before any new deposits, so it isolates market return from your contributions. Months Any month ends with a negative market return No
Beat benchmark [planned] Consecutive months where your portfolio's total return exceeds your chosen benchmark (WIG20, MSCI World, S&P 500). Requires benchmark data integration — not yet available in the current build. Months Any month your return trails the benchmark Yes — enable benchmark

Why only DEPOSIT for the contribution streak, not BUY? Buying with cash that's already in your portfolio is not new capital entering the system. The streak is designed to reward the habit of adding fresh money each month. Moving existing cash from one holding to another doesn't count.

Milestones

Milestones are one-time achievements. When you cross one, a badge unlocks and a roast fires. They never expire and can only be earned once each. The Achievements screen shows four separate milestone timelines — Portfolio value, Investor journey, Monthly deposit best, and FIRE progress (if a retirement plan is configured). Each timeline shows past milestones with the date reached, the next target with a live progress bar, and future milestones muted below.

Portfolio value

Triggered when your total portfolio value (all portfolios combined) crosses a threshold for the first time. The badge is awarded on first crossing only — if the market dips below and recovers, no second badge. XP from each threshold is awarded cumulatively: a 150 000 PLN portfolio earns XP for all thresholds below it.

Value (PLN)Roastfolio says…
1 000"It begins. Technically you're an investor now."
10 000"Five figures. A threshold that matters."
50 000"Halfway to six figures. The market will try to undo this. Don't let it."
100 000"Six figures. This is the number that changes behavior. Guard it."
250 000"A quarter million. At this point your money has a support group."
500 000"Half a million. The math is doing most of the work now."
1 000 000"One million. You either DCA'd for 30 years or got very lucky."
2 000 000"Two million. The returns alone are someone's annual salary."
5 000 000"Five million. The portfolio manages you more than you manage it."

Investor journey (from first investment date)

Time-based milestones counted from the date of your first ever recorded transaction — not account creation. The clock starts when money moves.

Since first investmentMilestone
3 months"Still here. Initial excitement survived."
6 months"Half a year. One market wobble behind you, probably."
1 year"One full year as an investor. All four seasons of market behavior."
2 years"The 'long term' is no longer hypothetical."
5 years"You're a different investor than when you started."
10 years"A decade. Compounding has had time to become your co-pilot."

Monthly deposit personal best

Unlocked the first time you deposit above a threshold in a single calendar month — rewards your biggest months, not just consistency.

Thresholds: 1 000 / 2 500 / 5 000 / 10 000 / 25 000 / 50 000 / 100 000 PLN in one month.

Retirement progress

Triggered at 10 / 25 / 50 / 75 / 90 / 100% of your FIRE target. Only active if you have a retirement plan configured. See Setup & configuration for details.

Badges & tiers

Every milestone and streak achievement unlocks a badge. Badges come in five tiers — rarity reflects how long or how difficult the achievement is to earn, not whether it's valuable. A Seed badge for your first deposit matters.

TierWhat it representsVisual
Seed Participation and first steps. Nearly everyone earns these early. Gray border, muted glow
Bronze Early consistency — showing up for a few months, hitting initial thresholds. Amber border
Silver Sustained effort — a full year of something, meaningful portfolio thresholds. Steel blue border
Gold Genuine milestones — six-figure portfolio, multi-year streaks, survival through bad markets. Yellow border, slow pulse glow
Diamond Rare, long-term, legendary. Five-year streaks, seven-figure portfolio, full trading year of check-ins. Cyan border, continuous shimmer pulse

In the achievements screen, locked badges appear as silhouettes at reduced opacity. Tapping a locked badge shows exactly what it requires and how close you are. Nothing is hidden — you always know what's next.

Retroactive awards: When the achievements system launches for your account, all badges you've already earned through past transactions are awarded immediately and silently. You'll see a "New" indicator in the achievements drawer and a one-time message: "We went through your history. Turns out you've been building this longer than you realized."

XP & Level Calculation

XP (experience points) are derived entirely from real transaction and portfolio snapshot data. There are no artificial engagement loops — you earn XP for financially meaningful actions only. The engine (xp-engine.js) recomputes automatically every time transaction history or portfolio prices are refreshed.

No manual entry. XP is never entered by hand. If a transaction is deleted, the XP it generated disappears on the next recalculation. The number is always consistent with your actual history.

Storage decision — why XP is not stored in the database. XP, levels, badges, milestones, and streaks are computed client-side on every load from transaction history already fetched from the API. Storing computed values in DynamoDB would create a stale-data sync problem every time calculation rules change. Persisted between sessions: streak personal bests (localStorage — they only ever increase), monthly deposit goal (localStorage key xpe_deposit_goal), and FIRE target (localStorage key xpe_fire_target, refreshed from the retirement plans API). Because computation is deterministic and fast (<10ms), no server-side caching is needed. Existing and new users get a full retroactive recalculation from their complete transaction history automatically on every load.

XP rule table

CategoryActionXP awarded
DepositsPer deposit event+15
Amount ≥ 500 PLN+5 bonus
Amount ≥ 1 000 PLN+15 bonus
Amount ≥ 5 000 PLN+30 bonus
Amount ≥ 10 000 PLN+60 bonus
ConsistencyPer calendar month with ≥1 deposit+20
GoalPer month with total deposits ≥ 1 000 PLN+10 bonus
IncomePer dividend received+15
TradingFirst sell transaction+20
Each additional sell+5
Portfolio milestonesPortfolio ≥ 1 000 PLN+50
Portfolio ≥ 10 000 PLN+100
Portfolio ≥ 50 000 PLN+200
Portfolio ≥ 100 000 PLN+400
Portfolio ≥ 250 000 PLN+600
Portfolio ≥ 500 000 PLN+1 000
Diversification3+ unique holdings bought+30
5+ unique holdings bought+50
10+ unique holdings bought+80
RetirementRetirement plan configured+50
BadgesPer badge unlocked+50

Deposit bonuses are tiered, not cumulative. A 1 200 PLN deposit earns base (+15) plus the ≥1 000 PLN tier (+15) — the +5 (≥500) tier is superseded. Diversification bonuses are cumulative: reaching 10 holdings awards all three tiers (+30 + +50 + +80 = +160 total). Portfolio milestone bonuses are also cumulative — a 150 000 PLN portfolio earns +50 + +100 + +200 + +400 = 750 XP for all four crossed thresholds.

Level thresholds

Level nameXP required (total)
Seed0
Observer100
Participant250
Saver500
Contributor1 000
Operator2 500
Veteran5 000
Institution10 000

Streak calculation

Streaks are computed from transaction history every time data refreshes. Each streak also persists its personal best in localStorage so the all-time high is never lost if a streak resets.

StreakHow it’s calculated
Deposit streak Consecutive calendar months (YYYY-MM) ending at the most recent month that contain at least one deposit. A month with zero deposits breaks the streak.
Active days Total count of distinct calendar days on which any transaction (buy, sell, deposit, dividend) occurred. Used as a proxy for engagement frequency.
Withdrawal-free Number of calendar months between the date after the last withdrawal and today. If no withdrawal has ever been recorded, the streak counts from the first deposit date.
Goal streak Consecutive calendar months (most recent first) where the sum of all deposits equals or exceeds the monthly goal (default: 1 000 PLN across all portfolios).
Green months Consecutive calendar months where the net portfolio return — (end value − start value) − (new capital deposited) — is positive. Requires portfolio snapshot history.

Badge derivation

Each badge is evaluated against a specific condition derived from real data. Below is the full derivation logic for every badge in the system.

BadgeUnlocks when…
Showing UpAt least one deposit exists in transaction history
ReliableDeposit months total ≥ 3 (deposit streak or cumulative)
Eyes OpenDistinct transaction-active days ≥ 7
Hands OffWithdrawal-free streak ≥ 6 months and no withdrawal ever recorded
Four DigitsHistorical peak portfolio value ≥ 1 000 PLN
Getting SeriousHistorical peak portfolio value ≥ 10 000 PLN
Not One BasketUnique tickers bought ≥ 3
Actually DiversifiedUnique tickers bought ≥ 5
Baptism by FirePortfolio saw a ≥10% drawdown from running peak and no withdrawal was ever recorded
FIRE CuriousAt least one retirement plan saved (checked via RetirementPlansClient, cached in localStorage)
Red Day SurvivorPortfolio snapshot history shows a ≥10% drawdown, or snapshot history exists (proxy: you have seen red days)
Sold SomethingAt least one Sell transaction exists
CommittedGoal streak ≥ 3 consecutive months
The Long GameDeposit months ≥ 12
Weekly HabitDistinct active days ≥ 90
UntouchedWithdrawal-free streak ≥ 36 months
On TrackGoal streak ≥ 12 months
Six FiguresHistorical peak portfolio value ≥ 100 000 PLN
Held the LinePortfolio saw a ≥20% drawdown and no withdrawal ever recorded
World CitizenTransactions span ≥3 country/asset-class proxies (Polish .WA tickers, foreign tickers, crypto)
Tax EfficientTransactions exist in both an IKE and an IKZE wallet
InstitutionalizedDeposit months ≥ 36
Comma ClubHistorical peak portfolio value ≥ 1 000 000 PLN

Data required: Most badges only need transaction history. Green months, drawdown badges (Baptism by Fire, Held the Line, Red Day Survivor), and portfolio milestone badges additionally require portfolio snapshot data — available once the app has loaded at least one price update.

Most streaks and milestones require no setup — they're computed automatically. Two features need configuration to unlock their respective achievements:

🎯
Monthly deposit goal
Call XPEngine.setDepositGoal(amount) in the browser console, or set localStorage.xpe_deposit_goal

Set a target amount (in PLN) you want to deposit each month. This activates the Goal streak counter and the monthly goal bonus XP. The goal applies to your total deposits across all portfolios in a calendar month — not per individual portfolio. Default is 1 000 PLN.

Changing your goal at any time does not reset your streak. The system records that you had a goal and hit it; what the goal was is secondary to whether you met it.

🔥
Retirement plan (FIRE target)
Retirement → Configure plan

Set a target portfolio value you want to reach before retirement. This activates the retirement progress milestones (10% / 25% / 50% / 75% / 90% / 100%) and the corresponding badges. The FIRE progress is measured as: total current value of all portfolios divided by your retirement target.

IKE and IKZE utilization tracking is independent of your FIRE target — it looks at deposits into accounts tagged as IKE/IKZE and compares them to the annual statutory limits (updated each year).

Multiple retirement plans — how progress is calculated

Roastfolio allows you to create more than one retirement plan (e.g., a conservative scenario and an aggressive scenario with different target values). Here is how the achievements system handles that:

Design decision

One plan drives the achievement badges at a time. If you have multiple retirement plans, you designate one as your primary plan for achievements. Only this plan contributes to the FIRE progress milestones and retirement badges.

If no primary is designated, the system defaults to the most recently modified plan.

ScenarioHow it's handled
Single retirement plan Used automatically. No action needed.
Multiple plans, one marked primary The primary plan drives all retirement badges and the FIRE progress bar.
Multiple plans, none marked primary The most recently modified plan is used. A soft prompt suggests marking one as primary.
IKE / IKZE utilization badges Always calculated from actual deposits into IKE- and IKZE-tagged accounts — independent of which retirement plan is primary. The annual limit comparison is against the statutory maximum regardless of your personal FIRE target.

The FIRE progress percentage is always calculated as:

/* Total value of ALL portfolios ÷ primary retirement plan target */
FIRE % = (sum of all portfolio values) / (primary plan target) × 100

This means your full wealth counts toward the goal, not just the assets in "retirement-tagged" portfolios. Rationale: when you retire, all your money is relevant — artificially restricting the calculation to specific portfolios would understate your actual progress.

Monthly deposit goal — similar rule applies: The goal streak measures total deposits across all portfolios in a calendar month. If you set a goal of 1 000 PLN/month and deposit 400 PLN into IKE and 700 PLN into XTB, the streak counts it as 1 100 PLN — goal met. The system doesn't care which portfolio received the money.

🎨 Design System

This section documents the design decisions that define Roastfolio's visual identity. It is updated whenever a significant UI change is made so the reasoning behind each decision is never lost.

Audience: This section is for ADVANCED users and contributors who want to understand why the app looks and behaves the way it does — not just what it looks like.

Design Philosophy

Roastfolio sits at the intersection of premium fintech and internet humor. Every design decision asks two questions: does this feel like it belongs next to Revolut? and does this feel like it was made by someone who lost money on CDR and laughed about it?

The design language is dark-native (not dark-mode as an afterthought), card-first, and number-obsessed — financial data is the hero, personality is the seasoning.

The tone lives in copy, not chrome. Roastfolio's sarcasm comes from error messages, milestone notifications, and empty states. The UI itself is as premium as Revolut. Users need to trust the app with financial data — humor and visual roughness don't mix well in fintech.

Design inspiration: Revolut · Robinhood · Coinbase · TradingView · Apple Fitness · Linear

Color Palette

Background depth layers

bg-base
#090f18
Page background — deepest layer
bg-surface
#0e1a2a
Cards, panels
bg-surface-raised
#132032
Elevated cards, overlays
bg-surface-strong
#1a2c42
Interactive surfaces, hover states

Brand accent

accent
#2b88cf
Primary brand blue — buttons, links, active states
accent-strong
#36a3f0
Hover, active, gradient end

This specific blue — not Robinhood green, not Coinbase deep blue — lands in "trustworthy retail bank that's cooler than your actual bank." Saturated enough to feel premium, desaturated enough to not feel crypto-scammy. Green is avoided as a brand accent because it is semantically loaded in finance (it means profit).

Semantic colors

success
#27ae60
Profit, positive P/L, confirmations
danger
#c0392b
Loss, negative P/L, destructive actions
warning
#e67e22
Pending states, caution
neutral
#7fa4c4
Flat / no-change states

Text hierarchy

text-primary
#e8f0f8
Headlines, financial values — blue-tinted white (avoids harsh pure white on OLED)
text-secondary
#a8c0d8
Labels, captions
text-muted
#6a8ba8
Metadata, hints, secondary info

Typography

System font stack — ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif. On iPhone, this resolves to SF Pro; on macOS, SF Pro or Helvetica Neue; on Android, Roboto; on Windows, Segoe UI. Zero network cost, perfect rendering, device-tuned letter spacing.

-webkit-font-smoothing: antialiased and text-rendering: optimizeLegibility are applied globally so text appears sharp on retina and ProMotion displays. font-variant-numeric: tabular-nums is applied to all financial figures — keeping column alignment stable and preventing number-jank during live value updates.

143,240.00
display · 700 · −0.04em · portfolio total
Wallet Management
h1 · 700 · screen titles
Holdings
h2 · 700 · card titles
CD Projekt — 10 units
body · 15px · primary content
CDR.WA · Live holding
label · 13px · metadata, captions
Select wallet
micro · 11px · all-caps labels

Spacing & Grid

4px base unit — matches the iOS and Android native layout grid. Every spacing value is a multiple of 4, which means composed layouts snap to device pixels and feel native rather than web-ported. At 3× pixel density (iPhone 15 Pro), a 4px CSS unit is 12 physical pixels — sharper-than-retina rendering for borders and shadows.

4px — space-1 · micro gap
8px — space-2 · tight gap
12px — space-3 · compact padding
16px — space-4 · base card padding / screen gutter
24px — space-6 · section gaps
32px — space-8 · large section separation
44px — tap-target · iOS minimum touch target (HIG)

Card System

Cards are the primary content container. No raw tables on mobile, no naked lists. Each card uses border-radius: 16px (the Revolut/Apple standard for modern cards), a 1px semi-transparent blue-tinted border, and an inset top-shadow that simulates light catching the upper rim — borrowed from Apple's glass design language.

🃏
Base card
#0e1a2a background · 1px border at 12% opacity · inset 0 1px 0 rgba(255,255,255,0.04) top edge highlight
⬆️
Elevated card
Raised surface #132032 + drop shadow · used for overlays and action panels
👆
Interactive card
scale(0.992) on tap — registers as "responded" without feeling like a button press. Same micro-interaction as Apple app icons.
📊
Holding card
2-row grid: identity + value on top, units · allocation + Buy/Sell on footer. Footer separated by 1px border at 6% opacity.

Button System

VariantUsage ruleKey traits
Primary One per screen. High-value action (Sign in, Save, Confirm trade). Gradient accent → accent-strong + colored box-shadow. 44px height. Scale 0.97 on press.
Secondary Supporting action alongside primary. Ghost with 1px border. 44px height.
Ghost Tertiary or destructive context. No border, no background. Danger variant turns red.
FAB One per page. Always-visible primary mobile action (New Transaction). Fixed position above tab bar. bottom: calc(env(safe-area-inset-bottom) + 72px). Pill shape. Accent gradient + glow shadow.
Buy / Sell chips Inline on holding cards. 32px height. Green/red tinted background + border. 12px bold text.

Why gradient on primary? A flat background on a primary button reads as a toggle. The gradient + shadow creates visual mass that communicates "this is the action" without relying on color alone.

Bottom Sheets & Modals

Bottom sheets are the correct modal pattern for mobile. They emerge from the direction of the thumb, they don't block the full screen (the user still knows where they are), they match iOS/Android native sheet behavior exactly, and they're trivially dismissible (tap backdrop or swipe down).

📐
Max height: 92dvh
dvh (dynamic viewport height) instead of vh fixes the iOS Safari address-bar bug where 100vh includes hidden browser chrome.
🌀
Spring entry curve
cubic-bezier(0.32, 0.72, 0, 1) — Apple's spring-curve approximation for sheet presentations. Creates natural deceleration that reads as physical.
🫧
Backdrop blur
backdrop-filter: blur(4px) on the overlay so the user retains spatial context. Semi-transparent dark overlay at 82% opacity.
📱
Safe area aware
Bottom padding uses env(safe-area-inset-bottom) so content isn't hidden under iPhone home indicator.

Responsive Breakpoints

Mobile-first: no media query = mobile base. There is no classical 768px tablet breakpoint. The app jumps from phone (≤640px) to desktop (≥641px). Tablet-specific layouts add code for an audience that doesn't exist in personal finance apps — retail investors use iPhones and laptops, not iPads for portfolio tracking.

≤374px
Compact mobile
iPhone SE — reduce horizontal padding to 12px
375–519px
Standard mobile
Base layout — iPhone 14/15 Pro. All decisions start here.
≥520px
Large mobile
Live-data status labels expand from short form to full text
≥641px
Desktop
Wallet layout switches: chip strip → dropdown, FAB hidden, overview card shown, holdings table shown
≥860px
Wide desktop
Wallet selector stays compact; wider layout unlocks
≥1024px
Full desktop
Screen gutters expand to 24px; two-column layouts available

CSS Design Tokens

All design values live as CSS custom properties on :root. Component CSS never uses raw values — always tokens. This means a brand color change is a one-line edit, not a search-and-replace.

/* ── Background depth ── */
--fintech-bg:             #090f18;
--fintech-surface:        #0e1a2a;
--fintech-surface-raised: #132032;
--fintech-surface-strong: #1a2c42;

/* ── Brand ── */
--fintech-accent:         #2b88cf;
--fintech-accent-strong:  #36a3f0;

/* ── Semantic ── */
--fintech-success:        #27ae60;
--fintech-danger:         #c0392b;
--fintech-warning:        #e67e22;

/* ── Typography ── */
--fintech-text:           #e8f0f8;
--fintech-text-muted:     #6a8ba8;
--fintech-text-subtle:    #7fa4c4;

/* ── Shape ── */
--fintech-radius-sm:      8px;
--fintech-radius-md:      12px;
--fintech-radius-lg:      16px;  /* cards */
--fintech-radius-xl:      20px;  /* bottom sheets */

/* ── Shadow ── */
--fintech-shadow-sm:      0 1px 4px rgba(0,0,0,0.3);
--fintech-shadow-md:      0 4px 16px rgba(0,0,0,0.35);
--fintech-shadow-accent:  0 8px 24px rgba(43,136,207,0.4);
--fintech-shadow-inset:   inset 0 1px 0 rgba(255,255,255,0.04);

/* ── Borders ── */
--fintech-border:         rgba(74,159,212,0.12);
--fintech-border-strong:  rgba(74,159,212,0.22);

/* ── Spacing ── */
--fintech-space-1: 4px;  --fintech-space-2: 8px;
--fintech-space-3: 12px; --fintech-space-4: 16px;
--fintech-space-6: 24px; --fintech-space-8: 32px;

Motion & Animation

Motion communicates confidence. Physical-feeling animations tell the user the UI is responsive and stable. CSS default easing feels like something broke. iOS-native easing feels like money is being handled correctly.

spring
cubic-bezier(0.32,0.72,0,1)
Apple's sheet presentation curve. Used for bottom drawer entry. Decelerates sharply — reads as physical weight arriving.
snappy
cubic-bezier(0.2,0,0,1)
Fast but smooth. Used for chip transitions, card state changes. Responds instantly to touch.
duration-fast · 120ms
Button press, chip active state, micro-interactions. Anything under the threshold of "I notice a delay."
duration-base · 200ms
Card hover, overlay backdrop fade. Standard transition speed.
duration-slow · 300ms
Bottom sheet entry/exit. Long enough to feel intentional, short enough to not feel sluggish.
scale-press · 0.992
Applied to tappable cards on :active. Under 1% scale reduction — registers as "responded" without feeling like a button.

Never animate layout properties (width, height, top, left, margin). Animate only transform, opacity, and filter. Layout animations trigger reflow and cause jank on mobile — especially visible during scroll on iPhones with ProMotion displays.

Mobile UX Quality Checklist

A reference of intentional decisions that separate native-feel PWAs from desktop-ports-on-mobile. Every item below is implemented in the current build.

CategoryDecisionWhy it matters
Font rendering-webkit-font-smoothing: antialiased + text-rendering: optimizeLegibility on bodyText looks razor-sharp on retina and ProMotion screens. The difference is visible even to non-designers.
Font stackui-sans-serif, system-ui, -apple-system — full system chainSF Pro on iOS, Roboto on Android, Segoe UI on Windows. Zero loading cost, native letter-spacing and ligatures per device.
Responsive titleclamp(1.4rem, 5.5vw, 2rem) on the brand titlePrevents the header from crowding on 320px iPhone SE and smaller Android devices.
Tab navigationSVG icons instead of emoji in the bottom tab barEmoji renders inconsistently across Android, Windows, and older iOS. SVG icons are crisp at any DPI, visually consistent, and brand-controlled.
Touch targetsBottom tab bar buttons fill the full width ÷ 7; FAB is 48×48px minimumApple HIG minimum: 44×44pt. Smaller targets cause mis-taps on moving transit.
Viewport zoomviewport-fit=cover without maximum-scale=1.0Removing maximum-scale=1.0 restores accessibility zoom for users with low vision. Safari since iOS 10 handles input zoom prevention natively.
Card radiusDashboard cards use border-radius: 18px, reducing to 14px on mobileMatches iOS native card radius (introduced in iOS 14). Radiused corners read as modern, safe, and touchable.
Card shadows0 4px 24px rgba(31,95,159,0.10), 0 1px 4px rgba(31,95,159,0.06)Dual-layer shadow creates depth. The tight inner shadow catches ambient light; the wide outer shadow grounds the card. Matches Revolut and Apple Card UI.
Safe-area insetsenv(safe-area-inset-*) on header padding, FAB bottom, tab bar bottomPrevents content from sliding under the Dynamic Island, notch, or home indicator on all iPhone generations from X onward.
Overlay scrolloverscroll-behavior: contain on overlay panelsPrevents bottom-sheet scroll from rubber-banding the page behind it on iOS Safari. Eliminates one of the most jarring mobile web artefacts.
Tap highlight-webkit-tap-highlight-color: transparent on all interactive elementsRemoves the blue flash on tap. iOS apps don't have it; web apps shouldn't either.
Touch actiontouch-action: manipulation on all buttonsEliminates 300ms tap delay on touch devices without disabling scroll. Buttons respond the instant the finger lifts.
Inline stylesTransactions tab layout extracted to .tx-summary-bar, .tx-activity-card, .tx-history-cardInline styles skip the design-token system and dark-mode overrides. Semantic class names allow responsive and theme overrides.
Tab iconsStatistics headings use plain text, not emoji prefixesEmoji in headings breaks screen readers, renders inconsistently between platforms, and undermines the premium brand tone.

Ongoing rule: Every new section of UI must pass the question: does this feel like a native iOS finance app, or like a website that happens to be on a phone? If the answer is the latter, the section is not finished.

Gamification UX Audit — Applied Fixes

A senior UX audit was applied across the achievements, streaks, milestones, and engagement strip pages. The following issues were identified and fixed.

FindingFix appliedRule
Viewport zoom blocked on achievements pageRemoved maximum-scale=1.0 from achievements.html viewport metaAccessibility — users with low vision must be able to zoom. Apply to all pages, not just index.
Back button was 36×36px.ach-back-btn enlarged to 44×44px, added touch-action: manipulation + :active stateApple HIG: minimum 44pt touch target on all interactive controls.
Filter tabs height ~30px.ach-filter-btn padding changed to 9px 14px, min-height: 40px added, added inset box-shadow on active stateSegmented control buttons must be ≥40px tall on mobile. The active state should be visually distinct beyond background opacity alone.
Badge press scale too aggressive (0.93)Changed .ach-badge:active { transform: scale(0.97) }. Hover only activates on hover: hover devices.Industry standard for card press-feedback is 0.96–0.97. 0.93 feels like a crush, not a tap. Desktop-only hover effects must not fire on touch devices (@media (hover: hover)).
Locked badges opacity 0.3 — nearly invisibleIncreased to opacity: 0.45; filter: grayscale(0.6)Users should be able to see and identify locked badges so they have something to aim for. Apple Health and Strava use 0.4–0.5 opacity for locked achievements.
Badge names 10.5px, tier labels 9pxBadge names → 11px, tier labels → 10px11px is the minimum comfortable legibility on iPhone at arm's length. Font sizes should be on the 4px design grid (8, 10, 11, 12, 13, 14, 15, 16…).
Badge progress bar border-radius: 0Changed track + fill to border-radius: 3px, height increased from 3px to 4pxEvery other progress bar in the codebase has rounded ends. An angular progress bar is a visual inconsistency that breaks the premium feel. 4px height is the minimum thumb-visible size.
Streak card hover causes layout shift on touchRemoved transform: translateY(-2px) from hover. Moved hover rule inside @media (hover: hover). Reduced min-width from 122px to 118px.CSS hover states fire as ghost-hover on iOS Safari after a tap, causing the card to jump. Touch-safe hover requires the hover: hover media query guard.
Streak scroll has no right-edge affordanceAdded mask-image fade on the right edge of .ach-streaks-scroll. Added overscroll-behavior-x: contain.Horizontal scrollers must have a visual indicator that more content exists (partial card peek or gradient fade). Without it, users assume all cards are visible.
Milestone roast hidden on small screensChanged from display: none to -webkit-line-clamp: 2; text-align: left at 520pxThe sarcastic roast is Roastfolio's core personality differentiator. Hiding it entirely on the most common screen size (375px iPhone) removes the key engagement hook.
achFadeUp translateY(18px) too dramatic for inline cardsReduced to translateY(10px)Full-page hero entrances use 16–24px. Inline card entrances should use 8–12px. Over-travel reads as heavy and slow on mobile.
Infinite animations drain battery on mobileAdded @media (prefers-reduced-motion: reduce) block disabling achDiamondPulse, achGoldShimmer, achNodePulse and all entrance transitionsContinuous animations prevent the GPU from entering low-power mode on OLED devices. System preference must always be respected per WCAG 2.3.
aria-atomic="true" on badge gridRemoved aria-atomic="true" from #ach-badges-gridWith atomic=true, every filter change would read the entire badge grid (22+ items) to screen reader users. Without it, only the changed nodes are announced.
Engagement strip horizontal scroll bleeds into page scroll on iOSAdded overscroll-behavior-x: contain to .eng-strip. Reduced animation from 0.28s to 0.22s.Without overscroll containment, reaching the end of a horizontal strip on iOS triggers page rubber-band scroll, breaking the sense of two separate scroll axes.
Milestone timeline connector barely visibleIncreased connector opacity from 0.18 → 0.25 (active) / 0.07 → 0.10 (faded)The timeline metaphor depends on the connector line being clearly readable. 0.18 opacity on a dark surface is below perceptible threshold on many screens.