# Aether UI — Design System Style Guidelines
> **Version:** 1.2 (2026-03-20)
> **Last Updated:** 2026-03-20
> **Author:** One Sky IT / Scott Idem
> **Scope:** All Aether SvelteKit frontend components
> **Related:** `ae-firefly.css`, `documentation/MODULE__AE_Journals.md`, `documentation/MODULE__AE_Events_Presentation_Management.md`
> **Historical implementation log:** `documentation/archive/PROJECT__AE_Style_Review_2026-03.md`

---

## 1. Design Philosophy

**"Shiny serenity, like a firefly."**

The Aether UI is calm, focused, and softly luminous. It must be immediately readable under conference-room lighting, at a glance, by presenters who are nervous and in a hurry. Staff in the Speaker Ready Room need scan-speed identity confirmation. Remote presenters uploading files from home need a clear, unambiguous interface that doesn't waste their time.

Core principles:
- **Identity first.** The user's first question is always *"Am I in the right place?"* Answer it with the hero card — name, time, room — before anything else is shown.
- **Progressive disclosure.** Admin fields (codes, IDs, passcodes) are hidden unless `edit_mode` is active.
- **Theme-aware always.** Zero hardcoded colors. Every background, border, and text color must respond to light/dark mode and the active theme via CSS variables.
- **Transitions, not pops.** Every interactive state change is smoothed with `transition-colors duration-200`.
- **Section 508 / WCAG 2.1 AA** compliance is non-negotiable. Contrast ratios, focus indicators, ARIA labels, and screen-reader regions are required everywhere.

---

## 2. Technical Stack Mandates (2026 Standard)

To maintain codebase health and performance, all new development must adhere to the following stack:

### 🚀 Svelte 5 Runes
- **Mandatory**: Use `$state`, `$derived`, and `$effect`.
- **Snippet pattern**: Use `{@render snippet()}` for reusable UI blocks within components.
- **Avoid**: Legacy `export let` (use `$props()`), `onMount` for reactive derived state (use `$derived` or `$effect`), and `$$slots` (use Snippets).

### 🎨 Tailwind 4 + Skeleton v4
- **Mandatory**: Use `preset-*` classes for interactive elements (e.g., `preset-tonal-primary`).
- **Forbidden**: Legacy Skeleton v3 `variant-*` classes.
- **Customization**: Use Tailwind 4 `@theme` blocks for project-wide overrides.
- **URLs**: Skeleton for Svelte for LLMs docs: https://www.skeleton.dev/llms-svelte.txt

### 🔣 Lucide Icons
- **Mandatory**: Use `@lucide/svelte` components (e.g., `<Calendar size="1em" />`).
- **Migration**: Replaced all FontAwesome `fas fa-*` icons in general modules.
- **🚨 Exception: IDAA Module**: The IDAA module **must** retain FontAwesome and Bootstrap classes. It integrates with Novi CMS which relies on these legacy standards. **Do not migrate IDAA icons.**

---

## 3. The AE_Firefly Theme

**App default since 2026-03-06.** Set in `ae_stores.ts` as `theme_name = 'AE_Firefly'`.
File: `src/ae-firefly.css` | Activated by: `data-theme="AE_Firefly"`

| Role | Palette Name | Hue | Use Case |
|---|---|---|---|
| **Primary** | Luminescent Teal | ~184° | Primary actions, date/time chips, focus rings, anchor links |
| **Secondary** | Warm Amber-Gold | ~90° | Secondary actions, copy/email buttons, soft highlights |
| **Tertiary** | Night-Sky Indigo | ~277° | Location/room chips, depth accents |
| **Surface** | Moonlit Slate | ~215–233° | All backgrounds — off-white (light) → midnight slate (dark) |
| **Warning** | Amber semantic | n/a | Disabled/inactive records, "no results" states, code badges |
| **Success** | Green semantic | n/a | Active/complete states, file count badges |
| **Error** | Red semantic | n/a | Errors, disabled-presenter states |

