Ctrl K
ComunidadGuías…

DESIGN.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Relmio design system

This file is the design contract for every Relmio interface: the local browser
wizard in `src/ui` and the hosted web app in `web/`. Read it with
[BRANDKIT.md](BRANDKIT.md), which holds the logo, colors, type and voice. Agents
and frontend skills should treat both files as the project brief and the
existing design tokens.

The two apps share one component kit, `src/ui/relmio-ui.css`. The wizard links
it directly. The web app imports a generated copy, `web/app/relmio-ui.css`,
which `npm run ui:sync` in `web/` refreshes and `npm run ui:check` verifies in
CI. Edit only the source file.

## Principles

1. One screen, one job in the wizard. Each wizard and dashboard view answers
   one question or completes one step, and it fits the window at laptop size.
   The next action is always visible. The hosted site is a scrolling page; see
   [The hosted site](#the-hosted-site).
2. Safety stays in sight. Warnings that change a decision are visible without a
   click. Background and technical detail sits behind a labelled disclosure.
3. Same parts everywhere. A button, field, notice or top bar looks and behaves
   the same in the wizard and on the website.
4. Two themes, one accent. Light is pastel green, dark is black. The accent
   (black in light, pastel green in dark) marks the primary action, the current
   place and selection.
5. Plain words. Short sentences, sentence case, no jargon without a hint. See
   the voice rules in BRANDKIT.md.

## Surfaces

| Surface | Where | Job |
| --- | --- | --- |
| Setup wizard | `src/ui/index.html` (`/`) | Choose a route, check the server, choose n8n, review, finish |
| Local dashboard | `src/ui/local.html` (`/local`) | See and manage connections on this computer |
| Route wizards | `/supergrok-vps`, `/local-model-vps`, `/assistant` | Provider or companion setup on a server |
| Hosting options | `/hosting` | Compare hosts and build a plan without remote changes |
| Home | `web/app/page.tsx` (`/`) | Explain Relmio, show the routes and start the install |
| Install | `/install` | Pick a terminal and copy the command |
| Hosted chat notice | `/#chat`, a section of the home page (`/chat` redirects there) | Say hosted chat is off and remove an old saved sign-in |
| Docs | `/docs`, `/docs/[slug]` | Find and read a guide |
| Changelog | `/changelog` | Read release notes |

The wizard runs on the user's computer with a strict Content Security Policy:
same-origin scripts, styles, fonts and images only, no inline styles or
scripts, and `data:` images allowed. The web app runs on Vercel with a
per-request nonce policy; see the web implementation rules.

## The one-screen rule

This rule covers the local wizard and dashboard (`src/ui`) only. The hosted
web app does not use it; see [The hosted site](#the-hosted-site).

Every wizard step and dashboard view must show all of its controls and content
without a page scrollbar or a panel scrollbar at these window sizes:

- 1280 x 720, 1366 x 768, 1440 x 900 and 1920 x 1080
- 1024 x 768

The rule covers every default, busy and error state. When a person opens an
optional disclosure, the panel body or the rail notes may scroll; the page and
the main area never scroll, and the footer actions stay visible. Owner
decision, 2026-10-03: optional explanations may stay in disclosures that
scroll this way, so they do not have to move to separate help views. A chat
transcript and an activity log may scroll inside their own region.

The shell makes this possible: put `rm-app rm-app--fit` on `<body>`. From
1024 x 600 the shell is exactly one window tall and `.rm-app__main` is the only
scroll container. Views are designed to fit, so that container never scrolls
at the target sizes. If a view still overflows, for example at 200% zoom, the
main area scrolls instead of clipping. Below 1024 pixels wide the page scrolls
normally. Never hide overflow to pass the rule.

Ways to fit a view, in order of preference:

1. Remove repetition. Say a safety fact once, in the place it matters.
2. Split a long step into two steps, or move secondary tasks to their own view
   or route.
3. Put optional explanation in an `rm-disclosure` and long warnings in an
   `rm-notice`. Keep the warning's first sentence visible.
4. Use the width: two-column forms, a `rm-grid` of choices, a definition list
   for review facts.
5. Keep actions in the panel footer, which stays visible.

Narrow screens (390 x 844 and 320 wide) must never scroll sideways. The first
screen of every view shows its title and its primary action or first choice.

Verify the rule by measuring, not by eye: at each target size,
`document.documentElement.scrollHeight` must equal `innerHeight`, and no
element with `overflow: auto` or `scroll` may have `scrollHeight` above its
`clientHeight`, except a chat transcript or an activity log.

## The hosted site

The web app in `web/` is a marketing and documentation site. It scrolls as a
normal page and may be expressive, as long as it stays in the brand: the same
tokens, kit components, top bar and themes as the wizard.

- The root layout renders `rm-app` without `rm-app--fit`, then the top bar,
  the page's `main` and the shared `SiteFooter`. Never lock a page to the
  window height.
- Home runs top to bottom: the hero with the Doorway scene, How it works
  (`#how-it-works`), the hosted chat notice (`#chat`), the safety boundary
  (`#security`), then the footer. Hosted chat and its ChatGPT sign-in stay off
  until OpenAI approves access; see
  [issue #95](https://github.com/Demonbane18/relmio/issues/95). The notice links
  to the local wizard and offers to remove an old saved sign-in from the
  browser. Install, docs, changelog and the 404 page follow the same rhythm: a
  large title, generous sections, the footer. Docs and changelog keep their
  sidebar in view while the page scrolls.
- Full-width bands carry the sections. A soft hill edge joins one band to the
  next. The safety boundary is a night band that re-points the ink, accent
  and focus tokens to the terminal values, which stay dark in both themes.
- Kit shadows and tilted `--rm-accent-soft` sheets add depth to cards such as
  the notice's cleanup card and the install toolbox. The home illustration
  stays unframed on the page canvas.
- Brand teal and cream are illustration accents only: the doorway and mascot,
  and the teal wavy underline in the home headline. They never mark state.
- Sections may rise into view with scroll-driven animation
  (`animation-timeline: view()`). Content stays visible without support and
  with reduced motion. Animate transform and opacity only.
- Every page ends with the footer: brand and one-line description, product
  links (Install, Docs, Changelog, npm, GitHub), the creator's profiles with
  icon and visible handle, and the line
  `© <year> John Paul Fusin. Relmio is released under the Apache-2.0 license.`
  The year is computed, never typed.
- Pages never scroll sideways at 320 px, keep 24 px targets and AA contrast in
  both themes, and hide decoration in forced colors. The home Doorway scene is
  the one exception: it keeps its colors in forced colors because it is the
  hero artwork and hiding it leaves empty space (owner decision,
  2026-10-03).

## Layout

```
┌ rm-topbar ─────────────────────────────────────────────────────────┐
│ brand   nav links                     theme  GitHub  support  menu │
├ rm-split ──────────────────────────────────────────────────────────┤
│ rail            │ rm-panel                                         │
│  quest track    │  header: eyebrow, title, one-line description    │
│  safety summary │  body: the step                                  │
│                 │  footer: Back · secondary · primary action       │
└─────────────────┴──────────────────────────────────────────────────┘
```

- Top bar 56 px. Rail 272 px. Gutter 16 to 32 px, fluid.
- Content width caps at 1216 px; long-form text caps at 46rem.
- The rail holds the quest track and a compact status or safety summary. It
  never holds long notices. The quest track (`rm-stepper--track`) keeps the
  steps in one row at every width with a check on each finished step, so the
  rail stays short and leaves room for the setup guide at its foot.
- Below 1024 px the rail moves above the panel.
- Web pages without a rail use `rm-container` inside `rm-app__main`, and
  full-width bands put their content in an `rm-container`.
- Below 64rem, the panel footer sticks to the bottom of the viewport while its
  panel scrolls past, with a surface fill, top divider and safe-area padding.
  Keep the desktop panel layout unchanged.
- On narrow screens, document scroll padding leaves room for the sticky top
  bar and panel footer when focusing or scrolling to content.

## Navigation

Both apps use the same top bar markup and classes. Only the destinations
differ.

| App | Primary links | Actions |
| --- | --- | --- |
| Wizard | Setup (`/`), This computer (`/local`), Hosting options (`/hosting`), Docs (external, new tab) | Theme switch, GitHub chip with version, Ko-fi support |
| Web | Home, Install, Docs, Changelog | Theme switch, GitHub chip with stars and version, Ko-fi support |

Rules:

- Mark the current page with `aria-current="page"`. A route wizard such as
  `/supergrok-vps` marks Setup with `aria-current="true"`.
- At 1024 px and wider the links sit in `.rm-nav`. Narrower windows hide
  `.rm-nav` and show the native `<details class="rm-menu">` with the same links.
  Elements with `rm-topbar__wide` hide on narrow windows; the menu repeats
  anything essential.
- External links open in a new tab, show the `external` icon and use
  `rel="noopener noreferrer"`. The accessible name says "opens in a new tab".
- Wizard links between routes keep the private session with
  `bindWizardNavigation` from `src/ui/session.js`. `src/ui/topbar.js` binds
  every `[data-wizard-route]` link and blocks navigation while an operation is
  running.
- The theme switch is the same radio group in both apps: name `color-theme`,
  values `system`, `light`, `dark`, stored under `relmio-color-mode`. An explicit
  choice sets `data-theme` on `<html>`; System removes it.
- Section navigation inside a page uses `rm-sidebar`. Guided flows use the
  `rm-stepper rm-stepper--track` quest track.

Top bar markup (the web renders the same structure from
`web/app/components/ui/TopBar.tsx`):

```html
<header class="rm-topbar">
  <div class="rm-topbar__inner">
    <a class="rm-brand" href="/">
      <img class="rm-brand__logo" src="/relmio-icon-96.png" width="32" height="32" alt="" />
      <span>Relmio</span>
    </a>
    <nav class="rm-nav" aria-label="Primary">
      <ul class="rm-nav__list">
        <li><a class="rm-nav__link" href="/" aria-current="page">Setup</a></li>
      </ul>
    </nav>
    <div class="rm-topbar__actions">
      <!-- theme switch, rm-chip GitHub link, support rm-icon-button -->
      <details class="rm-menu">
        <summary class="rm-icon-button" aria-label="Menu">
          <span class="rm-icon rm-icon--menu" aria-hidden="true"></span>
        </summary>
        <div class="rm-menu__panel"><nav aria-label="Primary">…same links as rm-menu__link…</nav></div>
      </details>
    </div>
  </div>
</header>
```

## Components

All classes start with `rm-`. One class makes a component; a `--modifier`
class picks a variant; ARIA and `data-*` attributes carry state. Base element
styles use `:where()`, so a page stylesheet can override anything without
`!important`. Class names are public API: rename one only together with every
caller in both apps.

| Component | Classes | Use |
| --- | --- | --- |
| App shell | `rm-app`, `rm-app--fit`, `rm-app__main`, `rm-split`, `rm-container` | Page frame; `rm-app--fit` applies the one-screen rule in the wizard and dashboard only |
| Top bar | `rm-topbar`, `rm-brand`, `rm-nav`, `rm-menu`, `rm-topbar__actions` | Global navigation |
| Sidebar | `rm-sidebar`, `rm-sidebar__link` | Views inside a page |
| Stepper | `rm-stepper`, `--track`, `rm-stepper__item`, `rm-stepper__link`, `rm-stepper__marker`, `rm-stepper__label` | Progress through a flow; `aria-current="step"`, `data-state="done"`. `--track` is the wizards' quest track: one row at every width, markers only below 480 px. In forced colors the current step is a ringed Highlight disc and a done step shows only its check |
| Panel | `rm-panel`, `__header`, `__heading`, `__body`, `__footer` | The working surface of a view |
| Card | `rm-card`, `--flat`, `--muted`, `--compact` | Grouped content |
| Choice | `rm-choice` and its `__icon`, `__title`, `__text`, `__meta` | A selectable option row or card; `__meta` is one short fact under the text, such as what the option needs |
| Button | `rm-button`, `--primary`, `--ghost`, `--danger`, `--sm` | Actions; `aria-busy="true"` shows progress; `--sm` only for secondary controls |
| Icon button | `rm-icon-button`, `--outline`, `--sm` | Icon-only actions with an accessible name; `aria-pressed="true"` marks a toggle that is on |
| Link | `rm-link`, `--quiet`, `--standalone` | Text links; `--standalone` for a link outside a sentence |
| Chip | `rm-chip`, `rm-chip__meta` | GitHub link with stars and version |
| Field | `rm-field`, `__label`, `__hint`, `__error`, `rm-input`, `rm-select`, `rm-textarea`, `rm-form-grid`, `rm-fieldset` | Forms |
| Check | `rm-check`, `--boxed`, `rm-check__hint` | Checkbox or radio with its label |
| Segmented | `rm-segmented`, `__item`, `__input` | Theme switch and small exclusive choices |
| Tabs | `rm-tabs__list`, `rm-tabs__tab` | Switch panels in place |
| Callout | `rm-callout`, `--success`, `--warning`, `--danger`, `--neutral`, `__icon`, `__content`, `__title`, `__body`, `__close` | A short message in a view |
| Notice | `rm-notice`, `--info`, `__title`, `__text`, `__toggle`, `__body` | A one-line warning that expands for detail |
| Badge | `rm-badge`, `--accent`, `--success`, `--warning` | Short labels |
| Status | `rm-status`, `rm-status__dot`, `data-tone` | A dot plus a sentence |
| Progress | `rm-progress`, `--indeterminate`, `rm-progress__bar` | Work in progress |
| Definition list | `rm-dl` | Review facts and results |
| Table | `rm-table-wrap`, `rm-table` | Tabular data; the wrap scrolls sideways on narrow screens |
| Terminal | `rm-terminal`, `__bar`, `__actions`, `__body`, `__prompt`, `__note` | Commands; always dark |
| Disclosure | `rm-disclosure`, `--plain`, `rm-disclosure__body` | Optional detail |
| Prose | `rm-prose` | Generated Markdown and long text |
| Type | `rm-display`, `rm-h1`, `rm-h2`, `rm-h3`, `rm-eyebrow`, `rm-lede`, `rm-muted`, `rm-small` | Text styles |
| Icon | `rm-icon` plus `rm-icon--<name>`, sizes `--xs`, `--sm`, `--lg` | Lucide icons drawn in `currentColor` |
| Utilities | `rm-cluster`, `rm-grid`, `rm-push`, `rm-visually-hidden`, `rm-skip-link` | Layout and accessibility helpers |

Icon names: monitor, sun, moon, menu, x, check, check-circle, alert, info,
external, coffee, copy, chevron-down, chevron-right, arrow-right, arrow-left,
arrow-up-right, terminal, server, laptop, shield, lock, book, home, plug, key,
activity, refresh, network, cloud, bot, message, search, box, star, download,
help, list-checks, log-out, settings, github. Add an icon by appending its
Lucide geometry to the token list in the kit and documenting it here. The
hosted footer's social links are the only exception: the web `Icon` component
draws the X, LinkedIn, YouTube and Facebook brand marks from
`simple-icons@13.21.0` (CC0-1.0) as inline SVG.

Component rules:

- One primary button per view, in the panel footer or next to the field it
  submits. The local dashboard uses Add connection; its management views use
  Choose another connection. Row, detail and disclosure actions use the default
  style. Destructive actions use `rm-button--danger` and say what they remove.
- Primary and final destructive actions keep the standard 40 px size. Use
  `rm-button--sm` (32 px) only for secondary controls such as copy, refresh
  or show password.
- A link that stands alone, outside a sentence, uses `rm-link--standalone` for
  a 24 px target. Links inside a sentence keep their underline, including
  `rm-link--quiet`.
- A region that scrolls sideways, such as a wide table wrap or a code block,
  takes `tabindex="0"`, a role and a name, so keyboard users can scroll it
  and see the focus ring.
- Every icon-only control has an accessible name. Decorative icons get
  `aria-hidden="true"`.
- Choices are real controls: a `<button>` with `aria-pressed` or
  `aria-expanded`, a link, or a `<label>` around `rm-choice__input`.
- Selected controls keep a persistent non-text indicator with at least 3:1
  contrast against adjacent colors in both themes, even without focus.
  Segmented controls use a 2 px inset `--rm-accent` ring without changing their
  size. Keep the outer keyboard-focus ring separate. In forced colors, an inset
  `Highlight` border marks selection without replacing the focus outline.
  Toggles such as the setup guide button use `aria-pressed`; the kit draws a
  pressed `rm-icon-button` or `rm-menu__link` the same way.
- Start cards say who each route is for in `rm-choice__text` and what the
  person needs in `rm-choice__meta`, in plain words. Give a duration only when
  a doc states one.
- Fields keep a visible label. Hints and errors connect through
  `aria-describedby`; invalid fields set `aria-invalid="true"`.
- Status never relies on color: a status dot always sits next to text, and
  badges carry words.
- Errors that block progress use `rm-callout--danger` with `role="alert"` and
  take focus when they appear. In a step panel at 1024 px and wider, place the
  error in the panel header beside the step title or in the rail, so it never
  pushes the step past one screen; on narrower screens, place it next to the
  footer actions.
  Passing progress messages use `role="status"`.
- Commands use `rm-terminal`. The copy button announces success through a
  polite live region and does not move focus.
- Long tokens such as fingerprints, hostnames and URLs wrap
  (`overflow-wrap: anywhere` is built into fields, checks, callouts and
  definition lists).

Example step panel:

```html
<section class="rm-panel" aria-labelledby="vps-title">
  <header class="rm-panel__header">
    <div class="rm-panel__heading">
      <span class="rm-eyebrow">Step 2 of 5</span>
      <h2 id="vps-title" class="rm-h1" tabindex="-1">Check your server</h2>
      <p class="rm-muted">Confirm the server identity before you sign in to it.</p>
    </div>
  </header>
  <div class="rm-panel__body">…</div>
  <footer class="rm-panel__footer">
    <button class="rm-button rm-button--ghost" type="button">Back</button>
    <button class="rm-button rm-button--primary rm-push" type="submit">Connect</button>
  </footer>
</section>
```

## Setup guide

Every wizard page loads the small `/guide.css` and `/guide.js` and names its
guide content in `<body data-guide-page>`. The dock (`/guide-dock.js` with its
`/guide-dock.css`), the mascot (`/mascot.js`) and the page's tips load
asynchronously, keeping contextual Help available with the walkthrough off.
The first visit asks once whether to start the walkthrough.
`#guide-toggle` in the top bar, repeated in the menu on narrow windows, turns
it on or off, and `/api/ui/preferences` keeps the choice.

| Part | Classes | Rules |
| --- | --- | --- |
| Dock | `rm-guide`, `__head`, `__quest`, `__dots`, `__body`, `__title`, `__say`, `__steps`, `__example`, `__find`, `__next`, `__actions` | An `aside` named "Setup guide": the mascot, the quest name, numbered by the page's current step, and progress dots, one tip with an optional example and "Where do I find this?", then Back, Next tip, Show me, Hide and Skip guide. Its buttons use the default or ghost style, so the page keeps its one primary action. Escape collapses it and returns focus |
| Chip | `rm-guide-chip` | The collapsed dock, available on every view even with the walkthrough off. "Help" opens contextual help; an error uses "Need help with this error?" |
| Pointer | `rm-guide-pointer`, `rm-guide-marker` | A ring around the target and a cursor with a short action label. It glides with `transform` only. With reduced motion, Show me numbers the targets instead, and the numbers never overlap |
| Mascot | `rm-mascot` from `mascot.js` | Inline SVG from the logo vectors, decorative and hidden in forced colors. Poses: idle, look, happy, thinking, worried, waiting. The sleep cap uses `--rm-brand-cap`. While the guide is on, the mascot also stands in the quest track's current step |
| Badges | `rm-badge rm-badge--success`, `rm-guide-finish`, `__title`, `__badges`, `__next` | A chapter earns its badge when every action tip seen there is done; reading tips are optional. On `/` and `/local`, the Ready screen shows `#ready-quest` in the rail column with the badges earned and the page's next steps; the guide fills it and keeps it hidden while the guide is off. The route pages show the finish, the badges and the next steps in the dock. A tip whose control reloads the page uses `done: "manual"`, so the finish can still show |
| Toggle | `#guide-toggle`, `[data-guide-toggle]` | `aria-pressed` says whether the guide is on; the kit draws the pressed state |

Contextual help uses the existing dock, mascot and pointer. Its native
**Help topic** select lists the visible chapter's tips and bundled FAQ.
**Current step** prioritizes the bounded account state and operation state,
then the existing step tip. Identity, permission, ownership, first-use
acknowledgment, usage limits, busy and preview reasons stay distinct. An
inline **Help with Check the server** button remains keyboard reachable
beside the account status while that action is unavailable. Help controls
remain usable during operation locks; protected page controls stay locked.

**About this help** shows schema/revision, source version, review date and
source guides from `guide/context.js`. Each guide content module uses that
metadata. Update the metadata and source review together when help facts or
rules change; unsupported schema/revision and unknown state use a safe
fallback. Show me highlights and focuses without clicking, selecting or
confirming. Missing or hidden targets explain the gap and omit Show me.
Escape and Hide clear highlights and return focus; Help reopens without
changing the saved walkthrough choice. This is bundled help, with no model
request, retrieval service or saved question history.

Placement:

- The dock, chip and pointer are fixed layers. They never change page layout,
  so every view keeps the one-screen rule with the guide open.
- The dock never covers its protected items: the focused element, the view's
  primary action, an error callout, the panel footer's actions, and the
  buttons and links in a region marked `data-guide-keep`. The dashboard marks
  its selected connection and its empty state this way. The dock also keeps
  the rail column's content clear, apart from the quest track, and every
  warning. On wide windows it sits at the foot of the rail column, under its
  content, or at a corner of the panel. On narrow windows it is a sheet at the
  bottom of the window, above the panel footer or under the top bar.
- Until the person opens it from the chip, presses Start the guide, Back or
  Next tip, or moves focus into it, the dock also keeps the tip's target, the
  page and step titles and the quest track clear. When nothing fits, it
  collapses to the chip and the pointer still marks the target. An opened dock
  covers the target, titles or track before any safety content.
- An opened dock always shows. When no spot fits its full height, it is cut
  to the window below the top bar: in the rail column or at a panel corner on
  wide windows, and as a sheet or a half-width card on narrow ones. Its body
  scrolls, and its head and actions stay in view. It takes a stretch of at
  least 160 px that keeps the protected items clear. Only when a tiny window
  has no such stretch does it cover the fewest of them, instead of collapsing.
- The chip stays inside the window. It keeps the protected items clear before
  safety content and titles, and moves to the left edge when the right edge
  has no clear spot.
- The pointer hides while its target is scrolled out of view, including out
  of a scrolling panel body. Its action label never covers a checkbox or radio
  label or a disclosure summary. A checkbox or radio is pointed at from its
  left edge, and the label is left out when neither side is clear.
- Check placement at the five one-screen sizes plus 390 x 844, 320 x 640 and
  short windows such as 1280 x 320 and 568 x 320, in the default and error
  states.
- Game feel stays quiet: a badge chip, a checked progress dot and one hop of
  the mascot. No points, confetti, emoji or exclamation marks. Decorative
  motion stops within 5 seconds.

## Plan and usage panel

`usage-panel.js` loads on first use on `/` and `/local` and adds
`usage-panel.css` itself; its import finishes only once the stylesheet has
loaded, so pages that never show the panel load neither and nothing blocks the
first paint. It builds the panel with `textContent` from the page's own request
(`renderUsage(parts, info)`). The panel shows Relmio's counts of text requests
through the sidecar in the last 30 days, not the plan's usage. Model checks and
image requests are not counted, and tokens come from completed responses only.
OpenAI documents no plan percent, reset time or credits for this sign-in, so
the panel shows none of them and links to Manage usage instead.

Places:

- `/`: `#vps-usage` sits in the VPS owner panel after `#vps-owner-update`,
  with `usage-panel--divided`. It appears for an owned sidecar after Check
  installed account and reads only when the person presses Refresh usage.
  The server allows 10 reads in 15 minutes and answers 409 until the account
  check has run. The Ready screen has one line, `#result-usage`, in
  `.setup-results__main`, hidden while the key is held.
- `/local`: the dashboard has the nav link `#dashboard-usage-link` and the
  section `#dashboard-usage`, which reads by itself for a healthy n8n sidecar
  and shows a message otherwise. The installed view has
  `<details id="installed-usage">` after `#local-siwc-owner`. It reads when
  opened, for the n8n sidecar only.

Parts:

- `usage-panel__head` holds the title and Refresh usage (`rm-button--sm`). A
  polite `role="status"` line follows it.
- `usage-panel__view` holds two `usage-panel__group` columns, side by side
  when two 16rem columns fit and stacked below that. The counts group appears
  only with counts: the totals (`usage-panel__totals`: Requests, Tokens
  (completed), Active days, Peak day), an outcome line, and the three busiest
  models (`usage-panel__models`, `usage-panel__model`), each with its values in
  text and a decorative `rm-progress` bar. More models sit in a plain
  disclosure. The token qualifier stays in its label, since a hint line would
  make the first row of totals taller.
- The other group lists the account facts (`rm-dl usage-panel__facts`:
  account, plan use, the image add-on's plan type only while that add-on is
  signed in, models listed and verified), the last plan-usage event
  (`usage-panel__event`) with its next step, and a note
  (`usage-panel__note`) with the Manage usage link. The event shows its own
  Manage usage button only when that is the recovery. `/local` has no model
  checks, so there a verified model is one that returned tokens in the last
  30 days, and its label says so.

States: idle (the VPS panel before its first refresh), loading, `ok`, `empty`,
`unavailable` and a refused refresh. `ok`, `empty` and `unavailable` come from
the server's bounded view, and a malformed view reads as unavailable. On the
VPS, `empty` points at Review sidecar update. A refused refresh (409 or 429)
says what to do next in the panel's status line, not the page's error box, so
the guide's error help stays closed. The last counts stay, labelled with
their time.

## Color

Use semantic tokens only. Never write a raw color in a page stylesheet; add a
token to the kit when a new role appears. Hex values and contrast ratios are in
BRANDKIT.md.

| Token | Role |
| --- | --- |
| `--rm-canvas` | Page background |
| `--rm-surface` | Panels, cards, inputs |
| `--rm-surface-muted` | Quiet fills: tracks, table headers, hover |
| `--rm-surface-sunken` | Progress tracks and wells |
| `--rm-ink`, `--rm-ink-muted`, `--rm-ink-subtle` | Text, secondary text, placeholders and decoration |
| `--rm-line`, `--rm-line-strong`, `--rm-field-line` | Dividers, button outlines, form control borders |
| `--rm-accent`, `--rm-accent-hover`, `--rm-on-accent` | Primary action and selection |
| `--rm-accent-soft`, `--rm-accent-line`, `--rm-accent-ink` | Selected backgrounds, borders and text |
| `--rm-focus` | Focus ring |
| `--rm-success`, `--rm-warning`, `--rm-danger` and their `-soft`, `-line` pairs | Status |
| `--rm-terminal-*` | Terminal and code blocks, dark in both themes |
| `--rm-terminal-hover` | Terminal copy-button hover in both themes |
| `--rm-brand-*` | Logo colors and the mascot's sleep cap (`--rm-brand-cap`), for illustration only, never for UI state |

Light theme: pastel green surfaces, warm black ink, black primary buttons
with pale green text. Dark theme: black surfaces with no green tint, warm white
ink, pastel green primary buttons with black text. Orange is only for warnings,
red only for failures and destructive actions, and the success green only for
success. That success green is cooler than the accent in both themes and
than the light theme's yellow-green surfaces, so status never blends into the
page or looks like an action. Teal
appears only in the logo and mascot artwork, plus the hosted site's
illustration accents described above. No purple AI gradients, neon glows,
glass panels or decorative gradients.

## Typography

| Role | Family | Size | Weight |
| --- | --- | --- | --- |
| Display (web home headline) | Bricolage Grotesque | `--rm-text-display`, 36 to 60 px; the hosted home hero may reach 76 px | 800 |
| Web page and section titles | Bricolage Grotesque | 30 to 72 px, set per page | 800 |
| Page and step titles | Bricolage Grotesque | `--rm-text-h1`, 24 to 32 px | 760 |
| Section titles | Geist | 20 px | 650 |
| Body | Geist | 16 px | 400 |
| Interface labels | Geist | 14 px | 560 to 650 |
| Hints and metadata | Geist | 13 px | 400 |
| Eyebrows | System monospace | 12 px uppercase, 0.08em tracking | 600 |
| Commands and IDs | System monospace | 14 to 15 px | 400 |

Inputs use 16 px text so mobile browsers do not zoom. Keep body text under 75
characters per line. Use sentence case for headings, buttons and labels.

## Spacing, shape and elevation

- Spacing follows a 4 px grid: `--rm-space-1` (4) to `--rm-space-16` (64).
- Radii: 6 px small labels, 10 px controls, 14 px cards, 18 px panels, full
  round only for badges, chips and status dots.
- Elevation is rare. Cards use `--rm-shadow-sm`, panels `--rm-shadow-md`, menus
  and toasts `--rm-shadow-lg`. Lines carry most hierarchy, especially in dark
  mode.
- Do not nest more than two bordered containers.

## Motion

- Motion confirms a change: selection, a state change, new content. 120 to
  280 ms with `--rm-ease`.
- Animate only `transform` and `opacity`; never width, height, padding or
  position.
- `prefers-reduced-motion: reduce` stops spinners, pulses and slides; the kit
  provides static fallbacks.
- The home illustration may loop. It needs a visible pause control, pauses
  offscreen, and shows a complete still scene for reduced motion or missing
  SVG animation support. Owner decision, 2026-10-03: the Doorway keeps
  looping; the Pause control meets WCAG 2.2.2. Other decorative motion stops
  within 5 seconds.
- Hosted-site sections may rise into view as they scroll in. With reduced
  motion they are simply there.

## Copy

- Read the `humanizer` skill before writing interface text.
- Lead with the action or the fact. One idea per sentence. No em dashes in
  interface text.
- Keep these product labels exact and distinct: n8n with ChatGPT sign-in,
  SuperGrok OAuth, n8n Code Sandbox, Codex Chat Adapter, Codex App Server, and
  Local model.
- Distinguish verified ChatGPT identity, permission to use its plan, and a
  completed model request. None implies the others or creates an OpenAI
  Platform API key. Describe the new local/self-hosted flow as SIWC preview;
  legacy Codex credential copies require a fresh SIWC sign-in.
- Never imply a capability, permission or test result that has no recorded
  evidence. Hosted chat is off; never describe it as available.
- Prerelease builds show an `rm-notice` whose summary names the version and
  says "Use a test setup". The details hold the stable fallback command.
  Stable releases show no release notice.
- Label each experimental feature or provider with an `Experimental` badge
  (`rm-badge rm-badge--accent`) beside its name and one short warning that
  names the untested part. Do not repeat the warning on the same screen.
- Field errors name the field with its visible label, for example "Server
  address is invalid.", never an internal name such as "Hostname".

### ChatGPT plan controls

- Use **Continue with ChatGPT** to start the documented local sign-in flow.
  Keep account registrations distinct even when their email addresses match;
  show the active account and retain its stable label.
- After the first verified plan grant, explain **You're using your ChatGPT
  plan** in a dismissible, keyboard-accessible confirmation. Persist the
  acknowledgment; ordinary reauthorization must not show it again. Open it
  only in a visible view. Escape closes it without acknowledging; the plan
  stays unconfirmed and **Review plan notice** opens it again.
- Put **Using ChatGPT plan** and **Manage usage** near the relevant model
  selector or composer. Manage usage links to
  `https://chatgpt.com/settings/usage`; do not invent usage totals or reset times.
- A verified identity without the plan grant is signed in but cannot run
  inference. Offer explicit consent, not automatic repeated sign-in.
- Usage limits pause requests and make Manage usage the primary recovery.
  Never suggest account rotation or silently switch billing.
- Show where a transferred session is managed. Distinguish local cleanup from
  confirmed provider revocation, and require explicit consent for background
  n8n activity and the reviewed destination.

## Accessibility floor

- WCAG 2.2 AA. Text contrast at least 4.5:1, large text and controls at least
  3:1, in both themes. The focus ring is 3 px `--rm-focus` with a 2 px offset.
- Landmarks: one `header`, one `main`, labelled `nav` and `aside`. One `h1`
  per page; headings in order.
- Keyboard: everything reachable in reading order, no traps, visible focus,
  Escape closes menus. Do not reorder focusable content visually.
- Focused or scrolled-to elements must clear sticky controls. On narrow
  screens the document reserves top-bar and footer space through scroll
  padding; check heading focus, skip links and both Tab directions on phones.
- Modal dialogs move focus inside on open, keep Tab and Shift+Tab inside,
  close on Escape through their cancel action, and return focus to the trigger.
- Targets at least 24 x 24 px; primary controls 40 px or taller. A checkbox or
  radio may be smaller than 24 px when its whole visible label is clickable and
  that label meets the target size, as on the `/assistant` consent (owner
  decision, 2026-10-03).
- Do not disable zoom. Content reflows at 320 px wide without sideways
  scrolling.
- Respect `prefers-reduced-motion`, `forced-colors` and `prefers-color-scheme`.

## Implementation rules

Wizard (`src/ui`):

- Link `/relmio-ui.css` first, then any page stylesheet. Page stylesheets hold
  only layout specific to that page.
- Every new file in `src/ui` needs an entry in the server file map in
  `src/web/server.js` and in the package allowlist in
  `test/package-contents.test.js`.
- Keep element IDs, `data-*` hooks and class names that scripts or tests use.
  Search `src/ui/*.js` and `test/` before renaming.
- Write untrusted text with `textContent`, never `innerHTML`.
- Never weaken a safety step: host-key confirmation before authentication,
  the reviewed plan, and the final confirmation before remote writes.

Web (`web/`):

- Import `./relmio-ui.css` in the root layout before other styles.
- Use the React components in `web/app/components/ui/`. They render the kit
  markup; add to them instead of creating new styling systems.
- Page-specific styles go in a CSS module next to the page and use kit tokens.
  Styles shared across site pages, such as section titles, hill edges and
  scroll reveals, live in `app/site.module.css`.
- The root layout renders `SiteFooter` after every page. Keep its links and
  computed copyright year.
- Keep install commands and security attributes exact; tests guard them.
- `web/proxy.ts` sends a Content Security Policy with a fresh nonce on every
  page: scripts need the nonce (`'strict-dynamic'`), styles may be inline,
  and images, fonts, connections and frames stay on the site. Hosted ChatGPT
  sign-in is off until OpenAI approves access (issue #95), so add no OpenAI
  origin, loopback frame or extension exception before then.
  An inline `<script>` reads the nonce from the `x-nonce` request header;
  never add `'unsafe-inline'` to `script-src`.

## Verification

For each changed view, before handing off:

1. Run it: the wizard through `npm run preview` (sanitized, no live sign-in)
   or a QA launcher with fake services; the web app with `next dev`.
2. Use one headless browser tab and close it afterwards. Do not drive the
   owner's personal browser for routine checks.
3. Check 1280 x 720, 1440 x 900, 1024 x 768 and 390 x 844 in light and dark
   themes. In the wizard and dashboard, measure the one-screen rule. On the
   hosted site, check that the page scrolls naturally and add 320 px wide.
   Everywhere, measure horizontal overflow, check focus order with the
   keyboard, and read the console.
4. Audit against the Front-End Checklist with the `frontend-checklist-global`
   skill. Report findings with rule ids.
5. Run the affected root and web tests. Tests guard behavior, not class names
   or wording.

## Home illustration

The home page keeps the Doorway scene: the green two-eyed mascot, its cream
doorway, a VPS cloud and a local workshop, drawn as original vectors. It is
the hero of the scrolling home page, an unboxed landscape below the headline
and the install action. Let it span the page width and blend into the canvas,
without a card background, border, rounded clipping, shadow or backing sheet
(owner decision, 2026-10-04). Keep the headline and actions in their content
container. On phones, place Pause above the artwork so it does not cover it.
Preserve the original logo files exactly; the scene is separate artwork. Its
backdrop follows the theme: pastel green sky and darker green hills by day,
black sky and charcoal hills by night. Night mode may add a moon, stars, a lit
window and the mascot's sleep cap. Keep the pause control and the still
fallback described under Motion.

## Do and don't

Do:

- Reuse a kit component before writing new CSS.
- Write the shortest copy that keeps the meaning, then test it in the layout.
- Put the next action where the eye ends: the panel footer, right side.

Don't:

- Add a second component library, CSS framework or icon set.
- Stack warnings. One notice per view, with details inside it.
- Use a scrollbar to fit content at the target sizes.
- Center everything, use four equal feature cards with an oversized banner, or
  decorate with badges.