---
name: dandex-brand-guidelines
description: "Design, build, or substantially improve an official Dandex-authored surface: pages in the dandex.ai Next.js app, company and person profiles, change feed and news pages, setup and skill pages, the public chat, standalone reports built from CVR data (credit checks, due diligence, market research, prospect lists), exports, embeds, and transactional email. Use for anything that must read as Dandex: evidence-led, light or dark by the reader's system setting, Figtree body with Bellefair display, one ambient teal gradient, translucent monochrome surfaces, pill controls, and honest handling of Danish registry data."
---

# Design Dandex surfaces

Act as an excellent Dandex designer, editor, information architect, data storyteller, and design engineer. Turn the available material into an official Dandex-authored surface. Shape the argument and the interface together; do not restyle a data dump or assemble generic components.

Dandex has its own opinions about theme, type, color, shape, shell, status semantics, data honesty, and language. This document records them. Wherever Dandex has no opinion, follow the Vercel design guidelines at https://vercel.com/design.md as the default. When the two disagree, Dandex wins. When neither speaks, prefer restraint.

## Dandex product and brand context

Dandex is Danish Company Data; the name comes from Danish Index. It holds the public record (the Danish Business Registry (CVR), Det Centrale Virksomhedsregister, and filed annual reports) and Dandex's own estimates, and serves them wherever people work: AI assistants over the Model Context Protocol (MCP), an HTTP API, a CLI, and a public site built from the same data: company profiles, person profiles, a change feed, business news, a setup guide, a skill for AI assistants, and a chat demo. Readers are founders, sales and credit teams, analysts, lawyers, journalists, and the AI assistants those people operate. They come to answer one question about a Danish company: who is behind it, is it solvent, what changed, is it worth pursuing.

The claim is "Danish Company Data". Marketing copy (the landing page, share images, the site description, email taglines) says what the reader learns: owners, people, figures, changes. It never ties Dandex to one channel ("for AI", "for any MCP client") or to one source ("CVR"), because both will grow. Record pages still name their sources, as evidence.

Make every surface precise, calm, direct, technically literate, evidence-led, and restrained. Build confidence through the data and its provenance, never through hype, decoration, novelty, false certainty, or exaggerated claims. Dandex is a registry, not a pitch.

Start with the reader's job, not the page category. Identify what the reader needs to understand or decide, the strongest supported answer the registry gives, the evidence that earns that answer, and the caveat that could change it. A company profile is an answer to "should I trust this company"; a change event is an answer to "what happened and does it matter"; a setup page is an answer to "how do I connect in under five minutes".

Treat every page as a brand surface, including product-like ones such as search, chat, or a calculator. Communicate Dandex authorship through the shell, the two typefaces, the teal atmosphere, and restraint, without resembling a generic SaaS dashboard, a marketing campaign, or a government portal.

## Use this priority order

When requirements compete, protect them in this order:

1. Preserve supplied facts, CVR identifiers, legal forms, filing years, units, currency, qualifiers, periods, privacy requirements, and task constraints.
2. Preserve the caller's framework, routes, delivery surface, and established Dandex foundation (Next.js App Router, Tailwind v4, shadcn primitives, the tokens in `app/globals.css`).
3. Make the reader's question, the strongest supported answer, and the material evidence immediately clear.
4. Establish unmistakable Dandex authorship through the shell, Figtree and Bellefair, the floor with its one ambient gradient, translucent monochrome surfaces, and restraint.
5. Choose a composition specific to this material; avoid both generic model defaults and a fixed template.
6. Refine responsive behavior, interaction, and details without weakening the hierarchy.

Ask one grouped set of questions only when proceeding could change commercial meaning, legal claims, privacy, formulas, units, populations, periods, customer identity, recommendations, approvals, deadlines, owners, or calls to action. Otherwise omit the unknown, label it honestly, and proceed.

## Integrate with the caller's project

Preserve the host framework, file structure, routes, component conventions, build system, and output form. Edit the files that naturally own the experience. Do not force a filename, a single-file deliverable, raw HTML, or a new framework.

There are four hosts, and each has its own way to reach the same visual system:

**Inside the dandex.ai Next.js app.** Use the installed foundation and nothing parallel: Tailwind utilities, the shadcn tokens declared in `app/globals.css` (`bg-background`, `text-foreground`, `text-muted-foreground`, `ring-foreground/10`, `bg-foreground/5`, and so on), the shadcn primitives in `components/ui/`, and the two font variables `--font-figtree` and `--font-bellefair` set on `<html>` by `app/layout.tsx`. Do not link the brand stylesheet and do not add `dbg-*` classes to app code. Do not edit files in `components/ui/` directly; extend through `className` or wrappers. See "Host mapping" below for the utility-to-token correspondence.

**A standalone Dandex-authored page** (a report exported from a tool, an embed, a static artifact, a page outside the app). Resolve the foundation stylesheet from https://dandex.ai/dandex-brand.css. Link it once at the nearest shared boundary and use the public API documented below; do not read the stylesheet implementation into context. If the URL is not served as CSS or runtime loading fails, copy the asset into the project or inline its exact bytes. Never emit a `file://` URL, an unresolved path, a placeholder URL, a translated token system, or a CSS `@import`.

```html
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Figtree:wght@400..600&family=Bellefair&display=swap" rel="stylesheet" referrerpolicy="no-referrer">
<link href="https://dandex.ai/dandex-brand.css" rel="stylesheet">
```

In React, omit `<body>`, use `className`, and load `<link rel="stylesheet" href={resolvedDbgCssUrl} precedence="dbg" />` at the nearest boundary. In Next.js, load Figtree and Bellefair through `next/font/local` from the self-hosted files in `app/fonts` (as `app/layout.tsx` does), never `next/font/google`, which downloads the fonts during the build; the stylesheet reads `--font-figtree` and `--font-bellefair` before falling back to the Google Fonts faces.

**An in-chat view** (an MCP App, `lib/mcp/apps`). It is a standalone page with the stylesheet inlined, read between two chat messages, most often on a phone. Design it for a phone first. Inline it is a summary: no masthead and no footer, the answer, its controls and one source line, nothing that scrolls sideways, and no taller than the answer needs. A table keeps to the columns that fit and says what it leaves out; a long list shows its first entries. Everything else opens in full screen, where the masthead, the footer and the complete tables return, or in place on a host without full screen. The view reports its content's own height, so it shrinks when its content does.

**Transactional email.** Email is inline-styled, dark, and uses the system font stack because web fonts do not load reliably in mail clients. See "Email" below for the exact palette.

Page-owned CSS may create page-specific topology, density, evidence geometry, and semantic compositions from the public tokens when the stock primitives would distort the material. Every page-authored selector names only the page-specific custom namespace (`dbg-custom-*` for layout, `dbg-viz-*` for visualization marks); never target a published `dbg-*` class from page CSS. A custom class that shares a foundation primitive must not change its layout, typography, surface, border, overflow, or control styling.

The default network allowlist is the foundation stylesheet, the Dandex assets listed under "Logos", the Google Fonts requests above, and user-supplied assets. Do not add third-party JavaScript, chart libraries, icon kits, stock assets, analytics, or other dependencies without authorization. Inside the app, `lucide-react` and `@paper-design/shaders-react` are already installed and are the only visual dependencies to reach for.

## Work in four passes

### Frame the reader's job

Inspect all available material before designing. Privately establish:

- Who opens this, in what context, to decide or understand what?
- What is the strongest answer the registry supports?
- What evidence makes that answer credible, and from which filing or snapshot?
- What tradeoff, uncertainty, or data limit changes its interpretation?
- What should remain available for audit without dominating the first read?

Normalize facts, units, dates, sources, formulas, contradictions, unknowns, and privacy constraints. Distinguish observation (what CVR records), derivation (what Dandex computed: scores, signals, proxies), projection, recommendation, and causation. Never invent intent, ownership, urgency, certainty, deadlines, approvals, future behavior, or confidentiality. A director change is an observation; "the company is restructuring" is a guess.

Order by reader need, not source order. Support two reading speeds:

- **Executive path:** identity, title, headings, decisive values, status, captions, and conclusion communicate the argument quickly.
- **Audit path:** exact tables, filing years, participant roles with periods, methodology, caveats, and sources preserve the record.

