# Theme design brief — two flagship themes

**Audience:** an AI model or designer producing two production themes for Quaeriva 0.9.0-rc1.
**Deliverable:** two complete theme directories (manifest + CSS + view overrides) **plus** a matching importable profile JSON for each.
**Non-negotiable:** every rule below is derived from the running code. A deliverable that violates the token contract is rejected by validation at import time, and a theme whose manifest is wrong silently falls back to core views. Neither failure is recoverable by "mostly working".

---

## 0. Scope — this is a layout job, not a palette job

A recoloured default theme is **not** an acceptable deliverable. Each theme must restructure the actual pages:

| Page | View name | Must be visibly restructured |
|---|---|---|
| Home feed | `home` | yes |
| Question detail | `question` | yes |
| Discussion listing | `topics` | yes |
| Discussion detail | `topic` | yes |
| Category / tag | `taxonomy` | yes |
| Search results | `search` | recommended |
| Page shell | `layouts/app` | only if the header/footer structure demands it |

Someone should be able to identify which theme is active from a screenshot of the home page **with the colours removed**.

### Two delivery layers

| Layer | What it is | How it ships | Managed at |
|---|---|---|---|
| **Theme directory** | `theme.json`, CSS, PHP view overrides | Filesystem only — copied under `public/themes/` | Selected in Appearance |
| **Theme profile** | Design tokens as JSON | A single `.json` file | Appearance → Profiles: import, preview, apply, roll back |

Both are required per theme. The directory supplies the structure; the profile supplies the palette and presets so an operator can preview and roll back safely.

**There is no browser upload of theme PHP, ever.** The directory is delivered as files.

---

## 1. Theme resolution — read this before writing a single view

```
public/themes/{theme}/views/{name}.php   ← used when present
app/views/{name}.php                     ← fallback
```

The resolver applies four gates in order. Failing any of them means your override is **silently ignored** and the core view renders instead:

1. **The manifest must load.** No `theme.json`, no overrides.
2. **The manifest's `directions` must contain the current direction.** A theme declaring only `["ltr"]` uses **core views for every Arabic page**. Declare `["rtl","ltr"]` and mean it.
3. **The template name must match `^[A-Za-z0-9_/-]+$`.** Backslashes and `..` are stripped; a name that does not match resolves to `null`.
4. **The file must exist** at `public/themes/{theme}/views/{name}.php`.

Because of gate 2, an untested RTL theme does not look broken — it looks like the default theme. Test in Arabic explicitly.

Assets are declared in the manifest and may be direction-specific. Use one stylesheet with logical properties instead; a mirrored `rtl.css` is a maintenance trap this application deliberately avoids.

---

## 2. View data contracts

These are the variables each view actually receives. Do **not** assume anything else exists — a core change will not warn you, your page will simply fatal or render blank.

### `home`

| Variable | Type | Contents |
|---|---|---|
| `communityFeed` | list | Unified feed items, newest activity first |
| `feedFilter` | string | `all`, `questions`, `discussions`, `unanswered` |
| `widgetsHtml` | string | Pre-rendered `homepage_main` widget region |
| `renderSection` | callable | `renderSection('home_sidebar')` returns a region's HTML |
| `currentUser` | array/null | The signed-in member |
| `searchQuery` | string | Legacy home search term |
| `submittedQuestionId` | int | Non-zero right after a submission |
| `dbAvailable` | bool | False when the feed query failed and the page degraded |
| `translator`, `localeContext` | object | Translation and direction |

**Feed item keys** — both types share the envelope, so one card component can render both:

