The frontend is server-rendered Jinja plus a fixed vocabulary of tokens and components. No build step, no CDN, no framework. Pages compose .fbb-* classes; they don't restyle ad-hoc. Rebrand once at the token level and it propagates everywhere.
Every colour, space, radius, and font is a CSS custom property under :root. Override the handful that define your brand and the change reaches every button, card, and layout at once — you don't touch component rules.
:root {
--fbb-accent: #6c5ce7; /* primary action, links, focus ring */
--fbb-accent-hover: #5a4bd4;
--fbb-ember: #e8603c; /* the "forged" secondary accent */
--fbb-bg: #0f1115; /* app background */
--fbb-surface: #171a21; /* card / panel surface */
--fbb-text: #e7e9ee;
--fbb-radius: 12px; /* global corner rounding */
--fbb-font-display: "Syne", system-ui, sans-serif;
--fbb-font-body: "DM Sans", system-ui, sans-serif;
}There are spacing tokens (--fbb-space-1 … --fbb-space-9), type-scale tokens (--fbb-text-base … --fbb-text-4xl), and motion tokens (--fbb-dur, --fbb-ease). Compose with the tokens; don't hardcode pixels in a page.
Build pages out of the ready classes. A few you'll reach for constantly:
.fbb-btn + a variant (-primary · -ember · -ghost) and size (-sm · -lg · -block) — buttons.
.fbb-card · .fbb-card-forged — panels; the forged variant is the accented hero card.
.fbb-field · .fbb-label · .fbb-input · .fbb-select · .fbb-textarea — form controls.
.fbb-badge (+ -success · -warning · -danger), .fbb-stack · .fbb-cluster · .fbb-grid-2/-3 — status pills and layout primitives.
.fbb-icon + <use href="…#name"> — an icon from the sprite, inheriting colour and stroke.
One global object carries the interactive pieces. After you inject HTML dynamically, call fabbro.init(root) to (re)bind widgets inside it.
// A toast
fabbro.toast("Saved.", { kind: "success" });
// A CSRF-aware fetch — sends the token and Accept: application/json,
// so an expired session returns 401 JSON, not a login-page 200.
const res = await fabbro.request("/notes", {
method: "POST",
body: JSON.stringify({ body: "hello" }),
});
// A modal, a loading veil, the theme + sidebar controllers
fabbro.modal({ title: "Confirm", body: "…" });
fabbro.loading(true);
fabbro.theme; fabbro.sidebar;Use fabbro.request rather than raw fetch for anything that mutates: it attaches the CSRF token from the <meta name="csrf-token"> the layouts render, so you never wire CSRF on the client by hand.
A page never registers a layout — it inherits one with {% extends %} and fills the blocks. Each wires the security contract (CSRF field, CSP nonce, no-flash theme) once, so pages get it free.
layout_auth.html — full-screen centered card. Login, register, forgot, set-password.
layout_public.html — institutional site: header, drawer, footer.
layout_app.html — logged-in app: retractable sidebar with a user card. New sidebar links go at its {# <fabbro:app_nav_links> #} anchor (admin-only links at app_nav_admin).
{% extends "layout_app.html" %}
{% block title %}Notes{% endblock %}
{% block content %}
<h1 class="fbb-h1">Notes</h1>
{% for n in notes %}
<div class="fbb-card">{{ n.body }}</div>
{% endfor %}
{% endblock %}Jinja autoescapes user text; on the JS side build rows with textContent, never innerHTML, so an email stays text on both sides.