# RentaFleet — Design system

**Fluent Mica** — a Windows 11 Fluent surface rendered in the browser: soft
layered "mica" surfaces, subtle shadows instead of heavy borders, one accent +
neutral grays + semantic states, rounded-but-not-pill corners, calm 150–200ms
motion. **Two fully independent light/dark palettes**, switched by the `.dark`
class on `<html>` — never one palette inverted. This document is the contract:
keep it current as the system grows.

---

## 1. Materials & depth

| Material | Where | How it's built |
|---|---|---|
| **Mica** | App background, title bar, nav pane | `body::before` — a fixed, blurred radial wash of the accent over `--base` (warm cream in light). Never a flat grey. See `resources/css/app.css`. |
| **Layer** | Content cards (`.fluent-layer`, `x-ui.card`) | Opaque `--layer` fill, 1px `--stroke` hairline, soft shadow (`shadow-card` = `--shadow-card`). |
| **Acrylic** | Flyouts, dropdowns, dialogs, toasts, command-bar overflow (`.fluent-acrylic`) | `backdrop-blur` + 72% surface opacity + 1px inner top highlight. **Never on a full page or a card.** |

Depth is **border + tiny shadow**, never a heavy drop shadow. Max shadow:
`--shadow-flyout` (`0 8px 16px …`), used on interactive overlays only.

---

## 2. Colour tokens

Neutrals + accent + semantic states are `R G B` triples on `:root` / `.dark` in
`resources/css/app.css`, exposed to Tailwind as `rgb(var(--token) / <alpha>)`.
Tints, shadows and the standard-button set are **literal** CSS values in their own
vars (no alpha modifier) — `bg-accent-tint`, `bg-hover-tint`, `shadow-card`, etc.

| Token | Tailwind | Light | Dark |
|---|---|---|---|
| `--base` | `bg-base` | `#F7EFE4` cream | `#202020` slate |
| `--layer` | `bg-layer` | `#FDF7F0` | `#2B2B2B` |
| `--layer-alt` | `bg-layer-alt` | `#F3E8DA` | `#323232` |
| `--stroke` | `border-stroke` | `rgba(46,51,48,.14)` → `#DBD5CB` | `#383838` |
| `--stroke-strong` | `border-stroke-strong` | `rgba(46,51,48,.28)` → `#BFBAB2` | `#4A4A4A` |
| `--text` | `text-content` | `#2E3330` | `#FFFFFF` |
| `--text-secondary` | `text-content-secondary` | `#5C625E` | `#C7C7C7` |
| `--accent` | `bg-accent` / `text-accent` | `#1F7261` sage | `#4CC2FF` |
| `--accent-hover` | `bg-accent-hover` | `#17604F` | `#6FCFFF` |
| `--success` | `text-success` | `#1B6B5A` | `#6CCB5F` |
| `--caution` (spec `warning`) | `text-caution` | `#8A4F12` | `#FCE100` |
| `--critical` (spec `danger`) | `text-critical` | `#C42B1C` | `#FF99A4` |
| `--apricot` | `bg-apricot` | `#F5A65B` | `#F5A65B` |

Tint / interaction vars: `--accent-tint`, `--success-tint`, `--caution-tint`,
`--critical-tint`, `--hover-tint` (generic row/button hover wash), plus the
standard-button set `--btn-standard-bg / -hover / -border / -fg`.

**Accent is switchable.** `resources/js/fluent.js` holds preset swatches
(`green` = the sage identity accent, then `blue teal purple magenta red orange
graphite`); `$store.theme.setAccent(name)` writes `--accent` + `--mica-tint`
inline and persists to `localStorage['rf.accent']`. The dark/light value is
re-resolved whenever the theme flips. An inline `<head>` script in
`components/layouts/fluent.blade.php` applies the saved theme + accent before
first paint (no FOUC). Org default lives in Settings (`appearance.default_accent`
= `green`).

Semantic fills use the `*-tint` tokens (`bg-success-tint` etc.) with the solid
token for text/icon. **Red (`critical`) is reserved for destructive / emergency
states only — never decorative.** `critical` as a solid button fill flips its
text to near-black in dark mode (the dark token is a light pink).

---

## 3. Corner radius