| Key | Notes |
|---|---|
| `type` | `question` or `topic` — the only reliable discriminator |
| `id`, `slug`, `title`, `body` | Body is Markdown source; excerpt it, never render it raw |
| `activity_at` | Sort key already applied |
| `created_at`, `updated_at` | |
| `category_name`, `category_slug` | Empty string when uncategorised |
| `author_name`, `author_username`, `author_avatar_url`, `author_user_id` | **Empty for anonymous questions** — handle the empty case |
| `tags` | List of `{name, slug}` |
| `views_count` | |
| **question only** | `votes_count`, `answers_count`, `best_answer_id` |
| **topic only** | `kind` (`discussion`/`announcement`), `is_pinned`, `is_locked`, `comments_count` |

### `question`

| Variable | Contents |
|---|---|
| `question` | The question row |
| `answers` | Answers on the current page |
| `answersMeta` | `page`, `per_page`, `total`, `total_pages` |
| `questionTags` | List of tags |
| `comments` | `['question' => [...], 'answers' => [answerId => [...]]]` |
| `isFollowing` | bool |
| `reportedItems` | `['question' => [...], 'answer' => [...], 'comment' => [...]]` — already-reported ids |
| `slug`, `currentUser`, `csrfToken` | |
| `expandQuestionComments` | bool — open the comment thread on load |
| `notice` | Translated status message, or empty |
| `renderSection` | For `question_sidebar` |

### `topics`

`topics`, `topicsMeta` (pagination), `categories`, `languages`, `filters`, `mode`, `error`, `values`, `currentUser`, `csrfToken`, `currentPath`, `site`.

### `topic`

`topic`, `comments`, `reportedItems`, `isFollowing`, `slug`, `notice`, `query`, `postingRules`, `currentUser`, `csrfToken`, `currentPath`, `site`.

### `taxonomy`

`type` (`category`/`tag`), `taxonomy`, `questions`, `topics`, `isFollowing`, `currentUser`, `csrfToken`, `currentPath`, `site`.

### `search`

`results`, `options`, `dbAvailable`.

**Rule for every view:** read the core view in `app/views/` before overriding it. Match the variables it uses, escape every value, and build every link with the URL helpers.

---

## 3. Rules that apply to all view overrides

- **Escape everything.** An override is application code with the same obligations as core.
- **Never hard-code `/admin`, a domain, or a leading `/`.** Use the URL helpers; internal paths are written without a leading slash (`'c/' . $slug`).
- **Never render `body` as HTML.** It is Markdown source. Excerpt it as text in cards.
- **Handle the anonymous author case.** `author_name` is an empty string, not null, and not "Anonymous".
- **Handle the empty state.** An empty feed, a question with no answers, a locked topic, a failed query (`dbAvailable === false`) must each render something deliberate.
- **Keep the widget regions.** Dropping `widgetsHtml` or `renderSection` silently disables features the operator configured.
- **Keep pagination.** Removing the answer or comment pager breaks canonical self-reference and buries content.
- **Logical properties only.** `margin-inline-start`, `padding-block`, `inset-inline-end`. No `left`/`right`.

---

## 4. The token contract

The profile JSON must match this exactly. This is a real export from the running system:

```json
{
  "format": "quaeriva-theme-profile",
  "schema": 1,
  "name": "Theme name",
  "description": "One sentence: the look, and who it suits.",
  "tokens": {
    "colors": {}, "typography": {}, "shape": {},
    "shadow": {}, "components": {}, "pages": {}, "layout": {}
  }
}
```

`format` exactly `quaeriva-theme-profile`; `schema` exactly `1`; `name` ≤ 120 chars; `description` ≤ 500 chars; file under **512 KB**. Unknown keys are dropped, invalid values rejected.

### `colors` — 13 keys, `#RRGGBB` only

`bg`, `surface`, `surface_soft`, `text`, `muted`, `border`, `primary`, `primary_hover`, `secondary`, `accent`, `success`, `warning`, `danger`

No `rgba()`, no `hsl()`, no three-digit hex, no named colours.

### `typography`

| Key | Allowed |
|---|---|
| `font_body`, `font_heading`, `font_arabic` | **only** `system`, `inter`, `cairo`, `noto_kufi` |
| `body_size`, `body_line_height`, `h1_size`, `h2_size`, `h3_size` | safe dimensions |
| `h1_weight`, `h2_weight`, `h3_weight` | integer 400–900 |

