Explorar o código

Convert the UI foundation to the Spry CSS framework

Goal 41 T1 (phase A foundation). main.html adopts the Spry topbar/nav/menu/submenu hover menus in place of the custom header and details dropdowns (every link, the admin display-guards, the username and the logout button kept; stm bindings untouched), the container page shell for main, and links spry.css plus all seven theme stylesheets (inert until data-theme is set; T2 wires the picker). data-mode=auto on the html roots; the core theme is the default look.

app.css is rewritten as a thin app-component layer against --spry-* custom properties: the app keeps its own components (badge status family mapped to semantic spry vars, comments/footnotes, blocks and plan review, goal identity, event log, advisor chat, terminal, toasts, quote popover, ssh key panel, cards grid) and drops every rule that duplicated a Spry primitive (body typography, buttons, forms, tables, spacing, muted/error utilities). The old ink/line/surface/accent tokens are gone; --goal derives from --spry-accent.

All twenty pages get the mechanical class remap only (form-row to field, muted to text-muted, error to text-danger, primary/approve to btn-primary, reject to btn-danger, inline buttons to btn-ghost, checkbox labels to choice, inline forms to cluster, wide tables wrapped in table-wrap); layouts, section order and expanders are untouched, that is T3 to T9. login.html is fully converted as the reference for form markup. app.js and task_log.js class names follow the same map.

docs/spry-conversion.md is the binding conventions document for the page tasks: the layering contract, the class mapping table, the badge-colour contract (status to semantic var), markup rules, and the app.css append-only discipline. src/pages/breadcrumb.html adds the pstm-fragment breadcrumb partial (Spry .breadcrumb, crumbs array input), wired into meson template_deps; adoption lands with T5/T6/T9.

Verification: build 127/127; bundles pages 121, advisor_fix 47, goalui 128, chatui 44, all 0 failed; full sequential suite 1511 passed / 6 failed with the 6 in advisor_arc, which passes 41/0 standalone (timing flake per the project note, base run had the same counts). /_statum/resource/spry.css and the theme stylesheets verified 200 from the booted app; the served pages rewrite pstm-res-href to the resource URLs.
saddle-ClankerSaddle hai 2 días
pai
achega
55223c41c2

+ 187 - 0
docs/spry-conversion.md