| Radius | Tailwind | Use |
|---|---|---|
| `8px` | `rounded-fluent` | Cards, dialogs, flyouts, buttons |
| `4px` | `rounded-fluent-sm` | Nested elements — list rows, inputs inside a card, icon buttons |
| `999px` | `rounded-full` | Avatars, toggle knobs, pills, badges |

Never mix a sharp and a rounded corner on one surface.

---

## 4. Typography

Stack: `"Segoe UI Variable Display", "Segoe UI", Inter, system-ui, sans-serif`.
**Inter** is loaded as a web font so non-Windows machines keep the metrics.

| Ramp | Tailwind | Size / line |
|---|---|---|
| Caption | `text-caption` | 12 / 16 |
| Body | `text-body` | 14 / 20 |
| Body strong | `text-body-strong` | 14 / 20 semibold |
| Subtitle | `text-subtitle` / `text-subtitle-strong` | 20 / 28 |
| Title | `text-title` / `text-title-strong` | 28 / 36 |
| Display | `text-display` / `text-display-strong` | 40 / 52 |

Page titles = Title. Card headers = Body strong. Never bold body copy for
emphasis — use `text-content-secondary` to de-emphasise instead.

---

## 5. Motion

| Purpose | Easing | Duration |
|---|---|---|
| Hover / colour | `ease-fluent` = `cubic-bezier(0.1,0.9,0.2,1)` | 150ms (`duration-fast`) |
| Transform / open-close | `ease-fluent` / `ease-fluent-in` (alias) | 200ms (`duration-moderate` / `duration-flyout`) |
| Exit | `ease-fluent-out` = `cubic-bezier(0.8,0,1,1)` | 150ms |

Calm by default — nothing longer than 200ms. `ease-fluent-in` is an alias of
`ease-fluent` for back-compat.

- **Page navigation**: 12px upward slide + fade on the content region only
  (`animate-fluent-page`). Title bar and nav pane stay put.
- **Press**: `.fluent-press` scales the element to `0.97`.
- **Hover**: raise the fill one step (`hover:bg-layer-alt`), never the shadow.
- **Selection pill**: a 3px × 16px accent bar (`.nav-pill`) that `translateY`s
  between nav items (`resources/views/components/shell/nav.blade.php`).
- **Reveal**: `x-reveal` tracks the pointer and paints a soft accent border on
  hover (`.fluent-reveal`) — used on interactive cards.
- Everything degrades under `@media (prefers-reduced-motion: reduce)`.

---

## 6. Shell anatomy

`resources/views/components/layouts/fluent.blade.php` composes:

- **Title bar** (`components/shell/title-bar.blade.php`) — 48px, Mica. Hamburger +
  wordmark · centred global search (max 460px, `Ctrl/⌘+K`) · theme toggle ·
  notification bell · account avatar (acrylic flyout).
- **NavigationView** (`components/shell/nav.blade.php`) — 280px expanded / 48px
  compact, remembered in `localStorage['rf.nav']`. Auto-compacts < 1024px,
  becomes an overlay drawer < 768px (with scrim, `z-40` over the tab bar,
  own `overflow-y-auto`). Selection pill + `layer-alt` fill + filled icon on
  the active item.
- **Bottom tab bar** (`components/shell/tab-bar.blade.php`) — `md:hidden`, fixed,
  56px, Mica + blur, `env(safe-area-inset-bottom)` padding. Four primary
  destinations (Accueil · Réservations · Flotte · Clients, `@can`-filtered) +
  a **Menu** button that opens the nav drawer (`$store.nav.toggle()`). The
  content region reserves bottom space for it; the toaster lifts above it.
- **Content region** — `max-w-[1400px]`, `px-4 sm:px-5 md:px-8`, animated per
  navigation.
- **Toaster** (`components/shell/toaster.blade.php`) — bottom-right (above the
  tab bar on mobile), stacked, 4s auto-dismiss. Fire from anywhere:
  `$store.toasts.push({title, message, intent})` or a Livewire
  `$this->dispatch('toast', title: '…', intent: 'success')`.

Alpine state lives in three stores (`resources/js/fluent.js`): `theme`, `nav`,
`toasts`. Alpine is **Livewire's bundled instance** — never import or start a
second one; register on `alpine:init`.

