---
name: App shell and styling
type: architecture-map-page
slice: 01-frontend
last_verified: 2026/05/09
---

# App shell and styling

> **Where you are:** [Architecture map](../README.md) → [Frontend](README.md) → App shell and styling

**Plain English summary.** Where the visual chrome and CSS plumbing live. `app.html` is the SvelteKit document. `app.css` carries the Tailwind v4 import, the `@theme` token block, the brand `@font-face` declarations, the sensory zones, and the accessibility CSS variables. There is no separate `tailwind.config.{js,ts}`; Tailwind v4 is configured entirely in `app.css`. Fonts ship from `src/fonts/` (bundled by Vite) and `static/fonts/` (served as-is).

## At a glance

- **Path:** `src/app.html`, `src/app.css`, `src/app.d.ts`, `src/fonts/`, `src/assets/`, `static/`.
- **LOC:** 13 (`app.html`) + 583 (`app.css`) + 8 (`app.d.ts`).
- **Frameworks:** Tailwind v4 via `@tailwindcss/vite`. No PostCSS config. No standalone Tailwind config file.
- **Adapter:** `@sveltejs/adapter-static` with `index.html` fallback (SPA, `svelte.config.js`).

## What's in here

### `src/app.html` (13 LOC)

Standard SvelteKit document. Sets `lang="en"`, charset, viewport, favicon, title (`Lumotia`), and applies `data-sveltekit-preload-data="hover"` on `<body>`. The body content is wrapped in `<div style="display: contents">` so the SvelteKit-injected children render inline.

### `src/app.d.ts` (8 LOC)

Ambient declaration extending `Window` with `__TAURI_INTERNALS__` and `isTauri` so `utils/runtime.ts` typechecks.

### `src/app.css` (583 LOC)

Layered:
1. `@import "tailwindcss"` (Tailwind v4 entry).
2. `@font-face` for Lexend, Atkinson Hyperlegible Next, OpenDyslexic, JetBrains Mono, Instrument Serif Italic. URLs use root-absolute paths (`/fonts/...`) served from `static/fonts/` by Vite.
3. `@theme` token block. Sets the design tokens that Tailwind v4 picks up as utility classes:
   - Colour tokens: `--color-bg`, `--color-bg-raised`, `--color-text`, `--color-text-secondary`, `--color-text-tertiary`, `--color-accent`, `--color-warning`, `--color-success`, `--color-border`, `--color-border-subtle`, `--color-hover`, `--color-nav-active`, plus a sensory-zone palette family.
   - Typography tokens: `--font-display`, `--font-body`, `--font-mono`, plus `--font-size-*` and `--line-height-*`.
   - Motion tokens: `--duration-ui`, `--duration-fast`, `--duration-slow`. Easing tokens for `cubic-bezier` curves.
   - Shadow tokens: `--shadow-accent-md`, `--shadow-accent-glow`, etc.
4. Sensory-zone overrides keyed on `[data-zone="..."]` on `<html>`. Switches token values to dim, focus, etc.
5. Theme overrides keyed on `[data-theme="dark"]` and `[data-theme="light"]`. The `preferences` store writes both `data-theme` and `data-zone`.
6. Accessibility variables on `<html>`: `--font-family-body`, `--font-size-body`, `--letter-spacing-body`, `--line-height-body`. Set by the `preferences` store via `applyToDOM()`.
7. Base styles: `body` background, font, body font-family bound to the variable, scrollbar styling, focus ring.
8. Utility classes for grain texture (`.grain` uses the noise asset under `assets/grain.svg`), CRT-style transcript surface, etc.
9. Animations: `@keyframes fade-in`, `@keyframes pulse`, etc. Reduced motion guard at the bottom (`@media (prefers-reduced-motion: reduce)`).

### `src/fonts/` (bundled woff2)

- `atkinson-hyperlegible-next.woff2`
- `instrument-serif-italic.woff2`
- `jetbrains-mono.woff2`
- `lexend-variable.woff2`
- `opendyslexic.woff2`

Bundled by Vite (referenced from `app.css` via `/fonts/...` paths that Vite resolves). The same files live in `static/fonts/` for the static-served path. Confirm whether both paths are required; if Vite handles font copying, `static/fonts/` may be redundant.

### `src/assets/`

- `grain.svg`. Noise texture used by the `.grain` utility class.
- `waveform-mark.svg`. Brand glyph.
- `wordmark.svg`. Brand wordmark.

### `static/`

Served as-is from the webview root.

- `favicon.png`. Site favicon.
- `fonts/`. Same five woff2 files as `src/fonts/`. Likely the source of `app.css /fonts/...` URLs.
- `pcm-processor.js`. AudioWorklet processor (slice 02 owns the integration). 32-line file that converts microphone input to int16 PCM frames and posts them up.
- `textures/grain.png`. PNG version of the grain texture.
- `svelte.svg`, `tauri.svg`, `vite.svg`. SvelteKit defaults; technically unused. Candidate for removal.

## How preferences map to the DOM

| Preference | DOM target |
|---|---|
| `theme` ("light" / "dark" / "system") | `data-theme` on `<html>`. `system` resolves to OS preference at apply time. |
| `zone` ("default" / ...) | `data-zone` on `<html>`. |
| `accessibility.fontFamily` ("lexend" / "atkinson" / "opendyslexic") | `--font-family-body` CSS variable on `<html>`. |
| `accessibility.fontSize` | `--font-size-body` (pixels). |
| `accessibility.letterSpacing` | `--letter-spacing-body` (em). |
| `accessibility.lineHeight` | `--line-height-body` (unitless). |
| `accessibility.bionicReading` | `<html data-bionic-reading="true|false">`. The `bionicReading` action reads this. |
| `accessibility.reduceMotion` | `<html data-reduce-motion="reduce|no-preference|system">`. Pairs with the `prefers-reduced-motion` media query. |

Plus the legacy `settings.fontSize` writes `--font-size-transcript` on `<body>` directly (`+layout.svelte:204`). That is separate from `accessibility.fontSize`.

## Tailwind v4 setup

- Installed via `@tailwindcss/vite` in `vite.config.js:3`.
- Entry: `src/app.css` line 1, `@import "tailwindcss"`.
- Tokens declared inline via `@theme` blocks in `app.css`.
- No `tailwind.config.{js,ts}` exists. Do not look for one.
- Class scanning: Tailwind v4 scans the source tree by default. Custom paths can be configured with `@source` directives if needed.

## Watch outs

- **Two font sources.** `src/fonts/` (Vite bundled) and `static/fonts/` (raw served). Confirm whether both are needed; redundancy bloats the bundle.
- **Mirror file `src/design-system/colors_and_type.css`** must be updated whenever `app.css` `@theme` changes. There is no automated check.
- **`prefers-reduced-motion` honoured by CSS** but JS animations (e.g. `CompletionSparkline`'s stagger) are guarded in component code, not via the media query alone.
- **Tailwind v4 `@theme` is class-scanning sensitive.** If you put utility class strings inside conditional template literals that Tailwind cannot see at scan time, they will not be generated.
- **Removing default SvelteKit assets** (`static/svelte.svg`, `vite.svg`, `tauri.svg`) requires confirming nothing references them in `app.html` or `README.md`.
- **`pcm-processor.js`** is a static asset because AudioWorklet processors must be served from a same-origin URL. Bundling it through Vite would break the worklet registration. Leave it in `static/`.

## See also

- [Design system](design-system.md). The reference mirror of `app.css` tokens.
- [Stores](stores.md). The `preferences` store that writes the DOM.
- [Components](components.md). Where the utility classes are consumed.
