Appearance
Design system (House Style)
Single reference for reskinning Settings / Admin / Auth / public Vue pages. All facts below are extracted verbatim from the current sources — no guessing. Import paths use the @/ alias = resources/js/.
Source files:
- Primitives:
Window.vue,Button.vue,Badge.vue,StatusPill.vue,Stamp.vue,ThemeToggle.vue,Marquee.vue,Taskbar.vue,NookMark.vue - Tokens:
resources/css/app.css - References:
ConsoleLayout.vue,Dashboard.vue,FileManager.vue
Themes
Four themes ship: system (matches the OS), light ("House Style", the paper look), dark ("Dim & cozy"), and desktop ("Windows 95"). The active theme is the data-theme attribute on <html>; every token below is redefined per theme so a theme swap recolors the whole UI live.
ThemeToggle.vue is the switcher: it applies the choice optimistically (data-theme on <html>), writes the theme cookie + localStorage key imaginook.theme, and PATCHes /settings/theme when signed in. It is already placed in ConsoleLayout.vue's header — do not re-add it inside console pages; add it to auth/public layouts that lack the console chrome.
Two structural facts hold across all four themes:
- Radius. Every primitive applies
:style="{ borderRadius: 'var(--radius)' }"internally, so corners are 3px in light/dark and 0px in desktop. You never set radius yourself on a primitive. - Bevels.
ui-raised/ui-sunkenare the desktop-theme Win-95 bevel hooks (inert off desktop). Primitives carry them where needed; hand-rolled panels must add them.
Nothing in the kit is ever tilted, in any theme.
Accessibility
The semantic text and border tokens are WCAG-AA tuned per theme — the contrast between --content / --content-muted / --edge and their backgrounds is checked for AA against each theme's actual surface colors, not once globally. The deterministic contrast check lives in the dev tooling, so a token that drifts below AA fails locally rather than shipping.
A few rules keep the palette honest:
- The fixed brand hues (teal / gold / coral / charcoal) are ground colors only — they back the logo and the title bars, never AA-critical body text.
- Title-bar ink follows the bar: the system and studio title bars use dark ink (
#1c1c1a) on their lighter grounds, while social uses white on coral. - There is a dedicated
--warning-texttoken: gold is a fill-only color (it fails AA as text), so warning text uses--warning-textrather than the gold fill. - The light theme focus ring is charcoal, deliberately distinct from the teal accent fill so the ring never visually collides with an accent-filled control.
- The
--cm-*tokens are the CodeMirror editor colors and are not part of this AA pass.
Design tokens
Tokens are CSS custom properties on :root / [data-theme=...]; Tailwind utilities reference them via @theme inline (so every theme swap recolors live). Always reach for the token utility, never a literal hex or raw brand color — except inside NookMark.vue (the fixed brand mark).
Canonical token utilities (use these)
| Purpose | Utility class(es) | Backing token |
|---|---|---|
| Page ground | bg-surface | --surface (paper / dark #1b2127 / desktop teal) |
| Window/card body | bg-surface-raised | --surface-raised (white / #232c33 / silver #c3c7cb) |
| Wells, insets, tracks, table headers | bg-surface-sunken | --surface-sunken (--warm-100 / #141a1f / white) |
| Primary text | text-content | --content (ink / #f3efe6 / ink) |
| Secondary text | text-content-muted | --content-muted (--warm-600 / #b4babc / #3f474c) |
| Hints, placeholders, timestamps | text-content-subtle | --content-subtle (--warm-400 / #8b9298 / #6c7378) |
| 2px OS-window border | border-edge | --edge (charcoal / #3a444c / charcoal) |
| Soft divider/border | border-edge-soft, divide-edge-soft | --edge-soft |
| Accent (teal) surface | bg-accent + text-accent-contrast | --accent / --accent-contrast |
| Accent text/icon | text-accent | --accent |
| Accent hover/active | bg-accent-strong | --accent-strong |
| Danger (coral) | text-danger, border-danger, bg-danger, bg-danger/10 | --danger |
| Warning (gold) | text-warning, bg-warning | --warning |
| Title-bar grounds | bg-chrome-system / bg-chrome-studio / bg-chrome-social | --chrome-* (use via <Window variant>, rarely raw) |
Tints & alpha
Token utilities accept Tailwind alpha: bg-accent/5, bg-accent/10, bg-accent/15, bg-danger/10, border-edge/40, bg-surface/85. Used for selection/hover rows and soft callouts.
Brand-hue utilities (kept literal — narrow uses only)
bg-gold / text-gold / border-gold → --brand-gold; bg-red / text-white → --brand-coral; bg-teal / text-teal → --brand-teal. These are wired in @theme inline to the fixed brand hues. Prefer the semantic tokens (accent / warning / danger) for UI; use brand-hue utilities only where a primitive does (e.g. Badge gold/coral). Do NOT write raw hex like #1f9b90.
Legacy aliases (themed — safe but discouraged)
bg-paper → --surface, text-ink → --content. They theme correctly because they're repointed, but the swap rules below replace them with the explicit bg-surface / text-content names for clarity.
Type
font-mono&font-sans→ Space Mono. Label voice =font-mono text-xs font-bold uppercase tracking-wider(ortracking-wide).font-display→ Archivo Black. Headlines/wordmark:font-display text-2xl tracking-tight(h1),font-display text-lg(h2/section).
Shape / shadow / radius
border-2 border-edge= the canonical 2px OS border.- Radius: apply
:style="{ borderRadius: 'var(--radius)' }"on any raw element that needs the rounded corner (primitives already do this). Token utilityrounded-window(--radius-window) also exists. Never hardcoderounded-mdetc. shadow-window= the hard offset shadow3px 3px 0 0 var(--win-shadow).ui-raised/ui-sunken= the desktop-theme Win-95 bevel hooks (inert off desktop). Putui-raisedon raised panels/cards you hand-roll,ui-sunkenon wells; pair withborder-2 border-edge+shadow-window..dither= radial-dot halftone — text-free surfaces ONLY (progress fills, avatar tiles, divider bands). Never under text.
Component utility classes (from @layer components)
Still defined and themed; primitives are preferred, but these exist for plain markup: .input, .label, .err, .btn, .link, .tool, .tool-danger, .dither. The desktop theme rewrites .btn / .tool / .tool-danger / .input bevels automatically via the unlayered [data-theme='desktop'] rules.
Focus / motion (free, do not override)
:focus-visible → outline: 2px solid var(--focus) globally. Reduced-motion media query neutralizes animations/transitions. Don't add custom focus rings or remove outlines.
Components / UI primitives
Every primitive applies :style="{ borderRadius: 'var(--radius)' }" internally (so 3px in light/dark, 0px desktop) and carries ui-raised / ui-sunken where bevels are needed. You do not add radius or bevels yourself — the primitive handles all three themes. Nothing in the kit is ever tilted.
Window — Window.vue
The flat OS-window container. Renders a <section> with ui-raised border-2 border-edge bg-surface-raised text-content shadow-window, a colored title <header> (2px bottom border), and a p-4 body.
| Prop | Type | Default | Accepted values |
|---|---|---|---|
title | String | '' | any text (rendered mono/xs/bold/uppercase/tracking-wider in default title slot) |
variant | String | 'system' | system (teal bar, #1c1c1a ink) · studio (gold bar, #1c1c1a ink) · social (coral bar, white ink) |
controls | Boolean | true | show the decorative – × chrome squares (aria-hidden; purely decorative) |
Title-bar color comes from bg-chrome-system / bg-chrome-studio / bg-chrome-social. Pick the variant by domain: system = settings/account/security/general, studio = code/site building, social = profiles/comments/community.
Slots:
- default — body content (inside
p-4) title— replaces the default mono title span (use for custom title markup)actions— right side of the title bar, left of the– ×controls (put buttons/badges/links here)
vue
<Window variant="system" title="Profile">
<template #actions>
<Badge variant="gold">beta</Badge>
</template>
<!-- form / content here -->
</Window>Button — Button.vue
Flat House Style button. Renders an Inertia <Link> when href is set, otherwise a <button>. Base classes: inline-flex items-center justify-center gap-2 px-4 py-2 text-sm font-bold transition active:translate-x-px active:translate-y-px disabled:cursor-not-allowed disabled:opacity-60.
| Prop | Type | Default | Accepted values / notes |
|---|---|---|---|
variant | String | 'accent' | accent (teal CTA: bg-accent text-accent-contrast, hover bg-accent-strong) · neutral (toolbar control: bg-surface-raised text-content, hover bg-surface-sunken) · danger (border-danger bg-surface-raised text-danger, hover bg-danger/10) · ghost (borderless transparent, hover bg-surface-sunken) |
href | String | null | when set → renders Inertia <Link>; else <button> |
type | String | 'button' | only applied to <button> (ignored when href set). Set type="submit" for form submits |
block | Boolean | false | adds w-full |
Slot: default — label (icons/text; gap-2 handles spacing). Pass Inertia attrs (method, as, :data) through via v-bind/attrs only when it renders a <Link> — note the component forwards only href; for method="post" logout-style links use a raw <Link> instead (see ConsoleLayout.vue).
vue
<Button variant="accent" type="submit" :disabled="form.processing">Save changes</Button>
<Button variant="neutral" href="/settings/profile">Cancel</Button>
<Button variant="danger" @click="confirmDelete">Delete account</Button>
<Button variant="ghost" block>Show more</Button>Badge — Badge.vue
Small flat tag — mono, uppercase, letter-spaced. Base: inline-flex items-center gap-1 border px-1.5 py-0.5 font-mono text-[10px] font-bold uppercase leading-none tracking-wider. Used for "beta", roles, counts.
| Prop | Type | Default | Accepted values |
|---|---|---|---|
variant | String | 'neutral' | neutral (bg-surface-raised text-content) · accent (bg-accent text-accent-contrast) · gold (bg-gold text-[#1c1c1a]) · coral (bg-red text-white) · muted (border-edge-soft bg-transparent text-content-muted) |
Slot: default — label text.
vue
<Badge variant="gold">beta</Badge>
<Badge variant="accent">admin</Badge>
<Badge variant="muted">3 items</Badge>StatusPill — StatusPill.vue
Status indicator: a small colored dot + label in a flat neutral pill. Base: inline-flex items-center gap-1.5 border border-edge-soft bg-surface-raised px-2 py-0.5 font-mono text-[11px] font-bold uppercase tracking-wide text-content-muted.
| Prop | Type | Default | Accepted values |
|---|---|---|---|
tone | String | 'idle' | ok (dot bg-accent) · warn (dot bg-warning) · danger (dot bg-danger) · idle (dot bg-content-subtle) |
label | String | '' | used when no slot content given |
Slot: default — overrides label. Tone drives only the dot color; the pill body is always neutral.
vue
<StatusPill tone="ok">live</StatusPill>
<StatusPill tone="warn">pending</StatusPill>
<StatusPill :tone="user.banned ? 'danger' : 'ok'">{{ user.banned ? 'banned' : 'active' }}</StatusPill>Stamp — Stamp.vue
"Rubber stamp" label — thick (2px) bordered, uppercase mono, wide tracking (tracking-[0.2em]). The retro "UNDER CONSTRUCTION" / "BETA" voice. Base: inline-flex items-center border-2 px-2 py-1 font-mono text-xs font-bold uppercase tracking-[0.2em]. Flat and straight — never tilted.
| Prop | Type | Default | Accepted values |
|---|---|---|---|
tone | String | 'edge' | edge (border-edge text-content) · accent (border-accent text-accent) · danger (border-danger text-danger) · gold (border-gold text-gold) |
Slot: default — stamp text. Primary use: centerpiece of an empty state.
vue
<Stamp tone="gold">Under Construction</Stamp>
<Stamp tone="danger">Account Suspended</Stamp>ThemeToggle — ThemeToggle.vue
Self-contained theme switcher dropdown (system / light / dark / desktop). Applies the choice optimistically (data-theme on <html>), writes the theme cookie + localStorage key imaginook.theme, and PATCHes /settings/theme when signed in.
- No props, no slots. Drop it in. Already placed in
ConsoleLayout.vueheader — do not re-add inside console pages. Use it in auth/public layouts that lack a ConsoleLayout chrome. - Options array:
system("Match your OS"),light("House Style"),dark("Dim & cozy"),desktop("Windows 95").
vue
<ThemeToggle />Marquee — Marquee.vue
CSS scrolling ticker (two seamless copies, translateX -50%). Pauses on hover; frozen under reduced-motion. Used in the Taskbar.
| Prop | Type | Default | Notes |
|---|---|---|---|
duration | Number | 24 | seconds for one full loop |
Slot: default — the scrolling content (rendered twice; second copy is aria-hidden). Style the text via classes on the component (mono/uppercase as in Taskbar).
vue
<Marquee :duration="28" class="font-mono text-[11px] uppercase tracking-wider text-content-muted">
✦ news ✦ updates ✦
</Marquee>Taskbar — Taskbar.vue
The charcoal footer: Start lozenge (the StartMenu launcher), coral squiggle, Marquee ticker, live clock. Solid #2f3942 in every theme (brand chrome, never dithered). No props, no slots. Already rendered by ConsoleLayout.vue's footer — only add it to bespoke layouts.
vue
<Taskbar />StartMenu — StartMenu.vue
The taskbar's Start lozenge → a Win-95 Start menu: a branded vertical strip + the app/system launcher entries (from desktop/shortcuts.js) plus Log out. Opens upward from the taskbar. Click-away + Esc to close; arrow keys / Home / End move focus. Themed via tokens, so it's a launcher in every theme. Entries are the existing full-page routes — no window manager. No props, no slots (lives inside <Taskbar>).
DesktopIcon — DesktopIcon.vue
A Lite-desktop shortcut: glyph tile + label. Single-click selects (highlight), double-click / Enter / Space opens (navigates to href). Keyboard operable. Labels use text-content (near-ink — the AA-legible choice on the teal desktop ground; never muted/accent there).
| Prop | Type | Default | Notes |
|---|---|---|---|
label | String | — | required; shown under the glyph, clamped to 2 lines |
glyph | String | '▦' | emoji / char (decorative, aria-hidden) |
href | String | null | route to open |
method | String | 'get' | Inertia visit method |
selected | Boolean | false | controlled highlight (parent owns single selection) |
Emits select (single click) and open (double-click / key). Used by the desktop surface.
vue
<DesktopIcon
v-for="icon in desktopIcons"
:key="icon.key"
:label="icon.label" :glyph="icon.glyph" :href="icon.href"
:selected="selectedIcon === icon.key"
@select="selectedIcon = icon.key"
/>NookMark — NookMark.vue
The brand mark SVG (isometric "nook"). Palette is FIXED literal brand hues (teal #1f9b90, gold #e3b341, coral #c1554f, charcoal #2f3942 strokes, white doorway) — pixel-identical across all themes by design. This is the one place literal brand hex is correct; do not "tokenize" it.
| Prop | Type | Default | Notes |
|---|---|---|---|
size | Number|String | 32 | width & height in px |
title | String | 'Imaginook' | accessible label (<title> + aria-label) |
No slots.
vue
<NookMark :size="56" />
<NookMark :size="32" title="Imaginook home" />Page-structure recipes
Page wrapper
Console-area pages wrap everything in ConsoleLayout.vue (provides header nav, ThemeToggle, avatar, Taskbar). Pass title (sets <Head> + nav title).
vue
<ConsoleLayout title="Profile">
<!-- page content -->
</ConsoleLayout>Auth/public pages without ConsoleLayout must supply their own <Head>, and include <ThemeToggle />
- brand chrome (
NookMark,font-displaywordmark) themselves.
Page header block (h1 + subtitle + primary action)
Pattern from Dashboard.vue / FileManager.vue — flex row, headline left, primary <Button> right:
vue
<div class="mb-8 flex flex-wrap items-end justify-between gap-4">
<div>
<h1 class="font-display text-2xl tracking-tight">Profile</h1>
<p class="mt-1 font-mono text-sm uppercase tracking-wider text-content-muted">
Your account details.
</p>
</div>
<Button href="/settings/security">Security</Button>
</div>Section heading (between windows)
vue
<h2 class="mb-3 mt-8 font-display text-lg">Your sites</h2>Card / panel = <Window>
Choose variant by domain (system/studio/social). Body content sits in the p-4 default slot. Use #actions for in-bar controls.
vue
<Window variant="system" title="Email address">
<!-- form fields -->
</Window>Grid of windows (Dashboard): class="grid gap-4 sm:grid-cols-2".
A panel WITHOUT a title bar (e.g. FileManager side asides/upload panel): hand-roll with the canonical raw-panel recipe —
vue
<aside class="ui-raised border-2 border-edge bg-surface-raised p-4 shadow-window"
:style="{ borderRadius: 'var(--radius)' }"> … </aside>Forms (labels, inputs, errors, actions)
Label voice = .label; field = .input; error = .err (or the danger box). Submit via <Button type="submit">.
vue
<form @submit.prevent="form.post('/settings/profile')" class="space-y-4">
<div>
<label for="name" class="label">Display name</label>
<input id="name" v-model="form.name" class="input" type="text" />
<p v-if="form.errors.name" class="err">{{ form.errors.name }}</p>
</div>
<div class="flex items-center gap-2">
<Button type="submit" :disabled="form.processing">Save changes</Button>
<Button variant="neutral" href="/dashboard">Cancel</Button>
</div>
</form>Inline create/edit row (FileManager): class="input max-w-xs" next to <Button variant="neutral">. Page-level error banner:
vue
<p v-if="error" class="mb-3 border-2 border-danger bg-danger/10 px-3 py-2 text-sm font-bold text-danger"
:style="{ borderRadius: 'var(--radius)' }">{{ error }}</p>Tables (the canonical FileManager pattern)
Table lives inside a raised section. Header row = mono uppercase on bg-surface-sunken; rows divided by divide-edge-soft; hover/selection use accent tints.
vue
<section class="ui-raised overflow-hidden border-2 border-edge bg-surface-raised shadow-window"
:style="{ borderRadius: 'var(--radius)' }">
<table class="w-full text-left text-sm">
<thead class="bg-surface-sunken font-mono text-xs uppercase tracking-wider text-content-subtle">
<tr>
<th class="px-3 py-2">Name</th>
<th class="px-3 py-2 text-right">Size</th>
<th class="px-3 py-2"></th>
</tr>
</thead>
<tbody class="divide-y divide-edge-soft">
<tr class="cursor-pointer hover:bg-accent/5" :class="isSel ? 'bg-accent/10' : ''">
<td class="px-3 py-2">{{ row.name }}</td>
<td class="px-3 py-2 text-right text-content-muted">{{ row.size }}</td>
<td class="px-3 py-2 text-right">
<button type="button" class="text-xs link">edit</button>
</td>
</tr>
</tbody>
</table>
</section>Conventions: cell padding px-3 py-2; secondary cell text text-content-muted, subtle/timestamp text-content-subtle; numeric cols text-right (Dashboard also uses tabular-nums for figures); action links use class="text-xs link". Loading row: <td colspan=... class="px-3 py-6 text-center text-content-subtle">Loading…</td>.
Empty states
Two patterns:
(a) Window-framed empty state (Dashboard "No sites yet") — NookMark + Stamp + copy + CTA, centered:
vue
<Window variant="system" title="No sites yet">
<div class="flex flex-col items-center gap-4 py-8 text-center">
<NookMark :size="56" />
<Stamp tone="gold">Under Construction</Stamp>
<p class="max-w-md font-mono text-sm text-content-muted">No data yet.</p>
<Button href="/sites/new">Get started</Button>
</div>
</Window>(b) In-table empty row (FileManager): <td colspan="N" class="px-3 py-10 text-center text-content-subtle">This folder is empty. …</td>.
Status & tags
- Live/active/pending/banned →
<StatusPill :tone="…">. - Roles, "beta", counts →
<Badge :variant="…">. - Big retro callout →
<Stamp :tone="…">.
Progress bars / fills (the ONE place dither belongs)
Track = sunken bordered box; fill = .dither colored by state. Width via inline style. No text on the fill.
vue
<div class="h-3 overflow-hidden border-2 border-edge bg-surface-sunken"
:style="{ borderRadius: 'var(--radius)' }">
<div class="dither h-full transition-all"
:class="exceeded ? 'bg-danger' : near ? 'bg-warning' : 'bg-accent'"
:style="{ width: pct + '%' }" />
</div>State→color mapping used across the app: exceeded/error → bg-danger / text-danger; near/warn → bg-warning / text-warning; ok → bg-accent / text-accent.
Avatar / initials tile (ConsoleLayout pattern)
vue
<span class="grid size-9 place-items-center border-2 border-edge bg-accent/15 font-mono text-sm font-bold text-accent"
:style="{ borderRadius: 'var(--radius)' }">{{ initials }}</span>Nav links (active state)
Underline-on-active, mono uppercase (ConsoleLayout):
vue
<Link class="cursor-pointer border-b-2 pb-0.5 transition"
:class="active ? 'border-accent text-accent' : 'border-transparent text-content-muted hover:text-accent'"
:aria-current="active ? 'page' : undefined">{{ label }}</Link>Inline action links / breadcrumbs
- Inline link:
class="link"(+text-xs font-boldfor small). - Breadcrumb button:
class="px-1.5 py-0.5 hover:bg-surface-sunken hover:text-accent"+ radius style; separators<span class="text-content-subtle">/</span>in afont-mono text-xs uppercase tracking-wider text-content-mutednav.
Lite desktop (Desktop theme only)
In the Desktop theme the dashboard swaps to a desktop surface — DesktopIcon shortcuts on the teal wallpaper — while Light/Dark keep the card dashboard. The swap is CSS-gated, no JS theme detection, no FOUC: the server stamps data-theme on <html>, and app.css toggles two wrapper classes. Below sm the surface stays hidden and the card layout shows (touch fallback).
vue
<section class="desktop-surface" aria-label="Desktop"> … <DesktopIcon …/> … </section>
<div class="console-cards"> …the normal card dashboard… </div>css
.desktop-surface { display: none; }
@media (min-width: 640px) {
[data-theme='desktop'] .desktop-surface { display: block; }
[data-theme='desktop'] .console-cards { display: none; }
}Shortcuts come from desktop/shortcuts.js: appShortcuts(page) (the consistent app/system set, shared with the Start menu) and siteShortcuts(sites) (one per active site). Per-site entries resolve against the primarySite shared prop. No window manager — opening an icon navigates to the existing full-page route (the draggable webtop is the post-launch v1.1 epic).
Migration / swap rules
Goal: replace ad-hoc .btn / .tool / .input classes and literal colors with primitives + token utilities, keeping all three themes correct. Where a primitive exists, prefer it over the @layer components class.
| # | Before (ad-hoc / literal) | After (primitive / token) |
|---|---|---|
| 1 | <button class="btn">Save</button> | <Button variant="accent" type="submit">Save</Button> |
| 2 | <button class="tool">Edit</button> | <Button variant="neutral">Edit</Button> |
| 3 | <button class="tool-danger">Delete</button> | <Button variant="danger">Delete</Button> |
| 4 | <a class="btn" href="/x">Go</a> | <Button href="/x">Go</Button> (renders Inertia Link) |
| 5 | class="rounded-lg bg-white border ..." card | <Window variant="system" title="..."> (or, if no title bar wanted, a raw panel: class="ui-raised border-2 border-edge bg-surface-raised shadow-window p-4" + :style="{ borderRadius: 'var(--radius)' }") |
| 6 | bg-white | bg-surface-raised (cards/windows) or bg-surface (page ground) |
| 7 | text-ink / text-black / text-[#1c1c1a] | text-content |
| 8 | text-gray-500 / muted greys | text-content-muted; for hints/timestamps text-content-subtle |
| 9 | bg-gray-100 / well backgrounds | bg-surface-sunken |
| 10 | border border-gray-300 / any literal border | border-2 border-edge (or border-edge-soft / divide-edge-soft for soft dividers) |
| 11 | text-[#1f9b90] / raw teal / text-teal for UI | text-accent; bg-accent + text-accent-contrast for filled |
| 12 | hover :hover:bg-teal-700 | hover:bg-accent-strong |
| 13 | text-red-600 / coral hex for errors | text-danger; error box border-2 border-danger bg-danger/10 text-danger |
| 14 | text-yellow-600 / gold hex warnings | text-warning (or bg-warning for fills) |
| 15 | rounded, rounded-md, rounded-xl | drop it; add :style="{ borderRadius: 'var(--radius)' }" (or rounded-window) |
| 16 | shadow, shadow-md, custom drop shadow | shadow-window |
| 17 | <span class="badge">beta</span> / hand-rolled tag | <Badge variant="gold">beta</Badge> |
| 18 | hand-rolled status dot + text | <StatusPill tone="ok">live</StatusPill> |
| 19 | "coming soon" / placeholder banner, rotated stamps | <Stamp tone="gold">…</Stamp> (never rotate-* / skew-*) |
| 20 | class="font-bold text-xl" headings | h1 font-display text-2xl tracking-tight; h2 font-display text-lg |
| 21 | inline label <label class="text-sm"> | .label class or font-mono text-xs font-bold uppercase tracking-wider text-content-muted |
| 22 | raw <input class="border p-2"> | class="input" (or class="input max-w-xs"); never literal border/bg |
| 23 | <a class="text-blue-600 underline"> | class="link" (= text-accent + hover underline) |
| 24 | any rotate-2 / -rotate-1 / skew-* | remove entirely — nothing is ever tilted |
| 25 | .dither / halftone applied under text | move it to a text-free element only (progress fill, tile) |
| 26 | hardcoded panel needing bevels in desktop | add ui-raised (panels/cards) or ui-sunken (wells); primitives already include this |
Notes:
.btnisw-fullby default in CSS;<Button>is NOT (blockprop for full width). Don't blindly assume width parity — addblockwhere the old.btnfilled its row..toolcarriescursor-pointer;<Button>uses default button cursor (fine).- For Inertia method links (e.g.
method="post" as="button"logout), keep a raw<Link>styled with token utilities —<Button>only forwardshref.
Locked design rules
Enforce these on every reskinned page:
- Palette is ONLY teal/coral/gold/charcoal + neutrals via tokens. No literal hex, no
bg-white, notext-blue-*, no new colors. (NookMark's fixed hues are the sole exception.) - Type = Space Mono (
font-mono, uppercase letter-spaced labels) + Archivo Black (font-displayheadlines). - Flat OS-window look:
border-2 border-edge,var(--radius), colored title bar via<Window variant>,shadow-window. Buttons/pills/stamps are flat. - Never tilt anything — no
rotate-*/skew-*. .ditheronly on text-free surfaces (progress fills, tiles, bands).- Prefer primitives over hand-rolled markup so light/dark/desktop (Win-95 bevels via
ui-raised/ui-sunken) all render correctly; when hand-rolling a panel addui-raised/ui-sunken. - Do not touch security invariants (preview sandbox, CSP, PathGuard, EditableFileGuard, inert serving,
encryptCookiesexcepttheme) — these are server-side; reskin stays in markup/classes.