Write the executive path in plain language the least specialized reader can repeat. Keep exact registry vocabulary, legal-form abbreviations, industry codes, and metric names in the audit path. Define an unfamiliar Danish or accounting term in plain words at first use, then use the exact term consistently. Never let authoring vocabulary such as composition, hierarchy, or focal relationship leak into page copy.

Simplify language, never the claim. Preserve every qualifier, population, period, unit, condition, comparison basis, and uncertainty that changes meaning. "No annual report in the CVR filing" is not "no revenue". "Dissolved after merger" is not "bankrupt". A credit score of 62 is not "healthy"; it is 62 out of 100 with a grade, a date, and six components.

Keep exhaustive ledgers after the decision path or behind native disclosure. A filterable table with dozens of rows should default to a neutral, decision-relevant subset (all active participants, all events this quarter, every row named in the conclusion), never "All" and never a hand-picked favorable set. State the active filter and selection rule, keep a way to inspect all rows, and show current and total counts.

Every section must answer a new reader question. Combine duplicates. Remove ceremony. Keep one evidence home for each claim.

### Choose the composition

The first viewport is the argument, not a masthead followed by setup. On a company profile the argument is the entity itself: name, status, and the three or four facts that establish what it is. On a change event it is the event and its date. On the landing page it is the product claim and a single way in. If the reader saw only this viewport, they should remember the central fact, decision, or tool, not merely the title or mood.

Before designing, privately name the obvious layout the page category would suggest. Reject it unless the material earns it. A company profile need not be a stack of cards; a news page need not be a grid of tiles. Let the reader's question and the shape of the evidence determine the composition. When the material admits multiple structures, compare two materially different hypotheses before coding, varying topology, density, and evidence placement rather than palette.

Match the opening to the job:

- **An entity (company, person, fund):** name and status co-primary, then the identifying facts as a fact list, then the evidence sections in order of reader need.
- **A decisive recommendation** (credit check, due diligence): make the answer and its decisive basis co-primary.
- **A comparison** (prospect lists, peers): put alternatives on the same visual basis so the difference is seen, not reconstructed.
- **A trend** (financials over years, change history): let the relationship or exception lead; keep exact records below.
- **A tool** (search, chat, calculator): the working tool is the dominant object in the first viewport; do not delay it below orientation copy.
- **A brief with no supported decision:** lead with the strongest supported state, implication, limit, or open question rather than inventing a call to action.

Choose geometry before components. Map the material to a visual variable: magnitude to length on a common scale, change over time to horizontal order, composition to proportion, threshold to distance from a boundary, process or ownership to connection and sequence, qualitative alternatives to aligned rows. Use tables for precise lookup, prose for one conclusion, and charts only for relationships that become faster to understand visually. Do not default to bars because values exist.

Compose the page as a field, not a stack of components. Establish one page-level throughline and one focal relationship in each section. Pace the scroll deliberately: vary density and quiet while retaining one visual grammar. Repetition creates rhythm only when the repeated items are true peers. End with the resolved decision, implication, next action, or open question, then let sources and the footer follow quietly.

Give every artifact one evidence-bearing organizing move that belongs to its material: a share-of-ownership geometry, a solvency threshold, a timeline of board turnover, a filing-year comparison. It must clarify the subject, not decorate it.

Use a squint test: the dominant claim or evidence should be obvious and the reading path stable. Use a text-mask test: with the words blurred, the hierarchy should still communicate identity, emphasis, grouping, and progression. If every block has equal weight, redesign before coding.

Create presence through commitment, not additional effects. When a page feels too safe, strengthen one focal relationship through proportion, hierarchy, density, pacing, or evidence placement. When the material feels thin, improve its selection, hierarchy, comparison, or explanation; leave unsupported gaps honest. Never fill an evidence gap with panels, borders, icons, color fields, decorative charts, or effects.

### Authoritative Dandex visual system

Treat this section as the design authority. Use the foundation stylesheet for exact tokens, type roles, states, controls, and primitives. Use these instructions for composition, hierarchy, and when those primitives are appropriate. Do not introduce a parallel visual system.

#### Theme

The app has two themes, light and dark, and the reader's system setting chooses between them (`prefers-color-scheme`). There is no switcher and no stored choice: `app/globals.css` declares the light tokens on `:root` and the dark ones under the media query, Tailwind's `dark:` variant follows the same query, and nothing runs in the browser to pick a theme, so a static page is the same HTML for both. The two themes are one design: the same shell, type, shape and layers, with the values turned over. Light is ink on a plain floor of warm paper; dark is near-white on the teal-black floor, and that floor's colour is the light theme's ink. Design every surface in both and test contrast against both floors. `/brand` shows the system's parts in the theme in use.

Outside the app there is one theme so far: the standalone stylesheet, the in-chat view, share images and email are dark (the stylesheet sets `color-scheme: dark`), and print is ink on paper because a dark page on paper is unreadable and wasteful.

#### Authorship shell

Every completed page has the same Dandex authorship outcome. Inside the app, the marketing layout already renders the shell; do not add a second header or footer. Standalone pages use the exact shell below.

- **Header:** 56px tall, full width, content inset 24px, capped at 1536px and centered. The wordmark sits on the left at 20px tall. The right side holds at most one action (the app uses "Share", preceded by a quiet "Sign in" text link in secondary color, "Sign out" once the browser is signed in, read in the browser so pages stay static, and a compact 36px search field that lists matching companies and people in an opaque 18px panel as you type and submits to /search, shown as a magnifier link below 640px and hidden on /search and /chat) or, on a report, at most two sourced metadata fields such as the subject company, the period, or a confidentiality note. Align the wordmark and the right-side content to one baseline. A 1px hairline separates the header from the page.
- **Footer:** same inset and cap, separated from the page by a hairline. In the app, the links stand in three groups with short labels: Explore (Search, Screener, Statistics, Glossary), Connect (Setup, Skill, Chat) and Dandex (Pricing, Manage, Brand, Legal, Contact). From 1024px the footer is one 56px row: "Dandex: Danish Company Data © 2026" on the left in 12px secondary text, the links on the right as a quiet row, each group set apart by a wider gap and its label kept for screen readers only. Below 1024px the groups stand as three columns, each under its label in 12px medium, with the name and claim on a line beneath; links get a 24px tall hit area. A new footer link joins the group it belongs to (`FOOTER_GROUPS` in `app/(marketing)/layout.tsx`) rather than lengthening the row. On a report, the right side holds one sourced line such as "Source: CVR, Erhvervsstyrelsen. Annual reports via XBRL."
- **Between them:** the ambient gradient floor and the page content. Both shell regions are separated by hairlines, never by boxes or color bands.

Keep preparation, audience, and document-state metadata in the masthead. Do not repeat it as a preamble between the masthead and the page-defining title.

When using the standalone stylesheet, preserve this direct-child order:

```html
<body class="dbg-report">
  <div class="dbg-shell">
    <a class="dbg-skip-link" href="#main">Skip to content</a>
    <header class="dbg-header">
      <div class="dbg-masthead">
        <span class="dbg-identity"><span class="dbg-wordmark" role="img" aria-label="Dandex"></span></span>
        <div class="dbg-document-meta">...</div>
      </div>
    </header>
    <main id="main">...</main>
    <footer class="dbg-footer">
      <span>Dandex © 2026</span>
      <span>Source: CVR, Erhvervsstyrelsen</span>
    </footer>
  </div>
</body>
```

The stylesheet supplies the wordmark and mark as masks that follow `currentColor`. Do not substitute text, a different logo file, or a decorative treatment.

#### Grid and alignment

Dandex sets the outer measure; Vercel's grid fills in the rest.

- **Shell width:** 1536px. **Content tracks:** 1024px for entity pages (company, person, fund), 896px for guided pages (setup), 768px for reading pages (legal, news detail, long prose), 448px for a lone form. Inside the app these are `max-w-screen-2xl`, `max-w-screen-lg`, `max-w-4xl`, `max-w-3xl`, and `max-w-md`. Horizontal inset is always 24px (`px-6`).
- **Grid:** 12 columns on desktop, 6 on tablet, 4 on mobile, 24px gutters. Reading prose occupies 6 to 7 desktop columns and never exceeds 68 characters per line. Tables, charts, calculators, diagrams, and major comparisons may use all 12.
- **Vertical page padding:** 32px on entity pages, 48px on guided pages, 64px on the landing page.