There is no webfont URL field. Fonts are local, and the public shell must not fetch a font CDN at runtime. **Requesting a Google Font is an automatic rejection.**

### `shape` / `shadow` / `components` / `pages`

`radius_sm|md|lg|xl` · `sm|md|lg` · `button_radius`, `button_primary_bg`, `button_primary_text`, `card_bg`, `card_border`, `card_radius`, `card_shadow`, `table_header_bg`, `table_border`, `header_bg`, `header_border`, `sidebar_bg`, `sidebar_border` · `home_feed_gap`, `question_max_width`, `topic_max_width`, `article_max_width`

Component tokens may reference others: `"card_bg": "var(--color-surface)"`. Prefer references so recolouring the palette recolours the components.

### `layout` — allow-listed strings

| Key | Values |
|---|---|
| `home_preset` | `community`, `qa_focus`, `compact` |
| `question_preset` | `classic`, `wide`, `sidebar` |
| `topic_preset` | `discussion`, `compact` |
| `header_preset` | `standard`, `compact` |
| `density` | `compact`, `comfortable`, `spacious` |
| `sidebar_position` | `start`, `end` |

`sidebar_position: start` is left in LTR and **right in RTL**. That is the point of logical values.

### The hard gate: contrast

Import is refused unless **both** reach **4.5:1**:

- `text` on `surface`
- `text` on `bg`

Validation, not advice. Choose `text` and the two backgrounds first, verify the ratio, then build the accent palette. The other order wastes an iteration.

Also check (not validated, still required by this brief): `muted` on `surface`, and `button_primary_text` on `button_primary_bg`.

### Where tokens land

Tokens compile into `<style data-theme-tokens>` as `:root` custom properties. Layout presets become attributes on `<body>`:

```html
<body class="page-home theme-mytheme"
      data-home-layout="qa_focus" data-question-layout="classic"
      data-topic-layout="discussion" data-header-layout="standard"
      data-density="compact" data-sidebar-position="end">
```

Your CSS may select on these to vary within your own theme:

```css
body[data-density="compact"] .card { padding-block: .75rem; }
body[data-sidebar-position="start"] .grid { … }
```

---

## 5. The two themes

Produce two genuinely different products. Each emulates a **familiar interaction pattern** — the archetype, not a specific company.

> **Hard boundary:** emulate the *pattern* (dense reputation-first Q&A; flowing discussion thread). Never copy a real product's name, logo, wordmark, illustration set, or distinctive brand dress, and never imply affiliation. The archetype is a UX convention; the branding is somebody's property.

---

### Theme A — "Dense knowledge base" archetype

**Pattern:** the reputation-driven technical Q&A layout. Scanning density over comfort. A visitor arrives from search, evaluates in three seconds, reads one answer, leaves.

**Tokens:** `density: compact`, `home_preset: qa_focus`, `question_preset: classic`, `header_preset: compact`, tight radii, borders instead of shadows, near-neutral palette with one confident accent.

**`home` — the scan list**
- Rows, not cards. Full-width dividers, no per-item shadow.
- **Left metric rail** per row: votes, answers, views as stacked numbers with tiny labels. The answers count is the visual anchor — colour it when an answer is accepted, outline it when the count is zero.
- Title one line, ellipsised. Excerpt at most one line, or none.
- Tag chips small, monochrome, directly under the title.
- Author line minimal: avatar + name + relative time, right-aligned in LTR (`inset-inline-end`).
- Filters as a compact segmented control, not pills.
- Sidebar: unanswered, popular tags, community stats. Utility, not decoration.

