Docs website redesign brief — docs.youngle.au
Docs website redesign brief — docs.youngle.au
This is the design of record for docs.youngle.au. It covers content IA, layout, and branding for the gated Docs surface, and is the input Stage 5 of the vhost plan has been waiting on.
Audience: operators and customers reading reference material. This is a documentation site, not a marketing site. Every rule below is chosen for findability and legibility of reference text, not for persuasion. Concretely: no hero, no feature grid, no pricing, no testimonials, no CTA banner, no "trusted by" logos. If a section of the site could plausibly appear on a brochure page, it does not belong here.
What this brief is not. It does not redesign Pulse (that spec is
[pulse-homepage-design](pulse-homepage-design), and Pulse is the public surface). It does not
redesign the fleet dashboard (that is [fleet-dashboard-design-of-record-2026-10-20](fleet-dashboard-design-of-record-2026-10-20)).
It does not touch the Authelia gate (that is Sol's infra lane, already proven
live). It specifies the shape and skin of the Docs site; it does not
implement it.
0. What I measured before writing this
Everything below is measured against the running system, not assumed. Probes run
2026-10-21 from LAB against prod 161.33.71.67 and the LAB vault.
| # | Fact | Measured value | How |
|---|---|---|---|
| M1 | Stack | Docus 5.12.3, Nuxt 4.5.1, @nuxt/content 3.15.2, @nuxt/ui 4.10.0, Tailwind 4.3.3 | package-lock.json on prod |
| M2 | Content collections | 2 — docs (content/Docs/**, prefix /docs), pulse (content/Pulse/**, prefix /pulse) | content.config.ts |
| M3 | Site content volume | 580 pages published — 250 Docs + 330 Pulse (.md + .mdx); .md only: 248 Docs + 327 Pulse | find content on prod |
| M4 | Vault volume | 1,806 .md under Docs/ (incl. 1,571 in 05-archive); 660 .md/.mdx reach the forge after archive + junk-dir exclusion | find on LAB vault + forge walk |
| M5 | Content depth | max 5 path segments under Docs/; 18 pages at depth 5, 54 at depth 4 | awk -F/ '{print NF-1}' on the prod tree |
| M6 | Nav defect — /docs | aside renders 248 links; 331 on /pulse | link count in served HTML |
| M7 | Nav defect — cross-section | /pulse aside contains 1 /docs link; /docs aside contains 1 /pulse link (the section tabs) | grep of served HTML |
| M8 | Grid | lg:grid-cols-10; upstream UPage = left 2 / center 8 / right 2 | .nuxt/ui/page.ts |
| M9 | Sidebar override | aside[data-slot=left] forced to span 3, div[data-slot=center] to span 7 — 3 + 7 + 2 = 12 in a 10-column grid | app/assets/css/sidebar.css |
| M10 | Primary token | --ui-primary: var(--ui-color-primary-500) → emerald 500 = oklch(69.6% 0.17 162.48) = #00bc7d | served HTML :root |
| M11 | Neutral | zinc; --ui-text-highlighted: var(--ui-color-neutral-900); --ui-text-muted: neutral-500; --ui-text-dimmed: neutral-400 | served HTML |
| M12 | Font | system stack, no webfont: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, … | built CSS --font-sans |
| M13 | Type scale | Nuxt UI: text-3xl sm:text-4xl page H1, text-pretty on headings, text-sm nav | served HTML |
| M14 | Radius / container / header | --ui-radius: .25rem · --ui-container: 80rem (90rem ≥1536px) · --ui-header-height: 4rem | built CSS |
| M15 | Auth gate | docs.youngle.au → 302 → auth.youngle.au/?rd=…; nginx include snippets/authelia-forward-auth.conf; no auth_basic anywhere | live curl + nginx -T |
| M16 | Doc-class coverage — vault | of the 213 Docs/ files that carry frontmatter (of 235 outside 05-archive): title: 212, status: 203, date: 198, author: 190, authority: 130, lane: 87, category: 56, description: 9 | frontmatter parse over LAB vault Docs/ |
| M17 | Status vocabulary | 41 distinct status: values in Docs/: 33 authority, 29 verified, 24 finding, 16 research, 12 decision, 10 done, 8 design, 8 study, 7 proposal, 6 plan, 5 manual-copy, 4 verification, 3 implemented/superseded/design-for-review, + ~26 singletons | frontmatter parse over LAB vault Docs/ |
| M18 | Category vocabulary | category: is inconsistent — 23 research, 20 implementation, 9 fleet-ops, 2 security, 2 architecture; ~6% coverage | grep on vault |
| M19 | Longest doc names | medusa-notification-channels-and-email-provider-2026-10-20 (58 chars) | find on vault |
| M20 | Route collision | /docs/agents serves AGENTS.md (the vault README) — title "Young's Mind Vault" — while SolOS/agents/ is at /docs/solos/agents | live curl on prod :8091 |
| M21 | Storefront stack | nuxt ^3.14.0 + @nuxt/ui ^2.18.0 (installed 2.22.3) at /home/ubuntu/builds/storefront | prod package.json |
| M22 | Colour contrast | computed by rasterising the Tailwind v4 OKLCH ramps in a real browser canvas — see §4.4 | Chromium canvas probe |
| M23 | Frontmatter is stripped in transit | the forge deletes the entire YAML frontmatter block from every published page. Vault: 213 of 660 Docs/ files carry frontmatter; published: 0 — only the 39 generated index.md landings keep a title: | build_site.py FRONT.sub("", text, count=1) + a parse of vault / forge output / repo content/ |
| M24 | Site content is a moving target | the publish lane pushes every 10 min. The prod tree was at 0b2eeb3 (250 Docs pages) when measured; LAB's solos-docs was already at d3e3419. Page counts in this brief are a snapshot of d3e3419, and will drift | git rev-parse on both ends |
Four findings that change the design, stated plainly:
- The nav is broken in a way no visual redesign will fix. The aside on
/docsrenders 248 links and on/pulse331 — one flat, fully-expanded tree. The cause is upstream Docus:useSubNavigationmaps a route to a section byroute.path.startsWith(item.path + '/'), and the merged navigation array's first matching root wins. Withdocsandpulseconcatenated,/docs/...resolves correctly, but every level below it is flattened becauseUContentNavigationis givencollapsible: false(DocsAsideLeftBody.vue). The 3/7 column override insidebar.cssthen makes the grid 12 columns wide inside a 10-column grid (M9). A brief that only says "make it prettier" would ship the same unusable nav. §2.3 specifies the fix. - The publish lane deletes the frontmatter before the site ever sees it.
This is the single most consequential measurement in this brief, and it
invalidates the obvious design (M23). The forge does
FRONT.sub("", text, count=1)on every file — it removes the entire YAML block. Measured end to end:files with frontmatter title:status:description:Vault (Docs + Pulse, excl. archive) 660 362 (54.8%) 275 312 15 Forge output /tmp/site_build580 39 (6.7%) 39 0 0 Published solos-docs/content/580 39 (6.7%) 39 0 0
The 39 survivors are the forge's own generatedindex.mdlandings, which are written after the strip. Scoped toDocs/alone, 213 of 235 non-archive files carry frontmatter — so the loss is not a rounding error, it is the whole metadata surface. Every hand-authored page reaches the site with no frontmatter at all. A status badge, a date line, a lede, a per-section filter — none of it can be built on frontmatter today. It is all in the vault, and it is all deleted in transit. §3.1 specifies the frontmatter contract and the one-line forge change that makes it real; §6.1 sequences that change as step 0, because steps 6–8 all depend on it. - Frontmatter coverage is uneven even before the strip. Only 9
Docs/files carry adescription:(M16) andstatus:has 41 distinct values (M17). So even after the strip is fixed, a design that renders a description under every page title will render an empty string on most pages. §3.1 specifies a frontmatter contract with a fallback, and states plainly which fields may be relied on. - The brand green and the theme green are different greens. The logo
green is
#16a34a; the theme primary is emerald 500#00bc7d(M10).#16a34aon white is 3.30:1 — it fails AA for body text and only just clears the 3:1 non-text threshold. The wordmark and the links are therefore not the same colour today, and the links are the wrong green for a reading surface. §4 resolves it: one accent, specified as a ramp with an explicit shade per role.
1. Content IA — the top-level sections
1.1 The rule that decides the IA
The site is a mirror of the vault tree, with one change: the vault's numbered folders become named sections.
The vault is organised by lifecycle stage (00-scope → 06-implementation),
which is right for a repo and wrong for a reader, who asks "where is the thing
I need?" The site reorganises the same content into eight named sections.
The URL keeps the vault path (so existing links and the publish pipeline never
break); the navigation and landing pages speak the reader's vocabulary.
This is a presentation-layer change. No content moves, no file is renamed, the forge is untouched, and the publish lane keeps working. §1.4 records this explicitly because it is the single most important scope statement in the brief.
1.2 The eight sections
| # | Section label | Vault source | Published pages | What the reader finds there |
|---|---|---|---|---|
| 1 | Start Here | Docs/Fleet/manual/ + Docs/VAULT-INDEX.md + Docs/AGENTS.md | 10 | The operator's manual, entry point, and the manifest of what is current vs historical |
| 2 | Product | Docs/Fleet/{00-scope,02-architecture,04-structure-snapshot}/ + Fleet/README.md | 26 | Scope, the design of record, the structure of record — what we are building |
| 3 | Research | Docs/Fleet/01-research/ + Docs/Projects/vhost/research/ | 56 | Studies and third-party analysis — what we learned |
| 4 | Decisions | Docs/Fleet/03-decisions/ + Docs/Projects/vhost/decisions/ | 56 | Rulings and their reasons — what we chose and why |
| 5 | Build & Run | Docs/Fleet/06-implementation/ + Docs/Projects/vhost/implementation/ | 46 | Plans, stage registers, provider builds — how it was built |
| 6 | Operations | Docs/Fleet/05-operations/ + Docs/SolOS/ | 45 | Runbooks, host topology, spend ledger — how it runs today |
| 7 | Projects | Docs/Projects/{youngle,yukonit}/ | 5 | Per-client project trees |
| 8 | Tenancy Platform | Docs/Projects/vhost/index.md | 1 | The vhost platform entry point |
Counts are the published pages, measured on the content tree at
solos-docs rev d3e3419 (2026-10-21 18:42), which mirrors prod. That tree
holds 250 markdown files under content/Docs/: 26 are generated index.md
landings and 224 are hand-authored documents. The eight sections address
245 of 250; the remaining 5 are the section landings themselves —
Docs/index.md, Docs/Fleet/index.md, Docs/Projects/index.md,
Docs/Kanban/index.md and Docs/Kanban/fleet-board.md — all handled
explicitly in §1.5.
These counts will drift. The publish lane pushes every 10 minutes (M24); between measurement and this sentence the tree moved from 248 to 250 pages. The counts are a snapshot for sizing the navigation and the landing pages, not a number to assert later. The mapping rules in §1.4 and §2.5 are the durable part — they are what an implementer codes against.
Why eight and not four. The vault's four top-level folders (Fleet,
Projects, SolOS, Kanban) are a storage taxonomy; a reader's taxonomy
runs on the question being asked. Four vault folders also cut across the
reader's questions in exactly the wrong way: Fleet/03-decisions and
Projects/vhost/decisions are the same kind of thing (a ruling) in different
places, and a reader looking for "decisions" must currently look in two
unrelated branches of the sidebar. Merging them by kind rather than by
owner is the whole point of the reorganisation. Eight is the smallest
number where no section holds two unrelated kinds of document.
1.3 Nesting rules
- Depth 1 — the eight sections. Always visible as the top-level list.
- Depth 2 — the vault's subfolders (
01-research,hosts,manual, …), rendered with their reader-facing name, not the raw folder name. The00-scope→ "Scope" mapping table is §2.5. - Depth 3 — individual documents, grouped under their depth-2 parent. Collapsed by default. This is the fix for M6. 249 always-expanded links is the reason the aside is unusable today.
- Depth 4+ — the document itself, plus its headings. Never more than two levels of nav below depth 2.
Never expose a raw vault folder name to a reader. 00-scope, 01-research,
05-operations are repo hygiene; a reader needs "Scope", "Research",
"Operations". §2.5 gives the full mapping.
1.4 What each existing vault doc class lands in
This is the acceptance criterion "where does each existing vault doc class land". Every class present in the vault, mapped to exactly one section:
| Vault doc class | Detected by | Lands in | Published pages |
|---|---|---|---|
| Scope, lanes, plan | Fleet/00-scope/ + Fleet/README.md | Product | 6 |
| Design of record, architecture, structure | Fleet/02-architecture/, Fleet/04-structure-snapshot/ | Product | 20 |
| Studies, third-party analysis | Fleet/01-research/, Projects/vhost/research/ | Research | 56 |
| Rulings, decisions, proposals | Fleet/03-decisions/, Projects/vhost/decisions/ | Decisions | 56 |
| Implementation plans, stage registers, builds | Fleet/06-implementation/, Projects/vhost/implementation/ | Build & Run | 46 |
| Ops runbooks, audits, spend | Fleet/05-operations/ | Operations | 21 |
| Host topology, agents, subscriptions | Docs/SolOS/ | Operations | 24 |
| Operator manual, manifest | Fleet/manual/ | Start Here | 8 |
| Vault index | VAULT-INDEX.md | Start Here | 1 |
| Per-client trees | Projects/youngle/, Projects/yukonit/ | Projects | 5 |
| Platform entry point | Projects/vhost/index.md | Tenancy Platform | 1 |
| Board stub | Kanban/fleet-board.md | Operations | 1 — a stub, and the board truth is hermes kanban, not a docs page |
| Vault operating instructions | AGENTS.md | Start Here | 1 — reads as a page about the vault itself; right, but see M20 |
| Research notes, briefs, podcast | Pulse/** | not here | Pulse is the public surface; separate site, separate spec |
| Superseded, archived | Fleet/05-archive/** | not published | Already excluded by the forge; stays excluded |
Note on the Board stub row. Kanban/fleet-board.md is listed under
Operations, but Kanban/index.md is a generated landing and sits with the
other section landings in the residual set of §1.2. The two are different
kinds of file and are counted separately.
1.5 Deliberately absent, and why
Absence is load-bearing. Writing it down stops the next session rebuilding it.
| Not published | Why |
|---|---|
Fleet/05-archive/** | Superseded by ruling. The forge already excludes it (v4). MANIFEST.md classifies what is current vs historical — that is the replacement for reading the archive. |
Pulse/** on the Docs site | Pulse is the single public surface (pulse.youngle.au). It has its own spec. Duplicating it inside the gate creates two homes for one fact. |
Docs/Kanban/*.kanban | Not .md; Nuxt Content cannot parse them; the projector rewrites them every minute. Already excluded from publish. fleet-board.md is a stub and is retired. |
SolOS/subscriptions/ | Empty directory. It is a declared home with no consumer. It renders as a nav node that leads nowhere. Gate it: it appears when the first subscription doc lands. |
Raw AGENTS.md as a top-level page | It collides at /docs/agents with SolOS/agents/ (M20). The page is real and useful — it is the vault's operating instructions — so it stays, but it moves under Start Here as "Vault Operations", and SolOS/agents/ keeps the /docs/solos/agents path. The route collision is a real defect today and must be fixed by the brief's implementer. |
| Any marketing surface | Acceptance criterion 5. This is a reference site. |
1.6 The landing pages
One landing page per section, at the section route. Each is a directory, not a feature grid — a reader scanning for a document needs a list, not tiles.
Structure of every section landing:
- H1 — the section name (
Product,Decisions, …). One<h1>per page. - One-line scope — what this section holds, in the reader's terms. ("Decisions — rulings, proposals, and the reasons behind them.")
- Subsection rows — one row per depth-2 group: name · one-line scope · page count →. A row, not a card. Five equal cards is a marketing layout; a directory row list carries hierarchy by content.
- Status filter note — where the section supports it, a one-line note
that
status:frontmatter is shown on document cards (§3.4).
The site landing (/docs) is the eight-section directory, in the order of
§1.2, each row showing name · scope · count. Plus one "Start Here" call-out
at the top, because "Start Here" is a reader's first move.
2. Layout — templates and navigation
2.1 Templates, as reusable patterns
Five templates. Every page on the site is one of these. A template is a pattern with named slots, not a one-off layout.
T1 — Reference page (the workhorse)
┌────────────────────────────────────────────────────────────────────┐
│ HEADER [logo] [Docs] [Pulse] [search ⌘K] [theme] [menu] │
├──────────────┬──────────────────────────────────┬──────────────────┤
│ SECTION RAIL │ BREADCRUMB Decisions › Medusa │ ON THIS PAGE │
│ ▸ Product │ H1 Medusa notification … │ Decision │
│ ▸ Research │ ───────────────────────────── │ Channels │
│ ▾ Decisions │ STATUS decision · 2026-10-20 │ Providers │
│ Medusa │ lede (1–2 sentences) │ Open questions │
│ Platform │ ───────────────────────────── │ │
│ ▸ Build │ body — max 72ch │ EDIT / REPORT │
│ ▸ Operations│ ... │ ──────────────── │
│ ... │ ───────────────────────────── │ ← Prev Next → │
│ │ [Related documents] │ │
└──────────────┴──────────────────────────────────┴──────────────────┘
2/10 cols 6/10 cols 2/10 cols
- Rail — the eight sections. Depth-2 groups under the active section only. Collapsed by default. This is the M6 fix.
- Centre — breadcrumb, H1, status line, lede, body, related.
- Right — on-this-page TOC (H2+ only), edit/report links, prev/next.
- Body measure —
max-width: 72ch. Long lines are the single most common readability defect in a docs site and the default prose width is not it. - Right rail hides below
lg. Below that, the TOC moves into a drawer (T1-mobile, below).
T2 — Section landing
BREADCRUMB Docs › Decisions
H1 Decisions
lede Rulings, proposals, and the reasons behind them.
─────────────────────────────────────────────────────
▸ Medusa notification channels [12 docs] →
▸ Platform architecture [ 8 docs] →
▸ Marketing & social add-on [ 6 docs] →
Rows, not cards. Row = border-top 1px --border, padding 16px 0,
name (600) · scope (muted, flex-1) · count (mono, dimmed) · arrow.
Hover: background --surface-2, arrow nudges 2px.
T3 — Directory index (the site landing, and every "all documents" view)
Same row pattern as T2, one level deeper. Adds a right-aligned count and a
status: badge where present. Used at /docs and for any group with > 20
documents.
T4 — Document card (used in lists, never as a page)
┌──────────────────────────────────────────┐
│ DECISION · 2026-10-20 │
│ Medusa notification channels and … │
│ Two sentences of what this document │
│ decides. │
│ ──────────────────────────────────────── │
│ 12 docs in this group → │
└──────────────────────────────────────────┘
bg --surface, border 1px --border, radius --r-md, padding 24px.
Label = mono, uppercase, 12px, dimmed. Title = 1.05rem/600, clamped to 2
lines. Hover: border → --border-strong, shadow → --shadow-lift,
transform: translateY(-1px).
T5 — Operator dashboard (already designed — do not restate)
/dashboard is the fleet dashboard and is specified in
[fleet-dashboard-design-of-record-2026-10-20](fleet-dashboard-design-of-record-2026-10-20). It lives in the same app
and the same gate but uses a full-width layout with no sidebars — data
panels must not compete with a navigation rail. This brief specifies only
what the dashboard must borrow from the docs shell: the header, the token
set (§4), and the footer. The dashboard's own panels, colour rules, and
empty-state doctrine are in that document and are not restated here.
2.2 The header
Two rows, 112px total on desktop (64px header + 48px section bar) — the
--ui-header-height: 112px override in app/app.vue already does this.
Row 1 (64px)
- Left: logo mark (green brain SVG) + wordmark. The existing
public/logo/light.svg/dark.svgath-6— already correct, do not redraw. - Centre: search trigger.
⌘K/Ctrl Kbadge. Opens the command palette. - Right: theme toggle, here only — the floating duplicate toggle in the bottom-right of the current build is a rendering artifact and must go. Then the mobile menu button.
Row 2 (48px) — the section bar. The eight sections as a horizontal
UNavigationMenu, variant="pill". On /pulse this row is replaced by
Pulse's own sections. Below lg, the whole bar collapses into the mobile
drawer.
Sticky header must not cover a focused element. With a 112px sticky
header, every heading anchor needs scroll-margin-top: calc(var(--ui-header-height) + 16px).
The current build already does this for H1 (scroll-mt-[calc(45px+var(--ui-header-height))]) —
extend it to H2/H3, which the current build does not do.
2.3 The navigation model — and the fix for M6
Target model (three levels, collapsed by default):
Sections (8, always visible)
└── Depth-2 groups (only under the active section)
└── Documents (collapsed by default; expand on click)
The defect, stated exactly. node_modules/docus/app/composables/useSubNavigation.ts:
const sidebarNavigation = computed(() => {
if (subNavigationMode.value && currentSection.value) {
return currentSection.value.children || []
}
return navigation?.value || []
})
and currentSection finds the first root whose path prefixes the route.
With the merged [...docsNav, ...pulseNav] array, /docs/x matches the
Docs root correctly. But DocsAsideLeftBody.vue renders
<UContentNavigation :collapsible="false" …> — collapsible: false is the
bug. It forces every one of the 249 depth-3 documents to render expanded,
in one flat list, with no way to collapse a group.
The fix, in the order it must be applied:
- Set
collapsibletotrueon the aside'sUContentNavigation, and setdefault-openon the depth-2 level only. Docus shipsDocsAsideLeftBody.vueas an upstream file — the repo already overrides upstream files (2-line diff in[...slug](...slug).vueper[docs-nav-v2](docs-nav-v2)), so a diff-sized override of this one is the established pattern here. - Filter the navigation tree to the active section before rendering.
The merged array must not reach the sidebar whole. Either filter in
app.vuewherenavigationis provided, or use Docus'suseSubNavigationcurrentSectionand rendercurrentSection.childrenexplicitly rather thansidebarNavigation. - Keep the section bar from the merged roots — that is what
transformNavigationinapp/app.vuealready preserves, and the/docs+/pulsetabs must keep working (M7 shows they do).
Verify with a count, not an eyeball. The acceptance probe is: the aside on any page renders ≤ 40 links (8 sections + ~10 groups + a collapsed group's title), not 249. After expanding one group it renders that group's documents only.
2.4 The grid — fixing M9
The current app/assets/css/sidebar.css forces:
aside[data-slot=left] { grid-column: span 3 / span 3 !important; }
div[data-slot=center] { grid-column: span 7 / span 7 !important; }
inside a lg:grid-cols-10 grid. 3 + 7 + 2 = 12 columns in a 10-column
grid. The right rail (TOC) is pushed out of the grid, and the layout
overflows its own container. The override was a fix for a real problem
(deep labels truncating to "C…" — Young's 2026-10-15 readability ruling) and
it solved it by breaking the grid.
The correct fix — 3 / 5 / 2, summing to 10:
| Slot | Upstream | Target | Reason |
|---|---|---|---|
| Left (rail) | 2 | 3 | Keep Young's ruling — deep labels need the width |
| Centre (body) | 8 | 5 | With the nav collapsible, the centre no longer needs 8 |
| Right (TOC) | 2 | 2 | Unchanged |
Implementation: override the UPage theme slots rather than the raw grid
classes, so the arithmetic stays visible in one place:
// app/app.config.ts
ui: {
page: {
slots: {
root: 'flex flex-col lg:grid lg:grid-cols-10 lg:gap-6',
left: 'lg:col-span-3',
center: 'lg:col-span-5',
right: 'lg:col-span-2 order-first lg:order-last',
},
},
}
Then delete the !important grid override from sidebar.css and keep
only the two rules that were the actual intent:
/* sidebar.css — keep these two, drop the grid-column overrides */
aside[data-slot="left"] ul[data-slot="root"] { margin-inline-start: 0.75rem !important; }
aside[data-slot="left"] [data-slot="linkLabel"] {
white-space: normal !important;
overflow: visible !important;
display: -webkit-box !important;
-webkit-line-clamp: 2 !important;
-webkit-box-orient: vertical !important;
}
Do not remove the readability rules. The nav must fit the 3-column rail with two-line labels; that was Young's ruling and it stands. What changes is that the rail is now 3 of 10 with a 5-column body, not 3 of 10 with a 7-column body in a 10-column grid.
Body measure. The centre column at 5/10 of an 80rem container minus gaps
is ~35rem. Prose inside it is capped at max-width: 72ch, centred or
left-aligned within the column. Do not let the body text stretch to the full
column.
2.5 Vault folder → reader label
Every raw folder name the reader would see, and what it becomes:
| Vault folder | Reader label | Section |
|---|---|---|
Fleet/manual/ | Operator's Manual | Start Here |
Fleet/00-scope/ | Scope & Lanes | Product |
Fleet/02-architecture/ | Architecture | Product |
Fleet/04-structure-snapshot/ | Structure | Product |
Fleet/01-research/ | Fleet Research | Research |
Projects/vhost/research/ | Platform Research | Research |
Fleet/03-decisions/ | Fleet Decisions | Decisions |
Projects/vhost/decisions/ | Platform Decisions | Decisions |
Fleet/06-implementation/ | Fleet Build | Build & Run |
Projects/vhost/implementation/ | Platform Build | Build & Run |
Fleet/05-operations/ | Operations | Operations |
SolOS/hosts/ | Hosts | Operations |
SolOS/agents/ | Agents | Operations |
SolOS/subscriptions/ | (gated — empty) | Operations |
Projects/youngle/ | Young Le | Projects |
Projects/yukonit/ | Yukon IT | Projects |
Projects/vhost/ | vHost Platform | Tenancy Platform |
Kanban/ | (not published) | — |
AGENTS.md | Vault Operations | Start Here |
The mapping lives in one place: a LABEL dictionary in the forge
(build_site.py), alongside the existing PRETTY dictionary that already
does exactly this job for solosv3, 00-scope, 01-research, etc. One
dictionary, one owner, no second copy of the naming.
2.6 Mobile
| Breakpoint | Behaviour |
|---|---|
| ≥ 1024px | Full 3 / 5 / 2 grid, section bar visible, rail + TOC both visible |
| 768–1023px | Rail collapses into the left drawer (Docus's DocsAsideMobileBar already does this); TOC moves to a right drawer; body full width |
| < 768px | Single column. Section bar → drawer. TOC → drawer. Body measure 72ch (≈ 45 characters at 16px, comfortable). |
Touch targets ≥ 44px. Drawers get overscroll-behavior: contain. The page
keeps overscroll-behavior: none on <html> under @media (pointer: fine)
only, so mobile pull-to-refresh still works.
2.7 Footer
--surface-2 band, padding 64px 0. Left: wordmark + one line ("SolOS
operations and product documentation"). Right: the eight sections as a
compact link list, grouped in two columns. Bottom row: © 2026 Young's Mind
· the gate note ("Internal — behind Authelia") · last-publish date.
The gate note is deliberate. A reader who screenshots a page and sends it
to someone without access should know it is not public. pulse.youngle.au is
the public surface; docs.youngle.au is not.
3. Document-level conventions
3.1 The frontmatter contract
The site reads frontmatter from the markdown. Two things must be true for that to work, and today only one of them is.
First: the frontmatter has to survive the publish lane. It does not (M23). The forge strips it:
# /home/lab/.solos/operation/ops/build_site.py
FRONT = re.compile(r"^---\n.*?\n---\n", re.S)
def conv(text):
body = FRONT.sub("", text, count=1) # <-- deletes the whole YAML block
The fix: split the file, transform the body only, and re-emit the
frontmatter. The @nuxt/content v3 collections parse YAML frontmatter
natively — the strip is not a Nuxt Content requirement, it is a leftover.
Three constraints make the naive one-line change wrong:
- The wikilink rewrite (
[label](x)→[label](x)) is a body transform. Run it over a file whose frontmatter holds arelated:list of[wikilinks](wikilinks)and it rewrites those into Markdown links inside the YAML, which breaks the parse. - 23 of the 213 files have malformed YAML — an unquoted colon in a
value (
lane: cost/footprint. Design: sol. Coordination: min.). Emitting those verbatim would make@nuxt/contentthrow on the page. - The transform must be idempotent — the forge re-runs over the vault and a non-idempotent transform corrupts content on the second pass.
The following implementation was written and tested against all 235
Docs/ files before being put in this brief. It satisfies all three:
# /home/lab/.solos/operation/ops/build_site.py
import yaml
FRONT = re.compile(r"^(---\n)(.*?)\n---\n", re.S)
def _fm_ok(fm_text: str) -> bool:
"""True only if the block is a parseable, non-empty YAML mapping."""
try:
d = yaml.safe_load(fm_text)
except Exception:
return False
return isinstance(d, dict) and bool(d)
def conv(text: str, src: str = "?") -> str:
m = FRONT.match(text)
if not m:
return body_transforms(text)
fm_text, body = m.group(2), text[m.end():]
body_out = body_transforms(body)
if not _fm_ok(fm_text):
# Malformed YAML: strip it (as today) and name the file, so the
# vault owner fixes it. Never emit unparseable frontmatter.
print(f"[build_site] WARNING: unparseable frontmatter, stripped: {src}",
file=sys.stderr)
return body_out
return "---\n" + fm_text.rstrip() + "\n---\n\n" + body_out
body_transforms() is the existing body-only transform chain, unchanged.
Measured result of that change, over all 235 Docs/ files:
| Check | Result |
|---|---|
| Frontmatter preserved | 190 (all valid YAML; 189 carry a title:) |
| Stripped with a warning (malformed YAML) | 23 — every one has a usable H1 title |
| Files with no frontmatter in the vault | 22 |
| Malformed YAML emitted as frontmatter | 0 |
| Frontmatter wikilinks rewritten | 0 (correct — they are data) |
| Body wikilinks left unrewritten | 0 |
| Not idempotent | 0 |
| Body text lost | 0 |
Verify with a count, not an eyeball. After the change, the published
tree must have frontmatter on ~190 of 235 Docs/ files, not 39 of 250.
That is the acceptance probe, and it is one grep on the repo:
grep -rl '^title:' --include=*.md content/Docs | wc -l # expect ~190, was 18
Second: the vault's frontmatter is inconsistent (M16–M17), so the contract below is what the site reads, and what happens when it is missing. It is written to be safe on today's data — i.e. before the vault is cleaned up.
| Field | Used for | If missing |
|---|---|---|
title | Page H1, nav label, <title> | Fall back to the first H1 in the body, then the filename, title-cased |
description | Page lede, meta description, search result | Empty — no lede block, no meta description. Do not synthesise from body text |
status | Status badge on the page and in lists | No badge. Do not show "unknown" |
date | Status line, sort key | Omit the date, show only the status |
author / lane | Page footer meta | Omit the block |
tags | Not rendered | — |
title needs a fallback chain, not a filename guess. With frontmatter
restored, 212 of 213 Docs/ files have a title: (M16) — but the fallback
still matters for the 23 malformed-YAML files and the 22 with no
frontmatter at all. Order: frontmatter title → first H1 in the body →
filename, title-cased. The H1 step is not decoration: most vault documents
begin with a proper # Heading that is a better title than the filename
ever would be (SCOPE.md → "SolOS v3 — scope", not "Scope"). All 23
malformed files were checked and all 23 have a usable H1.
status: is the one field worth investing in — 203 Docs/ files carry
it (M16), and it is the only metadata that tells a reader whether a document
is current. §3.2 specifies the badge mapping.
Do not render category: — it is inconsistent (M18: 23 research, 20
implementation, 9 fleet-ops, 2 security, 2 architecture, ~6% coverage
— 56 of 235 files). Fix the vault first or ignore the field. Rendering an
inconsistent field produces an unpredictable site.
Do not render related: as a visible list. It is a wikilink list, and
[X](x) inside YAML has no valid YAML value. The forge's wikilink rewrite
does not apply to frontmatter (that is the point of the body split above),
so related: stays as raw [wikilinks](wikilinks) — valid as data for a
future "related documents" feature, invalid as a rendered link. §2.1's
[Related documents] slot is populated from the nav tree, not from
related:.
Do not render authority: as a badge. It is a governance field
(whose ruling this is) with 130 occurrences, and it does not map to the
four-state reader model in §3.2. It belongs in the page footer meta, if
anywhere.
3.1a The 23 malformed frontmatter files
The malformed files are a vault defect, not a site defect, and the fix
belongs to the vault owner. Each has an unquoted colon inside a value —
lane: cost/footprint. Design: sol. Coordination: min. — which is valid
in a note-taking app and invalid in YAML. Quoting the value fixes it:
# before
lane: cost/footprint. Design: sol. Coordination: min.
# after
lane: "cost/footprint. Design: sol. Coordination: min."
The 23 files are listed in §7 as open item O8. Until they are fixed, the site still renders them correctly (frontmatter stripped, H1 title used), so this is not a blocker for the build — it is a blocker for the metadata features in §3.4.
3.2 The status vocabulary
The vault uses 30+ distinct status: values (M17). 13 cover 190 files; the
rest are singletons. The site maps them to four badges, so the reader
learns four states, not thirty:
| Badge | Vault status: values | Colour |
|---|---|---|
| Live | authority, live, implemented, done, verified, verification, manual-copy | --ok (green) |
| Design | design, design-for-review, proposal, plan, implementation-plan, study, research, draft | --info (blue) |
| Superseded | superseded, retired | --muted, strikethrough-free, with an explicit "Superseded" word — never colour alone |
| Working | in-progress, in-progress-nova-lane, draft-for-team-review, draft-for-sol, report-to-coordination, pending, open, in-progress | --warn (amber) |
Every unmapped value falls back to "Design" — never to "Live", and never to a blank. A doc with an unknown status is more likely a draft than a shipped fact, and a site that guesses "Live" is lying.
Status is shown with a word, not colour alone (WCAG 1.4.1). The badge reads "Live", not a green dot.
3.3 Body typography
The body is the product. Rules:
- Measure —
max-width: 72chon prose blocks. - Line height — 1.7 for body, 1.15–1.2 for headings. Docus's default prose is 1.6–1.7; do not compress it.
- Code — mono,
font-variant-ligatures: none. Never below 0.875rem. - Tables — full width of the body column,
font-variant-numeric: tabular-numson numeric cells. Many vault tables are cost/port tables where column alignment is the whole point. - Long tokens —
overflow-wrap: anywhereon code and table cells. Vault doc names reach 58 characters (M19) and container/port IDs are longer. - Headings —
text-wrap: balance; anchors on H2/H3 withscroll-margin-top: calc(var(--ui-header-height) + 16px). - Ellipsis —
…, never.... Curly quotes“ ”, never straight. - Loading — "Loading…", "Saving…", never "Loading...".
3.4 The status block
Below the H1 and above the lede, on documents that have both a status: and a
date::
DECISION · 2026-10-20 · Nova
Mono, uppercase, 12px, letter-spacing: 0.08em, --ink-3. This is the one
place the vault's own metadata becomes visible, and it does the job the
MANIFEST.md classification does on paper.
On section landings and list views, a document row carries a small status badge on the right — same four-colour mapping, same "word not colour alone" rule.
3.5 What the page does not show
- No "Last updated" that is not real. Nuxt Content exposes
modifiedAtfrom the file mtime, which the publish lane resets. If a real last-updated is wanted, it must come from frontmatterdate:— which is a document date, not an edit date. Do not labeldate:as "Last updated". Label it "Dated" or show it bare. - No reading-time estimate — it is wrong for reference material.
- No social share row, no comments, no "Was this helpful?" — a docs site for a gated internal audience does not need them, and each is a component to maintain.
4. Branding
4.1 The one accent
One accent colour: the brand green. No second hue anywhere on the site. The brand is the green brain; a second accent would dilute it.
The problem: the logo green and the theme green are different greens (M10).
The logo is #16a34a; the Docus theme's primary is emerald 500 #00bc7d.
#16a34a on white is 3.30:1 — it fails AA for body text and barely
clears the 3:1 non-text threshold. So today the wordmark and the links are
different greens, and the links are too light for a reading surface.
Resolution: keep the logo exactly as it is (#16a34a is the identity and
the mark is an SVG that can carry any colour), and re-key the theme to the
same hue family with an explicit shade per role, so the accent is one green
ramp, not two unrelated greens.
Decision: re-key the theme primary from emerald to green. Emerald
500 at #00bc7d is a teal-leaning green; the logo's #16a34a is a warmer,
yellower green. They read as different brands side by side. Tailwind's
green ramp is the closer match, and re-keying is a one-line change to
app.config.ts.
// app/app.config.ts — the entire branding change
ui: {
colors: {
primary: 'green', // was: 'emerald' (Docus default)
neutral: 'zinc', // unchanged — already correct
},
}
This is the only change to the palette. Everything else in §4.2–4.4 is a specification of what the existing tokens must be, so the implementer does not have to re-derive them.
4.2 The colour ramp — role to shade, measured
Values from Tailwind v4's theme.css (read on prod), hex resolved and
contrast computed by rasterising the OKLCH values in a real browser canvas
(M22). Green 500/600/700 are the ramp the green key resolves to.
| Role | Token | Light | Dark | Contrast (light / dark) |
|---|---|---|---|---|
| Links, prose accents, active nav | --ui-primary (= primary-500) | #00c950 | #00c950 | 2.22:1 ❌ / 8.97:1 ✅ |
| Link hover, active nav underline | primary-600 | #00a63e | #00a63e | 3.22:1 ⚠️ / 6.18:1 ✅ |
| Primary button fill | primary-600 (light) / primary-400 (dark) | #00a63e | #05df72 | white on 600 = 3.22:1 ⚠️ |
| Primary button fill, text-safe | primary-700 (light) | #008236 | — | white on 700 = 4.95:1 ✅ |
| Focus ring | primary-600 | #00a63e | #05df72 | 3.22:1 ✅ (needs ≥3:1) / 11.20:1 ✅ |
| Logo / wordmark | #16a34a fixed | #16a34a | #16a34a | 3.30:1 (mark, not text) |
| Body ink | --ui-text-highlighted = neutral-900 | #18181b | #fff | 17.72:1 ✅ |
| Secondary ink | --ui-text-muted = neutral-500 | #71717b | #71717b | 4.83:1 ✅ / 4.12:1 ⚠️ |
| Tertiary ink | --ui-text-dimmed = neutral-400 | #9f9fa9 | #9f9fa9 | 2.62:1 ❌ / 7.59:1 ✅ |
| Page background | --ui-bg = neutral-50 / neutral-950 | #fff | #09090b | — |
| Borders | border-default = neutral-200 / neutral-800 | — | — | — |
Three accessibility findings, each with a required action:
primary-500on white is 2.22:1 — it fails AA as link text. A reader following a link in body text gets 2.22:1. Required: prose links and inline nav links useprimary-700(light) /primary-400(dark), not the default--ui-primary. Implement by overriding theproselink colour inapp.config.tsand thecontentNavigationlink colour, not by changing the global primary — the global primary is also the button fill, where it sits on a coloured surface and passes differently.ui: { prose: { slots: { a: 'text-primary-700 dark:text-primary-400 font-medium' } }, contentNavigation: { slots: { link: 'text-default hover:text-primary-700 dark:hover:text-primary-400' } }, }- White on
primary-600is 3.22:1 — below AA for button text. A 16px button label needs 4.5:1. Required: the primary button fill isprimary-700in light mode (#008236, white on it = 4.95:1) andprimary-400in dark mode (#05df72, ink on it = 11.20:1 withzinc-950ink). Specify viadefaultVariantsonUButtonrather than per-instancecolorprops. --ui-text-dimmed(neutral-400) on white is 2.62:1 — it fails AA for any text. It is currently used for metadata (page counts, dates, the status line). Required: metadata uses--ui-text-muted(neutral-500, 4.83:1) in light mode. Reservetext-dimmedfor genuinely decorative marks, or override it toneutral-600(7.72:1).
The dark-mode text-muted is 4.12:1 — passes AA-large only. On dark
mode, bump muted to neutral-400 (#9f9fa9, 7.59:1). Both changes are in
app.config.ts under ui.colors.neutral overrides via @theme.
Do not use the logo green #16a34a for text or UI controls. It is
3.30:1 on white — fine for a mark, not for a link. This is the one place the
brand colour and the UI colour deliberately differ, and the reason is
measured contrast, not taste.
4.3 Type scale
The site uses the system font stack (M12) — no webfont, no FOUT, no
font CDN. @nuxt/fonts is installed but unused; do not add a webfont for a
reference site. The one exception is the wordmark, which is baked into the
existing logo SVG.
Base size 16px. The scale is Tailwind's, with the roles fixed:
| Role | Size | Weight | Line height | Tracking | Token |
|---|---|---|---|---|---|
| Page H1 | 30px → 36px | 700 | 1.15 | -0.02em | text-3xl sm:text-4xl |
| Section H1 (landing) | 36px | 700 | 1.1 | -0.02em | text-4xl |
| H2 | 24px | 600 | 1.2 | -0.01em | text-2xl |
| H3 | 19px | 600 | 1.3 | — | text-lg |
| H4 | 16px | 600 | 1.4 | — | text-base |
| Body | 16px | 400 | 1.7 | — | text-base |
| Lede | 18px | 400 | 1.6 | — | text-lg |
| Small / meta | 14px | 400 | 1.5 | — | text-sm |
| Micro / status line | 12px | 500 | 1.4 | +0.08em, uppercase | text-xs |
| Mono / counts | 12px | 500 | 1.4 | — | font-mono text-xs |
- Headings —
text-wrap: balance;text-prettywhere Docus already applies it (it does, onUPageHeader). - Body copy —
text-prettyon paragraphs over 3 lines;text-wrap: prettyavoids orphans. - Numbers —
font-variant-numeric: tabular-numson every table column that holds a number, and on the page counts in list rows. - Non-breaking spaces — in
10 MB,⌘ K, and in brand names like "Young's Mind" where a line break would look wrong.
4.4 Spacing, radius, elevation
Spacing — a 4px base, with the scale the site actually uses:
| Token | Value | Used for |
|---|---|---|
--space-1 | 4px | icon gaps, tight label padding |
--space-2 | 8px | inline gaps, meta label → value |
--space-3 | 12px | between a label and its value, list item padding |
--space-4 | 16px | card inner padding, row padding, grid gap (mobile) |
--space-5 | 24px | card padding, grid gap (desktop) |
--space-6 | 40px | between blocks in a landing page |
--space-7 | 64px | section padding, footer padding |
Radius — --ui-radius: 0.25rem (4px) is the Nuxt UI default and is
correct for a dense reference site. Cards use --r-md (8px), rows use 0,
pills use --r-full. Do not round everything to 16px — that is the
marketing-site look this brief is avoiding.
Elevation — border-led, not shadow-led. A reference site is flat by nature; a shadow on every card is noise.
--shadow-card: 0 1px 2px 0 rgb(24 24 27 / 0.04);
--shadow-lift: 0 4px 12px -2px rgb(24 24 27 / 0.08), 0 1px 2px 0 rgb(24 24 27 / 0.04);
Shadows appear only on hover and on overlay surfaces (dropdowns, modals, the command palette). A resting card has a 1px border and no shadow.
4.5 Logo and wordmark
- The mark — the green brain.
public/logo/light.svg(dark ink) anddark.svg(light ink) already exist and are correct. Use them; do not redraw. Both carry the brand green#16a34awith the#22c55eaccent nodes. - Header — mark at
h-6(24px) + the wordmark, as the existingAppHeaderLeftdoes. The wordmark is part of the SVG, so it does not scale with the font — ath-6the wordmark reads at ~14px, which is correct for a header. - Favicon —
public/favicon.svg, the green brain on#0e1116. Already correct. - Never stretch the logo, recolour the brain, put it on a non-white background without the dark variant, or use the wordmark alone as a logo.
- Open Graph —
defineOgImage('Docs', …)already renders a per-page OG image. Keep it; it should carry the section name, not the site name.
4.6 Motion
- Hover —
transform: translateY(-1px)+ shadow swap, 120ms,cubic-bezier(0.2, 0, 0, 1). Nav row hover: background + 2px arrow nudge, 120ms. - Transitions —
transformandopacityonly. Nevertransition: all. Every transition lists its properties. prefers-reduced-motion: reduce— disable all transforms and transitions. Not optional; it is a WCAG 2.3.3 concern and the site's users are reading for hours.- No entrance animations on first paint. A reference page must be readable the instant it paints.
- Focus —
:focus-visiblering,2px solid var(--ui-primary-600),offset 2px. Neveroutline: nonewithout a replacement. The Docus nav already carriesfocus-visible:outline-3andoutline-primary/25— keep that mechanism, and make the ring the--ui-primary-600value from §4.2 so it clears 3:1.
4.7 Voice
The site's own copy, in one paragraph: active voice, second person, specific labels. "Save API key", not "Continue". "The deployment failed at the migration step", not "An error occurred". Errors name the fix.
Headings in Title Case. Numerals for counts ("8 deployments", not "eight
deployments"). & over "and" where space is constrained.
The vault's own prose is written for engineers reading for themselves; the site does not rewrite it. Where the site adds its own copy — section scopes, landing ledes, empty states — it follows the rules above.
5. Accessibility checklist (Web Interface Guidelines)
This is the acceptance list for the implementer. It is the guidelines applicable to a documentation site, with the ones that do not apply marked.
5.1 Must pass
| # | Rule | Where it applies here |
|---|---|---|
| A1 | Icon-only buttons have aria-label | Theme toggle, menu, drawer triggers, TOC toggle |
| A2 | Form controls have a <label> or aria-label | Search input, any future filter |
| A3 | <button> for actions, <a>/<NuxtLink> for navigation | Every nav row, every card, every breadcrumb |
| A4 | Images have alt (or alt="" if decorative) | Logo (has it), OG images, any diagram |
| A5 | Decorative icons have aria-hidden="true" | Iconify icons render aria-hidden by default — verify, do not assume |
| A6 | Headings hierarchical H1–H6; one H1 per page | Landing pages and reference pages each have exactly one H1 |
| A7 | scroll-margin-top on heading anchors | calc(var(--ui-header-height) + 16px) on H2/H3 — the current build only does H1 |
| A8 | Skip link to main content | Not present today. Add one; it is the single highest-value a11y addition on a keyboard-heavy reference site |
| A9 | Semantic HTML before ARIA | <nav>, <main>, <article>, <aside> |
| A10 | Visible focus on every interactive element | :focus-visible ring — see §4.6 |
| A11 | focus-visible over focus | Do not show a ring on mouse click |
| A12 | Sticky header does not cover the focused element | Verified by the scroll-margin-top in A7 |
| A13 | aria-live="polite" on async updates | Search results count, any client-side filter |
| A14 | No outline: none without a focus replacement | Grep the final diff |
| A15 | Touch targets ≥ 44px | Mobile drawers, section bar, TOC toggle |
| A16 | overscroll-behavior: contain on drawers | Mobile nav drawer and TOC drawer |
| A17 | touch-action: manipulation on buttons | Prevents the 300ms double-tap delay |
| A18 | Text containers handle long content | min-w-0 on flex children; line-clamp-2 on card titles; overflow-wrap: anywhere on code and table cells |
| A19 | Empty states handled | A section with 0 documents must not render a broken list |
| A20 | text-wrap: balance on headings | All headings |
| A21 | … not ...; curly quotes; loading states end with … | All site-authored copy |
| A22 | font-variant-numeric: tabular-nums on numeric columns | Every table and every count |
| A23 | color-scheme: dark on <html> for dark themes | Docus handles this; verify in both modes |
| A24 | <meta name="theme-color"> matches the page background | Not present today. Add, per mode |
| A25 | prefers-reduced-motion honoured | See §4.6 |
| A26 | No transition: all | Grep the final diff |
| A27 | URL reflects state | Deep-link the expanded nav group (§5.2) |
| A28 | Images have explicit width/height | Logo, OG images — prevents CLS |
| A29 | alt="" on decorative images | The logo mark next to the wordmark is decorative when the wordmark is text |
| A30 | translate="no" on brand names and code tokens | "Young's Mind", container names, port IDs |
5.2 Deliberately out of scope
Not applicable to a gated, read-only reference site: form validation
patterns, unsaved-changes warnings, destructive-action confirmation, file
upload, drag-and-drop, gesture-only actions, autoplay media, carousel
controls, focus-trap behaviour beyond what UDrawer/UModal already provide,
and i18n locale switching (the site is English-only by vault rule 8).
5.3 One additional rule this site earns
Deep-link the expanded nav group. A reader who shares
/docs/fleet/03-decisions expects the Decisions group to be open when they
arrive, and their colleague's link to land on the same view. Encode the
expanded group in the URL (?group=decisions or a path segment), and read
it back on load. This is the "URL reflects state" rule, applied to the one
piece of state this site actually has.
6. Implementation notes for whoever builds this
The stack is not in question. Docus 5.12.3 / Nuxt 4.5.1 / Nuxt UI 4.10.0
/ Tailwind 4.3.3 (M1). The repo already overrides upstream Docus files with
small diffs — [docs-nav-v2](docs-nav-v2) documents a 2-line diff in
app/pages/[lang](lang)/[...slug].vue. The changes in this brief are the same
shape: override app.config.ts theme slots, override
app/pages/[lang](lang)/[...slug].vue where the layout needs it, and adjust
app/assets/css/sidebar.css.
Do not fork Docus. Build on top, as [docs-nav-v2](docs-nav-v2) established.
The publish lane is not affected. The forge (build_site.py) mirrors the
vault; the section reorganisation is a navigation and landing layer, not a
content move. The vault paths stay the URLs. The one place the forge changes
is the PRETTY label dictionary (§2.5) and the two landing pages it
generates (Docs/index.md, Pulse/index.md).
One caveat on the storefront. The card notes the storefront is Nuxt 3 +
@nuxt/ui 2.18 (M21 confirms nuxt ^3.14.0 + @nuxt/ui ^2.18.0, 2.22.3
installed). The Docs site is a different stack — Nuxt 4 + Nuxt UI 4 +
Tailwind 4. The token values in §4 are Tailwind v4 / Nuxt UI v4 values and
do not transfer to the storefront. What transfers is the structure:
one accent, a named role per shade, a measured contrast floor, a 4px spacing
base, and the same template patterns. The storefront's own brand work is a
separate brief in a separate lane.
6.1 Work order
Each step is independently shippable and independently verifiable.
| # | Step | Verifiable by |
|---|---|---|
| 1 | Fix the nav — collapsible: true, filter the tree to the active section (§2.3) | The aside renders ≤ 40 links, not 249 |
| 2 | Fix the grid — 3 / 5 / 2, delete the !important grid override (§2.4) | No horizontal overflow at 1024–1440px; the TOC is visible at ≥1024px |
| 3 | Re-key the palette — primary: 'green', apply the §4.2 shade-per-role overrides | The contrast table in §4.2 holds, measured in a browser |
| 4 | Add the section layer — the eight sections, the label dictionary, the landing pages (§1, §2.5) | Each section landing renders the directory rows for its content |
| 5 | Apply the templates — T1, T2, T3, T4 (§2.1) | A reference page, a landing, and a card list all render per spec |
| 6 | Typography and prose — 72ch measure, the §3.3 rules (§3.3) | Measure is 72ch; no line exceeds it in a body paragraph |
| 7 | Status badges — the four-badge mapping (§3.2) | Every doc with a status: shows one of four badges with a word |
| 8 | Accessibility — the §5.1 checklist | Each row of §5.1 is a pass or a filed defect |
| 9 | Frontmatter — the description: field added to the vault where it matters (§3.1) | Section landing ledes and page meta descriptions are populated where they matter |
Step 9 is a vault task, not a site task — it is the only step that changes content rather than presentation, and it is the slowest. It is listed last on purpose: the site is shippable without it, because §3.1 specifies a fallback for every missing field.
6.2 What is explicitly NOT in this brief
- The marketing site (acceptance criterion 5). No hero, no feature grid, no pricing, no testimonials, no CTA banner.
- The Pulse public site —
[pulse-homepage-design](pulse-homepage-design). - The fleet dashboard —
[fleet-dashboard-design-of-record-2026-10-20](fleet-dashboard-design-of-record-2026-10-20). - The Authelia gate — Sol's infra lane, already live and verified (M15).
- The tenant storefront / admin console — separate products, separate briefs, a different Nuxt version.
- Implementation — this is a design of record. Nova does not implement production code; the build lane is Sol's.
7. Open items — for Young, and for Min
Ordered by what they unblock. None of them blocks steps 1–3 of §6.1.
| # | Open question | Recommendation | Owner | Blocks |
|---|---|---|---|---|
| O1 | Do the eight reader-facing section names have Young's approval? The vault names (Fleet, Projects, SolOS) are storage names; the site names (Product, Research, Decisions, Build & Run, Operations, Projects, Tenancy Platform) are reader names. This brief takes the second set. | Approve the eight names, or supply replacements. Renaming afterwards means re-touching the label dictionary and eight landing pages. | Young | Step 4 |
| O2 | /docs/agents route collision (M20) — AGENTS.md and SolOS/agents/ both want the same path. | Move AGENTS.md under Start Here as "Vault Operations" (§1.5). Requires a forge change and a redirect. | Sol (forge) + Min | Step 4 |
| O3 | The empty SolOS/subscriptions/ directory renders as a nav node leading nowhere. | Gate it — render the node only when it has content. One line in the forge. | Sol | Step 4 |
| O4 | Frontmatter coverage. 61 of 1,798 files have a description:. §3.1 falls back gracefully, so this is not a blocker, but page ledes and meta descriptions are thin without it. | A vault pass adding description: to the highest-traffic documents (the Fleet/manual/ set and the section index files) — 15 files, not 1,798. | Min (register) + doc owners | Step 9 |
| O5 | status: vocabulary is 30+ values. §3.2 maps them to four badges with a "Design" fallback, so the site works today, but the underlying vocabulary should narrow. | A vault ruling narrowing status: to the four badge values plus an "other" bucket. The site does not need to wait. | Min | Nothing — §3.2 is the default |
| O6 | Should the Docs site keep the two-section Docs / Pulse split in its own header? M7 shows the tabs work. Pulse is the public surface; a gated reader does not need a link to it in the header. | Keep the tab but label it "Pulse (public)" so the audience boundary is explicit. | Young | Step 2 |
| O7 | The dashboard route. [fleet-dashboard-design-of-record-2026-10-20](fleet-dashboard-design-of-record-2026-10-20) §10 Phase 2 assigns the page to Nova and is still open. The dashboard's data layer (Phase 1) is not built. | Confirm Nova's dashboard page work is in scope for a separate card, so this brief's T5 does not get built twice. | Min | T5 only |
What this brief does not decide: anything about the Authelia gate's
upgrade path (2FA / O365 SSO — [authelia-optimization](authelia-optimization) and the 2FA
spec), anything about which documents are "curated" versus mirrored (Stage 5's
"curated from LAB, not vault mirror" — this brief specifies the shape of the
site and is deliberately compatible with either a full mirror or a curated
subset), and anything about the tenant onboarding of the site itself (Stage 5
moves it into the tenant vhost; that is Sol's lane and it does not change the
design).
8. What I measured but did not change
Recorded so the next session does not re-derive them.
- The publish lane works.
docs-publish.sh(LAB, every 10 min) →solos-docsrepo →docs-pull.sh(prod) → build → restart. Verified live: the prod tree was at4ab36bc, matching LAB'sorigin/main, with a clean build and an active service. A redesign must not disturb this lane. - The auth gate works and is correct.
302→auth.youngle.au, forward-auth include, zeroauth_basicoccurrences. Docs/AGENTS.mdis the only vault doc that describes the site itself — and it is stale (it describes the retiredsolos-docs-watcher/port.pysync, which was replaced by the git publish lane on 2026-10-21). It is parked as a vault-operations page (§1.5) rather than deleted, because the vault is the source of truth and deleting it is not this brief's call. It should be corrected — in the vault, by its owner — not by the site implementer.- The
MANIFEST.mdclassification is the site's freshness mechanism.Fleet/manual/MANIFEST.mdalready classifies every document as CURRENT or HISTORICAL. Astatus:badge per §3.2 is the visible version of that classification. If the manifest and the badges disagree, the manifest wins.
Register: Min (vhost index + cross-links). Build lane: Sol. Content
lane: Tam (forge + publish). Design: Nova (this document).
Related: [prod-implementation-stages-2026-10-18](prod-implementation-stages-2026-10-18) (Stage 5),
[pulse-docs-redesign-brief](pulse-docs-redesign-brief), [pulse-homepage-design](pulse-homepage-design),
[fleet-dashboard-design-of-record-2026-10-20](fleet-dashboard-design-of-record-2026-10-20),
[vault-structure-authority-2026-10-18](vault-structure-authority-2026-10-18), [fleet-ground-rules-2026-10-20](fleet-ground-rules-2026-10-20).
Docker tenancy implementation plan
Cross-customer MUST-FAIL test + eval suite gate — build record
Card: t_b18ed62d (board docker-vhost, assignee sol, created by tam). Supersedes: card t_1beb171c (board solos-v3, archived) — same scope, incomplete: the prior session hit its 150-iteration budget mid-run with 3 of 7 evals failing. Its board was rotated to _archived/; the source tree was recovered from _archived/solos-v3-1792495130/workspaces/t_1beb171c/eve-cs.

