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.
Get started
Pick the path that matches the app. Both read the same tokens, so they match.
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>
The reference stack. No new libraries; lucide-react for icons, Recharts for charts.
- Replace
src/index.csswith kit/tokens/index.css. - Merge tailwind.config.patch.md into
theme.extend, removing nothing. - Add the fonts and meta from fonts-index-html.md to
index.html. - Copy lib/format.ts and lib/fx.ts to
src/lib/with their tests. - Copy components/: Segmented, KpiCard, Sparkline, pnl-structure.
- Replace every
toFixed/toLocaleStringon amounts withformat.ts.
- 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.
- 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.
- 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.
HTML
On dark the mark uses --primary at 68% lightness, so it keeps 4.5:1 against the app background.
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.fy-kpi · 28/600.fy-h1 · 22/600.fy-title · 17/600.fy-h2 · 16/600body · 14/400.fy-num · 13/500 mono.fy-small · 12/400.fy-label · 11/600 caps.fy-eyebrow · 10/600 capsTokens --space-1…12. Card padding 24. Table cell 10/12. Page gutter 32 desktop, 16 mobile. Section gap 24.
--radius: 0.625rem05 — 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.
| Case | EN | IT | Rule |
|---|
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.
Labels are budgeted at +35%. Column headers wrap to two lines rather than truncate; nav items and buttons never truncate.
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.
HTML
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.
| Item | EUR | USD | % Sales |
|---|---|---|---|
| (+) Sales | 74,215.40 | 31,980.00 | 100.0% |
| PayPal Sales | 41,208.00 | 12,400.00 | 55.5% |
| Stripe Sales | 34,890.40 | 19,580.00 | 47.0% |
| Refunds & disputes | −1,883.00 | — | −2.5% |
| (−) Direct Costs | 35,813.30 | 15,870.00 | 48.3% |
| ADV Cost | 35,813.30 | 15,870.00 | 48.3% |
| Stripe Fees | 0.00 | 0.00 | 0.0% |
| = Gross Direct Profit | 38,402.10 | 16,110.00 | 51.7% |
HTML
| Date | Description | Category | Company | Amount |
|---|---|---|---|---|
| 12 Mar 2026 | Meta Ads — invoice 2210 | Advertising | Fyro Ltd | −€ 2,480.00 |
| 11 Mar 2026 | Stripe payout | Stripe Sales | Fyro US Inc | +$ 12,904.11 |
| 10 Mar 2026 | WISE*TRANSFER 88213 | Unmapped | Fyro 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.
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.
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.
Import a CSV or add a transaction to start the month.
HTML
The request timed out after 30s. Nothing was changed.
req_8f21c04a
HTML
No average for Mar 2026 yet; using Feb 2026 (1.080100).
They are excluded from the P&L until mapped in Setup.
412 rows, 0 duplicates.
HTML
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.