+
WAN
diff --git a/steward/static/steward.css b/steward/static/steward.css
new file mode 100644
index 0000000..0623587
--- /dev/null
+++ b/steward/static/steward.css
@@ -0,0 +1,275 @@
+/* steward/static/steward.css — Steward's base stylesheet.
+ *
+ * Values come from theme.css, the stylesheet design system 6 ("Steward",
+ * inheriting FabledSword) generates. The short names below are Steward's
+ * aliases onto those tokens, kept so templates written against them still
+ * render; new work writes the --fs-* token. Contrast measured 2026-10-06
+ * (#5092): every text colour here is at least 4.5:1 on page, card, hover
+ * and its own 15-25% tint.
+ */
+:root {
+ --bg: var(--fs-surface-page);
+ --bg-card: var(--fs-surface-raised);
+ --bg-elevated: var(--fs-surface-hover);
+ --border: color-mix(in srgb, var(--fs-border-color) 70%, var(--fs-surface-raised));
+ --border-mid: var(--fs-border-color);
+ --text: var(--fs-text-primary);
+ --text-muted: var(--fs-text-secondary);
+ --text-dim: var(--fs-text-tertiary);
+ --gold: var(--fs-accent);
+ --gold-hover: color-mix(in srgb, var(--fs-accent) 85%, var(--fs-text-primary));
+ --gold-dim: var(--fs-accent-soft);
+ --accent: var(--fs-accent);
+ --accent-hover: var(--gold-hover);
+ /* Status: the -fg colour is the text and indicator; the -dim tint sits behind it. */
+ --green: var(--fs-success-fg);
+ --green-dim: color-mix(in srgb, var(--fs-success) 20%, transparent);
+ --yellow: var(--fs-warning-fg);
+ --yellow-dim: color-mix(in srgb, var(--fs-warning) 20%, transparent);
+ /* Between warning and error: the worse-than-warning band (slow, degraded). */
+ --orange: color-mix(in srgb, var(--fs-warning-fg) 50%, var(--fs-error-fg));
+ --orange-dim: color-mix(in srgb, var(--fs-warning) 20%, transparent);
+ --red: var(--fs-error-fg);
+ --red-dim: color-mix(in srgb, var(--fs-error) 20%, transparent);
+ --font-serif: var(--fs-font-display);
+}
+* { box-sizing: border-box; margin: 0; padding: 0; }
+body { font-family: var(--fs-font-body); background: var(--bg); color: var(--text); line-height: var(--fs-leading-body); }
+a { color: var(--accent); text-decoration: none; }
+a:hover { color: var(--accent-hover); }
+code { font-family: var(--fs-font-mono); font-size: 0.875em; background: var(--bg-elevated); padding: 0.1em 0.35em; border-radius: 3px; }
+
+#candle-glow {
+ position: fixed;
+ inset: 0;
+ pointer-events: none;
+ z-index: 0;
+ background:
+ radial-gradient(ellipse 80% 60% at 50% 40%, color-mix(in srgb, var(--fs-accent) 4.5%, transparent) 0%, transparent 70%),
+ radial-gradient(ellipse 50% 40% at 20% 80%, color-mix(in srgb, var(--fs-accent) 2.5%, transparent) 0%, transparent 65%);
+}
+
+/* ── App shell: persistent left sidebar + content column ───────────────────── */
+.app-shell { display: flex; min-height: 100vh; position: relative; z-index: 1; }
+.sidebar {
+ width: 220px; flex-shrink: 0; background: var(--bg-card);
+ border-right: 1px solid var(--border-mid);
+ display: flex; flex-direction: column;
+ position: sticky; top: 0; height: 100vh; z-index: 20;
+}
+.sidebar .brand {
+ font-family: var(--font-serif); font-weight: 500; font-size: 1.15rem;
+ letter-spacing: 0.01em; color: var(--gold); text-decoration: none;
+ display: flex; align-items: center; gap: 0.5rem;
+ padding: 0.85rem 1.25rem; border-bottom: 1px solid var(--border);
+}
+.sidebar .brand svg { flex-shrink: 0; }
+/* Only the nav list scrolls when long — the brand and user/logout stay pinned. */
+.side-nav { flex: 1; min-height: 0; overflow-y: auto; padding: 0.75rem 0; }
+.nav-group { margin-bottom: 0.9rem; }
+.nav-group-label {
+ font-size: 0.66rem; text-transform: uppercase; letter-spacing: 0.09em;
+ color: var(--text-dim); font-weight: 500; padding: 0 1.25rem; margin-bottom: 0.3rem;
+}
+.side-nav a {
+ display: flex; align-items: center; gap: 0.5rem;
+ padding: 0.4rem 1.25rem; font-size: 0.9rem; color: var(--text-muted);
+ border-left: 2px solid transparent; text-decoration: none;
+ transition: color .12s, background .12s, border-color .12s;
+}
+.side-nav a:hover { color: var(--text); background: var(--bg-elevated); }
+.side-nav a.active {
+ color: var(--gold); border-left-color: var(--gold);
+ background: color-mix(in srgb, var(--gold) 8%, transparent);
+}
+.side-user { border-top: 1px solid var(--border); padding: 0.7rem 1.25rem; }
+.side-user form { margin: 0; }
+.side-user button {
+ color: var(--text-muted); font-size: 0.84rem; text-decoration: none;
+ display: flex; align-items: center; gap: 0.4rem;
+ background: none; border: 0; padding: 0; font-family: inherit; cursor: pointer;
+}
+.side-user button:hover { color: var(--text); }
+/* Keyboard focus: a clear inset ring on every interactive sidebar element. */
+.sidebar .brand:focus-visible,
+.side-nav a:focus-visible,
+.side-user button:focus-visible {
+ outline: 2px solid var(--accent); outline-offset: -2px; color: var(--text);
+}
+.app-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
+.topbar { display: none; } /* slim mobile bar; shown only under 900px */
+.nav-scrim { display: none; }
+
+/* Layout */
+main { padding: 1.5rem 2rem; max-width: 1400px; margin: 0 auto; width: 100%; box-sizing: border-box; position: relative; z-index: 1; }
+
+/* Responsive: sidebar slides off-canvas under 900px, toggled by ☰ */
+@media (max-width: 900px) {
+ .sidebar {
+ position: fixed; left: 0; top: 0; bottom: 0; height: 100%;
+ transform: translateX(-100%); transition: transform .2s ease;
+ box-shadow: var(--fs-shadow-3);
+ }
+ body.nav-open .sidebar { transform: translateX(0); }
+ .topbar {
+ display: flex; align-items: center; gap: 0.75rem; height: 48px; padding: 0 1rem;
+ background: var(--bg-card); border-bottom: 1px solid var(--border-mid);
+ position: sticky; top: 0; z-index: 15;
+ }
+ .sidebar-toggle {
+ background: none; border: none; color: var(--text-muted); font-size: 1.3rem;
+ cursor: pointer; line-height: 1; padding: 0.25rem; border-radius: 4px;
+ }
+ .sidebar-toggle:hover { color: var(--text); }
+ .sidebar-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
+ .topbar .brand-sm {
+ font-family: var(--font-serif); color: var(--gold); font-weight: 500;
+ font-size: 1.15rem; text-decoration: none;
+ }
+ body.nav-open .nav-scrim {
+ display: block; position: fixed; inset: 0; background: var(--fs-overlay); z-index: 18;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .sidebar { transition: none; }
+ .side-nav a { transition: none; }
+}
+
+/* Alerts */
+.alert { padding: 0.75rem 1rem; border-radius: 6px; margin-bottom: 1rem; font-size: 0.9rem; }
+.alert-error { background: var(--red-dim); border: 1px solid color-mix(in srgb, var(--fs-error) 45%, transparent); color: var(--red); }
+.alert-success { background: var(--green-dim); border: 1px solid color-mix(in srgb, var(--fs-success) 60%, transparent); color: var(--green); }
+
+/* Cards */
+.card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem; margin-bottom: 1rem; }
+.card-flush { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; margin-bottom: 1rem; }
+
+/* Typography */
+.page-title { font-family: var(--font-serif); font-size: 1.6rem; font-weight: 500; color: var(--text); line-height: var(--fs-leading-heading); margin-bottom: 1.5rem; }
+/* The tiny micro-label: body face (display face only at 18px and up), caps, tracked. */
+.section-title { font-family: var(--fs-font-body); font-size: var(--fs-size-tiny); font-weight: 500; color: var(--text-muted); text-transform: uppercase; letter-spacing: var(--fs-tracking-tiny); }
+h1, h2 { font-family: var(--font-serif); font-weight: 500; }
+h3, h4 { font-family: var(--fs-font-body); font-weight: 500; }
+strong, b, th { font-weight: 500; }
+
+/* Tables */
+.table { width: 100%; border-collapse: collapse; }
+.table th { text-align: left; padding: 0.6rem 1rem; color: var(--text-muted); font-weight: 500; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.04em; border-bottom: 1px solid var(--border-mid); }
+.table td { padding: 0.7rem 1rem; border-bottom: 1px solid var(--border); font-size: 0.9rem; }
+.table tbody tr:last-child td { border-bottom: none; }
+.table tbody tr:hover td { background: var(--bg-elevated); }
+.table .td-actions { text-align: right; white-space: nowrap; }
+
+/* Forms */
+.form-group { margin-bottom: 1rem; }
+label { display: block; margin-bottom: 0.3rem; font-size: 0.85rem; color: var(--text-muted); font-family: var(--fs-font-body); }
+input[type=text], input[type=email], input[type=password], input[type=number], input[type=url], select, textarea {
+ width: 100%; padding: 0.5rem 0.75rem; background: var(--bg); border: 1px solid var(--border-mid);
+ border-radius: 5px; color: var(--text); font-size: 0.9rem; font-family: inherit;
+ transition: border-color .15s;
+}
+input:focus, select:focus, textarea:focus { outline: none; border-color: var(--accent); }
+select { cursor: pointer; }
+textarea { resize: vertical; }
+
+/* Buttons */
+.btn { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.45rem 1rem; background: var(--accent); color: var(--fs-surface-page); border: none; border-radius: 5px; cursor: pointer; font-size: 0.875rem; font-family: inherit; font-weight: 500; transition: background .15s; text-decoration: none; }
+.btn:hover { background: var(--accent-hover); color: var(--fs-surface-page); }
+.btn-sm { padding: 0.3rem 0.65rem; font-size: 0.8rem; }
+.btn-ghost { background: transparent; color: var(--text-muted); border: 1px solid var(--border-mid); }
+.btn-ghost:hover { background: var(--bg-elevated); color: var(--text); }
+.btn-danger { background: var(--fs-action-destructive); color: var(--fs-text-on-action); }
+.btn-danger:hover { background: var(--fs-action-destructive-hover); color: var(--fs-text-on-action); }
+.btn-warn { background: var(--yellow-dim); color: var(--yellow); border: 1px solid color-mix(in srgb, var(--fs-warning) 60%, transparent); }
+.btn-warn:hover { background: color-mix(in srgb, var(--fs-warning) 32%, transparent); }
+
+/* Badges / Status */
+.badge { display: inline-block; padding: 0.2em 0.55em; border-radius: var(--fs-radius-sm); font-size: 0.75rem; font-weight: 500; letter-spacing: 0.02em; }
+.badge-green { background: var(--green-dim); color: var(--green); }
+.badge-red { background: var(--red-dim); color: var(--red); }
+.badge-yellow { background: var(--yellow-dim); color: var(--yellow); }
+.badge-orange { background: var(--orange-dim); color: var(--orange); }
+.badge-gold { background: var(--gold-dim); color: var(--fs-accent-fg); }
+.badge-dim { background: var(--bg-elevated); color: var(--text-muted); }
+.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
+.dot-up { background: var(--green); box-shadow: 0 0 4px var(--green); }
+.dot-down { background: var(--red); }
+.dot-warn { background: var(--yellow); }
+.dot-dim { background: var(--text-dim); }
+
+/* Empty state */
+.empty { color: var(--text-muted); font-size: 0.9rem; padding: 2rem; text-align: center; }
+
+/* Breadcrumbs */
+/* Breadcrumb renders as a kicker line tucked directly above the page header
+ (.page-title), so nested and top-level views share one consistent header. */
+.breadcrumb { display:flex; align-items:center; flex-wrap:wrap; gap:0.35rem; font-size:0.78rem; color:var(--text-dim); margin:0 0 0.45rem; }
+.breadcrumb a { color:var(--text-muted); }
+.breadcrumb a:hover { color:var(--text); }
+.breadcrumb-sep { color:var(--border-mid); }
+.breadcrumb-cur { color:var(--text-muted); }
+
+/* Stat cards (dashboard) */
+.stat-val { font-size: 2rem; font-weight: 500; line-height: 1; font-variant-numeric: tabular-nums; }
+.stat-label { font-size: 0.8rem; color: var(--text-muted); margin-left: 0.3rem; }
+
+/* Ping pills */
+.ping-card { padding: 0.75rem 1.25rem; }
+.ping-row { display:flex; align-items:center; gap:1rem; padding:0.45rem 0; border-bottom:1px solid var(--border); }
+.ping-row:last-child { border-bottom:none; }
+.ping-meta { min-width:120px; max-width:180px; flex-shrink:1; overflow:hidden; }
+.ping-name { font-size:0.875rem; font-weight:500; display:block; overflow-wrap:anywhere; }
+a.ping-name:hover { text-decoration:underline; }
+/* Dashboard widget grid — Gridstack-rendered. Static (positioned but inert) in
+ view mode; the dashboard itself becomes the edit surface when Edit is toggled,
+ so drag-resize happens over live widget data. The bottom drawer is a fixed
+ overlay, so the dashboard width is identical entering/leaving edit mode. */
+.grid-stack-item-content {
+ background:var(--bg-card); border:1px solid var(--border-mid); border-radius:8px;
+ padding:0.8rem 1rem; display:flex; flex-direction:column; overflow:hidden;
+}
+.grid-stack-item-content .dash-cell-body { flex:1; min-height:0; overflow:auto; }
+/* The widget body is a query container, so fragments can restyle to their OWN
+ panel width (not the viewport) — widgets adapt as you resize them. */
+.dash-cell-body { container-type:inline-size; }
+/* Host-list widgets flow into columns as the panel widens: use the width, drop
+ the vertical deadspace; stay single-column (and untruncated) when narrow. */
+.host-blocks { display:grid; grid-template-columns:1fr; gap:0 1.5rem; }
+@container (min-width:520px) { .host-blocks { grid-template-columns:1fr 1fr; } }
+@container (min-width:900px) { .host-blocks { grid-template-columns:1fr 1fr 1fr; } }
+.panel-chrome { display:none; }
+.panel-drag { cursor:grab; color:var(--text-dim); user-select:none; }
+.panel-drag:active { cursor:grabbing; }
+body.dash-editing .panel-chrome { display:inline-flex; align-items:center; }
+body.dash-editing .grid-stack-item-content { outline:1px dashed var(--border-mid); outline-offset:-1px; }
+body.dash-editing main { padding-bottom:46vh; }
+.widget-drawer {
+ position:fixed; left:0; right:0; bottom:0; transform:translateY(100%);
+ transition:transform .25s ease; max-height:44vh; overflow-y:auto;
+ background:var(--bg-card); border-top:1px solid var(--border-mid);
+ box-shadow:var(--fs-shadow-3); z-index:60; padding:1rem 1.5rem 1.5rem;
+}
+body.dash-editing .widget-drawer { transform:translateY(0); }
+.ping-addr { display:block; color:var(--text-muted); font-size:0.75rem; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
+.ping-pills { display:flex; gap:2px; flex:1; min-width:0; overflow:hidden; align-items:center; }
+.pill { display:inline-block; width:7px; height:22px; border-radius:2px; cursor:default; transition:opacity .1s; }
+.pill:hover { opacity:.75; }
+.pill-empty { background:var(--bg-elevated); }
+.ping-cur { min-width:72px; text-align:right; font-size:0.85rem; font-variant-numeric:tabular-nums; }
+.ping-cur-good { color:var(--green); }
+.ping-cur-warn { color:var(--yellow); }
+.ping-cur-bad { color:var(--orange); }
+.ping-cur-down { color:var(--red); font-weight:500; }
+.ping-cur-nd { color:var(--text-dim); }
+
+/* Widget headers */
+.widget-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:0.75rem; }
+.widget-title { font-family:var(--fs-font-body); font-size:var(--fs-size-tiny); font-weight:500; color:var(--text-muted); text-transform:uppercase; letter-spacing:var(--fs-tracking-tiny); }
+.widget-link { font-size:0.8rem; color:var(--text-muted); }
+.widget-link:hover { color:var(--text); }
+
+/* Time range selector */
+.range-btn { background:none; border:1px solid var(--border-mid); border-radius:4px; color:var(--text-muted); padding:0.18rem 0.5rem; cursor:pointer; font-size:0.75rem; font-family:inherit; transition:background .1s,color .1s,border-color .1s; }
+.range-btn:hover { background:var(--bg-elevated); color:var(--text); border-color:var(--accent); }
+.range-btn.active { background:var(--accent); border-color:var(--accent); color:var(--fs-surface-page); }
diff --git a/steward/static/theme.css b/steward/static/theme.css
new file mode 100644
index 0000000..348611e
--- /dev/null
+++ b/steward/static/theme.css
@@ -0,0 +1,176 @@
+/*
+ * Steward — generated stylesheet
+ * Source: design system 6.
+ *
+ * Purpose tokens only. This sheet declares what values MEAN; it styles
+ * no elements. Buttons, tables and input schemes live as snippets that
+ * reference these names, so each value is stated once and reused rather
+ * than restated per element.
+ *
+ * Generated — edit the design system, not this file.
+ */
+
+:root {
+ /* accent */
+ --fs-accent: #C8A840; /* Steward's single signature colour */
+ --fs-accent-soft: color-mix(in srgb, var(--fs-accent) 15%, transparent); /* Pill, tag and badge backgrounds */
+ --fs-accent-faint: color-mix(in srgb, var(--fs-accent) 8%, transparent); /* The faintest accent wash */
+ --fs-accent-deep: color-mix(in srgb, var(--fs-accent) 70%, black); /* The accent, darkened — the far end of a CTA gradient */
+ --fs-accent-wash: color-mix(in srgb, var(--fs-accent) 22%, transparent); /* The heaviest accent tint — selection and emphasis washes */
+ --fs-gradient-cta: linear-gradient(135deg, var(--fs-accent), var(--fs-accent-deep)); /* Gradient for a deliberate brand moment */
+ --fs-glow-cta: 0 2px 10px color-mix(in srgb, var(--fs-accent) 35%, transparent); /* Glow under a brand-moment CTA */
+ --fs-glow-cta-hover: 0 4px 24px color-mix(in srgb, var(--fs-accent) 65%, transparent); /* Glow under a hovered brand-moment CTA */
+ --fs-accent-fg: color-mix(in srgb, var(--fs-accent) 45%, var(--fs-text-primary)); /* Accent-coloured TEXT sitting on an accent tint — the readable partner of --fs-accent-soft / -faint / -wash */
+
+ /* action */
+ --fs-action-primary: #4A5D3F; /* Save, Submit, Confirm */
+ --fs-action-secondary: #8B7355; /* Non-destructive alternates */
+ --fs-action-tertiary: var(--fs-border-color); /* Ghost / outline actions */
+ --fs-action-destructive: var(--fs-destructive); /* Destructive actions — delete, purge, revoke */
+ --fs-action-primary-hover: color-mix(in srgb, var(--fs-action-primary) 88%, white); /* Primary action, hovered */
+ --fs-action-secondary-hover: color-mix(in srgb, var(--fs-action-secondary) 88%, white); /* Secondary action, hovered */
+ --fs-action-destructive-hover: color-mix(in srgb, var(--fs-action-destructive) 88%, white); /* Destructive action, hovered */
+
+ /* border */
+ --fs-border-color: #3F4651; /* Borders, dividers and ghost outlines */
+ --fs-border: 0.5px solid var(--fs-border-color); /* The default structural border */
+ --fs-border-hover: 0.5px solid color-mix(in srgb, var(--fs-text-secondary) 30%, transparent); /* Border on hover or emphasis */
+ --fs-border-active: 2px solid var(--fs-accent); /* Selected card or active tab only */
+
+ /* elevation */
+ --fs-shadow-1: 0 1px 0 rgba(0,0,0,0.4); /* Hairline lift */
+ --fs-shadow-2: 0 4px 12px rgba(0,0,0,0.35); /* Dropdowns and popovers */
+ --fs-shadow-3: 0 16px 40px rgba(0,0,0,0.5); /* Modals */
+
+ /* focus */
+ --fs-focus-ring: 0 0 0 2px var(--fs-accent); /* Focus indication, as a box-shadow */
+
+ /* font */
+ --fs-font-display: 'Fraunces', Georgia, serif; /* Display, headings and wordmarks */
+ --fs-font-body: 'Inter', system-ui, sans-serif; /* Body text, UI and labels */
+ --fs-font-mono: 'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace; /* Code, terminal output and monospaced data */
+
+ /* icon */
+ --fs-icon-stroke: 1.5px; /* Icon stroke width at 24px */
+ --fs-icon-stroke-sm: 1px; /* Icon stroke width at 16px */
+
+ /* layout */
+ --fs-layout-page-max: 1200px; /* Maximum page width */
+ --fs-layout-page-pad: 1rem; /* Horizontal page padding */
+ --fs-layout-sidebar: 260px; /* Sidebar / nav rail width */
+ --fs-layout-header: 52px; /* App header height */
+
+ /* motion */
+ --fs-ease: cubic-bezier(0.2, 0.6, 0.2, 1); /* The one easing curve, used by every transition */
+ --fs-dur-fast: 120ms; /* Hovers, colour and border changes */
+ --fs-dur-base: 180ms; /* Most state changes */
+ --fs-dur-slow: 280ms; /* Larger surface or layout shifts */
+
+ /* radius */
+ --fs-radius-sm: 4px; /* Pills, tags and code spans */
+ --fs-radius-md: 8px; /* Buttons, inputs and small cards */
+ --fs-radius-lg: 12px; /* Cards, panels and modals */
+ --fs-radius-xl: 16px; /* Hero containers and major surfaces */
+ --fs-radius-pill: 9999px; /* Fully rounded — avatars, toggles */
+
+ /* semantic */
+ --fs-success: var(--fs-action-primary); /* Success states */
+ --fs-warning: #8B6F1E; /* Warnings */
+ --fs-error: #C04A1F; /* Errors — something went wrong */
+ --fs-info: #3D5A6E; /* Informational states */
+ --fs-destructive: #6B2118; /* Irreversible actions */
+ --fs-error-fg: #EC7A6C; /* Error / down / firing as TEXT or a status indicator */
+ --fs-warning-fg: #E08A3C; /* Warning / degraded / pending as TEXT or a status indicator */
+ --fs-info-fg: color-mix(in srgb, var(--fs-info) 50%, var(--fs-text-primary)); /* Info colour when it is TEXT: a status pill's label, an informational line. Not for fills or borders. */
+ --fs-success-fg: #5FB87A; /* Success / up / ok as TEXT or a status indicator: badge labels, status dots, "up" counts */
+
+ /* space */
+ --fs-space-1: 4px; /* Spacing step 1 */
+ --fs-space-2: 8px; /* Spacing step 2 */
+ --fs-space-3: 12px; /* Spacing step 3 */
+ --fs-space-4: 16px; /* Spacing step 4 */
+ --fs-space-5: 20px; /* Spacing step 5 */
+ --fs-space-6: 24px; /* Spacing step 6 */
+ --fs-space-7: 32px; /* Spacing step 7 */
+ --fs-space-8: 48px; /* Spacing step 8 */
+ --fs-space-9: 64px; /* Spacing step 9 */
+ --fs-space-10: 96px; /* Spacing step 10 */
+
+ /* state */
+ --fs-disabled-opacity: 0.5; /* Opacity for disabled controls */
+ --fs-overlay: rgba(0, 0, 0, 0.65); /* Scrim behind modals and dialogs */
+
+ /* surface */
+ --fs-surface-page: #14171A; /* Page background, the deepest surface */
+ --fs-surface-raised: #1E2228; /* Cards and raised elements */
+ --fs-surface-hover: #2C313A; /* Hovered surfaces, secondary elevation */
+ --fs-surface-code: var(--fs-surface-page); /* Code block background */
+ --fs-surface-code-inline: var(--fs-surface-raised); /* Inline code span background */
+ --fs-table-stripe: color-mix(in srgb, var(--fs-text-primary) 3%, transparent); /* Alternating table row tint */
+
+ /* text */
+ --fs-text-primary: #E8E4D8; /* Primary text on a page or raised surface — body, headings, labels */
+ --fs-text-secondary: #C2BFB4; /* Secondary text and captions */
+ --fs-text-tertiary: #9C9A92; /* Hints and metadata */
+ --fs-text-on-action: #E8E4D8; /* Text on a filled coloured surface — action buttons, semantic badges, the accent */
+ --fs-text-tertiary-fg: color-mix(in srgb, var(--fs-text-tertiary) 55%, var(--fs-text-primary)); /* Tertiary text on a tertiary tint — a muted chip that stays readable */
+ --fs-text-secondary-fg: color-mix(in srgb, var(--fs-text-secondary) 90%, var(--fs-text-primary)); /* Secondary text on a secondary tint — completes the pattern; the value barely moves */
+
+ /* type */
+ --fs-size-display: 40px; /* Display — display face, medium weight */
+ --fs-size-h1: 32px; /* H1 — display face, medium weight */
+ --fs-size-h2: 24px; /* H2 — display face, medium weight */
+ --fs-size-h3: 18px; /* H3 — body face, medium weight */
+ --fs-size-body: 15px; /* Body — body face, regular weight */
+ --fs-size-body-sm: 13px; /* Small body — body face, regular weight */
+ --fs-size-label: 12px; /* Label — body face, medium weight */
+ --fs-size-code: 13px; /* Code — mono face, regular weight */
+ --fs-size-tiny: 11px; /* Tiny micro-label — body face, medium weight */
+ --fs-weight-regular: 400; /* Regular weight */
+ --fs-weight-medium: 500; /* Medium weight — the heaviest the system goes */
+ --fs-leading-heading: 1.3; /* Line height for headings */
+ --fs-leading-body: 1.5; /* Line height for body text */
+ --fs-leading-longform: 1.7; /* Line height for long-form reading surfaces */
+ --fs-tracking-tiny: 0.08em; /* Letter-spacing on the tiny uppercase label */
+}
+
+[data-theme="light"] {
+ /* accent */
+ --fs-accent-wash: color-mix(in srgb, var(--fs-accent) 20%, transparent);
+ --fs-glow-cta-hover: 0 4px 20px color-mix(in srgb, var(--fs-accent) 55%, transparent);
+
+ /* border */
+ --fs-border-color: #D9D6CE;
+
+ /* state */
+ --fs-overlay: rgba(0, 0, 0, 0.45);
+
+ /* surface */
+ --fs-surface-page: #F5F1E8;
+ --fs-surface-raised: #FBF8F0;
+ --fs-surface-hover: #EFEAE0;
+ --fs-surface-code: #EBEDF0;
+ --fs-surface-code-inline: #EBEDF0;
+
+ /* text */
+ --fs-text-primary: #14171A;
+ --fs-text-secondary: #5A5852;
+ --fs-text-tertiary: #9A9890;
+}
+
+/* SUPERSEDES — write the token, not the literal.
+ * #c84048 -> --fs-error-fg
+ * #ff8090 -> --fs-error-fg
+ * #ffb0b8 -> --fs-error-fg
+ * #ffb0b0 -> --fs-error-fg
+ * #d4a840 -> --fs-warning-fg
+ * #c87840 -> --fs-warning-fg
+ * var(--fs-info) -> --fs-info-fg
+ * #4aa86a -> --fs-success-fg
+ * #90f0b0 -> --fs-success-fg
+ * #fff -> --fs-text-on-action
+ * #ffffff -> --fs-text-on-action
+ * white -> --fs-text-on-action
+ * bold -> --fs-weight-medium
+ * bolder -> --fs-weight-medium
+ */
diff --git a/steward/templates/ansible/browse.html b/steward/templates/ansible/browse.html
index a7cea6d..96aa26d 100644
--- a/steward/templates/ansible/browse.html
+++ b/steward/templates/ansible/browse.html
@@ -56,7 +56,7 @@
{% if not sd.playbooks %}
-
No playbooks found at this path.
+
No playbooks found at this path.
{% else %}
diff --git a/steward/templates/ansible/run_started.html b/steward/templates/ansible/run_started.html
index d8aa1ee..3d958ac 100644
--- a/steward/templates/ansible/run_started.html
+++ b/steward/templates/ansible/run_started.html
@@ -7,7 +7,7 @@
-
+
Streaming output…
-
+
+
{% block head %}{% endblock %}
diff --git a/steward/templates/dashboard/share_view.html b/steward/templates/dashboard/share_view.html
index d510a5f..855eb98 100644
--- a/steward/templates/dashboard/share_view.html
+++ b/steward/templates/dashboard/share_view.html
@@ -75,7 +75,7 @@
-
+
{{ dashboard.name }}
Steward · read-only view
diff --git a/steward/templates/settings/_ansible_sources.html b/steward/templates/settings/_ansible_sources.html
index 610102c..6a449aa 100644
--- a/steward/templates/settings/_ansible_sources.html
+++ b/steward/templates/settings/_ansible_sources.html
@@ -14,7 +14,7 @@
{{ src.type }}
diff --git a/steward/templates/settings/index.html b/steward/templates/settings/index.html
deleted file mode 100644
index a8e0a84..0000000
--- a/steward/templates/settings/index.html
+++ /dev/null
@@ -1,157 +0,0 @@
-{# steward/templates/settings/index.html #}
-{% extends "base.html" %}
-{% block title %}Settings — Steward{% endblock %}
-{% block content %}
-Settings
-
-{% endblock %}
diff --git a/tests/test_style_tokens.py b/tests/test_style_tokens.py
new file mode 100644
index 0000000..09bad2f
--- /dev/null
+++ b/tests/test_style_tokens.py
@@ -0,0 +1,73 @@
+"""Every colour a page draws comes from the design-system tokens (#5092).
+
+- Every var(--x) a template or stylesheet uses is defined somewhere Steward
+ serves, so nothing silently falls back to the browser default.
+- The base stylesheet states no colour or heavy weight of its own: values
+ are theme.css tokens (generated from design system 6) or aliases of them.
+- Hex colours written into templates can only go down: each page moved onto
+ the tokens lowers HEX_CEILING (milestone 477 retires the rest).
+"""
+import pathlib
+import re
+
+import steward
+
+_REPO = pathlib.Path(steward.__file__).parent.parent
+_STATIC = _REPO / "steward" / "static"
+_COMMENTS = re.compile(r"\{#.*?#\}||/\*.*?\*/", re.S)
+_DEFINED = re.compile(r"(--[\w-]+)\s*:")
+_USED = re.compile(r"var\(\s*(--[\w-]+)")
+# A hex colour where a colour goes: after a CSS ':', '(' or ',', or as a quoted JS
+# string -- but not an HTML attribute's value (href="#abc" is a link to an id).
+_HEX = re.compile(r"""(?:[:(,]\s*|(? set[str]:
+ defined, used = set(), set()
+ for t in texts:
+ defined |= set(_DEFINED.findall(t))
+ used |= set(_USED.findall(t))
+ return used - defined
+
+
+def hex_colours(text: str) -> list[str]:
+ return _HEX.findall(_COMMENTS.sub("", text))
+
+
+def test_the_guards_can_fail():
+ assert undefined_vars([":root { --a: 1; } .x { color: var(--a); background: var(--b); }"]) == {"--b"}
+ page = '
'
+ assert len(hex_colours(page)) == 2
+ assert hex_colours("{# color:#fff #}") == []
+
+
+def test_every_css_variable_used_is_defined():
+ texts = [_text(p) for p in [*_templates(), *_sheets()]]
+ assert not undefined_vars(texts)
+
+
+def test_the_base_stylesheet_states_no_colour_or_heavy_weight_of_its_own():
+ css = _text(_STATIC / "steward.css")
+ assert not re.findall(r"#[0-9a-fA-F]{3,8}\b|rgba?\(|hsla?\(", css)
+ assert not re.findall(r"font-weight\s*:\s*(?:[6-9]00|bold)", css)
+
+
+def test_hex_colours_in_templates_only_go_down():
+ count = sum(len(hex_colours(p.read_text())) for p in _templates())
+ assert count <= HEX_CEILING, (
+ f"{count} hex colours in templates, ceiling {HEX_CEILING}: use a token (theme.css / steward.css)")