Every object must align to a shared edge, baseline, grid line, or deliberate optical center. Equivalent blocks share type roles, value positions, internal rows, and action alignment. A split heading and paragraph align on their first text baselines. Tables own the full evidence width of their section. Do not strand content in a narrow track while usable columns remain empty.

Make column gutters unmistakable. Open space must amplify the focal object; large empty rectangles caused by an underfilled split or an orphaned third item are layout failures. Three true peers occupy one three-column row. Do not force materially unequal findings into equal cells; rank them or give the decisive one more visual consequence.

The landing page and the chat empty state are the two surfaces that center their content. Everything else is left-aligned.

#### Typography and rhythm

Two typefaces, and each has one job.

- **Figtree** (400, 500, 600) is the sans for everything: body, UI, labels, controls, tables, figures, dates, counts, percentages, currency, and every heading that is not a page title. It is loaded with exactly those three weights. Never request 700; the browser silently substitutes 600 and the intent is lost. Emphasis is medium (500), headings are semibold (600).
- **Bellefair** (400 only) is the display serif. It carries the page-defining title, editorial headlines on the news and change pages, and the display claim on the landing page. It is always regular weight, always tracked at -0.07em, and never used below 24px, in buttons, in tables, in labels, or in running text. Bellefair is the memorable thing on the page; spend it once per viewport.
- **Monospace** is the system stack (`ui-monospace, SFMono-Regular, Menlo, Consolas`). Use it only for code, commands, paths, raw tokens, server URLs, and short operational identifiers such as an API token or an endpoint. Set only the identifier in mono, not its sentence. Do not set section labels, CVR numbers, dates, or currency in mono; those are sans with tabular numerals.

Use the published type roles and weight tokens. Do not create arbitrary font sizes or numeric font weights.

| Role | Face | Size | Weight | Leading | Tracking | Use |
| --- | --- | --- | --- | --- | --- | --- |
| display | Bellefair | clamp(24px, 7vw, 72px) | 400 | 1.05 | -0.07em | the single page-defining claim on the landing page; it shrinks below 36px only so it stays on one line on a phone |
| headline | Bellefair | clamp(36px, 5vw, 60px) | 400 | 1.1 | -0.07em | the lead story on an editorial page |
| page-title | Bellefair | clamp(30px, 4vw, 36px) | 400 | 1.1 | -0.07em | the normal page or entity title |
| title | Bellefair | 24px | 400 | 1.25 | -0.07em | secondary editorial headline |
| heading-24 | Figtree | 24px | 600 | 32px | -0.025em | major section turn |
| heading-20 | Figtree | 20px | 600 | 28px | -0.025em | section |
| heading-18 | Figtree | 18px | 600 | 26px | -0.02em | section inside a dense entity page |
| heading-16 | Figtree | 16px | 500 | 24px | -0.01em | nested structure |
| lede | Figtree | 18px | 400 | 28px | 0 | one short orientation passage, secondary color |
| body | Figtree | 16px | 400 | 24px | 0 | reading prose |
| compact | Figtree | 14px | 400 | 20px | 0 | the UI default: cards, lists, tables, forms |
| label | Figtree | 12px | 500 | 16px | 0 | field names, table headers |
| metadata | Figtree | 12px | 400 | 16px | 0 | dates, counts, breadcrumbs, captions |

The UI default is compact (14px). Reading prose is body (16px). Never go below 12px. Never use tiny gray copy to make density fit; rewrite before shrinking.

Tabular numerals on every figure that sits in a column or a peer group. Equivalent peers always share role, size, weight, leading, and numeric treatment; never resize one because its string is longer or its value is larger.

Section labels inside dense data panels (a participant list split into "Active" and "Past", the field groups of a detail panel) may use the label role in secondary color with wide tracking and uppercase, because they are field names, not headlines. The uppercase is a CSS transform (`uppercase` in the app, `.dbg-label[data-variant="group"]` standalone) over sentence-case source text, so screen readers, copied text and search read "Active roles", never "ACTIVE ROLES". That is the only permitted use of all-caps. Never place an all-caps eyebrow, kicker, or overline above a headline or a section heading; the heading itself states the claim.

Build vertical rhythm from relationships:

- Heading to its first paragraph: close (8 to 12px).
- Paragraph to paragraph or list: one body rhythm (16px).
- Label to value to detail: identical across peers (4px).
- Content group to new section: clearly larger (32px).
- Caption or source to the evidence it qualifies: close enough to read together (12px).

Give every gap one owner. A flow, stack, grid, or page-owned wrapper sets the gap; children must not add competing margins. Within-group gaps are `--dbg-space-2` through `--dbg-space-4`, between-group gaps `--dbg-space-6` through `--dbg-space-8`, major section turns `--dbg-space-8` through `--dbg-space-12`. Reserve `--dbg-space-16` for a true chapter break.

Establish hierarchy through typography before surfaces or color. Separate paragraphs with space, never first-line indents. Inspect important line breaks; fix stranded words in Bellefair titles by improving the copy or measure, not by shrinking the element. The landing claim is deliberately kept to one line at every viewport; a display claim that wraps has the wrong copy.

#### Color, surfaces, and boundaries

Design in monochrome on the floor, with one atmospheric exception and a small semantic palette. The values below are the dark theme's and the standalone stylesheet's; the light theme has the same roles with its own values, and `app/globals.css` is the home of both sets.

**The floor** of the dark theme is `#050D10`, a near-black with a teal cast. It is the page background, the theme color, the app icon square, and the ink of the dark wordmark. In the light theme the floor is pale and `#050D10` is the text colour.

**The one gradient.** Dandex has exactly one gradient: the ambient teal grain that sits behind the marketing shell, made of three teals pooled in the corners over the floor: `hsl(195 60% 9%)`, `hsl(190 55% 12%)`, `hsl(200 65% 7%)`. It belongs to the dark theme: the light floor is plain warm paper, and its `--ambient-*` tokens are transparent. In the app the marketing layout renders it as a static CSS radial gradient (`GradientBackground`), and the landing page alone adds the slowly moving WebGL grain over it once the browser is idle (`AmbientShader`); in the standalone stylesheet it is the static CSS fallback on the report root. It is the brand's atmosphere and it lives only on the page background. It never appears on text, cards, buttons, borders, charts, or as a section band. No other gradient is permitted anywhere. If a surface needs presence, it gets a translucent layer, not a color.

**The teal itself** appears in exactly two more places: the text selection color (`--selection`) and the floor. It is never a UI accent, a link color, a chart series, or a status color.

**Everything above the floor is a translucent layer of the text colour.** Surfaces, borders, and dividers are the text colour at a fixed alpha (white in the dark theme, ink in the light one) so they read the same over any part of the gradient. The hairline is the one layer whose alpha differs by theme (`--hairline`): 5% white would vanish as 5% ink on a pale floor.

| Alpha | Token | Use |
| --- | --- | --- |
| 5% | `--dbg-gray-alpha-100` | hairlines (header and footer rules), resting surface of a card or list row |
| 8% | `--dbg-gray-alpha-200` | input field fill |
| 10% | `--dbg-gray-alpha-300` | default border, card ring, hover surface |
| 15% | `--dbg-gray-alpha-400` | input border |
| 20% | `--dbg-gray-alpha-500` | strong border, emphasized input border |
| 40% | `--dbg-gray-alpha-700` | placeholder text, disabled glyphs |

In the dark theme text is near-white (`oklch(0.985 0 0)`) for primary and zinc-400 (`oklch(0.705 0.015 286.067)`) for secondary, and popovers, dialogs, and sheets that must be opaque use the zinc-900 card color (`oklch(0.21 0.006 285.885)`). In the light theme text is the ink, secondary text a cool gray, and opaque panels are white.

**Primary action** is a fill in the text colour with the floor's tone for its label: near-white (`oklch(0.92 0.004 286.32)`) with dark text in the dark theme, ink with near-white text in the light one. It is the only filled control on a page; there is at most one per viewport. Everything else is outlined, ghosted, or a text link.

**Semantic color** exists for meaning only and is always paired with a non-color cue (a word, a sign, a glyph). The light theme uses darker values of the same four hues, so they keep their contrast as text on a pale floor:

- Success and positive figures: green (`oklch(0.627 0.194 149.214)` for text; 10% tint and 20% border for a note).
- Warning and pending: amber (`oklch(0.769 0.188 70.08)`; 10% tint, 30% border).
- Info: blue (`oklch(0.623 0.214 259.815)`; 10% tint, 20% border).
- Error, destructive, dissolved, and negative figures: red (`oklch(0.704 0.191 22.216)`; 10% tint, 20% border).

