The Design System (Tailwind 4 + shadcn/ui)
Your brand as a web design system — tokens, base tags and stock shadcn components, branded by CSS alone.
Braaand hands a coding agent the brand and the agent builds the interface. That worked for colours and type, and stopped short of components: an agent handed tokens still has to write sixty of them, and every agent writes them differently. The brand was consistent; the output wasn't.
The UI kit closes that. Braaand ships no component code. Components come from upstream shadcn/ui and stay current with it; Braaand ships the theme that makes them yours. An untouched <Button> comes out on-brand.
Why it works without forking anything
Every shadcn primitive — and every sub-part — carries a data-slot attribute: data-slot="button", data-slot="card-title", data-slot="input". Combined with the CSS variables its components already read, that means a brand can restyle unforked upstream components from CSS alone.
So the kit is two layers:
1. Tokens. The full shadcn surface — --background, --foreground, --primary, --secondary, --muted, --accent, --destructive, --border, --input, --ring, --chart-1..5, --sidebar-* — in light and dark, plus --radius and the font stacks. All derived from the brand's colour roles, type styles, base styles and design tokens.
2. Identity CSS. Rules on the slots, for what a token cannot say.
That second layer is not a nicety. --radius gives cards, inputs and dialogs their corner — but a brand with pill buttons and 16px cards needs the button specifically pilled, not every dialog rounded into a stadium. Uppercase CTAs, letter-spacing and a display face on card titles have no token slot at all.
Where the values come from
Every token carries its provenance:
brand |
You stated it. Your accent became --primary. |
derived |
Computed from something you stated — --border is your background tinted toward your text. |
default |
shadcn's own value, kept because your brand says nothing about it. |
Your brand wins wherever it has an opinion; shadcn fills the gaps. Nothing is invented for you. On a typical brand about 44 of the 69 values come straight from the deposit, 20 are derived from it, and 4 or 5 stay shadcn's.
Two of those defaults are deliberate. --destructive is never derived from your accent — an error colour is a convention, not an identity, and a green brand still needs a red delete button. Chart colours come from your palette when you have one, and fall back to a monochrome ramp in your own hue rather than shadcn's unrelated five.
Brand → Design System shows all of this on real components, with the provenance table beside it.
Browsing and tuning it
Brand → the "…" menu → Design System opens the page: every installed shadcn component in the left rail, the selected one rendered big in the middle, and an inspector on the right.
The preview is a real page in an iframe, not a mockup — which is also why overlays behave
correctly. A dialog, dropdown or tooltip portals into document.body, so inside its own document
the brand theme is the document theme and nothing leaks into braaand's own chrome.
Each control in the inspector writes one property on one data-slot, and says where the current
value comes from: from your brand (a typeStyle or base style), from a convention (something
the website pass observed), or tuned here. Leave a field blank and shadcn's own value stays.
Changes save to the brand as you make them, and every surface derives from the same place — so a
tune here is in the next export_design_system, the next braaand sync, and the next
shadcn add @<brand>/kit. There is no separate design state to publish.
The Code tab shows exactly what your brand emits, which is what ships in ui/identity.css and
dist/ui.css.
The gallery opens on a composition — a dashboard, a settings screen, a pricing page — rather than a single component, because a radius or a heading face is hard to judge on one isolated control and obvious across a whole screen. The per-component specimens are still there below them, and selecting one brings up its slot controls.
Theme, radius and chart colours sit in the Design tab and change the preview as you move them; picking a theme previews exactly what that theme's export and registry emit. Tune this theme, beside the theme select, turns that view into an edit: tag, class, button, structure and link changes then land on that theme's style guide (over the brand's — a reset falls back to the brand's value), while tokens, faces and the UI kit stay on the brand. Reopening the page always lands on viewing.
Two whole-ladder controls sit on the Typography tab with nothing selected. Heading ladder picks one ratio (major second … golden) and derives h2–h6 from h1 by h1 ÷ ratioⁿ, floored at the body size — instead of the 4xl → base steps; a level with its own size keeps it. Mobile scale picks one ratio (×0.7 / ×0.8 / ×0.9) and every rem step of the type scale becomes a clamp() that shrinks toward it on small screens (360px → 1280px by default); px steps stay put. Both are web-only, like the type scale itself: no ad reads --brand-text-*, so none of these repaint anything — only spacing, corners, shadows and borders still wear the amber ads chip. Icon library and RTL live in the Project tab and are live too. Component style is the one setting that cannot be previewed — it decides which component source a project installs, so the preview keeps braaand's own.
What braaand configures
Beyond colours and type, a brand can state the project settings shadcn already has field names for. They live on uiKit.shadcn, spelled exactly as components.json spells them, and ride the kit as its registry config:
| Field | What it decides |
|---|---|
style |
Which component source a project installs — one of the 26 {radix|base|aria}-{vega|nova|maia|lyra|mira|luma|sera|rhea} values |
iconLibrary |
lucide, hugeicons or phosphor |
tailwind.baseColor |
The greys a newly added component falls back to before the theme lands |
menuColor, menuAccent |
Menu treatment |
rtl |
Right-to-left |
A brand that has set none of them ships no config at all, so a project keeps whatever it already had. aliases is not a field and can never be written: braaand says what a brand's components look like, never where a project keeps its files.
Tune them in the app under Brand → Design System → Project, or write uiKit.shadcn with update_brand_config.
The style guide — base tags and named classes
Beside the shadcn theme, the export carries the brand as a web document baseline: base.css styles the bare HTML tags (h1–h6 on the type scale in the heading face, p and small in the body face, links in the accent when it reads on the surface, block quotes, lists, inputs and the unclassed <button>), and utilities.css carries a closed set of named classes you can point at from any markup:
| Family | Classes |
|---|---|
| Headings on any tag | heading-style-h1 … heading-style-h6, subheading, eyebrow (the small line above a headline) |
| Text | `text-size-tiny |
| Colour | `text-color-text |
| Buttons | button, with is-secondary, is-tertiary, is-link, is-small, is-large, is-icon |
| Structure | padding-global, `padding-section-small |
| Radius + shadow | rounded-brand-* and shadow-brand-* — from the @theme aliases, so Tailwind's own shadow-sm is never redeclared |
How a button answers a hover is part of the guide. styleGuide.buttons.hover picks one of a closed set of recipes — tint (the default: the fill shifts toward the text), slide (a second fill slides in under the label, with a direction of travel), wipe (the same fill revealed edge to edge), lift (the control rises 2px and gains the brand's medium shadow), invert (fill and text swap), underline-grow (an underline grows out from the left) or none — and styleGuide.links.hover does the same for links (tint / underline-grow / none). The travelling fill and the label colour over it are per-variant colour picks (hoverFill / hoverText, defaulting to the text and surface roles). One emitter renders the recipe onto the style guide's .button and bare <button>, onto the shadcn Button ([data-slot="button"], in the kit's braaand layer) and, as the generic .brand-hover-slide / -wipe / -lift / -invert / -underline classes in motion.css, onto anything else — a card, a nav item — so the three can never disagree. Timing comes from the brand's motion vars; reduced motion snaps every recipe into its hovered state. Set it on the Design System page's button (Hover on the .button inspector, Hover on the a tag) or through update_brand_config.
The eyebrow is a class, and a button has a typeface. The small line above a headline is one class, .eyebrow, which every section wears. It resolves like a tag: left alone it is the body face, semibold, all caps, in the accent (or the brand's own eyebrow type style when it has one), and it is tuned like a tag on the Typography tab or through styleGuide.eyebrow — a mono kicker is { role: "caption", transform: "none" }. The button inspector carries Face and Size: styleGuide.buttons.role picks which of the brand's type styles the web button is set in, and scale its step on the type scale. Both are web-only. The ads' CTA keeps reading the cta type style, so a brand whose site buttons are set in a mono face gets them without moving a single ad.
Everything derives from the brand deposit — type styles, colour roles, the button shape, the design tokens — so a brand that has set nothing here already has a complete guide, and update_brand_config's styleGuide field is only the override layer on top. Two rules to know: in a Tailwind 4 project import the bundle's with-tailwind.css once — it imports Tailwind first and the brand second, which is the whole order rule, owned by the file (a project that already imports tailwindcss keeps that line and imports styles.css after it: the tag rules sit in the same base layer as Preflight and win only by coming later, and cascade layers order by first declaration, so no extra layer can make that rule disappear); and the bare a / button / input rules are guarded to unclassed elements, so a shadcn component never picks them up. The synced folder ships the same thing three ways — dist/style-guide.with-tailwind.css (the one import), dist/style-guide.tailwind.css for a project with its own Tailwind import, and dist/style-guide.css as plain CSS for any stack.
The house sizes. A brand that has set nothing still gets campaign-site proportions, tuned against a real petition page: body copy at 1.125rem (18px) with a 1.6 line height, an h1 at 4.5rem (72px), section padding of 3rem, 5rem and 7rem, and a page gutter of 5% of the width that never drops under 1.5rem or grows past 5rem. Buttons and form fields share one control height of 3rem (48px). It is a minimum, so a long label still grows its button; small and large buttons sit at 2.5rem and 3.5rem, and styleGuide.forms.height tunes all of them at once. On a narrow page, under 768px wide, h1 steps down to 60% of its size, h2 to 72%, and section padding to 60%. Each narrow rule is written twice: as a media query, for a real phone, and as a container query, so a page previewed at phone width inside a wider window (the site builder's phone view) answers the same way. Headings from h3 down never step down, because an h3 is as often a card's title as a page's. Your own settings still win: a spacing scale that sets lg, xl, 2xl or 3xl keeps those as the gutter and section padding, and a brand on fluid type is already shrinking with the screen, so its headings take no second step. None of it moves an ad. One thing to expect: the Design System page's preview is a real page too, so when its middle column is narrower than 768px, the headings you see there are the narrow ones.
Sections — the guide on real pages
A style guide proves itself on a page, not on a specimen. Design System → Sections is a library of Relume-style marketing sections — navbar, hero, header, event, event list, features, stats, process, timeline, CTA, banner, statement, contact, pricing, comparison, FAQ, testimonials, gallery, portfolio, logos, team, blog, blog post, long form, newsletter, link page, agera, cookie consent, footer — built from the guide's own vocabulary: bare tags and the named classes above (container-large, padding-section-large, heading-style-h2, subheading, text-size-small, button is-secondary, …), with shadcn primitives only where UI is real (an Accordion for the FAQ, Input / Textarea / Select / Checkbox for the contact form, a Card for pricing, an Input and a Button for the newsletter). Copy and pictures are named slots (<Copy k="headline"> / <Picture k="image"> from the sections' _fill.tsx helper): in the gallery they show their placeholders, and a page built on the library fills them through one SectionFillProvider per section — the same seam whether the fill is generated or typed. Pick one in the rail and it renders in your brand's guide; every look it has is a class you can click through to and change.
Agera campaign sections include one-question surveys (Agera 12, answer cards; Agera 13, an opinion scale), image polls (Agera 14, a picture grid; Agera 15, a split story and picture poll), a free-guide lead form (Agera 16), and volunteer recruitment (Agera 17). Edit the question, answer labels, pictures, contact copy and privacy link through their slots; the answer and interest lists also have editable counts. The controls are native HTML: radio choices are mutually exclusive, cards show selection and keyboard focus, and required contact fields and consent use browser validation. Text fields inherit the brand's form styling.
These forms need submission logic in the receiving site, like the existing Agera signups. Polls submit answer=choices.<index>; the answer label is the matching choices.<index> copy slot in surveys or choices.<index>.label in image polls. Volunteer forms submit one interests=interests.<index> entry per checked interest. All six submit firstName, email and consent=yes; volunteer signup also includes phone (optional) and postcode. Bind by section id when choosing campaign behavior: the Agera family now includes surveys and lead forms as well as petitions. Guide delivery, campaign storage and success states belong to that integration.
Every section has a number, and the number is its name. Like Relume's, a block is called by its family's word and a number — Hero 2, Gallery 5, Blog Post 1 — with the descriptive label ("Centered", "Masonry") beside it. Numbers are identity: assigned once, never reused and never renumbered, so a block you noted as "Event 3" is Event 3 next month and in every other brand. The block picker, the sitemap cards, the rail and the agent reads (name on every list_ui_sections row) all say the same name, and the picker's search treats a bare number as a number: 25 finds every family's 25th, header 25 finds one block.
The agency shapes. Twelve of the blocks are the shapes an editorial studio site is built from: a navbar that is only the mark and a row of links (Navbar 5), the brand's own mark set huge as the hero (Hero 9), a statement-size page title with one paragraph on its baseline (Header 5), a claim in the accent colour over a scatter of pictures (Statement 2), services as headline links (Feature 7) or as accordion rows with examples and the person who leads each (Feature 8), selected work and a full case list (Portfolio 5 and 6), a closing ask with a named contact (CTA 5), a team directory that grows to the size of the company (Team 6), a three-column contact block (Contact 5) and a footer with the mark along its bottom edge (Footer 5). Their pictures are square-cornered on purpose. The sections that show a person — the contact card, the directory, the service leads and the five older team layouts — bind to the brand's People.
An event's page and a case study are built by mixing blocks. An event page is Event 5 · Poster → Long form 4 · Text → Event 6 · Speakers → Event 7 · Facts → Event 8 · Signup. A case study alternates pictures and short text: Gallery 8 · Full bleed (the cover) → Portfolio 7 · Case intro → Stats 5 · Figures → then Long form 5 · Paragraphs and Gallery 9 · Plain / Gallery 10 · Duo in turn → Statement 4 · Accent line → a portfolio block for more cases. The picture blocks carry no title on purpose, so they can sit between two texts. An optional copy slot (optional: true in a section's slots: a caption, a speaker's tag) may be stored empty, and the line around it hides itself; an empty value in any other slot falls back to the placeholder.
Sections that move on scroll. Five blocks carry a scroll effect: Hero 10 and Statement 3 (the headline holds the screen while two layers of pictures rise past it at different speeds, one in front of the words and one behind), Gallery 6 (three columns drifting against each other), Gallery 7 (a long row sliding sideways while the section holds the screen) and Header 6 (a picture opening from a card to the whole screen while the words make way). The movement is CSS scroll-driven animation, shipped as the .brand-scroll-* classes in the brand's motion.css (in the export, the synced dist/ and the Polyther motion package): .brand-scroll-stage is the runway (--brand-scroll-length, 250svh) and .brand-scroll-pin its sticky child; .brand-scroll-rise travels through its authored position (--brand-scroll-travel), .brand-scroll-expand opens a picture to the whole pin, .brand-scroll-fade lets words make way and .brand-scroll-track slides a wide row to its end. No JavaScript and no client component, so the static HTML twin moves exactly like the React one. The authored layout is the rest pose: each block lays its still composition out with ordinary utilities, and without motion.css, without browser support for animation-timeline, under reduced motion, or inside an ancestor carrying data-scroll-still, it is simply that still. The site builder's canvas and the Sections tab show the still; the full-size page preview and a built site move. An embedder that cannot scroll the page (an editor canvas, an auto-height iframe) should stamp data-scroll-still on its wrapper.
Whole pages, and the surfaces they sit on
The rail leads with Pages — five whole one-pagers composed from the library: a product launch, a campaign, an event, an agency site and a studio site. Pick one and the sections stack full-bleed in your brand's guide, so you judge the guide the way a visitor would meet it. A page is nothing but sections in an order, each on a surface: the page's own, the guide's muted background, inverted — the opposite mode, a dark band on a light page — or accent, the brand's accent colour as a band. A page that is all one colour reads as a wireframe; alternating surfaces is what makes it read as a site. The inspector lists the stack with each section's tone (click one to open it), and carries the page's code in both forms, exactly as with a section.
A surface is a wrapper, never a property of the section: <div class="background-color-muted"> for muted, <div class="dark"> for an inverted band on a light page and <div class="light"> on a dark one. Every class is declared by the brand's stylesheet — the colours live under :root, .light and .dark, so a wrapper re-declares every colour for whatever sits inside it. The accent band is the same idea aimed at your accent colour: <div class="accent-surface background-color-surface"> swaps the pair, so the accent becomes the surface and the colour your brand holds readable on it becomes the type, and a button inside knocks out — filled in the type colour, labelled in the accent. It reads whichever mode it sits in, so one band is right on a light page and a dark one. No section paints its own band any more: the statement, the call to action and the newsletter used to arrive pre-coloured and ignored the chips, and now the page decides. Every section's inspector has the same four tones as chips, so you can preview any section on any surface and copy the wrapper line.
Your colour schemes are surfaces too. A colourful brand does not want only muted, inverted and its one accent — it wants an orange band and a green band. Every custom colour scheme on the brand (Brand → Colors → Color Schemes, the same palettes your ads flip between) is emitted as a surface class, .scheme-<id>, beside .dark in both halves of the stylesheet: the style guide's --brand-mode-* set and the shadcn kit's tokens. Wrap a section in <div class="scheme-<id> background-color-surface"> and everything inside — headings, body, links, buttons, cards, inputs — wears that palette; the link colour is re-checked for contrast on that surface, exactly as it is for light and dark. The chips appear after the three built-ins in every section's inspector, the sections README and the agent reads (tones in list_ui_sections) name them, and the synced brand.json carries each scheme's surfaceClass.
A section carries no styling of its own, which is what makes one section set on-brand for every brand: the layout is Tailwind utilities, the look arrives with the brand's stylesheet. The responsive layout is container queries (@3xl: = 48rem, @2xl: = 42rem, Tailwind 4's built-ins), so a section needs an @container ancestor — the pages put @container/page on <main>; a section placed on its own needs one too, or it keeps its phone layout at every width. That is also what lets the site builder preview a page at phone or tablet width inline. The container is named for a second reason: the guide's own narrow-page rules — the heading ladder, the section rhythm, the gutter — ride @container page (max-width: 767px) beside their media query, so they answer a page previewed at phone width inside a wider window. Unnamed they would match the nearest container of any name, including a card's, and shrink a card title on a desktop. Two forms of each, both in the inspector with a copy button:
- TSX — the React component as-is for a Next.js project (
components/sections/<id>.tsx). It imports onlyreact,lucide-reactand@/components/ui/*; install those primitives from upstream shadcn and theme them with the kit. The exceptions are braaand's own primitives — the Counter (components/ui/counter.tsx, the Agera signup's signature count) and the Countdown (components/ui/countdown.tsx, the Event headers' time-left figures) — which upstream shadcn does not have: each rides along as a file wherever a section that imports it goes (the registry block, the export'sui/components/, the synced_shared/ui/,componentFilesinget_ui_section), andnpx shadcn@latest add @<brand>/counteror@<brand>/countdowninstalls one on its own. They theme through the kit like any primitive, and their parts are tunable under UI Elements → Counter / Countdown. Both are display-only and server-safe: their numbers are props, because a live count or the time left is DATA your page's own logic writes, never copy a fill could invent. - HTML — a static twin for any other stack. Interactive state (an open accordion, a select's menu) is absent; wire it per stack. Translate the Tailwind layout utilities into your stack's idiom and keep the classes — they are what the brand's CSS resolves.
Install one straight into a shadcn project with npx shadcn@latest add @<brand>/section-<id> (each section is a registry:block beside the kit, its primitives as bare upstream dependencies), or a whole page with @<brand>/page-<id> — one self-contained block carrying the page component and every section it stacks. The export carries sections opt-in — ?sections=all or a list of ids — as sections/<id>.tsx + .html with a README, and a page rides along as pages/<id>.tsx + .html whenever every section it uses is in the bundle. The synced folder ships the whole library once per machine under _shared/sections/ (an index.json lists the families, the tones and the pages) with the pages under _shared/pages/. Agents read the same thing through list_ui_sections / get_ui_section / get_ui_page, GET /api/brands/{id}/ui-sections[/{sectionId}] and GET /api/brands/{id}/ui-pages/{pageId}, and braaand design-system sections list | get / pages list | get. No per-section tunables: the section is the guide applied — tune the guide.
Starting from the brand's own website, or a stylesheet
Every brand already has a complete style guide from its own fonts, colours, tokens and button — and for a brand whose website is badly styled, that guide is the better seed. When a site or a stylesheet says something better, braaand can read it and propose changes: Design System → Import, with two doors — the website's address, or a pasted / dropped stylesheet (a Webflow export, a site's main CSS, a Tailwind 4 sheet with @theme / @layer base / @utility, or a tailwind.config).
A homepage is often all hero — no h3, no blockquote, no list — so the website door takes up to three more pages of the same site (an /about, a pricing page, a blog post; --page on the CLI, pages on MCP and REST). Each is fetched with the same guard, sheets the homepage already linked are read once, and a page that fails is named and skipped rather than losing the run.
It is a reasoning pass, not a CSS parse. The deterministic half digests the stylesheet (element rules with their @media context, :root variables, the named classes people style type and layout with, @font-face; Webflow's generated .w-* bloat dropped) and lists which tags it defines and which it leaves undefined. A model then reads that beside the brand's current resolved guide and says what fits, what is lacking, and extrapolates the rest — an h1 alone is the base for h2–h6 on a ratio matching the site's feel, a primary button implies its secondary, and a client that runs very large or very small type keeps that feel. A size within 10% of a brand step snaps to the step; a hex that is one of the brand's roles becomes the role. A button's hover is classified into one of the recipes above from its :hover / ::before / ::after rules (a pseudo fill translating in is a slide, direction from the sign of its resting offset; a clip-path reveal is a wipe; a rise plus a shadow is a lift) rather than transcribed as keyframes.
It proposes; it never re-seeds silently. Every row carries before → after and the model's one-line reasoning, and the rows are split by consequence — the split is a real property, not a caution:
| Group | What it writes | Consequence |
|---|---|---|
| Won't move ads | styleGuide (tag overrides, classes, structure, button variants, links, forms) + uiKit.conventions + the type scale (designTokens.typeScale — no ad reads --brand-text-*) |
Not in the brand's visual hash. Cannot move a rendered ad. Ticked by default. |
| Repaints ads | The heading or body face, the accent, the button's radius / padding / weight, the spacing / corner / shadow / border tokens | In the visual hash. Repaints every ad the brand has made. Off until ticked. |
A null from the model means "the site already matches, or showed nothing" — never "clear it". What the stylesheet leaves undefined is listed, and the brand's own guide keeps covering it. Two rows are shaped specially: when the model's h2–h6 sizes all sit within 5% of h1 ÷ ratioⁿ, the ladder is one row (styleGuide.typeRatio) rather than five sizes that drift apart the moment one is edited; and a face the brand does not carry is no longer a dead-end warning — the row mints a new type style for that family and points the tag at it (brand group), and says whether the font files are already in the library or need adding under Assets first.
For agents:
braaand design-system show <brandId> # the resolved guide, with provenance
braaand design-system import <brandId> https://yourbrand.com # propose only
braaand design-system import <brandId> https://yourbrand.com --page /about --page /blog/latest # + more pages
braaand design-system import <brandId> --css ./site.css --apply style-guide # the web-only half
braaand design-system import <brandId> --tailwind ./app.css # a Tailwind 4 sheet or a tailwind.configMCP: get_style_guide reads, propose_style_guide proposes (url [+ pages] | css | tailwind, the same apply values). REST: GET /api/brands/{id}/style-guide and POST /api/brands/{id}/style-guide/import. Extraction quality varies — a heavily minified or hashed stylesheet yields less, and a thin result is reported honestly rather than padded out. Costs one model pass, billed to the brand's pool (Free brands draw on the shared analyze sampler).
Installing
Which command applies what. Only init reads a kit's config; this is the part worth getting right, because the usual advice is the wrong flag here.
A new project — one command
npx shadcn@latest init <kit-url> -y -f
npx shadcn@latest init ./ui/kit.json -y -f # offline, from an export
npx shadcn@latest init ~/Documents/Braaand/<brand>/dist/ui-kit.json -y -fThe brand's style and icon set, its theme, and stock shadcn components, in one go.
Do not pass -d. It means --preset=base-nova, and the preset overrides the brand's settings — you get the theme and shadcn's own shape.
A project that already exists
shadcn add applies tokens and CSS but never touches components.json, so the CLI writes the settings and lists every field it changes:
braaand ui install <brandId> # --no-config wires only the registry
npx shadcn@latest add @<brandId>/kitIt also merges the registries block and notes BRAAAND_TOKEN in .env.local — put a Braaand API key there.
Items: theme (tokens + identity only), kit (theme + ~22 essential components), kit-full (everything upstream ships), fonts.
Changing style decides which component source future adds produce. Components already in components/ui/ are untouched — re-add them to pick up a new style.
By hand — any stack
@import "tailwindcss";
@layer theme, base, components, utilities, braaand;
@import "./ui/theme.css";
@import "./ui/identity.css";That @layer line matters. Cascade layers resolve by layer order before specificity, so the brand's identity rules have to sit in a layer declared after utilities — otherwise Tailwind's own utility classes win and half of them silently do nothing. The installers handle this for you; only the by-hand route needs it written out.
The one rule
Never hand-edit components/ui/* to brand it. An edited component is invisible to a rebrand — the next sync can't reach it, and the file drifts from upstream forever. Re-run the install instead; that is what it is for.
History — exactly what changed since version N
Every exported and synced file carries brand v<N> in its provenance header, and every version bump now keeps a snapshot of the config it came from (a version bumps only when something that reaches a stylesheet or an ad moved — a prose edit keeps it). So "pick up the latest brand" is no longer a diff an agent works out by hand: braaand design-system diff <brand> --from 14 — or get_style_guide_diff, or GET /api/brands/{id}/style-guide/diff?from=14, or the History panel in the Design System page's Export sheet — answers with rows for colours (roles and palette), fonts, design tokens, base tag rules, class rules, structure and motion, each with before → after, plus the plain-terms summary lines to show the person: "accent #1A4D2E → #16402A, one new palette colour, fonts unchanged". Both snapshots resolve through the same style-guide resolver the export uses, so a row is a value that actually moved, never a re-serialized key. design-system versions lists the history. No restore in this round.
Rebranding
The kit is regenerated from the brand on every export and every braaand sync, and the brand's version bumps when the kit changes. Re-running the install picks up the new theme; your component files are untouched, because they were never yours to begin with.
For Polyther
Polyther's site renderer can't run the shadcn CLI at request time, so the same theme also ships as data: sites/ui-package.json under --profile sites, carrying the tokens (flat keys, light/dark/theme), their provenance, the font faces with absolute URLs, the compiled identity CSS, and a scope selector so one page can paint several tenants without repainting its own chrome. Sites installs upstream shadcn once at build time and applies this per tenant — the same relationship sites/motion-package.json already has for motion.