--- name: KeluargaMart ERP Frontend description: Desk-and-mouse ERP for warehouse, finance, sales and HR staff. Operate mode — density and scanability outrank expression. Brand lives in precise details, never decoration. mode: Operate stack: Vue 3 + Vite + Tailwind CSS v4 (tokens in src/assets/base.css @theme) brand: 50: "#F0F5FF" 100: "#DCE7FC" 200: "#C4D4F5" 300: "#9DB8EA" 400: "#28459A" 500: "#142B80" 600: "#102268" 700: "#071F68" 800: "#051952" 900: "#04123D" surfaces: page: "#F5F8FF" surface: "#FFFFFF" surface-muted: "#EEF3FA" border: "#D9E2F0" border-strong: "#BCCBE4" ink: ink: "#14213D" ink-muted: "#475569" ink-subtle: "#64748B" ink-disabled: "#94A3B8" status: primary: { fg: "#142B80", soft: "#DCE7FC", line: "#C4D4F5" } success: { fg: "#15803D", soft: "#ECFAF0", line: "#BBF7D0" } warning: { fg: "#B45309", soft: "#FFF8EB", line: "#FDE68A" } danger: { fg: "#C0372C", soft: "#FBE7E4", line: "#F3C6C0" } info: { fg: "#0E7490", soft: "#ECFEFF", line: "#A5F3FC" } neutral: { fg: "#475569", soft: "#EEF3FA", line: "#D9E2F0" } typography: headline: fontFamily: "'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif" fontSize: "1.5rem" lineHeight: 1.25 fontWeight: 600 letterSpacing: "-0.015em" title: fontFamily: "'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif" fontSize: "1.125rem" lineHeight: 1.4 fontWeight: 600 body: fontFamily: "'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif" fontSize: "0.875rem" lineHeight: 1.5 fontWeight: 400 body-sm: fontFamily: "'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif" fontSize: "0.8125rem" lineHeight: 1.45 fontWeight: 400 label: fontFamily: "'IBM Plex Mono', ui-monospace, SFMono-Regular, monospace" fontSize: "0.75rem" lineHeight: 1.33 fontWeight: 500 letterSpacing: "0.04em" micro: fontFamily: "'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif" fontSize: "0.6875rem" lineHeight: 1.3 fontWeight: 500 badge: fontFamily: "'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif" fontSize: "0.625rem" lineHeight: 1.3 fontWeight: 600 tiny: fontFamily: "'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif" fontSize: "0.5625rem" lineHeight: 1.3 fontWeight: 600 rounded: badge: "6px" button: "8px" input: "8px" card: "12px" modal: "16px" spacing: [4, 8, 12, 16, 20, 24, 32, 40, 48] elevation: card: "0 1px 2px rgba(20,33,61,.04), 0 1px 3px rgba(20,33,61,.06)" popover: "0 6px 20px rgba(20,33,61,.10), 0 1px 3px rgba(20,33,61,.06)" modal: "0 16px 48px rgba(20,33,61,.18), 0 4px 12px rgba(20,33,61,.08)" focus: "outline: 2px solid #142B80; outline-offset: 2px" components: field: height: "44px" rounded: "8px" border: "{surfaces.border-strong}" background: "{surfaces.surface}" button-primary: height: "44px" background: "{brand.500}" hoverBackground: "{brand.600}" textColor: "#FFFFFF" rounded: "8px" card: background: "{surfaces.surface}" border: "1px solid {surfaces.border}" rounded: "12px" shadow: "{elevation.card}" badge: rounded: "6px" fontSize: "{typography.micro.fontSize}" fontWeight: 600 --- > **Family contract:** shared tokens, per-app deltas, and the sync protocol live in `UNIFORMITY.md` — byte-identical across B2B, ERP, and POS. Edit the B2B copy, then run `bash scripts/sync-design.sh`. # Design system — KeluargaMart ERP ## Mode **Operate.** This is a tool people sit in front of for eight hours with a mouse and a keyboard. Every screen is a list, a form, or a number. Density and scanability beat expression. There are no hero sections, no decorative gradients, no marketing surfaces. The brand shows up in precise details: the navy, the mono numerals, the borders, one consistent focus ring. ## North star A warehouse clerk should be able to find a stock figure in under two seconds, and a finance user should never have to guess whether a colour means "normal" or "act now". Nothing on screen is ambiguous, nothing is decorative, and nothing important is ever truncated. ## Colour roles The navy scale is the only brand hue. `brand-500` (`#142B80`) is the primary — buttons, active nav, links, focus. `brand-600` is its hover. `brand-900` is the scrim behind modals. The lighter steps (`50`–`200`) are soft fills for active states and primary badges; `300`–`400` exist for charts. Surfaces are a three-step stack and nothing else: the page is `#F5F8FF`, cards and inputs are `#FFFFFF`, and quiet zones (table headers, detail tiles, muted panels) are `#EEF3FA`. Two line weights: `#D9E2F0` for structure, `#BCCBE4` for field boundaries. ## Type scale IBM Plex Sans for everything, IBM Plex Mono for numbers, codes and the one mono `label` step. Both are self-hosted via `@fontsource` — the app makes no network font request. | Token | Size | Line | Weight | Tracking | Where | | --- | --- | --- | --- | --- | --- | | `text-headline` | 1.5rem | 1.25 | 600 | −0.015em | page `

