# GMIAU Tools — Standalone Tool Specification
**Version 1.0 · Ryan Bestford**

---

## Overview

This folder holds **single-file HTML tools** that run directly in a browser
with no server. Each tool is self-contained: one `.html` file, plus (for
the offline variant) the same file with all third-party libraries inlined.

_(Historically there was also a toolkit-embedded form of each tool under
`gmiau-toolkit/gmiau-modified/src/renderer/tools/`. The toolkit was retired
2026-04-28 — standalone HTML is now the only delivery format.)_

Reference implementation: **`gmiau-pdf-encrypt.html`** (full UI +
library-loading pattern) and **`gmiau-pdf-compress.html`** (pdf.js +
pdf-lib pipeline with progress bar).

---

## Quick checklist for every standalone tool

- [ ] Self-contained HTML (one file, no sibling assets)
- [ ] Uses the shared CSS variable set — no hardcoded colours
- [ ] Uses the `@@IFYI_THEME@@` sentinel; defines no palette of its own
- [ ] Includes the theme init + `toggleTheme()` block
- [ ] Has its own titlebar (theme button) and footer (copyright)
- [ ] Loads heavy libraries from `cdnjs.cloudflare.com`
- [ ] Uses `'JetBrains Mono', monospace` throughout
- [ ] Log area for user feedback (`addLog('info'|'ok'|'warn'|'err'|'load', ...)`)
- [ ] Files are processed in-browser — **never uploaded anywhere**
- [ ] Header sub-line states "files never leave this tab"
- [ ] If offline variant is needed: paired `build-<tool>-offline.py` script

---

## File naming

| File                              | Purpose                                           |
|-----------------------------------|---------------------------------------------------|
| `<tool-name>.html`                | **Source of truth.** Loads libs from CDN. Edit here. |
| `<tool-name>-offline.html`        | Generated output — libs inlined. **Do not edit by hand.** |
| `build-all-offline.py`            | Single generic builder for every tool in this folder. |

There is **one** builder (`build-all-offline.py`) — it discovers every
`<tool>.html` in the folder and generates the matching
`<tool>-offline.html`. Do not add per-tool builder scripts; the generic
one handles CDN scripts, Google Fonts (WOFF2 inlined as base64), and
pdf.js worker substitution automatically.

---

## CSS variables

The GMIAU Shell ships **two** themes: GMIAU Light and GMIAU Dark. Do not
define a third, and do not hardcode a palette in a tool.

The canonical token values live in `themes/themes/gmiau-{dark,light}.json`,
compiled by `themes/build.py` into `themes/dist/ifyi-theme.css` and inlined
into every tool at the `<!-- @@IFYI_THEME@@ -->` sentinel. A tool must
reference tokens as `var(--token)` and must never restate their values.

Restating them is how the 2026-07 drift happened: tools inlined a copy of
`:root` that matched the theme exactly until the theme moved, and the stale
copies then silently overrode it. If a tool needs a token the theme does not
define, alias it locally onto one that exists (`--ac2: var(--cyan)`) rather
than assigning a literal.

Canonical token names (see the compiled CSS for current values):

`--bg` `--panel` `--surface` `--surface2` `--surface3` `--border` `--border2`
`--text` `--text-muted` `--text-faint` `--muted` `--faint` `--accent`
`--accent2` `--accent-hover` `--accent-dim` `--accent-bg` `--link` `--cyan`
`--green` `--success` `--warn` `--warning` `--red` `--error` `--danger`
`--highlight` `--go-bg` `--go-color`
| `--drop-hover-bg`  | `rgba(139,233,253,0.05)`      | `rgba(164,198,23,0.07)`       |
| `--drop-loaded-bg` | `rgba(189,147,249,0.05)`      | `rgba(23,73,106,0.06)`        |
| `--algo-on-bg`     | `rgba(189,147,249,0.12)`      | `rgba(23,73,106,0.09)`        |
| `--sec-hdr-bg`     | `rgba(68,71,90,0.35)`         | `rgba(199,222,118,0.18)`      |
| `--status-bg`      | `rgba(33,34,44,0.97)`         | `rgba(232,240,224,0.97)`      |
| `--mode-on-bg`     | `rgba(189,147,249,0.14)`      | `rgba(23,73,106,0.10)`        |
| `--mode-on-bd`     | `#bd93f9`                     | `#17496a`                     |
| `--mode-on-col`    | `#bd93f9`                     | `#17496a`                     |
| `--banner-bg`      | `rgba(68,71,90,0.28)`         | `rgba(199,222,118,0.18)`      |

A tool need only include the variables it actually uses — but any tool
using the shared component patterns below (cards, drop zones, log, etc.)
will need most of them. Copy the full block from
`gmiau-pdf-encrypt.html` when starting a new tool.

---

## Theme handling (required boilerplate)

> **Update 2026-04-19 — prefer the central `@@GMIAU_THEME@@` sentinel.**
> New or refactored tools SHOULD drop a single HTML comment
> `<!-- @@GMIAU_THEME@@ -->` inside `<head>` and let
> `scripts/build-all-offline.py` inline the shared `gmiau-theme.{css,js}`
> bundle (from `../../gmiau-themes/dist/`) at build time. The central
> bundle exposes `gmiauTheme.cycle()` plus a `storage`-event listener so
> iframed tools (the `evidence-exhibitor` shell) track the parent's theme
> automatically. Only tools that have to run entirely unbuilt (a
> hand-shipped single file) still need the hand-pasted boilerplate below.
>
> The CSS-variable table further up is a **floor**, not a ceiling —
> each theme JSON in `gmiau-themes/themes/` now ships additional aliases
> beyond the ones listed here. When adding a new variable, add it to
> every theme JSON first and rebuild; the table will be refreshed when
> the set stabilises.

The theme is persisted to `localStorage` under key `gmiau_cbi_theme`
(values: `"light"` or `"dark"`), and honours the user's OS preference on
first load.

```javascript
const THEME_KEY = 'gmiau_cbi_theme';
const mq = window.matchMedia('(prefers-color-scheme: light)');

function applyTheme(t) {
  document.body.classList.toggle('light', t === 'light');
  document.getElementById('themeBtn').textContent = t === 'light' ? '◑ Dark' : '◑ Light';
}
function toggleTheme() {
  const next = document.body.classList.contains('light') ? 'dark' : 'light';
  localStorage.setItem(THEME_KEY, next);
  applyTheme(next);
}
applyTheme(localStorage.getItem(THEME_KEY) || (mq.matches ? 'light' : 'dark'));
mq.addEventListener('change', e => {
  if (!localStorage.getItem(THEME_KEY)) applyTheme(e.matches ? 'light' : 'dark');
});
```

The shared `gmiau_cbi_theme` key means all standalone tools switch
together if opened in the same browser profile — intentional, so the user
sets their theme once.

> **Toolkit note:** the toolkit shell also reads/writes this same
> `gmiau_cbi_theme` key when embedding a tool, so standalone and
> toolkit forms stay in sync automatically.

---

## Layout skeleton

```html
<body>
  <div class="wrapper">

    <div class="top-bar no-print">
      <button class="theme-btn" id="themeBtn" onclick="toggleTheme()">◑ Light</button>
    </div>

    <header class="hdr">
      <div class="hdr-tag">PDF UTILITY</div>           <!-- category, uppercase -->
      <div class="hdr-title">Tool Name</div>
      <div class="hdr-sub">One-line purpose — files never leave this tab</div>
    </header>

    <div class="card">
      <!-- tool UI: sec-hdr groupings, fields, drop zone, action button -->
    </div>

    <div class="log-wrap no-print">
      <div class="log" id="log">
        <div class="ll ll-info"><span class="pfx">▸</span><span class="msg">Ready. …</span></div>
      </div>
    </div>

    <div class="foot">
      <div>No server — processing is local — your files never leave this tab</div>
      <div class="no-print" style="font-size:.59rem;margin-top:.2rem;color:var(--muted)">
        &copy; <span id="yr"></span> Ryan Bestford / rb. All rights reserved.
      </div>
    </div>

  </div>
</body>
```

