Skip to content

Shift Playbook

The everything-you-need page for working on shift.aguidetocloud.com — the AI job-change wire. Static-first Astro site, fully isolated planet in the cosmos. Written 2026-05-05 after a two-round UX audit + plain-English clarity pass.

When this applies

Any code change in C:\ssClawy\shift\. The repo's own README.md is the v1 launch artefact (still useful for orientation and data schema); this page is the operating manual for everything that came after.

TL;DR — what's at stake

  • Site: shift.aguidetocloud.com
  • Repo: C:\ssClawy\shift\ · GitHub susanthgit/shift · Cloudflare Pages project aguidetocloud-shift
  • Stack: Astro 6 + React 19 (islands only where needed) + IBM Plex (self-hosted)
  • Identity: Wire+Gauge atmosphere · Playfair Display headlines · paper #F0E9D6 / ink #14110C / red accent #B22222
  • Content: 35 hand-written role dispatches, 5 voices, 15 sources
  • Build: ~7s for 708 pages
  • Cosmos law #6 firewall: enforced at build time. Don't touch /guided/ from here. Ever.

File map (where things actually live)

Path What it is
src/data/roles.json The 35 dispatches. Single source of truth. Validator in scripts/validate-roles.mjs.
src/data/voice.json Sush's "honest takes". One per role max. Cap of 5 spicy across all 35.
src/data/sources.json Citation registry. Skills reference these by source_id.
src/lib/dispatch.ts Auto-generates the deck + body for every role. Plain-English lookup tables per dial × trajectory verb. Touch with care.
src/lib/trajectory.ts Maps the 3-stop pressure series → trajectory verbs (rising fast / edging up / stable / easing / falling fast).
src/lib/related.ts "You should also read" algorithm. Accepts excludeSlugs[] so curated adjacent entries can never collide with auto-related.
src/lib/quiz.ts 6-question PressureQuiz scoring. Maps answers → role weights.
src/components/ThreeDialsStrip.astro The plain-English explainer strip. Used on home + every role page above the headline.
src/components/PressureInstrument.tsx The signature gauge SVG (hero gauge + 3 dim cards + time-machine year toggle).
src/components/PressureQuiz.tsx Client-side quiz flow + result page. Takes roleCount prop at build time.
src/components/FreshnessBadge.astro Token-driven REVISED THIS WEEK / FRESH / REVIEW DUE / STALE pills. Uses --fresh-* tokens.
src/components/SkimToggle.tsx "Quick read (60 sec)" toggle. Sets data-skim="true" on the .role-dispatch article.
src/components/ShareDispatch.astro Copy-link button with 🔗 icon (platform-neutral, no Mac ⌘).
src/layouts/BaseLayout.astro Shell. Owns ticker, masthead grid, hamburger toggle, footer, theme cookie, OG tags, optional noindex prop.
src/pages/index.astro Home page. Hero + launcher + ThreeDialsStrip + Lead Dispatch + 9-tile featured grid + Editor's Note.
src/pages/role/[slug].astro Per-role dispatch. Section-rule → ThreeDialsStrip → headline → deck → body → instrument → pivot-callout (4 roles only) → skills → voice → share → poster → related → adjacent → sources.
src/pages/method.astro Credibility anchor. Has id="three-dials" anchor (target of footer Glossary link).
src/pages/data.astro Human-readable role-index data table. Skeptic-conversion lever.
src/pages/quiz/index.astro AI Pressure Quiz wrapper. Imports rolesData to pass roleCount to PressureQuiz.
src/pages/wire/index.astro Hidden newsletter page. Unlinked from nav + footer. noindex={true} prop. Sitemap-excluded. Direct URL still 200 for re-enable later.
src/pages/about.astro Story + cosmos map + how to read a dispatch.
src/pages/all.astro Domain-grouped catalogue.
src/pages/compare/[pair].astro Side-by-side role comparison.
src/pages/og/[slug].astro 1200×630 OG card per role.
src/pages/og-vertical/[slug].astro 1080×1920 story / reel poster per role.
src/styles/tokens.css All theme tokens. Includes --force-automation/-augmentation/-resilience (the dial dot colours) and --fresh-* (badge colours).
src/styles/global.css Body bg, section-rule (with mobile flex-wrap), drop-cap, container, type scale, etc.
scripts/check-paid-firewall.mjs Cosmos law #6 enforcement. Runs first in build pipeline.
scripts/validate-roles.mjs Data contract validator. 35 roles + 5 voices + 15 sources. Validates show_pivot_callout/pivot_callout_text if present.
scripts/deploy-pages.mjs Direct Cloudflare Pages API deploy. Bypasses wrangler (workerd doesn't run on win32-arm64).
scripts/generate-og.mjs OG card image generator (Playwright-based).
qa/live-audit.mjs The personas × viewports × themes audit. Accepts SHIFT_BASE + SHIFT_OUT env vars.
qa/mobile-fixes-audit.mjs Targeted mobile regression suite (section-rule + hamburger + /wire/-leak detection).
qa/desktop-spotcheck.mjs Quick desktop + wire-page direct-access spot check.
astro.config.mjs Astro config. Sitemap filter excludes /wire/.

The Three Dials framework (read this first)

This is the conceptual core of every dispatch. Every public-facing surface must respect this framework.

Dial Plain-English blurb Token Where it appears
Automation pressure what AI does instead of you --force-automation (red-ish) Every role page (instrument), home (strip), method (defs), data (cols)
Augmentation pressure what AI changes about how you do it --force-augmentation (yellow-ish) Same
Human resilience what still needs a person --force-resilience (teal/green) Same

First-read rule (the one rubber-duck saved us on)

Any new copy that uses these terms MUST either (a) appear AFTER <ThreeDialsStrip /> on the page, or (b) be inside the strip itself. Never use the words automation, augmentation, or resilience above the strip. A non-tech reader will bounce before they reach the explanation.

The strip is rendered:

  • On / between the launcher form and the Lead Dispatch section
  • On every /role/<slug>/ page directly under the section-rule, ABOVE the dispatch-head

The strip has data-skim-hide, so Quick Read mode hides it (those readers already know the framework).

Voice rules (write copy that lands)

Sush's voice + the plain-English clarity pass merged into a single rulebook:

Keep these phrases

  • "stays stubbornly human" — Sush signature, plain English, kept everywhere
  • "no fluff. no doom. no signup." — three-beat denial rhythm, brand cadence
  • "taste, trust, presence, judgement, accountability" — the human-only descriptor list, used in the resilience body paragraph
  • "the job will look noticeably different — but it's not going away" — heavy-tier closer; resists doom
  • "this job is more sheltered than most — but no job is completely untouched" — light-tier closer; resists complacency

Drop these (banned analyst-prose phrases)

❌ Don't write ✅ Write instead
"AI pressure profile" (just drop — the strip already explains the framework)
"knowledge-worker" "different jobs" / "people who do X"
"citation-anchored" "with sources"
"dossier" (in deck text) "report" / "page" / dispatch only as a brand word
"interpretive prediction not diagnosis" "rough match, not perfect"
"production by model" "in seconds" / "AI is taking over"
"at the ceiling" "the big jumps already happened"
"generative tooling" "AI tools" / "the first wave of AI tools"
"marginal improvement is smaller" "the changes are smaller, not bigger"
"out of the wind" "completely untouched"
"analyst-take" / "analyst-take is X confidence" "Confidence is X — higher where we have a named source"
"VIEW EDITION" (UI control) "Year:"
"Skim mode" "Quick read (60 sec)"
"sweep" (the gauge animation button) "Animate"
"dispatches indexed" (launcher hint) "jobs covered"

Trajectory verbs (already plain — use them)

rising fast · rising · edging up · stable · volatile · edging down · easing · falling fast

These come from lib/trajectory.ts — don't rename. The plain-English sentences in lib/dispatch.ts are mapped per (dial × verb) lookup table:

AUTOMATION_LINES['rising fast']   = 'AI is rapidly taking over more of this work.'
AUGMENTATION_LINES['edging down'] = 'The big workflow changes are settling down.'
RESILIENCE_LINES['stable']        = 'The parts that need a real person are holding.'

Add a new verb? Add an entry to all THREE tables (and to RESILIENCE_PHRASE + AUTOMATION_SHARE for body copy). Validator/build will not catch a missing entry — TypeScript will at compile time because the records use Record<TrajectoryVerb, string>.

Newspaper metaphor — kept on purpose

Wire / dispatch / edition / masthead / byline / deck / lede — these stay. They ARE the brand. The clarity pass introduces the dials FIRST so by the time the user meets these words, they already know what's being talked about.

Build pipeline (the order matters)

npm run build
# = check-paid-firewall.mjs   (cosmos law #6)
#   ↓ aborts if any /guided/ leak detected
# → validate-roles.mjs         (data contract: 35 roles, 5 voices, 15 sources)
#   ↓ aborts on schema violation
# → astro build               (708 pages, ~7s)

Never use npm run build:unsafe — it skips firewall + validator. Only for emergency debugging.

Deploy flow

The git push does NOT auto-deploy (no GitHub Actions workflow, no Cloudflare Pages git integration). Deploys are manual via the deploy-pages.mjs script.

cd C:\ssClawy\shift
$env:CLOUDFLARE_API_TOKEN = (Get-Content "$env:USERPROFILE\.copilot\secrets\cloudflare-api-token" -Raw).Trim()
$env:CLOUDFLARE_ACCOUNT_ID = "d42846fe2c29daf890ec57877fda5e04"
npm run build           # always rebuild before deploy
node scripts/deploy-pages.mjs

Account ID: d42846fe2c29daf890ec57877fda5e04 (also in copilot-instructions-reference.md) Project: aguidetocloud-shift Branch: main Output dir: dist/

The script: 1. Walks dist/ (currently ~893 files, ~83 MB) 2. blake3-hashes each (matches wrangler's content-addressed scheme) 3. POSTs upload-token, checks which hashes CF already has 4. Batch-uploads new files only 5. Finalises deployment with manifest

A typical incremental deploy is ~50 new files, the rest cached = under 10 seconds.

Deploy gotchas (already fixed but document for future)

  1. @noble/hashes v2 export paths — must import as @noble/hashes/blake3.js (with .js), not @noble/hashes/blake3. v1 syntax was extension-less. Already fixed in the script.
  2. blake3() input must be Uint8Array in v2 (was string-coercible in v1). Use new TextEncoder().encode(base64 + ext). Already fixed.
  3. win32-arm64 can't run wrangler because workerd doesn't ship a binary. This is WHY we use deploy-pages.mjs in the first place. Don't try to "modernise" it back to wrangler.
  4. Display bug in script output — the success line prints https://https://... (double scheme). It's cosmetic. The actual deploy ID + project are correct.

Verify production after deploy

$urls = @(
  'https://shift.aguidetocloud.com/',
  'https://shift.aguidetocloud.com/role/graphic-designer/',
  'https://shift.aguidetocloud.com/data/',
  'https://shift.aguidetocloud.com/method/'
)
foreach ($u in $urls) {
  $code = (Invoke-WebRequest -Uri $u -Method Head -UseBasicParsing -SkipHttpErrorCheck).StatusCode
  Write-Host "$code  $u"
}

Important: PowerShell variable $HOME is read-only — use a different name (e.g. $page) when fetching content.

Audit framework

Three audit scripts, three purposes

Script What it does When to run
qa/live-audit.mjs 5 personas × 2 viewports × 2 themes × ~8 steps each = ~80 screenshots. The full-coverage suite. Before any deploy. After any UX change.
qa/mobile-fixes-audit.mjs Targeted regression for the 3 mobile fixes (section-rule overflow, /wire/ leak detection, hamburger open/closed states). 32 screenshots. After any change to BaseLayout.astro, global.css, or anything mobile-related.
qa/desktop-spotcheck.mjs Quick 3-screenshot desktop sanity check + verifies /wire/ is still accessible directly + has noindex meta. When you only need a fast yes/no.
qa/full-qa-prod.mjs Round-5 production sweep. 4 viewports × 2 themes × 11 pages = 88 captures + 7-step interaction flow. Captures readability + overflow + nav errors. Note: the script's contrast checker is buggy on rgba backgrounds — trust visual screenshots over its contrast numbers. When auditing prod for cross-platform issues.
qa/r6-verify.mjs Round-6 fix-verification suite. 4 viewports × 2 themes × 6 key pages = 48 captures + targeted checks (overflow widths, ticker visibility, card bg, rule colour, placeholder swap, TOC anchors, scroll-to-anchor). After any change to the 8 round-6 fix areas.

Run the full audit against local preview (don't pollute the prod baseline)

cd C:\ssClawy\shift
# Terminal A — keep preview running
npm run preview
# Terminal B — audit against localhost
$env:SHIFT_BASE = "http://localhost:4323"
$env:SHIFT_OUT  = "qa/clarity-pass"     # or whatever this round is called
node qa/live-audit.mjs

Output: qa/<SHIFT_OUT>/ with one PNG per (persona × viewport × theme × step) + findings.json + index.html for browser viewing.

Audit folders are gitignored (qa/**/*.png, qa/**/findings.json, qa/**/index.html). Keep the scripts committed; never commit the screenshots.

Personas (defined in qa/live-audit.mjs)

  • curious-newcomer — first-time visitor; the clarity-pass primary target
  • anxious-junior — early-career, AI-worried; pivot-callout primary target
  • skeptic-decision-maker — CEO-type, "prove it"; data + method primary target
  • shared-link-clicker — LinkedIn drop-in; share + related primary target
  • quiz-taker — engagement-funnel; quiz + result primary target

To add a persona: edit the PERSONAS array. Each entry has name + steps[] (route + optional interaction). Mirror the pattern.

Mobile patterns (don't reinvent)

Hamburger menu (BaseLayout.astro)

  • Desktop (>860px): nav inline as auto 1fr auto auto grid (brand · meta · nav · theme-toggle). Hamburger button hidden via CSS display: none.
  • Mobile (≤860px): 3-row grid: [brand toggle theme] / [meta meta meta] / [nav nav nav]. Nav hidden by default, opens on hamburger click.
  • Toggle JS is a small inline script (next to the theme-toggle script). Sets data-nav-open="true" on .masthead, toggles aria-expanded, closes on link-click + Escape.
  • Sub-480px: hamburger drops the "Menu" text label, icon-only.

The structural rule: theme-toggle is a sibling of the nav, NOT inside it. This is what allows the toggle to stay visible whether the nav is open or closed.

Section-rule overflow (global.css)

.section-rule is display: flex; flex-wrap: wrap; gap: 4px var(--s-4); overflow-wrap: anywhere. The flex-wrap is what stops long uppercase strings from spiking off-canvas on mobile. The overflow-wrap: anywhere is the safety net for very long unbroken words (rare).

Under 560px, letter-spacing shrinks from .14em to .08em for extra mobile breathing room.

Body background + noise overlay (global.css, dark seam fix)

Lesson learned the hard way: Playwright fullPage screenshots stitch viewport-height tiles. If you have body { background-attachment: fixed } + body::before { position: fixed } overlay, each tile gets its own copy of the fixed gradient — visible seam between tiles in dark mode.

Fix (already in global.css): - body has NO background-attachment: fixed - body::before noise overlay has background-size: 200px 200px; background-repeat: repeat

If you ever add another fixed background layer, test the dark-mode fullpage screenshot of /all/ or /method/ before shipping.

Instrument time-machine on mobile (PressureInstrument.tsx)

The time-machine year selector (Year: 2026 2028 2030 ▶ Animate) is display: inline-flex on desktop — total natural width ~430px. On any viewport below ~540px this overflows.

Fix (already shipped 2026-05-05 round 3): at <=540px, the time-machine restructures into a CSS grid:

row 1: YEAR: label  (spans all 3 columns)
row 2: 2026 | 2028 | 2030  (3 equal columns, dividers between)
row 3: ▶ ANIMATE  (spans all 3 columns)

Borders adjusted: label gets border-bottom, animate gets border-top, year buttons get right-border between them (third one drops it).

If you add another button to the time machine (e.g. Reset, Compare), update the :nth-of-type(N) selector for the right-border rule.

Hero gauge on mobile

.hero-gauge is now fluid at <=760px: width: 100%; max-width: 260px; aspect-ratio: 1/1. At <=540px it shrinks to max-width: 240px. Don't replace with a fixed pixel width — it'll overflow on iPhone SE 320px again.

Known minor issue (not yet fixed): the band labels (MED-LOW, MED-HIGH) at x=68 / x=332 in the SVG viewBox can clip at the outer edge of the gauge on viewports below 360px. Easy fix when someone takes the polish pass: add padding: var(--s-3) to .hero-gauge or shift the band labels inward in the SVG.

Wide tables on mobile (/data/)

The 8-column data table at <=760px hides the year-band columns + Last verified + Domain and shows ROLE · TIER · CONFIDENCE only. Above the table sits a .data-mobile-hint: "On mobile we show role · tier · confidence only. Open this page on a wider screen for the full year-by-year band readings."

CSS pattern:

.data-mobile-hint { display: none; }
@media (max-width: 760px) {
  .data-mobile-hint { display: block; }
  .data-table-scroll { overflow-x: visible; border: 0; }
  .data-table { min-width: 0; table-layout: fixed; width: 100%; }
  .data-table .col-domain,
  .data-table .col-stop,
  .data-table .col-verified { display: none; }
  .data-table .col-role { width: auto; overflow-wrap: anywhere; }
  .data-table .col-tier { width: 5.5rem; }
  .data-table .col-conf { width: 5.5rem; }
  .data-table .data-tagline { display: none; }
}

Don't switch to overflow-x: auto and let users swipe — was the original approach, broke at 360px because role-column min-width: 200px plus tier + confidence already exceeded viewport. Hide-and-hint is cleaner UX and still preserves the data on desktop.

On-this-page TOC for long prose pages (about, method)

.page-toc is a small reading-map at the top of long prose pages. Pattern:

<nav class="page-toc" aria-label="On this page">
  <p class="page-toc-eyebrow">— ON THIS PAGE —</p>
  <ol class="page-toc-list">
    <li><a href="#section-id">Section title</a></li>
    ...
  </ol>
</nav>

Each <section> gets an id. Add this CSS once (currently duplicated per page because Astro scopes <style>):

.page-toc { margin: var(--s-5) 0 var(--s-7); padding: var(--s-4) var(--s-5);
  background: var(--paper-2); border-left: 3px solid var(--accent); }
.page-toc-eyebrow { font-family: var(--font-mono); font-size: var(--t-eyebrow);
  letter-spacing: .14em; text-transform: uppercase; color: var(--accent);
  margin: 0 0 var(--s-2); font-weight: 700; }
.page-toc-list { padding: 0 0 0 var(--s-5); display: grid; gap: var(--s-1); }
.page-toc-list li { font-family: var(--font-display); }
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}
.about section[id], .method section[id] { scroll-margin-top: var(--s-6); }

scroll-margin-top is essential — without it, anchored headings tuck under the masthead after scroll.

Astro CSS scoping with [data-theme=...] selectors (:global() required)

Round 6 trap: I tried to lift featured cards in dark mode with:

[data-theme='dark'] .featured-card { background: var(--paper-2); }
inside the <style> block of src/pages/index.astro. It didn't work. Astro's CSS scoping rewrote it to:
[data-astro-cid-XXX][data-theme='dark'] .featured-card[data-astro-cid-XXX] { ... }
The <html> element does NOT carry data-astro-cid — so the rule never matches.

Fix (already shipped): wrap the <html>-targeted attribute with :global():

:global([data-theme='dark']) .featured-card { background: var(--paper-2); }
Astro respects :global() and skips that part of the selector. The .featured-card half still gets scoped, which is correct.

Rule of thumb: any selector targeting an attribute on <html> or <body> from a scoped Astro <style> block needs :global() on that part. Affects [data-theme='dark'], [data-page='home'], [data-front-page], etc.

Diagnostic: open DevTools → inspect the rule → if you see a data-astro-cid next to a selector that should have stayed unscoped, that's the bug.

Sticky front-page rails — height-aware (@media (min-height: 900px))

Round 4 used position: sticky; max-height: calc(100dvh - var(--s-7)); overflow-y: auto on .front-rail. At 1280×800 (very common 13" laptop) the rail content was taller than viewport and the overflow scrollbar hid 4 of 12 BY DOMAIN entries behind a sub-scroll the user wouldn't notice.

Fix (round 6): drop max-height + overflow-y: auto. Apply position: sticky only when the viewport has the height to hold the content:

@media (min-width: 1280px) and (min-height: 900px) {
  .front-rail { position: sticky; top: var(--s-5); align-self: start; }
}
At <900px height, rails flow naturally; the user scrolls past them, then reads main content. Acceptable trade-off vs hidden content behind a sub-scroll.

Tape ticker per-route hide (data-front-page attribute)

Mobile non-home pages don't need the tape ticker — it eats 24px of first-page real-estate before the brand logo. Round 6 fix:

In BaseLayout.astro frontmatter:

const isFrontPage = Astro.url.pathname === '/';
On <html>:
<html data-page={pageKind} data-front-page={isFrontPage ? '' : undefined}>
In the <style>:
@media (max-width: 760px) {
  html:not([data-front-page]) .tape { display: none; }
}

Why data-front-page and not data-page="home": the quiz and wire pages also pass pageKind="home" (legacy choice) — they're not the actual front page. Pathname check is the source of truth.

Method-page visual-break pattern (round 7 phase 3)

Long prose pages (/method/, /about/) get exhausting top-to-bottom. Five inline rest patterns work well, all using existing tokens so dark mode follows for free:

  1. .method-pq pull-quote — typewriter red font, double-bordered (border-top + border-bottom: 4px double var(--accent)), centred. Tight variant .method-pq-tight for in-section use.
  2. .dials-strip 3-column swatch row — coloured circles using --force-automation/-augmentation/-resilience, plain-English glosses below. At <=540px, column-stacks with border-bottom between cells.
  3. .band-strip 5-cell horizontal gradientaspect-ratio: 5/2, each cell filled with --band-low--band-high. Labels overlaid in cream #FAF5E2 with text-shadow for legibility on any band colour.
  4. .conf-strip confidence-pill swatch — three .confidence pills inside a --paper-2 framed row. Uses existing .confidence-high/-medium/-low styling.
  5. .method-callout framed claim4px double ink border with eyebrow + display-font quote. Closes the section on a deliberate, framed claim rather than trailing prose.

Don't try to source new images. The wire+gauge atmosphere is text + colour + rule lines — keeping it fully inline is part of the identity.

Mobile rules — at <=540px: dials strip column-stacks, band-cell labels shrink to 9px so all 5 still fit, conf-strip centres. The band-cells lose their aspect-ratio: 5/2 and become min-height: 56px because the aspect-ratio would shrink them too thin in a single row.

Theme detection (BaseLayout.astro inline script)

var m = document.cookie.match(/(?:^|; )agtc_theme_v1=(light|dark)/);
var pref = m ? m[1] : 'light';   // ← always defaults to light
document.documentElement.setAttribute('data-theme', pref);

Default is LIGHT regardless of system preference. This is deliberate (Sush's call 2026-05-05): the wire reads as a newspaper first; respect the cookie if the user has explicitly chosen dark via the theme toggle.

Don't re-add prefers-color-scheme as a fallback — it makes the site look dark on a non-trivial portion of mobile devices where users haven't opted into Shift's dark theme. The toggle still works for users who want it; the cookie persists for 1 year.

Test gotcha: Playwright extraHTTPHeaders: { Cookie: 'agtc_theme_v1=dark' } does NOT populate document.cookie in the page — only the request header. To make the inline theme script see the cookie, use context.addCookies([{ name, value, domain, path }]) instead. Round 6 audit script qa/r6-verify.mjs uses the correct pattern.

Home page 3-column rail layout (src/pages/index.astro)

The home page uses a true newspaper layout at desktop widths. Below the breakpoint, falls back to the existing single-column flow.

Structure

.front-grid (max-width: 66rem default · 88rem at >=1280px)
├─ .front-rail.front-rail-left  (display:none default · flex at >=1280px)
│   ├─ <RailQuizCta />           ← B (relocated quiz CTA, classified-ad style)
│   ├─ <RailTierCounts />        ← A (35 dispatches across N heavy / N moderate / N light)
│   └─ <RailByDomain />          ← D (alphabetical mini directory, anchors to /all/#<slug>)
├─ .front-main (always visible, sections stack vertically)
│   ├─ <section class="front-hero">     H1 → SEARCH BOX → deck (search-prominent order)
│   ├─ <section class="front-three-dials">
│   ├─ <section class="front-lead">     Lead dispatch
│   ├─ <aside class="front-quiz-mid">   Interim quiz CTA (display:none at >=1280px)
│   ├─ <section class="front-featured"> 3x3 featured grid
│   └─ <section class="front-explainer"> Editor's note (max-width: --max-w-prose)
└─ .front-rail.front-rail-right (display:none default · flex at >=1280px)
    ├─ <RailRecentlyRevised />   ← C (top 5 by last_verified)
    ├─ <RailCosmos />            ← E (Earth, Brain Bar, Shift you-are-here)
    └─ <RailForDevelopers />     ← F (data table, /role-index.json, MCP endpoint)

Breakpoints

  • <1280px: Single column, max-width 66rem. Rails hidden. Interim quiz CTA visible mid-page.
  • >=1280px: 3-col grid (14rem | minmax(0,1fr) | 14rem), max-width 88rem. Rails sticky (position: sticky; top: var(--s-5)). Interim quiz CTA hidden.
  • The breakpoint width was chosen so the rails fit comfortably on a 1280px laptop; below that the layout would feel cramped.

Adding a new rail widget

  1. Create src/components/rail/Rail<Name>.astro
  2. Wrap in <aside class="rail-widget" aria-label="..."> so it inherits the shared widget styles (rail-widget + rail-widget gets a 1px top rule + padding)
  3. Use <p class="rail-eyebrow">— TITLE —</p> for the header (mono uppercase red)
  4. Import + render in src/pages/index.astro inside the appropriate rail
  5. Sush picks slot order — left rail is action-focused, right rail is depth-focused (don't blur the line)

Data anchors used by widgets

  • RailByDomain links to /all/#<domainSlug>. The slug logic must match /all/'s slugifier exactly. If you change the slugifier in either file, change BOTH. The pattern: d.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '').
  • RailTierCounts links to /data/ (the human-readable table) — that page surfaces tier per role. Don't link individual tier numbers to non-existent /all/#heavy style anchors.

Common pitfalls

  • Source-order CSS conflict: .front-quiz-mid { display: flex } written AFTER @media (min-width: 1280px) { .front-quiz-mid { display: none } } will OVERRIDE the media query (equal specificity, later source wins). Don't set display on the default rule — use text-align: center for inline-level child centering instead. (Bug shipped + fixed 2026-05-05 round 4.)
  • .container class is no longer used inside .front-main — the grid handles widths. Don't add class="container" to inner sections; they'll get double-constrained.
  • Sticky rails need align-items: start on .front-grid (otherwise grid stretches them and sticky breaks).
  • At exactly 1280px viewport, padding is 0 each side because the grid hits the viewport edge. Cosmetic, not a bug. Comfortable from 1300px+.

CSS token system

Read src/styles/tokens.css first — every colour, spacing, font-size, radius, transition is tokenised. Two themes (light + dark) share variable names; only values differ.

Critical tokens

Token Light value Dark value Where used
--paper #F0E9D6 #18140E body bg
--ink #14110C #F0E9D6 text
--accent #B22222 #E25555 red accents (passes WCAG AA at 5.15:1 on dark — don't bump without a contrast check)
--force-automation red-ish red-ish dial dot 1
--force-augmentation yellow-ish yellow-ish dial dot 2
--force-resilience teal/green teal/green dial dot 3
--fresh-revised / -fresh / -review-due / -stale (tokenised pair: fg + bg per state) (same) FreshnessBadge — must always be tokenised, never hardcode hex
--rule rule line subtle rule line subtle section-rule borders
--s-1 to --s-8 4px → 96px scale (same) spacing — don't use arbitrary values

Token rules

  • No hardcoded colours — every color/background/border-color must use var(--token). The FreshnessBadge incident proved this — hardcoded #B22222 looked muddy on dark.
  • Spacing on the 8px grid — pull from --s-1 to --s-8. No arbitrary 15px, 22px.
  • Transitions ≤ 200ms ease-out — only transform, opacity, color, background-color, border-color, box-shadow.
  • No backdrop-filter — banned for performance (Zen system rule).

Pre-push checklist (Shift-specific)

Adapted from the global 19-step checklist. Shift is Astro, not Hugo, so the Hugo-specific steps don't apply. What does:

# Step
1 npm run validate — must pass clean
2 npm run firewall — must pass clean (cosmos law #6)
3 npm run build — must complete clean (708 pages)
4 If you changed lib/dispatch.ts, spot-check 3+ role pages' deck output for jargon regressions
5 If you changed BaseLayout.astro, run node qa/mobile-fixes-audit.mjs against local preview
6 If you changed copy on a high-visibility page (home, role, quiz), run the curious-newcomer + anxious-junior persona pairs of qa/live-audit.mjs
7 git add explicit paths only — never git add . / -A / -a. Read git status -s for M indicators per parallel-git-rules
8 git pull --rebase before push
9 Commit message must include Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
10 After git push origin main: rebuild + node scripts/deploy-pages.mjs (the push itself does NOT deploy)
11 Verify production: 4 URLs minimum (home, one role page, /data/, /method/)

Hidden surfaces (here be dragons)

/wire/ newsletter page

  • Status: code-complete but feature-disabled.
  • Hidden by: removed from masthead nav + footer · noindex={true} BaseLayout prop · sitemap filter in astro.config.mjs.
  • Direct URL still works (200) so any existing bookmarks don't break.
  • To re-enable: remove the noindex prop from src/pages/wire/index.astro, restore the nav + footer links in BaseLayout.astro, drop the /wire/ filter from astro.config.mjs.
  • Reason it's hidden: Sush isn't ready to commit to a weekly newsletter cadence yet (May 2026).

Pivot callout (only on 4 roles)

Not a hidden feature — but easy to miss. The show_pivot_callout: true + pivot_callout_text flag on roles.json adds a .pivot-callout aside above the skills grid. Currently set on graphic-designer, corporate-lawyer, copywriter, translator — the heavy-tier roles where anxious-junior personas need the constructive lead-in before the doom-heavy skill grid.

To add to a new role: validator enforces (a) boolean for show_pivot_callout, (b) ≥30 chars for pivot_callout_text (so it can't be a one-word stub).

Quick Read mode (SkimToggle)

Sets data-skim="true" on .role-dispatch. CSS hides anything with [data-skim-hide] attribute. Currently hides: .wire-body, .sources-section, .poster-section, .related-section, .pull-quote, .share-dispatch, and .three-dials (the ThreeDialsStrip).

If you add a new section to the role page that experienced readers don't need on a quick scan, add data-skim-hide to it.

Process patterns that worked

Always rubber-duck before non-trivial copy changes

The clarity pass nearly went wrong: my first plan was "add an explainer panel and call it done." The rubber-duck agent caught that the auto-generator at lib/dispatch.ts produces analyst-prose deck text BEFORE any chrome explainer can save the reader — AND that text feeds SEO/social previews. Lesson: for any change involving generated text, fix the generator, not the wrapper.

Drafting copy for sign-off before commit

For voice-sensitive changes (deck text, body templates, home deck, about paragraphs), draft the proposed copy inline in the response and ask Sush to approve wording before committing. Saves rework when his voice ear catches something off-tone.

Audit against local preview, not production

The audit script's SHIFT_BASE env var lets you run the full 80-screenshot persona pass against localhost:4323 without polluting the qa/live/ baseline. Standard pattern: build → preview → audit → review → deploy → re-audit against production.

Data-driven editorial proposals (round 7 phase 1 + 2)

For changes that touch all 35 roles' data (confidence labels, source backfill), don't eyeball it. Run a stats script first, then propose specific changes with rationale, get sign-off, then apply via a committed mutation script.

The pattern:

  1. Aggregate with a short node -e "..." script that prints per-role stats. E.g. confidence distribution: count high/medium/low per dimension across all 35.
  2. Diagnose. Round 7 found the imbalance was augmentation under-credited (17.8% high), not "auto skews high" (which Sush expected). Don't trust the user's framing of the problem — verify it against data.
  3. Propose specific changes in a table. Include the source you'd attach, why it fits, why related-but-skipped claims weren't included.
  4. Sign-off via ask_user with multiple options ("apply all", "subset", "skip").
  5. Apply via a committed mutation script (scripts/round7-phase2.mjs) so the change is auditable.
  6. Validate with the existing tooling (npm run validate + npm run test) — both already enforce schema integrity.

The committed mutation script is essential — it documents exactly what changed, lets future rounds re-run / extend the same logic, and survives if the underlying JSON changes shape.

URL verification before adding new sources (round 7 phase 2)

Sources need real, citable URLs. Pattern:

  1. Draft the new source list with publisher + title + claim it would back.
  2. Run parallel web_search for each new source to find the canonical URL.
  3. Only add sources whose URL resolved cleanly. If a search returns nothing or returns affiliate / re-host pages, fall back to attaching an existing broader source.
  4. Add to sources.json with title, url, publisher, year (validator enforces all four).

Round 7 added: NRA State of Restaurant Industry 2024, NASW AI ethics, BLS personal-care OOH, Edison Infinite Dial 2024 — each verified before commit.

Deferred items (the future-work backlog)

In rough order of value × effort. Pick from here for next sessions.

Item Why deferred Effort
2-column dispatch density on mobile The wire-body 2-column flows at >720px work great on desktop, get dense on mobile. Risky CSS pass without typography rework. M
OG story-PNG optimisation Some 1080×1920 PNGs are ~1MB. Run through optipng/svgo equivalent. S
Source backfill — second pass Round 7 backfilled 4 zero-source roles + 6 high-impact links. Still ~25 observable-but-unsourced automation high-conf claims (e.g. copywriter "email/social variant generation", music-producer "AI-mixed and mastered demos"). Each is 10 min of search + verify. L
Confidence audit — second pass Round 7 promoted 12 medium augmentation claims to high. The contested-but-plausible second tier (paralegal case-timeline, hairdresser style-recs, social-worker risk-flagging, podcaster guest-booking, GP rare-condition alerts) was deliberately left at medium. Worth revisiting as the underlying tech matures. M
MCP tool extensions shift_get_role / shift_compare / shift_recent_changes exist on the MCP worker (mcp-shift.aguidetocloud.com/mcp) — could add shift_search_skills for skill-based queries across roles. M
Wildcard subdomains designer.shift.aguidetocloud.com/role/designer/. Cloudflare Workers + DNS. Vanity feature. M
Newsletter signup (re-enable /wire/) Code complete; awaiting Sush's commitment to weekly cadence. S to re-enable, recurring effort to actually publish
Vanity domain e.g. shift.live. Marketing question, not engineering. S to provision
Freshness automation Monthly review workflow (a script that flags last_verified >30/60/90/180 days old). Pattern from aguidetocloud-revamp mind-maps. M
More roles 35 → 60 → 100 over time. Sush curates. Each role takes ~2 hours of his writing + sourcing. Recurring

When in doubt

  • Read this page first
  • Read C:\ssClawy\shift\README.md for v1 launch context (data schema, file map)
  • Read src/pages/method.astro for the honesty model
  • Read ~/.copilot/copilot-instructions.md § Practice Exam SLA — even though /guided/ isn't in this repo, it's the cosmos rule that overrides everything
  • Run npm run firewall && npm run validate before any non-trivial change

Session log

Date Session Outcome
2026-05-04 Shift v1 launch 35 hand-written role dispatches, 708 pages, deploy-pages.mjs created (wrangler-free win32-arm64 path)
2026-05-05 round 1 UX audit + 17 fixes section-rule overflow, FreshnessBadge contrast, dark seam fix, /data/ page, pivot-callout, dedupe, mobile masthead grid (still 6-row before round 2)
2026-05-05 round 2 Mobile UX fixes + plain-English clarity pass section-rule wrap, hamburger menu, /wire/ hidden, ThreeDialsStrip, dispatch.ts generator rewrite, voice rules codified, deploy script v2-noble fix. Live as commit 6138e6a deployment 4fe62eba.
2026-05-05 round 3 Mobile instrument overflow + theme default Time-machine restructured into 3-row grid at <=540px (label / years / animate); hero-gauge + dim-gauge made fluid with aspect-ratio; default theme dropped prefers-color-scheme fallback (always LIGHT now). Live as commit 07589fc deployment 75bc3c10.
2026-05-05 round 4 Newspaper 3-column home layout Hero restructured (search above deck, quiz CTA out); 6 rail widgets built (RailQuizCta / RailTierCounts / RailByDomain / RailRecentlyRevised / RailCosmos / RailForDevelopers); 3-col grid 14rem \| 1fr \| 14rem at >=1280px, max-width bumped to 88rem; /all/ got per-domain id anchors so RailByDomain links work; interim mid-page quiz CTA for <1280px viewports. Live as commit 1177a60 deployment c1abb355.
2026-05-05 round 5 Production QA audit (gather-only) Built qa/full-qa-prod.mjs (88 captures × 4 viewports × 2 themes × 11 pages + interaction flow). 0 nav/console/HTTP errors. Surfaced 4 real issues: compare mobile overflow, data table mobile cutoff, sticky rails clipping at 1280×800, featured cards melting in dark mode. Plus 4 minor improvements (placeholder, --rule, ticker, TOC). No code changes — observation pass.
2026-05-05 round 6 Ship 8 production QA fixes All 8 round-5 findings fixed and verified. Compare <code> word-break + form min-width:0; data table at <=760px hides year+verified+domain cols; rails dropped max-height/overflow + sticky only at >=900px height; featured cards dark :global([data-theme='dark']) .featured-card { background: var(--paper-2) } with the Astro :global() scoping fix; mobile placeholder swap via matchMedia + data-placeholder-narrow/-wide; --rule softened in dark mode (#4A4231→#36301F); ticker hidden on mobile non-home via data-front-page attr on <html>; about+method got <nav class="page-toc"> with anchored ids + scroll-margin-top. Verified locally + on prod via qa/r6-verify.mjs (48 captures, 0 overflow, 100% expected behaviour). Live as commit 995e581 deployment ff99367b.
2026-05-05 round 7 Confidence audit + source backfill + method polish + v1 framing Phase 0 — added v1 expectation-setting to launcher hint ("35 roles in v1 — the wire is still growing"), added empty-state with Tell-us link when search has 0 matches, added Feedback link to footer (all → aguidetocloud.com/about/). Live as commit 0b5e5ad deploy 9de975c7. Phase 1 — confidence audit found augmentation was the under-credited dial (17.8% high vs auto 59.5%, res 75.2%). Promoted 12 medium augmentation claims to high, each linked to an existing source. Augmentation now 31.1% high. Live as commit d61d4e5 deploy 1bf476d6. Phase 2 — source backfill: added 4 new sources (NRA restaurant, NASW AI ethics, BLS personal care, Edison Infinite Dial 2024), attached to the 4 zero-source roles (chef/social-worker/hairdresser/podcaster); plus 6 link-only attachments using existing sources for software-engineer/customer-support/translator/music-producer/journalist high-conf claims. URLs verified via web_search before adding. Validator now 19 sources. Live as commit b0d3417 deploy faebe769. Phase 3 — method page visual breaks: 5 inline rests added (pull-quote after lede, three-dial swatch row, five-band horizontal stripe, "intentionally chunky" tight pull-quote, confidence pills swatch row, "useful for thinking" double-bordered callout). All inline (no images), all using existing tokens so dark mode follows for free. Mobile rules at <=540px: dials strip column-stacks, band-cell labels shrink to 9px. Live as commit 56eca22 deploy ee94cbc8.