### Contrast Guarantees
- Body text (`surface-950` on `surface-50`): > 15:1 in light mode ✓
- Primary buttons: ≥ 3:1 for interactive component threshold ✓
- Designed using OKLCH perceptual lightness — not HSL estimates

---

## 4. Color Token Rules

### ✅ Always Use Theme Tokens

**Backgrounds / surfaces:**
```
bg-surface-50-900      ← card faces (light: near-white, dark: deep slate)
bg-surface-100-900     ← inner sections, code blocks, secondary panels
bg-surface-50-950      ← page-level containers
bg-surface-200-800     ← skeleton pulse placeholders, dividers
```

**Borders:**
```
border-surface-200-800  ← standard card/panel borders
border-primary-500      ← focus rings (via focus-visible:ring)
border-warning-500      ← warning/caution interactive zones
border-surface-500/20   ← subtle section dividers
```

**Primary color accents (teal):**
```
bg-primary-500/10       ← tinted chip background (time chips)
text-primary-700 dark:text-primary-300  ← chip text with auto dark mode
hover:text-primary-500  ← link hover color
```

**Tertiary color accents (indigo):**
```
bg-tertiary-500/10      ← tinted chip background (room/location chips)
text-tertiary-700 dark:text-tertiary-300
```

**Skeleton presets:**
```
preset-tonal-warning    ← "no results", disabled rows, code tag badges
preset-tonal-primary    ← primary action buttons
preset-tonal-surface    ← neutral/secondary actions, status badges
preset-tonal-success    ← success states, file count badges
preset-tonal-error      ← error/blocked states
preset-filled-*-500     ← solid-fill buttons (hover state for tonal)
```

**Warning/error semantic backgrounds:**
```
bg-warning-100 border border-warning-300  ← inline warning banners
bg-error-100 border border-error-300      ← inline error banners
```

---

### ❌ Never Use These

| Forbidden | Reason | Replace With |
|---|---|---|
| `bg-gray-*` | Fixed color, breaks dark mode | `bg-surface-*` tokens |
| `bg-neutral-*` | Same — fixed hue | `bg-surface-*` tokens |
| `bg-white` | Light-mode only | `bg-surface-50-900` |
| `text-gray-*` | Breaks dark mode | `opacity-60` on inherited text |
| `text-neutral-*` | Same | `opacity-*` |
| `border-gray-*` | Non-theme border | `border-surface-200-800` |
| `bg-yellow-*`, `text-yellow-*` | Bypasses warning semantic | `preset-tonal-warning` |
| `bg-red-*`, `text-red-*` | Bypasses error semantic | `bg-error-100` / `preset-tonal-error` |
| `bg-white dark:bg-gray-800` | Manual light/dark pair | Let theme tokens handle it — remove entirely |
| `text-gray-600 dark:text-gray-400` | Manual light/dark pair | `opacity-60` |
| `rounded-container-token` | Skeleton v3 class | `rounded-xl` |
| `variant-*` (v3) | Skeleton v3 class | `preset-*` (v4) |
| `preset-filled-surface-300-700` | v3 dual-shade notation | `bg-surface-200-800` or `bg-surface-100-900` |
| `preset-filled-surface-400-600` | v3 dual-shade notation | `bg-surface-100-900` |
| `overflow-x-scroll` | Forces scrollbar visible | `overflow-x-auto` |

---

## 5. Transitions & Animation

All interactive state changes must be smoothed — no hard pops.

| Element | Classes |
|---|---|
| Table row | `transition-colors duration-200` on `<tr>` |
| List item card | `transition-colors duration-200` on `<li>` |
| Link hover | `transition-colors duration-200` on `<a>` |
| Info chips | `transition-colors duration-200` on `<span>` |
| QR code toggle (size) | `transition-all duration-500` on `<img>` |
| Collapsible sections | Use Skeleton `Accordion` or CSS `transition-all` — don't add custom unless needed |
| Buttons | Handled automatically by Skeleton preset classes |

---

## 6. Loading / Skeleton States