@@ -0,0 +1,187 @@
+# Spry conversion conventions (goal 41, binding for T3–T10)
+
+ClankerSaddle runs on the Spry CSS framework that ships inside
+libspry. This document is the binding contract for every page task in
+the UI/UX goal: what Spry provides, what `app.css` owns, how classes
+map from the old stylesheet, and the rules for new markup.
+
+## How the framework is loaded
+
+`main.html` and `login.html` link, in this order:
+
+1. `spry.css` — core + default theme (light palette, dark palette on
+   `prefers-color-scheme: dark`). This IS the product default; no
+   `data-theme` is set anywhere by default.
+2. `spry-theme-{airy,dwarf,formal,glassy,h2o,living-sky,modern}.css`
+   — the seven optional themes, loaded but INERT until
+   `<html data-theme="slug">` is set. T2 wires the account-settings
+   picker that sets that attribute; until then they cost a few KB of
+   cache and do nothing.
+3. `app.css` — the app component layer, always LAST so it wins ties.
+
+Color mode is `data-mode="auto"` on `<html>` (spry's default behavior
+— omitting the attribute is also auto). Never hard-code a mode.
+
+## Where styles live — the layering contract
+
+- **spry.css owns every primitive**: typography, links, focus rings,
+  buttons (`button`, `.btn`, `.btn-primary/danger/outline/ghost/link`,
+  `.btn-sm/lg/block`, `.btn-group`), forms (`input/select/textarea`
+  element styles, `.field`, `.label`, `.hint`, `.is-invalid/valid`,
+  `.choice(s)(-inline)`, `.switch`), tables (`table`, `.table-wrap`,
+  `.table-striped/hover/compact/borderless`), cards (`.card`,
+  `-flat/-header/-body/-footer/-title/-subtitle`), alerts (`.alert`
+  + `-danger/success/warning/info`), badges (`.badge` +
+  `-danger/success/warning/info/muted`), progress/meter/spinners,
+  dialogs (`<dialog>`, `.dialog-sm/lg/titlebar/title/close/body/footer`,
+  `[popover]`, `.popover-menu`, `.anchor`/`.popover-anchor`),
+  expanders (`details`/`summary`, `.expander-group`), hero/avatar/otp/
+  drawer/pagination, topbar/nav/menu/submenu/breadcrumb/tabs, layout
+  utilities (`.container(-sm/-lg)`, `.stack`, `.cluster`, `.center`,
+  `.with-sidebar`, `.block/inline/hidden/visually-hidden`, width/
+  position/overflow utilities), spacing utilities
+  (`.{m,p}{t,r,b,l,x,y}-{0..5}`, `.mx-auto`), and text utilities
+  (`.text-{xs..2xl}`, `.text-muted/accent/danger/success/warning/info`,
+  `.fw-*`, `.mono`, `.num`, `.truncate`, `.clamp-2/3`, `.measure`,
+  `.lead`, `.small`).
+- **app.css owns only ClankerSaddle components**: status/event badges
+  (the `.badge-*` family below), the goal identity, comments and
+  footnote treatment, blocks + plan review, the event log, advisor
+  chat, the terminal, toasts, the quote popover, the login shell,
+  `.cards` grid, `.grid-2`, and chrome shims (`.brand-emblem`,
+  `.nav-auth`, `.nav-user`, `.site-footer`).
+- **Never restyle a Spry primitive from app.css** and never
+  re-declare Spry's reset/typography. If a page needs something Spry
+  has, USE the Spry class; only add to app.css when the component is
+  genuinely app-specific.
+
+### app.css append-only discipline
+
+app.css is organised in sections with `==== header ====` comments.
+Within a section, new rules are APPENDED under the section header with
+a `/* comment */` naming the component and the reason (which page,
+which plan/goal). Never edit another task's rules in place — merge
+conflicts in this file are append-per-section and resolved trivially.
+Sections may be added at the END of the file.
+
+## Class mapping (old app.css → Spry)
+
+| old class | use instead |
+| --- | --- |
+| `body` 64rem column | `.container` on `<main>` (72rem, fluid padding) |
+| `.form-row` | `.field` (label + input inside) |
+| `button.primary` | `.btn-primary` |
+| `button.approve` | `.btn-primary` |
+| `button.reject` | `.btn-danger` |
+| `form.inline` | `.cluster` (wrap the fields/buttons) |
+| `label.inline` (checkbox row) | `.choice` |
+| `.muted` | `.text-muted` |
+| `.error` (text color) | `.text-danger` |
+| `details` custom | bare `details`/`summary` (Spry styles them) |
+| `.card` | `.card` (Spry) + `.card-body` etc. as needed |
+| `header` custom chrome | `.topbar` + `.topbar-brand` + `ul.nav`/`.has-submenu`/`ul.submenu` |
+| `.nav-menu`/`.nav-dropdown` | deleted — see topbar row above |
+| tables | bare `<table>` (+ `.table-hover`/`.table-compact` where dense) inside `.table-wrap` when wide |
+
+Kept as app components (now tokenised): `.comment`, `.footnote`,
+`.block-card`, `.plan-review`, `.plan-popover`, `.review-banner`,
+`.goal-*`, `.log-*`, `.event-*`, `.term-*`, `.terminal`, `.chat-*`,
+`.toast*`, `.quote-*`, `.ssh-key`, `.badge-*` status family,
+`.cards`, `.grid-2`, `.status-head`, `.status-corner`,
+`.waiting-drafts`, `.rule-card`, `.chip-remove`, `.yeehaw-*`.
+
+App tokens: `--goal: var(--spry-accent)` and
+`--goal-surface: var(--spry-accent-soft)` (goal identity derives from
+the theme accent; goal pages read as themselves through layout and
+badges, not a competing hue). All other old app tokens
+(`--ink`, `--line`, `--surface`, `--accent`, `--success`, …) are
+DELETED — use the `--spry-*` equivalents.
+
+## The badge-colour contract
+
+Every status/health/event badge consumes a semantic Spry variable —
+NO hard-coded hex anywhere. A `.badge-<status>` rule in app.css maps
+to exactly one of:
+
+- **muted** (`--spry-bg-alt` bg, `--spry-fg-muted` fg): draft, paused,
+  cancelled, offline, reference, command, created, shell-reset,
+  mode-implement, and their goal-* twins.
+- **queued** (`--spry-warning-soft`/`--spry-warning`): queued,
+  enqueued.
+- **info** (`--spry-info-soft`/`--spry-info`): assigned, planning,
+  started, resumed, dispatch, query, question, user-comment,
+  co-developed, mode-investigate, chat-awaiting_answer, kind-task,
+  state_change.
+- **warning** (`--spry-warning-soft`/`--spry-warning`): working,
+  blocked, block, suspended, review_requested, review,
+  completion_review, mode-review, busy, chat-running,
+  suggestion-pending, yeehaw, and the block-raised family.
+- **success** (`--spry-success-soft`/`--spry-success`): done, idle,
+  active, complete, completed, progress, mode-test, chat-idle,
+  suggestion-approved, and the answered family.
+- **danger** (`--spry-danger-soft`/`--spry-danger`): failed and every
+  rejected/failed goal transition.
+- **accent** (`--spry-accent-soft`/`--spry-accent`): plan, plan_review,
+  rfc, mode-orchestrate, block-plan, admin, capability, kind-goal,
+  goal-plan-submitted. (The old violet plan token retired: plans and
+  goals now ride the theme accent.)
+- **neutral inset** (`--spry-bg-inset`/`--spry-fg-muted`): comment,
+  tool.
+- Health badges are FILLED (strong var as bg, `--spry-accent-fg` fg)
+  so they read at a glance on the saddles page.
+
+New statuses in page tasks: add ONE line to the matching group in the
+badge section of app.css, never a new hex.
+
+## Markup rules for page tasks (T3–T10)
+
+1. **Page shell**: every page's content sits directly under `<main
+   class="container">`; page-level stacks use `.stack` or
+   `.stack`-like sections with spacing utilities. Detail pages may
+   adopt `.with-sidebar` from T5/T6 for side navigation.
+2. **Spacing** comes from Spry utilities (`.mt-3`, `.p-2`, `.gap-*`
+   via `.cluster`/`.stack`), not bespoke margins. If you write
+   `margin: …` in app.css, you need a reason comment.
+3. **Every colour via `--spry-*`** (or an app token derived from
+   one). Dark mode must work with zero extra rules — if you find
+   yourself writing `@media (prefers-color-scheme)`, stop: you
+   hard-coded something.
+4. **Buttons**: the element default is already Spry-styled; add
+   `.btn-primary` for the one primary action per view, `.btn-danger`
+   for destructive, `.btn-ghost` for quiet/inline actions (the old
+   `button.inline`), `.btn-sm` inside dense rows/tables.
+5. **Forms**: `<div class="field"><label…>…</label><input…>
+   <p class="hint">…</p></div>`. Checkboxes/radios use `.choice` /
+   `.choices(-inline)`; toggles use `.switch`. Wide editors
+   (textareas) live in `.field`s capped by `.form-rows .field
+   { max-width: 34rem }` — group a page's edit fields in
+   `.form-rows`.
+6. **History folds / actions surfaces**: per the goal plan, the
+   collapsed-history expander pattern and "actions surface" reshaping
+   (modals via native `<dialog>` with `.dialog-titlebar/-title/
+   -close/-body/-footer`) arrive WITH the page tasks (T3–T9), not in
+   this foundation. When you add one: `<dialog class="…" id="…">`
+   with a `.dialog-titlebar` carrying `<p class="dialog-title">` and
+   a `.dialog-close` button, body in `.dialog-body`, actions right-
+   aligned in `.dialog-footer`.
+7. **Expanders**: plain `<details>`/`<summary>` (Spry styles them;
+   group siblings in `.expander-group` to fuse borders).
+8. **Badges**: `<span class="badge badge-<status>">` per the contract
+   above; event kinds on the log/narrative use the same semantic
+   groups via `.event-kind-*` in app.css.
+9. **Wide tables** sit in `.table-wrap`; dense tables add
+   `.table-compact`, row-hover `.table-hover`.
+10. **Login is the reference** for form markup conventions
+    (`src/pages/login.html`).
+
+## The breadcrumb partial
+
+`src/pages/breadcrumb.html` is a `pstm-fragment` partial (the
+`progress_feed` pattern) that renders a Spry `.breadcrumb` from a
+`crumbs` array of `{href?, label}` items — the last crumb carries
+`aria-current="page"`. Include it as:
+
+    <pstm-fragment name="breadcrumb" crumbs="page.crumbs"/>
+
+Adoption by detail pages happens in T5/T6/T9 (it needs each page's
+entrypoint to expose a `crumbs` vector — do not fake it client-side).

+ 1 - 0
meson.build

@@ -50,6 +50,7 @@ template_deps = files(
     'src/pages/progress_feed.html',
     'src/pages/block_answer.html',
     'src/pages/block_reply.html',
+    'src/pages/breadcrumb.html',
 )
 
 # spry:pages-begin

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 407 - 644
src/pages/app.css


+ 2 - 2
src/pages/app.js

@@ -120,7 +120,7 @@
             actions.className = 'actions';
             var add = document.createElement('button');
             add.type = 'button';
-            add.className = 'primary';
+            add.className = 'btn-primary btn-sm';
             add.textContent = 'Add remark';
             add.addEventListener('click', function () {
                 if (body.value.trim().length === 0) return;
@@ -208,7 +208,7 @@
                     entry.appendChild(body);
                     var remove = document.createElement('button');
                     remove.type = 'button';
-                    remove.className = 'inline';
+                    remove.className = 'btn-ghost btn-sm';
                     remove.setAttribute('data-plan-remove', '');
                     remove.textContent = 'Remove';
                     entry.appendChild(remove);

+ 1 - 1
src/pages/block_answer.html

@@ -23,7 +23,7 @@
                 <textarea name="other" rows="2"></textarea>
             </label>
         </div>
-        <button type="submit" class="primary">Answer</button>
+        <button type="submit" class="btn-primary">Answer</button>
     </form>
 </body>
 </html>

+ 2 - 2
src/pages/block_reply.html

@@ -9,9 +9,9 @@
 </head>
 <body>
     <form stm-if="!block.answered && block.block_type != 'question' && block.block_type != 'review'"
-          stm-action="block.answer_action" class="inline">
+          stm-action="block.answer_action" class="cluster">
         <textarea name="answer" rows="3" placeholder="Your reply (delivered to the agent)"></textarea>
-        <button type="submit" class="primary">Reply</button>
+        <button type="submit" class="btn-primary">Reply</button>
     </form>
 </body>
 </html>

+ 20 - 0
src/pages/breadcrumb.html

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <!-- A partial (goal 41 T1): the Spry breadcrumb for detail pages —
+         Tasks → #42, Goals → #7 → Tasks… Each page supplies a
+         `crumbs` array of {href?, label} items; trailing crumb gets
+         aria-current="page" (Spry's .breadcrumb styles it). Adoption
+         by detail pages happens in T5/T6/T9, each page's entrypoint
+         exposing the crumbs vector. -->
+    <pstm-name>breadcrumb</pstm-name>
+</head>
+<body>
+    <pstm-input name="crumbs"/>
+    <nav aria-label="Breadcrumb">
+        <ol class="breadcrumb">
+            <li stm-for-item-in="crumbs"><a stm-if="item.href != null" stm-attribute.href="item.href" stm-text="item.label">label</a><span stm-if="item.href == null" stm-attribute.aria-current="'page'" stm-text="item.label">label</span></li>
+        </ol>
+    </nav>
+</body>
+</html>

+ 19 - 19
src/pages/chat_detail.html

@@ -126,7 +126,7 @@
 
         <div stm-if="chat != null && chat.id == 0">
             <h2>Chat not found</h2>
-            <p class="muted">That chat does not exist, or belongs to another user -
+            <p class="text-muted">That chat does not exist, or belongs to another user -
                 chats are private. <a href="/chats">Back to your chats.</a></p>
         </div>
 
@@ -135,8 +135,8 @@
                 <h2 stm-text="chat.title">title</h2>
                 <p><span stm-attribute.class="'badge badge-chat-' + chat.status"
                          stm-text="chat.status_display">status</span>
-                    <span class="muted">advisor chat</span>
-                    <form class="inline">
+                    <span class="text-muted">advisor chat</span>
+                    <form class="cluster">
                         <button type="button" class="chat-delete" stm-action="chat.delete"
                                 stm-confirm="Delete this chat? Its messages and suggestions go with it.">Delete chat</button>
                     </form></p>
@@ -157,15 +157,15 @@
                         <details>
                             <summary><span class="badge badge-tool">tool</span>
                                 <code stm-text="message.tool_name">name</code></summary>
-                            <p class="muted">arguments</p>
+                            <p class="text-muted">arguments</p>
                             <pre stm-text="message.arguments">args</pre>
-                            <p class="muted">result</p>
+                            <p class="text-muted">result</p>
                             <pre stm-text="message.result">result</pre>
                         </details>
                     </div>
                     <div stm-if="message.kind == 'user'" class="chat-bubble chat-user">
                         <div class="md" stm-markdown="message.content">message</div>
-                        <p class="muted" stm-text="message.when_display">when</p>
+                        <p class="text-muted" stm-text="message.when_display">when</p>
                     </div>
                     <!-- A tool-calling assistant row renders NO bubble
                          of its own when it said nothing worth reading
@@ -176,7 +176,7 @@
                     <div stm-if="message.kind == 'assistant' && !message.is_question && !message.bubble_hidden"
                          class="chat-bubble chat-assistant">
                         <div class="md" stm-markdown="message.content">message</div>
-                        <p class="muted" stm-text="message.when_display">when</p>
+                        <p class="text-muted" stm-text="message.when_display">when</p>
                     </div>
                     <div stm-if="message.kind == 'assistant' && message.is_question"
                          class="chat-question">
@@ -186,35 +186,35 @@
                         <div stm-if="!message.answered && chat.status == 'awaiting_answer'"
                              class="card chat-question-card"
                              stm-attribute.data-message-id="message.id">
-                            <p stm-if="message.options.length > 0" class="muted">Choose an
+                            <p stm-if="message.options.length > 0" class="text-muted">Choose an
                                 answer, or type your own below.</p>
                             <div class="chat-options" stm-if="message.options.length > 0">
                                 <button type="button" stm-for-option-in="message.options"
                                         stm-key="option" stm-text="option">option</button>
                             </div>
-                            <form class="inline"
+                            <form class="cluster"
                                   onsubmit="window.submitAdvisorOther(this); return false;">
                                 <input type="text" name="other"
                                        placeholder="Or type your own answer">
                                 <button type="submit">Answer</button>
                             </form>
                         </div>
-                        <p stm-if="message.answered" class="muted">You answered:
+                        <p stm-if="message.answered" class="text-muted">You answered:
                             <strong stm-text="message.answer">answer</strong></p>
                     </div>
                     <div stm-if="message.kind == 'error'" class="chat-error">
                         <p stm-text="message.content">error</p>
                     </div>
                     <div stm-if="message.kind == 'system'" class="chat-system">
-                        <p class="muted" stm-text="message.content">system</p>
+                        <p class="text-muted" stm-text="message.content">system</p>
                     </div>
                 </div>
 
                 <div stm-if="chat.status == 'running'" class="chat-thinking">
-                    <p class="muted">thinking…</p>
+                    <p class="text-muted">thinking…</p>
                 </div>
 
-                <p stm-if="chat.messages.length == 0" class="muted">Nothing yet - say
+                <p stm-if="chat.messages.length == 0" class="text-muted">Nothing yet - say
                     something below.</p>
             </div>
 
@@ -227,22 +227,22 @@
                              stm-text="suggestion.kind">task</span>
                         <span stm-attribute.class="'badge badge-suggestion-' + suggestion.status"
                               stm-text="suggestion.status">pending</span>
-                        <span class="muted" stm-text="suggestion.created_display">when</span></p>
+                        <span class="text-muted" stm-text="suggestion.created_display">when</span></p>
                     <p><strong stm-text="suggestion.title">title</strong></p>
                     <div class="md" stm-markdown="suggestion.body">body</div>
-                    <p class="muted" stm-if="suggestion.kind == 'task'">
+                    <p class="text-muted" stm-if="suggestion.kind == 'task'">
                         mode <span stm-text="suggestion.mode.length > 0 ? suggestion.mode : 'implement'">mode</span>
                         - priority <span stm-text="suggestion.priority">5</span>
                         <span stm-if="suggestion.project_id > 0"> - project
                             <span stm-text="suggestion.project_id">id</span></span></p>
-                    <p class="muted" stm-if="suggestion.kind == 'goal'">
+                    <p class="text-muted" stm-if="suggestion.kind == 'goal'">
                         <span stm-if="suggestion.branch_name.length > 0">branch
                             <code stm-text="suggestion.branch_name">branch</code> - </span>
                         <span stm-if="suggestion.project_id > 0">project
                             <span stm-text="suggestion.project_id">id</span> - </span>
                         created as an unqueued draft</p>
                     <div stm-if="suggestion.status == 'pending'" class="chat-suggestion-actions">
-                        <button type="button" class="primary" data-decision="approve">Approve</button>
+                        <button type="button" class="btn-primary" data-decision="btn-primary">Approve</button>
                         <button type="button" data-decision="approve_enqueue">Approve + enqueue</button>
                         <button type="button" data-decision="decline">Decline</button>
                     </div>
@@ -259,11 +259,11 @@
                 <textarea id="chat-composer" rows="3"
                           stm-attribute.disabled="chat.status == 'running'"
                           placeholder="Reply to the advisor..."></textarea>
-                <button type="button" class="primary"
+                <button type="button" class="btn-primary"
                         stm-attribute.disabled="chat.status == 'running'"
                         onclick="window.sendAdvisorMessage()">Send</button>
             </div>
-            <p class="muted" stm-if="chat.status == 'awaiting_answer'">The advisor is
+            <p class="text-muted" stm-if="chat.status == 'awaiting_answer'">The advisor is
                 waiting for your answer above.</p>
         </div>
     </section>

+ 9 - 5
src/pages/chats.html

@@ -7,7 +7,7 @@
 <body>
     <section>
         <h2>Chats</h2>
-        <p class="muted">Your advisor conversations. Ask the advisor on the
+        <p class="text-muted">Your advisor conversations. Ask the advisor on the
             <a href="/">dashboard</a> to start a new one - it can read the fleet's
             state, ask you questions and suggest tasks or goals, but never runs
             commands itself.</p>
@@ -19,7 +19,9 @@
         <p stm-if="chats == null">Loading…</p>
 
         <div stm-if="chats != null">
-            <table stm-if="chats.chats.length > 0">
+            <div class="table-wrap">
+                <table stm-if="chats.chats.length >
+ 0">
                 <thead><tr><th>Chat</th><th>Status</th><th>Created</th><th>Updated</th><th></th></tr></thead>
                 <tbody>
                     <tr stm-for-chat-in="chats.chats" stm-key="chat.id">
@@ -30,15 +32,17 @@
                         <td stm-text="chat.created_display">when</td>
                         <td stm-text="chat.updated_display">when</td>
                         <td>
-                            <form class="inline">
+                            <form class="cluster">
                                 <button type="button" stm-action="chat.delete"
                                         stm-confirm="Delete this chat? Its messages and suggestions go with it.">Delete</button>
                             </form>
                         </td>
                     </tr>
                 </tbody>
-            </table>
-            <p stm-if="chats.chats.length == 0" class="muted">No chats yet - type a
+            
+                </table>
+            </div>
+            <p stm-if="chats.chats.length == 0" class="text-muted">No chats yet - type a
                 question into the "Ask the advisor" box at the top of the
                 <a href="/">dashboard</a> to start one.</p>
         </div>

+ 8 - 8
src/pages/discoveries.html

@@ -37,15 +37,15 @@
 <body>
     <section>
         <h2>Discoveries</h2>
-        <p class="muted">The fleet's editable knowledge base of non-project facts — compiler
+        <p class="text-muted">The fleet's editable knowledge base of non-project facts — compiler
             quirks, library behaviours, tool flags, protocol gotchas. Agents search this
             before working with a technology and refine notes in place; you can do the same.</p>
 
         <p stm-if="discoveries == null">Loading…</p>
 
         <div stm-if="discoveries != null">
-            <form class="inline" onsubmit="window.setDiscoveryQuery(document.getElementById('discovery-q').value); return false;">
-                <div class="form-row"><label for="discovery-q">Search</label>
+            <form class="cluster" onsubmit="window.setDiscoveryQuery(document.getElementById('discovery-q').value); return false;">
+                <div class="field"><label for="discovery-q">Search</label>
                     <input type="search" id="discovery-q"
                            stm-attribute.value="discoveries.query"
                            placeholder="meson, valac, sqlite…">
@@ -55,27 +55,27 @@
             <div stm-for-discovery-in="discoveries.discoveries" stm-key="discovery.id"
                  stm-if="window.discoveryRowVisible(discovery)" class="card">
                 <div class="md" stm-markdown="discovery.body">body</div>
-                <p class="muted">Created <span stm-text="discovery.created_display">when</span><span
+                <p class="text-muted">Created <span stm-text="discovery.created_display">when</span><span
                         stm-if="discovery.updated_display != ''"> · Updated <span
                         stm-text="discovery.updated_display">when</span></span></p>
                 <details>
                     <summary>Edit</summary>
                     <form stm-action="discovery.update">
                         <input type="hidden" name="discovery_id" stm-attribute.value="discovery.id">
-                        <div class="form-row"><label>Body</label>
+                        <div class="field"><label>Body</label>
                             <textarea name="body" rows="6" stm-attribute.value="discovery.body"></textarea></div>
                         <button type="submit">Save</button>
                     </form>
                 </details>
-                <form class="inline">
+                <form class="cluster">
                     <button type="button" stm-action="discovery.delete"
                             stm-confirm="Delete this discovery?">Delete</button>
                 </form>
             </div>
-            <p stm-if="discoveries.discoveries.length == 0" class="muted">
+            <p stm-if="discoveries.discoveries.length == 0" class="text-muted">
                 No discoveries yet — the knowledge base starts empty and grows as agents
                 record what they learn.</p>
-            <p stm-if="discoveries.more > 0" class="muted">… and <span
+            <p stm-if="discoveries.more > 0" class="text-muted">… and <span
                     stm-text="discoveries.more">n</span> more (narrow the search).</p>
         </div>
     </section>

+ 1 - 1
src/pages/goal_branches.html

@@ -17,6 +17,6 @@
         <span stm-attribute.class="branch.pushed ? 'badge badge-done' : 'badge badge-failed'"
               stm-text="branch.pushed ? 'pushed' : 'unpushed'">state</span>
     </span>
-    <span class="muted" stm-if="note != null" stm-text="note">note</span>
+    <span class="text-muted" stm-if="note != null" stm-text="note">note</span>
 </body>
 </html>

+ 50 - 50
src/pages/goal_detail.html

@@ -124,7 +124,7 @@
         <p><a href="/goals">&larr; Goals</a></p>
         <h2 stm-if="goal.exists"><span class="goal-icon">🎯</span> #<span stm-text="goal.id">id</span> —
             <span stm-text="goal.title">title</span></h2>
-        <p stm-if="!goal.exists" class="muted">Goal not found.</p>
+        <p stm-if="!goal.exists" class="text-muted">Goal not found.</p>
 
         <div stm-if="goal.exists">
             <!-- Status head (first, always): lifecycle badge, branch
@@ -142,7 +142,7 @@
                 </div>
                 <p><span stm-attribute.class="'badge badge-' + goal.status" stm-text="goal.display">status</span>
                     · goal branch <code stm-text="goal.branch_name">branch</code>
-                    <span class="muted">from</span> <code stm-text="goal.base_branch">base</code>
+                    <span class="text-muted">from</span> <code stm-text="goal.base_branch">base</code>
                     <a stm-if="goal.compare_url.length > 0" stm-attribute.href="goal.compare_url"
                        target="_blank" rel="noopener">· compare changes</a>
                     · priority <strong stm-text="goal.priority">3</strong>
@@ -150,7 +150,7 @@
                         <strong stm-text="goal.agent_saddle">saddle</strong></span></p>
                 <pstm-fragment name="goal-branches"
                                note="&quot;(the daemon's /status report)&quot;"/>
-                <p class="muted">Project:
+                <p class="text-muted">Project:
                     <a stm-attribute.href="'/projects/' + goal.project_id" stm-text="goal.project_name">project</a>
                     · Created <span stm-text="goal.created_display">when</span>
                     <span stm-if="goal.started_display.length > 0"> · Started
@@ -158,13 +158,13 @@
                     <span stm-if="goal.completed_display.length > 0"> · Finished
                         <span stm-text="goal.completed_display">when</span></span></p>
                 <p>
-                    <button type="button" class="primary" stm-if="goal.start != null"
+                    <button type="button" class="btn-primary" stm-if="goal.start != null"
                             stm-action="goal.start">Start goal</button>
                     <button type="button" stm-if="goal.pause != null"
                             stm-action="goal.pause">Pause</button>
                     <button type="button" stm-if="goal.resume != null"
                             stm-action="goal.resume">Resume</button>
-                    <button type="button" class="primary" stm-if="goal.retry_run != null"
+                    <button type="button" class="btn-primary" stm-if="goal.retry_run != null"
                             stm-action="goal.retry_run">Retry supervision</button>
                     <button type="button" stm-if="goal.requeue_run != null"
                             stm-action="goal.requeue_run"
@@ -178,20 +178,20 @@
                             stm-action="goal.cancel"
                             stm-confirm="Cancel this goal? Its tasks are cancelled with it.">Cancel goal</button>
                 </p>
-                <p class="muted" stm-if="goal.status == 'draft'">Starting locks the brief and
+                <p class="text-muted" stm-if="goal.status == 'draft'">Starting locks the brief and
                     hands the goal to its agent (an orchestrate-mode run task, priority 3).</p>
-                <p class="muted" stm-if="goal.status == 'paused'">Paused — supervision frozen;
+                <p class="text-muted" stm-if="goal.status == 'paused'">Paused — supervision frozen;
                     task events queue and coalesce into the resume.</p>
                 <!-- Plan 15 C4: the ONE muted activity line (the
                      Current-activity card is gone) — what the run is
                      doing right now, folded into the status head. -->
-                <p class="muted" stm-if="goal.current_activity.length > 0"
+                <p class="text-muted" stm-if="goal.current_activity.length > 0"
                    stm-text="goal.current_activity">activity</p>
-                <p class="muted" stm-if="goal.saddle_health.length > 0"
+                <p class="text-muted" stm-if="goal.saddle_health.length > 0"
                    stm-text="goal.saddle_health">health</p>
                 <!-- S5: the goal's aggregate LLM usage across its tasks
                      (the run plus the workers) — empty renders nothing. -->
-                <p class="muted" stm-if="goal.usage_line.length > 0"
+                <p class="text-muted" stm-if="goal.usage_line.length > 0"
                    stm-text="goal.usage_line">LLM: 1,234 calls · 45.2M in / 1.1M out tok · 3h 12m</p>
             </section>
 
@@ -203,7 +203,7 @@
             <section class="card">
                 <h3>Agent progress</h3>
                 <pstm-fragment name="progress-feed" entries="goal.progress"/>
-                <p stm-if="goal.progress.length == 0" class="muted">No progress reports yet — the goal
+                <p stm-if="goal.progress.length == 0" class="text-muted">No progress reports yet — the goal
                     agent reports via <code>saddle progress-update</code>.</p>
             </section>
 
@@ -220,7 +220,7 @@
                     <p><span stm-attribute.class="'badge badge-block-' + block.block_type"
                              stm-text="block.block_type">kind</span>
                         <strong stm-text="block.question">question</strong>
-                        <span class="muted" stm-text="block.created_display">when</span></p>
+                        <span class="text-muted" stm-text="block.created_display">when</span></p>
                     <!-- The pending completion review renders in the rich
                          review form (highlight-remarks queue shared with
                          the plan review; push chips + compare above the
@@ -235,19 +235,19 @@
                         <div class="md rfc-body goal-body" data-plan-body
                              stm-markdown="block.body">report</div>
                         <div class="actions">
-                            <form stm-action="block.answer_action" class="inline">
+                            <form stm-action="block.answer_action" class="cluster">
                                 <input type="hidden" name="verdict" value="accept">
-                                <button type="submit" class="approve" data-plan-accept>Accept goal</button>
+                                <button type="submit" class="btn-primary" data-plan-accept>Accept goal</button>
                             </form>
                             <button type="button" data-plan-toggle>Reject with feedback</button>
                         </div>
                         <div data-plan-feedback hidden>
-                            <p class="muted">Highlight report text to attach a remark to it — the queue
+                            <p class="text-muted">Highlight report text to attach a remark to it — the queue
                                 rides the rejection numbered, then clears. Accepting ignores the queue.</p>
                             <ul class="plan-comments" data-plan-remarks></ul>
                             <textarea name="feedback" rows="2"
                                       placeholder="Additional feedback (optional)"></textarea>
-                            <button type="button" class="primary" data-plan-submit>Submit feedback</button>
+                            <button type="button" class="btn-primary" data-plan-submit>Submit feedback</button>
                         </div>
                     </div>
                     <div stm-if="!block.is_completion_review">
@@ -263,7 +263,7 @@
                         <p><span stm-attribute.class="'badge badge-block-' + block.block_type"
                                  stm-text="block.block_type">kind</span>
                             <strong stm-text="block.question">question</strong>
-                            <span class="muted" stm-text="block.created_display">when</span></p>
+                            <span class="text-muted" stm-text="block.created_display">when</span></p>
                         <div stm-if="block.show_body && block.body.length > 0"
                              class="md rfc-body goal-body" stm-markdown="block.body">body</div>
                         <blockquote stm-if="block.answered" class="term-comment-quote">
@@ -301,19 +301,19 @@
                     <div class="md rfc-body goal-body" data-plan-body
                          stm-markdown="goal.plan_pending_block.body">revision</div>
                     <div class="actions">
-                        <form stm-action="goal.plan_pending_block.answer_action" class="inline">
+                        <form stm-action="goal.plan_pending_block.answer_action" class="cluster">
                             <input type="hidden" name="verdict" value="accept">
-                            <button type="submit" class="approve" data-plan-accept>Approve plan</button>
+                            <button type="submit" class="btn-primary" data-plan-accept>Approve plan</button>
                         </form>
                         <button type="button" data-plan-toggle>Reject with feedback</button>
                     </div>
                     <div data-plan-feedback hidden>
-                        <p class="muted">Highlight plan text to attach a remark to it — the queue
+                        <p class="text-muted">Highlight plan text to attach a remark to it — the queue
                             rides the rejection numbered, then clears. Approving ignores the queue.</p>
                         <ul class="plan-comments" data-plan-remarks></ul>
                         <textarea name="feedback" rows="2"
                                   placeholder="Additional feedback (optional)"></textarea>
-                        <button type="button" class="primary" data-plan-submit>Submit feedback</button>
+                        <button type="button" class="btn-primary" data-plan-submit>Submit feedback</button>
                     </div>
                 </div>
 
@@ -323,11 +323,11 @@
                             stm-text="goal.plan_revisions.length">0</span>)</summary>
                     <div stm-if="goal.plan.length > 0" class="md rfc-body goal-body" data-plan-body
                          stm-markdown="goal.plan">plan</div>
-                    <p stm-if="goal.plan.length == 0" class="muted">No approved plan yet.</p>
+                    <p stm-if="goal.plan.length == 0" class="text-muted">No approved plan yet.</p>
                     <div stm-for-revision-in="goal.plan_revisions" class="plan-revision">
                         <p><span stm-attribute.class="'badge ' + (revision.approved ? 'badge-done' : (revision.answered ? 'badge-failed' : 'badge-plan'))"
                                  stm-text="revision.approved ? 'approved' : (revision.answered ? 'rejected' : 'pending')">verdict</span>
-                            <span class="muted">Revision #<span stm-text="revision.id">n</span> &mdash; <span stm-text="revision.created_display">when</span></span></p>
+                            <span class="text-muted">Revision #<span stm-text="revision.id">n</span> &mdash; <span stm-text="revision.created_display">when</span></span></p>
                         <details>
                             <summary>Submitted markdown</summary>
                             <div class="md rfc-body" stm-markdown="revision.body">body</div>
@@ -360,7 +360,7 @@
                     <button type="button" stm-attribute.class="window.goalFilterClass('failed')"
                             onclick="window.goalSetFilter('failed')"
                             stm-text="'failed ' + goal.failed_tasks">failed</button>
-                    <span class="muted">— click a status to filter</span>
+                    <span class="text-muted">— click a status to filter</span>
                 </p>
                 <table>
                     <thead><tr><th>#</th><th>Title</th><th>Status</th><th>Mode</th><th>Requires</th><th>Created by</th></tr></thead>
@@ -378,10 +378,10 @@
                             <td><span stm-for-capability-in="goal_task.capabilities">
                                 <span class="badge badge-capability"
                                       stm-text="capability">tag</span> </span></td>
-                            <td class="muted" stm-text="goal_task.creator">who</td>
+                            <td class="text-muted" stm-text="goal_task.creator">who</td>
                         </tr>
                         <tr stm-if="goal.goal_tasks.length == 0">
-                            <td colspan="6" class="muted">No tasks yet — the goal agent fans
+                            <td colspan="6" class="text-muted">No tasks yet — the goal agent fans
                                 them out once the plan is approved.</td></tr>
                     </tbody>
                 </table>
@@ -398,20 +398,20 @@
             <section class="card" data-long-form data-field="goal"
                      data-lf-open="0">
                 <h3>Initial brief
-                    <button type="button" class="inline" stm-if="goal.update != null"
+                    <button type="button" class="btn-ghost" stm-if="goal.update != null"
                             data-lf-toggle>Edit</button>
-                    <span class="muted" stm-if="goal.started_display.length > 0">(locked)</span></h3>
+                    <span class="text-muted" stm-if="goal.started_display.length > 0">(locked)</span></h3>
                 <div data-lf-view>
                     <div class="md rfc-body goal-body" data-brief-body stm-markdown="goal.brief">brief</div>
                 </div>
                 <div stm-if="goal.update != null">
                     <form data-lf-edit hidden stm-action="goal.update">
-                        <div class="form-row"><label>Title</label>
+                        <div class="field"><label>Title</label>
                             <input type="text" name="title" data-lf-src="goal.title"></div>
-                        <div class="form-row"><label>Brief (markdown)</label>
+                        <div class="field"><label>Brief (markdown)</label>
                             <textarea name="brief" rows="8"
                                       data-lf-src="goal.brief">brief</textarea></div>
-                        <div class="form-row"><label>Priority (1–9)</label>
+                        <div class="field"><label>Priority (1–9)</label>
                             <input type="number" name="priority" min="1" max="9"
                                    data-lf-src="goal.priority"></div>
                         <button type="submit">Save</button>
@@ -426,14 +426,14 @@
                 <p stm-if="goal.links.length > 0"><span stm-for-link-in="goal.links">
                     <span stm-attribute.class="'badge badge-' + link.relationship"
                           stm-text="link.name + ' (' + link.relationship + ')'">link</span>
-                    <button type="button" class="inline" stm-action="link.remove"
+                    <button type="button" class="btn-ghost" stm-action="link.remove"
                             stm-confirm="Remove this link?">&times;</button>
                     <input type="hidden" name="link_id" stm-attribute.value="link.id">
                 </span></p>
-                <p stm-if="goal.links.length == 0" class="muted">Only the primary project.</p>
+                <p stm-if="goal.links.length == 0" class="text-muted">Only the primary project.</p>
                 <details stm-if="!goal.terminal">
                     <summary>Link a project</summary>
-                    <form class="inline" stm-action="goal.add_link">
+                    <form class="cluster" stm-action="goal.add_link">
                         <select name="linked_project_id" required>
                             <option stm-for-project-in="goal.project_options" stm-attribute.value="project.id"
                                      stm-text="project.name">project</option>
@@ -444,7 +444,7 @@
                         </select>
                         <button type="submit">Link</button>
                     </form>
-                    <form class="inline" stm-action="goal.apply_stack"
+                    <form class="cluster" stm-action="goal.apply_stack"
                           stm-if="goal.stack_options.length > 0" style="margin-top: 0.5em;">
                         <label for="goal-stack">Apply stack</label>
                         <select id="goal-stack" name="stack_id" required>
@@ -460,13 +460,13 @@
                  the agent and every worker it fans out. -->
             <section class="card">
                 <h3>Rules</h3>
-                <p class="muted">The effective set dispatched with this goal's work — task rules,
+                <p class="text-muted">The effective set dispatched with this goal's work — task rules,
                     the primary project's, then co-developed projects' in link order.</p>
                 <div stm-for-rule-in="goal.rules" class="rule-card">
                     <h4 stm-text="rule.name">name</h4>
                     <div class="md" stm-markdown="rule.body">body</div>
                 </div>
-                <p stm-if="goal.rules.length == 0" class="muted">No rules apply to this goal.</p>
+                <p stm-if="goal.rules.length == 0" class="text-muted">No rules apply to this goal.</p>
             </section>
 
             <!-- The supervision narrative (plan 14 B6): state events
@@ -474,10 +474,10 @@
                  feed reads as events-with-occasional-footnotes. -->
             <section class="card">
                 <h3>Supervision narrative</h3>
-                <p stm-if="goal.events.length == 0" class="muted">Nothing yet — start the goal.</p>
+                <p stm-if="goal.events.length == 0" class="text-muted">Nothing yet — start the goal.</p>
                 <div stm-for-event-in="goal.events"
                      stm-attribute.class="'goal-event' + (event.kind == 'comment' ? ' footnote' : '')">
-                    <span class="muted" stm-text="event.at_display">when</span>
+                    <span class="text-muted" stm-text="event.at_display">when</span>
                     <span stm-attribute.class="'badge badge-goal-' + event.kind"
                           stm-text="event.kind.replace('-', ' ')">kind</span>
                     <!-- Comment events render as footnotes (plan 13 B4
@@ -509,11 +509,11 @@
                     <p class="comment-meta">
                         <strong stm-if="!comment.is_system" stm-text="comment.author">author</strong>
                         <span stm-if="comment.is_system" class="system-label">system</span>
-                        <span class="muted" stm-text="comment.created_display">when</span>
+                        <span class="text-muted" stm-text="comment.created_display">when</span>
                     </p>
                 </div>
-                <p stm-if="goal.comments.length == 0" class="muted">No comments yet.</p>
-                <p stm-if="goal.run_task_id == 0" class="muted">The goal agent has not started
+                <p stm-if="goal.comments.length == 0" class="text-muted">No comments yet.</p>
+                <p stm-if="goal.run_task_id == 0" class="text-muted">The goal agent has not started
                     yet — comments open with its run.</p>
                 <!-- Goal 8 task 4: the composer stays OPEN on terminal
                      goals — a comment there re-opens the goal and
@@ -521,13 +521,13 @@
                      the resume (cancelled goals stay closed: their
                      composer keeps the run gate alone). -->
                 <form stm-if="goal.run_task_id > 0" stm-action="goal.comment">
-                    <div class="form-row"><label for="goal-comment-body">Add comment</label>
+                    <div class="field"><label for="goal-comment-body">Add comment</label>
                         <textarea id="goal-comment-body" name="body" rows="3"
                                   placeholder="A timeless note for a future reader — status belongs in progress updates (markdown)"></textarea></div>
-                    <p stm-if="goal.terminal && goal.status != 'cancelled'" class="muted">
+                    <p stm-if="goal.terminal && goal.status != 'cancelled'" class="text-muted">
                         This goal is terminal — commenting re-opens it: the agent resumes
                         with your comment and submits a fresh completion report.</p>
-                    <button type="submit" class="primary">Comment</button>
+                    <button type="submit" class="btn-primary">Comment</button>
                 </form>
                 <dialog id="quote-comment-dialog">
                     <h3>Comment on selected text</h3>
@@ -537,7 +537,7 @@
                         <input type="hidden" name="return_to">
                         <textarea name="body" rows="4" placeholder="Your comment…"
                                   style="width:100%; margin:0.5em 0;"></textarea>
-                        <button type="submit" class="primary">Send comment</button>
+                        <button type="submit" class="btn-primary">Send comment</button>
                         <button type="button"
                                 onclick="document.getElementById('quote-comment-dialog').close()">Cancel</button>
                     </form>
@@ -550,7 +550,7 @@
                 <h3>Goal agent runs</h3>
                 <p><a stm-attribute.href="'/tasks/' + goal.run_task_id + '/log'"
                       target="_blank" rel="noopener">Event log</a>
-                    <span class="muted">— run task
+                    <span class="text-muted">— run task
                         #<span stm-text="goal.run_task_id">id</span></span></p>
                 <table>
                     <thead><tr><th>Run</th><th>Saddle</th><th>Started</th><th>Ended</th><th>Outcome</th></tr></thead>
@@ -558,9 +558,9 @@
                         <tr stm-for-run-in="goal.runs">
                             <td><a stm-attribute.href="run.terminal_url" target="_blank" rel="noopener"
                                    stm-text="'session ' + run.id">run</a></td>
-                            <td class="muted" stm-text="run.saddle_name">saddle</td>
-                            <td class="muted" stm-text="run.started_display">when</td>
-                            <td class="muted" stm-text="run.ended_display">when</td>
+                            <td class="text-muted" stm-text="run.saddle_name">saddle</td>
+                            <td class="text-muted" stm-text="run.started_display">when</td>
+                            <td class="text-muted" stm-text="run.ended_display">when</td>
                             <td><span stm-if="run.open" class="badge badge-working">open</span>
                                 <span stm-if="!run.open" stm-text="run.outcome">outcome</span></td>
                         </tr>

+ 17 - 13
src/pages/goals.html

@@ -19,30 +19,30 @@
 <body>
     <div class="page">
         <h1><span class="goal-icon">🎯</span> Goals</h1>
-        <p class="muted">User-created units of agent-orchestrated work — brief, plan,
+        <p class="text-muted">User-created units of agent-orchestrated work — brief, plan,
             task fan-out, one shared goal branch per involved repo.</p>
 
         <details class="history-expander">
             <summary><strong>New goal</strong></summary>
             <form stm-action="goals.create">
-                <div class="form-row"><label for="goal-project">Project</label>
+                <div class="field"><label for="goal-project">Project</label>
                     <select id="goal-project" name="project_id" required>
                         <option stm-for-project-in="goals.projects" stm-attribute.value="project.id"
                                  stm-text="project.name">project</option>
                     </select></div>
-                <div class="form-row"><label for="goal-title">Title</label>
+                <div class="field"><label for="goal-title">Title</label>
                     <input type="text" id="goal-title" name="title" required
                            placeholder="What should the goal achieve?"></div>
-                <div class="form-row"><label for="goal-brief">Initial brief</label>
+                <div class="field"><label for="goal-brief">Initial brief</label>
                     <textarea id="goal-brief" name="brief" rows="6" required
                               placeholder="Markdown brief — locks once the goal starts"></textarea></div>
-                <div class="form-row"><label for="goal-branch">Goal branch name</label>
+                <div class="field"><label for="goal-branch">Goal branch name</label>
                     <input type="text" id="goal-branch" name="branch_name" required
                            placeholder="goal/my-feature"></div>
-                <div class="form-row"><label for="goal-base">Base branch <span class="muted">(default: the project's default branch)</span></label>
+                <div class="field"><label for="goal-base">Base branch <span class="text-muted">(default: the project's default branch)</span></label>
                     <input type="text" id="goal-base" name="base_branch"
                            placeholder="main"></div>
-                <div class="form-row"><label for="goal-priority">Priority</label>
+                <div class="field"><label for="goal-priority">Priority</label>
                     <select id="goal-priority" name="priority">
                         <option value="1">1 — highest</option>
                         <option value="2">2</option>
@@ -54,11 +54,13 @@
                         <option value="8">8</option>
                         <option value="9">9 — lowest</option>
                     </select></div>
-                <button type="submit" class="primary">Create goal</button>
+                <button type="submit" class="btn-primary">Create goal</button>
             </form>
         </details>
 
-        <table>
+        <div class="table-wrap">
+            <table>
+
             <thead><tr><th>#</th><th>Title</th><th>Status</th><th>Branch</th><th>Priority</th>
                 <th>Tasks</th><th>Created</th></tr></thead>
             <tbody>
@@ -69,12 +71,14 @@
                               stm-text="goal.display">status</span></td>
                     <td><code stm-text="goal.branch_name">branch</code></td>
                     <td stm-text="goal.priority">3</td>
-                    <td><span class="muted" stm-text="goal.draft_tasks + ' draft / ' + goal.queued_tasks + ' queued / ' + goal.running_tasks + ' running / ' + goal.done_tasks + ' done'">counts</span></td>
-                    <td class="muted" stm-text="goal.created_display">when</td>
+                    <td><span class="text-muted" stm-text="goal.draft_tasks + ' draft / ' + goal.queued_tasks + ' queued / ' + goal.running_tasks + ' running / ' + goal.done_tasks + ' done'">counts</span></td>
+                    <td class="text-muted" stm-text="goal.created_display">when</td>
                 </tr>
-                <tr stm-if="goals.goals.length == 0"><td colspan="7" class="muted">No goals yet.</td></tr>
+                <tr stm-if="goals.goals.length == 0"><td colspan="7" class="text-muted">No goals yet.</td></tr>
             </tbody>
-        </table>
+        
+            </table>
+        </div>
     </div>
 </body>
 </html>

+ 26 - 26
src/pages/home.html

@@ -264,11 +264,11 @@
                           stm-attribute.disabled="!dashboard.advisor_configured"
                           placeholder="What should the fleet work on next?"></textarea>
                 <div class="advisor-hero-actions">
-                    <button type="submit" class="primary"
+                    <button type="submit" class="btn-primary"
                             stm-attribute.disabled="!dashboard.advisor_configured">Start chat</button>
                 </div>
             </form>
-            <p class="muted" stm-if="dashboard.advisor_configured">Reads your projects,
+            <p class="text-muted" stm-if="dashboard.advisor_configured">Reads your projects,
                 tasks, goals, rules and discoveries - asks you questions and suggests
                 work. <a href="/chats">Your chats</a></p>
             <p class="notice" stm-if="!dashboard.advisor_configured">The advisor is not
@@ -296,14 +296,14 @@
                  "Send test notification" fires the real end-to-end
                  probe through this user's subscriptions. -->
             <section class="card push-bell-card">
-                <form class="inline">
-                    <button type="button" data-push-bell data-bell-on class="primary"
+                <form class="cluster">
+                    <button type="button" data-push-bell data-bell-on class="btn-primary"
                             hidden>Enable notifications</button>
                     <button type="button" data-push-bell data-bell-off
                             hidden>Turn off notifications</button>
                     <button type="button" data-push-test
                             hidden>Send test notification</button>
-                    <span class="muted" data-bell-help>— be told when an agent asks a
+                    <span class="text-muted" data-bell-help>— be told when an agent asks a
                         question, raises an RFC or review</span>
                 </form>
             </section>
@@ -312,10 +312,10 @@
                 <section class="card">
                     <h3>Queue</h3>
                     <p><strong stm-text="dashboard.ready_count">0</strong> ready to dispatch</p>
-                    <p class="muted"><span stm-text="dashboard.queued_count">0</span> queued ·
+                    <p class="text-muted"><span stm-text="dashboard.queued_count">0</span> queued ·
                         <span stm-text="dashboard.active_count">0</span> active ·
                         <span stm-text="dashboard.blocked_count">0</span> blocked</p>
-                    <p class="muted"><span stm-text="dashboard.done_count">0</span> done ·
+                    <p class="text-muted"><span stm-text="dashboard.done_count">0</span> done ·
                         <span stm-text="dashboard.failed_count">0</span> failed</p>
                 </section>
                 <section class="card">
@@ -333,7 +333,7 @@
             <h3>Notes</h3>
             <section class="card">
                 <form stm-action="dashboard.add_note">
-                    <div class="form-row">
+                    <div class="field">
                         <textarea name="body" rows="2" required
                                   placeholder="Note down a batch of changes before it is ready to be a goal…"></textarea></div>
                     <button type="submit">Add note</button>
@@ -349,7 +349,7 @@
                     <li stm-for-discovery-in="dashboard.discoveries" class="md footnote"
                         stm-markdown="discovery.body">body</li>
                 </ul>
-                <p class="muted"><a href="/discoveries">All discoveries →</a></p>
+                <p class="text-muted"><a href="/discoveries">All discoveries →</a></p>
             </div>
             <table stm-if="dashboard.notes.length > 0">
                 <thead><tr><th>Done</th><th>Note</th><th>Created</th><th></th></tr></thead>
@@ -360,14 +360,14 @@
                                     stm-text="note.done ? 'Done' : 'Open'">toggle</button></td>
                         <td><span stm-attribute.class="note.done ? 'footnote' : ''"
                                   stm-text="note.body">body</span></td>
-                        <td class="muted" stm-text="note.created_display">when</td>
+                        <td class="text-muted" stm-text="note.created_display">when</td>
                         <td>
                             <span stm-if="note.goal_id > 0">
                                 <span class="goal-icon">🎯</span>
                                 <a stm-attribute.href="'/goals/' + note.goal_id"
                                    stm-text="note.goal_title">goal</a>
                             </span>
-                            <form class="inline" stm-if="note.goal_id == 0 && !note.done"
+                            <form class="cluster" stm-if="note.goal_id == 0 && !note.done"
                                   stm-action="note.promote">
                                 <select name="project_id" required>
                                     <option stm-for-project-in="dashboard.projects"
@@ -376,22 +376,22 @@
                                 </select>
                                 <input type="text" name="branch_name" required
                                        placeholder="goal/branch" size="14">
-                                <button type="submit" class="primary">Create goal</button>
+                                <button type="submit" class="btn-primary">Create goal</button>
                             </form>
-                            <span class="muted" stm-if="note.goal_id == 0 && note.done">—</span>
-                            <form class="inline" stm-action="note.delete">
+                            <span class="text-muted" stm-if="note.goal_id == 0 && note.done">—</span>
+                            <form class="cluster" stm-action="note.delete">
                                 <button type="submit">Delete</button>
                             </form>
                         </td>
                     </tr>
                 </tbody>
             </table>
-            <p stm-if="dashboard.notes.length == 0" class="muted">No notes yet — jot down the
+            <p stm-if="dashboard.notes.length == 0" class="text-muted">No notes yet — jot down the
                 next batch of changes above.</p>
 
             <h3 stm-if="dashboard.pinned.length > 0">Needs a human
                 (<span stm-text="dashboard.pinned.length">0</span> open)</h3>
-            <p class="muted" stm-if="dashboard.pinned.length > 0">Open questions, RFCs and
+            <p class="text-muted" stm-if="dashboard.pinned.length > 0">Open questions, RFCs and
                 reviews across all tasks and goals, newest first — follow the link to
                 answer on the task or goal page.</p>
             <div class="cards" stm-if="dashboard.pinned.length > 0">
@@ -402,7 +402,7 @@
                         <span stm-attribute.class="'badge badge-kind-' + blockview.item_type"
                               stm-text="blockview.item_type">task</span>
                         <span stm-attribute.class="'badge badge-' + blockview.block_type" stm-text="blockview.block_type">type</span>
-                        <span class="muted" stm-text="blockview.created_display">when</span></p>
+                        <span class="text-muted" stm-text="blockview.created_display">when</span></p>
                     <p><strong stm-text="blockview.question">question</strong></p>
                     <p stm-if="blockview.item_type == 'task'">
                         <a stm-attribute.href="'/tasks/' + blockview.task_id" stm-text="blockview.task_title">task</a></p>
@@ -410,7 +410,7 @@
                         <a stm-attribute.href="'/goals/' + blockview.goal_id" stm-text="blockview.goal_title">goal</a></p>
                 </section>
             </div>
-            <p stm-if="dashboard.pinned.length == 0" class="muted">Nothing needs a human —
+            <p stm-if="dashboard.pinned.length == 0" class="text-muted">Nothing needs a human —
                 no open questions, RFCs or reviews.</p>
 
             <h3 stm-if="dashboard.active_tasks.length > 0">Active tasks</h3>
@@ -422,8 +422,8 @@
                         <td><span stm-attribute.class="'badge badge-' + active.status" stm-text="active.status">status</span></td>
                         <td stm-text="active.task_branch">branch</td>
                         <td><strong stm-if="active.latest_progress.length > 0" stm-text="active.latest_progress">progress</strong>
-                            <span class="muted" stm-if="active.latest_progress.length > 0" stm-text="active.progress_display">when</span>
-                            <span class="muted" stm-if="active.latest_progress.length == 0">—</span></td>
+                            <span class="text-muted" stm-if="active.latest_progress.length > 0" stm-text="active.progress_display">when</span>
+                            <span class="text-muted" stm-if="active.latest_progress.length == 0">—</span></td>
                     </tr>
                 </tbody>
             </table>
@@ -433,22 +433,22 @@
                 <section class="card" stm-for-saddle-in="dashboard.saddles">
                     <h3 stm-text="saddle.name">name</h3>
                     <p><span stm-attribute.class="'badge badge-' + saddle.status" stm-text="saddle.status">status</span>
-                        <span class="muted" stm-text="saddle.host">host</span></p>
+                        <span class="text-muted" stm-text="saddle.host">host</span></p>
                     <p stm-if="saddle.current_goal_id > 0">Supervising goal
                         <a stm-attribute.href="'/goals/' + saddle.current_goal_id"
                            stm-text="saddle.current_goal_title">goal</a>
                         <span class="badge badge-kind-goal">goal</span></p>
                     <p stm-if="saddle.current_task_id > 0 && saddle.current_goal_id == 0">Working on
                         <a stm-attribute.href="'/tasks/' + saddle.current_task_id" stm-text="saddle.current_task_title">task</a></p>
-                    <p stm-if="saddle.current_task_id == 0" class="muted">No current task</p>
+                    <p stm-if="saddle.current_task_id == 0" class="text-muted">No current task</p>
                 </section>
                 <section class="card" stm-if="dashboard.saddles.length == 0">
-                    <p class="muted">No saddles registered.</p>
+                    <p class="text-muted">No saddles registered.</p>
                 </section>
             </div>
 
             <h3>Activity</h3>
-            <p class="muted">The last <span stm-text="dashboard.activity.length">0</span> events
+            <p class="text-muted">The last <span stm-text="dashboard.activity.length">0</span> events
                 across all tasks, newest first.</p>
             <table>
                 <thead><tr><th>When</th><th>Task</th><th>Event</th></tr></thead>
@@ -458,7 +458,7 @@
                         <td stm-if="item.goal_id > 0">
                             <span class="badge badge-kind-goal">goal</span>
                             <a stm-attribute.href="'/goals/' + item.goal_id" stm-text="item.goal_title">goal</a>
-                            <a stm-if="item.task_id > 0" class="muted"
+                            <a stm-if="item.task_id > 0" class="text-muted"
                                stm-attribute.href="'/tasks/' + item.task_id"
                                >&rarr; <span stm-text="'task #' + item.task_id">task</span></a></td>
                         <td stm-if="item.goal_id == 0"><a stm-attribute.href="'/tasks/' + item.task_id" stm-text="item.task_title">task</a>
@@ -468,7 +468,7 @@
                             <span stm-attribute.class="item.kind == 'comment' || item.kind == 'goal-comment' || item.kind == 'goal-system' ? 'footnote' : ''"
                                   stm-text="item.text">activity</span></td>
                     </tr>
-                    <tr stm-if="dashboard.activity.length == 0"><td colspan="3" class="muted">Nothing yet.</td></tr>
+                    <tr stm-if="dashboard.activity.length == 0"><td colspan="3" class="text-muted">Nothing yet.</td></tr>
                 </tbody>
             </table>
         </div>

+ 23 - 11
src/pages/login.html

@@ -6,6 +6,14 @@
     <pstm-uri>/login</pstm-uri>
     <pstm-title>ClankerSaddle — Log in</pstm-title>
     <script pstm-res-src="statum.js"></script>
+    <link rel="stylesheet" pstm-res-href="spry.css">
+    <link rel="stylesheet" pstm-res-href="spry-theme-airy.css">
+    <link rel="stylesheet" pstm-res-href="spry-theme-dwarf.css">
+    <link rel="stylesheet" pstm-res-href="spry-theme-formal.css">
+    <link rel="stylesheet" pstm-res-href="spry-theme-glassy.css">
+    <link rel="stylesheet" pstm-res-href="spry-theme-h2o.css">
+    <link rel="stylesheet" pstm-res-href="spry-theme-living-sky.css">
+    <link rel="stylesheet" pstm-res-href="spry-theme-modern.css">
     <link rel="stylesheet" pstm-res-href="app.css">
     <link rel="icon" type="image/png" pstm-res-href="clanker-saddle.png">
     <link rel="apple-touch-icon" type="image/png" pstm-res-href="clanker-saddle.png">
@@ -27,21 +35,25 @@
     </script>
 </head>
 <body>
-    <section class="login">
-        <div class="login-card">
+    <main class="login">
+        <div class="card login-card">
             <img class="login-emblem" alt="" pstm-res-src="clanker-saddle-emblem.png">
             <h1>ClankerSaddle</h1>
-            <p class="muted">The agent works while you sleep.</p>
+            <p class="text-muted">The agent works while you sleep.</p>
             <p stm-if="auth != null">Welcome <strong stm-text="auth.display_name">user</strong>!</p>
-            <form stm-if="auth == null" stm-action="page.login">
-                <div class="form-row"><label for="login-username">Username</label>
-                    <input id="login-username" type="text" name="username" required autocomplete="username" autofocus></div>
-                <div class="form-row"><label for="login-password">Password</label>
-                    <input id="login-password" type="password" name="password" required autocomplete="current-password"></div>
-                <button type="submit" class="primary">Log in</button>
+            <form stm-if="auth == null" stm-action="page.login" class="stack">
+                <div class="field">
+                    <label for="login-username">Username</label>
+                    <input id="login-username" type="text" name="username" required autocomplete="username" autofocus>
+                </div>
+                <div class="field">
+                    <label for="login-password">Password</label>
+                    <input id="login-password" type="password" name="password" required autocomplete="current-password">
+                </div>
+                <button type="submit" class="btn-primary btn-block">Log in</button>
             </form>
-            <p class="error" id="login-error" hidden></p>
+            <p class="text-danger" id="login-error" hidden></p>
         </div>
-    </section>
+    </main>
 </body>
 </html>

+ 58 - 46
src/pages/main.html

@@ -1,74 +1,86 @@
 <!DOCTYPE html>
-<html lang="en">
+<html lang="en" data-mode="auto">
 <head>
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <pstm-name>main</pstm-name>
     <pstm-title>ClankerSaddle</pstm-title>
     <script pstm-res-src="statum.js"></script>
+    <link rel="stylesheet" pstm-res-href="spry.css">
+    <link rel="stylesheet" pstm-res-href="spry-theme-airy.css">
+    <link rel="stylesheet" pstm-res-href="spry-theme-dwarf.css">
+    <link rel="stylesheet" pstm-res-href="spry-theme-formal.css">
+    <link rel="stylesheet" pstm-res-href="spry-theme-glassy.css">
+    <link rel="stylesheet" pstm-res-href="spry-theme-h2o.css">
+    <link rel="stylesheet" pstm-res-href="spry-theme-living-sky.css">
+    <link rel="stylesheet" pstm-res-href="spry-theme-modern.css">
     <link rel="stylesheet" pstm-res-href="app.css">
     <link rel="icon" type="image/png" pstm-res-href="clanker-saddle.png">
-    <link rel="apple-touch-icon" pstm-res-href="clanker-saddle.png">
+    <link rel="apple-touch-icon" type="image/png" pstm-res-href="clanker-saddle.png">
     <link rel="stm-guard" require="auth != null || location.pathname == '/login'" href="/login">
     <script pstm-res-src="app.js"></script>
 </head>
 <body>
-    <header>
-        <h1><a href="/"><img class="brand-emblem" alt="" pstm-res-src="clanker-saddle-emblem.png">ClankerSaddle</a></h1>
-        <nav stm-if="auth != null">
+    <header class="topbar">
+        <a class="topbar-brand" href="/"><img class="brand-emblem" alt="" pstm-res-src="clanker-saddle-emblem.png">ClankerSaddle</a>
+        <nav class="nav" stm-if="auth != null">
             <!-- spry:nav-begin -->
-            <details class="nav-menu" name="nav-menu">
-                <summary>Work</summary>
-                <div class="nav-dropdown">
-                    <a href="/">Dashboard</a>
-                    <a href="/tasks">Tasks</a>
-                    <a class="nav-goal" href="/goals"><em class="goal-icon">🎯</em> Goals</a>
-                    <a href="/chats">Chats</a>
-                </div>
-            </details>
-            <details class="nav-menu" name="nav-menu">
-                <summary>Library</summary>
-                <div class="nav-dropdown">
-                    <a href="/projects">Projects</a>
-                    <a href="/rules">Rules</a>
-                    <a href="/stacks">Stacks</a>
-                    <a href="/discoveries">Discoveries</a>
-                </div>
-            </details>
-            <details class="nav-menu" name="nav-menu">
-                <summary>Admin</summary>
-                <div class="nav-dropdown">
-                    <a href="/saddles">Saddles</a>
-                    <!-- The users and model links are display-guarded
-                         (the pages' entrypoint guards are
-                         authoritative); auth.permissions rides the
-                         signed auth slot, so an admin sees them
-                         everywhere. -->
-                    <a stm-if="auth != null && auth.permissions.indexOf('admin') != -1" href="/users">Users</a>
-                    <a stm-if="auth != null && auth.permissions.indexOf('admin') != -1" href="/models">Model</a>
-                </div>
-            </details>
+            <ul class="nav" role="menubar">
+                <li class="has-submenu">
+                    <a href="/">Work</a>
+                    <ul class="submenu" role="menu">
+                        <li><a href="/">Dashboard</a></li>
+                        <li><a href="/tasks">Tasks</a></li>
+                        <li><a class="nav-goal" href="/goals"><em class="goal-icon">🎯</em> Goals</a></li>
+                        <li><a href="/chats">Chats</a></li>
+                    </ul>
+                </li>
+                <li class="has-submenu">
+                    <a href="/projects">Library</a>
+                    <ul class="submenu" role="menu">
+                        <li><a href="/projects">Projects</a></li>
+                        <li><a href="/rules">Rules</a></li>
+                        <li><a href="/stacks">Stacks</a></li>
+                        <li><a href="/discoveries">Discoveries</a></li>
+                    </ul>
+                </li>
+                <li class="has-submenu">
+                    <a href="/saddles">Admin</a>
+                    <ul class="submenu" role="menu">
+                        <li><a href="/saddles">Saddles</a></li>
+                        <!-- The users and model links are display-guarded
+                             (the pages' entrypoint guards are
+                             authoritative); auth.permissions rides the
+                             signed auth slot, so an admin sees them
+                             everywhere. -->
+                        <li stm-if="auth != null && auth.permissions.indexOf('admin') != -1"><a href="/users">Users</a></li>
+                        <li stm-if="auth != null && auth.permissions.indexOf('admin') != -1"><a href="/models">Model</a></li>
+                    </ul>
+                </li>
+            </ul>
             <!-- spry:nav-end -->
             <span class="nav-auth">
-                <details class="nav-menu" name="nav-menu">
-                    <summary>Account</summary>
-                    <div class="nav-dropdown">
-                        <span class="nav-user" stm-text="auth.username">user</span>
-                        <a href="/profile" title="Your profile">Profile</a>
-                        <button type="button" class="inline" stm-action="auth.logout">Log out</button>
-                    </div>
-                </details>
+                <ul class="menu" role="menu">
+                    <li class="has-submenu">
+                        <a href="/profile">Account</a>
+                        <ul class="submenu" role="menu">
+                            <li class="nav-user" stm-text="auth.username">user</li>
+                            <li><a href="/profile" title="Your profile">Profile</a></li>
+                            <li><button type="button" class="btn-ghost" stm-action="auth.logout">Log out</button></li>
+                        </ul>
+                    </li>
+                </ul>
                 <!-- spry:nav-auth-begin -->
                 <!-- spry:nav-auth-end -->
             </span>
         </nav>
     </header>
-    <main>
+    <main class="container">
         <div stm-preloader>Loading…</div>
         <div stm-content hidden>
             <pstm-content></pstm-content>
         </div>
     </main>
-    <footer><p>ClankerSaddle — the agent works while you sleep.</p></footer>
+    <footer class="site-footer"><p>ClankerSaddle — the agent works while you sleep.</p></footer>
 </body>
 </html>

+ 15 - 15
src/pages/model_settings.html

@@ -7,7 +7,7 @@
 <body>
     <section>
         <h2>Model</h2>
-        <p class="muted">The deployment's model configuration: the OpenAI-compatible endpoint
+        <p class="text-muted">The deployment's model configuration: the OpenAI-compatible endpoint
             every saddle assignment is sent to, and the advisor's optional overrides. Stored
             in the database — environment variables no longer configure it (a pre-page
             deployment's SADDLE_LLM_* environment is imported once, on first read).</p>
@@ -15,42 +15,42 @@
 
         <div stm-if="model != null">
             <p stm-if="!model.dispatch_ready"><span class="badge badge-offline">paused</span>
-                <span class="muted">No base URL is configured — task assignment is paused and
+                <span class="text-muted">No base URL is configured — task assignment is paused and
                     the advisor is disabled until one is saved.</span></p>
             <p stm-if="model.dispatch_ready"><span class="badge badge-done">ready</span>
-                <span class="muted">Updated <span stm-text="model.updated_display">when</span>.</span></p>
+                <span class="text-muted">Updated <span stm-text="model.updated_display">when</span>.</span></p>
 
             <form stm-action="model.save">
                 <h3>Main endpoint</h3>
-                <p class="muted">Used by every saddle assignment (the harness's LLM).</p>
-                <div class="form-row"><label>Base URL</label>
+                <p class="text-muted">Used by every saddle assignment (the harness's LLM).</p>
+                <div class="field"><label>Base URL</label>
                     <input type="text" name="base_url" placeholder="http://127.0.0.1:9999/v1"
                            stm-attribute.value="model.base_url"></div>
-                <div class="form-row"><label>Model</label>
+                <div class="field"><label>Model</label>
                     <input type="text" name="model" placeholder="(the provider's default)"
                            stm-attribute.value="model.model"></div>
-                <div class="form-row"><label>API key</label>
+                <div class="field"><label>API key</label>
                     <input type="password" name="api_key" autocomplete="off"
                            placeholder="Leave empty to keep the stored key">
-                    <span class="muted" stm-if="model.api_key_set">A key is stored
+                    <span class="text-muted" stm-if="model.api_key_set">A key is stored
                         (<span stm-text="model.api_key_hint">hint</span>)</span>
-                    <label class="inline"><input type="checkbox" name="clear_api_key" value="1"> Clear the stored key</label></div>
+                    <label class="choice"><input type="checkbox" name="clear_api_key" value="1"> Clear the stored key</label></div>
 
                 <h3>Advisor endpoint (optional)</h3>
-                <p class="muted">Overrides for the AI Chat advisor. Leave the base URL empty to
+                <p class="text-muted">Overrides for the AI Chat advisor. Leave the base URL empty to
                     use the main endpoint's fields.</p>
-                <div class="form-row"><label>Base URL</label>
+                <div class="field"><label>Base URL</label>
                     <input type="text" name="advisor_base_url" placeholder="(use the main endpoint)"
                            stm-attribute.value="model.advisor_base_url"></div>
-                <div class="form-row"><label>Model</label>
+                <div class="field"><label>Model</label>
                     <input type="text" name="advisor_model" placeholder="(use the main endpoint's model)"
                            stm-attribute.value="model.advisor_model"></div>
-                <div class="form-row"><label>API key</label>
+                <div class="field"><label>API key</label>
                     <input type="password" name="advisor_api_key" autocomplete="off"
                            placeholder="Leave empty to keep the stored key">
-                    <span class="muted" stm-if="model.advisor_api_key_set">A key is stored
+                    <span class="text-muted" stm-if="model.advisor_api_key_set">A key is stored
                         (<span stm-text="model.advisor_api_key_hint">hint</span>)</span>
-                    <label class="inline"><input type="checkbox" name="clear_advisor_api_key" value="1"> Clear the stored key</label></div>
+                    <label class="choice"><input type="checkbox" name="clear_advisor_api_key" value="1"> Clear the stored key</label></div>
 
                 <button type="submit">Save configuration</button>
             </form>

+ 6 - 6
src/pages/profile.html

@@ -14,20 +14,20 @@
                 <h3 stm-text="profile.name_display">name</h3>
                 <p><span stm-attribute.class="profile.is_admin ? 'badge badge-admin' : 'badge'"
                          stm-text="profile.is_admin ? 'admin' : 'user'">role</span></p>
-                <p class="muted">Username: <strong stm-text="profile.username">username</strong></p>
-                <p class="muted">Email: <span stm-text="profile.email">email</span></p>
-                <p class="muted">Member since <span stm-text="profile.created_display">when</span></p>
+                <p class="text-muted">Username: <strong stm-text="profile.username">username</strong></p>
+                <p class="text-muted">Email: <span stm-text="profile.email">email</span></p>
+                <p class="text-muted">Member since <span stm-text="profile.created_display">when</span></p>
             </div>
 
             <h3>Change password</h3>
             <form stm-action="profile.change_password" style="max-width: 24rem">
-                <div class="form-row"><label for="current-password">Current password</label>
+                <div class="field"><label for="current-password">Current password</label>
                     <input id="current-password" type="password" name="current_password"
                            required autocomplete="current-password"></div>
-                <div class="form-row"><label for="new-password">New password</label>
+                <div class="field"><label for="new-password">New password</label>
                     <input id="new-password" type="password" name="new_password"
                            required autocomplete="new-password"></div>
-                <div class="form-row"><label for="confirm-password">Confirm new password</label>
+                <div class="field"><label for="confirm-password">Confirm new password</label>
                     <input id="confirm-password" type="password" name="confirm_password"
                            required autocomplete="new-password"></div>
                 <button type="submit">Change password</button>

+ 2 - 2
src/pages/progress_feed.html

@@ -13,12 +13,12 @@
     <pstm-input name="entries"/>
     <div stm-if="entries.length > 0">
         <div class="progress-latest md" stm-markdown="entries[0].summary">latest</div>
-        <p class="muted" stm-text="'reported ' + entries[0].when_display">when</p>
+        <p class="text-muted" stm-text="'reported ' + entries[0].when_display">when</p>
         <details class="history-expander" stm-if="entries.length > 1">
             <summary>Show history (<span stm-text="entries.length">0</span> updates)</summary>
             <ul class="progress-history">
                 <li stm-for-item-in="entries">
-                    <span class="muted" stm-text="item.when_display">when</span> —
+                    <span class="text-muted" stm-text="item.when_display">when</span> —
                     <span stm-text="item.summary">summary</span>
                 </li>
             </ul>

+ 34 - 34
src/pages/project_detail.html

@@ -7,7 +7,7 @@
 <body>
     <section>
         <h2 stm-if="project != null && project.exists" stm-text="project.name">Project</h2>
-        <p stm-if="project != null && !project.exists" class="muted">Project not found.</p>
+        <p stm-if="project != null && !project.exists" class="text-muted">Project not found.</p>
 
         <div stm-if="project != null && project.exists">
             <p><a href="/projects">All projects</a></p>
@@ -16,21 +16,21 @@
                     <h3>Details</h3>
                     <p><strong stm-text="project.git_url">git_url</strong> · branch
                         <em stm-text="project.default_branch">main</em></p>
-                    <p class="muted" stm-if="project.summary.length > 0" stm-text="project.summary">summary</p>
-                    <p class="muted">Created <span stm-text="project.created_display">when</span></p>
+                    <p class="text-muted" stm-if="project.summary.length > 0" stm-text="project.summary">summary</p>
+                    <p class="text-muted">Created <span stm-text="project.created_display">when</span></p>
                 </section>
 
                 <section class="card">
                     <h3>Edit</h3>
                     <form stm-action="project.update">
                         <input type="hidden" name="project_id" stm-attribute.value="project.id">
-                        <div class="form-row"><label>Name</label>
+                        <div class="field"><label>Name</label>
                             <input type="text" name="name" stm-attribute.value="project.name"></div>
-                        <div class="form-row"><label>Git URL</label>
+                        <div class="field"><label>Git URL</label>
                             <input type="text" name="git_url" stm-attribute.value="project.git_url"></div>
-                        <div class="form-row"><label>Branch</label>
+                        <div class="field"><label>Branch</label>
                             <input type="text" name="default_branch" stm-attribute.value="project.default_branch"></div>
-                        <div class="form-row"><label>Summary</label>
+                        <div class="field"><label>Summary</label>
                             <input type="text" name="summary" stm-attribute.value="project.summary"></div>
                         <button type="submit">Save</button>
                     </form>
@@ -47,10 +47,10 @@
                         <td><span stm-attribute.class="'badge badge-' + link.relationship" stm-text="link.relationship">relationship</span></td>
                         <td><button type="button" stm-action="link.remove" stm-confirm="Remove this link?">Remove</button></td>
                     </tr>
-                    <tr stm-if="project.links.length == 0"><td colspan="4" class="muted">No linked projects.</td></tr>
+                    <tr stm-if="project.links.length == 0"><td colspan="4" class="text-muted">No linked projects.</td></tr>
                 </tbody>
             </table>
-            <form class="inline" stm-action="project.add_link">
+            <form class="cluster" stm-action="project.add_link">
                 <label for="link-target">Link project</label>
                 <select id="link-target" name="linked_project_id" required>
                     <option stm-for-option-in="project.linkable" stm-attribute.value="option.id" stm-text="option.name">project</option>
@@ -62,7 +62,7 @@
                 <button type="submit">Add link</button>
             </form>
 
-            <form class="inline" stm-action="project.apply_stack" stm-if="project.stack_options.length > 0">
+            <form class="cluster" stm-action="project.apply_stack" stm-if="project.stack_options.length > 0">
                 <label for="stack-target">Apply stack</label>
                 <select id="stack-target" name="stack_id" required>
                     <option stm-for-stack-in="project.stack_options" stm-attribute.value="stack.id"
@@ -70,20 +70,20 @@
                 </select>
                 <button type="submit">Apply</button>
             </form>
-            <p class="muted" stm-if="project.stack_options.length == 0">Manage reference
+            <p class="text-muted" stm-if="project.stack_options.length == 0">Manage reference
                 collections on the <a href="/stacks">stacks page</a> and apply them here.</p>
 
             <h3>Rules</h3>
-            <p class="muted">Attached rules ride along with every task dispatched for this project.</p>
+            <p class="text-muted">Attached rules ride along with every task dispatched for this project.</p>
             <div stm-for-rule-in="project.rules" stm-key="rule.link_id" class="card">
                 <h3 stm-text="rule.name">name</h3>
                 <div class="md" stm-markdown="rule.body">body</div>
-                <form class="inline">
+                <form class="cluster">
                     <button type="button" stm-action="rule.detach" stm-confirm="Detach this rule?">Detach</button>
                 </form>
             </div>
-            <p stm-if="project.rules.length == 0" class="muted">No rules attached.</p>
-            <form class="inline" stm-action="project.attach_rule" stm-if="project.rule_options.length > 0">
+            <p stm-if="project.rules.length == 0" class="text-muted">No rules attached.</p>
+            <form class="cluster" stm-action="project.attach_rule" stm-if="project.rule_options.length > 0">
                 <label for="rule-target">Attach rule</label>
                 <select id="rule-target" name="rule_id" required>
                     <option stm-for-option-in="project.rule_options" stm-attribute.value="option.id"
@@ -91,13 +91,13 @@
                 </select>
                 <button type="submit">Attach</button>
             </form>
-            <p class="muted" stm-if="project.rule_options.length == 0">Create rules on the <a href="/rules">rules page</a>.</p>
+            <p class="text-muted" stm-if="project.rule_options.length == 0">Create rules on the <a href="/rules">rules page</a>.</p>
 
             <h3>Understanding</h3>
-            <p class="muted">A markdown map of the project's structure and anything
+            <p class="text-muted">A markdown map of the project's structure and anything
                 unusual, written by an agent against a commit and refreshed on demand.</p>
             <div class="card" stm-if="project.understanding_current.body.length > 0">
-                <p class="muted"><span stm-if="project.understanding_current.has_commit">commit
+                <p class="text-muted"><span stm-if="project.understanding_current.has_commit">commit
                         <code stm-text="project.understanding_current.commit_hash">hash</code> · </span>refreshed
                     <span stm-text="project.understanding_current.created_display">when</span><span
                         stm-if="project.understanding_current.source_task_id > 0"> · from task
@@ -105,13 +105,13 @@
                            stm-text="'#' + project.understanding_current.source_task_id">task</a></span></p>
                 <div class="md" stm-markdown="project.understanding_current.body">body</div>
             </div>
-            <p class="muted" stm-if="project.understanding_current.body.length == 0">No understanding yet —
+            <p class="text-muted" stm-if="project.understanding_current.body.length == 0">No understanding yet —
                 press Refresh to have an agent explore the project.</p>
-            <form class="inline" stm-action="project.refresh_understanding">
+            <form class="cluster" stm-action="project.refresh_understanding">
                 <button type="submit" stm-confirm="Spin up an agent task to refresh the understanding?">Refresh</button>
             </form>
             <div stm-for-revision-in="project.understanding_history" stm-key="revision.id" class="card">
-                <p class="muted"><span stm-if="revision.has_commit">commit
+                <p class="text-muted"><span stm-if="revision.has_commit">commit
                         <code stm-text="revision.commit_hash">hash</code> · </span>refreshed
                     <span stm-text="revision.created_display">when</span><span
                         stm-if="revision.source_task_id > 0"> · from task
@@ -121,19 +121,19 @@
             </div>
 
             <h3>Project notes</h3>
-            <p class="muted">Written by agents via <code>saddle note</code>; curated here — delete what
+            <p class="text-muted">Written by agents via <code>saddle note</code>; curated here — delete what
                 stops being useful. There is no manual create.</p>
             <div stm-for-note-in="project.notes" stm-key="note.id" class="card">
                 <p><span class="badge" stm-text="note.project_name">project</span>
-                    <span class="muted" stm-text="note.created_display">when</span>
-                    <span class="muted" stm-if="note.source_task_id > 0"> · from task
+                    <span class="text-muted" stm-text="note.created_display">when</span>
+                    <span class="text-muted" stm-if="note.source_task_id > 0"> · from task
                         <a stm-attribute.href="'/tasks/' + note.source_task_id" stm-text="'#' + note.source_task_id">task</a></span></p>
                 <div class="md" stm-markdown="note.body">body</div>
-                <form class="inline">
+                <form class="cluster">
                     <button type="button" stm-action="note.delete" stm-confirm="Delete this note?">Delete</button>
                 </form>
             </div>
-            <p stm-if="project.notes.length == 0" class="muted">No notes yet — they appear as agents
+            <p stm-if="project.notes.length == 0" class="text-muted">No notes yet — they appear as agents
                 record them with <code>saddle note</code>.</p>
 
             <h3>Tasks in this project</h3>
@@ -147,7 +147,7 @@
                         <td><span stm-attribute.class="'badge badge-' + task.status" stm-text="task.display">status</span></td>
                         <td stm-text="task.priority">priority</td>
                     </tr>
-                    <tr stm-if="project.tasks.length == 0"><td colspan="4" class="muted">No tasks yet — create one from the <a href="/tasks">queue</a>.</td></tr>
+                    <tr stm-if="project.tasks.length == 0"><td colspan="4" class="text-muted">No tasks yet — create one from the <a href="/tasks">queue</a>.</td></tr>
                 </tbody>
             </table>
 
@@ -157,17 +157,17 @@
                  delete a renamed project or one with running work. -->
             <div stm-if="project.is_admin" class="card">
                 <h3>Delete project</h3>
-                <p class="muted">Removing <strong stm-text="project.name">name</strong> also removes
+                <p class="text-muted">Removing <strong stm-text="project.name">name</strong> also removes
                     what it owns: <strong stm-text="project.delete_counts">0 goals, 0 tasks</strong>
                     (tasks include the goal agents' own run rows), with their comments, progress
                     and logs.</p>
-                <p stm-if="project.active_tasks > 0" class="muted">Deletion is refused while
+                <p stm-if="project.active_tasks > 0" class="text-muted">Deletion is refused while
                     <span stm-text="project.active_tasks">0</span> task(s) are still queued, working
                     or awaiting review — cancel or let them finish first.</p>
-                <p class="muted"><strong>Linked projects' work is untouched</strong> — projects,
+                <p class="text-muted"><strong>Linked projects' work is untouched</strong> — projects,
                     tasks and goals in other projects that reference this one as co-developed or
                     reference are kept; only their link entry pointing here is removed.</p>
-                <button type="button" class="reject"
+                <button type="button" class="btn-danger"
                         onclick="document.getElementById('project-delete-dialog').showModal()">Delete
                     project…</button>
             </div>
@@ -179,7 +179,7 @@
                 <p><strong>Linked projects' work is untouched.</strong> Other projects' tasks,
                     goals and rows that merely reference this project survive — only their link
                     entry pointing here goes.</p>
-                <p stm-if="project.active_tasks > 0" class="muted">Note: this project still has
+                <p stm-if="project.active_tasks > 0" class="text-muted">Note: this project still has
                     <span stm-text="project.active_tasks">0</span> live task(s) — the deletion
                     will be refused until they are cancelled or finished.</p>
                 <p>Type the project name <strong stm-text="project.name">name</strong> to
@@ -189,8 +189,8 @@
                     <input type="text" name="confirm_name" autocomplete="off"
                            stm-attribute.placeholder="'Type ' + project.name + ' to confirm'"
                            style="width:100%; margin:0.5em 0;">
-                    <button type="submit" class="reject">Delete this project forever</button>
-                    <button type="button" class="inline"
+                    <button type="submit" class="btn-danger">Delete this project forever</button>
+                    <button type="button" class="cluster"
                             onclick="document.getElementById('project-delete-dialog').close()">Cancel</button>
                 </form>
             </dialog>

+ 12 - 8
src/pages/projects.html

@@ -13,32 +13,36 @@
             <details>
                 <summary><strong>New project</strong></summary>
                 <form stm-action="projects.create">
-                    <div class="form-row"><label>Name</label>
+                    <div class="field"><label>Name</label>
                         <input type="text" name="name" required placeholder="web-stack"></div>
-                    <div class="form-row"><label>Git URL</label>
+                    <div class="field"><label>Git URL</label>
                         <input type="text" name="git_url" required placeholder="https://example.com/repo.git"></div>
-                    <div class="form-row"><label>Default branch</label>
+                    <div class="field"><label>Default branch</label>
                         <input type="text" name="default_branch" placeholder="main"></div>
-                    <div class="form-row"><label>Summary</label>
+                    <div class="field"><label>Summary</label>
                         <input type="text" name="summary" placeholder="What this project is for"></div>
                     <button type="submit">Create project</button>
                 </form>
             </details>
 
-            <table>
+            <div class="table-wrap">
+                <table>
+
                 <thead><tr><th>Name</th><th>Git URL</th><th>Branch</th><th>Tasks</th></tr></thead>
                 <tbody>
                     <tr stm-for-project-in="projects.projects">
                         <td><a stm-attribute.href="'/projects/' + project.id" stm-text="project.name">name</a>
-                            <div class="muted" stm-if="project.summary.length > 0" stm-text="project.summary">summary</div></td>
+                            <div class="text-muted" stm-if="project.summary.length > 0" stm-text="project.summary">summary</div></td>
                         <td stm-text="project.git_url">git_url</td>
                         <td stm-text="project.default_branch">branch</td>
                         <td><span stm-text="project.open_tasks">0</span> open ·
                             <span stm-text="project.done_tasks">0</span> closed</td>
                     </tr>
-                    <tr stm-if="projects.projects.length == 0"><td colspan="4" class="muted">No projects yet.</td></tr>
+                    <tr stm-if="projects.projects.length == 0"><td colspan="4" class="text-muted">No projects yet.</td></tr>
                 </tbody>
-            </table>
+            
+                </table>
+            </div>
         </div>
     </section>
 </body>

+ 8 - 8
src/pages/rules.html

@@ -7,7 +7,7 @@
 <body>
     <section>
         <h2>Rules</h2>
-        <p class="muted">Reusable instructions dispatched with every task that carries them —
+        <p class="text-muted">Reusable instructions dispatched with every task that carries them —
             attach rules on the project and task pages.</p>
         <p stm-if="rules == null">Loading…</p>
 
@@ -15,9 +15,9 @@
             <details>
                 <summary><strong>New rule</strong></summary>
                 <form stm-action="rules.create">
-                    <div class="form-row"><label>Name</label>
+                    <div class="field"><label>Name</label>
                         <input type="text" name="name" required placeholder="Use Invercargill"></div>
-                    <div class="form-row"><label>Body</label>
+                    <div class="field"><label>Body</label>
                         <textarea name="body" rows="3" required
                                   placeholder="Never use libgee; use Invercargill enumerables."></textarea></div>
                     <button type="submit">Create rule</button>
@@ -27,23 +27,23 @@
             <div stm-for-rule-in="rules.rules" stm-key="rule.id" class="card">
                 <h3 stm-text="rule.name">name</h3>
                 <div class="md" stm-markdown="rule.body">body</div>
-                <p class="muted">Created <span stm-text="rule.created_display">when</span></p>
+                <p class="text-muted">Created <span stm-text="rule.created_display">when</span></p>
                 <details>
                     <summary>Edit</summary>
                     <form stm-action="rule.update">
                         <input type="hidden" name="rule_id" stm-attribute.value="rule.id">
-                        <div class="form-row"><label>Name</label>
+                        <div class="field"><label>Name</label>
                             <input type="text" name="name" stm-attribute.value="rule.name"></div>
-                        <div class="form-row"><label>Body</label>
+                        <div class="field"><label>Body</label>
                             <textarea name="body" rows="4" stm-attribute.value="rule.body"></textarea></div>
                         <button type="submit">Save</button>
                     </form>
                 </details>
-                <form class="inline">
+                <form class="cluster">
                     <button type="button" stm-action="rule.delete" stm-confirm="Delete this rule everywhere?">Delete</button>
                 </form>
             </div>
-            <p stm-if="rules.rules.length == 0" class="muted">No rules yet.</p>
+            <p stm-if="rules.rules.length == 0" class="text-muted">No rules yet.</p>
         </div>
     </section>
 </body>

+ 30 - 26
src/pages/saddles.html

@@ -13,9 +13,9 @@
             <details stm-if="saddles.is_admin">
                 <summary><strong>Register saddle</strong></summary>
                 <form stm-action="saddles.register">
-                    <div class="form-row"><label>Name</label>
+                    <div class="field"><label>Name</label>
                         <input type="text" name="name" required placeholder="saddle-1"></div>
-                    <div class="form-row"><label>Host</label>
+                    <div class="field"><label>Host</label>
                         <input type="text" name="host" required placeholder="localhost:7100"></div>
                     <button type="submit">Register</button>
                 </form>
@@ -25,62 +25,62 @@
                 <section class="card" stm-for-saddle-in="saddles.saddles">
                     <h3 stm-text="saddle.name">name</h3>
                     <p><span stm-attribute.class="'badge badge-' + saddle.status" stm-text="saddle.status">status</span>
-                        <span class="muted" stm-text="saddle.host">host</span></p>
+                        <span class="text-muted" stm-text="saddle.host">host</span></p>
                     <p stm-if="saddle.current_goal_id > 0">Supervising goal:
                         <a stm-attribute.href="'/goals/' + saddle.current_goal_id"
                            stm-text="saddle.current_goal_title">goal</a>
                         <span class="badge badge-kind-goal">goal</span>
-                        <span class="muted" stm-if="saddle.allocated_display.length > 0" stm-text="'since ' + saddle.allocated_display">since</span></p>
+                        <span class="text-muted" stm-if="saddle.allocated_display.length > 0" stm-text="'since ' + saddle.allocated_display">since</span></p>
                     <p stm-if="saddle.current_task_id > 0 && saddle.current_goal_id == 0">Current task:
                         <a stm-attribute.href="'/tasks/' + saddle.current_task_id" stm-text="saddle.current_task_title">task</a>
-                        <span class="muted" stm-if="saddle.allocated_display.length > 0" stm-text="'since ' + saddle.allocated_display">since</span></p>
-                    <p stm-if="saddle.current_task_id == 0" class="muted">No current task</p>
+                        <span class="text-muted" stm-if="saddle.allocated_display.length > 0" stm-text="'since ' + saddle.allocated_display">since</span></p>
+                    <p stm-if="saddle.current_task_id == 0" class="text-muted">No current task</p>
                     <p><span stm-attribute.class="'badge badge-health-' + saddle.health"
                               stm-text="saddle.health">healthy</span>
-                        <span class="muted" stm-if="saddle.daemon_state.length > 0"
+                        <span class="text-muted" stm-if="saddle.daemon_state.length > 0"
                               stm-text="'daemon ' + saddle.daemon_state">daemon</span>
-                        <span class="muted" stm-if="saddle.daemon_state.length > 0 && saddle.uptime_display.length > 0">&middot; up <span stm-text="saddle.uptime_display">uptime</span></span>
-                        <span class="muted" stm-text="'contacted ' + saddle.last_contact_display + ' ago'">contacted</span></p>
-                    <p stm-if="saddles.is_admin && saddle.token.length > 0" class="muted">API token:
+                        <span class="text-muted" stm-if="saddle.daemon_state.length > 0 && saddle.uptime_display.length > 0">&middot; up <span stm-text="saddle.uptime_display">uptime</span></span>
+                        <span class="text-muted" stm-text="'contacted ' + saddle.last_contact_display + ' ago'">contacted</span></p>
+                    <p stm-if="saddles.is_admin && saddle.token.length > 0" class="text-muted">API token:
                         <code stm-text="saddle.token">token</code></p>
                     <details stm-if="saddles.is_admin && saddle.ssh_public_key.length > 0">
                         <summary>SSH public key</summary>
-                        <p class="muted">Add this as a <strong>deploy key</strong> on the git server
+                        <p class="text-muted">Add this as a <strong>deploy key</strong> on the git server
                             (with write access) so this saddle can clone and push.</p>
                         <pre class="ssh-key"><code stm-text="saddle.ssh_public_key">key</code></pre>
                         <button type="button"
                                 stm-attribute.data-clipboard="saddle.ssh_public_key"
                                 onclick="navigator.clipboard.writeText(this.getAttribute('data-clipboard')).then(function () { this.textContent = 'Copied'; }.bind(this))">Copy key</button>
                     </details>
-                    <p stm-if="saddles.is_admin && saddle.ssh_public_key.length == 0" class="muted">No SSH key reported yet —
+                    <p stm-if="saddles.is_admin && saddle.ssh_public_key.length == 0" class="text-muted">No SSH key reported yet —
                         it appears here once the daemon generates one.</p>
                     <details stm-if="saddles.is_admin">
                         <summary>Git identity</summary>
-                        <form stm-action="saddle.update_identity" class="inline">
+                        <form stm-action="saddle.update_identity" class="cluster">
                             <label>Committed as</label>
                             <input type="text" name="git_name" stm-attribute.value="saddle.git_name"
                                    placeholder="saddle-1">
                             <input type="text" name="git_email" stm-attribute.value="saddle.git_email"
                                    placeholder="saddle-1@example.net">
                             <button type="submit">Save</button>
-                            <span class="muted">Empty fields fall back to the daemon's default identity.</span>
+                            <span class="text-muted">Empty fields fall back to the daemon's default identity.</span>
                         </form>
                     </details>
                     <details stm-if="saddles.is_admin">
                         <summary>Prompt note</summary>
-                        <form stm-action="saddle.update_prompt_note" class="inline">
+                        <form stm-action="saddle.update_prompt_note" class="cluster">
                             <label for="prompt-note">Served inside every assignment's system prompt</label>
                             <textarea id="prompt-note" name="prompt_note" rows="3"
                                       stm-attribute.placeholder="'e.g. You have root and may install development tooling via DNF'"
                                       stm-text="saddle.prompt_note"></textarea>
                             <button type="submit">Save</button>
-                            <span class="muted">A standing message about THIS machine for its agents; empty removes it (capped at 2000 chars).</span>
+                            <span class="text-muted">A standing message about THIS machine for its agents; empty removes it (capped at 2000 chars).</span>
                         </form>
                     </details>
                     <p stm-if="saddles.is_admin && saddle.capabilities.length > 0">Capabilities:
                         <span stm-for-capability-in="saddle.capabilities" stm-key="capability.link_id">
                             <span class="badge"><span stm-text="capability.name">tag</span>
-                                <button type="button" class="inline chip-remove"
+                                <button type="button" class="btn-ghost chip-remove"
                                         stm-action="capability.detach" stm-confirm="Remove this capability?">×</button></span>
                         </span>
                     </p>
@@ -89,8 +89,8 @@
                             <span class="badge"><span stm-text="capability.name">tag</span></span>
                         </span>
                     </p>
-                    <p stm-if="saddle.capabilities.length == 0" class="muted">No capabilities — matches only tasks with none required.</p>
-                    <form class="inline" stm-if="saddles.is_admin && saddles.capability_options.length > 0"
+                    <p stm-if="saddle.capabilities.length == 0" class="text-muted">No capabilities — matches only tasks with none required.</p>
+                    <form class="cluster" stm-if="saddles.is_admin && saddles.capability_options.length > 0"
                           stm-action="saddle.attach_capability">
                         <label stm-attribute.for="'cap-' + saddle.id">Add capability</label>
                         <select stm-attribute.id="'cap-' + saddle.id" name="capability_id" required>
@@ -99,29 +99,31 @@
                         </select>
                         <button type="submit">Add</button>
                     </form>
-                    <form class="inline">
+                    <form class="cluster">
                         <button type="button" stm-if="saddles.is_admin" stm-action="saddle.refresh">Refresh status</button>
                         <button type="button" stm-if="saddles.is_admin" stm-action="saddle.unregister"
                                 stm-confirm="Remove this saddle? A running task is re-queued.">Remove</button>
                     </form>
                 </section>
                 <section class="card" stm-if="saddles.saddles.length == 0">
-                    <p class="muted">No saddles registered. The dispatcher pairs queued tasks with idle saddles automatically.</p>
+                    <p class="text-muted">No saddles registered. The dispatcher pairs queued tasks with idle saddles automatically.</p>
                 </section>
             </div>
 
             <h3>Capabilities</h3>
-            <p class="muted">Tags the dispatcher routes by: a task is only assigned to a saddle carrying every
+            <p class="text-muted">Tags the dispatcher routes by: a task is only assigned to a saddle carrying every
                 tag the task requires. Manage a saddle's tags on its card above; require tags on a task's page.</p>
             <details stm-if="saddles.is_admin">
                 <summary><strong>New capability</strong></summary>
-                <form stm-action="saddles.create_capability" class="inline">
+                <form stm-action="saddles.create_capability" class="cluster">
                     <label for="new-capability">Name</label>
                     <input type="text" id="new-capability" name="name" required placeholder="gpu">
                     <button type="submit">Create capability</button>
                 </form>
             </details>
-            <table>
+            <div class="table-wrap">
+                <table>
+
                 <thead><tr><th>Tag</th><th>Saddles carrying it</th><th>Tasks requiring it</th><th></th></tr></thead>
                 <tbody>
                     <tr stm-for-capability-in="saddles.capabilities">
@@ -132,10 +134,12 @@
                                     stm-confirm="Delete this capability everywhere?">Delete</button></td>
                     </tr>
                     <tr stm-if="saddles.capabilities.length == 0">
-                        <td colspan="4" class="muted">No capabilities defined yet.</td>
+                        <td colspan="4" class="text-muted">No capabilities defined yet.</td>
                     </tr>
                 </tbody>
-            </table>
+            
+                </table>
+            </div>
         </div>
     </section>
 </body>

+ 9 - 9
src/pages/stack_detail.html

@@ -7,24 +7,24 @@
 <body>
     <section>
         <h2 stm-if="stack != null && stack.exists" stm-text="stack.name">Stack</h2>
-        <p stm-if="stack != null && !stack.exists" class="muted">Stack not found.</p>
+        <p stm-if="stack != null && !stack.exists" class="text-muted">Stack not found.</p>
 
         <div stm-if="stack != null && stack.exists">
             <p><a href="/stacks">All stacks</a></p>
             <div class="cards">
                 <section class="card">
                     <h3>Details</h3>
-                    <p class="muted" stm-if="stack.summary.length > 0" stm-text="stack.summary">summary</p>
-                    <p class="muted">Created <span stm-text="stack.created_display">when</span></p>
+                    <p class="text-muted" stm-if="stack.summary.length > 0" stm-text="stack.summary">summary</p>
+                    <p class="text-muted">Created <span stm-text="stack.created_display">when</span></p>
                 </section>
 
                 <section class="card">
                     <h3>Edit</h3>
                     <form stm-action="stack.update">
                         <input type="hidden" name="stack_id" stm-attribute.value="stack.id">
-                        <div class="form-row"><label>Name</label>
+                        <div class="field"><label>Name</label>
                             <input type="text" name="name" stm-attribute.value="stack.name"></div>
-                        <div class="form-row"><label>Summary</label>
+                        <div class="field"><label>Summary</label>
                             <input type="text" name="summary" stm-attribute.value="stack.summary"></div>
                         <button type="submit">Save</button>
                     </form>
@@ -32,7 +32,7 @@
             </div>
 
             <h3>Projects in this stack</h3>
-            <p class="muted">Applying the stack adds each of these to a project's or a goal's
+            <p class="text-muted">Applying the stack adds each of these to a project's or a goal's
                 linked projects with the relationship shown here.</p>
             <table>
                 <thead><tr><th>Project</th><th>Git URL</th><th>Relationship</th><th></th></tr></thead>
@@ -43,10 +43,10 @@
                         <td><span stm-attribute.class="'badge badge-' + member.relationship" stm-text="member.relationship">relationship</span></td>
                         <td><button type="button" stm-action="member.remove" stm-confirm="Remove this project from the stack?">Remove</button></td>
                     </tr>
-                    <tr stm-if="stack.members.length == 0"><td colspan="4" class="muted">No projects in this stack yet.</td></tr>
+                    <tr stm-if="stack.members.length == 0"><td colspan="4" class="text-muted">No projects in this stack yet.</td></tr>
                 </tbody>
             </table>
-            <form class="inline" stm-action="stack.add_member" stm-if="stack.options.length > 0">
+            <form class="cluster" stm-action="stack.add_member" stm-if="stack.options.length > 0">
                 <label for="member-target">Add project</label>
                 <select id="member-target" name="project_id" required>
                     <option stm-for-option-in="stack.options" stm-attribute.value="option.id" stm-text="option.name">project</option>
@@ -58,7 +58,7 @@
                 <button type="submit">Add</button>
             </form>
 
-            <form class="inline">
+            <form class="cluster">
                 <button type="button" stm-action="stack.delete" stm-confirm="Delete this stack? Its projects keep any links already applied.">Delete stack</button>
             </form>
         </div>

+ 12 - 8
src/pages/stacks.html

@@ -7,7 +7,7 @@
 <body>
     <section>
         <h2>Stacks</h2>
-        <p class="muted">Named bundles of reference projects — apply one to a project or a
+        <p class="text-muted">Named bundles of reference projects — apply one to a project or a
             goal to attach its whole collection of links in a single click.</p>
         <p stm-if="stacks == null">Loading…</p>
 
@@ -15,27 +15,31 @@
             <details>
                 <summary><strong>New stack</strong></summary>
                 <form stm-action="stacks.create">
-                    <div class="form-row"><label>Name</label>
+                    <div class="field"><label>Name</label>
                         <input type="text" name="name" required placeholder="Web stack references"></div>
-                    <div class="form-row"><label>Summary <span class="muted">(optional)</span></label>
+                    <div class="field"><label>Summary <span class="text-muted">(optional)</span></label>
                         <input type="text" name="summary" placeholder="What this collection is for"></div>
                     <button type="submit">Create stack</button>
                 </form>
             </details>
 
-            <table stm-if="stacks.stacks.length > 0">
+            <div class="table-wrap">
+                <table stm-if="stacks.stacks.length >
+ 0">
                 <thead><tr><th>#</th><th>Name</th><th>Projects</th><th>Summary</th><th>Created</th></tr></thead>
                 <tbody>
                     <tr stm-for-stack-in="stacks.stacks">
                         <td stm-text="stack.id">id</td>
                         <td><a stm-attribute.href="'/stacks/' + stack.id" stm-text="stack.name">name</a></td>
                         <td stm-text="stack.member_count">0</td>
-                        <td class="muted" stm-text="stack.summary">summary</td>
-                        <td class="muted" stm-text="stack.created_display">when</td>
+                        <td class="text-muted" stm-text="stack.summary">summary</td>
+                        <td class="text-muted" stm-text="stack.created_display">when</td>
                     </tr>
                 </tbody>
-            </table>
-            <p stm-if="stacks.stacks.length == 0" class="muted">No stacks yet.</p>
+            
+                </table>
+            </div>
+            <p stm-if="stacks.stacks.length == 0" class="text-muted">No stacks yet.</p>
         </div>
     </section>
 </body>

+ 56 - 56
src/pages/task_detail.html

@@ -60,7 +60,7 @@
     <section>
         <h2 stm-if="task != null && task.exists">#<span stm-text="task.id">id</span> —
             <span stm-text="task.title">title</span></h2>
-        <p stm-if="task != null && !task.exists" class="muted">Task not found.</p>
+        <p stm-if="task != null && !task.exists" class="text-muted">Task not found.</p>
 
         <div stm-if="task != null && task.exists">
             <!-- Goal 8 task 4: a standalone task (no project) shows
@@ -73,7 +73,7 @@
                        stm-text="'#' + task.enqueue_after_task_id">after</a></span></p>
             <p stm-if="task.project_id == 0"><a href="/tasks">All tasks</a> ·
                 <strong>(standalone)</strong>
-                <span class="muted">— no project: the agent works in its own ephemeral
+                <span class="text-muted">— no project: the agent works in its own ephemeral
                     workspace and delivers its completion report</span></p>
 
             <!-- Progress feed ABOVE the status card (plan 11 task D):
@@ -81,7 +81,7 @@
                  behind the "Show history" expander. -->
             <h3>Progress</h3>
             <pstm-fragment name="progress-feed" entries="task.progress"/>
-            <p stm-if="task.progress.length == 0" class="muted">No progress reports yet — the agent reports via
+            <p stm-if="task.progress.length == 0" class="text-muted">No progress reports yet — the agent reports via
                 <code>saddle progress-update</code>.</p>
 
             <!-- Status + description side by side in a 50/50 grid (plan
@@ -97,29 +97,29 @@
                     <p><span stm-attribute.class="'badge badge-' + task.status" stm-text="task.display">status</span>
                         <span stm-attribute.class="'badge badge-mode-' + task.mode" stm-text="task.mode">mode</span>
                         · priority <strong stm-text="task.priority">5</strong></p>
-                    <p class="muted" stm-if="task.enqueue_after_task_id > 0">Auto-enqueues after
+                    <p class="text-muted" stm-if="task.enqueue_after_task_id > 0">Auto-enqueues after
                         <a stm-attribute.href="'/tasks/' + task.enqueue_after_task_id"
                            stm-text="'#' + task.enqueue_after_task_id + ' ' + task.enqueue_after_title">after</a></p>
-                    <p class="muted">Created <span stm-text="task.created_display">when</span></p>
-                    <p class="muted" stm-if="task.started_display.length > 0">Started <span stm-text="task.started_display">when</span></p>
-                    <p class="muted" stm-if="task.completed_display.length > 0">Completed <span stm-text="task.completed_display">when</span></p>
+                    <p class="text-muted">Created <span stm-text="task.created_display">when</span></p>
+                    <p class="text-muted" stm-if="task.started_display.length > 0">Started <span stm-text="task.started_display">when</span></p>
+                    <p class="text-muted" stm-if="task.completed_display.length > 0">Completed <span stm-text="task.completed_display">when</span></p>
                     <!-- S5: the aggregate LLM usage stat line (empty for
                          zero-usage tasks renders nothing). -->
-                    <p class="muted" stm-if="task.usage_line.length > 0"
+                    <p class="text-muted" stm-if="task.usage_line.length > 0"
                        stm-text="task.usage_line">LLM: 1,234 calls · 45.2M in / 1.1M out tok · 3h 12m</p>
                     <p stm-if="task.task_branch.length > 0">Branch: <strong stm-text="task.task_branch">branch</strong>
-                        <span class="muted" stm-if="task.branch_from.length > 0"> from <span stm-text="task.branch_from">base</span></span></p>
-                    <p stm-if="task.status == 'suspended'" class="muted">Parked (`saddle suspend`) —
+                        <span class="text-muted" stm-if="task.branch_from.length > 0"> from <span stm-text="task.branch_from">base</span></span></p>
+                    <p stm-if="task.status == 'suspended'" class="text-muted">Parked (`saddle suspend`) —
                         the goal agent resumes on its goal's task events, with the
                         batched status report riding the resume.</p>
-                    <form class="inline">
+                    <form class="cluster">
                         <input type="hidden" name="task_id" stm-attribute.value="task.id">
-                        <button type="button" stm-if="task.draft" class="primary"
+                        <button type="button" stm-if="task.draft" class="btn-primary"
                                 stm-action="task.enqueue">Enqueue task</button>
                         <button type="button" stm-if="task.draft"
                                 stm-action="task.delete"
                                 stm-confirm="Delete this draft task? This cannot be undone.">Delete</button>
-                        <button type="button" stm-if="task.status == 'failed'" class="primary"
+                        <button type="button" stm-if="task.status == 'failed'" class="btn-primary"
                                 stm-action="task.retry">Retry task</button>
                         <button type="button" stm-if="task.resume_available"
                                 stm-action="task.resume"
@@ -133,11 +133,11 @@
                         <button type="button" stm-if="!task.draft && !task.terminal"
                                 stm-action="task.cancel" stm-confirm="Cancel this task?">Cancel task</button>
                     </form>
-                    <form class="inline">
+                    <form class="cluster">
                         <input type="hidden" name="task_id" stm-attribute.value="task.id">
                         <button type="button" stm-action="task.toggle_notify"
                                 stm-text="task.notify_me ? 'Don&#39;t notify me' : 'Notify me'">Notify me</button>
-                        <span class="muted">— this task&#39;s first start, failure and completion
+                        <span class="text-muted">— this task&#39;s first start, failure and completion
                             (questions and reviews always notify subscribed users)</span>
                     </form>
                 </section>
@@ -149,7 +149,7 @@
                     <p><span stm-attribute.class="'badge badge-' + task.goal_status"
                              stm-text="task.goal_status.replace('_', ' ')">status</span>
                         <a stm-attribute.href="'/goals/' + task.goal_id" stm-text="task.goal_title">goal</a></p>
-                    <p class="muted">Goal branch <code stm-text="task.goal_branch">branch</code>
+                    <p class="text-muted">Goal branch <code stm-text="task.goal_branch">branch</code>
                         <span stm-if="task.is_goal_run">— this task IS the goal agent's run
                             (orchestrate mode; the goal page owns its lifecycle)</span>
                         <span stm-if="!task.is_goal_run">— this task runs under the goal agent's
@@ -166,11 +166,11 @@
                 <section class="card" data-long-form data-field="task"
                          data-lf-open="0">
                     <h3>Mandate
-                        <button type="button" class="inline" stm-if="task.editable_all"
+                        <button type="button" class="cluster" stm-if="task.editable_all"
                                 data-lf-toggle>Edit</button></h3>
                     <div data-lf-view>
                         <div stm-if="task.description.length > 0" class="md" stm-markdown="task.description">description</div>
-                        <p class="muted" stm-if="task.description.length == 0">No mandate.</p>
+                        <p class="text-muted" stm-if="task.description.length == 0">No mandate.</p>
                     </div>
                     <!-- The stm-if wrapper (not the form itself) owns
                          draft-gating: statum toggles `hidden` on
@@ -179,7 +179,7 @@
                     <div stm-if="task.editable_all">
                         <form data-lf-edit hidden stm-action="task.update">
                             <input type="hidden" name="task_id" stm-attribute.value="task.id">
-                            <div class="form-row"><label>Mandate</label>
+                            <div class="field"><label>Mandate</label>
                                 <textarea name="description" rows="8"
                                           data-lf-src="task.description">description</textarea></div>
                             <button type="submit">Save</button>
@@ -211,37 +211,37 @@
                  properties only. -->
             <details class="edit-expander" stm-if="task.editable_all || task.editable_priority">
                 <summary><strong>Edit task</strong>
-                    <span class="muted" stm-if="task.editable_all">— every field except status while in draft</span>
-                    <span class="muted" stm-if="!task.editable_all && task.editable_priority">— priority only once work has begun</span></summary>
+                    <span class="text-muted" stm-if="task.editable_all">— every field except status while in draft</span>
+                    <span class="text-muted" stm-if="!task.editable_all && task.editable_priority">— priority only once work has begun</span></summary>
                 <form stm-action="task.update" stm-if="task.editable_all">
                     <input type="hidden" name="task_id" stm-attribute.value="task.id">
-                    <div class="form-row"><label>Title</label>
+                    <div class="field"><label>Title</label>
                         <input type="text" name="title" stm-attribute.value="task.title"></div>
-                    <div class="form-row" stm-if="!task.is_goal_run"><label>Mode</label>
+                    <div class="field" stm-if="!task.is_goal_run"><label>Mode</label>
                         <select name="mode" stm-attribute.value="task.mode">
                             <option value="implement">implement — build the change on a task branch</option>
                             <option value="investigate">investigate — explore and report, no code changes</option>
                             <option value="review">review — read and critique existing work</option>
                             <option value="test">test — write and run tests</option>
                         </select></div>
-                    <div class="form-row" stm-if="task.creator == 'human' && !task.is_goal_run"><label>Auto-enqueue after
-                            <span class="muted">(enqueues itself when the predecessor is done; none clears it)</span></label>
+                    <div class="field" stm-if="task.creator == 'human' && !task.is_goal_run"><label>Auto-enqueue after
+                            <span class="text-muted">(enqueues itself when the predecessor is done; none clears it)</span></label>
                         <select name="enqueue_after_task_id">
                             <option value="">none</option>
                             <option stm-for-option-in="task.enqueue_after_options" stm-attribute.value="option.id"
                                      stm-text="'#' + option.id + ' ' + option.title">task</option>
                         </select></div>
-                    <div class="form-row"><label>Priority (1–9)</label>
+                    <div class="field"><label>Priority (1–9)</label>
                         <input type="number" name="priority" min="1" max="9" stm-attribute.value="task.priority"></div>
-                    <div class="form-row"><label>Task branch</label>
+                    <div class="field"><label>Task branch</label>
                         <input type="text" name="task_branch" stm-attribute.value="task.task_branch" placeholder="task/<id>-<slug>"></div>
-                    <div class="form-row"><label>Branch from</label>
+                    <div class="field"><label>Branch from</label>
                         <input type="text" name="branch_from" stm-attribute.value="task.branch_from"></div>
                     <button type="submit">Save</button>
                 </form>
                 <form stm-action="task.update" stm-if="!task.editable_all && task.editable_priority">
                     <input type="hidden" name="task_id" stm-attribute.value="task.id">
-                    <div class="form-row"><label>Priority (1–9)</label>
+                    <div class="field"><label>Priority (1–9)</label>
                         <input type="number" name="priority" min="1" max="9" stm-attribute.value="task.priority"></div>
                     <button type="submit">Save</button>
                 </form>
@@ -249,7 +249,7 @@
 
 <!-- Current blocks render in full (plan 11 task D);
                  answered ones fold behind the history expander. -->
-            <h3>Blocks <span class="muted" stm-if="task.open_blocks.length > 0">(<span stm-text="task.open_blocks.length">0</span> open)</span></h3>
+            <h3>Blocks <span class="text-muted" stm-if="task.open_blocks.length > 0">(<span stm-text="task.open_blocks.length">0</span> open)</span></h3>
             <div stm-for-block-in="task.open_blocks" stm-key="block.id"
                  stm-attribute.class="'block-card ' + block.block_type + (block.answered ? ' answered' : '')">
                 <div stm-if="!block.answered && block.block_type == 'review'" class="review-banner">
@@ -259,33 +259,33 @@
                         <a stm-if="task.review_diff_url.length > 0" stm-attribute.href="task.review_diff_url"
                            target="_blank" rel="noopener">· compare changes</a></p>
                      <div class="actions">
-                         <form stm-action="block.answer_action" class="inline">
+                         <form stm-action="block.answer_action" class="cluster">
                              <input type="hidden" name="verdict" value="accept">
-                             <button type="submit" class="approve">Approve</button>
+                             <button type="submit" class="btn-primary">Approve</button>
                          </form>
-                        <form stm-action="block.answer_action" class="inline">
+                        <form stm-action="block.answer_action" class="cluster">
                             <textarea name="feedback" rows="2" placeholder="What should change?"></textarea>
-                            <button type="submit" class="reject">Request changes</button>
+                            <button type="submit" class="btn-danger">Request changes</button>
                         </form>
                     </div>
                 </div>
 
                 <p><span stm-attribute.class="'badge badge-' + block.block_type" stm-text="block.block_type">type</span>
-                    <span class="muted" stm-text="block.created_display">when</span></p>
+                    <span class="text-muted" stm-text="block.created_display">when</span></p>
 
                 <div stm-if="block.show_body && block.body.length > 0" class="md rfc-body"
                      stm-markdown="block.body">body</div>
 
                 <div stm-if="block.answered">
                     <p>Answer: <strong stm-text="block.answer">answer</strong>
-                        <span class="muted" stm-text="block.answered_display">when</span>
+                        <span class="text-muted" stm-text="block.answered_display">when</span>
                         <span class="badge" stm-if="block.answered_by == 'auto'">auto</span></p>
                 </div>
 
                 <pstm-fragment name="block-answer"/>
                 <pstm-fragment name="block-reply"/>
             </div>
-            <p stm-if="task.open_blocks.length == 0 && task.answered_blocks.length == 0" class="muted">No blocks — the agent has no open questions.</p>
+            <p stm-if="task.open_blocks.length == 0 && task.answered_blocks.length == 0" class="text-muted">No blocks — the agent has no open questions.</p>
 
             <!-- Historical (answered) blocks behind an expander, the
                  current ones in full above (plan 11 task D). -->
@@ -295,11 +295,11 @@
                      stm-attribute.class="'block-card answered ' + block.block_type">
                     <p><span stm-attribute.class="'badge badge-' + block.block_type" stm-text="block.block_type">type</span>
                         <span stm-text="block.question">question</span>
-                        <span class="muted" stm-text="block.created_display">when</span></p>
+                        <span class="text-muted" stm-text="block.created_display">when</span></p>
                     <div stm-if="block.show_body && block.body.length > 0" class="md rfc-body"
                          stm-markdown="block.body">body</div>
                     <p>Answer: <strong stm-text="block.answer">answer</strong>
-                        <span class="muted" stm-text="block.answered_display">when</span>
+                        <span class="text-muted" stm-text="block.answered_display">when</span>
                         <span class="badge" stm-if="block.answered_by == 'auto'">auto</span></p>
                 </div>
             </details>
@@ -311,11 +311,11 @@
             <div stm-for-comment-in="task.comments" stm-attribute.class="'comment' + (comment.from_agent ? ' agent' : '') + (comment.is_system ? ' system' : '')">
                 <strong stm-if="!comment.is_system" stm-text="comment.author">author</strong>
                 <span stm-if="comment.is_system" class="system-label">system</span>
-                <span class="muted" stm-text="comment.when_display">when</span>
+                <span class="text-muted" stm-text="comment.when_display">when</span>
                 <div class="md" stm-markdown="comment.body">body</div>
                 <blockquote stm-if="comment.quote.length > 0" stm-text="comment.quote">quote</blockquote>
             </div>
-            <p stm-if="task.comments.length == 0" class="muted">No comments yet.</p>
+            <p stm-if="task.comments.length == 0" class="text-muted">No comments yet.</p>
             <!-- Goal 8 task 4: comments stay OPEN on terminal tasks.
                  A terminal NON-GOAL task resurrects on comment (it
                  re-queues with the comment injected); a terminal
@@ -323,14 +323,14 @@
                  voice — the hint says so and the service refuses. -->
             <form stm-if="!(task.terminal && task.goal_id > 0)" stm-action="task.comment">
                 <input type="hidden" name="task_id" stm-attribute.value="task.id">
-                <div class="form-row"><label for="comment-body">Add comment</label>
+                <div class="field"><label for="comment-body">Add comment</label>
                     <textarea id="comment-body" name="body" rows="3" placeholder="A comment for the agent"></textarea></div>
-                <p stm-if="task.terminal && task.goal_id == 0" class="muted">
+                <p stm-if="task.terminal && task.goal_id == 0" class="text-muted">
                     This task is terminal — commenting re-queues it for another round
                     with your comment injected.</p>
-                <button type="submit" class="primary">Comment</button>
+                <button type="submit" class="btn-primary">Comment</button>
             </form>
-            <p stm-if="task.terminal && task.goal_id > 0" class="muted">
+            <p stm-if="task.terminal && task.goal_id > 0" class="text-muted">
                 Comments are closed — this task belongs to its goal; the goal agent
                 (its goal's page) owns the conversation.</p>
 
@@ -345,22 +345,22 @@
                     <input type="hidden" name="quote">
                     <input type="hidden" name="return_to">
                     <textarea name="body" rows="4" placeholder="Your comment…" style="width:100%; margin:0.5em 0;"></textarea>
-                    <button type="submit" class="primary">Send comment</button>
+                    <button type="submit" class="btn-primary">Send comment</button>
                     <button type="button" onclick="document.getElementById('quote-comment-dialog').close()">Cancel</button>
                 </form>
             </dialog>
 
             <h3>Rules</h3>
-            <p class="muted">Task rules lead the effective set dispatched with this task.</p>
+            <p class="text-muted">Task rules lead the effective set dispatched with this task.</p>
             <div stm-for-rule-in="task.rules" stm-key="rule.link_id" class="card">
                 <h3 stm-text="rule.name">name</h3>
                 <div class="md" stm-markdown="rule.body">body</div>
-                <form class="inline">
+                <form class="cluster">
                     <button type="button" stm-action="rule.detach" stm-confirm="Detach this rule?">Detach</button>
                 </form>
             </div>
-            <p stm-if="task.rules.length == 0" class="muted">No task-specific rules.</p>
-            <form class="inline" stm-action="task.attach_rule" stm-if="task.rule_options.length > 0">
+            <p stm-if="task.rules.length == 0" class="text-muted">No task-specific rules.</p>
+            <form class="cluster" stm-action="task.attach_rule" stm-if="task.rule_options.length > 0">
                 <label for="task-rule-target">Attach rule</label>
                 <select id="task-rule-target" name="rule_id" required>
                     <option stm-for-option-in="task.rule_options" stm-attribute.value="option.id"
@@ -370,16 +370,16 @@
             </form>
 
             <h3>Required capabilities</h3>
-            <p class="muted">The dispatcher only assigns this task to a saddle carrying all of these tags.</p>
+            <p class="text-muted">The dispatcher only assigns this task to a saddle carrying all of these tags.</p>
             <p stm-if="task.capabilities.length > 0">
                 <span stm-for-capability-in="task.capabilities" stm-key="capability.link_id">
                     <span class="badge"><span stm-text="capability.name">tag</span>
-                        <button type="button" class="inline chip-remove"
+                        <button type="button" class="btn-ghost chip-remove"
                                 stm-action="capability.detach" stm-confirm="Remove this requirement?">×</button></span>
                 </span>
             </p>
-            <p stm-if="task.capabilities.length == 0" class="muted">No requirements — any idle saddle may run it.</p>
-            <form class="inline" stm-action="task.attach_capability" stm-if="task.capability_options.length > 0">
+            <p stm-if="task.capabilities.length == 0" class="text-muted">No requirements — any idle saddle may run it.</p>
+            <form class="cluster" stm-action="task.attach_capability" stm-if="task.capability_options.length > 0">
                 <label for="task-capability-target">Require capability</label>
                 <select id="task-capability-target" name="capability_id" required>
                     <option stm-for-option-in="task.capability_options" stm-attribute.value="option.id"
@@ -389,7 +389,7 @@
             </form>
 
             <h3>Sessions</h3>
-            <p stm-if="task.saddle_health.length > 0" class="muted"
+            <p stm-if="task.saddle_health.length > 0" class="text-muted"
                stm-text="task.saddle_health">saddle health</p>
             <table>
                 <thead><tr><th>#</th><th>Saddle</th><th>Started</th><th>Ended</th><th>Outcome</th><th></th></tr></thead>
@@ -404,7 +404,7 @@
                         <td><a stm-attribute.href="'/tasks/' + task.id + '/log'"
                                target="_blank" rel="noopener">Event log</a></td>
                     </tr>
-                    <tr stm-if="task.sessions.length == 0"><td colspan="6" class="muted">No sessions yet.</td></tr>
+                    <tr stm-if="task.sessions.length == 0"><td colspan="6" class="text-muted">No sessions yet.</td></tr>
                 </tbody>
             </table>
         </div>

+ 3 - 3
src/pages/task_log.html

@@ -8,12 +8,12 @@
 <body>
     <div class="page log-page">
         <p class="log-back"><a href="/tasks">&larr; Tasks</a></p>
-        <div stm-if="!task_log.exists" class="muted">No task exists with this id.</div>
+        <div stm-if="!task_log.exists" class="text-muted">No task exists with this id.</div>
         <div stm-if="task_log.exists">
             <header class="log-header">
                 <div class="log-title">
                     <h2 data-log-title>Event log</h2>
-                    <p class="muted">the unified event ledger · newest at the bottom ·
+                    <p class="text-muted">the unified event ledger · newest at the bottom ·
                         loads older history on demand</p>
                 </div>
                 <div class="log-actions">
@@ -34,7 +34,7 @@
                 </div>
                 <div class="log-feed" data-log-feed>
                     <button type="button" class="log-more" data-log-more hidden>Load older events</button>
-                    <p class="muted log-empty">Waiting for the first events…</p>
+                    <p class="text-muted log-empty">Waiting for the first events…</p>
                 </div>
             </section>
         </div>

+ 5 - 5
src/pages/task_log.js

@@ -410,7 +410,7 @@
                                 ? compact : JSON.stringify(extras, null, 2)));
                         } else {
                             var empty = document.createElement('p');
-                            empty.className = 'muted';
+                            empty.className = 'text-muted';
                             empty.textContent = '(no output)';
                             div.appendChild(empty);
                         }
@@ -595,7 +595,7 @@
                 pre.className = 'event-pre event-command';
                 if (bits && bits.length > 0) {
                     var tail = document.createElement('span');
-                    tail.className = 'muted';
+                    tail.className = 'text-muted';
                     tail.textContent = '  \u00b7 ' + bits.join(' \u00b7 ');
                     pre.appendChild(tail);
                 }
@@ -957,7 +957,7 @@
                 badge.textContent = String(evt.kind || '').replace(/_/g, ' ');
                 head.appendChild(badge);
                 var actor = document.createElement('span');
-                actor.className = 'muted';
+                actor.className = 'text-muted';
                 actor.textContent = ' ' + (evt.actor || '') + ' \u00b7 '
                     + (evt.created_display || '');
                 head.appendChild(actor);
@@ -1007,7 +1007,7 @@
                     var quote = document.createElement('blockquote');
                     if (p.quote) {
                         var q = document.createElement('p');
-                        q.className = 'muted';
+                        q.className = 'text-muted';
                         q.textContent = '> ' + p.quote;
                         quote.appendChild(q);
                     }
@@ -1227,7 +1227,7 @@
                 var visibleRows = state.events.filter(visible);
                 if (visibleRows.length === 0) {
                     var empty = document.createElement('p');
-                    empty.className = 'muted log-empty';
+                    empty.className = 'text-muted log-empty';
                     empty.textContent = state.events.length === 0
                         ? 'Waiting for the first events\u2026'
                         : 'No events match the filters.';

+ 26 - 22
src/pages/tasks.html

@@ -59,7 +59,7 @@
     <div class="page">
         <h1>Tasks</h1>
 
-        <form class="inline" onsubmit="return false">
+        <form class="cluster" onsubmit="return false">
             <label for="filter-status">Status</label>
             <select id="filter-status" onchange="window.setTaskFilter('status', this.value)">
                 <option value="all">all</option>
@@ -85,13 +85,13 @@
                 <option stm-for-goal-in="tasks.goal_options" stm-attribute.value="goal.id"
                          stm-text="goal.title">goal</option>
             </select>
-            <span class="muted">— filters apply instantly</span>
+            <span class="text-muted">— filters apply instantly</span>
         </form>
 
         <details class="history-expander">
             <summary><strong>New task</strong></summary>
             <form stm-action="tasks.create">
-                <div class="form-row"><label for="task-project">Project</label>
+                <div class="field"><label for="task-project">Project</label>
                     <!-- Goal 8 task 4: "No project" posts project_id 0 —
                          a STANDALONE task with its own ephemeral
                          workspace; the completion report is the
@@ -101,27 +101,27 @@
                         <option stm-for-project-in="tasks.projects" stm-attribute.value="project.id"
                                  stm-text="project.name">project</option>
                     </select></div>
-                <div class="form-row"><label for="task-title">Title</label>
+                <div class="field"><label for="task-title">Title</label>
                     <input type="text" id="task-title" name="title" required></div>
-                <div class="form-row"><label for="task-mode">Mode</label>
+                <div class="field"><label for="task-mode">Mode</label>
                     <select id="task-mode" name="mode">
                         <option value="implement" selected>implement — build the change on a task branch</option>
                         <option value="investigate">investigate — explore and report, no code changes</option>
                         <option value="review">review — read and critique existing work</option>
                         <option value="test">test — write and run tests</option>
                     </select></div>
-                <div class="form-row"><label for="task-description">Mandate</label>
+                <div class="field"><label for="task-description">Mandate</label>
                     <textarea id="task-description" name="description" rows="4"
                               placeholder="What the agent must accomplish (the mandate)"></textarea></div>
-                <div class="form-row"><label for="task-branch">Task branch name
-                        <span class="muted">(optional — empty lets the system name the branch; goal worker tasks auto-name task/&lt;goal-id&gt;-&lt;task-id&gt;)</span></label>
+                <div class="field"><label for="task-branch">Task branch name
+                        <span class="text-muted">(optional — empty lets the system name the branch; goal worker tasks auto-name task/&lt;goal-id&gt;-&lt;task-id&gt;)</span></label>
                     <input type="text" id="task-branch" name="task_branch"
                            placeholder="task/&lt;id&gt;-&lt;slug&gt;"></div>
-                <div class="form-row"><label for="task-branch-from">Branch from
-                        <span class="muted">(default: the project's default branch)</span></label>
+                <div class="field"><label for="task-branch-from">Branch from
+                        <span class="text-muted">(default: the project's default branch)</span></label>
                     <input type="text" id="task-branch-from" name="branch_from"
                            placeholder="main"></div>
-                <div class="form-row"><label for="task-priority">Priority</label>
+                <div class="field"><label for="task-priority">Priority</label>
                     <select id="task-priority" name="priority">
                         <option value="1">1 — highest</option>
                         <option value="2">2</option>
@@ -133,27 +133,29 @@
                         <option value="8">8</option>
                         <option value="9">9 — lowest</option>
                     </select></div>
-                <div class="form-row"><label for="task-enqueue-after">Auto-enqueue after
-                        <span class="muted">(optional — enqueues itself when the predecessor is done)</span></label>
+                <div class="field"><label for="task-enqueue-after">Auto-enqueue after
+                        <span class="text-muted">(optional — enqueues itself when the predecessor is done)</span></label>
                     <select id="task-enqueue-after" name="enqueue_after_task_id">
                         <option value="">none</option>
                         <option stm-for-option-in="tasks.enqueue_after_options"
                                  stm-attribute.value="option.id" stm-text="option.name">task</option>
                     </select></div>
-                <div class="form-row" stm-if="tasks.capability_options.length > 0">
+                <div class="field" stm-if="tasks.capability_options.length > 0">
                     <label for="task-capabilities">Capabilities
-                        <span class="muted">(the saddle fleet must carry them until assigned)</span></label>
+                        <span class="text-muted">(the saddle fleet must carry them until assigned)</span></label>
                     <select id="task-capabilities" name="required_capabilities" multiple
                             size="3">
                         <option stm-for-capability-in="tasks.capability_options"
                                  stm-attribute.value="capability.id"
                                  stm-text="capability.name">cap</option>
                     </select></div>
-                <button type="submit" class="primary">Create task</button>
+                <button type="submit" class="btn-primary">Create task</button>
             </form>
         </details>
 
-        <table>
+        <div class="table-wrap">
+            <table>
+
             <thead>
                 <tr><th>#</th><th>Title</th><th>Status</th><th>Mode</th><th>Goal</th>
                     <th>Priority</th><th>Project</th><th>Capabilities</th></tr>
@@ -168,18 +170,20 @@
                     <td><span stm-attribute.class="'badge badge-mode-' + task.mode" stm-text="task.mode">mode</span></td>
                     <td stm-if="task.goal_id > 0">
                         <a stm-attribute.href="'/goals/' + task.goal_id" stm-text="task.goal_title">goal</a>
-                        <span class="muted" stm-if="task.creator == 'goal'">(agent)</span>
+                        <span class="text-muted" stm-if="task.creator == 'goal'">(agent)</span>
                     </td>
-                    <td stm-if="task.goal_id == 0" class="muted">—</td>
+                    <td stm-if="task.goal_id == 0" class="text-muted">—</td>
                     <td stm-text="task.priority">5</td>
                     <td stm-text="task.project_name">project</td>
                     <td><span stm-if="task.capabilities.length > 0"
                               stm-text="task.capabilities.join(', ')">caps</span>
-                        <span class="muted" stm-if="task.capabilities.length == 0">—</span></td>
+                        <span class="text-muted" stm-if="task.capabilities.length == 0">—</span></td>
                 </tr>
-                <tr stm-if="tasks.tasks.length == 0"><td colspan="8" class="muted">No tasks.</td></tr>
+                <tr stm-if="tasks.tasks.length == 0"><td colspan="8" class="text-muted">No tasks.</td></tr>
             </tbody>
-        </table>
+        
+            </table>
+        </div>
 
     </div>
 </body>

+ 28 - 24
src/pages/users.html

@@ -7,7 +7,7 @@
 <body>
     <section>
         <h2>Users</h2>
-        <p class="muted">Accounts and their roles. Normal users manage projects, tasks,
+        <p class="text-muted">Accounts and their roles. Normal users manage projects, tasks,
             goals and rules; admins additionally manage users, saddles and capabilities.</p>
         <p stm-if="users == null">Loading…</p>
 
@@ -15,27 +15,29 @@
             <details>
                 <summary><strong>New user</strong></summary>
                 <form stm-action="users.create">
-                    <div class="form-row"><label>Username</label>
+                    <div class="field"><label>Username</label>
                         <input type="text" name="username" required placeholder="ada"></div>
-                    <div class="form-row"><label>Email</label>
+                    <div class="field"><label>Email</label>
                         <input type="email" name="email" required placeholder="ada@example.com"></div>
-                    <div class="form-row"><label>Forename</label>
+                    <div class="field"><label>Forename</label>
                         <input type="text" name="forename" required placeholder="Ada"></div>
-                    <div class="form-row"><label>Surname</label>
+                    <div class="field"><label>Surname</label>
                         <input type="text" name="surname" required placeholder="Lovelace"></div>
-                    <div class="form-row"><label>Date of birth</label>
+                    <div class="field"><label>Date of birth</label>
                         <input type="text" name="date_of_birth" required placeholder="1990-01-31"></div>
-                    <div class="form-row"><label>Password</label>
+                    <div class="field"><label>Password</label>
                         <input type="password" name="password" required placeholder="At least 6 characters"></div>
-                    <div class="form-row"><label>Confirm password</label>
+                    <div class="field"><label>Confirm password</label>
                         <input type="password" name="confirm_password" required></div>
-                    <div class="form-row"><label>Role</label>
-                        <label class="inline"><input type="checkbox" name="admin" value="1"> Administrator</label></div>
+                    <div class="field"><label>Role</label>
+                        <label class="choice"><input type="checkbox" name="admin" value="1"> Administrator</label></div>
                     <button type="submit">Create user</button>
                 </form>
             </details>
 
-            <table>
+            <div class="table-wrap">
+                <table>
+
                 <thead><tr>
                     <th>Username</th><th>Email</th><th>Name</th><th>Role</th>
                     <th>Enabled</th><th>Created</th><th></th>
@@ -49,32 +51,32 @@
                                   stm-text="user.is_admin ? 'admin' : 'user'">role</span></td>
                         <td><span stm-attribute.class="user.enabled ? 'badge badge-done' : 'badge badge-offline'"
                                   stm-text="user.enabled ? 'enabled' : 'disabled'">enabled</span></td>
-                        <td class="muted" stm-text="user.created_display">when</td>
+                        <td class="text-muted" stm-text="user.created_display">when</td>
                         <td>
                             <details>
                                 <summary>Edit</summary>
                                 <form stm-action="user.alter">
-                                    <div class="form-row"><label>Username</label>
+                                    <div class="field"><label>Username</label>
                                         <input type="text" name="username" required stm-attribute.value="user.username"></div>
-                                    <div class="form-row"><label>Email</label>
+                                    <div class="field"><label>Email</label>
                                         <input type="email" name="email" required stm-attribute.value="user.email"></div>
-                                    <div class="form-row"><label>Forename</label>
+                                    <div class="field"><label>Forename</label>
                                         <input type="text" name="forename" required stm-attribute.value="user.forename"></div>
-                                    <div class="form-row"><label>Surname</label>
+                                    <div class="field"><label>Surname</label>
                                         <input type="text" name="surname" required stm-attribute.value="user.surname"></div>
-                                    <div class="form-row"><label>Date of birth</label>
+                                    <div class="field"><label>Date of birth</label>
                                         <input type="text" name="date_of_birth" required stm-attribute.value="user.date_of_birth"></div>
-                                    <div class="form-row"><label>Enabled</label>
-                                        <label class="inline"><input type="checkbox" name="enabled" value="1"
+                                    <div class="field"><label>Enabled</label>
+                                        <label class="choice"><input type="checkbox" name="enabled" value="1"
                                                stm-attribute.checked="user.enabled ? '' : undefined"> Account enabled</label></div>
-                                    <div class="form-row"><label>New password</label>
+                                    <div class="field"><label>New password</label>
                                         <input type="password" name="new_password"
                                                placeholder="Leave empty to keep the current password"></div>
                                     <button type="submit">Save</button>
-                                    <span class="muted">Your own account is edited on your profile page.</span>
+                                    <span class="text-muted">Your own account is edited on your profile page.</span>
                                 </form>
                             </details>
-                            <form class="inline">
+                            <form class="cluster">
                                 <button type="button" stm-action="user.set_enabled"
                                         stm-text="user.enabled ? 'Disable' : 'Enable'">toggle</button>
                                 <button type="button" stm-if="user.is_admin && !user.is_me" stm-action="user.revoke_admin">Revoke admin</button>
@@ -84,9 +86,11 @@
                             </form>
                         </td>
                     </tr>
-                    <tr stm-if="users.users.length == 0"><td colspan="7" class="muted">No users.</td></tr>
+                    <tr stm-if="users.users.length == 0"><td colspan="7" class="text-muted">No users.</td></tr>
                 </tbody>
-            </table>
+            
+                </table>
+            </div>
         </div>
     </section>
 </body>

+ 1 - 1
test/bundles/pages.py

@@ -777,7 +777,7 @@ def run(ctx):
           "the task comment card shows the body before the quote")
     status, css = http("GET", "/_statum/resource/app.css", timeout=15)
     check(status == 200 and ".footnote" in css
-          and re.search(r"\.comment\s*\{[^}]*color:\s*var\(--ink-muted\)", css)
+          and re.search(r"\.comment\s*\{[^}]*color:\s*var\(--spry-fg-muted\)", css)
           and re.search(r"\.comment\s*\{[^}]*font-size:\s*0\.88rem", css),
           "the stylesheet ships the footnote utility and muted comment cards")
     home_html = http_get_page(auth, "/")

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio