# RiLiGar Zen — Especificação completa para agentes > Design system autocontido. Tudo o que é necessário para construir, atualizar e **validar** uma interface RiLiGar está neste documento — não é preciso abrir o repositório. Versão 1.0.0 · extraído de `@riligar/components-kit` (24 componentes, 6 primitivas). Cada regra foi medida na frequência real de uso no código em produção. As contagens (`44×`, `57 ocorrências`) são dados, não estimativas. --- ## Como usar este documento 1. Leia **Regras invariantes** — são bloqueios, não sugestões. 2. Consulte **Tokens** para qualquer valor. Nunca invente um valor novo. 3. Use **API dos componentes** para escrever o código. 4. Copie de **Receitas** quando a tarefa se encaixar num padrão conhecido. 5. Antes de entregar, rode o **Protocolo de validação** — ele é executável. --- ## O sistema em 30 segundos ``` Uma fonte Montserrat, pesos 500 / 700 / 800 / 900 — nenhum outro Uma cor cinza, 10 degraus, #FFFFFF → #11181C Um raio 0 (57 ocorrências no kit, 0 exceções) Um contorno 1px solid #F3F4F6 Quatro papéis label 10–13px · dado 12–14px · corpo 15–20px · display 32–120px Dois fundos #FFFFFF e #F9FAFB Duas densidades palco (convencer) e painel (operar) — mesmos tokens ``` Não há cor de marca, sombra, canto arredondado ou segunda família tipográfica. **Tamanho, peso e espaço são os únicos instrumentos de hierarquia.** --- ## As cinco leis ### Lei 1 — Conteúdo sobre cromo **Cada elemento de cromo precisa responder "que informação eu comunico?". Se a resposta for "nenhuma, é estética", ele sai.** Sobreviveram ao corte: o filete de 1px (comunica separação), o numeral fantasma de Steps (comunica ordem), o badge dot (categoria com mínimo de tinta). Foram cortados: sombras, bordas de Card, raio. _Quando a página parece vazia demais, o erro está no conteúdo, não no design. A resposta é escrever melhor, não adicionar moldura._ ### Lei 2 — Hierarquia massiva **Não existe cor de destaque. A ênfase vem de contraste de escala, e o contraste precisa ser violento: salto de 3× a 6× entre níveis, nunca 1,2×.** O par canônico é o cabeçalho de palco: label minúsculo, leve em tinta e disperso em tracking, imediatamente acima de um título enorme, pesado e apertado. Opostos em cinco dimensões ao mesmo tempo — é isso que cria a tensão. _Implementado em SectionHeader. Toda seção do kit passa por ele._ ### Lei 3 — O Palco **Toda seção é um palco: fundo próprio, respiro vertical generoso, e um anúncio antes da performance.** Não é opcional nem reimplementado por seção — é a primitiva Section. As únicas exceções (Header, Hero, SocialProof, Footer) existem porque têm geometria própria. _Palcos consecutivos alternam white ↔ gray.1. É a única marcação de ritmo da página._ ### Lei 4 — Config-driven, não JSX **O consumidor passa dados, nunca marcação.** Não é preferência de API — é o que mantém as leis 1–3 aplicáveis. Se o consumidor puder injetar JSX, ele injeta cromo, e o sistema erode em três sprints. _Contrato padrão: id, badge, subtitle, title, background, items, cta, note._ ### Lei 5 — Honestidade verificável **Cada afirmação forte carrega o que a torna checável.** Por isso StatsGrid tem description sob cada número e note sob a grade; Callout tem rodapé de honestidade; Hero tem note com o que custa experimentar. A nota é sempre o degrau mais baixo: 13px, gray.6, lh 1.5. _Sem esta lei, hierarquia massiva vira só um número grande sem lastro. A nota é o que separa design de propaganda._ --- ## Regras invariantes Violar qualquer uma destas reprova o trabalho. Não são preferências: | Regra | Valor | Lei | | --- | --- | --- | | `border-radius` | **sempre 0**, sem exceção | 1 | | `box-shadow` | **nunca** | 1 | | Cor | só a rampa gray (exceção única: `red.6` para perda quantificada) | 1 | | Peso da fonte | só 500, 700, 800, 900 | 2 | | `letter-spacing` | só `-0.04em`, `1.5px`, `2px` | 2 | | Salto tipográfico | ≥3× entre níveis adjacentes | 2 | | Cor de corpo | `gray.6` — nunca `gray.9` | 2 | | Cor de título | `gray.9` — nunca `gray.6` | 2 | | `gray.4` | só em label uppercase (falha AA em corpo) | 2 | | Borda | `1px solid gray.2` | 1 | | Palco | envolver `Section`, nunca reimplementar | 3 | | Props | dados, nunca JSX | 4 | | Fundo | alterna `white` ↔ `gray` a cada seção | 3 | | Mobile | sempre 1 coluna | 3 | | Métrica grande | sempre acompanhada de nota de verificação | 5 | | Piso tipográfico | nada abaixo de 12px, exceto label uppercase (10–11px) | 2 | | Erro | sempre `red.6` — nunca a mesma cor do sucesso | 5 | | Skeleton | reproduz a grade real do conteúdo | 3 | --- ## Tokens — Cor ### A rampa Degraus 0–3 são superfície e estrutura; 4–9 são tinta. Nenhum atravessa a fronteira. | Token | Hex | Papel | Uso | Freq. | | --- | --- | --- | --- | --- | | `gray.0` | `#FFFFFF` | App base | Fundo branco puro | — | | `gray.1` | `#F9FAFB` | Superfície alternada | Palco alternado, numeral fantasma | — | | `gray.2` | `#F3F4F6` | Filete padrão | Toda borda do sistema | 10× | | `gray.3` | `#E5E7EB` | Borda muda | Citação, estado desabilitado | — | | `gray.4` | `#9CA3AF` | Texto quaternário | Label uppercase apenas | 7× | | `gray.5` | `#6B7280` | Texto terciário | Rótulos, legendas | 20× | | `gray.6` | `#4B5563` | Texto secundário | Corpo — a cor mais usada | 50× | | `gray.7` | `#374151` | Corpo enfático | Descrição de destaque | 6× | | `gray.8` | `#1F2937` | Texto primário escuro | Raro | 4× | | `gray.9` | `#11181C` | Preto RiLiGar | Títulos, métricas, botão preenchido | 36× | **As duas cores de trabalho:** `gray.9` para o que grita (36×), `gray.6` para o que fala (50×). Todo o resto é ajuste fino. ### Os três pretos - `gray.9` (#11181C) — títulos, métricas, fundo de botão. Não é `#000`: o preto puro em tela grande vibra e cansa. - `gray.7` (#374151) — corpo quando precisa de mais presença (parágrafo curto e central). - `gray.6` (#4B5563) — corpo padrão. Em dúvida sobre a cor de um parágrafo, é esta. ### Fundos de seção ```js SECTION_BACKGROUNDS = { white: 'white', gray: 'gray.1' } ``` Palcos consecutivos alternam. O contraste é de ~2% de luminância — deliberado: não separa as seções, apenas dá textura ao scroll. A separação real vem do `py` de 80–120px. ### Exceção cromática `red.6` (#E03131) — Custo/perda quantificada. Aparece uma vez no kit inteiro, em `Comparison.jsx`, sob o custo da coluna "modo tradicional". É semântica de **perda**, não de erro genérico. ### Contraste (sobre #FFFFFF) | Token | Ratio | Status | | --- | --- | --- | | `gray.9` | 16,8:1 | AAA | | `gray.7` | 10,4:1 | AAA | | `gray.6` | 7,5:1 | AAA | | `gray.5` | 4,9:1 | AA em corpo | | `gray.4` | 2,6:1 | **Falha AA em corpo** | `gray.4` só é legítimo em label uppercase peso 800 — texto curto, não-essencial, redundante com o título abaixo. Nunca em parágrafo. --- ## Tokens — Tipografia ### Família ```css font-family: Montserrat, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; ``` Pilha sistema-primeiro: se o Google Fonts falhar, degrada para a fonte nativa, não para Times. ### Pesos | Peso | Nome | Uso | Freq. | | --- | --- | --- | --- | | **900** | Black | Títulos, métricas, wordmark | 44× | | **800** | ExtraBold | Labels uppercase, botões, badges | 20× | | **700** | Bold | Ênfase em corpo, rótulo de métrica | 10× | | **500** | Medium | Legenda técnica, letra miúda | 5× | Não existe 400 declarado: quando o peso é escrito, é para subir. **900 e 800 nunca se tocam no mesmo bloco** sem um salto de tamanho de 4× separando-os. **600 não existe neste sistema.** Está próximo demais de 700 para carregar significado distinto — onde aparecer, vira 700. ### Os quatro papéis **Label** — _o anúncio_ ``` 10–13px · peso 800 · ls 1.5px · gray.4 · lh 1.4 · UPPERCASE ``` **Corpo** — _a performance_ ``` 15–20px · peso 400 · ls normal · gray.6 · lh 1.7 ``` **Dado** — _a evidência_ ``` 12–14px · peso 500 · ls normal · gray.7 · lh 1.4 ``` **Display** — _a declaração_ ``` 32–120px · peso 900 · ls -0.04em · gray.9 · lh 1.2 ``` ### Escala fluida ```css --fz-hero: clamp(2.5rem, 8vw + 1rem, 6rem); /* 40 → 96px — Primeira dobra */ --fz-section-title: clamp(2rem, 5vw + 1rem, 3.5rem); /* 32 → 56px — Título de seção — o mais frequente */ --fz-big-metric: clamp(3rem, 10vw + 1rem, 7.5rem); /* 48 → 120px — Métrica — a mais dramática */ --fz-big-metric-sm: clamp(3rem, 5vw, 4.5rem); /* 48 → 72px — Métrica em grade de 4 colunas */ ``` Fórmula sempre `clamp(min, Nvw + 1rem, max)`. O `+ 1rem` preserva a preferência de fonte do usuário — `vw` puro a ignora. Nenhum título tem tamanho fixo. ### Tracking | Nome | Valor | Uso | | --- | --- | --- | | tight | `-0.04em` | Todo display e título | | control | `1px` | Botão, link de navegação | | label | `1.5px` | Todo label uppercase | | wide | `2px` | Label de destaque, wordmark | `-0.04em` é relativo (escala com a fonte fluida); `1.5px` é absoluto (label é sempre 10–13px). A inversão de sinal é o ponto: display precisa reagrupar, label precisa respirar. ### Line-height | Valor | Uso | Freq. | | --- | --- | --- | | `1` | Métrica | 7× | | `1.1` | Hero | 1× | | `1.2` | Título de seção | 3× | | `1.4` | Rótulo compacto | 6× | | `1.5` | Nota | 3× | | `1.6` | Corpo curto | 16× | | `1.7` | Corpo padrão | 25× | | `1.8` | Bloco longo (legal) | 6× | ### O par canônico O padrão que define visualmente a marca — label e título opostos em cinco dimensões ao mesmo tempo: | Dimensão | Label | Título | | --- | --- | --- | | Tamanho | 11px | 44px | | Peso | 800 | 900 | | Caixa | Alta | Sentença | | Tracking | +1.5px | −0.04em | | Cor | `gray.4` | `gray.9` | ```jsx Prova verificável Números que você pode conferir. ``` Nunca reimplemente à mão — use `SectionHeader` ou `Section`. --- ## Tokens — Espaço ### A escala | Token | px | Uso | Freq. | | --- | --- | --- | --- | | `xs` | 10 | Dentro de um par (label ↔ título) | 18× | | `sm` | 12 | Itens de uma lista | 12× | | `md` | 16 | Padding de controle | 7× | | `lg` | 20 | Padding de card compacto | 5× | | `xl` | 32 | Entre blocos de uma seção | 18× | **`xs` e `xl` dominam com 18 usos cada.** Ou colado (é um só objeto), ou muito separado (são objetos distintos). O meio-termo é raro por design — ambiguidade de agrupamento é ruído. ### Ritmo vertical | Contexto | Valor | | --- | --- | | Padrão | `{ base: 80, md: 120 }` | | Hero | `{ base: 80, sm: 100, md: 140 }` | | Denso | `{ base: 40, md: 60 }` | ### Containers | Size | Largura | Quando | | --- | --- | --- | | `md` | 980px | Leitura contínua: legal, FAQ, artigo | | `lg` | 1180px | Padrão de toda seção de landing | | `xl` | 1320px | Faixas full-width: Header, Footer | ### Larguras de leitura (`maw`) | maw | Uso | | --- | --- | | `900` | Título centralizado | | `800` | Título padrão (default de Section) | | `780` | Parágrafo de introdução | | `700` | Título de CTABanner | | `620` | Corpo longo — 60–75 caracteres | | `550` | Descrição centralizada de CTA | | `220` | Nota sob métrica | **Regra:** título até 900. Corpo nunca acima de 780 (ideal 620 = 60–75 caracteres). Nota sempre estreita. ### Breakpoints | Token | em | px | Papel | | --- | --- | --- | --- | | `xs` | 36em | 576 | Raro | | `sm` | 48em | 768 | 1 col → 2 col | | `md` | 62em | 992 | O salto principal: mobile → desktop | | `lg` | 75em | 1200 | 2 col → 4 col | | `xl` | 88em | 1408 | Raro | Quase todo responsivo do kit é `{ base, md }`. **Mobile é sempre 1 coluna.** ### O palco ```jsx {/* separa anúncio de conteúdo */} {/* gap xs interno cola o par */} {children} ``` --- ## Tokens — Forma ### Raio: 0 `radius="0"` aparece **57 vezes** no kit; qualquer outro valor, **0 vezes**. É invariante, não preferência. Canto arredondado é cromo puro — não comunica nada, apenas suaviza, e suavizar é o oposto do que a hierarquia massiva precisa. O canto reto também alinha bordas adjacentes ao pixel. A única forma curva do sistema é o ponto do `Badge variant="dot"`, que é conteúdo. ### Sombra: nenhuma Profundidade vem de: contraste de superfície (card `white` sobre palco `gray.1`), filete de 1px, ou camada real via `z-index`. - **`Paper`** = superfície declarada (`withBorder`, `bg white`). Quando o bloco precisa se destacar do palco. - **`Card`** = agrupamento invisível (`withBorder: false`, `bg transparent`). Quando o espaço já basta. **Na dúvida, este.** ### Bordas | Nome | Valor | Uso | | --- | --- | --- | | Padrão | `1px solid gray.2` | Delimitação neutra | | Acento | `4px solid gray.2` | Topo do CTABanner: "aja aqui" | | Citação | `2px solid gray.3` | Compete com texto, não com fundo | | Input | `1px solid gray.2` | Borda inteira, radius 0 | ### Inputs O input é uma caixa de borda inteira — o campo se declara antes de ser tocado. ```js TextInput: { defaultProps: { size: 'lg', radius: '0' }, styles: { input: { border: '1px solid var(--mantine-color-gray-2)', paddingLeft: 16, paddingRight: 16 } }, } ``` `size="lg"` porque o alvo de toque precisa ser generoso; a borda de 1px em volta declara a área clicável antes do toque. ### Botões Um botão é um **label num retângulo preto**: `gray.9` preenchido, texto uppercase peso 800, `ls 1px`, raio 0. Hierarquia sem cor: `filled` (primário) → `outline` (secundário) → `subtle` (terciário). ### Movimento A única animação do sistema: ```css transition: transform 0.2s ease, filter 0.2s ease; /* :hover */ transform: translateY(-1px); filter: brightness(1.1); ``` 1px de subida, 10% de brilho. Sem escala, sem troca de cor, sem sombra crescendo. ### Ícones Tabler Icons, sempre `stroke={1.5}` — fino o bastante para não competir com texto peso 800 ao lado. `ThemeIcon` é sempre quadrado (`radius="0"`), nunca círculo. --- ## Palco e painel — uma escala, dois problemas Este é **um** design system. As leis, os tokens e a rampa são os mesmos em qualquer tela. O que muda é a densidade, e ela é decidida pelo problema que a tela resolve — não por ela ser "marketing" ou "app". | Superfície | Trabalho | Onde | Ritmo | Tipografia | | --- | --- | --- | --- | --- | | **Palco** | Convencer | Landing, marketing, legal, 404 | `py 80–120 · Container lg · gap xl` | Corpo 15–20px · display 32–120px | | **Painel** | Operar | Dashboard, listas, formulários, configurações | `px 48 · pt 40 · maw 1280 · gap 16–32` | Dado 12–14px · título de página 36px | A regra que separa os dois papéis de texto: **texto que se lê em sequência é `body`; texto que se compara em coluna é `data`.** Um parágrafo de 16px é confortável; uma tabela de 200 linhas em 16px é impossível de varrer. ### Geometria do painel ``` Sidebar 300px (colapsada 64px) · bg white · borderRight 1px solid gray.2 Conteúdo px 48 · pt 40 · pb 80 · maw 1280 Cabeçalho mb 32 Blocos gap 16 ``` O palco usa `py 80–120` porque separa seções de uma narrativa. O painel não tem seções: tem uma tela só com barra lateral fixa. O respiro externo vira padding do container. --- ## Cabeçalho de página O par canônico em versão de painel: label pequeno colado a um título pesado. Duas diferenças do palco — o título é 36px fixo (o painel tem largura controlada, não precisa escalar com a viewport) e a linha ganha zona de ações à direita. | Parte | Spec | Papel | | --- | --- | --- | | `crumb` | fz 10 · fw 800 · gray.4 · uppercase · ls 1.5px | Onde você está | | `title` | fz 36 · fw 900 · lh 1 · ls -0.04em | O que é esta tela | | `subtitle` | fz 15 · fw 500 · gray.6 · lh 1.5 | O que dá para fazer aqui | | `actions` | Group gap md, à direita | Um botão primário; busca opcional | ```jsx {crumb} {title} {subtitle} {search} {actions} ``` **Um botão primário por tela.** Se há duas ações igualmente importantes, uma delas não é. --- ## Navegação lateral O item ativo é marcado por **preenchimento `gray.9` + peso 800** contra 500 dos inativos. É a lei 2 aplicada à navegação: contraste de tinta e peso, sem introduzir cor de destaque. | Parte | Spec | | --- | --- | | `largura` | 300px expandida · 64px colapsada | | `superfície` | bg white · borderRight 1px gray.2 | | `marca` | Wordmark 28px no topo | | `contexto` | Switcher org → aplicação em Popover, com busca própria | | `label de seção` | fz 10 · fw 800 · gray.4 · uppercase · ls 1.5px | | `item` | fz 13 · px 12 · py 9 · ícone 18px stroke 1.5 · radius 0 | | `item ativo` | bg gray.9 · texto branco · fw 800 | | `item inativo` | transparente · gray.7 · fw 500 | | `rodapé` | Docs, suporte e conta, separados por borderTop 1px gray.2 | --- ## Tabela A implementação é **CSS Grid, não ``**: as colunas precisam de proporções (`2.4fr 1fr 1fr 40px`) e a linha inteira precisa ser clicável. Grid resolve as duas coisas sem lutar contra o layout de tabela. | Parte | Spec | | --- | --- | | `container` | Card p={0} withBorder · overflow hidden | | `header` | bg gray.1 · padding 12px 24px · borderBottom 1px gray.2 | | `header cell` | fz 10 · fw 800 · gray.5 · uppercase · ls 1.5px | | `row` | padding 14px 24px · borderBottom 1px gray.2 (exceto a última) | | `cell` | fz 13 · gray.7 · lh 1.4 — o papel "dado" | | `hover` | background gray.1 · transition 120ms | | `destaque` | background gray.1 + boxShadow inset 2px 0 0 gray.9 | | `grade` | gridTemplateColumns com fr + coluna fixa 40px para ações | ```jsx const COLS = '2.4fr 1fr 1fr 40px' // fr para conteúdo, px fixo para ações Membro Status Criado {items.map((item, i) => ( onOpen(item)} style={{ display: 'grid', gridTemplateColumns: COLS, gap: 12, alignItems: 'center', padding: '14px 24px', cursor: 'pointer', borderBottom: i === items.length - 1 ? 'none' : '1px solid var(--mantine-color-gray-2)', transition: 'background 120ms ease' }}> {/* células em fz 13, c gray.7 — o papel "dado" */} ))} ``` **Sem paginação e sem seleção múltipla** no padrão atual. Ao introduzir qualquer um dos dois, documente aqui antes. --- ## Busca Um padrão, dois alcances: **global** (⌘K, server-side, cruza entidades) e **local** (filtra a lista da tela). As duas usam a mesma caixa e o mesmo ícone; muda o alcance. | Aspecto | Valor | | --- | --- | | `shortcut` | ⌘K / Ctrl+K | | `minLength` | 2 | | `debounceMs` | 250 | | `iconSize` | 16 | | `escape` | Limpa o termo e devolve o foco | | `abort` | AbortController cancela a requisição anterior a cada tecla | | `dropdown` | Nunca abre vazio: ou tem resultado, ou tem o aviso de nenhum | | `rightSection` | Kbd com o atalho quando vazio; Loader 14px durante a busca | **Só um componente registra ⌘K por tela.** Dois listeners competem pelo foco. ```jsx const [debounced] = useDebouncedValue(query, 250) useEffect(() => { const term = debounced.trim() requestRef.current?.abort() // cancela a busca anterior if (term.length < 2) return setResults([]) const controller = new AbortController() requestRef.current = controller search(term, { signal: controller.signal }) .then(r => !controller.signal.aborted && setResults(r.data)) .catch(() => {}) return () => controller.abort() }, [debounced]) ``` --- ## Estados **Skeleton domina spinner.** O skeleton reproduz a grade real do conteúdo, então o layout não salta quando o dado chega. Spinner só onde não há forma a antecipar. | Estado | Padrão | Spec | | --- | --- | --- | | **Carregando (primeira vez)** | Skeleton com a grade exata do conteúdo | Mesmo gridTemplateColumns, mesmo padding. radius 0. | | **Carregando (refetch)** | Nada, ou LoadingOverlay sutil | O dado antigo continua legível — não pisca skeleton por cima. | | **Vazio (sem dado)** | Label uppercase + subtítulo + ação | Stack align center py 56 · título fz 10 fw 800 gray.4 uppercase ls 1.5px | | **Vazio (busca sem resultado)** | Mesma anatomia, citando o termo | "Nenhum resultado para «termo»" + botão de limpar busca | | **Erro** | Notificação vermelha, nunca cinza | red.6 com IconAlertCircle. Sucesso e erro precisam ser distinguíveis sem ler. | | **Bloqueado (falta contexto)** | Gate com skeleton | Sem aplicação selecionada, a tela mostra a forma, não um erro. | --- ## Formulários **Modal, nunca drawer.** | Regra | Detalhe | | --- | --- | | **Modal** | size md · centered · padding xl | | **Título** | ThemeIcon 32 + título h4 + descrição em gray.6 | | **Validação** | @mantine/form, síncrona, mensagem inline em português | | **Botões** | Submit à direita; destrutivo à esquerda. Sem "Cancelar" — o X do modal basta | | **Feedback** | notifications.show no sucesso e no erro, com cores distintas | | **Loading** | Botão com loading + disabled; overlay no corpo se a carga for longa | --- ## Ação destrutiva Confirmação por **permanência**, não por diálogo. Um "tem certeza?" é respondido no automático; segurar exige intenção contínua e é reversível até o último instante. | Aspecto | Valor | | --- | --- | | `holdMs` | 3000 | | `tickMs` | 50 | | `feedback` | RingProgress thickness 2 sobre o ícone | | `cancel` | onMouseUp · onMouseLeave · onTouchEnd | | `variants` | ícone (tabela) · texto com contagem regressiva (formulário) | | `escalation` | Impacto irreversível e amplo → Modal dedicado listando o que será perdido | > **Exigência aberta:** o padrão atual responde só a mouse e toque. Sem equivalente por teclado, a exclusão fica inacessível — resolva isso antes de replicar o padrão. --- ## Setup ```bash bun add @riligar/components-kit @mantine/core @mantine/hooks @tabler/icons-react ``` ```jsx import '@mantine/core/styles.css' import '@riligar/components-kit/style.css' import { MantineProvider } from '@mantine/core' import { theme } from '@riligar/components-kit' export default function App() { return ( {/* suas rotas */} ) } ``` > O theme exportado pelo kit já carrega toda a paleta, tipografia e defaults de componente. Não redefina tokens no consumidor — se um valor precisa mudar, ele muda no kit. **Peer dependencies:** | Pacote | Versão | | --- | --- | | `@mantine/core` | `^7.0.0 || ^8.0.0` | | `@mantine/hooks` | `^7.0.0 || ^8.0.0` | | `@tabler/icons-react` | `^3.0.0` | | `react` | `^18.0.0 || ^19.0.0` | | `react-dom` | `^18.0.0 || ^19.0.0` | --- ## Contrato universal de props Toda seção do kit aceita este conjunto. O mesmo shape em todos os componentes é o que permite mover um CTA de seção sem reescrevê-lo. | Prop | Tipo | Papel | | --- | --- | --- | | `id` | `string` | Âncora de scroll | | `badge` | `string` | Selo pontilhado acima do label | | `subtitle` | `string` | Label uppercase — o anúncio | | `title` | `string | node` | Título massivo | | `description` | `string` | Parágrafo de introdução | | `background` | `'white' | 'gray'` | Fundo do palco | | `ta` | `'left' | 'center'` | Alinhamento | | `items` | `object[]` | Os dados | | `cta` | `{ label, href, icon, variant, target }` | Ação | | `note` | `string` | Nota de verificação (Lei 5) | --- ## Primitivas ### Section O palco. Envolve qualquer seção com fundo, respiro e cabeçalho. | Prop | Tipo | Nota | | --- | --- | --- | | `children` | `ReactNode` | Conteúdo — a performance | | `badge` | `string` | Selo pontilhado acima do label | | `title` | `string | ReactNode` | Título massivo | | `subtitle` | `string | ReactNode` | Label uppercase — o anúncio | | `id` | `string` | Âncora de scroll | | `background` | `'white' | 'gray'` | Default: 'white' | | `py` | `number | object` | Default: { base: 80, md: 120 } | | `maw` | `number` | Max-width do título. Default: 800 | | `ta` | `'left' | 'center'` | Default: 'left' | | `stackGap` | `string` | Default: 'xl' | ```jsx
{/* conteúdo */}
``` ### SectionHeader O par canônico: label leve + título pesado. | Prop | Tipo | Nota | | --- | --- | --- | | `badge` | `string` | Selo pontilhado | | `subtitle` | `string | ReactNode` | Label uppercase | | `title` | `string | ReactNode` | Título | | `titleOrder` | `number` | Nível do heading. Default: 2 | | `titleFz` | `string` | Default: 'var(--fz-section-title)' | | `ta` | `'left' | 'center'` | Default: 'left' | | `maw` | `number` | Max-width do título | ```jsx ``` ### CTAButton Botão padronizado. Vira com href,