Skip to content

Cosmos Nav Playbook

How the cross-planet navigation rail works across the seven worlds of A Guide to Cloud, what each planet's render looks like at every viewport, animation patterns, and the common gotchas you only learn from breaking them.

TL;DR (current as of 8 May 2026)

Every planet vendors a planets.json (or planets.toml) manifest with the same data: slug · name · url · tagline. Each planet's render code chooses its own affordance — but the 7 planet-icon SVGs are now a shared visual contract rendered identically in every planet's masthead (Earth lotus image, Guided crescent moon, CMD $_, Shift dial, Plain AI bubble, Agentic Signal LED, Claw Bracket Mark [*]). Frame around them stays atmospheric per-planet. No chip frame around any cosmos icon. Mobile patterns differ per planet — see Mobile patterns.

🆕 8 May 2026 — Expanded from 4 worlds to 7 worlds

This section is the canonical update reference for any session picking up cosmos work after 8 May. The historical body below is preserved as architectural context — the architecture didn't change, only the count and some names.

What changed in one sentence: The cosmos rail grew from a 4-icon contract (earth · brainbar · shift · plainai) to a 7-icon contract (earth · guided · brainbar · shift · plainai · agentic · claw), and CMD replaced "Brain Bar" as the user-facing display name (slug brainbar kept for code stability).

What's new