When `liveQuery` data is still resolving, show pulse placeholders instead of nothing:

```svelte
<!-- Title placeholder -->
<div class="h-7 w-2/3 bg-surface-200-800 animate-pulse rounded"></div>

<!-- Chip/pill placeholder -->
<div class="h-5 w-1/2 bg-surface-200-800 animate-pulse rounded-full"></div>

<!-- Icon placeholder -->
<div class="h-5 w-5 bg-surface-200-800 animate-pulse rounded-full"></div>
```

Always wrap in `{#if $lq__obj}{...}{:else}...skeleton...{/if}` — **never** show real content structure before data exists.

---

## 7. Dark Mode Rules

- **Never write `dark:` overrides for background or text colors.** The Firefly theme handles both modes through CSS variables. Writing `dark:bg-gray-800` or `dark:text-gray-400` bypasses the theme and breaks if the user switches themes.
- **Exception allowed:** `dark:text-primary-300` and `dark:text-tertiary-300` in info chips are intentional — they reference theme variables that gracefully degrade.
- **Exception allowed:** `dark:border-surface-700` in fine-grained border work when `border-surface-200-800` isn't strong enough.

---

## 8. Button UX Standard

**Default rule: every button includes a Lucide icon AND a text label.**

The icon gives instant visual recognition; the label removes ambiguity. This matters especially for non-English-native users and anyone scanning a UI quickly under conference-room pressure.

| Context | Acceptable pattern |
|---|---|
| Standard action buttons | Icon (left, `size={14-16}`) + text label — **preferred** |
| Space-constrained toolbars / icon rows | Icon-only — requires `title="..."` and `aria-label="..."` |
| Extremely tight inline cells | Text-only — acceptable only when no icon fits |
| Destructive / irreversible actions | Always icon + text, never icon-only |

```svelte
<!-- ✅ Standard -->
<button class="btn preset-tonal-primary">
    <SquarePen size={14} class="mr-1" /> Edit
</button>

<!-- ✅ Icon-only (toolbar) -->
<button class="btn-icon preset-tonal-surface" title="Refresh results" aria-label="Refresh results">
    <RefreshCw size={14} />
</button>

<!-- ❌ Text-only — add an icon -->
<button class="btn preset-tonal-primary">Edit</button>
```

---

## 9. Accessibility (Section 508 / WCAG 2.1 AA)

| Requirement | Implementation |
|---|---|
| Decorative icons | `aria-hidden="true"` on all icons |
| Icon-only buttons | `aria-label="..."` or `title="..."` + visible context |
| Async content regions | `role="status" aria-live="polite"` on loading/empty sections |
| Focus indicators | `focus-visible:ring-2 focus-visible:ring-primary-500` on custom interactive elements |
| Interactive dialogs | `aria-haspopup="dialog"` on trigger buttons |
| Form inputs | Visible `<label>` linked via `for` / `id`, or explicit `aria-label` |
| Color-only information | Always pair color coding with icon or text — never color alone |
| Minimum touch target | 44×44px effective hit area for all tap targets |

---

## 9. Debug Code — Remove Before Committing

These patterns are breakpoint debuggers added during development. **Never commit them:**

```html
<!-- Breakpoint border debugger — REMOVE before commit -->
sm:border-l-red-400 md:border-l-yellow-400 lg:border-l-gray-100
sm:dark:border-l-red-600 md:dark:border-l-yellow-600 lg:dark:border-l-gray-700
```

Also flag on code review:
- `console.log(...)` that isn't behind a `log_lvl` guard
- `border-dashed border-y-transparent border-r-transparent` left on production components
- `overflow-x-scroll` (should be `overflow-x-auto`)

---

## 10. QR Code Pattern — Critical Bug Prevention

The async QR generation code uses a boolean `true` as a loading placeholder:

```typescript
$events_sess.pres_mgmt.session_qr_url[$lq__obj.id] = true;   // ← loading
// ... async ...
$events_sess.pres_mgmt.session_qr_url[$lq__obj.id] = result; // ← URL string
```

