Design system
Fyro · internal apps

One look for every Fyro app

Tokens, type, number rules and base components, shared by every internal tool. Link two stylesheets and a page looks like the rest of the suite, in light and dark. Each component maps to a shadcn/ui primitive, so the React apps and the plain-HTML apps match.

Geist / Geist Mono HSL CSS variables shadcn/ui compatible Light + dark EN / IT

Get started

Pick the path that matches the app. Both read the same tokens, so they match.

Any app: PHP, plain HTML, server-rendered

Put this in <head>, then use the fy- classes below.

<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@300..700&family=Geist+Mono:wght@400..600&display=swap">
<link rel="stylesheet" href="https://design.fyroadv.com/css/fyro-tokens.css">
<link rel="stylesheet" href="https://design.fyroadv.com/css/fyro.css">
<link rel="icon" href="https://design.fyroadv.com/img/favicon.svg" type="image/svg+xml">
<script src="https://design.fyroadv.com/js/fyro.js"></script>
<meta name="robots" content="noindex, nofollow">

Numbers and dates go through one formatter. Never use toFixed or number_format on an amount:

<script type="module">
  import { formatMoney, formatDelta } from "https://design.fyroadv.com/js/fyro-format.js";
  formatMoney(-8420, "EUR");                // −€ 8,420.00
  formatMoney(1234.56, "EUR", { locale: "it" }); // 1.234,56 €
</script>
React + Vite + Tailwind + shadcn/ui

The reference stack. No new libraries; lucide-react for icons, Recharts for charts.

  1. Replace src/index.css with kit/tokens/index.css.
  2. Merge tailwind.config.patch.md into theme.extend, removing nothing.
  3. Add the fonts and meta from fonts-index-html.md to index.html.
  4. Copy lib/format.ts and lib/fx.ts to src/lib/ with their tests.
  5. Copy components/: Segmented, KpiCard, Sparkline, pnl-structure.
  6. Replace every toFixed / toLocaleString on amounts with format.ts.
Browse the React kit
Colour
  • Every colour from a token. Hand-written HEX/HSL breaks dark mode.
  • No gradients. Primary violet is flat.
  • Green = good/incoming, red = loss/negative net, amber = needs checking.
Shape
  • Borders first; shadow only for things that float.
  • Top bar 56, filter bar 52, buttons 30–32. Touch targets ≥ 44 on mobile.
  • Page gutter 32 desktop / 16 mobile, section gap 24.
Content
  • UI in EN and IT; budget labels at +35%.
  • Database names (categories, items, P&L lines) are not translated.
  • Internal apps are noindex, no social meta.

01 — Brand

Wordmark in three sizes, plus a collapsed mark for the compressed sidebar. The product name after “Fyro” changes per app; it is always the muted half.

Fyro Finance Fyro Ledger collapsed sidebar mark
HTML
Fyro Finance

On dark the mark uses --primary at 68% lightness, so it keeps 4.5:1 against the app background.

mark.svg · mark-dark.svg · favicon.svg (follows OS theme)

02 — Colour tokens

Bare HSL triples on :root and .dark, used as hsl(var(--token)) or hsl(var(--token) / 0.5). Values below are read live from the stylesheet; switch the theme to see the dark set.

03 — P&L sections & categorical colour

One hue family for income, one for costs, neutral ink for subtotals. Tints stay faint (4% on light, 14% on dark), so a 26-column table never turns into a pastel quilt. Colour carries the sign; the label carries the meaning.

(+) Sales--pl-income
(−) Direct Costs--pl-cost-1
= Gross Direct Profit--pl-subtotal
(−) Operating Costs--pl-cost-2
= Gross Operating Profit--pl-subtotal
(−) Other Operating Costs--pl-cost-3
= EBITDA--pl-key
(+) Other Income--pl-income-2
(−) Additional Expenses--pl-cost-4
= Profit Net--pl-total
HTML

Categorical palette: 10 fixed slots

For cost centres, companies, any series. Assigned once, in order (alphabetical for companies, by amount for cost centres), and reused in every chart: same index, same colour, everywhere. Everything past ten is --cc-rest.

04 — Type, space, radius, elevation

Geist for the interface, Geist Mono for every figure. font-variant-numeric: tabular-nums is set on body, so columns align without extra classes.

.fy-display · 34/600
€ 482,610
.fy-kpi · 28/600
€ 74.2k
.fy-h1 · 22/600
Cash Flow — Monthly
.fy-title · 17/600
Page title in the top bar
.fy-h2 · 16/600
Operating Costs
body · 14/400
Default interface text and table labels.
.fy-num · 13/500 mono
1,234.56 · 98,410.00 · 1.086420
.fy-small · 12/400
Helper text, deltas, footnotes.
.fy-label · 11/600 caps
Column header
.fy-eyebrow · 10/600 caps
Sidebar section
Spacing: 4px base
4
8
12
16
24
32
48