**`question` — answers dominate**
- A vote column running down the inline-start edge of the question and each answer.
- Accepted answer marked with a check in the vote column and a subtle surface tint — **not** moved out of reading order.
- Answer count as a real section heading: "12 Answers", with a sort control.
- Comments visually subordinate: smaller type, indented, collapsed behind a "show N comments" control.
- Question metadata bar under the body: asked / modified / viewed.
- Pager at the bottom of the answer list, always visible.

**`topics` / `topic` — secondary by design**
- Listing: one line per topic, reply count and last-activity time. No excerpts.
- Detail: linear thread, compact avatars, tight spacing. Pinned and locked as small badges with icon plus text.

**`taxonomy`**
- Description block, follow button, then the same scan list. Questions and topics in one list with a type marker, not two separate sections.

---

### Theme B — "Editorial community" archetype

**Pattern:** the modern discussion-forum layout. Comfortable, conversational, built for browsing and returning rather than search-and-leave.

**Tokens:** `density: spacious`, `home_preset: community`, `question_preset: wide`, `topic_preset: discussion`, larger `home_feed_gap` and `article_max_width`, soft shadow, larger radii, warm palette with a real secondary.

**`home` — the activity stream**
- Generous cards with clear separation and a soft shadow.
- **Avatar-first**: a large author avatar on the inline-start edge sets the conversational tone.
- Title at a comfortable size, then a two-line excerpt.
- **Participant stack**: up to five small overlapping avatars of recent participants, then "+N".
- Activity emphasis: "last reply 2 hours ago" is the primary metadata, not the creation date.
- Category shown as a coloured label, not a plain link.
- Filters as full pills with counts.
- Sidebar: latest discussions, active members, guidelines block.

**`question` — readable article treatment**
- Wide, centred measure. Vote controls horizontal under the body, not a vertical rail.
- Accepted answer in a distinctly tinted panel with a labelled header — the label carries the meaning, the tint only reinforces it.
- Answers separated by generous whitespace with a large author block per answer.
- Comments inline and always visible, indented, in a softer surface.

**`topics` / `topic` — the centrepiece**
- Listing: rich rows with excerpt, participant stack, reply count, last-activity time, and a category label.
- Detail: a real thread. Author block per comment, permalink anchors, clear visual separation between the opening post and the replies.
- Locked state explains itself in place of the reply box.
- Pinned and announcement states get a full banner treatment, not a chip.

**`taxonomy`**
- Category hero: name, description, follow button, counts. Then the card stream.

**Arabic here is a first-class test, not an afterthought.** A spacious editorial layout exposes bad Arabic line height and heading weight immediately. Check `font_arabic`, `body_line_height`, and heading weights against real Arabic text before shipping.

---

### Naming

Short, distinctive, pronounceable. One or two words.

- No "Pro", "Ultimate", "Premium", "2.0", or a version number.
- Must not imply a real company, product or trademark.
- Must read well beside an Arabic interface.
- The `name` goes in the profile JSON and the manifest and must stay stable across exports — the operator identifies the profile by it.

Propose three candidates per theme with one line of reasoning, then commit to one.

---

## 6. SEO, performance, accessibility

Metadata, canonicals, structured data and sitemaps are produced by the server. A theme cannot improve them and must not try. What a theme genuinely controls:

**Core Web Vitals**
- **LCP** — the largest above-the-fold element is the first feed row or the question title. Introduce no hero image and no full-bleed background image.
- **CLS** — reserve height for every avatar, image, ad slot and widget region. A feed that jumps when ads fill is a theme defect.
- **INP** — no scroll-driven JavaScript, no layout-thrashing animation. Presets and states are CSS.

**Rendering**
- All content present in server-rendered HTML. Nothing important may require JavaScript to appear.
- Zero external runtime requests: no font CDN, no icon CDN, no analytics. The strict CSP option must stay usable.

**Semantics and accessibility**
- Exactly one `<h1>` per page; headings descend without skipping.
- The accepted answer is distinct **without** leaving reading order.
- Visible focus on every interactive element. Never `outline: none` without a replacement.
- Touch targets ≥ 44 px.
- Colour is never the only signal — accepted, pinned and locked each need an icon or a label.
- Metric rails and participant stacks need accessible text, not bare numbers.