**Always gate display on `typeof ... === 'string'`**, not just truthy:

```svelte
<!-- ✅ Correct -->
{#if typeof $events_sess.pres_mgmt.session_qr_url?.[$lq__obj.id] === 'string'}

<!-- ❌ Wrong — renders broken <img src="true"> during load -->
{#if $events_sess.pres_mgmt.session_qr_url[$lq__obj.id]}
```

---

## 11. Tailwind Utility Usage Notes

- **`opacity-*` for muted text**: Use `opacity-60` (secondary) or `opacity-40` (tertiary/hint) instead of `text-gray-*`. This works in any theme and both modes.
- **`/` opacity modifier**: `bg-primary-500/10` is preferred over separate `opacity-10` — it targets only the background.
- **`text-sm leading-relaxed`**: Standard for body-level descriptive text in cards.
- **`tracking-wide uppercase`**: Use for section label/eyebrow text with `opacity-40`.
- **`whitespace-pre-wrap`**: Required for any `<pre>` or `<p>` displaying user-entered multi-line text (preserves breaks without horizontal overflow).

---

## 12. Flowbite Svelte Modal — Standard Usage

**LLM docs:** [flowbite-svelte.com/llm/components/modal.md](https://flowbite-svelte.com/llm/components/modal.md)

All consent/agreement modals in pres_mgmt use this pattern. Do not deviate.

```svelte
<Modal
    title="Person Name — Agreement Title"
    bind:open={$store.show_modal__thing}
    autoclose={false}
    placement="top-center"
    size="lg">

    <YourFormComponent {prop} />

    {#snippet footer()}
        <div class="w-full text-center">
            <button
                type="button"
                onclick={() => ($store.show_modal__thing = false)}
                class="btn preset-tonal-warning border border-warning-500">
                <X size="1em" class="mr-1" />
                Close
            </button>
        </div>
    {/snippet}
</Modal>
```

**Rules:**
- `classes` prop, not deprecated `bodyClass`/`headerClass`/`footerClass`
- **`classes={{ footer: '...' }}` does NOT override footer padding** in the current Flowbite Svelte version — the default `p-4 md:p-5` applies regardless
- `autoclose={false}` — prevents backdrop click from dismissing the dialog
- `placement="top-center"` — keeps long forms near the top so content is immediately visible
- Scrolling inside a long body is **automatic** — never add `max-h` or `overflow` to the body
- The modal title should include the person's name so the inner component doesn't need a redundant `<h2>`

---

## 13. Known Issues & Workarounds

### `btn` + `preset-filled-*` resolves to transparent inside `card` components

**Symptom:** A button using `btn preset-filled-primary` (or any `preset-filled-*`) inside a `card` div renders with `background-color: transparent`, making it invisible against the card surface.

**Root cause:** The Skeleton v4 `btn` class sets a transparent background via a CSS variable chain. When nested inside a `card` element, the `preset-filled-*` class fails to win the specificity battle and the button appears invisible. This affects both light and dark mode.

**Workaround:** Skip `btn` and `preset-filled-*` entirely for buttons inside `card` elements. Use direct Tailwind token classes instead:

```svelte
<!-- ✅ Correct — works reliably inside cards -->
<button class="w-full rounded-xl py-5 font-bold flex items-center justify-center gap-2
               bg-primary-500 text-white hover:brightness-110 transition-all cursor-pointer">
    ...
</button>

<!-- Secondary / cancel button inside a card -->
<button class="w-full rounded-lg py-3 text-sm font-medium flex items-center justify-center gap-2
               border border-surface-500/40 hover:bg-surface-200-800 transition-colors cursor-pointer opacity-70">
    ...
</button>

<!-- ❌ Broken inside card — do not use -->
<button class="btn btn-xl preset-filled-primary">...</button>
```

**Scope:** `btn` + `preset-*` classes work correctly on standalone buttons (e.g. page headers, nav bars). The issue is specific to the `card` component context. If we migrate away from Skeleton `card`/`btn`, this issue goes away.