Do not turn a recommendation, a large revenue figure, or a longer bar green because it is favorable. Do not color a company name. Chart series are monochrome by default; the shadcn `--chart-*` tokens exist for library compatibility and are not a license to color series.

The page is one continuous canvas. Earn a surface or boundary only when it communicates selection, interaction, warning, contrast, or a grouping that spacing cannot express. Prefer spacing, alignment, typography, and a change in density before borders or boxes. Do not wrap every section, metric, or comparison in a card. Avoid nested panels. A card is for a navigable unit (a search result, a change event, a company summary), a linked whole with one hover state, not a decoration around a heading.

There are no drop shadows. Elevation is a 1px ring at 10% of the text colour, and an opaque card color for anything that floats.

Diagnose quantity separately from intensity. If the page feels busy, remove, combine, or reorder content. If it feels loud, reduce competing color, scale, weight, borders, surfaces, and motion. Preserve one deliberate anchor; restraint must not flatten the page into neutral sameness.

Hard reject decorative gradients other than the ambient floor, gradient text, glows, blobs, stripes, textures, grid backgrounds, glass effects, colored side rails, ornamental shadows, and fake depth. A gradient is otherwise acceptable only as a labelled continuous data scale.

#### Shape

The base radius is 10px. Shape encodes what a thing is:

- **Pills** (fully rounded): buttons, badges and status pills, inputs, selects, tab lists, and any single-line control. This is the Dandex signature and it comes from the shadcn "radix-maia" style the app is built on.
- **18px:** cards, dialogs, sheets, and other contained surfaces.
- **14px:** tab triggers, textareas, and multi-line controls where a pill would look wrong.
- **10px:** code blocks, notes, timeline rows, and small grouped surfaces.
- **8px:** menu items and the smallest interior elements.
- **0:** table cells, hairlines, and full-bleed bands.

Do not mix radii within one control group. A pill button next to a rounded-square input is a defect.

#### Status and change semantics

These are Dandex domain rules and they override generic badge styling.

**Company status** is derived from `sammensatStatus` and the life-cycle periods, never from an "ophørsdato" field, which does not exist. Show a status that is not active as a pill beside the entity name, labelled in English with the specific status (`detailEn` in `scripts/core/clients/company-status.ts`). The Danish registry term and its meaning are on the glossary at `/glossary`, never on the pill:

| Bucket | Tone | Examples |
| --- | --- | --- |
| active | no pill | Active, Registered, starts later |
| dissolved or terminated | negative (red tint) | Dissolved after merger, Under bankruptcy, Compulsorily dissolved, Ceased |
| unknown | secondary (gray fill) | status not derivable |

Active is the ordinary state and earns no pill at all (`CompanyStatusPill` renders nothing for it); a reader assumes it unless told otherwise. Dissolution earns red because it changes what the reader may do with the company.

**Change types** on the feed use the same pill with a fixed tone map. Labels are English; the Danish registry term for each is on the glossary:

| Tone | Change types |
| --- | --- |
| emphasis (light fill) | New company |
| negative | Company dissolved |
| secondary | Name change, Director change, Board change, Ownership change (changes to people or identity) |
| neutral outline | Address change, Industry change, Legal form change, Headcount change, Status change (changes to attributes) |

**Roles** (organs such as Direktion and Bestyrelse, and the registers) are named in English on people rows: Executive board, Board of directors, Partner. A name the glossary does not know is shown as registered.

**The glossary** (`lib/glossary/terms.ts`, published at `/glossary`) is the one home for these labels: each Danish term, the English label pages show, and a short definition, with an anchor per term (`/glossary#direktion`). A new status, role or change type gets its entry there before a page shows it.

**Signed figures** (profit or loss, equity change, year-on-year delta) carry their sign in the text and in color: green for positive, red for negative, secondary gray for zero or not reported. Negative numbers use a leading minus, never parentheses.

**Scores** (credit and health, 0 to 100 with a grade A to F, six components) always show the number, the scale, the grade, and the computation date together. The grade letter is the primary cue; color, if used at all, follows the semantic palette and is never the only signal. Never round a score into an adjective.

**Signals** are observations anchored to a change date and they expire. Show when a signal was observed and when it lapses. A signal that has lapsed is history, not a warning.

**Tool call states** in the chat (pending, approval requested, output, error, denied) use the semantic palette on a 16px glyph plus the state word. The glyph alone is not a state.

#### Data and evidence

Make the visual encoding honest, and make the registry the visible source.

- **Attribution.** Every figure names its source and period near the evidence: "CVR, snapshot 4 September 2026" for registry facts, "Annual report 2025, XBRL" for financials, "Dandex score, computed 4 September 2026" for derived values. The footer carries the general source line; the evidence carries the specific one.
- **Missing is not zero.** A company with no annual report in CVR shows "Not reported", not 0 kr. A year filed as a PDF alone, with no XBRL file, shows "PDF only" and links the PDF: the report exists, its figures are not machine-readable. A filing under the "Excluding Balance Sheet/Income Statement" schema with no ESEF report beside it legitimately has no parseable statement; say "Not available in the CVR filing" and do not render an empty chart. Large IFRS filers' figures come from their ESEF report.
- **Identity.** Show the CVR number with every company name the first time it appears in a section, as "CVR 35527710". Show a person's roles with their periods; an active role and a past role are different facts.
- **Legal forms** read in English with the registry abbreviation in parentheses: "Private limited company (ApS)", "Public limited company (A/S)". A form with no abbreviation keeps its short Danish term: "Sole proprietorship (enkeltmandsvirksomhed)", "Commercial foundation (erhvervsdrivende fond)". `legalFormLabel` in `lib/glossary/terms.ts` names the form for every page, share image and tool, and the Danish part carries `lang="da"`. A row with no room shows its short label, the English alone; no other shortening. A form the glossary does not know shows as the registry records it. Danish-language change articles keep the registry's Danish name.
- **Currency and units.** Amounts are DKK unless the report says otherwise (some filers report in EUR or USD). State the unit once per table or figure in the header ("DKK thousands", "DKK"), never per cell. Percentages carry one decimal at most. Employee counts carry their year.
- **Number format.** A decimal is always a point, never a comma, and thousands are grouped with a comma: "1,234,567.8", "2.5%", "1.8x", "DKK 12.4 million", "8.4M". This holds wherever Dandex writes a number for a person or an agent, in any language: pages, Danish-language change articles and legal text, share images, emails, report files, tool output and descriptions, server instructions, the CLI and docs. Danish words stay Danish ("2,400,000 kr."); only the digits follow the rule. Format through the shared formatters in `lib/format/number.ts`, never by hand and never with a page locale. Machine values (JSON numbers, CSV cells meant for import) are plain: a point decimal and no grouping. Excel cells hold numbers, so Excel draws their separators in the viewer's locale.
- **Dates.** Day-month-year, never month-first. Long form on editorial pages ("4 September 2026"), short form in dense lists ("4 Sep 2026"). Danish-language content formats dates with `da-DK`; English-language pages with `en-DK`. Times read "14:05".
- **Years as columns.** Financial tables run years left to right, oldest to newest, with the year in the header and the metric as the row label. Never put a year in a cell. The same holds for every file Dandex produces (Excel, CSV) and every time axis: the leftmost year column is the earliest and the rightmost the most recent.
- **Derived values in tables.** Dandex calculations beside filed figures, such as the credit metrics under a company's annual-report figures, sit in their own labelled row group ("Credit metrics (Dandex calculations)"), and the caption says they are calculations, not filed values. A value that needed an assumption or a fallback is a Dandex estimate: secondary text with a footnote number, and the caption lists each distinct note once ("1 Dandex estimate: EBIT rebuilt from profit before tax and financial items"). "n.m." means not meaningful, a ratio over a base that is not positive (gearing with negative EBITDA), and is distinct from "Not reported", an input that was not filed. Gearing at 0 or less reads "Net cash", never a negative multiple.

Use zero baselines for length encodings unless a clearly marked range or delta view better answers the question. Do not exaggerate small differences with cropped bars or hide them with nearly identical total bars; show the exact delta on the same basis. Every peer bar shares one documented scale and its length encodes the value; otherwise use aligned text. Size repeated horizontal bars as one layout with a shared label lane, plot lane, and value lane.