` | | `text-title` | 1.125rem | 1.4 | 600 | — | login heading, section titles | | `text-body` | 0.875rem | 1.5 | 400 | — | default body, table cells | | `text-body-sm` | 0.8125rem | 1.45 | 400 | — | dense secondary copy | | `text-label` | 0.75rem | 1.33 | 500 | 0.04em | mono product label (`ERP`) | | `text-micro` | 0.6875rem | 1.3 | 500 | — | badges, stamps, footers, section labels (11px) | | `text-badge` | 0.625rem | 1.3 | 600 | — | notification count badge (10px) | | `text-tiny` | 0.5625rem | 1.3 | 600 | — | SVG map label (9px) | `text-micro` / `text-badge` / `text-tiny` are real tokens, not brackets. Bracket `text-[10px]` and `text-[9px]` are covered by `text-badge` and `text-tiny`; remaining `text-[11px]` instances are intentional and stay as `text-[11px]` (detector waived) or map to `text-micro` where semantic. The two historical 10px instances (sidebar section labels, product image placeholder) came up to 11px. Micro-labels stay sans. Mono is for numerals, codes, timestamps and the single `label` step — never for prose. ## The ink usage rule This is the whole point of the ink scale. Four values, three of which have a hard boundary: | Token | Value | On page `#F5F8FF` | On `#FFFFFF` | On `#EEF3FA` | Permitted for | | --- | --- | --- | --- | --- | --- | | `ink` | `#14213D` | 15.03:1 | 15.97:1 | 14.33:1 | primary text, anywhere | | `ink-muted` | `#475569` | **7.13:1** | 7.58:1 | 6.80:1 | secondary text, anywhere | | `ink-subtle` | `#64748B` | 4.48:1 ✗ | **4.76:1** | 4.27:1 ✗ | `#FFFFFF` only | | `ink-disabled` | `#94A3B8` | 2.41:1 ✗ | 2.56:1 ✗ | 2.30:1 ✗ | **non-text only** | `ink-muted` replaced both of the old `ink-soft` and `ink-muted` roles. There is now exactly one secondary-text colour and it is safe on every family surface. `ink-subtle` appears in exactly three places — the placeholder text of `AppInput`, `AppTextarea` and `SearchInput`, all of which sit on white. `ink-disabled` never renders content. It is disabled control text, hover borders on fields, and the scrollbar thumb. The 31 places it previously carried real content at 2.3–2.6:1 all moved to `ink-muted`. ## Status tone semantics Six tones, each an `fg` / `soft` / `line` triplet. Every `fg` clears AA on white and on its own soft background, at the 11px semibold badge size: | Tone | fg | on `#FFFFFF` | on own soft | Means | | --- | --- | --- | --- | --- | | `primary` | `#142B80` | 12.45:1 | 10.01:1 | in flight, brand-owned state | | `info` | `#0E7490` | 5.36:1 | 5.15:1 | normal step in the flow, no action needed | | `success` | `#15803D` | 5.02:1 | 4.66:1 | done, settled, paid | | `warning` | `#B45309` | 5.02:1 | 4.75:1 | **someone has to act** | | `danger` | `#C0372C` | 5.49:1 | 4.62:1 | failed, or money at risk | | `neutral` | `#475569` | 7.58:1 | 6.80:1 | closed record, nothing to do | Warning is an alarm colour, so it is reserved for states that need action. `invoiced` and `unpaid` are normal steps and read `info`. `partial` needs a follow-up and reads `warning`. `overdue` stays `danger`. `cancelled` is `neutral` in every module — sales orders, purchase orders and invoices agree. Attendance follows the same ladder: `late` is `warning`, an unexplained `absent`/`alpha` is `danger`, and documented `leave`/`sick` is `info`. Roles are identity, not state, so the role badge only uses three tones and tone encodes privilege: `primary` for `admin`/`system`, `info` for the operational roles, `neutral` for `hr_staff`/`employee`. The eight keys in `ROLES` are exactly the eight the backend issues — there is one map, read by the sidebar, topbar, settings page and users table alike. ## Focus and motion One focus treatment, declared once in `@layer base` for every anchor, button, input, select, textarea and `[tabindex]`: `outline: 2px solid #142B80` with a 2px offset. The `focus-ring` utility emits the same thing so components that already referenced it look identical. `#142B80` against the page background is 11.71:1, so the ring is visible on every surface. `prefers-reduced-motion: reduce` collapses all animation and transition duration globally, with one deliberate exception: `.animate-spin` slows to 1.6s rather than freezing, because a stopped spinner is a broken loading indicator. Chart.js animation is disabled outright in that case, since CSS cannot reach it. ## Do - Use `ink-muted` for any secondary text, on any surface. - Put money and quantities in `tabular` mono and let them wrap rather than clip. - Give every list three real states beyond the happy one: loading skeleton, empty, and a recoverable error with a retry. - Label every field through `FormField`, which generates the id and wires `aria-describedby` to its own hint and error. - Reach for `text-micro` instead of a bracket value. - Keep cards as the container language; a card holds content, not other cards. ## Don't - Don't use `ink-disabled` for anything a user has to read. - Don't use `ink-subtle` outside a white surface. - Don't spend `warning` or `danger` on a state nobody needs to act on. - Don't truncate a financial figure. Reflow or step the size down. - Don't ship a control that a keyboard can't reach — no ``. - Don't add hero sections, gradient text, or glassmorphism. This is a tool. - Don't render a server error inside a field's validation slot. Form-level failures belong in the one `role="alert"` region above the submit button. - Demo login buttons (with role-based accounts) are intentionally kept on all login pages for development and demo purposes. Do not remove or gate them.