---

## 7. Component library — `x-ui.*`

`resources/views/components/ui/`. No screen writes raw utility soup; compose these.

| Component | Notes |
|---|---|
| `x-ui.button` | `variant`: `accent` (one solid sage primary per view, white text) · `standard` (apricot-tint bg + `#C97516` border + **ink** text, never white-on-tint) · `subtle` (transparent, `hover:bg-hover-tint`) · `danger` (solid red, destructive only); `size`: `sm · md`; `icon`, `href`. |
| `x-ui.card` | `title`, `subtitle`, `padding`, `reveal`, `href`; `header` / `footer` slots. |
| `x-ui.input` | `label`, `hint`, `error`, `icon`. Wires `aria-describedby` / `aria-invalid`. |
| `x-ui.select` | `options` (assoc or list), `placeholder`, `selected`. |
| `x-ui.toggle` | Windows switch; `model` binds `wire:model.live`. |
| `x-ui.badge` | `intent`: `neutral · accent · success · caution · critical`; `dot`. |
| `x-ui.table` | `headers`; on `< md` the whole table becomes a **grid of key/value cards** — never a side-scrolling table (see "Data grid" below). |
| `x-ui.dialog` | ContentDialog. Acrylic, teleported. Centered card on `sm+`, **bottom-sheet on mobile** (slides up, rounded top only, safe-area pad, scrollable). `actions` stack full-width on mobile, right-aligned row on `sm+`. Open: `$dispatch('open-dialog', {name})`; close: `$dispatch('close-dialog')`. |
| `x-ui.flyout` / `x-ui.flyout-item` | Acrylic menu. `trigger` slot; `align`, `width`. |
| `x-ui.teaching-tip` | Pointed callout around a `trigger`. |
| `x-ui.infobar` | Page-level message; `intent`, `title`, `dismissible`; `actions` slot. |
| `x-ui.command-bar` | Primary actions + `overflow` (⋯ acrylic) + `trailing` slots. On `< sm` it wraps to a column — actions on top, `trailing` on its own full-width row (mark search inputs `w-full sm:w-44`). |
| `x-ui.breadcrumb` | `items` `[['label','href'], …]`; last segment is current, not a link. |
| `x-ui.page-header` | Breadcrumb + Title + optional `subtitle` / `actions` / `commandBar` / `infobar`. |
| `x-ui.empty-state` | Illustration + one sentence + primary `action` slot. |
| `x-ui.pager` | Fluent pagination for an Illuminate paginator. |
| `x-icon` | Inline Fluent System Icons; `variant`: `regular` (idle) / `filled` (selected). |

### Data grid (`.data-grid` / `.data-panel` — `resources/css/app.css`)

Every list/table view uses this instead of a raw `<table>` + `overflow-x`:

```
<div class="data-panel">            {{-- bordered card on md+, transparent on mobile --}}
  <div class="md:overflow-x-auto">
    <table class="data-grid text-body">
      <thead class="hidden md:table-header-group">…</thead>
      <tbody>
        <tr><td data-label="Plaque">…</td> … <td class="text-right">⌄</td></tr>
```

- **`md` and up** — a normal table: sticky-ish `layer-alt` header, `divide-y`
  rows, hover wash.
- **below `md`** — `<tbody>` becomes a CSS **grid of cards** (one per row; two
  per row 600–767px). Each `<td>` is a `label ↔ value` line; the **first cell**
  is the card title (no label); a **trailing cell with no `data-label`** becomes
  a divided action footer. No horizontal scrolling, ever.

`x-ui.table` renders this shape for you; hand-rolled tables (sortable headers,
row selection) opt in with the two classes + `data-label` on every `<td>`.

---

## 8. Module pattern (reference: Fleet)

Every feature module follows the shape established by `app/Livewire/Fleet`:

- **Livewire class components** (`config/livewire.php` → `make.type = class`, no emoji):
  class in `app/Livewire/<Module>/`, view in `resources/views/livewire/<module>/`.
  `Index` (list), `Show` (tabbed detail), `Form` (create + edit share one component).