When peer denominators differ (companies of different size, years of different length), choose count or rate explicitly from the reader's question and show the base. Prefer direct labels to legends. Reserve a clear lane for every chart label. Use a caption to state what the reader should notice and what the chart does not establish. Provide a semantic table or concise text alternative for material chart data.

Tables are evidence, not decoration:

- Use a semantic `<table>` with caption, head, body, and optional foot.
- Span the full 12-column evidence width by default. Put the section introduction above it.
- Left-align text columns and their headers; right-align numeric columns and their headers, including placeholders and totals. In standalone tables, put `class="dbg-numeric"` on the numeric `<th>` and every numeric `<td>`.
- Keep peer units and precision consistent; do not add fake precision.
- Body cells align to the row's first text baseline; never vertically center body rows.
- Give the row-label column enough width for ordinary labels to stay on one line.
- Do not spend a column repeating the same category for a run of rows; group with row groups or separate tables.
- Use normal density for ordinary tables; compact density for genuinely dense lookup.
- A table with five or more columns, or any table whose headers wrap at desktop width, owns the full section width. Never clip or shrink a header to preserve a neighboring prose rail.

```html
<th scope="col">Year</th>
<th scope="col" class="dbg-numeric">Revenue (DKK thousands)</th>
<th scope="row">2025</th>
<td class="dbg-numeric">12,757</td>
```

#### Controls and interaction

Treat interaction as evidence, not decoration. Search returns the entity; chat returns the answer with its tool calls visible; a calculator makes one model legible.

- **One control height.** Every button, input and select is 36px tall, so controls that sit side by side line up: the header's search field and Share, a field and its submit, a row of actions. Icon buttons are 36px square. The only smaller controls sit inside another control, such as an input's clear button or a code block's copy button. `lib/__tests__/control-height.test.ts` fails on any other size.
- **Buttons** are pills, 14px medium text, 12px horizontal padding, a 16px glyph at most. Variants: primary (near-white fill), outline (10% border over 5% fill), ghost, destructive (red tint), link. One primary per viewport.
- **Inputs** are pills, 8% fill, 15% border, 40% placeholder. A search input carries a 16px magnifier glyph at the start and a clear button at the end when it has a value; the clear button has an accessible name.
- **Focus** is a 3px halo of the ring color at 50% plus a border color change to the ring color. It is neutral gray, not blue. It is always visible on keyboard focus and never removed.
- **Hover** on a navigable card raises its surface from 5% to 10% of the text colour; on a title it adds an underline with a 4px offset; on a secondary link it moves the text from secondary to primary color. All hover transitions are 150ms color transitions and nothing else moves.
- **Disabled** is 50% opacity with pointer events off.
- **Loading** is a 16px spinning glyph inside the control that started the work, or a pulsing skeleton in the shape of the content that is coming. Never a full-page spinner, never a progress bar for an unknown duration.
- **Speed.** Never assume a reader is fine with waiting, and never block a page on a slow source. A tap shows the destination's shape at once (a skeleton with what is already known, such as the name), not a spinner on the page being left. The shell (name, status, key facts) comes from one cached read and is sent within 2 seconds; every other section has its own boundary, a skeleton of its final size, and a budget of 2 seconds per source, so a cold page is complete within 3 seconds. A section whose source misses its budget says so in its place ("Dandex could not read the annual reports just now") with a "Try again" button, shows its last good copy with the date it was read when one is kept, and the rest of the page stands. Metadata reads only the shell's data. The budgets live in `PAGE_BUDGET` (`lib/company/section-cache.ts`).
- **Errors** are inline, next to the control, in destructive red with a 16px alert glyph and a sentence that says what went wrong and what to do. Errors do not apologize and are never vague.
- **Success** replaces the form with a confirmation in place (a 400ms opacity fade), keeping the layout stable.
- **Helper text** is 12px secondary, one line, below the control.
- **Sortable headers** are a button inside the `th`, never the `th` itself, with `aria-sort` on the `th` (`ascending`, `descending`, or `none`). The button holds the label and a 14px lucide arrow: up or down on the sorted column, a muted up-and-down arrow on the others. One column is sorted at a time; the first press sorts descending (ascending for a name), the second reverses. The sort lives in the URL.
- **Range filters** are a min and max pair of 36px inputs side by side under one label, each with the unit as a suffix inside the field ("DKK m", "%", "x"), and the coverage as helper text below: "Available for 24,770 of 368,153 companies with a processed annual report." Amounts are stored in each report's own currency, so an amount range or sort compares DKK filings only, and its helper text ends "DKK filings only." An empty bound is no bound; a range given the wrong way round is read the right way round.
- **Locked rows.** A value the reader's plan does not include keeps its usual place: the label with its Danish term, a 14px lucide lock, "Pro" linked to /pricing, and the item's one sentence below in 12px secondary text. Never a blurred, fake or placeholder number, and the same row whatever the value, so its presence hints at nothing; a row is left out only when its public inputs are (no annual report, no controlled company). A locked table row spans the year columns; a locked column header carries the lock and its cells a lock alone; a locked range filter has no inputs. Signed in with a plan that includes it, the value takes the row's place. One short "Pro for this company" section names what the locked rows hold, by group, in plain words, with one link to the plan.
- **View switch.** Two views of the same data, such as the network's Everyone and Subsidiaries only, take a segmented pill pair: two buttons of the one control height on a 5% pill track, the chosen one filled, each with `aria-pressed`, both reachable by keyboard. The choice lives in the URL hash (`#network=subsidiaries`), so a shared link opens on it while the server renders one page and crawlers see no new URL. Show the switch only when the second view has something in it.
- **Time range.** A monthly series offers fixed windows as a segmented pill on the same track, in this order: 6M, 1Y, 3Y, 5Y, Max (10Y once the data covers it), each with its spelled-out accessible name ("1 year") inside `role="group"` with `aria-label="Period"`. Every window counts back from the latest month in the data, and the change beside the big number is from the window's first month to its last, in percent and in units, with the months named ("over 1 year, Aug 2025 to Aug 2026"). A window the series cannot fill stays in place, disabled, with the reason as its description. The server renders the default window (1Y); a switch recomputes from data already in the page and fetches nothing, and nothing changes size. On coarse pointers the track's padding gives a 44px hit area while the buttons stay 36px. The window lives in the hash (`#range=5y`). The Dandex Labor Index (`components/labor-index/range-chart.tsx`) is the reference.

Define one canonical state model for any calculator: variables, fixed inputs, formulas, units, full precision, ranges, defaults, display precision, and dependencies. One control owns each variable. Pre-render the default result. Update dependent outputs atomically from full-precision state, then format. Preserve invalid entries and the last valid result rather than silently clamping. Keep everything usable by keyboard and screen reader.

With the standalone foundation, `.dbg-calculator` directly owns `.dbg-calculator-inputs` and `.dbg-calculator-output`. A unit control uses this nesting so its label and helper stay outside the field:

```html
<div class="dbg-field">
  <label class="dbg-label" for="rate">Interest rate</label>
  <div class="dbg-unit-field">
    <input id="rate" type="number" value="8">
    <span class="dbg-unit-suffix">%</span>
  </div>
  <p class="dbg-helper">From 4% to 12%.</p>
</div>
```

#### Motion

Default to stillness. The ambient gradient on the landing page is the only thing on a Dandex page that moves without being asked, and it moves slowly enough to be felt rather than seen. Everything else is still until the reader acts.

Permitted motion, all of it answering an action: overlays enter and exit in 150 to 200ms (the shadcn defaults), color transitions run 150ms, a submitted form fades to its confirmation in 400ms, loaders spin, skeletons pulse. Nothing reveals on scroll, nothing slides in on page load, nothing bounces, nothing parallaxes, nothing moves on hover. Respect `prefers-reduced-motion`, under which the ambient gradient is static and every transition collapses to zero.

For formal pages, create delight through unusually clear evidence or unusually low friction: a company understood at a glance, a setup that takes one paste, a change that explains itself. Do not manufacture personality with jokes, celebration, or effects.

#### Media, icons, and logos

**Logos.** The wordmark is the lens mark followed by DANDEX in small caps. The files, all at `https://dandex.ai/`:

