# danchilton.com — design system A single monorepo of nine Svelte SPA micro-apps sharing one design system. This is the complete system in one flat document — no JavaScript, no scrolling, no pagination. Per-page versions are alongside it: https://danchilton.com/styleguide/foundations.md, components.md, screens.md, drift.md Generated from source at build time, so it cannot drift from the code it describes. Rendered board: https://danchilton.com/styleguide/ ## Basis - Design system: **shadcn-svelte v1.5.0**, base colour `zinc` - Radius: `--radius: 0.5rem` (`sm` = r−4px, `md` = r−2px, `lg` = r, `xl` = r+4px, `2xl` = r+8px) - Body face: **Figtree**, fallback `ui-sans-serif, system-ui, sans-serif` - Tailwind **v4**, CSS-first config. Token source of truth: `src/shared/styles/theme.css` - Dark mode is live, class-strategy (`.dark` on the root element) - Spacing is Tailwind's 4px base. In practice: `gap-2`/`gap-3` inside rows, `gap-4` between cards, `px-4 py-2.5` for tappable rows, `min-h-9`/`min-h-11` for touch targets. **Rule:** always use semantic tokens (`bg-background`, `bg-card`, `text-muted-foreground`, `border-border`, `bg-primary`). Never raw ramps like `bg-gray-100` or `bg-blue-600`. The repo currently contains zero raw ramps; keeping it that way is the point. ## Colour tokens Every value is `oklch()`. Both themes are listed; consumers reference the semantic name, never the literal. ### Surfaces | Token | Light | Dark | | --- | --- | --- | | `--background` | `oklch(1 0 0)` | `oklch(0.141 0.005 285.823)` | | `--foreground` | `oklch(0.141 0.005 285.823)` | `oklch(0.985 0 0)` | | `--card` | `oklch(1 0 0)` | `oklch(0.21 0.006 285.885)` | | `--card-foreground` | `oklch(0.141 0.005 285.823)` | `oklch(0.985 0 0)` | | `--popover` | `oklch(1 0 0)` | `oklch(0.21 0.006 285.885)` | | `--popover-foreground` | `oklch(0.141 0.005 285.823)` | `oklch(0.985 0 0)` | | `--muted` | `oklch(0.967 0.001 286.375)` | `oklch(0.274 0.006 286.033)` | | `--muted-foreground` | `oklch(0.552 0.016 285.938)` | `oklch(0.705 0.015 286.067)` | | `--accent` | `oklch(0.967 0.001 286.375)` | `oklch(0.274 0.006 286.033)` | | `--accent-foreground` | `oklch(0.21 0.006 285.885)` | `oklch(0.985 0 0)` | | `--sidebar` | `oklch(0.985 0 0)` | `oklch(0.21 0.006 285.885)` | | `--sidebar-foreground` | `oklch(0.141 0.005 285.823)` | `oklch(0.985 0 0)` | | `--sidebar-accent` | `oklch(0.967 0.001 286.375)` | `oklch(0.274 0.006 286.033)` | | `--sidebar-accent-foreground` | `oklch(0.21 0.006 285.885)` | `oklch(0.985 0 0)` | ### Actions | Token | Light | Dark | | --- | --- | --- | | `--primary` | `oklch(0.21 0.006 285.885)` | `oklch(0.92 0.004 286.32)` | | `--primary-foreground` | `oklch(0.985 0 0)` | `oklch(0.21 0.006 285.885)` | | `--secondary` | `oklch(0.967 0.001 286.375)` | `oklch(0.274 0.006 286.033)` | | `--secondary-foreground` | `oklch(0.21 0.006 285.885)` | `oklch(0.985 0 0)` | | `--destructive` | `oklch(0.577 0.245 27.325)` | `oklch(0.704 0.191 22.216)` | ### Status Project additions on top of stock shadcn. Green = a goal met or a set completed, indigo = AI output, amber = advisory, slate = pending (not started, paused, set aside, below target). These carry information, so they are tokens rather than flattened to muted. Badge has matching success/warning/info/pending variants — use those, never a raw green-*/amber-* ramp. USE EACH TOKEN WITH ITS PAIRED FOREGROUND: only success (3.67:1) and pending (4.77:1) are legible as coloured text on the light card; warning is 2.15:1, below even the 3:1 large-text floor, so amber appears as text only as text-warning-foreground on a bg-warning/N tint. destructive is deliberately not in this group: it means an irreversible action, never a status and never a number. | Token | Light | Dark | | --- | --- | --- | | `--success` | `oklch(0.596 0.145 163.225)` | `oklch(0.696 0.17 162.48)` | | `--success-foreground` | `oklch(0.985 0 0)` | `oklch(0.21 0.006 285.885)` | | `--warning` | `oklch(0.769 0.188 70.08)` | `oklch(0.828 0.189 84.429)` | | `--warning-foreground` | `oklch(0.21 0.006 285.885)` | `oklch(0.21 0.006 285.885)` | | `--info` | `oklch(0.585 0.233 277.117)` | `oklch(0.673 0.182 276.935)` | | `--info-foreground` | `oklch(0.985 0 0)` | `oklch(0.21 0.006 285.885)` | | `--pending` | `oklch(0.554 0.046 257.417)` | `oklch(0.704 0.04 256.788)` | | `--pending-foreground` | `oklch(0.985 0 0)` | `oklch(0.21 0.006 285.885)` | ### Lines | Token | Light | Dark | | --- | --- | --- | | `--border` | `oklch(0.92 0.004 286.32)` | `oklch(1 0 0 / 10%)` | | `--input` | `oklch(0.92 0.004 286.32)` | `oklch(1 0 0 / 15%)` | | `--ring` | `oklch(0.705 0.015 286.067)` | `oklch(0.552 0.016 285.938)` | | `--sidebar-border` | `oklch(0.92 0.004 286.32)` | `oklch(1 0 0 / 10%)` | | `--sidebar-ring` | `oklch(0.705 0.015 286.067)` | `oklch(0.552 0.016 285.938)` | ### Chart ramp | Token | Light | Dark | | --- | --- | --- | | `--chart-1` | `oklch(0.646 0.222 41.116)` | `oklch(0.488 0.243 264.376)` | | `--chart-2` | `oklch(0.6 0.118 184.704)` | `oklch(0.696 0.17 162.48)` | | `--chart-3` | `oklch(0.398 0.07 227.392)` | `oklch(0.769 0.188 70.08)` | | `--chart-4` | `oklch(0.828 0.189 84.429)` | `oklch(0.627 0.265 303.9)` | | `--chart-5` | `oklch(0.769 0.188 70.08)` | `oklch(0.645 0.246 16.439)` | ### Superset Categorical, never semantic: a colour here says "these exercises are one superset" and nothing about state. Hues stay clear of success green, warning amber and destructive red so a stripe is never misread as a status. Picked by index at runtime via inline style="var(--superset-N)" — Tailwind cannot scan a dynamic class name, so there are no utilities for these. | Token | Light | Dark | | --- | --- | --- | | `--superset-1` | `oklch(0.585 0.233 277.117)` | `oklch(0.673 0.182 276.935)` | | `--superset-2` | `oklch(0.6 0.13 195)` | `oklch(0.72 0.12 195)` | | `--superset-3` | `oklch(0.6 0.22 330)` | `oklch(0.72 0.19 330)` | | `--superset-4` | `oklch(0.65 0.17 55)` | `oklch(0.75 0.15 55)` | ## Components Vendored into the repo under `src/shared/components/` and editable — they are not an installed package. The `$lib` alias maps to `src/shared`. ### Primitives — shadcn-svelte, vendored (23) `src/shared/components/ui/`. Stock shadcn-svelte. These carry the shared focus, sizing and disabled behaviour. - `alert` - `alert-dialog` - `badge` - `button` - `button-group` - `checkbox` - `dialog` - `dropdown-menu` - `input` - `label` - `native-select` - `popover` - `separator` - `sheet` - `sidebar` - `skeleton` - `sonner` - `spinner` - `switch` - `textarea` - `toggle` - `toggle-group` - `tooltip` **Button** variants: `default`, `outline`, `secondary`, `ghost`, `destructive`, `link` **Button** sizes: `default`, `xs`, `sm`, `lg`, `icon`, `icon-xs`, `icon-sm`, `icon-lg` **Badge** variants: `default`, `secondary`, `destructive`, `outline`, `ghost`, `success`, `warning`, `info`, `pending`, `link` — `success`/`warning`/`info` are project additions backed by the matching tokens. ### Composites — project-specific (19) `src/shared/components/`. Built on the primitives. These define what the apps actually look like, so they matter more than the primitives for matching the style. | Component | Props | | --- | --- | | **AppShell** | `appName`, `displayName`, `icon`, `addLabel`, `addIcon`, `onHomeClick`, `onAddClick`, `onSettingsClick`, `searchValue`, `onSearchInput`, `searchable`, `searchPlaceholder`, `searchFullWidth`, `hasAdd`, `hasSettings`, `sidebar`, `children` | | **BackLink** | `label`, `onclick` | | **ConfirmDeleteModal** | `open`, `onClose`, `onConfirm`, `title`, `message`, `confirmLabel`, `loading` | | **DeleteButton** | `variant`, `label`, `onclick`, `disabled`, `showIcon` | | **EmptyState** | `icon`, `title`, `description`, `action` | | **GroupLabel** | `use`, `sticky`, `children` | | **IconNameEditor** | `items`, `fallbackIcon`, `entityLabel`, `iconPlaceholder`, `emptyMessage`, `onSave`, `onDelete`, `deleteMessage` | | **ListRow** | `id`, `title`, `subtitle`, `leading`, `meta`, `trailing`, `layout`, `density`, `href`, `onclick`, `selectMode`, `checked`, `onCheckedChange`, `selected`, `disabled` | | **ListRowSkeleton** | `layout`, `density` | | **ListToolbar** | `filters`, `sort`, `view`, `select` | | **LucideIcon** | `name`, `strokeWidth` | | **Overlay** | `open`, `onClose`, `presentation`, `modal`, `size`, `responsive`, `title`, `subtitle`, `ariaLabel`, `dismissible`, `fill`, `header`, `headerActions`, `footer`, `children` | | **OverlayDetail** | `open`, `onClose`, `size`, `title`, `subtitle`, `ariaLabel`, `header`, `headerActions`, `footer`, `children` | | **PageHeader** | `title`, `scope`, `count`, `description`, `actions`, `toolbar` | | **SectionHeader** | `title`, `description`, `action` | | **SidebarItem** | `label`, `active`, `onclick`, `href`, `count`, `icon` | | **StatCard** | `label`, `value`, `caption`, `period`, `tone`, `secondary`, `children` | | **TagInput** | `selectedTags`, `allTags`, `onCreate`, `onchange`, `placeholder` | | **ViewBody** | `archetype`, `children` | ### Interaction states These do not appear in any screenshot, so they are easy to miss and easy to design away by accident: - **Row actions are hover-revealed on desktop, always visible on mobile.** `BookmarkRow` (archive/restore, open in new tab), `QuoteRow` (favourite) and `BookRow` (start/finish/reopen/queue) wrap their trailing actions in `md:opacity-0 md:group-hover:opacity-100 md:group-focus-within:opacity-100` on a `group` row. Focus-within is deliberate: the actions must reach the keyboard, not just the mouse. - Tag chips inside rows are buttons, and `stopPropagation` so they filter rather than opening the row. - Rows have a select mode that swaps the whole row click target for a checkbox. - Touch targets are `min-h-9` for pill nav items, `min-h-11`/`min-h-12` for primary actions. `AppShell` is the one to understand first: eight of the nine apps render inside it, so it defines the frame — collapsible sidebar, 4rem top bar with a pill search field, primary action button, and an app-switcher grid. Below the `md` breakpoint the sidebar goes off-canvas and the brand moves into the top bar. ## Screens 30 screens, each rendering the real app component against fixture data. Each has its own URL and returns the app's real DOM — fetch these individually rather than the board page, which is 30 iframes. Two caveats before reading these: - **Hover and focus states do not appear in a screenshot.** Every list row has per-row actions hidden until hover on desktop (`md:opacity-0 md:group-hover:opacity-100`) and always visible on mobile. Read the mobile rendering, or the component source, before concluding a row has no actions. - The board page at `/styleguide/screens` is 30 iframes. Fetch the individual URLs below instead. ### All screen URLs Flat list, in case you want to queue them. https://danchilton.com/styleguide/screens/bookmarks-empty https://danchilton.com/styleguide/screens/bookmarks-grid https://danchilton.com/styleguide/screens/bookmarks-list https://danchilton.com/styleguide/screens/bookmarks-settings https://danchilton.com/styleguide/screens/cars-fuel https://danchilton.com/styleguide/screens/cars-overview https://danchilton.com/styleguide/screens/cars-service https://danchilton.com/styleguide/screens/dexa-list https://danchilton.com/styleguide/screens/exercise-analytics https://danchilton.com/styleguide/screens/exercise-library https://danchilton.com/styleguide/screens/exercise-history https://danchilton.com/styleguide/screens/exercise-session https://danchilton.com/styleguide/screens/exercise-programs https://danchilton.com/styleguide/screens/exercise-summary https://danchilton.com/styleguide/screens/exercise-settings https://danchilton.com/styleguide/screens/exercise-today https://danchilton.com/styleguide/screens/games-list https://danchilton.com/styleguide/screens/hub-dashboard https://danchilton.com/styleguide/screens/hub-login https://danchilton.com/styleguide/screens/hub-settings https://danchilton.com/styleguide/screens/quotes-list https://danchilton.com/styleguide/screens/quotes-settings https://danchilton.com/styleguide/screens/quotes-stats https://danchilton.com/styleguide/screens/reading-library https://danchilton.com/styleguide/screens/reading-settings https://danchilton.com/styleguide/screens/reading-stats https://danchilton.com/styleguide/screens/reading-today https://danchilton.com/styleguide/screens/stats-activity https://danchilton.com/styleguide/screens/stats-settings https://danchilton.com/styleguide/screens/stats-today ### By app #### bookmarks - **Empty state** — https://danchilton.com/styleguide/screens/bookmarks-empty The real empty state, driven by an empty fixture response. Bordered icon in a rounded square, then a heading and one line of guidance — bookmarks and reading share this shape; stats and cars do not. - **Grid** — https://danchilton.com/styleguide/screens/bookmarks-grid Same list, one word changed: ListRow layout=“card”. Bookmarks, quotes and reading each used to ship a row *and* a card component — six doing two jobs. - **List** — https://danchilton.com/styleguide/screens/bookmarks-list The real BookmarkList and BookmarksSidebar, filled from the fixture backend. Rows are flat and full-bleed — no bg-muted pill, unlike the shared ListRow. - **Settings** — https://danchilton.com/styleguide/screens/bookmarks-settings Three SettingsGroups — Reminders, Collections & tags, Import. The reminder email is a deferred field: editing it raises the shared sticky Save/Discard bar rather than a per-screen Save button. #### cars - **Fuel** — https://danchilton.com/styleguide/screens/cars-fuel The stat grid and the fill-up list are both shared components now: StatCard for every number (each with a caption) and ListRow layout=“card” for the entries, whose Edit/Delete used to be a split full-width footer strip. - **Overview** — https://danchilton.com/styleguide/screens/cars-overview Vehicle summary. Cars shows a record as a tab set in the main pane rather than in an inspector panel — the one app where that reads better than a drawer, since a vehicle is the whole screen. - **Service log** — https://danchilton.com/styleguide/screens/cars-service Maintenance entries. Note the row treatment: a bordered card with a split Edit/Delete footer, unlike every other app's row. #### dexa - **Metric list** — https://danchilton.com/styleguide/screens/dexa-list The whole dexa app, rendered as-is — its data is a static local module, no PocketBase. It is also the only adopter of the shared ListRow, and the only app using zero shadcn primitives. #### exercise - **Analytics** — https://danchilton.com/styleguide/screens/exercise-analytics Personal records and trend charts. RpeTrendChart is a canvas, so it has to touch mode.current in its draw effect to survive a theme flip. - **Exercise library** — https://danchilton.com/styleguide/screens/exercise-library A windowed list — 3,242 rows is far too many for the DOM, so it grows a window as you scroll. The filter bar is exercise-only. - **History** — https://danchilton.com/styleguide/screens/exercise-history Completed sessions. Rows here are a third distinct row treatment — neither the shared ListRow pill nor the flat full-bleed rows bookmarks and quotes use. - **Live session** — https://danchilton.com/styleguide/screens/exercise-session The workout in progress — the densest screen in the repo, and the only place the categorical --superset-N palette appears. Two exercises share a superset group; the stripe says they are one group and deliberately nothing about status. - **Programs** — https://danchilton.com/styleguide/screens/exercise-programs ProgramList uses Badge success/warning/secondary for active/draft/archived — a good example of the status tokens carrying real information. - **Session summary** — https://danchilton.com/styleguide/screens/exercise-summary What a finished workout looks like on review. Reached from the live session via Finish, and from any row in History. - **Settings** — https://danchilton.com/styleguide/screens/exercise-settings Two SettingsGroups showing both halves of the save model at once: the Profile textareas defer to the sticky bar, while the App preferences toggles commit on flip and flash Saved. - **Today** — https://danchilton.com/styleguide/screens/exercise-today Unfinished in the app itself — TodayView is still the scaffold placeholder from 2026-08-12, so the board shows a header and "Hello world". The shell around it is real. #### games - **Picker** — https://danchilton.com/styleguide/screens/games-list AppShell with searchable, add and settings all off — the most stripped-back use of the shell. Games themselves ship deliberately hand-rolled stylesheets scoped to a game class, which is the one sanctioned exception to the token rules. #### hub - **Dashboard** — https://danchilton.com/styleguide/screens/hub-dashboard The app grid after sign-in, rendered from the generated app-list.json. The style guide sets hidden: true in its app.config.ts, so it is absent here by design. - **Login** — https://danchilton.com/styleguide/screens/hub-login The front door, and the only screen an anonymous visitor normally reaches. Hub is the one app not on AppShell — it has no sidebar and no app switcher of its own. - **Settings** — https://danchilton.com/styleguide/screens/hub-settings Account-scope settings: theme, passkeys and sign-out apply across all nine apps, so they live here rather than being declared nine times. Theme was previously reachable only as a two-state icon toggle, which could not express the three-state preference. #### quotes - **List** — https://danchilton.com/styleguide/screens/quotes-list The shared ListRow. QuoteRow and BookmarkRow had near-identical signatures — the same component written twice — and both are gone. Tag chips arrive through the meta slot, so the row no longer knows what a tag is. - **Settings** — https://danchilton.com/styleguide/screens/quotes-settings SettingsGroups with both halves of the save model: the delivery fields defer to the sticky Save/Discard bar, while Send time commits on change. Quotes used to carry two separate Save buttons. - **Stats** — https://danchilton.com/styleguide/screens/quotes-stats Quotes has its own stats view distinct from the Stats app. Card shapes differ between the two — a good candidate for a shared StatCard. #### reading - **Library** — https://danchilton.com/styleguide/screens/reading-library BookListRow maps a Book onto the shared ListRow; StarRating, StatusBadge and FormatLabel compose into its meta slot rather than being row props. - **Settings** — https://danchilton.com/styleguide/screens/reading-settings One SettingsGroup. Reading has no settings collection, so nothing here defers — tag edits persist inline and no save bar appears. The annual goal stays on Today: it is view state, not a setting. - **Stats** — https://danchilton.com/styleguide/screens/reading-stats The reading stats view. Compare the card treatment against the Stats app and quotes — three takes on the same idea. - **Today** — https://danchilton.com/styleguide/screens/reading-today The home shelf — what is being read now, what is up next. Card shapes here differ from the library grid two screens over. #### stats - **Activity detail** — https://danchilton.com/styleguide/screens/stats-activity YearHeatmap plus the stat cards. The heatmap is a canvas — it has to touch mode.current inside its draw effect or it keeps painting the old palette after a theme flip. - **Settings** — https://danchilton.com/styleguide/screens/stats-settings One SettingsGroup. The send-hour range slider became a select and commits on change; the recipient email defers to the sticky bar. - **Today** — https://danchilton.com/styleguide/screens/stats-today Tick-off list for the day. The only app with searchable={false} on AppShell, so the top bar collapses to brand + add + apps. ### By pattern Cross-cutting sets — the comparisons worth making across apps rather than within one. A screen can appear in more than one set. #### Row treatments The same job — a tappable list row — solved four ways. The clearest single view of the drift. - bookmarks · **List** — https://danchilton.com/styleguide/screens/bookmarks-list - quotes · **List** — https://danchilton.com/styleguide/screens/quotes-list - reading · **Library** — https://danchilton.com/styleguide/screens/reading-library - dexa · **Metric list** — https://danchilton.com/styleguide/screens/dexa-list - exercise · **History** — https://danchilton.com/styleguide/screens/exercise-history #### Card variants Three apps ship a card alongside their row, doubling the component count for one presentation choice. - bookmarks · **Grid** — https://danchilton.com/styleguide/screens/bookmarks-grid - quotes · **List** — https://danchilton.com/styleguide/screens/quotes-list - reading · **Today** — https://danchilton.com/styleguide/screens/reading-today #### Settings Six independent implementations of the same account + preferences surface. No shared shell. - quotes · **Settings** — https://danchilton.com/styleguide/screens/quotes-settings - reading · **Settings** — https://danchilton.com/styleguide/screens/reading-settings - stats · **Settings** — https://danchilton.com/styleguide/screens/stats-settings #### Empty states Bookmarks and reading share a shape; stats and cars do not. - bookmarks · **Empty state** — https://danchilton.com/styleguide/screens/bookmarks-empty #### Stat and metric displays Four takes on "show a number and its context" — the Stats app, reading, quotes, and the cars fuel grid. - stats · **Activity detail** — https://danchilton.com/styleguide/screens/stats-activity - reading · **Stats** — https://danchilton.com/styleguide/screens/reading-stats - quotes · **Stats** — https://danchilton.com/styleguide/screens/quotes-stats - cars · **Fuel** — https://danchilton.com/styleguide/screens/cars-fuel #### Densest screens Where the system is under the most pressure, and the only place the categorical superset palette appears. - exercise · **Live session** — https://danchilton.com/styleguide/screens/exercise-session - exercise · **Session summary** — https://danchilton.com/styleguide/screens/exercise-summary - cars · **Overview** — https://danchilton.com/styleguide/screens/cars-overview #### Shell extremes AppShell with everything on, versus search/add/settings all off, versus the one app not on AppShell at all. - exercise · **Exercise library** — https://danchilton.com/styleguide/screens/exercise-library - games · **Picker** — https://danchilton.com/styleguide/screens/games-list - hub · **Dashboard** — https://danchilton.com/styleguide/screens/hub-dashboard ## Cohesion audit 2 of 10 patterns are drifting, as of 2026-08-24. The 8 that are aligned: ListRow, DetailPanel, Overlay, Settings, AppShell, PageHeader, StatCard, Colour tokens. The headline: cohesion is **not** a token problem — it is that the same handful of compositions were built independently in each app. Anything generated against this system should route through the shared component rather than adding a tenth implementation. | Pattern | Status | Shared component | On the shared component | Own implementation | | --- | --- | --- | --- | --- | | ListRow | holding | `shared/ListRow.svelte` | bookmarks, cars, dexa, exercise, quotes, reading | — | | DetailPanel | holding | `shared/OverlayDetail.svelte` | bookmarks, quotes, reading | — | | Overlay | holding | `shared/Overlay.svelte` | bookmarks, exercise, quotes, reading, stats | — | | Sidebar wrapper | drifting | `shared/SidebarItem.svelte` | every app uses SidebarItem | BookmarksSidebar, CarsSidebar, ExerciseSidebar, QuotesSidebar, ReadingSidebar, StatsSidebar | | Settings | holding | `shared/settings/* · shared/lib/settings-state.svelte.ts` | bookmarks, exercise, hub, quotes, reading, stats | — | | AppShell | holding | `shared/AppShell.svelte` | bookmarks, cars, dexa, exercise, games, quotes, reading, stats | hub | | Primitive adoption | drifting | `ui/*` | exercise, reading, bookmarks, stats, quotes, cars, hub, dexa (via shared composites) | games — zero, by design | | PageHeader | holding | `shared/PageHeader.svelte` | all nine apps | — | | StatCard | holding | `shared/StatCard.svelte` | cars, dexa, exercise, quotes, reading, stats | — | | Colour tokens | holding | `shared/styles/theme.css` | all nine apps | — | ### Notes - **ListRow** (holding) — Closed 2026-08-25. All six per-app row/card components are deleted; one slot-based row covers both layouts. It drifted because the row knew about domain concepts — ontagClick, ontoggleArchive, ontoggleFavorite — so every app needed its own. Everything domain-specific now arrives through leading/meta/trailing: a domain word in the signature means the abstraction has slipped again. - **DetailPanel** (holding) — Closed 2026-08-25. DetailPanel is deleted; the record inspector is now an Overlay preset and, more importantly, is non-modal — opening a record no longer locks the list behind it. Cars and dexa still show records in the main pane, which is a deliberate layout choice rather than drift. - **Overlay** (holding) — Closed 2026-08-25. Modal, SideDrawer and DetailPanel are deleted; one component takes presentation (center/side/bottom) and modal as separate props. An ESLint rule now blocks importing ui/dialog or ui/sheet outside shared/components, which is how the third mechanism appeared in the first place. - **Sidebar wrapper** (drifting) — The leaf is shared, the container is not. Six wrappers repeat the same section-header + item-list shape. - **Settings** (holding) — Closed 2026-08-25. Five apps had five answers to "when does a setting save?"; that is now decided once — selects and switches commit on change, text defers to a sticky bar. Account-scope settings (theme, passkeys, sign-out) moved to the hub, which had no settings screen at all. The original entry listed cars as a stray, which was wrong: cars has no settings screen. - **AppShell** (holding) — Eight of nine. Hub is the login gateway and legitimately different — it has no sidebar and no app-switcher of its own. - **Primitive adoption** (drifting) — Partly closed 2026-08-25. Dexa now inherits primitives through ListRow, StatCard and PageHeader rather than importing any directly, so it picks up their focus and sizing behaviour; it still uses no ui/* import of its own. Games is the sanctioned exception. - **PageHeader** (holding) — Opened and closed 2026-08-25. Five screens had no page title at all, and three apps put the active filter in the h1 — so the page renamed itself as you browsed and nothing named the view. Every screen now renders exactly one h1 naming the view, with the filter as a scope chip. Named exceptions: the exercise live session, the hub login, and the games themselves. - **StatCard** (holding) — Opened and closed 2026-08-25. The same idea shipped four ways, and cars showed "39.9" under nothing but "GALLONS". One container now, and every value carries a caption saying what it means. tone has no destructive: red is for irreversible actions, never a number. - **Colour tokens** (holding) — Zero raw Tailwind ramps anywhere in the repo. This layer is not the problem — worth saying plainly, because it is the layer people assume is at fault.