**Mobile**
- No horizontal overflow at 360 px on any page.
- Editor toolbar usable at 360 px.
- Tables and code blocks scroll inside their own container; the body never scrolls sideways.
- Theme A's metric rail must collapse to a horizontal row on narrow screens, not shrink to unreadable.

---

## 7. Deliverables

Per theme:

```
public/themes/{slug}/
├── theme.json
├── assets/css/theme.css
└── views/
    ├── home.php
    ├── question.php
    ├── topics.php
    ├── topic.php
    ├── taxonomy.php
    └── search.php        (optional)
{slug}.json               ← the importable theme profile
```

Plus, in writing:

1. **A contrast table** with measured ratios for `text`/`surface`, `text`/`bg`, `muted`/`surface`, `button_primary_text`/`button_primary_bg`. State the numbers; do not assert "accessible".
2. **A rationale of at most 200 words** — who it is for, and the one decision you would defend hardest.
3. **A list of every view you overrode** and, for each, one line on what changed structurally.

`theme.json` must declare `directions: ["rtl","ltr"]` — and that claim must be true. See [Theme manifest](../reference/THEME-MANIFEST.md).

---

## 8. Self-check before delivering

Any "no" means it is not ready.

**Contract**
- [ ] `format` is `quaeriva-theme-profile`, `schema` is `1`
- [ ] 13 colour keys present, every value `#RRGGBB`
- [ ] Fonts only `system`, `inter`, `cairo`, `noto_kufi`
- [ ] Heading weights integers 400–900
- [ ] All six layout keys use allow-listed values
- [ ] Profile file under 512 KB
- [ ] `theme.json` declares both directions

**Contrast**
- [ ] `text` on `surface` ≥ 4.5:1 — number stated
- [ ] `text` on `bg` ≥ 4.5:1 — number stated

**Views**
- [ ] Every override reads only variables the core view receives
- [ ] Every output escaped; every link built with a URL helper
- [ ] `body` never rendered as HTML
- [ ] Anonymous authors (empty `author_name`) handled
- [ ] Empty states handled: empty feed, zero answers, locked topic, `dbAvailable === false`
- [ ] `widgetsHtml` and `renderSection` regions preserved
- [ ] Answer and comment pagination preserved

**Rendering**
- [ ] Tested in Arabic RTL **and** English LTR — Arabic actually uses your views, not the fallback
- [ ] 360 / 768 / 1024 / 1440 px, no horizontal overflow
- [ ] No external font, icon or script URL anywhere
- [ ] No `left`/`right` CSS anywhere
- [ ] One `<h1>` per page; focus states visible

**Identity**
- [ ] The two themes are distinguishable from a screenshot **with colours removed**
- [ ] No real product's name, logo or brand dress reproduced
- [ ] Name is stable, distinctive, free of version noise

---

## 9. How the operator installs it

1. Copy the theme directory to `public/themes/{slug}/`.
2. Select the theme in Appearance.
3. Import `{slug}.json` at `/{admin_path}?page=appearance&tab=profiles`.
4. **Preview** — renders for that admin session only; the public site is untouched.
5. **Apply** — snapshots the current version first, then activates.
6. **Restore** — rolls back to any earlier version.

That safety net exists, which is why a bold theme is a reasonable thing to ship. A timid one wastes it.

## Related

[Appearance tokens](APPEARANCE-TOKENS.md) ·
[Template overrides](TEMPLATE-OVERRIDES.md) ·
[Building a theme](BUILDING-A-THEME.md) ·
[Theme packages](THEME-PACKAGES.md) ·
[Theme manifest](../reference/THEME-MANIFEST.md) ·
[Theme resolution](../07-internals/THEME-RESOLUTION.md) ·
[SEO](../03-configuration/SEO.md)