- `dandex-logo.svg`: white, for the dark floor. The app draws this file in the header at 20px tall (about 128px wide), as a mask over the text colour, so it is white in the dark theme and ink in the light one (`ThemeLogo`).
- `dandex-logo-dark.svg`: ink `#050D10`, for a light surface outside the app such as a partner's white page or print.
- `mcp-icon.svg`: the lens mark alone on a full-bleed `#050D10` square, 512px, the MCP server identity and the source of the app, manifest and OAuth icons. Clients and launchers cut their own corners.

Every icon uses the lens as it sits in the wordmark: wide, with an upright pupil. The favicon (`app/icon.svg`, `app/favicon.ico`) is a rounded square with the lens taken to the edges and a heavier pupil, so it still reads at 16px. The maskable manifest icon keeps the lens inside Android's safe-zone circle.

Never recolor the wordmark, add an outline, place it on a gradient, stretch it, or render it below 16px tall. Clear space around it equals the height of the mark. The older serif lockup `logo.svg` stays at its URL only because outside listings may point at it; do not use it.

**Icons** come from `lucide-react` at default stroke width. Sizes: 14px beside metadata in a dense row (industry, city, employees, founded), 16px inside buttons and inputs, 40px at 1.5 stroke and 40% opacity for an empty state. Icons are metadata glyphs and control affordances, never decoration: no icon tiles, no colored discs, no oversized hero icons, no mixed icon sets on one surface. Prefer a text label unless an established glyph makes the action materially faster to recognize.