The wrapper is `width:100%; max-width:520px` by default (single-column
PDF utilities). Multi-pane tools (e.g. `bundle-splitter`, `passage-marker`)
widen or drop the wrapper entirely — pick what fits the UI.

---

## Typography

Font: `'JetBrains Mono', monospace` — load from Google Fonts in the
source file, inline or bundle only if the tool is used offline.

| Use                   | Size    | Weight | Notes |
|-----------------------|---------|--------|-------|
| Header title          | 1.6rem  | 700    | `--link` dark / `--accent` light |
| Header tag            | .63rem  | 700    | uppercase, letter-spacing .28em |
| Section header        | .61rem  | 700    | uppercase, letter-spacing .14em, `--sec-hdr-bg` pill |
| Body / labels         | .72-.83rem | 400-500 | |
| Small / hints         | .66-.72rem | 400 | `--faint` or `--muted` |
| Uppercase label       | .62rem  | 500    | letter-spacing .12em |
| Go button             | .82rem  | 700    | uppercase, letter-spacing .06em |

---

## Component patterns

All component classes below are defined in `gmiau-pdf-encrypt.html` and
`gmiau-pdf-compress.html` — copy the stanza you need.

| Class                         | Purpose                                         |
|-------------------------------|-------------------------------------------------|
| `.wrapper`                    | Centred max-width column                        |
| `.top-bar` + `.theme-btn`     | Top-right theme toggle pill                     |
| `.hdr` / `.hdr-tag` / `.hdr-title` / `.hdr-sub` | Page header                   |
| `.card`                       | Rounded surface holding the tool UI             |
| `.sec-hdr`                    | Pill-style section header inside a card         |
| `.drop` / `.drop.loaded` / `.drop-ico` / `.drop-name` | Drag-and-drop file zone |
| `.field` / `.lbl` / `.inp-row` | Labelled input row                             |
| `.chk-row` / `.chk-lbl`       | Checkbox with explanatory label                 |
| `.mbtn` / `.mbtn.on`          | Mode-tab row (Encrypt/Decrypt style)            |
| `.abtn` / `.abtn.on`          | Option buttons (AES-256 / RC4 style)            |
| `.pbtn` / `.pbtn.on`          | Preset buttons with title + sub-caption         |
| `.banner`                     | Informational/warning callout inside a card     |
| `.go` / `.go:disabled`        | Primary action button                           |
| `.prog-wrap` / `.prog-bar`    | Thin progress bar for long operations           |
| `.log-wrap` / `.log` / `.ll`  | Activity log under the card                     |
| `.foot`                       | Footer line + copyright                         |
| `.spin` + `@keyframes rot`    | Rotating `◌` spinner glyph                      |

### Log message helper

```javascript
function addLog(type, html) {
  const icons = { info:'▸', ok:'✓', warn:'!', err:'✗', load:'◌' };
  const logEl = document.getElementById('log');
  const d = document.createElement('div');
  d.className = 'll ll-' + type;
  d.innerHTML = '<span class="pfx">' + (icons[type]||'▸') + '</span><span class="msg">' + html + '</span>';
  logEl.appendChild(d);
  logEl.scrollTop = logEl.scrollHeight;
}
```

Use `'load'` for in-progress messages with `<span class="spin">◌</span>`;
finalise with `'ok'` on success or `'err'` on failure.

---

## Library loading

Source tools load third-party libraries from `cdnjs.cloudflare.com` only.
The offline build step substitutes each CDN `<script src>` tag with an
inline `<script>` block containing the minified source.

### Common libraries already vendored across tools