Tokens --space-1…12. Card padding 24. Table cell 10/12. Page gutter 32 desktop, 16 mobile. Section gap 24.

Radius: --radius: 0.625rem
sm 6 · badge
md 8 · button
lg 10
xl 14 · card
Elevation: borders first, shadow only when floating
flat
sm
md · menu
lg · popover
xl · dialog

05 — Number formatting

One rule set for every app, implemented once: fyro-format.js for plain pages, format.ts for React. Both pass the same test cases. The figures below come from fyro-format.js on this page.

CaseENITRule
Multi-currency totals

Original amounts are never added as they are. A single figure exists only in the Total column, after conversion at the month’s average rate. Quarters and years are sums of already-converted months. Elsewhere: stacked, one line per currency, largest first.

€ 74,215.40$ 31,980.00£ 1,240.00
IT layout check

Labels are budgeted at +35%. Column headers wrap to two lines rather than truncate; nav items and buttons never truncate.

Gross Operating ProfitUtile operativo lordo Expenses by Cost CenterSpese per centro di costo
One screen, one language

Every label comes from the dictionary, including category, item and cost-centre names, which carry an en and an it field. Data with no translation falls back to the stored name and is flagged in Setup, not mixed silently into the page.

06 — Buttons

32px tall (30 for --sm), 8px radius, never truncated. One primary action per view.

HTML

Cards & KPI

KPI values are compact (formatMoneyCompact), with the exact value in a title tooltip. Deltas are signed, mono and toned by direction; for costs, going up is negative. The loading state keeps the same box and height.

RevenueEUR
€ 74.2k
+8.3%vs Feb+21.7%vs Mar 25
Profit NetEUR
−€ 4.1k
−132.4%vs Febn/avs Mar 25
EBITDA margin%
18.4%
−1.6 ppvs Feb
Loading
HTML
Card titleEUR · Mar 2026
Body. Card padding is 24 (16 on mobile). Cards have a border and no shadow.
HTML

Tables

38px rows. Sticky header and first column. Numeric cells are mono, right-aligned, with no vertical rules. Lines inside a (−) section show the magnitude only, since the header already carries the sign. Missing data is a muted em dash, and a real zero is printed in muted ink.

ItemEURUSD% Sales
(+) Sales74,215.4031,980.00100.0%
PayPal Sales41,208.0012,400.0055.5%
Stripe Sales34,890.4019,580.0047.0%
Refunds & disputes−1,883.00—−2.5%
(−) Direct Costs35,813.3015,870.0048.3%
ADV Cost35,813.3015,870.0048.3%
Stripe Fees0.000.000.0%
= Gross Direct Profit38,402.1016,110.0051.7%
HTML
DateDescriptionCategoryCompanyAmount
12 Mar 2026Meta Ads — invoice 2210AdvertisingFyro Ltd−€ 2,480.00
11 Mar 2026Stripe payoutStripe SalesFyro US Inc+$ 12,904.11
10 Mar 2026WISE*TRANSFER 88213UnmappedFyro Ltd€ 640.00
HTML

Forms & controls

Native elements, styled. Segmented controls are for small exclusive sets (period, currency, language, theme); fyro.js moves the selection and fires fy:change.

Total in
FX Mar 2026 · 1.086420
HTML
HTML
HTML

Badges & chips

Badges label a row's state. Chips are removable active filters. Three-letter currency codes appear in badges, headers and filters only, never attached to an amount.

Income Expense Unmapped EUR Intra-group Reconciled Failed New
Bank: Wise EUR Category: Advertising
HTML

Feedback & dialogs

Empty states say what is missing and offer the next step. Errors say what happened and what did not happen, with a retry and a copyable id. Destructive confirmations name the count and show what will change.

No transactions in Mar 2026

Import a CSV or add a transaction to start the month.

HTML
HTML
Rate estimated

No average for Mar 2026 yet; using Feb 2026 (1.080100).

3 items unmapped

They are excluded from the P&L until mapped in Setup.

Import complete

412 rows, 0 duplicates.

HTML
Delete 12 transactions?

They will be removed from every P&L and from the consolidated view. This can’t be undone.

Period
Mar 2026
Net amount
€ 14,208.00
HTML

App shell

Sidebar (collapsible to the mark), 56px top bar with the page title, 52px filter bar with the global controls, then the page. Below 768px the sidebar becomes a drawer opened by [data-fy-nav-toggle]. This page uses it.

Cash Flow — Monthly
FX 1.086420
Revenue
€ 74.2k
+8.3%MoM
Costs
€ 51.9k
+4.1%MoM
Profit Net
€ 22.3k
+1.2 ppmargin
HTML