Initial Switch to V2. Completely Overhauled Backend, Frontend and General Structure.

This commit is contained in:
2026-04-17 14:37:36 +03:00
parent eb773c5531
commit 0a8a42d69b
447 changed files with 70696 additions and 492 deletions

627
DESIGN.md Normal file
View File

@@ -0,0 +1,627 @@
# DESIGN SYSTEM — BellSystems Control Panel v2
> Single source of truth for all UI/UX decisions.
> Read before writing any page, component, or modal.
> Never override these rules inline. Change the rule here first, then propagate.
>
> Live reference: `/dev/styleguide` — every component, every variant, every state.
---
## 1. Core Philosophy
- **Consistency over creativity.** Every page must feel like it belongs to the same product.
- **Tokens over hardcoded values.** Never write a raw color, spacing value, or font size. Always `var(--token)`.
- **Components over repetition.** If you write the same pattern twice, it becomes a shared component.
- **Page layout is global.** All pages share the same padding and spacing anchors. Content always starts at the same position.
- **Accessible by default.** ARIA labels, keyboard navigation, visible focus states on all interactive elements.
---
## 2. Page Layout Anatomy
Every authenticated page lives inside `MainLayout`, which provides:
```
┌─────────────┬────────────────────────────────────────────┐
│ │ HEADER (height: 56px / --header-height) │
│ │────────────────────────────────────────────┤
│ │ │
│ SIDEBAR │ CONTENT AREA │
│ (224px / │ ┌──────────────────────────────────────┐ │
│ --sidebar- │ │ .page-wrapper │ │
│ width) │ │ padding: 48px (--space-12) │ │
│ │ │ gap: 24px between sections │ │
│ │ │ │ │
│ │ │ <PageHeader> ← always first │ │
│ │ │ <content...> │ │
│ │ └──────────────────────────────────────┘ │
└─────────────┴────────────────────────────────────────────┘
```
### The consistency guarantee
`.page-wrapper` is defined once in `global.css`. It is the only wrapper used on every page:
```css
.page-wrapper {
flex: 1;
display: flex;
flex-direction: column;
padding: var(--space-12); /* 48px — desktop */
gap: var(--space-6); /* 24px between direct children */
min-width: 0;
}
/* Mobile: padding drops to --space-8 (32px), gap to --space-4 (16px) */
```
**Rules:**
- Every page's root element is `<div className="page-wrapper">` — no exceptions
- Never add extra padding, margin, or wrapper divs that shift content relative to `.page-wrapper`
- Never override `.page-wrapper` padding per-page
- The `<PageHeader>` is always the first child inside `.page-wrapper`
- This ensures that on every page, the title starts at exactly 48px from the top-left corner of the content area
### Content width modes
Every page falls into one of two modes:
#### 1. Full-width (default)
Content expands to fill the entire available area. Use this for all data-heavy pages: lists, tables, dashboards, detail views.
```jsx
<div className="page-wrapper">
{/* content fills the full content area */}
</div>
```
#### 2. Centered (narrow content)
For pages with a small number of elements that would look lost spanning the full viewport — e.g. settings forms, auth pages, single-entity configuration screens.
```jsx
<div className="page-wrapper page-wrapper--centered">
{/* every direct child is capped at --content-max-width-sm (640px) and centered */}
</div>
```
Default max-width is `--content-max-width-sm` (640px). Override per-page only when necessary:
```jsx
<div
className="page-wrapper page-wrapper--centered"
style={{ '--page-content-max-width': 'var(--content-max-width-md)' }}
>
```
Available width tokens:
| Token | Value | Use |
|-----------------------------|--------|-----------------------------------------|
| `--content-max-width-xs` | 480px | Tiny forms, login, auth |
| `--content-max-width-sm` | 640px | Small forms, simple settings (default) |
| `--content-max-width-md` | 800px | Medium forms, detail-light pages |
| `--content-max-width-lg` | 1024px | Moderate-width constrained pages |
**Rules:**
- Never use `page-wrapper--centered` on a list page, data table page, or any page where content should grow with the viewport
- Never hardcode a pixel `max-width` in a page file — always use a token
---
## 3. Color Tokens
All colors are CSS custom properties defined in `frontend/src/styles/tokens.css`.
The system is **dark-first**: `:root` = dark theme. `[data-theme="light"]` overrides exist as a placeholder.
### Rule: never write a raw color value in any component or page file. Always `var(--token)`.
### Background Surfaces (7-step tonal ladder)
| Token | Value | Use |
|------------------------|--------------|--------------------------------------------------------|
| `--color-bg-abyss` | `#0a0e14` | Deepest well: code blocks, input backgrounds |
| `--color-bg-base` | `#10141a` | Page background (viewport fill) |
| `--color-bg-void` | `#181c22` | Sidebar, header |
| `--color-bg-surface` | `#1c2026` | Default card / panel background |
| `--color-bg-elevated` | `#262a31` | Raised cards, hovered rows, dropdowns |
| `--color-bg-island` | `#31353c` | Active states, selected rows, pressed buttons |
| `--color-bg-float` | `rgba(53,57,64,0.80)` | Glassmorphism: modals, floating panels |
### Brand / Primary (Indigo Glow)
| Token | Value | Use |
|----------------------------|------------------------------|--------------------------------------|
| `--color-primary` | `#c0c1ff` | CTAs, active nav, key accent |
| `--color-primary-hover` | `#d2bbff` | Hover, gradient endpoint |
| `--color-primary-container`| `#8083ff` | Container fills |
| `--color-primary-subtle` | `rgba(128,131,255,0.12)` | Hover backgrounds, tinted areas |
| `--gradient-primary` | `linear-gradient(135deg, #c0c1ff, #d2bbff)` | Primary button fill |
### Semantic / State Colors
| Token | Value | Use |
|------------------------|---------------------------|------------------------------------------------|
| `--color-success` | `#4ade80` | Online, active, confirmed |
| `--color-success-bg` | `rgba(74,222,128,0.12)` | Success badge / button resting background |
| `--color-warning` | `#fbbf24` | Pending, needs attention |
| `--color-warning-bg` | `rgba(251,191,36,0.12)` | Warning badge / button resting background |
| `--color-danger` | `#ff5c5c` | Error text, destructive actions |
| `--color-danger-bg` | `rgba(255,92,92,0.12)` | Danger badge / button resting background |
| `--color-info` | `#7bd0ff` | Informational, aqua-sky accent |
| `--color-info-bg` | `rgba(123,208,255,0.12)` | Info badge background |
### Text Colors (4-step hierarchy)
| Token | Value | Use |
|---------------------------|-------------|---------------------------------------------------|
| `--color-text-primary` | `#dfe2eb` | Body copy, data values, headings |
| `--color-text-secondary` | `#c7c4d7` | Labels, metadata, inactive nav |
| `--color-text-muted` | `#908fa0` | Placeholders, disabled, category headers |
| `--color-text-inverse` | `#10141a` | Text on primary/accent backgrounds (dark on light)|
| `--color-text-accent` | `#c0c1ff` | Active nav items, links |
### Borders
| Token | Value | Use |
|-------------------------|----------------------------|--------------------------------------------|
| `--color-border` | `rgba(70,69,84,0.20)` | Resting inputs, card outlines |
| `--color-border-strong` | `rgba(70,69,84,0.45)` | Secondary buttons, stronger dividers |
| `--color-border-focus` | `rgba(192,193,255,0.40)` | Focus ring halo on inputs |
---
## 4. Typography
Two-font system. Three families total.
### Font Families
| Token | Font | Role |
|--------------------------|---------------------|---------------------------------------------------|
| `--font-family-display` | `Barlow Condensed` | H1, H2, page titles, modal titles |
| `--font-family-base` | `Onest` | All UI text, body, labels, buttons, table rows |
| `--font-family-mono` | `JetBrains Mono` | Serial numbers, IDs, code, API keys, terminal |
**Why this pairing:**
- `Barlow Condensed` has an industrial/engineering quality — feels like instrument panel labelling. Makes page titles immediately distinctive.
- `Onest` is a Ukrainian geometric grotesque with slightly unusual proportions and excellent numerics. Clean at 14px. Not the overused Inter/Space Grotesk.
- `JetBrains Mono` is the standard for developer-facing data.
### Font Sizes
| Token | Value | Use |
|--------------------|------------|----------------------------------------------|
| `--font-size-xs` | `0.6875rem` (11px) | Labels, sidebar category headers, chips |
| `--font-size-sm` | `0.75rem` (12px) | Captions, helper text, table headers |
| `--font-size-base` | `0.875rem` (14px) | Body text, table rows (default) |
| `--font-size-md` | `1rem` (16px) | Card titles, module headers |
| `--font-size-lg` | `1.125rem` (18px) | Section subheadings |
| `--font-size-xl` | `1.5rem` (24px) | Page headings (h1/h2) |
| `--font-size-2xl` | `3.5rem` (56px) | Hero KPI numbers, dashboard metrics |
### Font Weights
| Token | Value | Use |
|---------------------------|-------|----------------------------------------|
| `--font-weight-normal` | 400 | Body copy |
| `--font-weight-medium` | 500 | Emphasized body, table values |
| `--font-weight-semibold` | 600 | Headings, button labels, field labels |
| `--font-weight-bold` | 700 | Strong emphasis, hero metrics |
### Typography Usage Rules
- **Page titles (`<PageHeader>`):** `Barlow Condensed`, `1.75rem`, weight 600 (handled by `.v2-page-header-title`)
- **Modal titles:** `Barlow Condensed`, `1.125rem`, weight 600 (handled by `.v2-modal-title`)
- **H1, H2 globally:** `Barlow Condensed`, `var(--font-size-xl)`, weight 600 — set in `global.css`
- **H3H6:** `Onest` (body font), normal heading weights
- **Card titles:** `Onest`, `--font-size-base`, weight 600
- **Table headers:** `Onest`, `--font-size-sm`, weight 600, uppercase, `--tracking-wide`
- **Body / cell text:** `Onest`, `--font-size-base`, weight 400
- **Muted / helper text:** `Onest`, `--font-size-sm`, `--color-text-muted`
- **Serials, IDs, codes:** `JetBrains Mono`, `--font-size-sm`
### Letter Spacing
| Token | Value | Use |
|---------------------|-----------|--------------------------------------------|
| `--tracking-normal` | `0em` | Default |
| `--tracking-tight` | `-0.01em` | Barlow Condensed headings |
| `--tracking-wide` | `0.08em` | Uppercase labels, sidebar category headers |
| `--tracking-display`| `-0.02em` | Hero KPI numbers at 56px |
---
## 4b. Date, Time & Currency Formatting
All dates use **Greek/European style** (day-first). Never use US-style MM/DD/YYYY anywhere in the app.
All formatting is centralized in `frontend/src/lib/formatters.js`. Never use raw `toLocaleDateString()`, `Intl.DateTimeFormat`, `toLocaleString()`, or `toISOString().slice()` in pages or modals — always import from `@/lib/formatters`.
### Available formatters
| Function | Output example | Use for |
|---------------------|------------------------------------|--------------------------------------|
| `fmtDate` | `05/03/2026` | Short numeric dates (tables, lists) |
| `fmtDateMedium` | `5 Mar 2026` | Medium dates (cards, details) |
| `fmtDateLong` | `5 March 2026` | Long dates (headings, summaries) |
| `fmtDateFull` | `Wednesday, 5 March 2026` | Dashboard, full context |
| `fmtDateTime` | `5 March 2026, 2:30 pm` | Date + 12h time |
| `fmtDateTimeMedium` | `5 Mar 2026, 14:30` | Date + 24h time (compact) |
| `fmtDateTimeFull` | `Wed, 5 Mar 2026, 2:30 pm` | Emails, comms |
| `fmtRelative` | `5 minutes ago` | Relative timestamps |
| `fmtEuro` | `1.250,00 €` | Euro currency (Greek locale) |
### Form input helpers
| Function | Output example | Use for |
|---------------------|-------------------------|--------------------------------------------------|
| `toDatetimeLocal` | `2026-03-05T14:30` | Populating `datetime-local` inputs (local time) |
| `nowLocal` | `2026-03-05T14:30` | Current time for form defaults |
| `toDateInput` | `2026-03-05` | Populating `date` inputs |
### Critical rule: no `toISOString().slice()` for form inputs
`toISOString()` converts to **UTC**, which shifts the time by the user's timezone offset (e.g. 3 hours for Greece). Always use `toDatetimeLocal()` or `nowLocal()` instead.
---
## 5. Spacing System
4px base unit. All spacing must use tokens — no arbitrary pixel values.
| Token | Value | Common use |
|--------------|--------|--------------------------------------------------|
| `--space-1` | 4px | Tight gaps, icon padding |
| `--space-2` | 8px | Between label and input, inline gaps |
| `--space-3` | 12px | Table cell padding, compact button padding |
| `--space-4` | 16px | Between form fields, mobile page padding |
| `--space-5` | 20px | Tab item spacing |
| `--space-6` | 24px | **Page padding**, card padding, section gap |
| `--space-8` | 32px | Between major sections |
| `--space-10` | 40px | Large section gap |
| `--space-12` | 48px | Extra large spacing |
| `--space-16` | 64px | Maximum spacing, hero sections |
---
## 6. Border Radius & Shadows
### Border Radius
| Token | Value | Use |
|----------------|----------|-------------------------------------------|
| `--radius-sm` | 4px | Tags, small chips, select option rows |
| `--radius-md` | 6px | Buttons, inputs, table badges |
| `--radius-lg` | 8px | Cards, panels, dropdown menus |
| `--radius-xl` | 12px | Modals, large containers |
| `--radius-full`| 9999px | Status badge pills, avatars |
### Shadows
| Token | Value | Use |
|-------------------------|------------------------------------------|------------------------------------|
| `--shadow-card` | `inset 0 1px 0 rgba(192,193,255,0.05)` | Card top-edge glass reflection |
| `--shadow-sm` | `0 2px 8px rgba(10,14,20,0.40)` | Subtle lift |
| `--shadow-md` | `0 4px 16px rgba(10,14,20,0.50)` | Elevated cards |
| `--shadow-lg` | `0 8px 24px rgba(13,17,23,0.60)` | Modals, dropdowns |
| `--shadow-focus` | `0 0 0 3px rgba(192,193,255,0.20)` | Focus ring glow |
| `--shadow-primary-glow` | `0 4px 16px rgba(192,193,255,0.28)` | Primary button hover halo |
| `--shadow-danger-glow` | `0 4px 16px rgba(255,92,92,0.40)` | Danger button hover halo |
| `--shadow-success-glow` | `0 4px 16px rgba(74,222,128,0.35)` | Success button hover halo |
---
## 7. Component Rules
### Button
Import: `@/components/ui/Button`
**Variants:**
| Variant | Resting state | Hover state |
|------------------|---------------------------------------|----------------------------------------------------------|
| `primary` | Indigo→violet gradient, dark text | `+brightness(1.06)` + `--shadow-primary-glow` halo |
| `secondary` | Island bg, ghost border | Elevated bg + focus border + subtle indigo glow |
| `ghost` | Transparent | Elevated bg + whisper indigo glow |
| `danger` | Coral tint bg, coral text | **Solid coral fill**, dark text + `--shadow-danger-glow` |
| `success` | Emerald tint bg, emerald text | **Solid emerald fill**, dark text + `--shadow-success-glow` |
| `table-actions` | Fully transparent, muted text | Island bg + strong border (identical to `secondary`) — also activates on `tr:hover` |
**Sizes:** `sm`, `md` (default), `lg`
**Rules:**
- Never use a raw `<button>` element for a styled action
- Always pass `loading` prop for async actions (shows spinner, disables interaction)
- Icon-only buttons must have `aria-label`
- Active/press state: `filter: brightness(0.94)`, glow removed
---
### FormField
Import: `@/components/ui/FormField`
Wraps every form control: label + input/textarea/select + hint + error message.
Never place a raw `<input>` on a page.
**Types:** `text`, `email`, `password`, `number`, `tel`, `url`, `textarea`, `select`
**For `type="select"`**: pass `<option>` elements as children. FormField uses the custom `Select` component internally — the native `<select>` is never rendered.
```jsx
<FormField label="Status" name="status" type="select" value={val} onChange={handleChange}>
<option value="">Choose</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</FormField>
```
**Input appearance:** "cutout" inset-shadow treatment — the field appears recessed into the surface. Background: `--color-bg-abyss`. Focus: `--color-border-focus` ring.
---
### Select (standalone)
Import: `@/components/ui/Select`
Fully custom dropdown replacing native `<select>`. Floating menu via portal, keyboard navigation, checkmark on selected item. Usually consumed via `FormField type="select"`. Use directly when you need a select outside a form label context.
---
### DataTable
Import: `@/components/ui/DataTable`
**Always include:** column headers, loading skeleton, empty state, pagination.
**Rows:** alternate tint via `--color-tint-row` (`rgba(192,193,255,0.015)`). Hover: `--color-bg-island`.
**Status columns:** always `<StatusBadge>` — never raw text.
**Row actions:** last column, right-aligned, use portal-based action menu.
---
### Modal
Import: `@/components/ui/Modal`
Sizes: `sm` (480px), `md` (640px — default), `lg` (800px), `xl` (60vw/60vh), `xxl` (85vw/85vh), `full` (calc(100vw/100vh 64px)).
**Rules:**
- Always has: title, close (×) button, footer action buttons
- Closes on Escape + backdrop click unless `persistent={true}`
- Destructive prompts use `<ConfirmDialog>` instead
- Modal JSX never lives inside a page file — always in `frontend/src/modals/[domain]/`
---
### ConfirmDialog
Import: `@/components/ui/ConfirmDialog`
Wraps `<Modal size="sm">` with a centred icon + message. Use for any action that is destructive or hard to reverse.
Variants: `danger` (coral circle + triangle icon), `primary` (indigo circle + info icon).
---
### PageHeader
Import: `@/components/ui/PageHeader`
**Always the first element inside `.page-wrapper`.** Creates the page title block.
Props: `title` (required), `subtitle`, `breadcrumbs`, `children` (action buttons slot).
The title renders as `<h1>` with class `.v2-page-header-title` — uses `Barlow Condensed` at `1.75rem` / weight 600.
```jsx
<PageHeader title="Device Inventory" subtitle="All registered Bell units">
<Button variant="primary">Add Device</Button>
</PageHeader>
```
---
### Card
Import: `@/components/ui/Card`
Variants: `flat` (default), `elevated`, `outlined`.
Props: `title`, `subtitle`, `footer`, `padding` (bool, default true), `children`.
Card header has a faint indigo gradient ceiling (`linear-gradient` from top).
---
### Tabs
Import: `@/components/ui/Tabs`
Variants: `line` (default — underline indicator), `pill` (filled background).
Props: `tabs` (array of `{key, label, icon?, count?}`), `active`, `onChange`, `variant`.
Line variant uses a sliding indicator measured with `useLayoutEffect`. Pill variant uses filled backgrounds.
Spacing: line tabs have `gap: --space-5` between items, pill tabs `gap: --space-4`.
---
### Toast
Import: `@/components/ui/Toast``{ ToastProvider, useToast }`
Setup: wrap the app (or router) with `<ToastProvider>`. Then in any component:
```jsx
const toast = useToast()
toast.success('Saved', 'Device updated successfully.')
toast.danger('Error', 'Failed to connect.')
toast.warning('Warning', 'Firmware is outdated.')
toast.info('Info', 'Sync in progress.')
```
Toasts auto-dismiss after 4000ms. Hover pauses the timer. Stack appears in the bottom-right corner.
---
### SearchBar
Import: `@/components/ui/SearchBar`
Supports controlled (`value` + `onChange`) or uncontrolled mode.
Debounced by default (300ms). Clear button appears when text is present.
Appearance matches the `FormField` cutout treatment.
---
### Breadcrumbs
Import: `@/components/ui/Breadcrumbs`
Use on detail pages only (not list pages). Items: array of `{ label, href? }`. Last item has no href — it is the current page.
---
### Spinner
Import: `@/components/ui/Spinner`
Props: `size` (`sm`, `md`, `lg`), `color` (defaults to `--color-primary`).
Use inside loading states. Buttons show their own spinner via `loading` prop — do not add a separate `<Spinner>` inside buttons.
---
### StatusBadge
Import: `@/components/ui/StatusBadge`
Never use a raw `<span>` with a background color for status. Always `<StatusBadge>`.
Variants: `success`, `warning`, `danger`, `info`, `neutral`.
---
### Icon
Import: `@/components/ui/Icon`
Renders an inline SVG by name. 35 named icons available (see Style Guide `/dev/styleguide` → Icon section for the full list).
```jsx
<Icon name="edit" size={16} />
<Icon name="delete" size={20} color="var(--color-danger)" />
```
**Asset SVGs** (from `/assets/` folders) are displayed via `<img>` tags in the Style Guide, not via `<Icon>`. These are pre-rendered SVG files used for sidebar icons, comms icons, customer status icons, etc. Use them as image sources, not as Icon component names.
---
## 8. Icons
Three sources:
| Source | Use case | How to render |
|-------------------------------------|---------------------------------------|-----------------------------|
| `<Icon name="..." />` | Action icons, UI chrome | `@/components/ui/Icon` |
| `assets/side-menu-icons/*.svg` | Sidebar navigation | `<img src={...} />` |
| `assets/comms/*.svg` | Communication type indicators | `<img src={...} />` |
| `assets/customer-status/*.svg` | CRM status icons | `<img src={...} />` |
| `assets/global-icons/*.svg` | Legacy action icons (prefer `<Icon>`) | `<img src={...} />` |
| `assets/other-icons/*.svg` | Misc UI icons | `<img src={...} />` |
Never add a new icon library (e.g. heroicons, lucide). Use the existing sources.
---
## 9. Theming Rules
- Theme is controlled by `data-theme` attribute on `<html>`
- Default is dark (`:root` = dark theme)
- **Never** use Tailwind's `dark:` prefix — theming is handled entirely via CSS tokens
- `[data-theme="light"]` overrides exist in `tokens.css` as a future placeholder
---
## 10. Responsive Breakpoints
| Token | Value | Behaviour |
|--------------------|--------|--------------------------------------------------------|
| `--breakpoint-sm` | 640px | |
| `--breakpoint-md` | 768px | Sidebar collapses; page padding drops to `--space-4` |
| `--breakpoint-lg` | 1024px | Full sidebar shown |
| `--breakpoint-xl` | 1280px | |
Mobile (`< 768px`): single column, sidebar hidden (drawer), tables may become card lists.
---
## 11. Section Layout — Masonry Grid
**Default layout for ALL content pages with multiple variable-height sections.**
Sections on a content page must flow like physical objects stacked in columns — the next section always drops into the shortest column. This is CSS column masonry.
### How it works
```
Column 1 | Column 2 | Column 3
────────────┼─────────────┼────────────
Section A | Section B | Section C
(300px) | (250px) | (350px)
│ │
Section E | Section D |
(200px) | (200px) |
```
Sections fill left-to-right across the top, then each new section drops into whichever column is currently shortest. This is automatic — the browser handles placement via CSS `columns`.
### Usage
```jsx
{/* 2 columns */}
<div className="masonry-grid masonry-grid--2">
<Card title="Account Info"></Card>
<Card title="Profile"></Card>
<Card title="Security"></Card> {/* auto-drops into shortest column */}
</div>
{/* 3 columns */}
<div className="masonry-grid masonry-grid--3">
{sections.map(s => <Card key={s.id}></Card>)}
</div>
```
Available variants: `masonry-grid--2`, `masonry-grid--3`, `masonry-grid--4`
Responsive behaviour:
- `--4` collapses to 3 cols at 1024px, 1 col at 768px
- `--3` collapses to 2 cols at 1024px, 1 col at 768px
- `--2` collapses to 1 col at 768px
### Rules
- **Use `.masonry-grid` by default** on all content pages with 2+ variable-height sections
- **Do NOT** use `display: grid` with `gridTemplateColumns` for variable-height card layouts — this creates uneven whitespace when cards differ in height
- **Do NOT** use `.masonry-grid` for DataTable pages — tables span full width on their own
- **Do NOT** use `.masonry-grid` when sections must align horizontally (e.g. two fields that are semantically paired side-by-side within a card) — that's an internal card layout, not page-level masonry
- The `Card` component already has `break-inside: avoid` so it will never be split across columns
---
## 12. What Claude Code Must NEVER Do
- ❌ Write a hex color, `rgb()`, or `hsl()` value directly in any component or page file
- ❌ Write a pixel spacing or size value that isn't a `--space-*` token
- ❌ Use a raw `<button>`, `<input>`, or `<select>` for anything styled — always use the wrapper component
- ❌ Create a `.module.css` or any per-page CSS file
- ❌ Use Tailwind's `dark:` prefix — theming is via CSS tokens only
- ❌ Place modal JSX inside a page file — modals live in `frontend/src/modals/`
- ❌ Wrap `.page-wrapper` in additional divs that shift content alignment
- ❌ Override `.page-wrapper`'s padding to make a single page "different"
- ❌ Skip loading, error, and empty states on any data-fetching component
- ❌ Import from `_archive/` anywhere except `@/lib/api.js`, `@/hooks/useAuth.js`, and `@/providers/AuthProvider.jsx`
- ❌ Install a new icon library or introduce new SVG icons outside of `assets/`
- ❌ Invent new color values not in `tokens.css`