mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-07-27 22:45:03 +00:00
Live mode (impeccable.style/live-mode) never worked here because the skill was never actually installed: postCreate ran `skills add pbakaus/impeccable --global`, which silently no-ops — it's absent from ~/.claude/skills. Vendor it into .claude/skills/impeccable instead, so live mode's scripts/ sit on a stable path and everyone gets the same version (`npx impeccable update` to refresh). Drop the dead postCreate step. Live-mode wiring: - .impeccable/live/config.json for Next.js App Router (src/app/layout.tsx, </body>, jsx). detect-csp.mjs reports no CSP in this project, so no config patch is needed and cspChecked is honestly true. - Forward port 8400 with requireLocalPort. The script injected into the page hard-codes http://localhost:8400, so if VS Code remaps it the element picker silently never loads — the one devcontainer-specific trap here. - Ignore the per-session journal/screenshots; keep the configs tracked. DESIGN.md is required — live.mjs refuses to boot without it (context_missing). Generated via the `document` skill from the actual codebase, so it records what the code really does, including the gaps (dark mode defined but dead, --font-sans referenced but never defined, --radius unused, the broken Tremor colour block). Worth a read rather than a rubber stamp: it's the context live mode generates variants against. Verified: live.mjs boots ok with the helper on 8400 and injects into layout.tsx; live-server.mjs stop strips the tag back out cleanly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
344 lines
29 KiB
Markdown
344 lines
29 KiB
Markdown
---
|
||
name: Ara
|
||
description: Domna's portfolio intelligence platform — housing stock data turned into clear, costed retrofit and investment plans.
|
||
colors:
|
||
# Brand palette — tailwind.config.js › theme.extend.colors
|
||
brand-navy: "#14163d"
|
||
hover-navy: "#3e4073"
|
||
mid-navy: "#2d348f"
|
||
bright-blue: "#3943b7"
|
||
pale-blue: "#eff6fc"
|
||
brand-tan: "#d3b488"
|
||
hover-tan: "#947750"
|
||
brand-brown: "#c4a47c"
|
||
brand-gold: "#f1bb06"
|
||
hover-gold: "#c79d12"
|
||
# Semantic surface tokens — src/app/globals.css :root (shadcn "slate" base, components.json)
|
||
surface: "#ffffff"
|
||
ink: "#0f172a"
|
||
muted-surface: "#f1f5f9"
|
||
muted-ink: "#64748b"
|
||
hairline: "#e2e8f0"
|
||
focus-ring: "#94a3b8"
|
||
destructive: "#ff0000"
|
||
# EPC band scale — tailwind.config.js › theme.extend.colors (epc_a … epc_g)
|
||
epc-a: "#117d58"
|
||
epc-b: "#2da55c"
|
||
epc-c: "#8dbd40"
|
||
epc-d: "#f7cd14"
|
||
epc-e: "#f3a96a"
|
||
epc-f: "#ef8026"
|
||
epc-g: "#e41e3b"
|
||
# Chart series blues — tailwind.config.js safelist + src/lib/chartUtils.ts
|
||
chart-blue-4: "#5d6be0"
|
||
chart-blue-5: "#8b96e9"
|
||
chart-blue-6: "#b8bef4"
|
||
typography:
|
||
display:
|
||
fontFamily: "Manrope, ui-sans-serif, system-ui, sans-serif"
|
||
fontSize: "110px"
|
||
fontWeight: 800
|
||
lineHeight: 1
|
||
letterSpacing: "-0.05em"
|
||
headline:
|
||
fontFamily: "Manrope, ui-sans-serif, system-ui, sans-serif"
|
||
fontSize: "1.875rem"
|
||
fontWeight: 800
|
||
lineHeight: "2.25rem"
|
||
letterSpacing: "-0.025em"
|
||
title:
|
||
fontFamily: "Inter, ui-sans-serif, system-ui, sans-serif"
|
||
fontSize: "1.125rem"
|
||
fontWeight: 600
|
||
lineHeight: 1
|
||
letterSpacing: "-0.025em"
|
||
body:
|
||
fontFamily: "Inter, ui-sans-serif, system-ui, sans-serif"
|
||
fontSize: "0.875rem"
|
||
fontWeight: 400
|
||
lineHeight: "1.25rem"
|
||
letterSpacing: "normal"
|
||
label:
|
||
fontFamily: "Manrope, ui-sans-serif, system-ui, sans-serif"
|
||
fontSize: "0.75rem"
|
||
fontWeight: 700
|
||
lineHeight: "1rem"
|
||
letterSpacing: "0.1em"
|
||
rounded:
|
||
sm: "0.125rem"
|
||
md: "0.375rem"
|
||
lg: "0.5rem"
|
||
xl: "0.75rem"
|
||
2xl: "1rem"
|
||
full: "9999px"
|
||
tremor-small: "0.375rem"
|
||
tremor-default: "0.5rem"
|
||
tremor-full: "9999px"
|
||
spacing:
|
||
1: "0.25rem"
|
||
2: "0.5rem"
|
||
3: "0.75rem"
|
||
4: "1rem"
|
||
6: "1.5rem"
|
||
8: "2rem"
|
||
components:
|
||
button-primary:
|
||
backgroundColor: "{colors.ink}"
|
||
textColor: "#f8fafc"
|
||
rounded: "{rounded.md}"
|
||
padding: "0.5rem 1rem"
|
||
height: "2.5rem"
|
||
typography: "{typography.body}"
|
||
button-primary-hover:
|
||
backgroundColor: "{colors.ink}"
|
||
button-brand:
|
||
backgroundColor: "{colors.brand-navy}"
|
||
textColor: "#ffffff"
|
||
rounded: "{rounded.xl}"
|
||
padding: "0.625rem 1.5rem"
|
||
typography: "{typography.body}"
|
||
button-brand-hover:
|
||
backgroundColor: "{colors.hover-navy}"
|
||
button-outline:
|
||
backgroundColor: "{colors.surface}"
|
||
textColor: "{colors.ink}"
|
||
rounded: "{rounded.md}"
|
||
padding: "0.5rem 1rem"
|
||
height: "2.5rem"
|
||
button-ghost:
|
||
backgroundColor: "transparent"
|
||
textColor: "{colors.ink}"
|
||
rounded: "{rounded.md}"
|
||
padding: "0.5rem 1rem"
|
||
height: "2.5rem"
|
||
card-shadcn:
|
||
backgroundColor: "{colors.surface}"
|
||
textColor: "{colors.ink}"
|
||
rounded: "{rounded.lg}"
|
||
padding: "1.5rem"
|
||
card-portfolio:
|
||
backgroundColor: "{colors.surface}"
|
||
textColor: "{colors.ink}"
|
||
rounded: "{rounded.xl}"
|
||
padding: "1rem"
|
||
input-default:
|
||
backgroundColor: "{colors.surface}"
|
||
textColor: "{colors.ink}"
|
||
rounded: "{rounded.md}"
|
||
padding: "0.5rem 0.75rem"
|
||
height: "2.5rem"
|
||
typography: "{typography.body}"
|
||
badge-default:
|
||
backgroundColor: "{colors.ink}"
|
||
textColor: "#f8fafc"
|
||
rounded: "{rounded.full}"
|
||
padding: "0.125rem 0.625rem"
|
||
status-pill:
|
||
rounded: "{rounded.full}"
|
||
padding: "0.125rem 0.625rem"
|
||
typography: "{typography.label}"
|
||
epc-bubble:
|
||
textColor: "#ffffff"
|
||
rounded: "{rounded.full}"
|
||
height: "1.25rem"
|
||
width: "1.25rem"
|
||
navbar:
|
||
backgroundColor: "{colors.brand-navy}"
|
||
textColor: "#d1d5db"
|
||
height: "4rem"
|
||
padding: "0 1rem"
|
||
navbar-link-hover:
|
||
backgroundColor: "{colors.hover-navy}"
|
||
textColor: "#ffffff"
|
||
---
|
||
|
||
# Design System: Ara
|
||
|
||
## 1. Overview
|
||
|
||
**Creative North Star: "The Chartered Surveyor's Desk"**
|
||
|
||
Ara is a working instrument, not a showroom. Its users are analysts, asset managers and approvers with a spreadsheet open on the next monitor; the interface's job is to hand them a number they will stake a retrofit budget on. So the system reads like an institutional document rather than a product tour: white paper, hairline rules, one deep navy that signs the page, and figures set with more care than any decoration on the screen. PRODUCT.md names the register precisely — *"calm, expert, institutional — quiet confidence appropriate to housing-sector procurement, with measured warmth from the brand tan/brown accents."*
|
||
|
||
The surface is neutral by construction. Every semantic token in `src/app/globals.css` is a slate-based shadcn default: pure white background (`--background: 0 0% 100%`), near-black slate ink (`--foreground: 222.2 47.4% 11.2%`), a slate hairline (`--border: 214.3 31.8% 91.4%`). Colour is reserved for meaning: brand navy (`#14163d`) for identity and the single dark CTA; the seven-step EPC band scale (`#117d58` → `#e41e3b`) for the one dimension the whole product is about; amber/red only for risk. Density is high and deliberate — home-page cards sit at 16px padding (`p-4`, `PortfolioCard.tsx`), table cells at 12px/8px, because these screens carry hundreds of properties on large desk displays.
|
||
|
||
What the system rejects, in PRODUCT.md's own words: **generic SaaS admin templates** (Bootstrap/AdminLTE dashboards, hero-metric cards, widget walls), **the bare centred icon-card grid** of the old home page, and **over-designed AI mockup output** — 2rem-rounded cards, giant soft shadows, decorative gradients, invented data the schema cannot supply. If a surface could be dropped into any other B2B dashboard without changing a word, it is wrong.
|
||
|
||
**Key Characteristics:**
|
||
|
||
- White surface, slate ink, hairline borders — depth comes from rules and tone, not shadow.
|
||
- Navy (`#14163d`) as signature, not wallpaper: nav bar, one primary CTA, chart series anchor.
|
||
- Two typefaces, one job each: Inter reads, Manrope counts.
|
||
- `tabular-nums` on every figure that a user might compare down a column (39 usages across `src/`).
|
||
- Seven-step EPC colour scale is the only decorative-looking colour in the product, and it is load-bearing data.
|
||
- Light-mode only in practice — the dark palette exists but is dead (see the honest gaps below).
|
||
|
||
## 2. Colors
|
||
|
||
A near-monochrome institutional surface, signed in navy, warmed sparingly with tan/brown, and cut by a seven-step EPC scale that is the product's only saturated vocabulary.
|
||
|
||
### Primary
|
||
- **Brand Navy** (`#14163d`, `brandblue` in `tailwind.config.js`): the identity colour. It fills the nav bar (`src/app/components/Navbar.tsx`, `bg-brandblue`), the footer (`src/app/layout.tsx`), and the single dominant CTA on a page. 535 usages across `src/` — by far the most-used brand token, and the one most at risk of overuse.
|
||
- **Hover Navy** (`#3e4073`, `hoverblue`): the only hover partner for brand navy. Nav links and brand buttons lift to it (`hover:bg-hoverblue`).
|
||
- **Slate Ink** (`#0f172a`, `--primary` / `--foreground` in `src/app/globals.css`, expressed there as `222.2 47.4% 11.2%`): the shadcn primary. Every default `<Button>` and `<Badge>` is this near-black, not brand navy. Two dark colours therefore coexist in the product — see The One Dark Rule.
|
||
|
||
### Secondary
|
||
- **Mid Navy** (`#2d348f`, `midblue`) and **Bright Blue** (`#3943b7`, `brandmidblue`): the interactive blues. `brandmidblue` carries link hover, checkmarks and progress fills (`PortfolioCard.tsx`: `hover:text-brandmidblue`, budget bar `bg-brandmidblue`); `midblue` is the middle stop of the chart ramp.
|
||
- **Pale Blue** (`#eff6fc`, `brandlightblue`): the lightest tinted surface, used as a chart wash and card tint. Note the collision documented under gaps: `theme.extend.textColor.brandlightblue` is `#00a9f4`, a completely different colour from the background token of the same name.
|
||
|
||
### Tertiary
|
||
- **Brand Tan** (`#d3b488`) / **Hover Tan** (`#947750`) / **Brand Brown** (`#c4a47c`): the "measured warmth" PRODUCT.md asks for. In practice they are almost entirely a *chart* palette — `brandbrown` appears 94 times, mostly as a Tremor/Recharts series colour (`src/lib/chartUtils.ts`), while `brandtan` appears 7 times. The warmth is real but currently lives in the charts, not the chrome.
|
||
- **Brand Gold** (`#f1bb06`) / **Hover Gold** (`#c79d12`): reserved for the starred/pinned state (`PortfolioCard.tsx`, `fill={starred ? "#f1bb06" : "none"}`). At 1.77:1 against white it is decorative only, and the critiques flag it: state must never be carried by the gold fill alone.
|
||
|
||
### Neutral
|
||
- **Surface** (`#ffffff`, `--background`): the page. Always.
|
||
- **Muted Surface** (`#f1f5f9`, `--muted` / `--secondary` / `--accent`, all `210 40% 96.1%`): row hover, tab track, skeleton fill, secondary button.
|
||
- **Muted Ink** (`#64748b`, `--muted-foreground`): table headers, card meta, timestamps.
|
||
- **Hairline** (`#e2e8f0`, `--border` and `--input`): every divider and every input stroke, at exactly 1px.
|
||
- **Focus Ring** (`#94a3b8`, `--ring`): the 2px offset ring on every focusable primitive.
|
||
- **Destructive** (`#ff0000`, `--destructive: 0 100% 50%`): pure sRGB red. Honest note: this is a raw, unmodulated red and the only token in the system that is not tuned — it sits oddly against an otherwise slate-derived palette.
|
||
|
||
### The EPC Scale
|
||
Seven fixed hues, `epc_a` through `epc_g` (`tailwind.config.js`), rendered as 20px letter bubbles (`EpcLetterBubble`, `propertyTableColumns.tsx`) and as chart fills (`src/lib/chartUtils.ts`). This scale is **data, not decoration**: A `#117d58`, B `#2da55c`, C `#8dbd40`, D `#f7cd14`, E `#f3a96a`, F `#ef8026`, G `#e41e3b`.
|
||
|
||
### Named Rules
|
||
|
||
**The One Dark Rule.** A screen gets one dark button. PRODUCT.md puts navy "in small, deliberate doses", and the July critique of the property table found two competing navy CTAs side by side ("Add properties" and "Run modelling") and scored it down. Pick the one action the user should take; everything else is outline or ghost.
|
||
|
||
**The Band-Colour Rule.** `epc_a`–`epc_g` are reserved for EPC bands. They may never be borrowed for status, emphasis, or chart series that are not EPC bands. When a chart needs series colours, it takes them from the navy ramp (`#14163d` → `#2d348f` → `#3943b7` → `#5d6be0` → `#8b96e9` → `#b8bef4`) or `brandbrown`.
|
||
|
||
**The Tokenised-Hex Rule.** Raw hex in a component is prohibited. The critique of the home page caught exactly this drift (`#c3ccd8`, `#e2e7ee`, `#e4e9f1`, `#e9edf3`, `#f8fafc`, `#eceefb` inline in `PortfolioCard.tsx` and `PortfolioHome.tsx`) while a full brand palette sat unused in `tailwind.config.js`. Add the token; don't paste the hex.
|
||
|
||
## 3. Typography
|
||
|
||
**Display Font:** Manrope (`next/font/google`, weights 400/500/700/800, exposed as `--font-manrope`; `src/app/layout.tsx`)
|
||
**Body Font:** Inter (`next/font/google`, variable, applied via `inter.className` on `<html>`; `src/app/layout.tsx`)
|
||
**Label/Mono Font:** none. There is no monospace face in the system. Numeric alignment is achieved with Inter/Manrope's `tabular-nums` feature, not with a mono font.
|
||
|
||
**Character:** Inter is the clerk — invisible, legible at 13–14px, carrying every table cell and every paragraph. Manrope is the accountant — it appears only when a number or a label needs to assert itself: the 110px EPC score in `CurrentEfficiencyCard.tsx`, the plan headline in `PlanHeroCard.tsx`, the tiny uppercase section labels. 114 `font-manrope` usages, concentrated almost entirely in the building-passport surfaces.
|
||
|
||
### Hierarchy
|
||
- **Display** (Manrope, 800, up to `text-[110px]`, `leading-none`, `tracking-tighter`): the single hero figure on a building passport — the EPC score. One per screen, never more (`CurrentEfficiencyCard.tsx:124`).
|
||
- **Headline** (Manrope, 800, `text-3xl` = 1.875rem, `tracking-tight`): plan and passport page titles (`PlanHeroCard.tsx:173`).
|
||
- **Title** (Inter, 600, `text-lg` = 1.125rem / `leading-none` / `tracking-tight`): shadcn `CardTitle` and `DialogTitle`. Portfolio names on the home grid sit slightly below at `text-[17px]`, 600, `leading-snug` (`PortfolioCard.tsx:200`).
|
||
- **Body** (Inter, 400, `text-sm` = 0.875rem / 1.25rem line-height): the default everywhere — buttons, inputs, table cells, dialog copy. Card metadata drops to `text-[13px]`.
|
||
- **Label** (Manrope, 700, `text-xs` = 0.75rem / 1rem, uppercase, `tracking-widest`): micro-labels above figures (`CurrentEfficiencyCard.tsx:117`). Also used unstyled by Inter at `text-[11px]`, 600 for table headers (`PortfolioCard.tsx:245`).
|
||
- **Numeric** (any face, `tabular-nums`): mandatory on money, counts, percentages, EPC scores.
|
||
|
||
### Named Rules
|
||
|
||
**The Trustworthy Numbers Rule.** Every figure a user could compare down a column carries `tabular-nums`. PRODUCT.md: *"Figures are the product; typography and alignment treat numeric data with more care than any decorative element."* Money right-aligns. Counts right-align. A ragged numeric column is a defect, not a style choice.
|
||
|
||
**The Quiet Label Rule.** A micro-label must never outweigh the number it introduces. The property-table critique caught `RESULTS:` set in `font-black uppercase` — louder than the count beside it. Labels are `text-xs`/`text-[11px]`, uppercase, tracked, and *lighter* in visual weight than their value.
|
||
|
||
**The Two Voices Rule.** Inter reads, Manrope counts. Manrope on a paragraph of body copy is prohibited; Inter on the 110px hero score is prohibited.
|
||
|
||
## 4. Elevation
|
||
|
||
Ara is a **flat, hairline system**. Depth is carried by 1px `#e2e8f0` borders and by tonal shifts against white — `bg-muted/50` on a hovered table row, `bg-[#f8fafc]` on a hovered list row — not by shadow. The shadow vocabulary that exists is Tailwind's own and is used sparingly: `shadow-sm` dominates (98 usages) and is reserved for the resting card; `shadow-md` (34) and `shadow-lg` (25) appear almost exclusively on *floating* layers — dropdowns, popovers, dialogs, tooltips. That distribution is the system: shadow means "this element is in front of the page", not "this element is important".
|
||
|
||
### Shadow Vocabulary
|
||
- **Resting card** (`box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05)`, Tailwind `shadow-sm`; also `tremor-input` in `tailwind.config.js`): the shadcn `Card` default (`card.tsx`: `rounded-lg border bg-card shadow-sm`). Barely there — it exists to lift the card off white by a hair, and the 1px border does most of the work.
|
||
- **Overlay** (`box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)`, Tailwind `shadow-md`; `tremor-dropdown` in `tailwind.config.js`): select menus, popovers, tooltips, the portalled card menu (`PortfolioCard.tsx:96`).
|
||
- **Modal** (Tailwind `shadow-lg`): `DialogContent` only (`dialog.tsx:57`), paired with a `bg-background/80 backdrop-blur-sm` scrim.
|
||
- **Tremor card** (`0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)`, `tremor-card`): defined in `tailwind.config.js` for Tremor chart cards.
|
||
|
||
### Named Rules
|
||
|
||
**The Hairline-First Rule.** Reach for a 1px `#e2e8f0` border before you reach for a shadow. If both a border and a shadow are present on a resting surface, the shadow is probably wrong.
|
||
|
||
**The Floating-Only Rule.** `shadow-md` and above are reserved for elements that are literally floating above the page — menus, popovers, dialogs, toasts. A card, a table, a panel, a section: never. `shadow-xl` and `shadow-2xl` are drift; the codebase has 19 and 11 usages respectively and every one of them should be justified or removed.
|
||
|
||
**The 2014 Test.** If a surface looks like a 2014 admin panel, the shadow is too dark and the radius is too big. PRODUCT.md names the failure mode directly: *"2rem-rounded cards, giant soft shadows, decorative gradients."*
|
||
|
||
## 5. Components
|
||
|
||
The library is shadcn/ui (`components.json`, `style: default`, `baseColor: slate`, `cssVariables: true`) generated into `src/app/shadcn_components/ui/`, sitting on Radix primitives, with Tremor (`@tremor/react` v3) for charts and Headless UI for a handful of menus. Variants are declared with `class-variance-authority`.
|
||
|
||
### Buttons
|
||
`src/app/shadcn_components/ui/button.tsx`
|
||
- **Shape:** gently rounded (`rounded-md`, 0.375rem) at every size. Brand-styled buttons written by hand outside the primitive use `rounded-xl` (0.75rem) — a real inconsistency, not a documented variant.
|
||
- **Sizes:** default 40px tall (`h-10 py-2 px-4`), `sm` 36px (`h-9 px-3`), `lg` 44px (`h-11 px-8`).
|
||
- **Primary (`default`):** slate ink `#0f172a` on `#f8fafc` text, hover to 90% opacity (`bg-primary text-primary-foreground hover:bg-primary/90`). Note: 104 components bypass this and use `bg-brandblue` (`#14163d`) directly against 15 uses of `bg-primary` — brand navy is the *de facto* primary button in feature code even though the primitive says otherwise.
|
||
- **Focus:** 2px ring in `#94a3b8`, offset 2px, on `:focus-visible` only. Outline is removed and replaced, never removed and left.
|
||
- **Secondary / Outline / Ghost / Link / Destructive:** the five other cva variants. `outline` = 1px `#e2e8f0` stroke, transparent fill, hovers to muted surface; `ghost` = no stroke, hovers to muted surface; `link` = navy text with a 4px-offset underline on hover.
|
||
|
||
### Chips / Pills
|
||
Two distinct pill families exist and they are not interchangeable.
|
||
- **Badge** (`badge.tsx`): fully round (`rounded-full`), `px-2.5 py-0.5`, `text-xs font-semibold`, 1px border, four cva variants (default / secondary / destructive / outline).
|
||
- **Status pill** (`PortfolioCard.tsx:34-44`): same geometry, but an eight-tone map keyed to portfolio status — `early` slate, `tender` indigo, `underway` blue, `ontrack` green, `delayed` amber, `atrisk` red, `done` emerald, `review` yellow, each as a `-100` background with a `-800`/`-900` foreground. Every pill carries a 1.5px dot **and a text label** — colour is never the sole carrier of state.
|
||
|
||
### Cards / Containers
|
||
- **shadcn `Card`** (`card.tsx`): `rounded-lg` (0.5rem), white `--card` fill, 1px hairline border, `shadow-sm`. Internal padding `p-6` (24px) on header/content/footer; header stacks with `space-y-1.5`.
|
||
- **Portfolio card** (`PortfolioCard.tsx:195`): the denser home-page variant — `rounded-xl` (0.75rem), 1px border, **no shadow at all**, `p-4` (16px), `gap-2.5`. Hover changes only the border colour. This is the more honest expression of the system than the shadcn default.
|
||
- **Radius drift is real:** `rounded-lg` (249), `rounded-full` (232), `rounded-xl` (131), `rounded-md` (125), `rounded-2xl` (82) across `src/`. `rounded-2xl` (1rem) is the outer edge of acceptable and trends toward the "over-rounded AI mockup" anti-reference.
|
||
|
||
### Inputs / Fields
|
||
`input.tsx`, `select.tsx`
|
||
- **Style:** 40px tall (`h-10`), `rounded-md`, 1px `--input` (`#e2e8f0`) stroke on `--background` white, `px-3 py-2`, `text-sm`. Placeholder in muted ink.
|
||
- **Focus:** 2px `#94a3b8` ring, 2px offset — identical to buttons, so the focus vocabulary is one thing across the app.
|
||
- **Disabled:** `cursor-not-allowed` + 50% opacity.
|
||
- **Error:** not established in the primitives. There is no error variant on `Input`; forms use `react-hook-form` + `zod` (`form.tsx`) and render messages, but the *field* has no visual error state token. Flagged as a gap.
|
||
- **Select:** Radix `SelectTrigger` matches `Input` exactly (`h-10 rounded-md border border-input px-3 py-2`); content panel is `rounded-md` + `shadow-md` with a chevron and a check-mark indicator.
|
||
|
||
### Navigation
|
||
`src/app/components/Navbar.tsx`
|
||
- **Style:** a solid `bg-brandblue` (`#14163d`) bar, 64px tall (`h-16`), constrained to `max-w-7xl` with responsive gutters (`px-4 sm:px-6 lg:px-8`). White Domna wordmark on the left.
|
||
- **Links:** `text-gray-300`, `text-sm font-medium`, `px-3 py-2`, `rounded-md`. Hover fills to `hoverblue` (`#3e4073`) with white text. There is **no active/current-page state** — a genuine gap.
|
||
- **Mobile:** below `md`, links collapse into a hamburger panel with a Headless UI `Transition` (100ms ease-out in, 75ms ease-in out, opacity + 95% scale).
|
||
- **Hidden on `/`:** the nav returns `null` on the root path.
|
||
|
||
### EPC Letter Bubble (signature component)
|
||
`propertyTableColumns.tsx:68`. A 20px (`h-5 w-5`) circle, `rounded-full`, `text-xs font-bold text-white`, filled from the seven-step band scale via `getEpcColorClass` (`src/app/utils.ts:49`). Its empty state is deliberate and hard-won: a hollow circle with a 1px `slate-200` border, a slate-400 en-dash and an `aria-label="No rating yet"` — because the critique found that blank bubbles read as breakage. There is a companion `EmptyCell` (`—`, `text-slate-400`, `aria-label="No data"`) so that *one* absence vocabulary covers the whole table.
|
||
|
||
### Named Rules
|
||
|
||
**The One Overlay Rule.** Menus, dialogs and popovers come from one primitive stack (Radix via shadcn, or Headless UI `Menu` where portalling is needed). `window.confirm` is banned — the home-page critique found it live in `PortfolioHome.tsx` and scored Consistency 2/10 for having three overlay vocabularies on one page.
|
||
|
||
**The Portalled-Menu Rule.** Any menu inside a scrolling or `overflow-x-auto` container must portal to the top layer (`MenuButton` + `MenuItems anchor=` in `PortfolioCard.tsx`). A menu clipped by its container can hold focus while invisible — a hard accessibility failure the critiques caught in the list view.
|
||
|
||
**The Visible-Failure Rule.** A mutation that fails must be visible to sighted users, not only announced into an `sr-only` live region. Toasts exist (`toaster.tsx`, Radix Toast, mounted in `layout.tsx`); use them.
|
||
|
||
## 6. Do's and Don'ts
|
||
|
||
### Do:
|
||
- **Do** put brand navy `#14163d` on exactly one CTA per screen, and reach for `outline`/`ghost` for everything else. PRODUCT.md: navy carries identity *"in small, deliberate doses."*
|
||
- **Do** set every comparable figure in `tabular-nums` and right-align money and counts. Trustworthy numbers is the product's whole claim.
|
||
- **Do** separate surfaces with a 1px `#e2e8f0` hairline before considering a shadow.
|
||
- **Do** reserve `shadow-md`/`shadow-lg` for things that literally float: menus, popovers, dialogs, toasts.
|
||
- **Do** pair every colour-coded state with a text label — the status pill's dot-plus-label pattern (`PortfolioCard.tsx`) is the reference implementation.
|
||
- **Do** design the *empty* state as a first-class surface. The July critique's P0 was that a new portfolio — properties loaded, nothing modelled — falls through to a table of em-dashes and reads as "the import broke."
|
||
- **Do** use one absence vocabulary: the `EmptyCell` em-dash at `text-slate-400` with an `aria-label`. Not four.
|
||
- **Do** add a Tailwind token when you need a colour. The palette is in `tailwind.config.js` for a reason.
|
||
- **Do** keep focus rings visible: 2px `#94a3b8`, 2px offset, on `:focus-visible`. Never `focus:outline-none` without a replacement.
|
||
- **Do** meet WCAG 2.1 AA — ≥4.5:1 body text, ≥3:1 large text and non-text state indicators. PRODUCT.md warns that procurement may demand conformance evidence.
|
||
|
||
### Don't:
|
||
- **Don't** build **generic SaaS admin templates** — no Bootstrap/AdminLTE dashboards, no hero-metric cards, no widget walls (PRODUCT.md anti-reference #1).
|
||
- **Don't** rebuild **the current home page** — a bare centred grid of identical icon cards with no data, hierarchy, or organisation (PRODUCT.md anti-reference #2).
|
||
- **Don't** ship **over-designed AI mockup output**: *"2rem-rounded cards, giant soft shadows, decorative gradients, invented data the schema can't supply"* (PRODUCT.md anti-reference #3). Concretely: no `rounded-2xl` on a data card, no `shadow-xl`/`shadow-2xl`, no `bg-gradient-to-br` on a surface, no `bg-clip-text` gradient headings — all four exist in the codebase today and all four are drift.
|
||
- **Don't** put two dark navy CTAs on one screen. Two dark buttons say "both of these are the most important thing."
|
||
- **Don't** paste raw hex into a component. `#c3ccd8`, `#e2e7ee`, `#e9edf3`, `#eceefb` are all live in `src/` and all should be tokens.
|
||
- **Don't** borrow the EPC band colours (`#117d58`…`#e41e3b`) for anything that is not an EPC band.
|
||
- **Don't** carry state in the gold star fill alone (`#f1bb06` is 1.77:1 on white). Colour plus label, always.
|
||
- **Don't** set body copy in Manrope or hero figures in Inter.
|
||
- **Don't** use `window.confirm`, `alert`, or a bare `<details>` popover. One overlay stack.
|
||
- **Don't** render text below AA on white — the property-table critique found `text-slate-300` "No cost"/"Unknown" cells failing outright.
|
||
- **Don't** let a micro-label outshout its number (`RESULTS:` in `font-black uppercase` was the offender).
|
||
- **Don't** write a `dark:` variant in a component. Dark mode is defined but dead (see below); the only `dark:` classes in `src/` are inherited Tremor helper strings in `src/lib/utils.ts`, and adding one variant to one component makes the app *half*-dark, which is worse than light-only.
|
||
|
||
### Honest gaps: what this codebase does not establish
|
||
|
||
Every value in this document is copied from source. These are the things the codebase genuinely does **not** settle, recorded rather than invented — do not treat any of them as a licence to invent a value, and do not "fix" them silently in a feature PR:
|
||
|
||
1. **Dark mode is defined but dead.** `tailwind.config.js` sets `darkMode: ["class"]`, `globals.css` carries a full `.dark` token block, `next-themes` is a dependency, and a `dark-tremor` colour scale exists. The only `dark:` utilities in `src/` are the Tremor-derived focus/error class strings in `src/lib/utils.ts` (`focusInput`, `focusRing`, `hasErrorInput`); no component authored for this product writes one, and nothing ever applies the `dark` class, so those variants never activate. The dark palette is aspirational; treat the product as light-only until that changes.
|
||
2. **`--font-sans` is referenced but never defined.** `tailwind.config.js:157` maps `fontFamily.sans` to `var(--font-sans)`, but no file defines `--font-sans` (layout.tsx defines `--font-inter` and `--font-manrope`). `font-sans` therefore silently falls back to Tailwind's default stack. Inter is applied only because `inter.className` is on `<html>` — the token wiring is broken, the outcome happens to be right.
|
||
3. **Manrope weight 900 is used but not loaded.** `layout.tsx` requests weights `400/500/700/800`; `font-black` (900) appears in `CurrentEfficiencyCard.tsx` and `PlanHeroCard.tsx`. The browser synthesises or falls back. The frontmatter records 800, the weight that actually exists.
|
||
4. **`--radius: 0.5rem` is defined and unused.** `globals.css` declares it in both themes, but `tailwind.config.js` has no `borderRadius` mapping to it (its `borderRadius.extend` contains only `tremor-*` keys). All `rounded-*` classes resolve to Tailwind defaults. There is no project-defined radius scale — the values in the frontmatter's `rounded` block are Tailwind's, recorded because they are what the code actually renders.
|
||
5. **No project spacing scale.** `tailwind.config.js` extends no `spacing`. The `spacing` frontmatter block lists the Tailwind default steps the codebase actually leans on (`p-4` on dense cards, `p-6` on shadcn cards, `px-3 py-2` on dense table cells), not an invented scale.
|
||
6. **`brandlightblue` is two different colours.** `theme.extend.colors.brandlightblue` is `#eff6fc` (a near-white wash); `theme.extend.textColor.brandlightblue` is `#00a9f4` (a saturated cyan). `bg-brandlightblue` and `text-brandlightblue` are unrelated colours. This is a bug, not a system.
|
||
7. **The Tremor colour block is broken.** `theme.extend.colors.tremor.*` and `dark-tremor.*` are set to the **string literals** `"colors.blue[50]"`, `"colors.gray[200]"`, etc. — the `colors` import at the top of `tailwind.config.js` is never interpolated. Those tokens do not resolve to colours. Tremor charts are actually coloured through the safelisted arbitrary-value classes (`fill-[#14163d]` …) mapped in `src/lib/chartUtils.ts`.
|
||
8. **`domna-gradient`** is defined in `tailwind.config.js` (`linear-gradient(135deg, #14163d 0%, #2d348f 45%, #3943b7 70%, #eff6fc 100%)`) and used **nowhere** in `src/`. It is not part of the live system.
|
||
9. **No motion tokens.** No `--ease-*`, no `--duration-*`. What exists: `accordion-down`/`accordion-up` at `0.2s ease-out` (`tailwind.config.js`), Tailwind's default `transition-colors`, `tailwindcss-animate`, and 15 files using `framer-motion` ad hoc. There is no documented motion vocabulary and no `prefers-reduced-motion` handling in `globals.css`, despite PRODUCT.md requiring it.
|
||
10. **No error state on form fields**, no active/current state on nav links, and no defined breakpoint scale beyond Tailwind's defaults plus `container.screens.2xl: 1400px` and `maxWidth.8xl: 90rem`.
|
||
11. **The North Star is proposed, not confirmed.** PRODUCT.md marks the brand personality as a *"Hypothesis (user undecided — validate against mockup variants)"*. "The Chartered Surveyor's Desk" is derived from that hypothesis and from what the code actually does; it has not been ratified by the team.
|