- **List page**: `x-ui.page-header` (breadcrumb + title + `commandBar` slot) → status
  chips → grid **or** table (persisted with `#[Session]`) → `x-ui.pager`. Filters are
  `#[Url]` props so state is shareable; row/card selection enables command-bar
  Edit/Delete; destructive actions open `x-ui.dialog` naming the record.
- **Form**: a `Livewire\Form` object in `app/Livewire/Forms/` holds fields + `rules()`
  + `validationAttributes()` (the Livewire-idiomatic Form Request). Sectioned
  `x-ui.card`s; errors via `:error="$errors->first('form.field')"`.
- **Detail page**: `wire:click="setTab(...)"` + `#[Url] $tab`; Fluent pivot underline
  on the active tab; a Timeline tab merges every dated record into one feed.
- **Business logic** lives in `app/Services/<Model>Service` (create/update/status
  transitions write audit rows; domain guards throw `App\Exceptions\*` which the
  component catches and surfaces as a `critical` toast). **Authorization** in
  `app/Policies/<Model>Policy` (`.view` → viewAny/view, `.manage` → create/update/delete);
  nav items and command-bar buttons are hidden with `@can`, never shown-then-denied.
- **Money** is rendered only through `@money($minor)` / `@moneyCompact($minor)`
  (`app/Support/Money.php`), never `number_format` in a view.
- **Wizards** (reference: the booking wizard) — one Livewire component holds every
  step's state and an `int $step`; a numbered progress rail across the top (accent =
  current, success + check = done, clickable to go back only); each step is one
  `x-ui.card`; a sticky right-hand summary/price panel recomputes live from
  `#[Computed]` props. `next()` runs a per-step `passes*()` guard (which may
  `$this->validate()` and/or `addError()`); the final submit re-runs every guard and
  the service's own transactional check, bouncing back to the offending step on
  failure with a toast.
- **Charts** — `<div x-data="{ cfg: @js($config) }" x-chart="cfg" wire:ignore>`. The
  `x-chart` Alpine directive (`resources/js/chart.js`) owns one ApexCharts instance,
  merges `$config` over a Fluent base (theme mode, `--accent`/`--success`/… colours,
  hairline grid, no toolbar), and re-themes on the `rf:theme` window event that the
  theme/accent switch fires. Always inside `wire:ignore` so Livewire morphs don't
  fight the chart's own DOM. Dashboard uses area + donut; Reports uses bar / h-bar.
- **PDFs** (reference: rental agreement / invoice / report) — `resources/views/pdf/*` are
  standalone HTML built from `<table>` layouts and inline `<style>`, never Tailwind
  (dompdf has no fl​exbox/grid). Letterhead comes from `App\Support\Company::profile()`
  (Settings). Rendered by a service method returning `Barryvdh\DomPDF\PDF`; a thin
  `PdfController` streams it. Money is formatted with a local `number_format` helper
  inside the template, not `@money` (Blade directives don't run through dompdf's view).
- **State machines** (reference: `BookingStatus`) — the happy-path order lives on the
  enum (`::pipeline()`); the detail page draws it as a horizontal stepper with any
  terminal off-ramp (cancelled / no-show) called out below. Every transition is a
  method on the service, guarded and audited; the detail page's action buttons are a
  `@switch` on the current status.

## 9. Responsiveness

Breakpoints follow Tailwind defaults; `md` (768px) is the phone / desktop line.

- **Never** leave `overflow-x-auto` as the only mobile answer for tabular data —
  use the **Data grid** (§7). Every list has a real table on `md+` and a card
  grid below it.
- Touch has no hover: controls that appear on `group-hover` must be visible by
  default on `< md` (`opacity-100 md:opacity-0 md:group-hover:opacity-100`).
- Tap targets on mobile are ≥ 36px; pager / icon buttons grow to `w-9 h-9`.
- Fixed chrome (tab bar, drawer, toaster, bottom-sheet) pads for
  `env(safe-area-inset-bottom)`.
- Filter/search controls stretch to `w-full` below `sm`, fixed width above.

## 10. Do not

No purple/blue SaaS gradients. No glassmorphism on cards. No neumorphism.
No emoji as icons. No full-width coloured page headers. No shadow larger than
`shadow-flyout`. No sharp+round corner mix on one surface. No side-scrolling
data table on a phone.
