# 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 ``. 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 `© 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 `
` 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 ``; 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
Relmio
``` ## 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--`, 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 ` ``` ## Setup guide Every wizard page loads the small `/guide.css` and `/guide.js` and names its guide content in ``. 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 `
` 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 `