**Media.** Use supplied screenshots (the setup guide's client screenshots), diagrams, or company logos only when they are evidence or materially improve understanding. Never add stock imagery, decorative illustrations, abstract shapes, fake product screenshots, or mandatory hero media. The landing page has no image; its hero is the Bellefair claim.

#### Copy and language

- **Interface language is English.** Registry content (company names, Danish titles and summaries on the change feed, industry text) stays in Danish as recorded and is never machine-translated in place. Registry vocabulary that works as a label (statuses, roles, change types, legal forms) is shown in English from the glossary, with the Danish term kept there. When an English page shows Danish content, the surrounding labels are English.
- **The registry** has one English name: "Danish Business Registry (CVR)" on first use on a page, in a tool description or in a document, then "CVR". Never "Central Business Register", "CVR register" or "CVR registry". Its glossary entry is under Det Centrale Virksomhedsregister.
- **Case.** Sentence case everywhere: page titles, section headings, navigation, tab labels, button labels, body copy, helper text, captions, status labels, and change-type labels ("Setup guide", "Get access", "Who is behind it"). Capitalize only the first word and proper nouns (Dandex, Denmark, CVR, Claude, Pro). The one exception is the claim "Danish Company Data", which is a name and keeps its capitals wherever it appears. Danish terms keep Danish casing, which writes common nouns in lowercase: "Board of directors (bestyrelse)".
- **Separators.** No middle dots, no em dashes, no en dashes, and no spaced hyphen standing in for a dash (" - "), in copy, in `<title>` strings or in email subjects. Hyphens inside words ("one-line") stay; a range reads "2021 to 2025". Inline metadata runs are separated by a layout gap (a wrapping flex row), or by commas where the run is a string: "CVR 35527710, ApS, Software development". Breadcrumbs use a slash. A title joins its parts with a comma, a colon or parentheses: "IEX ApS (CVR 35527710) company profile". In running text use a period, a colon, or a new sentence.
- **Tone.** Precise, calm, and direct. Plain verbs, no filler, no exclamation marks, no superlatives. The banned words are those of the founder's voice guide (voice.md, section 9.9); `BANNED_WORDS` in `lib/copy/rules.ts` holds the list and the copy test enforces it. Describe what something is or does. Name things by what the reader understands (a server URL, a token, a company), not by how the system is built.
- **Actions** say what happens: "Get access", "Copy server URL", "Export to Excel". An action keeps the same name through its whole flow.
- **No emojis** anywhere: copy, code, logs, commits, email.
- **Empty and error states** direct the reader: what is missing, why, and the one thing to do next. "No companies match 'Novo Nordisk ApS'. Try the CVR number or a shorter name."
- A useful title says what happened, what changes, or what decision is needed; it does not name the page genre.

### Inspect and revise privately

Render the actual result when tooling exists. Inspect the first viewport, the full page, and the narrow reflow. Do not expose an evaluation matrix or critique unless asked.

Review in this order:

1. **First read:** Is Dandex authorship immediate (the floor, one gradient, the wordmark, Bellefair once), in both themes? Would the reader remember the central fact, decision, or tool rather than the mood?
2. **Language:** Can the least specialized reader explain the answer from the headings and captions? Is every Danish or accounting term defined once? Did simplification preserve every qualifier?
3. **Composition:** One dominant object? Does each section advance the argument? Is any empty space accidental?
4. **Typography:** Bellefair only where earned, Figtree at 400, 500, 600 only, roles consistent, peers equal, baselines aligned, no text below 12px, every gap with one owner?
5. **Evidence:** Does geometry prove the claim? Does every figure carry its source and period? Is "not reported" honest rather than zero? Are numeric columns right-aligned with tabular numerals? Are status pills mapped correctly?
6. **Restraint:** Can any surface, border, pill, icon, label, color, paragraph, or section be removed without losing meaning? If yes, remove it. Is there more than one gradient? Then one is wrong.
7. **Reflow:** Does the page recompose at 800px and 400px without overflow or character-level wrapping? Is the display claim still one line or was the copy fixed?
8. **Trust and access:** Landmarks, one h1, ordered headings, skip link, visible focus, accessible names on icon-only controls, text alternatives, sources, caveats.

Fix the highest-impact systemic defect, render again, and repeat until no known material issue remains. Deliver the implementation, not a score or a diary.

## Reject generated-design reflexes

Do not ship any of these recognizable defaults:

- A theme toggle, a stored theme choice, or a surface drawn for one theme only inside the app.
- A second gradient anywhere, or the ambient gradient on anything other than the page floor.
- Teal, green, or blue used as an accent, a link color, a button fill, or a chart series without meaning.
- All-caps eyebrows, kickers, or overlines above headings; decorative numbered section labels.
- Bellefair in a button, a label, a table, a badge, or below 24px; Bellefair set bold or in a second weight.
- Figtree at 700, or arbitrary sizes and weights outside the type roles.
- Monospace for dates, CVR numbers, currency, or section labels.
- Em dashes, en dashes, middle dots or a spaced hyphen as a dash in copy. Emojis anywhere.
- Title Case on a heading, button, tab or title; only the claim "Danish Company Data" keeps its capitals.
- A generic centered hero followed by a card grid; identical section silhouettes across unrelated reader questions.
- Repeated metric boxes when one composed relationship would be clearer.
- Cards nested inside cards, or borders used to repair weak hierarchy; a dark rounded rectangle around every chart.
- A badge or pill for ordinary metadata, chart annotations, or editorial labels; a green pill for "active".
- Drop shadows, glows, glass, textures, grid backgrounds, stripes, colored side rails.
- Icon tiles, oversized icons, mixed icon sets, icons as decoration.
- Tiny muted prose, ten-pixel labels, inconsistent peer values, misaligned baselines.
- A narrow table floating inside a wide section, or a wide table compressed into broken words.
- Zero rendered where the registry has no filing; a score rounded into an adjective; a lapsed signal shown as live.
- A legal form named anywhere but `legalFormLabel`, or shortened any way but its short label; month-first dates; hand-formatted numbers.
- Scroll reveals, page-load slide-ins, hover motion, marquees, typing cursors, pulsing status dots.
- Authoring-process narration in page copy; stock imagery; fake screenshots; decorative brand marks.

Do not compensate by producing a sterile anti-design template. Dandex restraint is an atmospheric floor, one serif claim, precise sans hierarchy, translucent surfaces that stay out of the way, and evidence that names its source. It is not merely black, thin rules, and empty margins.

## Use the published CSS API

Put `.dbg-report` on the page root and wrap standalone output in `.dbg-shell`. Use semantic HTML and only the primitives earned by the material.

The generator-facing shell and layout classes are:

`dbg-skip-link`, `dbg-header`, `dbg-masthead`, `dbg-identity`, `dbg-wordmark`, `dbg-mark`, `dbg-document-meta`, `dbg-recipient`, `dbg-state`, `dbg-date`, `dbg-confidentiality`, `dbg-context`, `dbg-opening`, `dbg-opening-claim`, `dbg-opening-proof`, `dbg-opening-context`, `dbg-section`, `dbg-chapter`, `dbg-reading`, `dbg-flow`, `dbg-stack`, `dbg-cluster`, `dbg-grid`, `dbg-split`, `dbg-band`, `dbg-surface`, `dbg-span-4`, `dbg-span-5`, `dbg-span-6`, `dbg-span-7`, `dbg-span-8`, `dbg-span-12`, `dbg-footer`.

The generator-facing type and evidence classes are:

`dbg-display`, `dbg-headline`, `dbg-title`, `dbg-page-title`, `dbg-heading-24`, `dbg-heading-20`, `dbg-heading-18`, `dbg-heading-16`, `dbg-lede`, `dbg-label`, `dbg-meta`, `dbg-caption`, `dbg-mono`, `dbg-numeric`, `dbg-visually-hidden`, `dbg-note`, `dbg-formula`, `dbg-sources`, `dbg-stat-strip`, `dbg-stat`, `dbg-stat-label`, `dbg-stat-value`, `dbg-stat-detail`, `dbg-comparison`, `dbg-table-wrap`, `dbg-chart`, `dbg-chart-header`, `dbg-chart-viewport`, `dbg-legend`, `dbg-bar-list`, `dbg-bar`, `dbg-bar-label`, `dbg-bar-value`, `dbg-bar-track`, `dbg-bar-fill`.

The Dandex entity classes are:

`dbg-breadcrumb`, `dbg-entity`, `dbg-entity-name`, `dbg-entity-meta`, `dbg-status`, `dbg-delta`, `dbg-facts`, `dbg-timeline`, `dbg-timeline-item`, `dbg-timeline-date`, `dbg-timeline-body`, `dbg-card`.

The generator-facing control classes are:

`dbg-calculator`, `dbg-calculator-inputs`, `dbg-calculator-output`, `dbg-control-group`, `dbg-field`, `dbg-unit-field`, `dbg-unit-prefix`, `dbg-unit-suffix`, `dbg-helper`, `dbg-error`, `dbg-range-ends`, `dbg-range-min`, `dbg-range-max`, `dbg-result-group`, `dbg-result`, `dbg-result-label`, `dbg-result-value`, `dbg-result-detail`, `dbg-button`, `dbg-input`.

Use these primitives according to their semantic names. A `.dbg-stat-strip` owns peer `.dbg-stat` blocks with label, value, and optional detail. A `.dbg-table-wrap` directly owns one semantic table. A `figure.dbg-chart` owns its header, focusable viewport with inline SVG, caption, and optional legend. A `.dbg-calculator` keeps its results, inputs, and output in one subtree. A `.dbg-entity` owns one `.dbg-entity-name` (the h1), an inline `.dbg-status`, and one `.dbg-entity-meta`. Do not interpose decorative wrappers or restyle foundation controls.

Status and tone attributes:

- `.dbg-status[data-tone="neutral" | "emphasis" | "secondary" | "positive" | "negative" | "warning" | "info"]`
- `.dbg-delta[data-sign="positive" | "negative" | "zero"]`
- `.dbg-note[data-tone="neutral" | "info" | "success" | "warning" | "error"]`
- `.dbg-button[data-variant="primary" | "outline" | "ghost" | "destructive" | "link"][data-size="xs" | "sm" | "lg"]`
- `.dbg-band[data-tone="secondary" | "contrast"]`, `.dbg-surface[data-variant="contained"]`, `.dbg-split[data-ratio="equal" | "wide"]`, `.dbg-opening[data-layout="split"]`, `.dbg-table-wrap[data-density="compact"]`, `.dbg-stat[data-priority="primary" | "supporting"]`, `.dbg-stat-strip[data-count="4"]`, `.dbg-report[data-ambient="off"]`
- `.dbg-shell[data-track="guided" | "reading" | "full"]` selects the 896px, reading, or full-width main track; the default is the 1024px entity track.
- `.dbg-label[data-variant="group"]` is the tracked uppercase field-group label, the only permitted all-caps role.
- `.dbg-card[data-size="sm"]`, `tr[data-highlight="true"]`, `.dbg-comparison > [data-recommended="true"]`, `.dbg-bar[data-emphasis="primary" | "secondary"]`, `.dbg-timeline-item[aria-current="true"]`.
- A bar's length is the inline custom property `--dbg-bar-width` on `.dbg-bar-fill` (`style="--dbg-bar-width: 62%"`), computed from the documented shared scale. It is the one token a page sets rather than reads.

Use the exact public child names. Do not invent synonyms such as `dbg-stat-note` for `dbg-stat-detail`:

```html
<div class="dbg-stat-strip">
  <div class="dbg-stat">
    <p class="dbg-stat-label">Revenue 2025</p>
    <p class="dbg-stat-value">12,757</p>
    <p class="dbg-stat-detail">DKK thousands, annual report via XBRL</p>
  </div>
</div>
```

```html
<header class="dbg-entity">
  <h1 class="dbg-entity-name">MS INVESTMENT ApS <span class="dbg-status" data-tone="negative">Dissolved after merger</span></h1>
  <p class="dbg-entity-meta"><span>CVR 35527028</span><span>Anpartsselskab</span><span>Computer programming</span></p>
</header>
```

Treat only the listed names and visualization names below as the public API. If none fits, use semantic HTML plus a page-owned `dbg-custom-*` or `dbg-viz-*` hook; never inspect the CSS for internal selectors, guess a `dbg-*` class, or extrapolate a name.

Published visualization classes are `dbg-chart-axis`, `dbg-chart-gridline`, `dbg-series-stroke`, `dbg-series-fill`, `dbg-data-point`, `dbg-chart-direct-label`, `dbg-chart-value`, `dbg-chart-annotation`, `dbg-chart-annotation-line`, and `dbg-series-1` through `dbg-series-6`. Combine the fill or stroke role with a numbered series class; never synthesize names such as `dbg-series-fill-1`. Use `dbg-viz-*` only for non-text visualization marks. Never apply custom visualization classes to SVG text.

Page-owned CSS may read only these public token families:

- Floor and atmosphere: `--dbg-floor`, `--dbg-teal-100`, `--dbg-teal-200`, `--dbg-teal-300`, `--dbg-selection`.
- Surfaces and text: `--dbg-surface-floor`, `--dbg-surface-primary`, `--dbg-surface-secondary`, `--dbg-surface-hover`, `--dbg-surface-contrast`, `--dbg-text-primary`, `--dbg-text-secondary`, `--dbg-text-tertiary`, `--dbg-text-on-contrast`, `--dbg-text-on-contrast-secondary`.
- Borders and state: `--dbg-border-subtle`, `--dbg-border-default`, `--dbg-border-strong`, `--dbg-border-on-contrast`, `--dbg-action`, `--dbg-action-hover`, `--dbg-action-text`, `--dbg-focus`, `--dbg-focus-ring`, `--dbg-color-info`, `--dbg-color-success`, `--dbg-color-warning`, `--dbg-color-error`, and the matching `--dbg-tint-*` and `--dbg-tint-border-*` pairs.
- Data: `--dbg-chart-1` through `--dbg-chart-6`.
- Rhythm and shape: `--dbg-space-1`, `--dbg-space-2`, `--dbg-space-3`, `--dbg-space-4`, `--dbg-space-5`, `--dbg-space-6`, `--dbg-space-8`, `--dbg-space-10`, `--dbg-space-12`, `--dbg-space-16`, `--dbg-radius-small`, `--dbg-radius`, `--dbg-radius-large`, `--dbg-radius-card`, `--dbg-radius-pill`.
- Type size: `--dbg-type-display`, `--dbg-type-headline`, `--dbg-type-page-title`, `--dbg-type-title`, `--dbg-type-section`, `--dbg-type-subsection`, `--dbg-type-heading`, `--dbg-type-lede`, `--dbg-type-body`, `--dbg-type-compact`, `--dbg-type-label`, `--dbg-type-metadata`.
- Type face, weight, and leading: `--dbg-font-sans`, `--dbg-font-display`, `--dbg-font-mono`, `--dbg-weight-regular`, `--dbg-weight-medium`, `--dbg-weight-semibold`, `--dbg-weight-display`, `--dbg-tracking-display`, `--dbg-tracking-heading`, `--dbg-leading-body`, `--dbg-leading-compact`, `--dbg-leading-caption`, `--dbg-leading-display`, `--dbg-leading-headline`, `--dbg-leading-page-title`, `--dbg-leading-title`, `--dbg-leading-section`, `--dbg-leading-subsection`, `--dbg-leading-lede`.

Use the exact names with `var()`. Never invent, alias, or redeclare a `--dbg-*` token. Prefer `currentColor`, `inherit`, or `transparent` when a custom mark needs no distinct role. Every custom `font-weight` uses a published weight token.

## Host mapping

Inside the Next.js app the same system is expressed through Tailwind utilities and the shadcn tokens in `app/globals.css`. Use this table to translate; do not add `dbg-*` classes or the stylesheet to app code.

| System role | Foundation token | In the app |
| --- | --- | --- |
| floor | `--dbg-floor` | `GradientBackground` in the marketing layout; `themeColor` in `app/layout.tsx`, one per theme; `bg-floor` (`--floor` in `app/globals.css`) for an opaque fill inside the page, such as a frozen table column |
| primary text | `--dbg-text-primary` | `text-foreground` |
| secondary text | `--dbg-text-secondary` | `text-muted-foreground` |
| placeholder | `--dbg-text-tertiary` | `placeholder:text-foreground/40` |
| hairline | `--dbg-border-subtle` | `border-hairline`, `divide-hairline`, or `bg-hairline` as a 1px div (`--hairline` in `app/globals.css`) |
| default border, card ring | `--dbg-border-default` | `border-foreground/10`, `ring-1 ring-foreground/10` |
| strong border | `--dbg-border-strong` | `border-foreground/20` |
| resting surface | `--dbg-surface-secondary` | `bg-foreground/5` |
| hover surface | `--dbg-surface-hover` | `hover:bg-foreground/10`, `hover:bg-muted/50` |
| opaque panel | `--dbg-surface-primary` | `bg-card`, `bg-popover` |
| input fill and border | `--dbg-gray-alpha-200`, `--dbg-gray-alpha-400` | `bg-input/30`, `border-input` (the `Input` primitive) |
| primary action | `--dbg-action` | `Button` default variant (`bg-primary text-primary-foreground`) |
| focus | `--dbg-focus-ring` | `focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:border-ring` (built into the primitives) |
| destructive | `--dbg-color-error` | `text-destructive`, `bg-destructive/10` |
| success | `--dbg-color-success` | `text-success` for figures, `bg-success/10 border-success/20` for a note |
| warning | `--dbg-color-warning` | `text-warning`, `bg-warning/10 border-warning/30` for a note |
| info | `--dbg-color-info` | `text-info`, `bg-info/10 border-info/20` for a note (`--info` in `app/globals.css`) |
| selection | `--dbg-selection` | `::selection` rule in `app/globals.css` |
| display face | `--dbg-font-display` | `font-[family-name:var(--font-bellefair)]` (the -0.07em tracking is applied globally by the `[class*="--font-bellefair"]` rule) |
| sans face | `--dbg-font-sans` | `font-sans` (set on `<body>`) |
| pill | `--dbg-radius-pill` | `rounded-4xl` (the primitives already use it) |
| card | `--dbg-radius-card` | `rounded-2xl` (the `Card` primitive) |
| tab trigger, textarea | `--dbg-radius-large` | `rounded-xl` |
| note, code block | `--dbg-radius` | `rounded-lg` |
| status pill | `.dbg-status` | `Badge` with `outline`, `default`, `secondary`, or `destructive` |
| change type pill | `.dbg-status` | `ChangeTypeBadge` in `components/changefeed` |
| navigable card | `.dbg-card` | `Card` inside a `Link`, `hover:bg-muted/50 transition-colors` |
| shell | `.dbg-header`, `.dbg-footer` | `app/(marketing)/layout.tsx` |
| stat | `.dbg-stat` | a `p` at `text-2xl font-semibold tabular-nums` over a `text-xs text-muted-foreground` label |
| facts | `.dbg-facts` | `FactList` and `FactRow` in `components/facts.tsx`: label beside value from sm, one hairline between rows |
| page title, headline | `.dbg-page-title`, `.dbg-headline` | `PageTitle` and `Headline` in `components/type/page-title.tsx`; never restate the Bellefair classes |
| section | `.dbg-section` | `Section` (and `SectionTitle`) in `components/section.tsx`, an h2 in the heading-18 role |
| note | `.dbg-note[data-tone]` | `Note tone=` in `components/note.tsx`, over the `Alert` primitive |
| locked value | | `ProMark` (lock and a 24px "Pro" link with an `srLabel`), `LockedFact` and `LockedCell` in `components/reader/locked.tsx` |
| missing value | | `Missing kind=` in `components/missing.tsx`: "Not reported", "PDF only", "Not registered", at the size of its cell |
| table cells | `.dbg-numeric` | `HEAD_ROW`, `ROW`, `TH`, `TH_NUM`, `TD`, `TD_NUM` and `FROZEN` in `components/table/classes.ts` |
| breadcrumb, pager | `.dbg-breadcrumb` | `Breadcrumb` in `components/breadcrumb.tsx`, `Pager` (outline pills) in `components/pager.tsx` |

Type roles in the app: `display` is `text-[clamp(1.5rem,7vw,4.5rem)]` (the clamp in the table above), `page-title` is `text-3xl sm:text-4xl` (`PageTitle`), `headline` is `text-4xl md:text-5xl lg:text-6xl leading-[1.1]` (`Headline`), all in Bellefair with `font-normal`. `heading-24` is `text-2xl font-semibold`, `heading-20` is `text-xl font-semibold`, `heading-18` is `text-lg font-semibold`, `heading-16` is `text-base font-medium`, `body` is `text-base`, `compact` is `text-sm`, `label` is `text-xs font-medium`, `metadata` is `text-xs text-muted-foreground`.

## Email

Transactional email (welcome, re-issue, portal link, limit reached, contact, feedback) is authored in `lib/email/templates` as inline-styled HTML. It follows the dark theme, with hex values that mail clients render reliably:

| Role | Value |
| --- | --- |
| page background | `#09090b` |
| card | `#18181b`, 1px `#27272a` border, 12px radius, 32px padding |
| code or nested block | `#09090b` on `#27272a` border, 8px radius |
| heading | `#fafafa`, 22px, 600 |
| body | `#e4e4e7`, line-height 1.6 |
| secondary | `#a1a1aa` |
| muted | `#71717a` |
| faint footer | `#52525b`, 12px |
| button | `#fafafa` fill, `#18181b` text, 8px radius, 10px by 24px padding, 14px 500 |

The font stack is `-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif`; do not load web fonts. Width is 600px. The email header is the word "Dandex" in text, not the SVG wordmark, because image loading is unreliable. Every email has a plain-text twin. No gradients, no images, no emojis.

## Accessibility and responsive behavior

Use landmarks, one descriptive `h1`, ordered headings, a skip link, native controls, semantic tables, figures with captions, accessible names on icon-only controls, visible focus, and text alternatives. Meet WCAG AA against the floor of each theme; secondary text passes on both floors as declared, so do not move a floor toward its text or a text colour toward its floor. Never rely on color alone: status has a word, a delta has a sign, a tool state has a label. Treat source order as reading order. Set `lang="en"` on the document and `lang="da"` on Danish content runs so screen readers switch voices.

Do not conceal page overflow. Give grid and flex children `min-width: 0`; reflow before shrinking. Preserve readable type (12px minimum) and control sizes (36px, 44px on touch). Short comparisons may stack; long ledgers may scroll locally when reordering and simplification cannot preserve lookup. The landing claim stays on one line by shrinking through its clamp, not by wrapping. The page must remain usable across desktop and narrow screens, in both themes.

## Guard tests

`lib/__tests__/design-rules.test.ts` fails on `dbg-` classes in app code, text below 12px, `font-bold`, raw palette colours, `rounded-sm` or `rounded-md` outside `components/ui`, Bellefair below 24px and low-alpha running text. Each rule keeps a list of allowed files with the reason, and the lists only shrink. `lib/__tests__/control-height.test.ts` holds every control to 36px, and `lib/__tests__/copy.test.ts` holds the copy rules above.

`components.json` declares `hugeicons` as the icon library, but application code uses `lucide-react`. Lucide is the standard; the setting only affects newly generated shadcn primitives.

The target is Dandex judgment, not Dandex decoration.