Slug Display name Tagline Mark
guided Guided cert prep — affordable practice exams 🌙 Indigo crescent moon (#6366F1) — Earth's Moon
agentic Agentic agent cockpit — for techies building with agents 🔵 Pulsing cyan Signal LED (#22D3EE, 2.4s) — cockpit register
claw Claw OpenClaw study reference — plain English [*] Bracket Mark — currentColor brackets, OpenClaw red asterisk

What was renamed

  • brainbar slug name: "Brain Bar" → "CMD" in all 5 manifest mirrors
  • Slug stays brainbar (folder paths, filter logic, internal references unchanged)
  • Display name "CMD" surfaces in cosmos rail aria-labels, About-page references, OG cards

Universal Law #7 — Lotus-as-default fallback (ADDED 8 May)

Codified in cosmos-philosophy.md:

  • The Earth Lotus (logo_agtc_dark_1.webp) is the canonical brand mark for the entire universe.
  • Every planet has TWO logo surfaces: tab favicon + masthead = host identity (lotus for Earth-family); cosmos rail + atlas = cosmic role (each planet's own purpose-built mark).
  • Purpose-built planets use the same mark on both surfaces.
  • Framework defaults (Astro/Vite/Next placeholders) are a brand violation.
  • Operationally: when a new planet is scaffolded, the first deploy MUST include the lotus favicon as a placeholder until the purpose-built mark is ready.

Drift audit script — Phase 5 governance

learning-docs/scripts/cosmos-drift-audit.mjs audits the cosmos for three drift surfaces:

  1. Manifest drift — 5 mirrors (2 TOML + 3 JSON) compared field-by-field
  2. Icon palette coverage — 5 mirrors checked for slug coverage; flags any manifest slug not handled
  3. Hardcoded label overrides — flags any slug→label dictionary that drifts from manifest p.name
cd learning-docs && node scripts/cosmos-drift-audit.mjs
# exit 0 = clean · exit 1 = drift detected (fix before commit)

This is the one-command sanity check before any cosmos-touching commit. Run before push, run as a pre-deploy CI gate.

Files updated 8 May 2026

5-mirror commit set (icon palette expansion):

File Repo Commit
layouts/partials/planet-icon.html aguidetocloud-revamp 0135e16b
brainbar/layouts/partials/planet-icon.html aguidetocloud-revamp 0135e16b
src/components/layout/PlanetIcon.astro guided a296596
src/components/PlanetIcon.astro shift cd71094
build-shared.mjs planetIcon() + masthead() plainai c0170eb

Brand-mark commits:

Where Repo Commit
Earth static/favicon.svg (☁️ → lotus) aguidetocloud-revamp 1cdab7b8
Guided public/favicon.svg (Astro placeholder → lotus) + lotus webp + apple-touch-icon guided e065588
Agentic public/favicon.svg + Nav.astro (planet+orbit → Signal LED) agentic-planet da9da14

Doc bookkeeping:

  • cosmos-philosophy.md — Universal Law #7 added; "Earth A badge" reference corrected to "Earth lotus"
  • cosmos-audit.md — Phase 4a/4b/4c/5 status updated
  • voice-drift-2026-05-08.md — Phase 4 inputs section corrected
  • logo-audit-2026-05-08.md — comprehensive walk of every planet's brand mark across every rendering surface
  • mkdocs.yml — logo audit added to Cosmos nav

What stayed the same

  • Architecture: data-shaped manifest, per-planet rendering, mobile breakpoints per planet, no chip frame, 44×44 tap targets
  • Atmosphere autonomy: each planet picks its own colors / fonts / register
  • One Body Two Organs: Earth + Guided still mirror chrome bytes
  • Visual contract: each planet's icon brings its own brand color (Shift red, Plain AI gradient, Agentic cyan, Claw red, Guided indigo) — none use generic black/white
  • Per-planet name field in manifest — still drives all aria-labels (no more PLAINAI_PLANET_LABELS override drift surface)

Adding the 8th planet (one-liner)

Read Cosmos Audit § New Planet Onboarding Checklist for the full step-by-step. Critical bits: 5-file manifest sync, 5-file icon palette sync (one commit each), drift audit must exit 0, lotus as fallback favicon until purpose-built ships.


Why this exists

Read Cosmos Philosophy first. Short version: each new product (planet) gets its own atmosphere — own font, palette, voice, tech. The only universal laws are quality, value, usability, honesty, don't collide, and no paid content crosses cosmic boundaries via free pathways.

Until 5 May 2026, the cosmos was invisible from inside any planet. Brain Bar had a tiny // aguidetocloud.com back-link in its header, but Earth had no way to discover Shift; Plain AI had no way to mention Brain Bar; Shift's sidebar listed Earth + Brain Bar but had hardcoded the list (and was missing Plain AI by the time it shipped).

The fix: a shared data manifest that each planet renders in its own register. Same data → different visual treatment per atmosphere. On 6 May 2026 the rendering ALSO became shared (icons, not just data) — see The 6 May rework.

The manifest schema

Every planet vendors a copy of the cosmos manifest. Two formats — TOML for Hugo planets, JSON for everything else.

Top-level structure

# Cross-cutting cosmos config (newsletter URL, feedback link, parent site)
[cosmos]
newsletter_url    = "https://guided-weekly.beehiiv.com/subscribe"
newsletter_label  = "A Guide to Cloud Newsletter"
newsletter_pitch  = "Cloud & AI insights, every Sunday — in 2 minutes. Free."
feedback_url      = "https://www.aguidetocloud.com/about/"
parent_site       = "https://www.aguidetocloud.com/"

# Planets list (data-shaped only — no visual fields)
[[planets]]
slug    = "earth"
name    = "A Guide to Cloud"
url     = "https://www.aguidetocloud.com/"
tagline = "the home — certs, tools, mind maps"

# … repeat for brainbar, shift, plainai

For details on the [cosmos] block (newsletter URL, etc.), see Cosmos Config Manifest.

Planet entry fields

Field Type Purpose
slug string, lowercase, no spaces Stable identifier. Used by render code to filter self out.
name string Human-readable display name ("A Guide to Cloud", "Brain Bar").
url string, must end with / Canonical homepage URL of the planet.
tagline string One-sentence positioning. Each planet picks how/whether to render it.

No visual fields. No icon URLs, no monogram, no color tokens. Visual decisions belong to each planet's render code. (Hardcode any planet-specific monograms/labels in the rendering code, NOT in the manifest.)

File map — where the manifest lives in each repo

Planet Manifest path Format
🌍 Earth (Hugo) aguidetocloud-revamp/data/planets.toml TOML — Hugo idiom
🌑 Moon (Astro Guided) guided/src/data/planets.json JSON — Astro idiom
🪐 Brain Bar (Hugo) aguidetocloud-revamp/brainbar/data/planets.toml TOML
🪐 Shift (Astro) shift/src/data/planets.json JSON
🪐 Plain AI (custom build) plainai/content/planets.json JSON — slurped by build.mjs + build-changes.mjs via build-shared.mjs

The 6 May 2026 rework — summary

This section captures the architectural changes you must understand before touching any nav file. Two big shifts:

1. Visual parity exception (icons-everywhere)

Was: each planet rendered the data manifest in its own register — Earth used SVG icons, Brain Bar used // comment text links, Shift used caps-mono monograms, Plain AI used caps-mono full-name labels.

Now: the 4 planet-icon SVGs are a shared visual contract. Every planet renders the same SVG palette in its masthead. Frame/CSS around them stays atmospheric (newspaper for Shift, dark for Plain AI, terminal for Brain Bar). Other cosmos surfaces (sidebars, page lists, footers) may still use editorial/text/emoji registers if their local purpose demands it — the icon contract is masthead-only.

2. No chip frame around any cosmos icon

Was: each icon was wrapped in a 36×36 circular chip with a 1px border (.planet-chip). The chip carried the click target and a hover background.

Now: the wrapper (.planet-chip / .cosmos-chip / .bb-cosmos-chip) is a bare click target — no border, no fixed dimensions, no border-radius. Each icon's native shape carries identity:

  • Earth — circular lotus image
  • Brain Bar $_ — terminal glyph
  • Shift — circular dial+needle
  • Plain AI — speech bubble with tail

The wrapper still needs min-width: 44px; min-height: 44px for tap targets, but no visual frame.

3. Earth icon — lotus image, not "A" badge

Was: Earth's slug rendered as an indigo circle with white "A" monogram.

Now: Earth's slug renders the actual lotus brand mark image — <img src="/images/logo_agtc.webp"> (or logo_agtc_dark_1.webp on Earth itself) wrapped in a 50% border-radius. Brain Bar / Shift / Plain AI each vendor a copy of the WebP under their static/images/ or public/images/ — see Lotus image vendoring below.

4. New Plain AI icon — speech-bubble-as-chip

Was: gradient-filled rectangle ("polaroid"). Plain AI's first visual concept used a museum-exhibit framing that was retired 8 May 2026; the visual register (dark + gradient) stays.

Now: outline speech bubble (currentColor stroke, 1.6px) with "AI" rendered inside in cyan→purple→pink gradient. ViewBox 0 0 38 30. Renders at 30×24 (height 24 to match other icons; non-square aspect preserved). Used as both the cosmos sibling icon AND the brand mark in Plain AI's own masthead. Same SVG content as the favicon (which uses solid-fill variant for legibility at 16×16).

5. Animations (SMIL inside SVG)

Each icon animates subtly — self-contained SMIL inside the SVG, no extra CSS or JS:

  • Brain Bar $_ — the underscore line blinks via <animate attributeName="opacity" values="1;0" keyTimes="0;0.5" dur="1.2s" repeatCount="indefinite" calcMode="discrete"/> (matches the CSS-driven blink on Earth/Moon's .bb-launch-btn palette button).
  • Shift dial — needle sweeps ±3° around centre over 4s (<animateTransform type="rotate" values="-3 30 30;3 30 30;-3 30 30" dur="4s">) and the active-sector path breathes opacity over 3s.
  • Earth lotus + Plain AI bubble — currently static. Animation could be added similarly (preferred SMIL pattern for consistency).

Why SMIL not CSS keyframes: SVG-internal <animate> and <animateTransform> keep the icon self-contained — copies into Hugo + Astro + Plain AI build script render identically without any global CSS dependency. SMIL is well-supported in all modern browsers (the deprecation rumours never landed).

How each planet renders the rail (current state)

Per cosmos philosophy, each planet picks its own affordance for the surrounding chip frame. Below is the post-6 May state.

🌍 Earth (Hugo) — bare SVG icons

Location: left side of nav, grouped next to logo with hairline divider. Rendering: SVG icons inline, no chip frame. Brain Bar still uses its existing $_ button (palette launcher), since on Earth itself the launcher opens the in-page palette rather than navigating away. Layout: [A logo] | [$_] [Shift dial] [Plain AI bubble] Exams · Guides · Tools · Maps · Videos · Licensing · Blog · About [theme] [Downloads] Mobile: cosmos hidden via @media (max-width: 1180px) { .nav-left .cosmos-group { display: none } }. Drawer drops down with all 8 nav links + a "// the cosmos" rail with all 3 sibling planets (lotus image + tagline). Files: layouts/partials/nav.html · layouts/partials/planet-icon.html · static/css/style.css · data/planets.toml · static/images/logo_agtc_dark_1.webp (already there as the brand mark)

🌑 Moon — pixel-mirror of Earth

Same layout, same files in guided/src/components/layout/. One Body Two Organs rule. Cross-domain image works via shared aguidetocloud.com domain — no separate vendoring needed for Moon (/images/logo_agtc_dark_1.webp resolves to Earth's static deploy).

🪐 Brain Bar (Hugo) — bare icons in terminal mono

Location: LEFT side of header next to brand, in a .bb-header-left flex wrapper with hairline divider (matches Earth's left-aligned cosmos pattern). Rendering: bare SVG icons (.bb-cosmos-chip wrapper — no frame, just hover color shift to var(--accent)). 44×44 minimum tap target. Mobile: @media (max-width: 540px) { .bb-cosmos-rail { display: none } }. Brain Bar's primary surface IS the palette — secondary nav appropriately hidden on mobile. Files: aguidetocloud-revamp/brainbar/layouts/_default/baseof.html · brainbar/layouts/partials/planet-icon.html (vendored) · brainbar/static/css/cmd.css · brainbar/static/images/logo_agtc.webp (vendored, 11.4 KB)

🪐 Shift (Astro) — bare icons in newspaper register

Location: LEFT side, inside a .brand-group flex wrapper that occupies the brand grid area (immediately right of SHIFT wordmark). Rendering: bare SVG icons (.cosmos-chip wrapper). Hover: ink-3 → ink (paper aesthetic). 44×44 minimum tap target. Hairline divider between brand wordmark and cosmos. Mobile: @media (max-width: 860px) { .brand-group .cosmos-rail { display: none } }. Shift's primary surface on mobile is the role lookup, not cross-planet nav. Files: shift/src/layouts/BaseLayout.astro · shift/src/components/PlanetIcon.astro (vendored) · shift/public/images/logo_agtc.webp (vendored) Note: RailCosmos.astro (sidebar widget) and about.astro (cosmos map list) intentionally NOT changed — they're editorial surfaces with their own emoji/text register. The masthead-only icon contract.

🪐 Plain AI (custom Node build script) — bare icons in dark register

Location: integrated INTO masthead next to wordmark via .brand-group wrapper. Was a separate .meta-bar strip above the masthead until 6 May; now consolidated. Rendering: bare SVG icons (.cosmos-chip wrapper, 44×44 minimum). Hover: muted → ivory. Mobile: @media (max-width: 640px) { .brand-group .cosmos-rail { display: none } }. Plus a tighter single-row masthead at <=600px — see Plain AI single-row mobile recipe. Files: plainai/build-shared.mjs (planetIcon() + metaBar() (deprecated, returns empty string) + masthead() accepts siblingPlanets) · plainai/templates/styles.css · plainai/content/planets.json · plainai/public/images/logo_agtc.webp (vendored)

Earth icon — lotus image vendored per planet

Why vendor instead of cross-origin: Brain Bar (cmd.aguidetocloud.com), Shift (shift.aguidetocloud.com), Plain AI (plainai.aguidetocloud.com) are different subdomains with their own Cloudflare Pages projects. Cross-origin requests for an inline <img> tag work but add a DNS lookup + handshake. Vendoring the 11.4 KB WebP locally on each subdomain is cleaner.

Per-planet paths:

Planet Image path Markup src
Earth aguidetocloud-revamp/static/images/logo_agtc_dark_1.webp /images/logo_agtc_dark_1.webp (already used as brand mark)
Moon (none — uses Earth's domain) /images/logo_agtc_dark_1.webp (cross-path on same origin)
Brain Bar aguidetocloud-revamp/brainbar/static/images/logo_agtc.webp /images/logo_agtc.webp
Shift shift/public/images/logo_agtc.webp /images/logo_agtc.webp
Plain AI plainai/public/images/logo_agtc.webp /images/logo_agtc.webp

Markup template (in each planet's planet-icon partial / component / function):

<img src="/images/logo_agtc.webp"
     width="24" height="24" alt="" loading="lazy" decoding="async"
     style="border-radius:50%;object-fit:cover;display:block" />

alt="" because the surrounding <a> has the aria-label and title attributes that name the planet. The image itself is decorative.

Mobile patterns (per planet)

The mobile breakpoints differ deliberately by planet because each planet has different dominant surfaces.

Planet Mobile breakpoint Cosmos rail visible? Why
Earth <=1180px Hidden in nav · visible in drawer with names + taglines The 8-link flat nav + cosmos + Downloads CTA needed the bigger breakpoint to fit at desktop. Drawer rail is rich (tagline included) because mobile users rarely use cosmos so when they do, give them context.
Moon <=1180px Same as Earth (mirror) One Body Two Organs
Brain Bar <=540px Hidden Brain Bar's primary surface is the $_ palette. Cosmos is genuinely secondary — hide it to save space.
Shift <=860px Hidden Same logic — newspaper masthead + Quiz/All/Compare/Method/About is the primary surface, cosmos is supporting.
Plain AI <=640px Hidden Same logic — at narrow widths, cosmos chips fight for attention with the bubble brand mark + 4 primary nav links.

Tap target standard — 44×44 minimum

iOS Human Interface Guidelines + WCAG 2.5.5 (Level AAA) call for at least 44×44 CSS pixels per touch target. WCAG 2.2 AA dropped to 24×24 minimum but iOS 44×44 is the better target.

Applied across all planets (6 May 2026):

  • .planet-chip, .cosmos-chip, .bb-cosmos-chipmin-width: 44px; min-height: 44px. Icons stay at their intrinsic size (24px or 30×24); the wrapper provides padding for the tap area.
  • .theme-toggle — 44×44 (was 36×36 on Earth/Moon)
  • .site-logo-img-wrap — 44×44 (was 36×36)
  • .bb-theme-togglemin-width: 44px; min-height: 44px (was 39×33)
  • .masthead-toggle (Shift hamburger) + #theme-toggle (Shift theme) — min-width: 44px; min-height: 44px
  • .search-launcher (Plain AI) — min-height: 44px (was 40)

Pattern for compact nav links that must stay tappable:

.masthead-link {
  padding: 0.35rem 0.4rem;     /* compact visual */
  font-size: 0.7rem;
  min-height: 44px;             /* tap area still 44 */
  display: inline-flex;         /* min-height needs flex/block */
  align-items: center;
}

Plain AI single-row mobile recipe

Plain AI mobile masthead got squeezed onto ONE line via these rules at the END of templates/styles.css (append-only to avoid collision with concurrent curriculum work in build-shared.mjs etc.):

@media (max-width: 600px) {
  .masthead, .masthead-actions { flex-wrap: nowrap; }
  .primary-nav {
    order: 0;          /* override the older order:99 push */
    width: auto;       /* override the older width:100% */
    margin-top: 0;
    padding-left: 0.5rem;
    margin-left: 0.25rem;
    border-left: 1px solid var(--hairline-soft);  /* visual divider */
  }
  .primary-nav .masthead-link {
    padding: 0.35rem 0.35rem;
    font-size: 0.7rem;
    min-height: 44px;
    white-space: nowrap;     /* ← critical: stops "What changed" → "What\nchanged" */
  }
  .wordmark .name, .wordmark .ai { display: none; }  /* bubble carries identity */
  .search-launcher { display: none; }                /* Cmd+K still works for kb users */
  .theme-toggle { width: 44px; min-width: 44px; }
  .theme-toggle .theme-divider { display: none; }
  /* Single-icon theme toggle: shows the OPPOSITE of current mode */
  :root:not([data-theme]) .theme-toggle .theme-dark,
  :root[data-theme="dark"] .theme-toggle .theme-dark { display: none; }
  :root[data-theme="light"] .theme-toggle .theme-light { display: none; }
}

@media (max-width: 480px) {
  /* Drop the 4th nav link ("About") — reachable from footer */
  .primary-nav .masthead-link:nth-child(4) { display: none; }
}

@media (max-width: 380px) {
  .primary-nav .masthead-link { padding: 0.3rem 0.25rem; font-size: 0.68rem; }
}

Result at 375px: bubble + hairline + Topics · Learn · What changed + ☀ (single theme icon, swaps to ☾ in light mode). 69px tall. No overflow. Search hidden. About hidden (recoverable from footer).

The :nth-child(4) selector targets "About" because it's the 4th link in DOM order: Topics, Learn, What changed, About. If the build code reorders them, update this selector.

Common gotchas (lessons paid for in production)

A list of things that look right in the audit data but render wrong, OR look wrong in the audit data but render fine. Real bugs from the 6–7 May rework.

1. Stale CSS from a previous layout

When you change layout (e.g., move cosmos out of one container into another), the OLD media-query rules don't auto-disappear. They keep firing wherever their selectors match.

Bug: After moving Shift's cosmos rail from end-of-nav into .brand-group, the legacy @media (max-width: 860px) { .cosmos-rail::before { content: "// other wires"; ... } } rule kept firing — now showing a phantom mono-caps label DANGLING above the chips inside the brand-group. Plus the chips were forced to flex-wrap: wrap; border-top: 1px solid which made them stack vertically next to the brand.

Fix: when moving an element, search the file for ALL rules selecting it and either delete or rewrite them. Don't just add new rules and assume the old ones are inert.

2. overflow-x: auto silently clips children

If you put overflow-x: auto on a flex container to allow horizontal scrolling, children that don't fit are CLIPPED, not just scrollable. A scrollbar would show — but scrollbar-width: none; ::-webkit-scrollbar { display: none } (often used to hide ugly scrollbars) makes the clip silent.

Bug: Plain AI mobile masthead used overflow-x: auto on .masthead-actions to "let the nav scroll if it can't fit". The audit reported overflow=0 because docW didn't extend past viewport. But the LAST nav link (About) was off-screen with no visible scrollbar. Users saw 3 of 4 links and didn't know.

Fix: don't use overflow-x:auto to hide content. Either let it overflow visibly (so users notice) OR explicitly hide overflowing items via display: none selectors.

3. white-space defaults to normal — text wraps inside flex items

Multi-word link labels like "What changed" break across 2 lines if their flex item gets narrower than the text wants. The flex item grows vertically to accommodate. Audit doesn't catch it because flex height is masked by min-height: 44px (the tap-target rule).

Bug: Plain AI's .masthead-link had no white-space: nowrap. At 360-414px viewports, "What changed" rendered as "What" / "changed" stacked. The min-height:44 made each link's bounding box still 44 tall, so the audit's "is this link 44 tall = wraps" check returned false negatives (everything was 44 tall regardless).

Fix: always add white-space: nowrap to .masthead-link (or any horizontal-nav link with multi-word labels). Test wrapping detection by comparing offsetHeight to scrollHeight on the link, OR by computing lineHeight × 1 and comparing.

4. CSS Grid tracks expand to content min-width by default

CSS Grid auto-tracks (e.g., grid-template-columns: 1fr 1fr) respect each item's content minimum width. A long unbreakable word inside a grid item PUSHES the track wider — and if the word exceeds the viewport, the document scroll-width grows past viewport.

Bug: Moon's /guided/explore/ cert grid had cards like paloalto-netsec-professional and hashicorp-terraform-associate. These long unbreakable strings forced a 2-column grid track to ~206px each at a 375 viewport. Document overflow: 37px.

Fix:

.zt-lic-card { min-width: 0; }    /* allow grid track to shrink below content */
.zt-lic-card-code, .zt-lic-card-name {
  overflow-wrap: anywhere;
  word-break: break-word;
}

5. Marquee tickers leak document scroll-width without overflow:hidden

A scrolling marquee uses padding-left: 100% + transform: translateX(-50%) on its inner content. The inner element is intentionally several screens wide (looped). Without overflow:hidden on the parent, that width contributes to document scrollWidth.

Bug: Shift's .tape element (the "HEAVY AI PRESSURE · ACCOUNTANT…" ticker) had no overflow:hidden. The inner .tape-content was 9700px wide. Document overflow: 60px on mobile.

Fix: overflow: hidden on .tape. Always check marquee containers explicitly.

Default <a> tags in a flex row don't wrap. A line of 5 footer links separated by · will extend past viewport on phones. Same for long em-dash phrases like "— THE AI JOB-CHANGE WIRE — VOL. 1 · NO. 19" with no whitespace breaks.

Bug: Shift's .masthead-foot-meta (5 footer links) had no flex-wrap. Plus .byline with em-dashes had no overflow-wrap. Combined: 60px overflow at 375.

Fix: flex-wrap: wrap on link rows; overflow-wrap: anywhere on long phrases.

7. PowerShell Invoke-WebRequest returns false 500 errors

Invoke-WebRequest fails with HTTP 500 on requests that work fine in curl. TLS handshake or User-Agent issue. Don't trust it for SLA checks.

Bug: Mid-session SLA check via Invoke-WebRequest returned 500 on the questions JSON endpoint. Sush was about to investigate a paid-product outage. Direct curl.exe showed all green — the 500 was a PowerShell artefact, not a real outage.

Fix: always use curl.exe (full path or curl.exe not curl alias) for SLA verification. The aliased curl in PowerShell maps to Invoke-WebRequest which has the bug.

8. Hugo minifies attribute quotes — your regex tests need to handle both forms

Hugo's HTML minifier strips quotes around attribute values when safe: class="bb-header-left" becomes class=bb-header-left. If your post-deploy regex check looks for class="bb-header-left", it returns 0 matches even though the markup is correct.

Bug: After deploying Brain Bar, the verification regex class="bb-header-left" reported 0 matches. Looked like deploy failed. Actually deploy was fine — the live HTML had class=bb-header-left (no quotes).

Fix: write regexes that accept both quoted and unquoted forms: class=["]?bb-header-left or test for the class via DOM (document.querySelector('.bb-header-left')).

9. min-height: 44px masks wrap detection in audits

If you measure a link's offsetHeight to detect wrapping ("if h > 32, it wrapped"), the min-height:44 tap-target rule will inflate every link's height to 44 regardless. Your detector returns false negatives.

Fix: detect wrapping by comparing offsetHeight to scrollHeight, OR by measuring the natural single-line height first (without min-height) and comparing.

10. Tape/marquee overflow:hidden on parent ≠ document overflow

overflow: hidden on a parent element clips its CHILD content visually. But if the child has position: absolute or is inside a flex/grid that lets it grow, the document scrollWidth may still expand. Check with document.documentElement.scrollWidth not just visual rendering.

11. Parent flex container flex-wrap: nowrap doesn't stop child overflow

flex-wrap: nowrap on the parent makes children stay on one line. If the children's combined width exceeds the parent, the LAST child overflows — sometimes silently if overflow-x: auto is present or min-width: 0 lets children compress weirdly.

Lesson: when forcing a single-row layout on mobile, also explicitly: - flex-shrink: 0 on items that must not compress (like the brand mark) - flex-shrink: 1 on items that should compress first - Test at multiple widths (320/360/375/390/414/480/600) — most bugs only appear at one specific width

12. display: none of a 4th nav item via :nth-child is brittle

If the build code reorders nav links, the :nth-child(4) selector now hides the wrong link. CSS doesn't know what "About" means.

Fix: prefer attribute selectors when possible (.masthead-link[href="/about"]) so the rule survives DOM reordering.

Animations (SMIL inside SVG)

Why not CSS keyframes:

  1. The 5 vendored copies of each icon must animate identically. CSS keyframes require GLOBAL CSS that all 5 stylesheets share — collision risk + parity drift.
  2. SMIL is self-contained inside the SVG. Copy-paste the SVG anywhere, animation comes with it.
  3. SMIL is well-supported in all modern browsers (the 2015 deprecation rumour from Chromium never landed; SMIL is here to stay).

Cursor blink (Brain Bar _ line):

<line x1="14" y1="14" x2="20" y2="14" stroke="currentColor" stroke-width="2.2">
  <animate attributeName="opacity" values="1;0" keyTimes="0;0.5"
           dur="1.2s" repeatCount="indefinite" calcMode="discrete"/>
</line>

calcMode="discrete" makes the value step abruptly (no fade). keyTimes="0;0.5" says "value 1 from 0% to 50% of duration, value 0 from 50% to 100%".

Subtle rotation (Shift dial needle):

<line x1="30" y1="30" x2="46" y2="14" stroke="#C12F2F" stroke-width="2.4">
  <animateTransform attributeName="transform" type="rotate"
                    values="-3 30 30;3 30 30;-3 30 30"
                    dur="4s" repeatCount="indefinite"/>
</line>

Rotates ±3° around pivot (30, 30) over 4s. The "30 30" values in each rotation stop are the pivot coordinates.

Subtle opacity pulse (Shift dial active sector):

<path d="..." fill="#C12F2F" opacity=".22">
  <animate attributeName="opacity" values=".18;.28;.18" dur="3s" repeatCount="indefinite"/>
</path>

Animation budget: keep each icon under 2 simultaneous animations and durations >= 1.2s. Anything faster feels nervous on a calm site.

Mirrors of the icon SVG palette

The 4 icon SVGs are a shared visual contract but ALSO vendored 5 times (no shared module across repos). Drift risk is real. Each vendored file has a header comment listing the other 4 mirrors:

Where Path
Earth Hugo aguidetocloud-revamp/layouts/partials/planet-icon.html
Moon Astro guided/src/components/layout/PlanetIcon.astro
Brain Bar Hugo aguidetocloud-revamp/brainbar/layouts/partials/planet-icon.html
Shift Astro shift/src/components/PlanetIcon.astro
Plain AI Node plainai/build-shared.mjs (planetIcon() function)

When you change ANY icon (colour, animation, viewBox, anything), update ALL 5. There is no canonical source — each is canonical for its own planet. Update commit message must list all 5 paths edited.

How to add a new planet

When the 5th sibling planet (e.g. "Atlas" / atlas.aguidetocloud.com) goes live:

  1. Decide the new planet's icon — design an SVG that fits the 4-icon visual contract. Same level of visual weight, same currentColor convention for the parts that should inherit text colour.
  2. Add the manifest entry to all 5 manifests in the same logical batch (per-repo commits since they're different repos):
    [[planets]]
    slug    = "atlas"
    name    = "Atlas"
    url     = "https://atlas.aguidetocloud.com/"
    tagline = "your one-line positioning"
    
  3. Add the icon SVG to all 5 vendored partials/components:
  4. Earth: add SVG branch to layouts/partials/planet-icon.html
  5. Moon: add to PlanetIcon.astro
  6. Brain Bar: add to its vendored planet-icon.html
  7. Shift: add to PlanetIcon.astro
  8. Plain AI: add to planetIcon() in build-shared.mjs
  9. Bump cache_version in Hugo planets (Earth + Brain Bar) so CSS/JS reloads.
  10. Update the audittest-guided-qa.cjs asserts a specific cosmos chip count. Bump it.
  11. Mobile review — decide whether the new planet's icon is hidden on mobile (matching Brain Bar/Shift/Plain AI) or visible (matching Earth's drawer). Add the appropriate @media rules.

How to remove a planet

Reverse the above. Remove the manifest entry from all 5 files in the same logical batch, plus clean up the local SVG branches. Leave the slug-based filter in render code as-is — it's just a no-op for the removed slug.

SLA guardrails for nav changes

The Earth Hugo + Astro Guided nav files are in the practice exam blast zone. Any change to nav.html or Header.astro must:

  1. Pass the 3 SLA curl.exe checks pre + post deploy (questions JSON · checkout API · practice page 200) — use curl.exe not PowerShell Invoke-WebRequest (see gotcha #7)
  2. Pass node test-guided-qa.cjs — has the flat-nav regression block (rewritten 6 May 2026 from the original Browse-popover block):
    • cosmos-group renders
    • 2 planet icons in nav
    • no legacy Browse controls remain (count 0 for #header-nav-browse-trigger, #header-nav-browse-panel, .nav-browse-panel)
    • all 8 flat nav links present (Exams, Guides, Tools, Maps, Videos, Licensing, Blog, About)
    • practice CTA clickable AFTER theme-toggle cycle (replaces the old "AFTER Browse open/close" — same click-flow guardrail intent, exercising an interaction that still exists)
    • no overlay residue after interactions
    • Mobile: drawer opens with cosmos rail (3 sibling planets), drawer closes, practice CTA clickable AFTER drawer cycle
  3. Bump cache_version if any CSS/JS changed
  4. Revert first, investigate second if anything breaks

The growing-guardrail principle: when the test suite changed (Browse popover → flat nav), I REPLACED the Browse-popover assertions with stronger no-legacy-Browse + flat-link-count assertions. The intent didn't shrink — it tightened. (See Incident Log § 5 May guardrail philosophy.)

Hugo↔Astro parity

The Earth (Hugo) and Moon (Astro Guided) headers are independent implementations that must stay byte-equivalent. There's no shared codebase between them yet. Today's process:

  1. Same-commit rule — never split Hugo nav change from Astro Header change across commits
  2. Post-deploy DOM diff (manual) — curl both pages, normalize whitespace, diff the nav sections
  3. Architectural follow-up filed — extract a single canonical nav source consumed by both. Not yet done; risky to combine with feature work

When working on either Hugo or Astro nav, always change both in the same session. Period.

Quick file-touch reference for nav changes

When the nav needs to change visually on both Earth + Moon (which is most of the time):

Hugo file Astro Guided equivalent
aguidetocloud-revamp/layouts/partials/nav.html guided/src/components/layout/Header.astro (markup section)
aguidetocloud-revamp/layouts/partials/planet-icon.html guided/src/components/layout/PlanetIcon.astro
aguidetocloud-revamp/static/css/style.css (cosmos block at end) guided/src/components/layout/Header.astro (<style is:global> section)
aguidetocloud-revamp/data/planets.toml guided/src/data/planets.json
aguidetocloud-revamp/hugo.toml (cache_version bump) (Astro auto-rebusts)
guided/test-guided-qa.cjs (regression block) (no Hugo equivalent — Hugo has no playwright suite)

Pre-existing bugs caught while building this

Bug Found by Fix
style.css had +1 brace imbalance for unknown duration — @media (max-width: 1024px) block opened at line 2335 was never closed before the next media query. New CSS appended after fell silently inside the mobile media query Sush, visually on production after first deploy ("overflow outside of banner") Close the }. Brace count is now perfectly 0. Lesson: ANY pre-existing brace imbalance MUST be tracked down before appending new rules
Hamburger button had z-index: 200 — well below nav-drawer-backdrop z-index 9998. When drawer was open, clicking where the X icon visually appeared actually clicked the backdrop. Backdrop click handler closed the drawer (so UX still worked), but the X icon itself was unclickable New mobile nav regression test (Playwright) Raised hamburger z-index to 10000 in both Hugo + Astro
Plain AI icon used hard-coded gradient ID pa-grad. If two Plain AI icons rendered on one page (e.g., nav rail + future footer), url(#pa-grad) would resolve to the same gradient, but if the IDs ever conflicted with another component using pa-grad, rendering would break Rubber-duck pre-flight critique Per-instance unique IDs: pa-grad-{scope}-plainai. Caller passes scope="nav" or scope="drawer" etc
Phantom // other wires mono-caps label dangling above stacked-vertical chips on Shift mobile masthead — caused by stale .cosmos-rail::before { content: "// other wires" } rule from when cosmos was at end of nav. After moving cosmos into .brand-group, the rule kept firing in the wrong place. Mobile audit screenshots (Playwright) Removed the ::before content + flex-wrap:wrap; border-top: 1px solid block entirely. Replaced with display: none for cosmos at <=860px
Plain AI mobile masthead 3-row stack at 375 (brand+cosmos / search+theme / nav) Mobile audit screenshots First fix: hide cosmos at <=640. Second fix: tighten everything to single row with flex-wrap: nowrap, white-space: nowrap on links, :nth-child(4) to drop "About" at <=480. Hide search + collapse theme to single icon at <=600.
Plain AI "What changed" wrapping to 2 lines on mobile Visual review of screenshots (the height-based wrap detector returned false negatives because of min-height: 44) Add white-space: nowrap on .masthead-link
overflow-x: auto on Plain AI's .masthead-actions SILENTLY clipped "About" link on tablet sizes (414+) — users only saw 3 of 4 nav links and didn't know Screenshot inspection (audit reported overflow=0 because clipped content didn't extend docW) Removed overflow-x: auto. Hide About via :nth-child(4) instead
Shift .tape marquee missing overflow:hidden — inner content was 9700px wide, contributing 60px to document scrollWidth Mobile audit at 375 (after fixing the nav, the overflow remained) overflow: hidden on .tape
Shift footer (.masthead-foot-meta + .byline) — flex row of 5 links + long em-dash phrase with no wrap forced 60px overflow at narrow widths Drill-down audit looking for non-tape elements out of bounds flex-wrap: wrap on .masthead-foot-meta, overflow-wrap: anywhere on .byline
Moon /guided/explore/ cert grid — 37px overflow at 375 from long unbreakable cert slugs (paloalto-netsec-professional) pushing the 2-column grid track wider than viewport Drill-down audit min-width: 0 on .zt-lic-card + overflow-wrap: anywhere; word-break: break-word on slug text
PowerShell Invoke-WebRequest returned false 500 errors mid-session (looked like paid-product outage); curl.exe confirmed all-green Cross-checking with explicit curl Always use curl.exe for SLA checks, never the PowerShell curl alias
Hugo's HTML minifier strips attribute quotes (class=bb-header-left); regex tests looking for class="bb-header-left" returned 0 matches and false-flagged a working deploy as broken Manual inspection of deployed HTML Use lenient regexes that accept both quoted and unquoted forms
Tap targets across all planets were 36×36 (and Brain Bar's theme toggle 39×33). WCAG 2.2 AA passes at 24×24 but iOS HIG calls for 44×44 Mobile audit tap-target check Bumped all .planet-chip, .cosmos-chip, .bb-cosmos-chip, .theme-toggle, .site-logo-img-wrap, .bb-theme-toggle, .masthead-toggle, #theme-toggle (Shift), .search-launcher to min-width: 44px; min-height: 44px

Audit tooling

The mobile audit script lives in ~/.copilot/session-state/<id>/files/mobile-audit.cjs (per-session — not committed). Pattern:

const { chromium } = require('C:/ssClawy/guided/node_modules/playwright');
const CHROME = 'C:/Users/.../ms-playwright/chromium-1208/chrome-win64/chrome.exe';
// Loop over PAGES × VIEWPORTS, take screenshots, collect diagnostics:
//  - document scrollWidth vs viewport
//  - element widths exceeding viewport
//  - tap targets < 44×44
//  - cosmos chip visibility
//  - cosmos rail position relative to brand

If you need the script in a fresh session, recreate it from the patterns in this doc + the post-deploy SLA verification habits. Run it BEFORE the post-deploy SLA checks complete (so you catch visual regressions in parallel).

Browser binary path: Playwright bundles Chromium at C:\Users\<user>\AppData\Local\ms-playwright\chromium-<v>\chrome-win64\chrome.exe. Pass it to chromium.launch({ executablePath: ... }).

Common viewport set: 320 (iPhone SE 1st gen — rare), 360 (small Android), 375 (iPhone SE 2/3), 390 (iPhone 14), 414 (iPhone 14 Plus), 480 (small tablet), 600 (mobile/tablet boundary), 768 (iPad portrait), 1024 (iPad landscape), 1100 (laptop), 1180 (desktop nav breakpoint), 1280 (standard desktop).

  • Cosmos Philosophy — the "why" behind the planet metaphor, includes the masthead-icon visual-parity exception (added 6 May 2026)
  • Cosmos Config Manifest — the [cosmos] block (newsletter, feedback, parent site) and what else could use this manifest pattern
  • Deployment Playbook — the 19-step pre-push checklist (apply on every nav change to Earth/Moon)
  • Incident Log — the 03 May 2026 checkout incident (why click-flow tests were added) PLUS the 6–7 May nav-rework incidents

Document established: 5 May 2026, immediately after Phase 1 + Phase 2 of the cosmos nav build. Major expansion: 6–7 May 2026 — visual-parity exception, no-chip-frame, lotus-image-for-Earth, animations, mobile patterns, gotchas.