| Library  | CDN URL                                                                    |
|----------|----------------------------------------------------------------------------|
| pdf.js   | `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`        |
| pdf.js worker | `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js` |
| pdf-lib  | `https://cdnjs.cloudflare.com/ajax/libs/pdf-lib/1.17.1/pdf-lib.min.js`     |
| pyodide  | `https://cdn.jsdelivr.net/pyodide/v0.26.0/full/pyodide.js` (only where pypdf/cryptography are needed; not offline-embeddable) |

Pin exact version numbers — never use `latest`. If you need a newer
version, bump it across every tool that uses it in one commit.

### pdf.js worker

Loaded via blob URL in the offline build so the worker runs without
network. The source form uses the normal CDN URL:

```javascript
pdfjsLib.GlobalWorkerOptions.workerSrc =
  'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
```

The build script finds this string and replaces it with a
`URL.createObjectURL(new Blob([...]))` call. Keep the string literal
exactly as above so the builder match succeeds.

---

## Offline build process

For every tool that has an offline variant:

1. Edit the source `source/<tool-name>.html` — iterate on this in the browser.
2. Run `python3 scripts/build-all-offline.py` from `gmiau-tools/` — it walks
   every `source/*.html` and (re)generates the matching `offline/*-offline.html`.
   Rebuild only one tool: `python3 scripts/build-all-offline.py <tool-name>.html`.
   Use `--force` to rebuild regardless of timestamps.
3. Do **not** edit `offline/*-offline.html` by hand — changes will be lost on
   the next rebuild.

Under the hood the builder:

- Inlines every `<script src="https://…"></script>` tag by downloading
  the JS.
- Inlines Google Fonts `<link>` tags, embedding the referenced WOFF2
  files as base64 `data:` URLs.
- For pdf.js, substitutes the
  `pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://…'` assignment
  with a blob URL built from the inlined worker source.
- Handles the `QPDF_WASM_BASE64 = ''` / `QPDF_WASM_URL = '…'`
  placeholder pattern for qpdf.wasm (see the PDF encrypt tool).
- Skips any tool that uses Pyodide (not practical to inline).
- Caches downloads in `.build-cache/` for fast rebuilds.

Output size typically lands between 1–3 MB for tools using pdf.js +
pdf-lib — fine for email / USB distribution.

---

## Confidentiality

See the parent `~/Projects/CLAUDE.md` — these tools are
designed to handle confidential legal-aid client data **locally**:

- All processing happens in-browser. No fetches to external endpoints.
- Do not add analytics, telemetry, error reporting, or font/CSS CDN
  requests beyond the vendored libraries listed above.
- The header sub-line and footer must make the local-processing guarantee
  explicit to the user on every tool.

---

## What NOT to include

| ❌ Don't include                                | ✅ Why                                      |
|------------------------------------------------|--------------------------------------------|
| Network requests at runtime (fetch, XHR, img src to remote) | Breaks offline guarantee; leaks filenames |
| Analytics / telemetry / error reporting SDKs   | Confidentiality rule                       |
| `window.prompt()` / `window.alert()`           | Use in-page UI and the `addLog()` helper   |
| Hardcoded colours outside `:root` / `body.light` | Use CSS variables                        |
| CDNs other than `cdnjs.cloudflare.com` (pyodide via `cdn.jsdelivr.net` excepted where required) | Keeps the offline build script simple |
| `latest` or unpinned library versions           | Rebuilds must be deterministic             |
| Sibling JS/CSS files                            | Tools must remain single-file              |

---

## Starting a new tool

1. Copy `gmiau-pdf-encrypt.html` (for a simple single-card form) or
   `gmiau-pdf-compress.html` (for a pdf.js → pdf-lib pipeline).
2. Rename and strip tool-specific UI; keep the theme block, header,
   footer, log, and base CSS variables.
3. Iterate on the source file in the browser.
4. Run `python3 scripts/build-all-offline.py <tool-name>.html` to generate
   the offline variant in `offline/`. No per-tool builder script is needed.
5. If the tool should ship in the colleague-facing share bundle, add an
   entry to `share/build.py`'s `TOOLS` list and run `share/share-update.sh`.
