# FedM8 design 003 — Card hub + social (status: standard for all new work, approved 2026-11-02; rollout to every page in progress, theme layer shipped 2026-11-03, see status.json)

Canonical: https://fedm8.com/design/003/ · machine-readable: spec.json · tokens.json · design-003.css · theme.schema.json

## How to use (AI)
- Read this file first. It is the build contract and the QA checklist for design 003.
- Build every page in pages[]; every module in modules[] must exist with the listed elements, interactions and states.
- Apply rules R1–R9. Where a comp (comps/*.dc.html or screens/*.png) differs from a rule or from a page's v2_changes, the rule wins.
- Use only semantic --fm-* tokens from tokens.json / design-003.css. No hex, px, radius or font literals in components. This is what makes user themes work.
- Validate by walking acceptance[] for each page; record pass/fail per line with page id + index (e.g. contract-detail#7).
- Data marked 'sample' must come from real tables in the build or show an empty state. Never ship SampleChip or sample members.
- Keep catalog shared rules: comments box on every page, plan tiles (B/P) after paid tools, naming (Match-M8, FedM8 AI · not a member, room, Post).

## Owner decisions
- Theme picked: style 04 "Card hub + social" (Nov 2, 2026). Applies to the whole logged-in app (fedm8.com/os).
- Users must be able to change their UX style on the fly (runtime theming). Every visual value is a token. No hard-coded colours, sizes, radii or fonts in components.
- Reddit-style discussion around everything; join any contract/grant as Prime / Sub / 1099; per-requirement "I can do this"; strong social presence.
- Veterans, Learn and Community (Forum) never hidden behind an "All tools" menu. No "Tap ? for help" hints. Few words, more icons.
- Chevron = arrow-segment step bar; used for walk-throughs and as breadcrumbs (depth ≥2).
- Walk-throughs TurboTax-simple: one question per screen, obvious progress, easy back/forward.
- FEDM8 LLC's SDVOSB is "In review" with SBA VetCert — never shown as certified.
- Veterans get 3 months Pro free at sign-up. Pro = contact roster members, AI proposal writer, AI requirement summaries, past-pay lookups.
- Never invent contract summaries, award amounts, fit scores. Missing data → explicit placeholder/loading state.

## Rules (override comps)
- R1 Navy (`--fm-nav-active-bg` / `--fm-selected-bg`) marks only "where you are": active nav pill, selected choice card. Selected list rows use `--fm-row-selected-bg` (cream tint) + bold title, never navy.
- R2 Exactly one primary action per screen, styled `--fm-action`, placed top-right of the focus area (wizards: bottom-right). All other actions outline or text.
- R3 Detail pages (contract, grant) order: Title + countdown → Summary (BLUF) → Can you do this? → Join as → Discussion. Facts collapse to one FactLine.
- R4 ChevronBreadcrumb only at depth ≥2. Top-level pages start with the H1.
- R5 Urgency colour: ≤48h `--fm-urgent`, ≤7 days `--fm-warn`, later neutral. Same on every surface.
- R6 Choice cards: label first (type-lg), count second (type-md, muted, with unit word). Big numbers only on Home StatCards.
- R7 Chips: two states only — selected (`--fm-chip-selected-bg`) and unselected (outline). Never navy-filled chips.
- R8 Minimum meta text 13px (`--fm-text-sm`) at contrast ≥4.5:1.
- R9 No unknown-count placeholders shown as large numbers; hide the number or show real data.

- R10 Plan names and gates come from tier.js only: Free, Basic, Pro, Pro + AI. The comp's Free / Pro / Team cards are replaced by those four tiers; use FedM8Tier.can(plan, featureId) and B/P plan tiles. No invented plan names or prices.

## Runtime theming
- Goal: Users change their UX style on the fly; no reload; persists per user across devices.
- apply: Set data-theme, data-density, data-corners, data-reduce-motion on <html>; set --fm-type-scale and accent overrides as inline custom properties on <html>.
- storage: profiles.ui_theme jsonb (validated against theme.schema.json) + localStorage cache for first paint; server value wins after sign-in. Field name is a proposal — confirm with the build agent.
- first_paint: Inline script in <head> reads the cached theme and sets attributes before CSS paints (no flash).
- guard: Accent swatches are filtered so every pair in tokens.contrast stays at or above its minimum; any user value outside theme.schema.json is ignored and the preset value used.
- Presets: card-hub, high-contrast, dark, compact
- Acceptance:
  - Given a signed-in user When they pick Dark in Appearance Then every page re-colours within 100ms without reload
  - Given a user sets text size 1.2 When they open any page Then all text tokens scale by 1.2 and no text is clipped at 1440 and 375 widths
  - Given density Compact When viewing lists Then row padding uses --fm-space-* × 0.85 and tap targets stay ≥44px
  - Given a theme is saved When the user signs in on another device Then the same theme applies on first paint
  - Given any preset When the automated contrast test runs over tokens.contrast pairs Then all pass
  - Given a component file When linted Then it contains no hex colour, px font-size or font-family literal (stylelint rule)
  - Given Reset to default When pressed Then preset card-hub with default scalars is restored and saved

## Semantic tokens (card-hub values)
- `--fm-bg` #f3f1ec — Page behind cards
- `--fm-surface` #ffffff — Cards, header, fields, dialogs
- `--fm-surface-alt` #fffaf0 — Highlight cards, claim bar, continue card
- `--fm-line` #e0dccf — Hairlines, card edges
- `--fm-line-strong` #8b8676 — Dashed empty states, disabled edges
- `--fm-ink` #14284b — Primary text, headings
- `--fm-ink-2` #3d4f6e — Secondary text
- `--fm-ink-3` #4a5b78 — Meta text (min 13px)
- `--fm-on-dark` #ffffff — Text on navy/selected fills
- `--fm-nav-active-bg` #14284b — Active nav pill, current chevron segment
- `--fm-nav-active-ink` #ffffff — Text on active nav
- `--fm-selected-bg` #14284b — Selected ChoiceCard
- `--fm-selected-ink` #ffffff — Text on selected card
- `--fm-row-selected-bg` #fdf3dc — Selected list row / chat item (R1)
- `--fm-row-selected-ink` #14284b — Text on selected row
- `--fm-action` #f5a524 — The one primary button per screen (R2)
- `--fm-action-ink` #14284b — Text on primary button
- `--fm-action-hover` #e0951a — Primary button hover
- `--fm-accent-soft` #fde3b0 — Badges, chevron middle segment, highlights
- `--fm-accent-ink` #8a4b00 — Text on accent soft
- `--fm-chip-selected-bg` #fde3b0 — Selected filter chip (R7)
- `--fm-chip-selected-ink` #14284b — Text on selected chip
- `--fm-success` #1f6b4a — I can do this ON, checks, online dot
- `--fm-success-soft` #e3f1ea — Accepted answer, success fills
- `--fm-pro` #5b3a8a — Pro plan tile, FedM8 AI badge
- `--fm-pro-soft` #efe8f7 — Pro/AI fills
- `--fm-urgent` #a3262a — Due ≤48h (R5)
- `--fm-urgent-soft` #fbe3e3 — Urgent chip fill
- `--fm-warn` #8a4b00 — Due ≤7 days (R5)
- `--fm-warn-soft` #fde3b0 — Warn chip fill
- `--fm-info-soft` #eaf0fb — Neutral type badges
- `--fm-focus-ring` #1d4ed8 — Keyboard focus, 2px, never removed

## Components
AppHeader, PillNav, NavPill, IconButton, WorkspaceSwitcher, Avatar, AvatarStack, PresenceDot, ChevronBreadcrumb, ChevronStepper, PageTitle, PrimaryButton, SecondaryButton (outline), TextButton, ChoiceCard (picture/icon, label, count, avatar stack, selected/next-up states), StatCard, SearchField, FilterChip, SavedSearchChip, SegmentedControl, Tabs, NoticeRow (list row with urgency chip, comment/team counts), UrgencyChip, TypeBadge, RoleBadge (Prime/Sub/1099/Mentor/Applicant/Partner/Reviewer), CertBadge (SDVOSB/VOSB/8(a)/HUBZone/WOSB), ProTag, SampleChip (comp-only; must not ship), FactLine, CountdownBanner, SummaryCard (AI BLUF, pending/loading/ready/failed states), RequirementToggle ("I can do this"), CoverageMeter, ClaimBar, JoinAsPicker (3 ChoiceCards), RosterList / RosterLane, ProLock (contact gated), FollowButton, TrackingBar (N tracking, N online, Follow), ThreadList, ThreadPost (vote rail, author, role badge, time, body, Reply/Share/Save, nested children with thread line), VoteRail, Composer, BotPost (FedM8 AI), SortTabs (Hot/New/Top), ActivityFeedItem, ProgressCard, Checklist, Drawer, KanbanColumn, KanbanCard, ChatList, ChatBubble, ContractEmbedCard, FileCard, ReactionChip, TypingIndicator, QuestionCard (wizard), TrackPicker, MentorCard, SquadCard, WinCard, CourseRow, LessonList, PlanCard, Toggle, SettingsNav, EmptyState, LoadingSkeleton, ErrorState, BottomTabBar (mobile), FAB (mobile), ThemeSwitcher (new, see Plan & settings)

## Totals: 16 pages · 163 modules · 206 features · 397 acceptance checks

## Page: Contract detail (`contract-detail`)
- Route: /os#/opportunities?notice=<noticeId> · Nav: Contracts · Breadcrumb: Find work ▸ Contracts ▸ <solicitation # e.g. H9821027SS0001>
- Purpose: Decide whether to track, team on, or bid a SAM.gov notice: see the deadline, read the AI summary, mark which requirements you can cover, join as Prime/Sub/1099, and discuss with other members.
- Primary action: Track — top-right of detail header (swaps to 'Tracking' outline-state once tracked; no second amber button on screen)
- Screen: https://fedm8.com/design/003/screens/Main.png · Comp: https://fedm8.com/design/003/comps/Main.dc.txt

### Eye path
1 Title + CountdownBanner ('Due tomorrow, 12:00 PM ET', urgency-coloured per R5) with Track button top-right
2 SummaryCard (AI BLUF; pending/loading/ready/failed) plus one-line FactLine (NAICS · set-aside · place · solicitation # · posted)
3 Can you do this? (RequirementToggles + CoverageMeter)
4 Join as (3 ChoiceCards Prime/Sub/1099) then ClaimBar
5 Discussion (Composer, SortTabs, threads, FedM8 AI BotPost)
Side context, never competing: notice list on the left (selected row cream, not navy), TrackingBar and links in the header, 'Who is here' presence

### Modules
- **0. App header and primary nav** (`app-header`, AppHeader + PillNav, header)
  - FedM8 shield logo + wordmark (home link)
  - NavPills with icons: Home, Contracts, Grants, Teaming, Certifications, Veterans, Learn, Community (all 8 always visible, none behind an 'All tools' menu)
  - Active pill = Contracts (navy fill, white ink)
  - WorkspaceSwitcher showing 'FEDM8 LLC'
  - Avatar 'JN' (account menu)
  - ↳ click pill navigates to that area
  - ↳ WorkspaceSwitcher opens workspace list
  - ↳ Avatar opens account menu (profile, settings, sign out)
  - ↳ Tab order: logo, pills, switcher, avatar
  - states: empty: n/a; loading: nav renders immediately; switcher shows skeleton until workspaces load; error: switcher falls back to last workspace name; selected: active pill navy with aria-current=page
  - data: nav items = real (static app config)
  - data: workspace name FEDM8 LLC = real (workspace/profile table (TBD name))
  - data: user initials = real (auth user profile)
- **1. Breadcrumb** (`breadcrumb`, ChevronBreadcrumb, header)
  - Arrow-segment chevrons: Find work > Contracts > solicitation number
  - Last segment = current page, not a link
  - ↳ click 'Find work' goes to Find work hub
  - ↳ click 'Contracts' goes to contract list/search
  - ↳ keyboard focusable segments
  - states: empty: n/a; loading: solicitation segment shows skeleton until notice resolves; error: shows 'Notice' if id unresolved; selected: last segment highlighted with --fm-accent-soft
  - data: solicitation number = real (SAM.gov notice (solicitationNumber))
- **2. Search and filters** (`search-filters`, SearchField + FilterChip + SavedSearchChip, left)
  - SearchField placeholder 'Search notices'
  - FilterChips: NAICS 541511 (removable x), Due < 30 days, Set-aside: Any
  - Saved search chip 'IT services - VA/MD/DC' with dropdown caret
  - Sort control 'Most discussed' with icon
  - ↳ typing filters list (debounced)
  - ↳ chip toggles selected/unselected (two states only, R7)
  - ↳ x on NAICS chip removes it
  - ↳ saved search chip applies all its filters
  - ↳ sort menu: Most discussed, Due soonest, Newest
  - states: empty: no filters = all open notices; loading: list shows 8 skeleton rows; error: ErrorState with Retry above list; selected: chip uses --fm-chip-selected-bg; unselected is outline
  - data: NAICS, set-aside, due filters = real (SAM.gov notice index (opportunities table, TBD))
  - data: saved searches = sample (TBD saved_searches table)
- **3. Notice list** (`notice-list`, NoticeRow x8 in ThreadList-style list, left) · social
  - 8 rows: title, agency, notice type, NAICS, due date, UrgencyChip, comment count icon+number, teaming count icon+number
  - Rows in comp: DHRA Web Sustainment (Nov 3, 24/13), EPA GAMS (Nov 4, 3/1), FDIC JFrog (Nov 5, 9/4), Air Force TransMagic (Nov 5, 2/2), FAA FOMS (Nov 6, 11/6), VA Steris (Nov 9, 5/2), US Marshals Cox Nevada (Nov 13, 1/0), DLA RFI (Nov 27, 7/3)
  - Selected row = cream tint + bold title (R1)
  - ↳ click row loads detail in main pane and updates breadcrumb/route
  - ↳ arrow keys move selection
  - ↳ urgency colour by days left (R5): <=48h urgent, <=7d warn, later neutral
  - states: empty: EmptyState 'No notices match' with Clear filters text button; loading: 8 skeleton rows; error: ErrorState with Retry; selected: --fm-row-selected-bg, bold title, no navy
  - data: title, agency, type, NAICS, due date = real (SAM.gov notices (opportunities table))
  - data: comment counts = sample (TBD discussion threads count)
  - data: teaming counts = sample (TBD roster members count)
- **4. Detail header** (`detail-header`, PageTitle + TypeBadge + Track PrimaryButton, main)
  - TypeBadge 'Sources Sought'
  - Title 'Web Sustainment/Content Management/Cloud Hosting'
  - Agency path 'Dept of Defense > Defense Human Resources Activity (DHRA)'
  - Track PrimaryButton (amber) top-right
  - IconButton 'More actions' (kebab)
  - ↳ Track adds notice to My pipeline (stage Watching) and changes to 'Tracking'
  - ↳ More actions: copy link, share to Community, mute
  - ↳ keyboard: Track reachable first in header
  - states: empty: n/a; loading: title and badge skeleton; error: ErrorState 'Notice unavailable' with link to SAM.gov; selected: Tracking state shown as outline with check icon
  - data: type, title, agency path = real (SAM.gov notice)
  - data: tracked state = sample (TBD pipeline table)
- **5. Countdown** (`countdown`, CountdownBanner + UrgencyChip, main)
  - Large text 'Due tomorrow, 12:00 PM ET' (replaces the small 'Due in 4 days' chip; comp shows 'Due in 4 days . Nov 3, 12:00 PM ET', compute live)
  - Colour by R5
  - ↳ updates without reload as time passes
  - ↳ click adds to calendar (text button)
  - states: empty: no deadline: 'No response date posted' neutral; loading: skeleton; error: falls back to plain date; selected: closed notices show 'Closed' muted
  - data: response deadline = real (SAM.gov responseDeadLine)
- **6. Tracking bar** (`tracking-bar`, TrackingBar + AvatarStack + FollowButton, main) · social
  - AvatarStack DR MT PS LG KA
  - '18 tracking' and '5 online now' with PresenceDot
  - Follow DHRA button (outline) to follow the agency
  - ↳ click stack opens roster drawer
  - ↳ Follow DHRA toggles to 'Following'
  - ↳ hover avatar shows name and role
  - states: empty: 'Be the first to track' no avatars; loading: skeleton stack; error: hides counts, keeps Follow; selected: Following = outline with check
  - data: tracking and online counts, avatars = sample (TBD tracking table, presence)
  - data: followed agencies = sample (TBD follows table)
- **7. AI summary (BLUF)** (`summary`, SummaryCard, main) · Pro
  - BLUF text from AI
  - States badge: pending/loading/ready/failed
  - Comp state: 'Summary is still being written from the 2 attachments' with 'Notify me when ready'
  - Pro tag (AI requirement summaries are Pro)
  - ↳ Notify me when ready subscribes user (text button)
  - ↳ Retry on failed
  - ↳ never shows invented summary
  - states: empty: no attachments: 'No attachments to summarise'; loading: pending copy plus skeleton lines; error: failed with Retry; selected: ready: 2-3 sentence BLUF, source link to attachments
  - data: AI summary = placeholder (TBD edge fn summarising attachments; never fabricated)
- **8. Fact line** (`fact-line`, FactLine, main)
  - Single line: Responses due Nov 3, 12:00 PM ET . NAICS 541511 . Set-aside None . Alexandria, VA 22350 . Solicitation H9821027SS0001
  - 'Posted Oct 21, 2026' is folded in (orphan tile removed)
  - ↳ NAICS value links to NAICS filter
  - ↳ text selectable
  - states: empty: missing fields omitted, never 'undefined'; loading: one skeleton line; error: n/a; selected: n/a
  - data: due, NAICS, set-aside, place, solicitation #, posted = real (SAM.gov notice)
- **9. Can you do this?** (`requirements`, RequirementToggle list + CoverageMeter, main) · social
  - Heading 'Can you do this?' and meter 'You cover 2 of 4'
  - 4 toggles: Web sustainment, Content management, Cloud hosting, NAICS 541511 - Custom computer programming services (each 'I can do this', ON = --fm-success-soft)
  - Note 'More requirements after the 2 attachments are read'
  - ↳ toggle sets I can do this and updates meter and ClaimBar
  - ↳ state persists per user per notice
  - ↳ keyboard Space toggles, aria-pressed
  - states: empty: no requirements parsed: 'Requirements appear after attachments are read'; loading: skeleton toggles; error: ErrorState Retry; manual add-requirement text button; selected: ON green soft with check
  - data: requirement list = placeholder (comp shows NAICS plus keywords; attachment-derived list TBD)
  - data: my toggles = sample (TBD user_requirement_claims)
- **10. Join as** (`join-as`, JoinAsPicker (3 ChoiceCards), main) · social
  - Cards: Prime 'Lead the bid' (2), Sub 'Join a prime' (7), 1099 'Contract by the task' (4)
  - Label first (type-lg), count second with unit word e.g. '2 members' (R6)
  - AvatarStack per role with +N
  - Names line e.g. 'Luis G. . Navy vet', 'Priya S. . Section 508 / UX', 'Marcus T. . USMC . Web dev'
  - 'Recommended 3x' chip only on the card where it stands out (not on all)
  - Selected card = navy, Prime shown selected in comp
  - ↳ click selects role (one at a time) and updates ClaimBar
  - ↳ click again opens roster lane for that role
  - ↳ Recommended chip shown only if recommendation exists
  - states: empty: role count 0 shows 'Be first'; loading: 3 skeleton cards; error: ErrorState Retry; selected: navy fill, white ink (R1)
  - data: role counts and avatars = sample (TBD teaming_roster)
  - data: recommended N x = sample (TBD Jev matching)
- **11. Claim bar** (`claim-bar`, ClaimBar + ProLock, main) · Pro · social
  - 'Your claim: Web sustainment, Cloud hosting' (from toggles)
  - Contact button with Pro tag (contact roster members)
  - ↳ Contact opens message composer to roster members if Pro
  - ↳ non-Pro opens upgrade drawer (ProLock)
  - ↳ claim text updates live
  - states: empty: 'Pick a role and what you can cover'; loading: skeleton; error: inline Retry; selected: claim lists chosen items
  - data: my claim = sample (TBD)
  - data: Pro entitlement = real (plan/subscription (veterans 3 months free))
- **12. Discussion** (`discussion`, ThreadList + Composer + SortTabs + ThreadPost + VoteRail, main) · social
  - Header 'Discussion' and '24 comments'
  - SortTabs Hot / New / Top
  - Composer: avatar JN, 'Add to the discussion', Post button (outline)
  - Post 1 Dana R. SDVOSB Army vet . Cloud ops . 2h, vote 31, body, Reply/Share/Save
  - Nested: Luis G. Prime 1h up 12; Priya S. Sub 40m up 6 (thread line)
  - Post 2 Marcus T. 1099 USMC . Web dev . 3h, vote 18
  - Up/Down vote buttons with aria labels
  - ↳ sort changes order
  - ↳ vote toggles up/down once per user
  - ↳ Reply opens inline composer
  - ↳ Share copies link/posts to Community
  - ↳ Save bookmarks
  - ↳ Post submits and appends
  - states: empty: EmptyState 'Start the conversation'; loading: 3 skeleton posts; error: Retry; failed post keeps draft; selected: own posts show Edit/Delete
  - data: posts, votes, authors, roles, times = sample (TBD discussion tables; role badge from roster)
- **13. FedM8 AI bot post** (`ai-bot-post`, BotPost, main) · social
  - Bot avatar, 'FedM8 AI' with Bot tag (--fm-pro), 1h
  - Message 'Summary is still being written from the 2 attachments' with Notify me when ready
  - ↳ Notify me subscribes
  - ↳ replaced by summary post when ready
  - states: empty: hidden if no bot activity; loading: n/a; error: hidden on failure; selected: n/a
  - data: bot message = placeholder (TBD ai summary job status)
- **14. Source and attachments** (`source-links`, FileCard + TextButton, right) · Pro
  - '2 attachments' count/link
  - Contacts: Matthew Poole (primary) . Tameka Laws
  - Open on SAM.gov external link
  - Ask FedM8 button (AI question, Pro)
  - Fit line: 'Fit - set up profile'
  - Past awards line: '[loading]'
  - ↳ attachments open list/download
  - ↳ contact names link to mailto or copy
  - ↳ Open on SAM.gov new tab
  - ↳ Ask FedM8 opens AI drawer
  - ↳ Fit prompts profile setup, never shows invented score
  - ↳ Past awards loads real or hides
  - states: empty: no contacts: row hidden; loading: Past awards skeleton (no '[loading]' text in prod); error: row hidden with Retry text; selected: n/a
  - data: attachments, contacts, SAM link = real (SAM.gov notice)
  - data: fit score = placeholder (TBD; shown only after profile exists)
  - data: past awards = placeholder (TBD USAspending lookup (Pro))
- **15. Who is here** (`presence`, AvatarStack + PresenceDot, right) · social
  - Online members on this notice with green PresenceDot
  - aria-label 'Who is here'
  - ↳ click avatar opens mini profile
  - states: empty: 'Just you'; loading: skeleton; error: hidden; selected: n/a
  - data: online members = sample (TBD realtime presence)

### Features
- Select any notice from list and see detail without leaving page
- Search notices and filter by NAICS, due window, set-aside
- Saved searches
- Sort by Most discussed
- Live countdown with urgency colour
- Track notice into pipeline
- Follow agency
- Tracking and online counts with avatars
- AI BLUF summary with pending/ready/failed
- Per-requirement I can do this with coverage meter
- Join as Prime, Sub or 1099 with roster counts
- Claim bar summarising what user covers
- Contact roster members (Pro)
- Reddit-style discussion with votes, nesting, sort, reply, share, save
- FedM8 AI bot post with notify-me
- Open on SAM.gov, attachments, contacts
- Ask FedM8 (AI)
- Fit and past-awards lookups
- Chevron breadcrumb
- Runtime theming via tokens

### v2 changes vs comp
- R1 selected list row becomes cream tint + bold title; navy only on active nav pill and selected Join-as card
- R3 reorder: title+countdown, Summary, Can you do this?, Join as, Discussion
- R3 six fact tiles collapse to one FactLine; orphan 'Posted' tile removed
- R2 Track is the only amber button; Post, Follow, Contact are outline
- R5 large countdown under title ('Due tomorrow 12:00 PM ET') replaces small chip; list rows use same urgency colours
- R6 Join-as label first, count second with unit word
- R7 filter chips two-state only
- R8 meta text >= 13px at 4.5:1
- R9 '[loading]' and unknown counts hidden or skeleton, not shown as text
- Remove 'Sample members' chip in production
- Recommended 3x chip only when notable

### Acceptance
1. Given page loads When viewing top of main pane Then breadcrumb shows 3 chevron segments and last is not a link
2. Given nav When page renders Then all 8 pills are visible, Contracts has aria-current=page and navy fill
3. Given search field When typing 'DLA' Then list filters to matching rows within 500ms
4. Given NAICS chip When x clicked Then chip removed and list re-queries
5. Given a chip When toggled Then it uses chip-selected-bg when on and outline when off, never navy
6. Given list When a row is selected Then row has cream tint and bold title and no navy fill
7. Given list rows When due in <=48h Then chip uses --fm-urgent; <=7d --fm-warn; later neutral
8. Given a notice When detail opens Then order top to bottom is header+countdown, Summary, Can you do this, Join as, Discussion
9. Given the header When inspected Then exactly one --fm-action button exists on the page (Track)
10. Given Track When clicked Then button becomes Tracking state and notice appears in My pipeline
11. Given deadline tomorrow noon When rendered Then countdown reads 'Due tomorrow' with time ET in urgent colour
12. Given TrackingBar When Follow DHRA clicked Then label changes to Following
13. Given summary pending When rendered Then pending copy and Notify me show and no fabricated text
14. Given summary failed When rendered Then Retry shown
15. Given FactLine When rendered Then one line holds due, NAICS, set-aside, place, solicitation # and posted, no tiles
16. Given requirement toggles When 2 of 4 ON Then meter reads 'You cover 2 of 4' and ClaimBar lists those 2
17. Given toggle When pressed Then aria-pressed flips and state persists after reload
18. Given Join as When rendered Then each card shows label first then 'N members' muted
19. Given a role card When clicked Then only that card is navy and ClaimBar role updates
20. Given non-Pro user When Contact clicked Then upgrade drawer opens and no message sent
21. Given Pro user When Contact clicked Then composer to roster opens
22. Given Discussion When Hot/New/Top clicked Then order changes and active tab shows selected style
23. Given a post When upvote clicked twice Then count returns to original
24. Given Composer empty When Post pressed Then button disabled
25. Given Composer text When Post pressed Then post appears at top and count increments
26. Given nested replies When rendered Then thread line connects child to parent
27. Given bot post When summary ready Then bot post is replaced by summary link
28. Given attachments row When clicked Then attachment list opens; Open on SAM.gov opens new tab to the real notice
29. Given Fit with no profile When rendered Then shows 'set up profile' and no number
30. Given past awards loading When rendered Then skeleton shows, never the text '[loading]'
31. Given presence When no one else online Then shows 'Just you'
32. Given any meta text When measured Then font-size >= 13px and contrast >= 4.5:1
33. Given production build When searched Then no 'Sample members' chip is rendered

### Responsive
<=1024: list collapses to a top drawer/segmented 'Notices' toggle, detail full width, right links stack under Discussion. <=480: BottomTabBar, 3-segment control Overview / Talk 24 / Team 13 (Can do inside Team), full-width amber Track, Join-as cards stack vertically, FAB for new post with bottom padding equal to its height.

### Accessibility
- Landmarks: header, nav aria-label Main, main with regions 'Notice list', 'Notice detail', 'Requirements', 'Join as', 'Discussion'
- Breadcrumb nav aria-label Breadcrumb with aria-current on last
- Focus order: nav, search, list, Track, summary, requirements, Join as, Discussion
- Vote buttons labelled Upvote/Downvote, avatar stacks have aria-labels with names
- Urgency conveyed by text and icon as well as colour
- Visible focus ring --fm-focus-ring, tap targets >= 44px
- Reduce motion respected for countdown ticks

### Open questions
- Where do attachment-derived requirements come from and when are they available?
- Is Fit scoring in scope for v1 or hidden?
- What happens to a user's claim and role when a notice closes?
- Can a user hold more than one role on the same notice?
- Does the 24 comments number include nested replies?

## Page: Home (`home`)
- Route: /os#/home · Nav: Home · Breadcrumb: —
- Purpose: Tell the user what needs attention today (deadlines, matches, invites, replies), let them jump straight to Find work, and show their network's activity.
- Primary action: Find work — top-right of the greeting row
- Screen: https://fedm8.com/design/003/screens/01-home.png · Comp: https://fedm8.com/design/003/comps/01-home.dc.txt

### Eye path
1 H1 greeting 'Morning, Joey' with date, Find work amber button top-right
2 Four StatCards: Due this week, New matches, Team invites, Replies to you (only big numbers on the app)
3 Your deadlines list (soonest first, DHRA row cream with red 'Tomorrow' chip, not navy)
4 Matched for you list
5 Right rail: From your network feed, FedM8 AI bot, Finish your profile (Continue is outline)

### Modules
- **0. App header and primary nav** (`app-header`, AppHeader + PillNav, header)
  - Logo, 8 NavPills with icons (Home active/navy, Contracts, Grants, Teaming, Certifications, Veterans, Learn, Community)
  - Two IconButtons (search and notifications, icon-only)
  - WorkspaceSwitcher 'FEDM8 LLC'
  - Avatar 'JN'
  - ↳ pill navigates
  - ↳ notification button opens list with unread dot
  - ↳ switcher and avatar open menus
  - states: empty: no unread: no dot; loading: nav immediate; error: n/a; selected: Home navy with aria-current
  - data: nav, workspace, user = real (app config, auth profile)
  - data: unread notifications = sample (TBD notifications table)
- **1. Greeting and primary action** (`greeting`, PageTitle + PrimaryButton, main)
  - H1 'Morning, Joey' (time-of-day aware)
  - Date 'Mon, Nov 2'
  - Find work amber button with search icon
  - No breadcrumb (R4)
  - ↳ Find work navigates to Find work hub
  - ↳ greeting changes Morning/Afternoon/Evening
  - states: empty: falls back to 'Welcome back' if name missing; loading: H1 skeleton; error: n/a; selected: n/a
  - data: first name = real (profile)
  - data: today's date = real (client clock)
- **2. StatCard: Due this week** (`stat-due`, StatCard, main) · social
  - Number 4 (type-stat)
  - Label 'Due this week'
  - Sub 'DHRA tomorrow, 12:00 PM ET'
  - AvatarStack LG PS +9
  - Card styled light (was navy; navy removed per R1)
  - ↳ click opens My pipeline filtered to due this week
  - ↳ avatar stack opens roster drawer
  - states: empty: 0 shows '0' with 'Nothing due' sub; loading: number skeleton (R9: no placeholder digits); error: number hidden, card shows Retry; selected: n/a
  - data: due-this-week count = real (pipeline items with deadline <= 7d (TBD query))
  - data: avatars +9 = sample (TBD teaming)
- **3. StatCard: New matches** (`stat-matches`, StatCard, main) · social
  - Number 3
  - Label 'New matches'
  - Sub 'NAICS 541511 . 541519'
  - AvatarStack DR MT
  - ↳ click scrolls/navigates to Matched for you
  - states: empty: 0 'No new matches' with link to edit NAICS; loading: skeleton; error: hide number; selected: n/a
  - data: new matches count = real (opportunities matching profile NAICS since last visit (TBD))
  - data: NAICS list = real (profile NAICS codes)
  - data: avatars = sample (TBD)
- **4. StatCard: Team invites** (`stat-invites`, StatCard, main) · social
  - Number 2
  - Label 'Team invites'
  - Sub 'Kim A. . VOSB, Prime' with CertBadge text
  - AvatarStack KA LG
  - ↳ click opens Teaming invites
  - states: empty: 0 'No invites'; loading: skeleton; error: hide number; selected: n/a
  - data: invites = sample (TBD team_invites)
- **5. StatCard: Replies to you** (`stat-replies`, StatCard, main) · social
  - Number 3
  - Label 'Replies to you'
  - Sub 'Luis G. in DHRA thread'
  - AvatarStack LG DR PS
  - ↳ click opens Messages/Community replies
  - states: empty: 0 'No replies yet'; loading: skeleton; error: hide number; selected: n/a
  - data: replies = sample (TBD discussion replies)
- **6. Your deadlines** (`deadlines`, NoticeRow list + Section header, main) · social
  - Heading 'Your deadlines' and '5 tracked'
  - 'My pipeline' link with arrow
  - Row 1 DHRA Web Sustainment: Sources Sought . Writing stage, 'Tomorrow . 12:00 PM' red chip, AvatarStack LG PS +11, 24 comments, '2 of 4 ready' coverage
  - Row 2 EPA GAMS: Special Notice . Watching, Nov 4 . 2 days, 3 comments, 1 team
  - Row 3 FDIC JFrog: Solicitation . Teaming, Nov 5 . 3 days, 9/4
  - Row 4 FAA FOMS: Sources Sought . Teaming, Nov 6 . 4 days, 11/6
  - Soonest first; DHRA row cream with red chip, not navy
  - ↳ row click opens contract detail
  - ↳ My pipeline link opens pipeline
  - ↳ urgency per R5
  - states: empty: EmptyState 'Nothing tracked yet' with Find work link; loading: 4 skeleton rows; error: ErrorState Retry; selected: n/a (cream tint only for the due-tomorrow row)
  - data: titles, agency, type, dates = real (SAM.gov notices in user's pipeline)
  - data: stage names = sample (TBD pipeline stages)
  - data: comment/team counts, avatars, '2 of 4 ready' = sample (TBD)
- **7. Matched for you** (`matched`, NoticeRow list, main) · social
  - Heading 'Matched for you' with '3 new' badge
  - 'All 7,829' link with arrow
  - TransMagic Renewal: Air Force . Special Notice . 541519, Nov 5, 2 comments, 0 teaming
  - Cox Nevada Telcom FY27 Option Year: US Marshals . Special Notice . 541519, Nov 13, 1/0, 'Sole source' tag
  - DLA RFI Market Research (WBOS): DLA . Sources Sought . 541512, Nov 27, 7/3
  - ↳ row opens contract detail
  - ↳ All 7,829 opens Contracts list
  - ↳ Sole source tag is informational
  - states: empty: 'No matches. Add NAICS codes' text button; loading: 3 skeleton rows; error: ErrorState Retry; selected: n/a
  - data: titles, agency, NAICS, dates, sole-source = real (SAM.gov notices)
  - data: 3 new, All 7,829 total = real (opportunities count (verify live))
  - data: comment/team counts = sample (TBD)
- **8. From your network** (`network-feed`, ActivityFeedItem list, right) · social
  - Heading 'From your network' with '5 online now' and PresenceDot
  - Priya S. joined your team as Sub, DHRA . 40m (RoleBadge)
  - Luis G. replied in the DHRA thread, 1h . up 12
  - Dana R. earned VOSB on SBA VetCert, 2h (CertBadge)
  - Kim A. is tracking FOMS Software Development, 3h
  - Marcus T. posted in Community, 5h . up 18
  - ↳ item opens its target (team, thread, profile, notice, post)
  - ↳ avatar opens profile
  - states: empty: 'Follow members to see activity'; loading: 5 skeleton items; error: Retry; selected: n/a
  - data: all activity items and online count = sample (TBD activity feed)
- **9. FedM8 AI note** (`ai-bot`, BotPost, right)
  - Bot avatar, 'FedM8 AI' Bot tag, 1h
  - 'DHRA summary is still being written.' and Notify me link
  - ↳ Notify me subscribes
  - states: empty: hidden; loading: n/a; error: hidden; selected: n/a
  - data: summary job status = placeholder (TBD)
- **10. Finish your profile** (`profile-progress`, ProgressCard + Checklist, right)
  - Heading 'Finish your profile' with 60%
  - Progress bar
  - 3 checklist rows with chevron: Upload capability statement, Add past performance, Connect SAM.gov UEI
  - Continue button (outline, was navy)
  - ↳ row opens that step
  - ↳ Continue goes to next incomplete step
  - ↳ percent recalculates as steps complete
  - states: empty: 100%: card collapses to 'Profile complete' and hides; loading: skeleton bar; error: hide percent; selected: n/a
  - data: completion % and steps = sample (TBD profile completeness calc)

### Features
- Personalised greeting and date
- Find work shortcut
- 4 StatCards deep-linking to their lists
- Deadlines list from pipeline with stage, coverage, comments, team
- Matched-for-you notices by NAICS
- Network activity feed with presence
- FedM8 AI notification
- Profile completion checklist
- Urgency colours per R5
- Runtime theming

### v2 changes vs comp
- R2 Find work is the only amber button; Continue becomes outline
- R1 'Due this week' card and DHRA row no longer navy: StatCard light, DHRA row cream with red 'Tomorrow' chip
- R4 no breadcrumb on Home
- R5 apply urgency colours to every deadline chip and 'Nov 4 . 2 days' strings
- R6/R9 big numbers only on StatCards; hide numbers while loading
- R8 meta >= 13px
- Remove 'Sample members' chip for production

### Acceptance
1. Given Home loads When inspecting header Then Home pill is navy with aria-current=page
2. Given it is morning When rendered Then H1 reads 'Morning, <first name>' and date matches today
3. Given page When counting amber buttons Then exactly one (Find work) exists
4. Given Find work When clicked Then app navigates to Find work hub
5. Given 4 StatCards When rendered Then they show Due this week, New matches, Team invites, Replies to you with labels and sub lines
6. Given a StatCard count loading When rendered Then no number or placeholder digit shows, only skeleton
7. Given Due this week card When inspected Then background is --fm-surface, not navy
8. Given StatCard click When clicked Then it navigates to its corresponding list
9. Given Your deadlines When rendered Then rows are sorted soonest first and heading shows '<N> tracked'
10. Given DHRA due tomorrow When rendered Then row has cream tint and chip uses --fm-urgent, no navy fill
11. Given a row due in 2-7 days When rendered Then chip uses --fm-warn
12. Given row click When clicked Then contract detail opens
13. Given My pipeline link When clicked Then pipeline opens
14. Given Matched for you When rendered Then '3 new' badge and 'All N' link show and N equals live opportunity count
15. Given a sole-source notice When rendered Then 'Sole source' tag is shown
16. Given From your network When rendered Then 5 activity items with avatar, text, time; online count with PresenceDot
17. Given activity item When clicked Then its target opens
18. Given FedM8 AI note When Notify me clicked Then subscription saved and link shows 'You will be notified'
19. Given profile 60% When rendered Then bar shows 60% and 3 checklist rows; Continue is outline
20. Given Continue When clicked Then next incomplete step opens
21. Given no tracked items When rendered Then EmptyState with Find work link shows
22. Given any meta text When measured Then >= 13px at >= 4.5:1

### Responsive
<=1024: right rail drops below main (network feed, AI note, profile progress stacked), StatCards 2x2. <=480: BottomTabBar, StatCards 2 columns compact then horizontal-scroll-free stack, Find work becomes full-width, deadline rows show title+chip only with meta on second line.

### Accessibility
- Landmarks: header, nav Main, main, aside 'From your network'
- H1 is the greeting; section headings h2
- StatCards are links with full text label
- Progress bar role=progressbar with value
- Urgency has text, not colour alone
- Focus order follows eye path; tap targets >= 44px

### Open questions
- Do 'New matches' count since last visit or last 24h?
- Which events appear in the network feed and can users mute it?
- Is 'Replies to you' limited to threads the user posted in?
- Should the profile card disappear at 100%?

## Page: Find work hub (`find-work`)
- Route: /os#/find-work · Nav: Contracts · Breadcrumb: —
- Purpose: Pick a source of work (Contracts, Grants, Subcontracts, Forecasts), recall a saved search, and scan the most-discussed open notices.
- Primary action: My pipeline — top-right of the H1 row (shows count of tracked items)
- Screen: https://fedm8.com/design/003/screens/02-find-work.png · Comp: https://fedm8.com/design/003/comps/02-find-work.dc.txt

### Eye path
1 H1 'Find work' with My pipeline (5) top-right
2 Four picture ChoiceCards left to right: Contracts, Grants, Subcontracts, Forecasts (label first, count second)
3 Saved searches row: IT services chip with '3 new', filter chips, Save search
4 Contracts list '8 of 7,829' sorted Most discussed; See all
Urgency colour on each row by days left

### Modules
- **0. App header and primary nav** (`app-header`, AppHeader + PillNav, header)
  - Logo, 8 NavPills (Contracts shown active in comp as Find work lives under Contracts)
  - Two IconButtons
  - WorkspaceSwitcher 'FEDM8 LLC'
  - Avatar 'JN'
  - ↳ pill navigates
  - ↳ icons open search/notifications
  - states: empty: n/a; loading: immediate; error: n/a; selected: active pill navy with aria-current
  - data: nav, workspace, user = real (app config, auth profile)
- **1. Title and pipeline action** (`page-title`, PageTitle + PrimaryButton, main)
  - H1 'Find work' (single-segment breadcrumb 'Find work' removed, R4)
  - 'Sample members' chip (comp only)
  - My pipeline button with icon and count 5
  - ↳ My pipeline opens pipeline page
  - ↳ count updates when tracking changes
  - states: empty: count hidden at 0; loading: count skeleton; error: count hidden; selected: n/a
  - data: pipeline count 5 = sample (TBD pipeline items count)
- **2. Source card: Contracts** (`choice-contracts`, ChoiceCard, main) · social
  - Picture/icon, label 'Contracts', sub 'SAM.gov notices'
  - Count '7,829 open' (count second, unit word)
  - AvatarStack LG PS KA
  - '142 members watching'
  - Selected state (navy) because Contracts list is below
  - ↳ click selects source and switches list below and chips
  - ↳ keyboard Enter/Space
  - ↳ arrow keys between cards
  - states: empty: count hidden if unknown; loading: count skeleton; error: count hidden; selected: navy fill, white ink (R1)
  - data: open notices count = real (opportunities table count (verify live))
  - data: members watching, avatars = sample (TBD)
- **3. Source card: Grants** (`choice-grants`, ChoiceCard, main) · social
  - Label 'Grants', sub 'Grants.gov'
  - Count (comp '[count] open' placeholder, must be real or hidden)
  - AvatarStack DR MT
  - '38 members watching'
  - ↳ click opens Grants list
  - ↳ same keyboard behaviour
  - states: empty: hide count; loading: skeleton; error: hide count; selected: navy when active
  - data: open grants count = placeholder (grants table count TBD; hide until real (R9))
  - data: members watching = sample (TBD)
- **4. Source card: Subcontracts** (`choice-subs`, ChoiceCard, main) · social
  - Label 'Subcontracts', sub 'SBA SubNet'
  - Count '[count] open' placeholder
  - AvatarStack PS LG DR
  - '57 members watching'
  - ↳ click opens Subcontracts list
  - states: empty: hide count; loading: skeleton; error: hide count; selected: navy when active
  - data: open subcontract count = placeholder (SubNet source TBD)
  - data: members watching = sample (TBD)
- **5. Source card: Forecasts** (`choice-forecasts`, ChoiceCard, main) · social
  - Label 'Forecasts', sub 'Planned buys'
  - Count '[count] upcoming' placeholder
  - AvatarStack KA MT
  - '21 members watching'
  - ↳ click opens Forecasts list
  - states: empty: hide count; loading: skeleton; error: hide count; selected: navy when active
  - data: upcoming forecasts count = placeholder (agency forecast source TBD)
  - data: members watching = sample (TBD)
- **6. Saved searches** (`saved-searches`, SavedSearchChip + FilterChip + TextButton, main)
  - Heading 'Saved searches'
  - Saved search chip 'IT services - VA/MD/DC' with '3 new' badge
  - Filter chips: NAICS 541511, Due < 30 days, SDVOSB set-aside
  - Save search text button with icon
  - ↳ saved chip applies its filters to the list
  - ↳ filter chip toggles (two states, R7)
  - ↳ Save search stores current filters and names it
  - ↳ new badge clears after opening
  - states: empty: 'No saved searches. Filter, then Save search'; loading: chip skeletons; error: Retry; selected: --fm-chip-selected-bg; unselected outline
  - data: saved searches and new counts = sample (TBD saved_searches)
  - data: filter options = real (NAICS, set-aside, due-date fields on notices)
- **7. List header** (`contracts-list-header`, Section header + sort, main)
  - Heading 'Contracts'
  - Count '8 of 7,829'
  - Sort control 'Most discussed'
  - 'See all 7,829' link with arrow
  - ↳ sort menu changes order
  - ↳ See all opens full Contracts search
  - states: empty: '0 results'; loading: count skeleton; error: hide count; selected: n/a
  - data: shown/total counts = real (opportunities query count)
- **8. Contracts list** (`contracts-list`, NoticeRow x8, main) · social
  - DHRA Web Sustainment/Content Management/Cloud Hosting, Sources Sought, 541511, Nov 3, 24 comments, 13 teaming
  - EPA GAMS Licenses and Maintenance, Special Notice, 541519, Nov 4, 3/1, Sole source tag
  - FDIC JFrog Artifactory Subscription Maintenance, Solicitation, 541519, Nov 5, 9/4
  - Air Force TransMagic Renewal, Special Notice, 541519, Nov 5, 2/0, Sole source tag
  - FAA FOMS Software Development, Sources Sought, 541511, Nov 6, 11/6
  - VA Steris Software Connectivity, Presolicitation, 541519, Nov 9, 5/2
  - US Marshals Cox Nevada Telcom FY27 Option Year, Special Notice, 541519, Nov 13, 1/0
  - DLA RFI Market Research (WBOS), Sources Sought, 541512, Nov 27, 7/3
  - Each row: UrgencyChip by days left, comment icon+count, teaming icon+count
  - ↳ row opens contract detail
  - ↳ keyboard arrow navigation
  - ↳ urgency colour R5
  - states: empty: EmptyState 'No notices match' + Clear filters; loading: 8 skeleton rows; error: ErrorState Retry; selected: hover cream tint; no persistent selection on this page
  - data: title, agency, type, NAICS, due date, sole source = real (SAM.gov notices)
  - data: comment and teaming counts = sample (TBD)

### Features
- Four source ChoiceCards with counts and watchers
- Switch list by source
- Saved searches with new-count badges
- Filter chips
- Save current search
- Most-discussed contract list with comment and teaming counts
- See all link
- My pipeline shortcut with count
- Urgency colours by days left
- Runtime theming

### v2 changes vs comp
- R9 '[count]' placeholders on Grants/Subcontracts/Forecasts replaced with real counts or hidden
- R4 single-segment breadcrumb dropped; page starts at H1
- R5 list rows coloured by days left
- R6 label first then count with unit word ('open', 'upcoming')
- R2 My pipeline is the one primary button
- R7 two-state chips
- Remove 'Sample members' chip for production

### Acceptance
1. Given page loads When viewing top Then there is no breadcrumb and the first element in main is H1 'Find work'
2. Given header When rendered Then My pipeline is the only amber button and shows the tracked count
3. Given My pipeline When clicked Then pipeline page opens
4. Given 4 ChoiceCards When rendered Then order is Contracts, Grants, Subcontracts, Forecasts left to right
5. Given each card When rendered Then label (type-lg) appears before count (type-md muted) with unit word
6. Given Contracts card When selected Then it is navy with white ink and others are white
7. Given a card count not yet known When rendered Then no '[count]' text and no placeholder number appears
8. Given card click When clicked Then list below switches to that source
9. Given AvatarStack When rendered Then up to 3 avatars with aria-label naming members
10. Given saved search chip When clicked Then filters apply and list re-queries
11. Given saved search with new items When rendered Then '3 new' badge shows and clears after opening
12. Given filter chip When toggled Then selected uses --fm-chip-selected-bg and unselected is outline, never navy
13. Given Save search When clicked with active filters Then a named saved chip is added
14. Given list header When rendered Then it shows 'Contracts', '8 of N' and 'See all N' where N is live total
15. Given sort When changed Then rows reorder
16. Given 8 rows When rendered Then each shows title, agency, type, NAICS, due date, comments and teaming counts
17. Given row due in <=48h When rendered Then chip uses --fm-urgent; <=7d --fm-warn; later neutral
18. Given row title long When rendered Then it clamps to 2 lines without wrapping the due date
19. Given sole-source notice When rendered Then Sole source tag shows
20. Given row click When clicked Then contract detail opens
21. Given no results When filtered Then EmptyState with Clear filters shows
22. Given any meta text When measured Then >= 13px at >= 4.5:1

### Responsive
<=1024: source cards 2x2, saved searches scroll horizontally in a single row, list full width. <=480: source cards become a horizontal segmented picker or stacked compact cards (icon, label, count), BottomTabBar, row meta wraps below title, counts remain visible.

### Accessibility
- Landmarks: header, nav Main, main
- Source cards are a radiogroup (aria-checked) or links with aria-current
- Saved search chips are buttons with aria-pressed
- List is ul with each row a single link
- Urgency not by colour alone
- Focus ring --fm-focus-ring, tap >= 44px

### Open questions
- Which pill is active on this hub, Contracts or a separate Find work item?
- Are Subcontracts and Forecasts data sources live in v1 or Coming soon?
- What counts as 'members watching' (tracking, following, or viewing)?
- Does Save search sync across devices and notify on new?

## Page: Grant detail (`grant-detail`)
- Route: /os#/grants?opportunity=<opportunityNumber> · Nav: Grants · Breadcrumb: Find work ▸ Grants ▸ <opportunity number>
- Purpose: Decide whether to track, apply for, partner on, or review a Grants.gov opportunity: see the close date and ceiling, read the AI summary, mark eligibility you meet, join as Applicant/Partner/Reviewer, and discuss.
- Primary action: Track — top-right of detail header
- Screen: https://fedm8.com/design/003/screens/03-grants.png · Comp: https://fedm8.com/design/003/comps/03-grants.dc.txt

### Eye path
1 Title + CountdownBanner ('Closes tomorrow, Nov 3', urgent colour) with Track top-right
2 SummaryCard (BLUF) plus one FactLine (closes, ceiling, awards, agency, opportunity #, posted)
3 Can you do this? (eligibility toggles + coverage)
4 Join as Applicant / Partner / Reviewer then ClaimBar
5 Discussion with FedM8 AI bot post
Left list of grants (selected row cream) and header tracking bar are supporting

### Modules
- **0. App header and primary nav** (`app-header`, AppHeader + PillNav, header)
  - Logo, 8 NavPills with Grants active (navy)
  - WorkspaceSwitcher 'FEDM8 LLC'
  - Avatar 'JN'
  - ↳ pill navigates
  - ↳ switcher and avatar menus
  - states: empty: n/a; loading: immediate; error: n/a; selected: Grants navy with aria-current
  - data: nav, workspace, user = real (app config, auth profile)
- **1. Breadcrumb** (`breadcrumb`, ChevronBreadcrumb, header)
  - Find work > Grants > NNH26ZTR001N-26STRI-B3 (depth 3)
  - Last segment current
  - ↳ Find work to hub, Grants to grants list
  - states: empty: n/a; loading: last segment skeleton; error: 'Grant' fallback; selected: last segment --fm-accent-soft
  - data: opportunity number = real (Grants.gov opportunityNumber)
- **2. Search and filters** (`search-filters`, SearchField + FilterChip, left)
  - SearchField 'Search grants'
  - Chips: Closes < 7 days, Agency: Any
  - Sort 'Most discussed'
  - ↳ typing filters list
  - ↳ chip toggles two states (R7)
  - ↳ sort menu: Most discussed, Closing soonest, Largest ceiling
  - states: empty: no filters = all; loading: skeleton rows; error: ErrorState Retry; selected: chip-selected-bg vs outline
  - data: filters = real (Grants.gov opportunity fields (grants table TBD))
- **3. Grant list** (`grant-list`, NoticeRow x6, left) · social
  - Row shows only: title, agency (opportunity number moved off the row, no 2-line wrap), ceiling, close date, urgency
  - NASA Space Technology Research Institutes, Nov 3, $13M, comments 6, partners 11
  - Bureau of Justice Assistance BJA FY26 DNA Capacity Enhancement for Backlog Reduction (CEBR), Nov 3, $4.9M, 2/1
  - BJA FY2026 Byrne JAG Local Formula, Nov 3, $4.96M, 1/0
  - BJA FY2026 John R. Justice Formula, Nov 3, $120K, 0/0
  - NSF State and Regional AI Infrastructure Hubs (26-513), Nov 4, 4/5
  - NSF Unlocking Dataset Value for AI-Enabled Scientific Discovery (26-512), Nov 4, 3/2
  - Selected row cream + bold title (R1)
  - Footer '50 awards' text in comp is unlabeled; treat as list count note (see open questions)
  - ↳ row click opens detail
  - ↳ arrow-key selection
  - ↳ urgency colour R5
  - states: empty: EmptyState 'No grants match' + Clear filters; loading: 6 skeleton rows; error: ErrorState Retry; selected: --fm-row-selected-bg, bold
  - data: title, agency, opportunity #, ceiling, close date = real (Grants.gov)
  - data: comment and partner counts = sample (TBD)
- **4. Detail header** (`detail-header`, PageTitle + TypeBadge + Track PrimaryButton, main)
  - Source badge 'Grants.gov'
  - Title 'Space Technology Research Institutes'
  - Agency 'NASA'
  - Track amber top-right
  - More actions IconButton
  - ↳ Track adds to My pipeline and flips to Tracking
  - ↳ More actions: copy link, share, mute
  - states: empty: n/a; loading: skeleton; error: ErrorState with Grants.gov link; selected: Tracking outline with check
  - data: title, agency, source = real (Grants.gov)
  - data: tracked state = sample (TBD pipeline)
- **5. Countdown** (`countdown`, CountdownBanner + UrgencyChip, main)
  - Large 'Closes tomorrow, Nov 3' in --fm-urgent (replaces small chip 'Closes tomorrow . Nov 3')
  - ↳ live update
  - ↳ add to calendar text button
  - states: empty: no close date: 'Open until funded' neutral; loading: skeleton; error: plain date; selected: closed shows 'Closed'
  - data: close date = real (Grants.gov closeDate)
- **6. Tracking bar** (`tracking-bar`, TrackingBar + AvatarStack + FollowButton, main) · social
  - AvatarStack DR MT PS LG
  - '9 tracking', '3 online now' with PresenceDot
  - Follow NASA button (outline)
  - ↳ avatar stack opens roster
  - ↳ Follow toggles Following
  - states: empty: 'Be the first to track'; loading: skeleton; error: hide counts; selected: Following outline with check
  - data: tracking and online counts, avatars = sample (TBD)
- **7. AI summary (BLUF)** (`summary`, SummaryCard, main) · Pro
  - BLUF of the notice
  - Pending/ready/failed
  - Comp state: 'Requirement summary is still being written' with Notify me when ready
  - Pro tag
  - ↳ Notify me subscribes
  - ↳ Retry on failed
  - states: empty: 'No notice text to summarise'; loading: pending + skeleton lines; error: failed + Retry; selected: ready shows BLUF
  - data: AI summary = placeholder (TBD edge fn, never invented)
- **8. Fact line** (`fact-line`, FactLine, main)
  - Closes Nov 3, 2026 . Award ceiling $13,000,000 . Awards 1 . Agency NASA . Opportunity NNH26ZTR001N-26STRI-B3 . Posted <date>
  - Comp 'Posted [loading]' must be real date or omitted
  - ↳ opportunity number copy on click
  - states: empty: missing fields omitted; loading: one skeleton line; error: n/a; selected: n/a
  - data: close date, ceiling, number of awards, agency, opportunity # = real (Grants.gov)
  - data: posted date = placeholder (Grants.gov postedDate; not in comp)
- **9. Can you do this?** (`requirements`, RequirementToggle list + CoverageMeter, main) · social
  - Meter 'You cover 2 of 4'
  - Toggles: Active SAM.gov registration, Grants.gov account, Eligible applicant type, Submit by Nov 3 (each 'I can do this')
  - Note 'Full eligibility after the notice is read' with loading state
  - ↳ toggle updates meter and ClaimBar, persists
  - ↳ aria-pressed
  - states: empty: 'Eligibility appears after the notice is read'; loading: skeleton toggles (no '[loading]' text); error: Retry; selected: ON green soft with check
  - data: requirement list = placeholder (generic 4 checks in comp; notice-derived eligibility TBD)
  - data: my toggles = sample (TBD user claims)
- **10. Join as** (`join-as`, JoinAsPicker (3 ChoiceCards), main) · social
  - Applicant 'Lead the application' (2)
  - Partner 'Join an applicant' (6)
  - Reviewer 'Check drafts' (3)
  - Label first, count second with unit word (R6)
  - AvatarStack per role
  - Names: Luis G. Navy vet . DevSecOps; Priya S. Section 508 / UX; Marcus T. USMC . Web dev
  - Applicant selected (navy)
  - ↳ click selects role, updates ClaimBar
  - ↳ second click opens roster lane
  - states: empty: 0 'Be first'; loading: 3 skeleton cards; error: Retry; selected: navy (R1)
  - data: role counts, avatars = sample (TBD roster)
- **11. Claim bar** (`claim-bar`, ClaimBar + ProLock, main) · Pro · social
  - 'Your role: Applicant'
  - Contact button with Pro tag
  - ↳ Contact opens composer if Pro, else upgrade drawer
  - ↳ role label updates live
  - states: empty: 'Pick a role'; loading: skeleton; error: Retry; selected: shows chosen role
  - data: my role = sample (TBD)
  - data: Pro entitlement = real (plan/subscription)
- **12. Discussion** (`discussion`, ThreadList + Composer + SortTabs + ThreadPost + VoteRail, main) · social
  - 'Discussion' and '6 comments'
  - Hot/New/Top
  - Composer JN 'Add to the discussion' + Post
  - Dana R. SDVOSB Army vet . Cloud ops . 3h, vote 14: 'One award only. Anyone partnering with a university lab...'
  - Nested Luis G. Applicant 2h up 7; Priya S. Partner 1h up 4
  - Marcus T. 1099 USMC . Web dev . 4h, vote 9: 'Closes tomorrow. Grants.gov registration can take days...'
  - Reply/Share/Save, up/down vote
  - ↳ sort
  - ↳ vote once
  - ↳ reply inline
  - ↳ share
  - ↳ save
  - ↳ post
  - states: empty: 'Start the conversation'; loading: skeleton posts; error: Retry, keep draft; selected: own posts Edit/Delete
  - data: posts, votes, authors, roles, times = sample (TBD discussion tables)
- **13. FedM8 AI bot post** (`ai-bot-post`, BotPost, main) · social
  - FedM8 AI, Bot tag, 1h
  - 'Requirement summary is still being written.' Notify me when ready
  - ↳ Notify me subscribes
  - ↳ replaced when summary ready
  - states: empty: hidden; loading: n/a; error: hidden; selected: n/a
  - data: summary job status = placeholder (TBD)
- **14. Source links and lookups** (`source-links`, TextButton + FactLine, right) · Pro
  - Open on Grants.gov external link
  - Ask FedM8 button (Pro)
  - Fit line 'set up profile'
  - Past awards '[loading]'
  - ↳ Grants.gov opens new tab
  - ↳ Ask FedM8 opens AI drawer
  - ↳ Fit prompts profile setup
  - ↳ Past awards loads real or hides
  - states: empty: rows hidden; loading: skeleton for past awards; error: hide with Retry; selected: n/a
  - data: Grants.gov link = real (opportunity URL)
  - data: fit, past awards = placeholder (TBD (USAspending for past awards))

### Features
- Select any grant from list and view detail
- Search grants and filter by close window and agency
- Live countdown with urgency colour
- Track into pipeline
- Follow agency
- Tracking and online counts
- AI BLUF summary states
- Eligibility toggles with coverage meter
- Join as Applicant, Partner or Reviewer
- Contact roster (Pro)
- Reddit-style discussion with votes, nesting, sort
- FedM8 AI bot post
- Open on Grants.gov, Ask FedM8, fit and past awards
- Award ceiling and awards count in fact line
- Chevron breadcrumb
- Runtime theming

### v2 changes vs comp
- R3 same reorder as contract detail: title+countdown, Summary, Can you do this?, Join as, Discussion
- R3 facts collapse to one FactLine
- List rows show only title, agency, ceiling, close date (opportunity number moved off row to stop 2-line wrap)
- R1 selected list row cream not navy
- R2 only Track is amber
- R5 urgency colours on countdown and rows (all Nov 3 rows urgent)
- R6 Join-as label first, count second
- R9 '[loading]' placeholders become skeletons or hidden
- Remove 'Sample members' chip for production

### Acceptance
1. Given page loads When viewing header Then breadcrumb shows Find work > Grants > opportunity number with last segment not a link
2. Given nav When rendered Then Grants pill is navy with aria-current=page
3. Given search When typing 'NSF' Then list filters to NSF rows
4. Given chips When toggled Then selected uses --fm-chip-selected-bg and unselected outline, never navy
5. Given list rows When rendered Then each shows only title, agency, ceiling, close date plus counts and does not wrap opportunity number
6. Given a row selected When rendered Then row is cream with bold title, no navy
7. Given row closes within 48h When rendered Then chip uses --fm-urgent
8. Given detail When opened Then order is header+countdown, Summary, Can you do this, Join as, Discussion
9. Given page When counting amber buttons Then exactly one (Track)
10. Given Track When clicked Then it becomes Tracking and grant appears in My pipeline
11. Given close date tomorrow When rendered Then banner reads 'Closes tomorrow' with date in urgent colour
12. Given Follow NASA When clicked Then label becomes Following
13. Given summary pending When rendered Then pending copy and Notify me show, no invented text
14. Given FactLine When rendered Then one line holds closes, ceiling $13,000,000, awards 1, agency, opportunity # and posted date or omits posted
15. Given Posted unknown When rendered Then no '[loading]' text appears
16. Given 2 of 4 toggles ON When rendered Then meter reads 'You cover 2 of 4' and persists after reload
17. Given Join as When rendered Then cards are Applicant, Partner, Reviewer with label first then 'N members'
18. Given role card click When clicked Then only that card is navy and ClaimBar shows 'Your role: <role>'
19. Given non-Pro When Contact clicked Then upgrade drawer opens
20. Given Discussion When sort tabs clicked Then order changes
21. Given vote When clicked twice Then count restores
22. Given Post with empty text Then button disabled
23. Given Open on Grants.gov When clicked Then real opportunity opens in new tab
24. Given Fit with no profile Then no number shown
25. Given any meta text When measured Then >= 13px at >= 4.5:1
26. Given production build When searched Then no 'Sample members' chip

### Responsive
<=1024: list becomes top drawer or 'Grants' toggle, detail full width, source links under Discussion. <=480: BottomTabBar, 3 segments Overview / Talk / Team, full-width amber Track, Join-as cards stacked, FAB with bottom padding.

### Accessibility
- Landmarks: header, nav Main, main with regions 'Grant list', 'Grant detail', 'Requirements', 'Join as', 'Discussion'
- Breadcrumb nav aria-current on last
- Vote buttons labelled
- Urgency also text
- Focus order follows eye path; focus ring --fm-focus-ring; tap >= 44px

### Open questions
- What does the '50 awards' footer in the list refer to (total awards, result count)?
- Where does generic eligibility come from before the notice is parsed?
- Should the Reviewer role be restricted to verified members?
- Do closed grants stay discoverable and how are they labelled?

## Page: My pipeline (`pipeline`)
- Route: /os#/pipeline · Nav: Contracts · Breadcrumb: Find work ▸ My pipeline
- Purpose: See every tracked notice by bid stage, spot what is due soonest, and open the team room for the one bid that needs work now.
- Primary action: Open team room — top-right of the bid drawer header row (drawer open); when no card is selected, top-right of the page title row shows 'Find work' as PrimaryButton instead. Never both at once.
- Screen: https://fedm8.com/design/003/screens/04-pipeline.png · Comp: https://fedm8.com/design/003/comps/04-pipeline.dc.txt

### Eye path
1 H1 'My pipeline' + '5 tracked' count (no breadcrumb duplication of the nav pill; breadcrumb stays only because depth is 2: Find work > My pipeline per R4)
2 Stage columns left to right: Watching, Teaming, Writing, Submitted, Won / Lost; column header is icon + label first, count second (R6 applied to stage headers; count is 18px muted, not 32px)
3 Most urgent card is visually first: urgency chip (R5) in red when <=48h, amber <=7d, neutral later
4 Selected card = cream tint + bold title (R1), never navy
5 Drawer overlays the board from the right and shows: title + countdown, Team, Before you submit
6 One amber action: Open team room (top-right of drawer focus area). Find work becomes outline while the drawer is open
7 Secondary: comment-thread link as text button

### Modules
- **1. App header** (`app-header`, AppHeader, header)
  - FedM8 logo link
  - PillNav with 8 NavPills: Home, Contracts, Grants, Teaming, Certifications, Veterans, Learn, Community (Veterans, Learn, Community always visible per owner decision)
  - Contracts pill is active (aria-current=page, --fm-nav-active-bg)
  - Notifications IconButton
  - Messages IconButton with unread dot (red)
  - WorkspaceSwitcher label 'FEDM8 LLC'
  - Avatar initials JN
  - ↳ click NavPill navigates to that section
  - ↳ click Notifications opens notifications panel
  - ↳ click Messages goes to /os#/messages
  - ↳ click WorkspaceSwitcher opens workspace list
  - ↳ click Avatar opens account menu incl. ThemeSwitcher link
  - states: empty: Messages dot hidden when 0 unread; loading: Avatar shows skeleton circle until profile loads; error: Header still renders; workspace label falls back to 'Workspace'; selected: Contracts NavPill filled navy (the only navy 'where you are' element in the chrome)
  - data: nav items = real (static app config)
  - data: unread messages flag = sample (TBD messages unread count)
  - data: workspace name FEDM8 LLC = real (workspace/org record)
  - data: user initials JN = real (auth profile)
- **2. Breadcrumb** (`breadcrumb`, ChevronBreadcrumb, header)
  - Segment 'Find work' (link, neutral fill)
  - Segment 'My pipeline' (current, aria-current=page)
  - ↳ click 'Find work' goes to the Find work hub
  - states: empty: n/a; loading: n/a; error: n/a; selected: current segment is the last one; per R1 the current segment uses a non-navy emphasis (bold + --fm-accent-soft) so navy stays reserved for nav pill and selected choice cards
  - data: path segments = real (router)
- **3. Page title row** (`page-title`, PageTitle, header)
  - H1 'My pipeline'
  - SampleChip 'Sample members' (comp-only, must not ship)
  - '5 tracked' count text
  - Find work PrimaryButton with search icon (comp) -> becomes SecondaryButton when drawer is open or a card is selected
  - ↳ click Find work goes to Contracts hub
  - states: empty: 0 tracked: count hidden, Find work stays primary (amber) as the single next step; loading: count shows skeleton; error: count hidden; selected: n/a
  - data: tracked count (sum of cards across stages) = real (user tracked notices table, TBD name)
- **4. Stage column: Watching** (`stage-watching`, KanbanColumn, main) · social
  - Icon eye in --fm-info-soft circle
  - Label 'Watching'
  - Count (comp 2)
  - KanbanCard: GAMS Licenses and Maintenance / EPA · Special Notice / UrgencyChip 'Nov 4 · 2 days' / AvatarStack (DR) / comment count 3
  - KanbanCard: Steris Software Connectivity / VA · Presolicitation / UrgencyChip 'Nov 9 · 7 days' / AvatarStack (MT, +1) / comment count 5
  - ↳ click card selects it and opens the drawer
  - ↳ drag card to another stage changes its stage
  - ↳ keyboard: card focusable, Enter opens drawer, 'Move to' menu on card as drag alternative
  - ↳ Card titles clamp to 2 lines with full title in tooltip
  - states: empty: dashed EmptyState 'Nothing yet'; loading: 3 LoadingSkeleton cards; error: ErrorState inline with Retry; selected: selected card cream tint + bold title
  - data: notice title, agency, notice type, due date = real (SAM.gov notices ingested for tracked items)
  - data: team avatars + overflow count = sample (TBD team roster by notice)
  - data: comment counts (3, 5) = sample (TBD discussion thread count by notice)
  - data: stage membership = sample (TBD user pipeline stage column)
- **5. Stage column: Teaming** (`stage-teaming`, KanbanColumn, main) · social
  - Icon two-circles in --fm-info-soft circle
  - Label 'Teaming'
  - Count (comp 2)
  - KanbanCard: JFrog Artifactory Subscription Maintenance / FDIC · Solicitation / chip 'Nov 5 · 3 days' / AvatarStack (LG, +3) / comments 9
  - KanbanCard: FOMS Software Development / FAA · Sources Sought / chip 'Nov 6 · 4 days' / AvatarStack (KA, PS, +4) / comments 11
  - ↳ same as Watching: select, drag, Move-to menu
  - states: empty: dashed 'Nothing yet'; loading: skeleton cards; error: inline ErrorState; selected: cream tint + bold title
  - data: notice facts and due dates = real (SAM.gov notices)
  - data: team avatars, comment counts = sample (TBD)
- **6. Stage column: Writing** (`stage-writing`, KanbanColumn, main) · social
  - Icon pencil in --fm-action circle
  - Label 'Writing'
  - Count (comp 1)
  - KanbanCard (selected in comp): Web Sustainment/Content Management/Cloud Hosting / DHRA · Sources Sought / UrgencyChip 'Tomorrow · 12:00 PM' in red text chip (<=48h) / AvatarStack (LG, PS, +11) / comments 24
  - ↳ select/drag/move as other columns
  - ↳ Column highlight (comp used navy border + hard shadow) is removed; selection shows on the card only (R1)
  - states: empty: dashed 'Nothing yet'; loading: skeleton card; error: inline ErrorState; selected: card cream tint + bold title; column keeps neutral border
  - data: DHRA notice H9821027SS0001, due Nov 3 12:00 PM ET = real (SAM.gov notice (matches Main contract detail))
  - data: team avatars (+11), comment count 24 = sample (TBD)
- **7. Stage column: Submitted** (`stage-submitted`, KanbanColumn, main)
  - Icon paper-plane in --fm-info-soft circle
  - Label 'Submitted'
  - Count 0
  - Dashed EmptyState 'Nothing yet'
  - ↳ drop target for cards moved from Writing; dropping asks 'Mark as submitted?' confirm
  - states: empty: this is the empty state shown in comp; loading: skeleton; error: inline ErrorState; selected: n/a
  - data: submitted cards = sample (TBD user pipeline stage column)
- **8. Stage column: Won / Lost** (`stage-won-lost`, KanbanColumn, main)
  - Icon trophy in --fm-accent-soft circle
  - Label 'Won / Lost' (must not wrap; use 'Results' short label at narrow widths)
  - Count 0
  - Dashed EmptyState 'Won 0 · Lost 0'
  - ↳ dropping a card asks Won or Lost via SegmentedControl in a small popover
  - states: empty: 'Won 0 · Lost 0'; loading: skeleton; error: inline ErrorState; selected: n/a
  - data: won / lost counts = real (derived from stage membership; zero is a true value for this user, shown as text not as a headline number)
- **9. Bid drawer header** (`drawer-header`, Drawer, drawer)
  - Stage chip 'Writing' (FilterChip style, unselected outline)
  - UrgencyChip 'Due tomorrow · 12:00 PM ET' (red, <=48h)
  - H2 notice title
  - Agency + solicitation number in mono 'DHRA · H9821027SS0001'
  - Close IconButton (X)
  - Overlay: drawer sits over the board, ~420px wide, does not shrink columns
  - ↳ Esc or X closes and returns focus to the originating card
  - ↳ click title opens Contract detail
  - ↳ click outside does not close (board stays usable)
  - states: empty: drawer closed when nothing selected; loading: skeleton header lines; error: ErrorState 'Could not load this bid' with Retry; selected: n/a
  - data: title, agency, solicitation number, due datetime = real (SAM.gov notice)
  - data: stage = sample (TBD pipeline stage)
- **10. Team (drawer)** (`drawer-team`, RosterList, drawer) · Pro · social
  - H3 'Team' + count 13
  - 3 ChoiceCards: Prime 2, Sub 7, 1099 4 (label first, count second with unit, R6; Prime is the user's role = selected navy only if it is a choice; here it is read-only so use neutral cards with the user's role marked by a 'You' badge)
  - 5 roster rows: Avatar, name, descriptor, RoleBadge (Luis G. Navy vet DevSecOps Prime; Kim A. Air Force PM VOSB Prime; Priya S. Section 508/UX Sub; Dana R. Army vet Cloud ops Sub; Marcus T. USMC Web dev 1099)
  - '+8 more on the roster' TextButton
  - ↳ click role card filters roster to that role
  - ↳ click member row opens Member profile
  - ↳ contact actions on a member are ProLock-gated
  - ↳ click '+8 more' expands the roster or opens the team room roster
  - states: empty: 'No one has joined yet' EmptyState with Invite TextButton; loading: skeleton rows; error: ErrorState inline; selected: role card selected state when filtered
  - data: role counts 2/7/4 and total 13 = sample (TBD team membership by notice)
  - data: member names, descriptors, badges = sample (TBD member profiles)
- **11. Before you submit** (`drawer-checklist`, Checklist, drawer)
  - H3 'Before you submit'
  - Progress text '2 of 4' + progress bar (amber fill 50%)
  - 4 checkbox rows: Capability statement (checked), SAM active (checked), Past performance (unchecked), Pricing (unchecked)
  - Checked rows use --fm-success-soft, not navy border
  - ↳ toggle a checkbox updates progress text and bar
  - ↳ SAM active auto-checks from the workspace SAM registration status and cannot be unchecked by hand
  - ↳ keyboard: Space toggles
  - states: empty: n/a (4 fixed items); loading: skeleton rows; error: toggle failure reverts with inline message; selected: checked = success-soft fill + check icon
  - data: checklist item states = sample (TBD per-user per-notice checklist)
  - data: SAM active = placeholder (TBD link to workspace SAM registration; must reflect real status, not a stored tick)
- **12. Drawer actions** (`drawer-actions`, PrimaryButton, drawer) · social
  - PrimaryButton 'Open team room' with link icon (the single amber button on the page when drawer is open)
  - TextButton '24 comments in thread' with comment icon
  - ↳ Open team room goes to /os#/messages with the notice room selected
  - ↳ comments link opens the notice Discussion on Contract detail
  - states: empty: if no team room exists, label becomes 'Start team room'; loading: button disabled with spinner while room is resolved; error: toast 'Could not open room'; selected: n/a
  - data: comment count 24 = sample (TBD thread count)

### Features
- Five-stage board: Watching, Teaming, Writing, Submitted, Won / Lost
- Tracked-count in page title
- Notice cards with title (2-line clamp), agency, notice type, urgency chip, team avatar stack, comment count
- Graded urgency colour on cards and drawer (R5)
- Select card to open overlay drawer; Esc closes
- Drag card between stages plus keyboard Move-to menu
- Empty-stage dashed placeholders
- Drawer: stage chip, countdown, title, agency, solicitation number
- Drawer team summary by role (Prime/Sub/1099) with roster rows and +N more
- Pro-gated member contact
- Before-you-submit checklist with progress
- Open team room primary action
- Comment thread link
- Find work shortcut
- Header: nav, notifications, messages unread dot, workspace switcher, avatar

### v2 changes vs comp
- R1: selected card is cream tint (--fm-row-selected-bg) with bold title; remove navy card fill, navy column border and hard shadow on the Writing column; Prime role card in drawer is not navy-filled
- R2: only 'Open team room' is amber when the drawer is open; 'Find work' drops to outline in that state
- R4: breadcrumb kept (depth 2) but current segment not navy
- R5: Nov 4 (2 days) shows red or amber by computed hours; Nov 5/6/9 show amber (<=7 days); the comp's blue chips are removed; 'Tomorrow' red text chip
- R6: stage header label first, count second at 18px muted (comp 32px count is removed); drawer role cards label first (22px) count second with unit word ('7 subs')
- R7: stage chip in drawer is outline; no navy chips
- R8: card meta (agency, type, comment count) 13px minimum (comp 12px); avatar overflow initials stay decorative
- R9: Submitted 0 and Won/Lost 0 columns show text 'Nothing yet' / 'Won 0 · Lost 0', no big zero numerals
- Page note: wider stage columns, titles clamp to 2 lines (comp wrapped to 4), headers 'Submitted0' and 'Won / Lost' must not collide or wrap
- Page note: drawer overlays the board instead of consuming 35% width
- Remove SampleChip before ship; show sample data only through an in-app sample flag
- Drawer (comp) shows a 'Prime' role card in navy; replace with neutral cards since role is read-only here

### Acceptance
1. Given the page loads with 5 tracked notices When rendered Then the H1 reads 'My pipeline' and the tracked count reads '5 tracked'
2. Given any viewport >=1024 When the board renders Then five columns Watching, Teaming, Writing, Submitted, Won / Lost appear in that order and no header text overlaps its count
3. Given a column header When measured Then the label is rendered before the count and the count font size is <= 18px (R6)
4. Given a card title longer than two lines When rendered Then it clamps to 2 lines with an ellipsis and the full title is available as a tooltip
5. Given a notice due in <=48h When its card renders Then the UrgencyChip uses --fm-urgent text on --fm-urgent-soft
6. Given a notice due in 3 to 7 days When its card renders Then the chip uses --fm-warn; given later than 7 days Then it is neutral
7. Given a Watching column When rendered Then it lists GAMS Licenses and Maintenance (EPA) and Steris Software Connectivity (VA) with agency and notice type under each title
8. Given the Teaming column When rendered Then it lists JFrog Artifactory Subscription Maintenance (FDIC) and FOMS Software Development (FAA)
9. Given the Writing column When rendered Then it lists the DHRA Web Sustainment card with a red 'Tomorrow · 12:00 PM' chip
10. Given an empty Submitted or Won / Lost column When rendered Then a dashed placeholder shows 'Nothing yet' or 'Won 0 · Lost 0' and no large numeral
11. Given a card When clicked Then the drawer opens over the board, the board columns do not change width, and the card shows cream tint with bold title (not navy)
12. Given the drawer is open When Esc is pressed Then the drawer closes and focus returns to the card that opened it
13. Given the drawer is open When the page is inspected Then exactly one amber PrimaryButton exists ('Open team room') and 'Find work' is outline
14. Given the drawer team module When rendered Then it shows role cards Prime, Sub, 1099 with label first and count with a unit word second, and five roster rows with RoleBadges
15. Given '+8 more on the roster' When clicked Then the full roster is revealed
16. Given a free-plan user When they activate a contact action on a roster member Then ProLock appears instead of opening contact
17. Given the Before you submit module When a checkbox is toggled Then the 'N of 4' text and bar fill update immediately
18. Given SAM registration is not active When the module renders Then 'SAM active' is unchecked and not user-toggleable
19. Given a card is dragged to Submitted When dropped Then the card appears there, both column counts update, and the change persists after reload
20. Given keyboard-only use When on a card Then 'Move to' menu allows changing stage without drag
21. Given 'Open team room' When clicked Then the user lands in Messages with the notice's team room selected
22. Given the header When rendered Then Contracts is the only active NavPill and Veterans, Learn, Community are visible without an 'All tools' menu
23. Given unread messages exist When header renders Then the Messages icon shows a red dot and aria-label includes 'unread'
24. Given any meta text on the page When contrast and size are measured Then it is >=13px and >=4.5:1 (R8)
25. Given production build When searched Then no SampleChip 'Sample members' is rendered

### Responsive
<=1024: columns become horizontally scrollable lanes with scroll-snap (min column width 240px) or a stage SegmentedControl showing one column at a time; drawer becomes a right sheet 100% wide up to 480px. <=480: single-stage view with Tabs for the five stages (counts in tab label), drawer becomes a full-screen bottom sheet, 'Open team room' becomes sticky bottom button, BottomTabBar replaces PillNav, no horizontal page scroll.

### Accessibility
- Landmarks: header (banner), nav aria-label Main, main with section aria-label Stages, aside/dialog for drawer
- Each column is a list with aria-label '<Stage>, N items'
- Cards are links/buttons with accessible name 'title, agency, due date, N comments'
- Drawer is role=dialog non-modal, aria-labelledby the H2; focus moves to the drawer heading on open and returns on close
- Drag has a keyboard equivalent and a live-region announcement on move
- Checklist uses native checkboxes with visible labels; progress bar has role=progressbar with aria-valuenow
- Colour is never the only urgency signal: chip text contains the time ('Tomorrow', '2 days')
- Focus ring uses --fm-focus-ring on all interactive items

### Open questions
- Are stage changes manual only, or do some auto-advance (e.g. Writing to Submitted when a SAM award notice appears)?
- Does 'Teaming' stage mean the user has a team, or is looking for one? Define entry criteria
- Can a user track a notice without any team room existing yet (what does Open team room do)?
- Where does a Grant appear: same board or a separate pipeline? Comp only shows contracts
- Is the Before you submit checklist per notice or per workspace for items like Capability statement and SAM active?
- Won/Lost: how is outcome recorded (manual vs award data)?

## Page: Teaming network (`teaming`)
- Route: /os#/teaming · Nav: Teaming · Breadcrumb: —
- Purpose: Find the right Prime, Sub, 1099 talent or Mentor, connect with them, and invite the best matches onto the bid that is due soonest.
- Primary action: Invite (top match) — top-right of the Matches rail header; the member-card Connect buttons are outline, not primary
- Screen: https://fedm8.com/design/003/screens/05-teaming.png · Comp: https://fedm8.com/design/003/comps/05-teaming.dc.txt

### Eye path
1 H1 'Teaming' (R4: the comp's People > Teaming breadcrumb is removed, depth 1)
2 'I'm looking for' ChoiceCards: label first (Prime / Sub / 1099 talent / Mentor), count second with unit word (R6); selected card navy (R1 allowed here: it is a selected choice card)
3 Matches rail for the due-soonest bid: heading + countdown, requirement chips, ranked members with coverage meter; one amber Invite on the top match (R2)
4 Filter row: search + FilterChips (two states only, R7) + sort
5 Member grid: name, role/cert badges, skills, mutual connections; 'Recommended N×' only on cards where it stands out
6 Card actions: Connect (outline), Message (ProLock for free plan)

### Modules
- **1. App header** (`app-header`, AppHeader, header)
  - FedM8 logo
  - PillNav: Home, Contracts, Grants, Teaming (active), Certifications, Veterans, Learn, Community
  - Notifications IconButton
  - Messages IconButton with unread dot
  - WorkspaceSwitcher 'FEDM8 LLC'
  - Avatar JN
  - ↳ nav click navigates
  - ↳ Avatar opens account menu with ThemeSwitcher
  - states: empty: unread dot hidden at 0; loading: avatar skeleton; error: header still renders; selected: Teaming pill navy
  - data: nav items, workspace, user = real (app config, auth profile, workspace record)
  - data: unread flag = sample (TBD messages unread)
- **2. Page title** (`page-title`, PageTitle, header)
  - H1 'Teaming' (new in v2; comp had breadcrumb 'People > Teaming' instead)
  - SampleChip 'Sample members' (comp-only, must not ship)
  - ↳ none
  - states: empty: n/a; loading: n/a; error: n/a; selected: n/a
  - data: title = real (static)
- **3. I'm looking for** (`looking-for`, ChoiceCard, main) · social
  - Section H2 "I'm looking for"
  - 4 ChoiceCards in a group: Prime (icon flag, count 12), Sub (icon two circles, 48), 1099 talent (icon person, 31), Mentor (icon star in --fm-pro-soft, 9)
  - Each: icon circle, label (type-xl) first, count with unit word second ('12 primes'), AvatarStack of 2 plus overflow (+10, +46, +30, +8)
  - Prime selected by default in comp (navy fill, amber icon circle)
  - ↳ click selects one card (single select) and filters the member grid and re-ranks matches
  - ↳ click selected card again does not deselect (always one role)
  - ↳ Mentor choice swaps card badges to mentor fields (area, availability)
  - ↳ keyboard: arrow keys move within the radio-style group
  - states: empty: count 0: card still selectable, shows 'None yet' and the grid shows EmptyState with 'Invite someone'; loading: counts hidden (R9) and cards skeletoned; error: cards render without counts; selected: navy fill + on-dark text (R1 allowed: selected choice card), aria-pressed or radio checked
  - data: role counts 12 / 48 / 31 / 9 = sample (TBD members by role in network directory)
  - data: avatars in stacks = sample (TBD member profiles)
- **4. Search and filters** (`filters`, FilterChip, main)
  - SearchField 'Search name, skill, NAICS'
  - FilterChip 'NAICS 541511 ×' (selected, removable)
  - FilterChip 'Clearance: Any' (unselected, was dashed in comp)
  - FilterChip 'Veteran: SDVOSB, VOSB' (selected)
  - State group MD / VA / DC (3 toggle chips, all selected in comp; selected = --fm-chip-selected-bg, never navy)
  - Sort dropdown 'Most recommended' (SegmentedControl/menu: Most recommended, Most mutual, Recently active, Name)
  - ↳ typing filters grid (debounced 250ms)
  - ↳ click chip toggles selected/unselected
  - ↳ × on a chip removes it
  - ↳ click unselected 'Clearance' opens option list
  - ↳ sort change re-orders grid
  - ↳ filters persist per user as a SavedSearchChip option (not shown in comp)
  - states: empty: no filters = all chips unselected; loading: grid skeleton while re-querying; error: inline 'Could not update results' with Retry; selected: --fm-chip-selected-bg fill, ink text
  - data: NAICS code options = real (NAICS reference list)
  - data: veteran status filter values = real (cert/veteran badge types SDVOSB, VOSB, 8(a), HUBZone, WOSB)
  - data: clearance values = placeholder (TBD member profile clearance field)
  - data: state values MD/VA/DC = real (member location field, default from workspace region)
- **5. Member grid** (`member-grid`, RosterList, main) · Pro · social
  - Responsive grid (min card 280px), 6 member cards in comp: Luis G. (Navy vet DevSecOps, SDVOSB, Prime), Priya S. (Section 508/UX, Woman-owned, Sub), Marcus T. (USMC Web dev, VOSB, 1099), Dana R. (Army vet Cloud ops, SDVOSB, Sub), Kim A. (Air Force Project manager, VOSB, Prime), Ramona B. (Army ret. Capture lead, SDVOSB, Mentor)
  - Each card: Avatar 56px with PresenceDot (green when online; absent for Marcus and Kim), name link (type-lg), descriptor line 13px, CertBadge + RoleBadge (Mentor badge in --fm-pro-soft), 3 skill chips, 'Recommended 3×' badge, mutual-connections AvatarStack + 'N mutual' (4, 6, 2, 5, 3, 7), Connect button, Message button with Pro tag + lock icon
  - ↳ click name opens Member profile
  - ↳ Connect sends a connection request (button becomes 'Requested' text state, then 'Connected')
  - ↳ Message: Pro users open DM, free users see ProLock with trial prompt
  - ↳ click mutual stack opens list of mutual connections
  - ↳ click skill chip adds it to the search filter
  - ↳ infinite scroll or Load more, 24 per page
  - states: empty: EmptyState 'No members match' with Clear filters TextButton; loading: 6 skeleton cards; error: ErrorState with Retry; selected: n/a (cards are not selectable)
  - data: member names, descriptors, skills = sample (TBD member directory; comp uses sample members)
  - data: CertBadge values (SDVOSB, VOSB) = sample (TBD self-declared vs SBA-verified; never show certified unless verified)
  - data: online presence = sample (TBD presence service)
  - data: mutual connection counts 4/6/2/5/3/7 = sample (TBD connection graph)
  - data: Recommended 3× count = sample (TBD recommendations table)
- **6. Recommended badge** (`recommended-badge`, ReactionChip, main) · social
  - 'Recommended N×' badge on member card, comp shows 3× on every card
  - ↳ click shows who recommended (AvatarStack popover)
  - states: empty: hidden when N < 2 or when value equals the grid median; loading: hidden; error: hidden; selected: n/a
  - data: recommendation count = sample (TBD member recommendations)
- **7. Matches rail header** (`matches-rail-header`, SummaryCard, right)
  - H2 'Matches for DHRA Web Sustainment'
  - Countdown line 'Due Nov 3, 12:00 PM ET · 541511' with red UrgencyChip (<=48h)
  - Requirement chips: Web sustainment, Content mgmt, Cloud hosting, 541511
  - ↳ click title opens Contract detail
  - ↳ bid selector (change which tracked bid drives matches) opens a list of tracked notices
  - states: empty: no tracked notices: EmptyState 'Track a contract to see matches' with Find work link; loading: skeleton lines; error: ErrorState inline; selected: n/a
  - data: DHRA notice title, due Nov 3 12:00 PM ET, NAICS 541511 = real (SAM.gov notice H9821027SS0001)
  - data: requirement chips = placeholder (TBD AI requirement extraction (Pro: AI requirement summaries); chips must come from the notice, not be invented)
- **8. Ranked matches** (`matches-list`, CoverageMeter, right) · social
  - 3 match rows: Luis G. Prime Covers 3 of 4 (75% bar) with amber Invite (primary); Dana R. Sub Covers 2 of 4 (50%) outline Invite; Marcus T. 1099 Covers 1 of 4 (25%) outline Invite
  - Avatar 38px, name + role, CoverageMeter text 'Covers N of 4' at 13px
  - Top match row uses cream tint (R1), not navy border
  - ↳ Invite sends team invite to that member for this bid (button becomes 'Invited')
  - ↳ click row opens Member profile
  - ↳ ranking re-computes when requirements are read
  - states: empty: 'No matches yet' EmptyState; loading: 3 skeleton rows; error: ErrorState inline; selected: top match cream tint + bold name
  - data: coverage fractions 3/4, 2/4, 1/4 = sample (TBD match engine (Jev matching) comparing member capabilities to extracted requirements)
  - data: match members = sample (TBD)
- **9. Live-update note** (`matches-note`, EmptyState, right)
  - Dashed note with AI icon (in --fm-pro): 'Matches update as requirements are read'
  - ↳ none; becomes a spinner row while requirements are being read
  - states: empty: n/a; loading: 'Reading requirements...' with spinner; error: 'Could not read requirements' with Retry; selected: n/a
  - data: requirements read state = placeholder (TBD AI requirement summary job status)
- **10. Open team room link** (`open-team-room`, TextButton, right) · social
  - TextButton 'Open DHRA team room' with arrow icon
  - ↳ goes to Messages with the DHRA room selected
  - states: empty: label 'Start team room' when none exists; loading: disabled; error: toast; selected: n/a
  - data: room id = sample (TBD team room by notice)

### Features
- Choose who you are looking for: Prime, Sub, 1099 talent, Mentor with counts and avatar stacks
- Search members by name, skill, NAICS
- Filter by NAICS, clearance, veteran/cert status, state (MD/VA/DC)
- Sort members (Most recommended, etc.)
- Member cards with online presence, cert and role badges, skills, mutual connections, recommendation count
- Connect and Message (Message gated by Pro)
- Matches rail for the due-soonest tracked bid with requirement chips
- Coverage meter 'Covers N of 4' per match
- Invite match to the bid
- Live-update note while requirements are read
- Open team room link
- Header nav, notifications, messages dot, workspace switcher

### v2 changes vs comp
- R4: remove People > Teaming breadcrumb; page starts with H1 'Teaming'
- R1: top match row cream tint (--fm-row-selected-bg) instead of navy outline; only the selected 'I'm looking for' choice card stays navy
- R2: one amber primary: Invite on the top match in the rail (top-right of the focus area). Connect buttons on cards become outline; Message stays outline with Pro tag
- R6: ChoiceCard counts move from 28px top-right to second line 18px muted with unit word ('12 primes'); label stays 22px first
- R7: unify filter chips to two states (selected amber-soft, unselected outline); State chips MD/VA/DC no longer navy-filled; 'Clearance: Any' dashed chip becomes outline
- R8: skill chips and badges at least 13px (comp 12px); 'N mutual' stays 13px
- R9: no unknown counts shown as large numbers; hide role counts while loading
- Page note: show 'Recommended' badge only when it stands out (N >= 2 and above grid median); not on every card
- Page note: on small screens the Matches rail moves above the member grid
- Remove SampleChip before ship
- Message button shows lock icon + Pro tag as ProLock; Veterans get 3 months Pro so lock should read 'Free for 3 months' where trial is available

### Acceptance
1. Given the page loads When rendered Then the first heading is H1 'Teaming' and no ChevronBreadcrumb is present (R4)
2. Given the header When rendered Then Teaming is the only active NavPill and Veterans, Learn, Community are visible
3. Given the 'I'm looking for' group When rendered Then it has four ChoiceCards in order Prime, Sub, 1099 talent, Mentor, each with icon, label first, count with unit word second, and an AvatarStack
4. Given Prime is selected When another card is clicked Then only that card is navy-filled, aria state updates, and the grid reloads for that role
5. Given counts are loading When cards render Then no placeholder numerals are shown (R9)
6. Given the filter row When rendered Then it contains a SearchField with placeholder 'Search name, skill, NAICS', NAICS chip, Clearance chip, Veteran chip, MD/VA/DC toggles, and a sort control
7. Given a selected FilterChip When inspected Then it uses --fm-chip-selected-bg and no chip anywhere uses a navy fill (R7)
8. Given the user clicks × on 'NAICS 541511' When applied Then the chip is removed and the grid refreshes
9. Given the sort control When changed to another option Then the grid order changes accordingly
10. Given the member grid When rendered Then each card shows Avatar, name link, 13px descriptor, badge(s), up to 3 skill chips, mutual count, Connect and Message
11. Given a member is online When the card renders Then a PresenceDot with aria-label 'Online' appears on the avatar; offline members show none
12. Given a member card When Connect is clicked Then a request is sent and the button state changes to Requested without a page reload
13. Given a free-plan user When Message is clicked Then ProLock explains the Pro feature and offers the trial; given Pro Then a DM opens
14. Given a member with recommendation count below 2 or equal to grid median When the card renders Then no 'Recommended' badge shows
15. Given a SDVOSB-labelled member who is not SBA-verified When rendered Then the badge does not read as certified
16. Given no members match filters When the grid renders Then an EmptyState with 'Clear filters' appears
17. Given the Matches rail When rendered Then the heading reads 'Matches for <tracked bid title>' with due date/time, NAICS and requirement chips
18. Given a bid due within 48h When the rail renders Then the due text uses the urgent style (R5)
19. Given the matches list When rendered Then rows are ordered by coverage descending and each shows 'Covers N of 4' with a bar
20. Given the top match When rendered Then it has the only amber button on the page ('Invite') and the other rows' Invite buttons are outline
21. Given Invite is clicked When successful Then the button becomes 'Invited' and the member is notified
22. Given requirements are still being read When the rail renders Then the note shows a loading state; when done it reads 'Matches update as requirements are read'
23. Given 'Open DHRA team room' When clicked Then Messages opens with that room selected
24. Given viewport <=1024 When rendered Then the Matches rail appears above the member grid
25. Given production build When searched Then no 'Sample members' chip is rendered

### Responsive
<=1024: single column; order is H1, Matches rail (collapsible card with top 3), ChoiceCards (2x2 grid), filters (chips horizontally scroll), member grid 2 columns. <=480: ChoiceCards 2x2 compact, filters behind a 'Filters' IconButton opening a sheet, member grid 1 column, Connect full width with Message as icon button, BottomTabBar replaces PillNav.

### Accessibility
- Landmarks: banner, nav Main, main, aside aria-label Matches
- 'I'm looking for' is a radiogroup (role=radiogroup) with arrow-key navigation, or aria-pressed buttons with only one pressed
- Search input has a visually hidden label; chips are toggle buttons with aria-pressed; removable chips announce 'Remove NAICS 541511'
- Avatar stacks have role=img with a text label listing names and overflow count
- PresenceDot has accessible text 'Online'
- CoverageMeter has text 'Covers 3 of 4' plus aria-valuenow
- Disabled/locked Message button is focusable and explains why (aria-describedby)
- Focus order: search, chips, sort, grid cards, then rail (or rail first when it is reordered on small screens, matching visual order)

### Open questions
- What does 'Recommended N×' mean exactly (N recommenders, or N matches to my past bids)?
- Is the '4' in 'Covers N of 4' always 4, or the real requirement count for the bid? Comp always shows 4
- Which bid drives the Matches rail by default (due soonest tracked, or last opened)?
- Do role counts reflect the entire network or only the filtered state set?
- Mentor cards: which fields replace skills (areas, availability, free vs paid)?
- Clearance filter value set is undefined

## Page: Member profile (`member-profile`)
- Route: /os#/people/<member-id> · Nav: Teaming · Breadcrumb: People ▸ Teaming ▸ <member name>
- Purpose: Decide whether to connect with and team with this member by seeing their capabilities, open teams, recommendations and reviews.
- Primary action: Connect — top-right of the profile header card
- Screen: https://fedm8.com/design/003/screens/06-profile.png · Comp: https://fedm8.com/design/003/comps/06-profile.dc.txt

### Eye path
1 ChevronBreadcrumb People > Teaming > member (depth 3, R4 applies)
2 Profile header: Avatar + name + one line (service, specialty, location) + badges; Connect is the one amber button at top-right (R2)
3 Tabs: Overview, Capabilities, Past work, Reviews, Resume (1099)
4 Capabilities with Can-do grid (green check = confirmed)
5 Open teams to join: outline cards; 'Join team' as TextButton, no navy card (R1)
6 Recommendations, then Reviews after teaming
7 Right rail: Mutual connections, Teamed with, Pro unlock to message

### Modules
- **1. App header** (`app-header`, AppHeader, header)
  - FedM8 logo
  - PillNav 8 pills; Teaming active
  - Notifications IconButton
  - Messages IconButton with unread dot
  - WorkspaceSwitcher 'FEDM8 LLC'
  - Avatar JN
  - ↳ nav click navigates
  - ↳ avatar opens menu
  - states: empty: dot hidden at 0; loading: avatar skeleton; error: renders without workspace name; selected: Teaming pill navy
  - data: nav, workspace, user = real (app config, auth, workspace)
- **2. Breadcrumb** (`breadcrumb`, ChevronBreadcrumb, header)
  - Segment 'People' (neutral)
  - Segment 'Teaming' (accent-soft, link)
  - Segment 'Luis G.' (current, aria-current=page)
  - ↳ click People goes to People hub; click Teaming goes back to Teaming with previous filters restored
  - states: empty: n/a; loading: last segment shows skeleton until name loads; error: falls back to 'Member'; selected: current segment bold; not navy-filled (R1)
  - data: path and member name = real (router + member record)
- **3. Profile header** (`profile-header`, Avatar, main) · Pro · social
  - Avatar 96px with 'Online now' PresenceDot
  - H1 name 'Luis G.'
  - SampleChip 'Sample member' (comp-only, must not ship)
  - Subline 'Navy vet · DevSecOps · Rockville, MD'
  - Badges: CertBadge SDVOSB, RoleBadge Prime, 'Recommended 3×' badge
  - Stats line: AvatarStack (DR, KA, +9) '11 teamed', comment icon '2 open teams'
  - Connect PrimaryButton (plus icon)
  - Message SecondaryButton with lock icon + Pro tag
  - Recommend SecondaryButton with thumbs-up icon
  - ↳ Connect sends request; becomes 'Requested' then 'Connected'
  - ↳ Message: Pro opens DM; free shows ProLock
  - ↳ Recommend opens a short form (optional note, role worked together) and increments Recommended N× after submit; only available to members who teamed with this person
  - ↳ click '11 teamed' scrolls to Teamed with rail card
  - ↳ click '2 open teams' scrolls to Open teams
  - states: empty: missing optional fields hidden (no 'Unknown'); loading: header skeleton; error: ErrorState 'Profile unavailable' with back link; selected: n/a
  - data: name, service, specialty, location = sample (TBD member profile record)
  - data: SDVOSB badge = sample (TBD cert status; show 'Verified' only when verified, otherwise 'Self-declared')
  - data: online presence = sample (TBD presence)
  - data: teamed count 11, open teams 2, Recommended 3× = sample (TBD team history, recommendations)
- **4. Profile tabs** (`profile-tabs`, Tabs, main)
  - 5 tabs: Overview (selected), Capabilities, Past work, Reviews, Resume with '1099' RoleBadge
  - Selected tab navy pill in comp
  - ↳ click tab shows that section; Overview shows all sections stacked as in comp; Capabilities, Past work, Reviews, Resume each focus one section
  - ↳ arrow keys move between tabs
  - ↳ Resume tab visible only when member is available for 1099 work
  - states: empty: tab with no content shows EmptyState (e.g. Past work 'No past work added'); loading: section skeleton; error: section ErrorState; selected: selected tab uses --fm-selected-bg (allowed: 'where you are' within page); the 1099 badge stays outline
  - data: tab list = real (static plus member 1099 flag)
  - data: Past work and Resume content = placeholder (TBD; comp shows only Overview)
- **5. Capabilities** (`capabilities`, Checklist, main)
  - H2 'Capabilities'
  - 6 skill chips: DevSecOps, Web sustainment, Cloud hosting, FedRAMP, CI/CD, Zero trust
  - NAICS row (mono): 541511, 541512, 541519, 518210
  - H3 'Can do' grid: confirmed (green check, success border) Web sustainment, Cloud hosting, Security / ATO support, NAICS 541511; unconfirmed (outline circle, muted) Content management, Section 508 testing
  - ↳ click a skill chip filters Teaming by that skill
  - ↳ click a NAICS code opens Find work filtered by that code
  - ↳ 'Can do' items that match the viewer's tracked bid requirements are highlighted with a 'Matches DHRA' tag
  - states: empty: 'No capabilities added yet'; loading: chip skeletons; error: ErrorState inline; selected: confirmed = --fm-success-soft with success icon
  - data: skills, NAICS codes, can-do items = sample (TBD member capabilities; NAICS codes themselves are real reference codes)
  - data: confirmed vs unconfirmed state = sample (TBD per-capability verification (past performance or peer confirmation))
- **6. Open teams** (`open-teams`, SquadCard, main) · social
  - H2 'Open teams' + 'Leading 2' text
  - Card 1: DHRA Web Sustainment / 'Sources Sought · due Nov 3, 12:00 PM ET' / red UrgencyChip 'Tomorrow' / need chips 'Needs: CMS sub', 'Needs: 508 tester' / AvatarStack (LG, PS, MT) / 'Covers 3 of 4' / Join team
  - Card 2: FOMS Software Development / 'FAA · Sources Sought · due Nov 6' / need chip 'Needs: QA lead' / AvatarStack (LG, KA) / Join team
  - Comp card 1 is navy with amber Join team; v2 makes both cards outline and Join team a TextButton
  - ↳ click card title opens the Contract detail
  - ↳ Join team opens JoinAsPicker (Prime/Sub/1099) preselected to the needed role, then adds user to the team room
  - ↳ need chips filter nothing; they link to the requirement on Contract detail
  - states: empty: 'Not leading any open teams'; loading: 2 skeleton cards; error: ErrorState inline; selected: n/a
  - data: DHRA notice and due date Nov 3 12:00 PM ET = real (SAM.gov notice H9821027SS0001)
  - data: FOMS notice, FAA, due Nov 6 = real (SAM.gov notice (also in My pipeline))
  - data: needs chips, team avatars, coverage 3 of 4, Leading 2 = sample (TBD team roster and requirement coverage)
- **7. Recommendations** (`recommendations`, WinCard, main) · social
  - H2 'Recommended 3×'
  - SampleChip 'Sample' (comp-only)
  - TextButton 'See all 3'
  - 2 quote cards: Dana R. 'Clear scope, fast answers. Ran our sustainment cutover with no surprises.'; Priya S. 'Brought me in early and kept 508 in the plan from day one.' each with Avatar, name, descriptor
  - ↳ click author opens their profile
  - ↳ See all 3 expands to all recommendations
  - states: empty: 'No recommendations yet' with 'Be the first' text link if viewer has teamed; loading: 2 skeleton cards; error: ErrorState inline; selected: n/a
  - data: recommendation quotes and authors = sample (TBD recommendations table; real quotes only from real members)
- **8. Reviews after teaming** (`reviews`, ThreadPost, main) · social
  - H2 'Reviews after teaming'
  - SampleChip 'Sample'
  - 2 review cards: Kim A. with chip 'Teamed on FAA sources sought', 5 of 5 stars, 'Reliable lead. Answered every thread the same day.'; Marcus T. with chip '1099 on a CMS migration', 4 of 5 stars, 'Paid on time, scope stayed clean. Would team again.'
  - Stars have accessible text '5 out of 5 stars'
  - ↳ reviews can only be written by members who teamed with this person (button 'Write a review' on tab view)
  - ↳ report review action in overflow menu
  - states: empty: 'No reviews yet'; loading: skeleton rows; error: ErrorState inline; selected: n/a
  - data: reviews, star ratings, context chips = sample (TBD reviews table; real reviews only with verified shared team)
- **9. Mutual connections** (`rail-mutual`, AvatarStack, right) · social
  - H2 'Mutual connections' + count 4
  - 4 rows: Dana R. Army vet · Cloud ops; Priya S. Section 508 / UX; Marcus T. USMC · Web dev; Kim A. Air Force · PM (avatar, name link, descriptor 13px)
  - ↳ click name opens that profile
  - states: empty: 'No mutual connections' hidden count; loading: skeleton rows; error: ErrorState inline; selected: n/a
  - data: mutual connections = sample (TBD connection graph)
- **10. Teamed with** (`rail-teamed-with`, AvatarStack, right) · social
  - H2 'Teamed with' + count 11
  - AvatarStack 40px: DR, KA, PS, MT, +7
  - TextButton 'See all'
  - ↳ click avatar opens profile
  - ↳ See all opens full list drawer
  - states: empty: 'Hasn't teamed yet'; loading: avatar skeletons; error: ErrorState inline; selected: n/a
  - data: teamed-with list and count 11 = sample (TBD team history)
- **11. Message Luis (Pro unlock)** (`rail-pro-unlock`, ProLock, right) · Pro
  - Dashed accent card with lock icon, 'Message Luis' + Pro tag
  - Copy 'Veterans get 3 months of Pro free.'
  - Button 'Start free Pro' (navy in comp; v2 outline since Connect is the amber primary)
  - ↳ Start free Pro activates the veteran trial or opens plan page for non-veterans
  - ↳ card hides for Pro users
  - states: empty: hidden for Pro users; loading: hidden; error: toast on activation failure; selected: n/a
  - data: Pro trial eligibility (veteran) = real (plan/entitlement record; rule: veterans get 3 months Pro free)

### Features
- Profile identity: avatar, presence, name, service/specialty/location, badges
- Connect (primary), Message (Pro), Recommend actions
- Teamed count and open-teams count shortcuts
- Tabs: Overview, Capabilities, Past work, Reviews, Resume (1099)
- Skills, NAICS codes, confirmed vs unconfirmed Can-do grid
- Open teams the member leads with needs and coverage; Join team
- Recommendations quotes with See all
- Reviews after teaming with star ratings and context chips
- Mutual connections list
- Teamed with avatar stack and See all
- Pro unlock card with veteran 3-month trial
- Breadcrumb People > Teaming > member

### v2 changes vs comp
- Page note: Connect becomes the amber PrimaryButton (top-right of header card); Message and Recommend are outline
- Page note/R1: open-team cards are both outline; the DHRA card is no longer navy-filled; 'Join team' is a TextButton (comp had amber on one card, outline on the other)
- R2: 'Start free Pro' in the rail is outline so Connect is the only amber button
- R4: breadcrumb kept (depth 3); current segment not navy-filled
- R5: 'Tomorrow' chip on DHRA card uses --fm-urgent; FOMS due Nov 6 uses --fm-warn
- R7: no navy chips; the '1099' badge in the Resume tab is outline
- R8: descriptor lines, need chips, quote attribution at least 13px (comp 12px)
- R9: counts 11, 4, 3 are only shown when real; hide while loading
- Remove SampleChips ('Sample member', 'Sample') before ship
- Selected tab keeps navy pill (where you are within page) but tab bar is the only navy element in main content

### Acceptance
1. Given a member id When the page loads Then the breadcrumb reads People > Teaming > <name> and the last segment has aria-current=page
2. Given the Teaming breadcrumb segment When clicked Then Teaming opens with the previous filters restored
3. Given the header card When rendered Then it shows Avatar, H1 name, a single descriptor line, CertBadge, RoleBadge, teamed count and open-teams count
4. Given the member is online When rendered Then the avatar shows a PresenceDot with accessible text 'Online now'
5. Given the page When inspected Then exactly one amber PrimaryButton exists in main/header area and it is 'Connect'
6. Given Connect is clicked When successful Then the button changes to 'Requested' and cannot be clicked twice
7. Given a free-plan user When Message is clicked Then ProLock explains Pro; given a Pro user Then a DM opens
8. Given the viewer has not teamed with this member When the header renders Then Recommend is hidden or disabled with an explanation
9. Given the Recommend button When used by an eligible member Then a recommendation form submits and the Recommended count increments
10. Given the tab bar When rendered Then tabs are Overview, Capabilities, Past work, Reviews and Resume (the Resume tab only if the member offers 1099)
11. Given a tab is activated by click or arrow key When shown Then aria-selected updates and the matching section is displayed
12. Given the Capabilities module When rendered Then it lists skill chips, a NAICS row with mono codes, and a 'Can do' grid where confirmed items show a green check and unconfirmed show an empty circle
13. Given a NAICS code When clicked Then Find work opens filtered by that code
14. Given the Open teams module When rendered Then each card shows notice title, agency/type/due, need chips, AvatarStack and a 'Join team' TextButton and neither card is navy-filled
15. Given a team card due in <=48h When rendered Then its chip uses --fm-urgent; due in <=7 days Then --fm-warn
16. Given Join team is clicked When the picker opens Then the needed role is preselected and confirming adds the viewer to the team room
17. Given the Recommendations module When rendered Then quote cards show quote, author Avatar, name and descriptor, and 'See all N' reveals all
18. Given the Reviews module When rendered Then each review shows author, context chip, star rating with text alternative and body
19. Given a member with no reviews When rendered Then the module shows an EmptyState instead of being hidden silently
20. Given the Mutual connections module When rendered Then it lists each mutual with avatar, name link and 13px descriptor and the count badge equals the list length
21. Given the Teamed with module When See all is clicked Then the full teamed list opens
22. Given a veteran viewer on free plan When the Pro unlock card renders Then it shows 'Veterans get 3 months of Pro free.' and 'Start free Pro' as an outline button
23. Given a Pro viewer When the rail renders Then the Pro unlock card is not shown
24. Given viewport <=1024 When rendered Then the rail stacks below main with Mutual connections first
25. Given production build When searched Then no SampleChip is rendered

### Responsive
<=1024: right rail drops below main; header card stacks (avatar above text, actions full width in a row); tabs scroll horizontally. <=480: header actions become Connect full-width plus Message and Recommend as icon buttons; Open teams and Recommendations are single column; Capabilities Can-do grid single column; BottomTabBar replaces PillNav.

### Accessibility
- Landmarks: banner, nav Main, nav Breadcrumb, main, aside aria-label Network
- H1 is the member name; sections use H2; Can-do uses H3
- Tabs follow the WAI-ARIA tabs pattern (tablist, tab, tabpanel, arrow keys)
- Star ratings use role=img with text '4 out of 5 stars'
- Avatar stacks expose names via aria-label
- Confirmed vs unconfirmed Can-do conveyed by text/icon not colour alone (add visually hidden 'Confirmed' / 'Not confirmed')
- Locked Message button remains focusable and describes the lock

### Open questions
- How is a Can-do item 'confirmed' (self-claim, past performance record, peer confirmation)?
- Who can write reviews and recommendations, and are reviews moderated?
- What do Overview vs the other tabs each show? Comp shows all sections stacked under Overview
- Is location (Rockville, MD) visible to everyone or only connections?
- What are the contents of Past work and Resume tabs (not designed)?
- Does 'Recommended 3×' count unique recommenders or endorsements?

## Page: Messages and team rooms (`messages`)
- Route: /os#/messages/<conversation-id> · Nav: Teaming · Breadcrumb: People ▸ Messages ▸ <conversation name>
- Purpose: Talk with a team room or a person, share the contract and files, and see roster, requirement coverage and pinned items without leaving the chat.
- Primary action: Send — bottom-right of the Composer (chat surface; amber per R2, comp used navy)
- Screen: https://fedm8.com/design/003/screens/07-messages.png · Comp: https://fedm8.com/design/003/comps/07-messages.dc.txt

### Eye path
1 Breadcrumb People > Messages > room (depth 3)
2 Left: conversation list (All / Rooms / DMs); active row cream tint + bold (R1), unread dots and counts
3 Centre: room header (name, online count, members, Coverage chip) then messages; contract ContractEmbedCard inside the first message
4 Composer at bottom: Attach, Link contract, text input, Send (the single amber button)
5 Right: Roster, Coverage (3 of 4), Pinned

### Modules
- **1. App header** (`app-header`, AppHeader, header)
  - FedM8 logo
  - PillNav 8 pills; Teaming active (comp)
  - Notifications IconButton
  - Messages IconButton with red unread dot
  - WorkspaceSwitcher 'FEDM8 LLC'
  - Avatar JN
  - ↳ nav click navigates
  - ↳ Messages icon returns to this page
  - states: empty: dot hidden at 0; loading: avatar skeleton; error: renders without workspace; selected: Teaming pill navy
  - data: nav, workspace, user = real (app config, auth, workspace)
  - data: unread dot = sample (TBD aggregate unread)
- **2. Breadcrumb** (`breadcrumb`, ChevronBreadcrumb, header)
  - People
  - Messages (accent-soft link)
  - DHRA team room (current)
  - ↳ click Messages deselects conversation (list view on mobile)
  - states: empty: n/a; loading: skeleton last segment; error: 'Conversation'; selected: current segment bold, not navy-filled (R1)
  - data: path = real (router)
- **3. Conversation list header and search** (`conversation-list-header`, SearchField, left)
  - H2 'Messages'
  - SampleChip 'Sample members' (comp-only)
  - SearchField 'Search'
  - SegmentedControl All / Rooms / DMs (All selected)
  - ↳ typing searches conversation names and message text
  - ↳ segment filters list to rooms or direct messages
  - ↳ compose-new DM IconButton (added) opens member picker
  - states: empty: no results: 'No conversations match'; loading: list skeleton; error: ErrorState inline; selected: selected segment uses --fm-chip-selected-bg (not navy fill) per R7
  - data: conversations = sample (TBD conversations table)
- **4. Conversation list** (`conversation-list`, ChatList, left) · Pro · social
  - Row 1 (active): team room icon (amber square), 'DHRA Web Sustainment', time 9:41, 'team room · 4 members', unread badge 3
  - Row 2: DM Priya S. with online dot, time 9:38, bold preview 'I can run the 508 pass tonight', red unread dot
  - Row 3: DM Luis G. online, 9:02, 'Sent the draft teaming agreement'
  - Row 4: group 'FAA FOMS team room', Yesterday, 'Kim A.: QA lead is still open', unread dot
  - Row 5: DM Dana R., Sun, 'Happy to review the cloud section'
  - Row 6: DM Marcus T., Fri, 'Rate sheet attached'
  - ↳ click row opens conversation
  - ↳ unread rows bold preview; opening clears unread
  - ↳ overflow per row: mute, pin, mark unread
  - ↳ relative time: today HH:MM, yesterday, weekday
  - states: empty: EmptyState 'No messages yet' with 'Find a teammate' link to Teaming; loading: 6 skeleton rows; error: ErrorState with Retry; selected: active row --fm-row-selected-bg + bold title (R1; comp used navy)
  - data: conversation names, previews, times = sample (TBD messages/conversations)
  - data: unread counts and dots = sample (TBD read receipts)
  - data: presence dots = sample (TBD presence)
- **5. Room header** (`room-header`, PageTitle, main) · social
  - Room icon
  - H1 'DHRA Web Sustainment'
  - TrackingBar-style presence line '3 online now · 4 members' with green dot
  - AvatarStack of members (LG, PS, MT, JN)
  - Search-in-room IconButton
  - Coverage chip 'Coverage 3 of 4' (added by page note; visible without the right rail)
  - ↳ click AvatarStack or Coverage chip scrolls/opens the right rail (drawer on small screens)
  - ↳ search IconButton opens in-room search with result list and jump to message
  - states: empty: n/a; loading: header skeleton; error: ErrorState 'Could not load room'; selected: n/a
  - data: room name = real (derived from notice title when room is tied to a notice)
  - data: online count, member count = sample (TBD presence and room membership)
  - data: Coverage 3 of 4 = sample (TBD coverage engine (matches Coverage module))
- **6. Message stream** (`message-stream`, ChatBubble, main) · social
  - Date divider 'Today · Mon, Nov 2'
  - Msg 1 Luis G. (Prime badge, 8:52 AM): 'Morning. Response is due tomorrow 12:00 PM ET. Here is the notice.'
  - Msg 2 Marcus T. (1099 badge, 9:05 AM): 'I can take content management. Past CMS migrations are in my profile.' with ReactionChips '+1 3' (pressed) and 'Check 1'
  - Msg 3 You (9:14 AM, navy bubble right-aligned): 'Great. Attaching our capability statement for the roster.' + FileCard + 'Seen by 3'
  - Msg 4 Priya S. (Sub badge, 9:38 AM): 'I can run the 508 pass tonight. Send me the outline when it is ready.' with ReactionChip '+1 2'
  - TypingIndicator 'Priya S. is typing'
  - ↳ scroll up loads older messages (infinite)
  - ↳ hover/focus message shows reply, react, pin, copy actions
  - ↳ click ReactionChip toggles own reaction and updates count
  - ↳ new message auto-scrolls only when user is at the bottom; otherwise shows 'N new' pill
  - ↳ own messages right-aligned with seen count
  - ↳ author name click opens Member profile
  - ↳ Check reaction on a 'I can' message can mark the requirement as covered (sync to Coverage)
  - states: empty: 'Say hi to your team' EmptyState with suggested first message; loading: message skeletons; error: inline 'Could not load messages' with Retry; failed send shows red 'Not sent. Retry'; selected: n/a
  - data: message text, authors, times = sample (TBD messages table)
  - data: role badges Prime/1099/Sub = sample (TBD team role by notice)
  - data: reaction counts, seen by 3, typing = sample (TBD realtime channel)
- **7. Contract card in chat** (`contract-embed`, ContractEmbedCard, main) · social
  - Card inside Luis's message: doc icon, TypeBadge 'Sources Sought', red UrgencyChip 'Due tomorrow · Nov 3, 12:00 PM ET'
  - Title 'Web Sustainment/Content Management/Cloud Hosting'
  - Agency 'Dept of Defense › DHRA' (13px)
  - Meta row: solicitation H9821027SS0001 (mono), NAICS 541511, comments 24, team 13
  - Card is a link with aria-label 'Open contract ...'; comp border is navy with hard shadow, v2 uses neutral border
  - ↳ click opens Contract detail
  - ↳ composer 'Link a contract' inserts this card type
  - states: empty: n/a; loading: card skeleton; error: 'Contract unavailable' text card with link; selected: n/a
  - data: title, agency, solicitation number, NAICS, due date, notice type = real (SAM.gov notice H9821027SS0001)
  - data: comments 24, team 13 = sample (TBD thread and team counts)
- **8. File attachment** (`file-card`, FileCard, main) · social
  - File icon tile (accent-soft)
  - Name 'Capability statement.pdf' (ellipsis)
  - Meta 'PDF · 2 pages'
  - Download IconButton (aria-label 'Download Capability statement.pdf')
  - 'Seen by 3' with double-check icon below the message
  - ↳ click name previews PDF
  - ↳ download button downloads
  - ↳ pinning from message menu adds to Pinned
  - states: empty: n/a; loading: upload progress bar in card; error: 'Upload failed. Retry'; selected: n/a
  - data: file name, type, page count = sample (TBD file storage; the capability statement itself is real FedM8 content (site/capability-statement.html))
  - data: Seen by 3 = sample (TBD read receipts)
- **9. Composer** (`composer`, Composer, bottom) · social
  - Attach file IconButton
  - Link a contract IconButton
  - Text input 'Message the room' with hidden label 'Message DHRA team room'
  - Send PrimaryButton with send icon (amber in v2)
  - ↳ Enter sends, Shift+Enter newline
  - ↳ Attach opens file picker and shows upload chip before send
  - ↳ Link a contract opens picker of tracked notices
  - ↳ typing triggers TypingIndicator for others
  - ↳ Send disabled when empty
  - ↳ @mention opens member list (added)
  - states: empty: Send disabled; loading: Send shows spinner while posting; error: message stays in input with 'Not sent' inline; selected: input focus ring
  - data: draft text = real (local draft only (per-conversation unsent draft in localStorage))
- **10. Roster** (`roster`, RosterList, right) · Pro · social
  - H2 'Roster'
  - 4 rows: Luis G. Prime; Priya S. Sub; Marcus T. 1099; You (JN) Sub
  - RoleBadge on each (Prime amber in comp; v2 uses neutral RoleBadge styles consistent across pages)
  - Invite dashed button with plus icon
  - ↳ click member opens profile
  - ↳ Invite opens member picker / Teaming matches
  - ↳ own row marked 'You'
  - ↳ roster contact actions are Pro-gated
  - states: empty: 'Only you so far' with Invite; loading: skeleton rows; error: ErrorState inline; selected: n/a
  - data: roster members and roles = sample (TBD team membership by notice)
- **11. Coverage** (`coverage`, CoverageMeter, right) · social
  - H2 'Coverage' + '3 of 4' + progress bar 75%
  - 4 requirement rows: Web sustainment (covered), Cloud hosting (covered), NAICS 541511 (covered), Content management (open, muted circle) with chip 'Marcus pending'
  - ↳ click a requirement scrolls to the requirement on Contract detail
  - ↳ 'pending' chip opens the member's claim
  - ↳ covered state changes when a member's 'I can do this' is confirmed
  - states: empty: 'Requirements not read yet' with loading state; loading: skeleton bar and rows; error: ErrorState inline; selected: covered = --fm-success icon
  - data: requirement labels and coverage 3 of 4 = placeholder (TBD per-requirement 'I can do this' claims; requirement text must come from the notice (AI requirement summaries are Pro))
  - data: Marcus pending = sample (TBD pending claim)
- **12. Pinned** (`pinned`, FileCard, right) · social
  - H2 'Pinned' with pin icon
  - 3 link rows: H9821027SS0001 (contract icon), Capability statement.pdf (file icon), Response outline (file icon)
  - ↳ click opens item
  - ↳ unpin from overflow menu
  - states: empty: 'Pin messages, files or the contract here'; loading: skeleton rows; error: ErrorState inline; selected: n/a
  - data: pinned contract H9821027SS0001 = real (SAM.gov notice)
  - data: pinned files = sample (TBD file storage)

### Features
- Conversation list with search, All/Rooms/DMs filter, unread dots and counts, presence
- Team room and DM conversation types
- Room header with online count, members, search-in-room, Coverage chip
- Chat messages with author, role badge, time, own-message alignment
- Reactions (+1, Check) with counts
- Contract card embedded in chat
- File attachments with preview/download
- Read receipts ('Seen by N')
- Typing indicator
- Composer: attach, link contract, send
- Roster with role badges and Invite
- Requirement Coverage with progress and pending claims
- Pinned contract and files
- Header nav, notifications, workspace

### v2 changes vs comp
- Page note: keep three-column layout as the pattern for every chat surface
- Page note: add 'Coverage 3 of 4' chip to the room header so coverage is visible without the right rail
- R1: active conversation row is cream tint + bold title (comp navy fill); All/Rooms/DMs selected segment uses chip-selected style (not navy)
- R2: Send becomes the amber PrimaryButton (comp navy); it is the only amber button on the page
- R4: breadcrumb kept (depth 3); current segment not navy-filled
- R5: contract card due chip uses --fm-urgent (due in <=48h)
- R7: no navy chips; segmented control selected = chip-selected-bg
- R8: message meta (names, times, badges, previews) at least 13px (comp 12px)
- R9: unread counts show only when >0; no placeholder numerals
- Contract card in chat loses navy border and hard shadow, uses neutral border
- Remove SampleChip before ship
- Roster RoleBadge colours: use the same RoleBadge styles as other pages instead of amber Prime

### Acceptance
1. Given the page loads When rendered Then three panes appear: Conversations (left), Chat (centre), Room info (right)
2. Given the conversation list When rendered Then it shows search, All/Rooms/DMs control and rows with name, time, preview, and unread indicator
3. Given a conversation is active When rendered Then its row has cream tint and bold title, and is not navy (R1)
4. Given a conversation with unread messages When opened Then its unread badge or dot disappears
5. Given 'Rooms' is selected When applied Then only team rooms are listed; 'DMs' shows only direct messages
6. Given text is typed in list search When applied Then the list filters to matching conversations
7. Given the room header When rendered Then it shows room name as H1, '3 online now · 4 members', member AvatarStack, search IconButton and a 'Coverage 3 of 4' chip
8. Given the Coverage chip When clicked Then the Coverage section is revealed (scroll or drawer)
9. Given messages When rendered Then each other-user message shows avatar, name, RoleBadge, time (>=13px) and a bubble; own messages are right-aligned with 'You'
10. Given a date change When rendered Then a date divider is inserted between messages from different days
11. Given a ReactionChip When clicked Then the user's reaction toggles and the count updates immediately
12. Given a message with a contract link When rendered Then a ContractEmbedCard shows type, due chip, title, agency, solicitation number, NAICS, comment count and team count, and clicking it opens Contract detail
13. Given a due time within 48h When the card renders Then the chip uses --fm-urgent
14. Given a file attachment When rendered Then FileCard shows name, type/pages and a Download button; clicking Download downloads the file
15. Given a sent message has been read When rendered Then 'Seen by N' appears under it
16. Given another member is typing When rendered Then 'Name is typing' with the animated indicator appears and disappears after 5s of inactivity
17. Given the Composer is empty When rendered Then Send is disabled; given text Then pressing Enter sends and the message appears at the bottom
18. Given Attach is clicked When a file is chosen Then an upload progress state shows and the file posts on Send
19. Given Link a contract is clicked When a notice is chosen Then a ContractEmbedCard is inserted
20. Given the page When inspected Then Send is the only amber button
21. Given the Roster module When rendered Then each member shows avatar, name, RoleBadge, and an Invite button is present
22. Given Invite is clicked When used Then a member picker opens and chosen members receive invites
23. Given the Coverage module When rendered Then it shows 'N of M' text, a progress bar and one row per requirement with covered/open icon and pending chips
24. Given a requirement has a pending claim When rendered Then a chip 'Name pending' shows on that row
25. Given the Pinned module When rendered Then it lists pinned contract and files and each opens on click
26. Given there are no conversations When the list renders Then an EmptyState links to Teaming
27. Given the production build When searched Then no 'Sample members' chip is rendered

### Responsive
<=1024: two panes; conversation list collapses into a drawer opened by a back/list IconButton, right info rail becomes a drawer opened from the Coverage chip or members AvatarStack. <=480: single pane at a time: list view then chat view with back arrow; Composer sticky bottom with 44px targets; info drawer is a bottom sheet; BottomTabBar hides inside an open chat.

### Accessibility
- Landmarks: banner, nav Main, nav Breadcrumb, complementary aria-label Conversations, main/region aria-label '<room> chat', complementary aria-label Room info
- Message list has role=log with aria-live=polite; new incoming messages are announced, typing indicator announced politely
- Each message has an accessible name 'Name, role, time, text'
- Unread indicators have text alternatives ('Unread', '3 unread')
- ReactionChips are toggle buttons with aria-pressed and count in label
- Composer input has a label; Send, Attach, Link contract IconButtons have aria-labels
- Presence dots have text alternatives
- Keyboard: Tab order list > chat > composer > info; Esc closes drawers; Ctrl/Cmd+K focuses list search (optional)

### Open questions
- Are team rooms auto-created per tracked notice, or created manually?
- Who can see a room: only roster members, or anyone tracking the notice (read-only)?
- Are DMs Pro-gated for the sender only or both sides? Comp shows DMs in the list without lock
- Does a 'Check' reaction from a member mark a requirement as covered automatically?
- Is 'Seen by N' shown in rooms of any size or only small teams?
- File storage limits and allowed types are not defined
- Room 'You' role shows Sub while pipeline context shows the user as FEDM8 LLC: define how the viewer's role is set per notice

## Page: Community (`community`)
- Route: /os#/community?space=<slug>&sort=hot · Nav: Community · Breadcrumb: —
- Purpose: Browse, vote on and join Reddit-style discussion across Spaces (Contracts, Grants, Certifications, VetCert help, Teaming, Wins, Off-duty), and start a post or poll.
- Primary action: Post — right end of the Composer row at the top of the feed (top-right of the focus area); disabled until the composer has text
- Screen: https://fedm8.com/design/003/screens/08-community.png · Comp: https://fedm8.com/design/003/comps/08-community.dc.txt

### Eye path
1 H1 with the active Space name ('All spaces') and the Hot/New/Top SortTabs on the same row (no breadcrumb, R4: Community is a top-level page)
2 Composer with the single amber Post button (R2)
3 Feed top to bottom. Every card uses the same white surface; amber appears only on badges (Win badge, urgency chips by R5)
4 Left rail Spaces (cream tint on the current Space, R1) for changing context; right rail Online now, Trending, Rules for secondary scanning
5 Linked contract/grant cards inside posts use the same styling as list rows, with urgency colour by days left (R5)

### Modules
- **1. App header and PillNav** (`app-header`, AppHeader, header)
  - FedM8 logo/home link
  - PillNav: Home, Contracts, Grants, Teaming, Certifications, Veterans, Learn, Community (Community is the navy active pill)
  - Notifications IconButton
  - Messages IconButton with unread PresenceDot-style red dot
  - WorkspaceSwitcher label 'FEDM8 LLC'
  - Avatar (user initials JN)
  - ↳ click nav pill -> navigates, active pill navy (R1)
  - ↳ click bell -> notifications panel
  - ↳ click messages -> /os#/messages
  - ↳ click workspace name -> WorkspaceSwitcher menu
  - ↳ click avatar -> account menu
  - states: empty: no unread: dot hidden; loading: avatar shows skeleton circle; error: header still renders; badges hidden; selected: Community pill navy with aria-current=page
  - data: nav items = real (static app config)
  - data: unread messages flag = real (messages table unread query, TBD name)
  - data: workspace name and user initials = real (auth session / workspaces table)
- **2. Spaces list** (`spaces-nav`, ThreadList (left rail list of Spaces), left) · social
  - Heading 'Spaces'
  - Rows: All spaces, Contracts, Grants, Certifications, VetCert help, Teaming, Wins, Off-duty
  - Per-row round icon (colour-coded) and member/post count
  - 'New space' dashed outline button
  - SampleChip next to heading (comp only)
  - ↳ click row -> filters feed and updates H1 and URL ?space=
  - ↳ click New space -> create-space dialog (name, icon, description, who can post)
  - ↳ keyboard: arrow keys move between rows, Enter selects
  - states: empty: only All spaces shown with 'No spaces yet' text; loading: 8 skeleton rows; error: ErrorState with Retry; All spaces still usable; selected: cream --fm-row-selected-bg + bold title (R1), not navy
  - data: space list (names, icons) = sample (TBD community_spaces table)
  - data: per-space counts (8.4k, 4.2k, 1.1k, 2.3k, 1.8k, 3.0k, 960, 2.7k) = placeholder (TBD; hide counts until real (R9). Unit is unclear (members vs posts) and must be labelled)
- **3. Feed title and sort** (`feed-header`, PageTitle + SortTabs, main) · social
  - H1 with current Space name
  - SampleChip 'Sample posts and members' (comp only)
  - SortTabs Hot / New / Top
  - ↳ click sort tab -> re-sorts feed, updates ?sort=, keeps scroll at top
  - ↳ arrow keys move between tabs
  - states: empty: n/a; loading: feed skeletons, tabs stay interactive; error: tabs stay, feed shows ErrorState; selected: selected tab uses --fm-chip-selected-bg (R7), not navy
  - data: sort mode = real (client state + query param)
  - data: Hot ranking formula = placeholder (TBD (votes x recency decay))
- **4. Start a post** (`composer`, Composer, main) · social
  - Avatar
  - Text field 'Start a post'
  - IconButton 'Link a contract'
  - IconButton 'Start a poll'
  - PrimaryButton 'Post' (amber per R2)
  - ↳ focus field -> expands to title, body, Space picker, post-type (Discussion/Question/Poll/Win)
  - ↳ Link a contract -> search picker for a notice or grant, inserts ContractEmbedCard
  - ↳ Start a poll -> adds 2-6 option inputs and a duration
  - ↳ Post -> publishes to the selected Space, prepends to New, disabled if empty or rate-limited
  - ↳ Esc collapses without losing draft
  - states: empty: placeholder 'Start a post', Post disabled; loading: Post shows spinner and disables; error: inline error with retry, draft kept; selected: expanded composer
  - data: user avatar and name = real (auth profile)
  - data: posts, polls, links = real (TBD community_posts, polls, post_links tables)
- **5. Contract thread post** (`post-contract-thread`, ThreadPost + ContractEmbedCard, main) · social
  - VoteRail (up, score 84, down)
  - Space link 'Contracts'
  - UrgencyChip 'Due tomorrow'
  - Byline 'Posted by Luis G. · 3h'
  - Title 'Who is responding to the DHRA web sustainment sources sought?'
  - ContractEmbedCard: icon, title, agency, notice type, solicitation number (mono), due-date chip 'Nov 3'
  - Action row: N comments, N teaming, Share, Save
  - ↳ up/down vote toggles, one vote per user, score updates optimistically
  - ↳ click title or comments -> thread view
  - ↳ click embed card -> contract detail /os#/opportunities?notice=<id>
  - ↳ click teaming count -> Teaming room for that notice
  - ↳ Share -> copy link/share sheet
  - ↳ Save -> toggles bookmark
  - states: empty: n/a; loading: post skeleton; error: vote failure reverts score with toast; selected: user vote highlights arrow; saved shows filled bookmark
  - data: post, author, time, score 84 = sample (TBD community_posts)
  - data: linked contract title, agency, notice number H9821027SS0001, due date = real (opportunities/notices table (SAM.gov ingest); verify the notice exists before use)
  - data: 24 comments, 13 teaming = sample (TBD comment count; teaming count from team room members for the notice)
  - data: due chip colour = real (computed from notice response deadline (R5))
- **6. Win post** (`post-win`, WinCard + ThreadPost, main) · social
  - VoteRail (score 212)
  - Avatar KA, Space 'Wins', green 'Win' badge, byline 'Kim A. · VOSB · 5h' with CertBadge
  - Trophy icon in amber circle
  - Title 'First sub award, found through a FedM8 team room'
  - Body line
  - 'Congrats 48' ReactionChip button
  - N comments, Share, Save
  - ↳ Congrats toggles reaction and increments count
  - ↳ vote, comment, share, save as other posts
  - ↳ click author -> member profile
  - states: empty: n/a; loading: skeleton; error: reaction failure reverts; selected: Congrats pressed state with chip-selected bg
  - data: win post content, author Kim A., VOSB cert = sample (TBD; real posts must come from a user and cert shown only if verified on profile)
  - data: Congrats 48, 212 score, 31 comments = sample (TBD reactions table)
- **7. Question post with accepted answer** (`post-question`, ThreadPost + accepted answer block, main) · social
  - VoteRail (57)
  - Space 'VetCert help', blue 'Question' badge, byline 'Marcus T. · 1d'
  - Title 'SBA VetCert application is in review. Can I bid veteran set-asides meanwhile?'
  - Accepted answer block (green): check icon, 'Accepted answer' badge, answerer Dana R. with SDVOSB CertBadge, vote count, answer text
  - 17 comments, Share, Save
  - ↳ asker can mark one answer accepted (moves it to top of thread)
  - ↳ vote on answer separately
  - ↳ click answer -> scrolls to it in thread
  - states: empty: no accepted answer: block hidden, 'Mark as answer' available to asker; loading: skeleton; error: accept failure shows toast; selected: accepted block uses --fm-success-soft
  - data: question title and answer text = sample (TBD; the sample answer is NOT verified guidance (see open_questions))
  - data: answerer role badge SDVOSB = sample (must derive from verified cert on profile, never self-declared text)
  - data: 57 score, 41 answer votes, 17 comments = sample (TBD)
- **8. Grant thread post** (`post-grant-thread`, ThreadPost + ContractEmbedCard (grant), main) · social
  - VoteRail (29)
  - Space 'Grants', UrgencyChip 'Closes Nov 3', byline 'Posted by Dana R. · 6h'
  - Title 'Anyone applying to the BJA DNA Capacity Enhancement (CEBR) grant?'
  - Grant embed card: icon, title, agency 'Bureau of Justice Assistance', opportunity number O-BJA-2026-172680 (mono), 'Ceiling $4,900,000' chip
  - 9 comments, Share, Save
  - ↳ same as contract thread; embed opens grant detail
  - states: empty: n/a; loading: skeleton; error: as contract thread; selected: as contract thread
  - data: grant title, agency, opportunity number, ceiling, close date = real (grants table (Grants.gov ingest); confirm before shipping; ceiling shown only when present on the record)
  - data: post, score 29, 9 comments = sample (TBD)
- **9. Poll post** (`post-poll`, ThreadPost + poll, main) · social
  - VoteRail (38)
  - Avatar PS, Space 'Teaming', purple 'Poll' badge, byline 'Priya S. · 2d'
  - Title 'How do you find your primes?'
  - 4 option bars: FedM8 team rooms 46%, Industry days 28%, Referrals 18%, Cold email 8% (user's pick outlined)
  - Footer '212 votes · 1d left', 14 comments, Share, Save
  - ↳ click option -> casts vote, reveals percentages, one choice, changeable until close
  - ↳ after close options disable and winner is bolded
  - ↳ percent bars animate (respect reduce motion)
  - states: empty: poll with 0 votes shows 0% bars and 'Be first'; loading: skeleton; error: vote failure reverts; selected: chosen option has outline plus chip-selected fill
  - data: options, percentages, 212 votes, time left = sample (TBD poll_options, poll_votes)
  - data: percent totals = real (computed from votes; must sum to 100)
- **10. Online now** (`online-now`, AvatarStack + PresenceDot, right) · social
  - Heading with green PresenceDot
  - AvatarStack of 4 members plus '+41'
  - ↳ hover/focus avatar -> name tooltip
  - ↳ click avatar -> member profile
  - ↳ click +N -> list of online members
  - states: empty: 'No one online' text, dot grey; loading: skeleton avatars; error: module hidden; selected: n/a
  - data: online members and count 45 = sample (TBD presence channel (Supabase Realtime presence))
- **11. Trending** (`trending`, ThreadList, right) · social
  - Heading with trend icon
  - Numbered list of 4 items: title, Space, N comments
  - ↳ click item -> thread view
  - states: empty: 'Nothing trending yet' EmptyState; loading: 4 skeleton rows; error: module hidden; selected: n/a
  - data: 4 trending threads = sample (TBD ranking over last 24-48h activity)
- **12. Community rules** (`rules`, Checklist (static card, dashed border), right)
  - Heading 'Rules'
  - 3 numbered rules: Be useful. Be kind. / No contact details from rosters. / Label ads and referrals.
  - ↳ none; 'Report' action on every post links back to rules
  - states: empty: n/a; loading: static; error: static; selected: n/a
  - data: rules text = real (static copy; confirm with owner)
- **13. Feed empty, loading, end, error** (`feed-states`, EmptyState + LoadingSkeleton + ErrorState, main)
  - Empty Space message with 'Start a post' prompt
  - Skeleton post cards
  - 'You're all caught up' end marker or infinite load more
  - Error with Retry
  - ↳ scroll to bottom -> loads next page
  - ↳ Retry reloads
  - states: empty: per-Space empty copy; loading: 3 skeleton cards; error: ErrorState inline; selected: n/a
  - data: pagination cursor = real (TBD API)

### Features
- Switch between Spaces (All spaces, Contracts, Grants, Certifications, VetCert help, Teaming, Wins, Off-duty)
- Create a new Space
- Sort feed Hot / New / Top
- Create a post, question, poll or win from one composer
- Link a contract or grant into a post (embed card)
- Upvote and downvote posts and answers
- Mark an accepted answer on a question
- Vote in a poll with live percentages and a countdown
- Congrats reaction on win posts
- Comment count opens thread view; teaming count opens the team room
- Share and Save any post
- See who is online and trending threads
- Rules card; no roster contact details in posts
- Urgency colour on due/close chips (R5)
- Runtime theming via tokens only

### v2 changes vs comp
- Win post: white --fm-surface card, amber only on the 'Win' badge and trophy; remove the amber card fill and 2px gold border (page note)
- Contract and grant embed cards match list-row styling (same radius, border, type scale, urgency chip) instead of a grey inset block
- Post button becomes amber --fm-action (R2); comp used navy
- Sort tabs and Spaces selection: Hot tab uses chip-selected style (R7); current Space row uses cream row-selected plus bold (R1), not navy fill
- Remove the 'People > Community' breadcrumb (R4, top-level page); page starts with H1
- Due chips coloured by days left: 'Due tomorrow' and 'Closes Nov 3' red when <=48h, warn when <=7d (R5); comp used amber for all
- Meta text (byline, embed sub-line, trending meta, counts) raised from 12px to 13px minimum (R8); 12px only for badges
- Space counts (8.4k etc) hidden until real, labelled with unit (R9)
- Online dot uses --fm-success, not a hard-coded green
- SampleChip elements removed from shipped build

### Acceptance
1. Given the page loads, When the header renders, Then the Community pill is the only navy pill and has aria-current=page, and all 8 nav pills are visible without an 'All tools' menu
2. Given the page loads, When I look above the H1, Then no breadcrumb is rendered
3. Given I click a Space row, When the feed reloads, Then the H1 shows the Space name, the URL has ?space=<slug>, and the row has cream background and bold text, not navy
4. Given the Spaces list has real data, When counts are unknown, Then no count is rendered (no '0', '[count]' or placeholder number)
5. Given I click New space, When the dialog opens, Then I can enter name and description and creating it adds a row and selects it
6. Given I click Hot, New or Top, When the feed re-sorts, Then the selected tab uses chip-selected styling and aria-pressed=true on exactly one tab
7. Given the composer is empty, When I view Post, Then it is amber and disabled; When I type text, Then it enables; this is the only amber button in the main column
8. Given I click Link a contract, When I pick a notice, Then an embed card appears in the draft and publishes with the post
9. Given I click Start a poll, When I add 2 to 6 options and Post, Then a poll post appears with 0% bars
10. Given a post, When I click Upvote then Upvote again, Then my vote toggles and score changes by 1 then back, and the control has aria-pressed
11. Given a vote request fails, When the response errors, Then the score reverts and a toast explains
12. Given a contract post due in 24h, When it renders, Then the due chip is --fm-urgent colour; due in 5 days is --fm-warn; later is neutral
13. Given a contract embed card, When I click it, Then I land on /os#/opportunities?notice=<id> and the card visually matches a list row
14. Given the Win post, When it renders, Then the card background is --fm-surface, the Win badge is the only amber/green accent, and Congrats toggles +1
15. Given a Question with an accepted answer, When it renders, Then the answer block uses --fm-success-soft, shows the 'Accepted answer' badge, and the answerer CertBadge shows only if their cert is verified
16. Given a grant embed with no ceiling in data, When it renders, Then no ceiling chip is shown
17. Given an open poll, When I click an option, Then percentages appear, my option is marked, and I can change it until close; After close all options are disabled
18. Given Share is clicked, When confirmed, Then a link to the post is copied and a toast confirms; Save toggles the bookmark and the post appears under Saved
19. Given Online now, When presence has N users, Then up to 4 avatars plus '+N-4' show with accessible names; when 0 online, the empty text shows
20. Given Trending, When data loads, Then 4 ranked items link to threads; with no data the EmptyState shows
21. Given the Rules card, When viewed, Then the 3 rules are present and not collapsible
22. Given the feed is loading, When data is pending, Then 3 skeleton cards show; on error an ErrorState with Retry shows and Retry reloads
23. Given any text on the page, When measured, Then non-badge text is at least 13px and contrast is at least 4.5:1 in default and one alternate theme
24. Given I post content containing a roster email or phone, When submitted, Then it is blocked or flagged per the rule 'No contact details from rosters'

### Responsive
<=1024: Spaces rail becomes a horizontal scrolling row of FilterChips above the feed; right rail stacks below the feed (Trending first, then Online now, Rules). <=480: single column, BottomTabBar replaces PillNav, composer collapses to a FAB that opens a full-screen Composer, vote rail stays at left of each card, action row wraps, tap targets >=44px, bottom padding equals FAB height so it never covers the last card's actions.

### Accessibility
- Landmarks: header, nav 'Main', nav 'Spaces', main, aside 'Community highlights'
- H1 is the Space name; each post title is an h2
- Vote buttons have aria-labels and aria-pressed; score announced via aria-live=polite
- Sort tabs are a role=group with aria-pressed buttons, or tablist with arrow keys
- Poll options are radio-like with aria-checked and result percentages in text, not colour only
- AvatarStacks have role=img with full names in aria-label
- Urgency conveyed by text plus colour
- Visible focus ring --fm-focus-ring on all controls; tab order: nav, Spaces, sort, composer, feed, right rail
- Respect prefers-reduced-motion for bar animation

### Open questions
- ACCURACY/LEGAL CHECK REQUIRED: the sample accepted answer ('Not as the certified firm until SBA approves. Bid open work now, or sub to a certified prime.') on bidding veteran set-asides while VetCert is in review has not been verified against SBA/FAR rules and must be reviewed for accuracy and legal correctness before any real use; the shipped build must not seed or display it as guidance, and an 'Accepted answer' on VetCert/legal topics should carry a 'Community answer, not legal advice' note
- Who may mark an accepted answer: asker only, or moderators too
- Moderation model: reporting, removal, roles, spam control, who can create Spaces
- How are the Space counts defined (members, posts, or active) and are they shown at all
- Do role/cert badges on authors come only from verified profile data
- Is downvoting allowed in all Spaces, and does it affect Hot ranking
- Are the Wins and Off-duty Spaces visible to non-veterans
- Rule enforcement for 'No contact details from rosters' (auto-detect vs report only)
- Does 'N teaming' count equal team-room members or distinct teams

## Page: Veterans (`veterans`)
- Route: /os#/veterans · Nav: Veterans · Breadcrumb: —
- Purpose: Show a veteran where they are on the path to winning federal work and let them pick the next move, find a mentor, see peer wins, join a squad and claim 3 months of Pro.
- Primary action: Continue to VetCert — top-right of the Your path section (label follows the current step)
- Screen: https://fedm8.com/design/003/screens/09-veterans.png · Comp: https://fedm8.com/design/003/comps/09-veterans.dc.txt

### Eye path
1 H1 'Level up' (no breadcrumb, R4: Veterans is top-level and pinned in the nav)
2 'Your path' ChevronStepper directly under the H1 (Profile, SAM, VetCert, First bid, First win) with '2 of 5 done' and progress bar; this is the where-am-I element
3 Choose your next move: 4 ChoiceCards. 'Get certified' has an amber 'next up' ring, not navy selected (R1); label first, count second (R6)
4 Mentors then Wins wall in the main column
5 Right rail: Squad, then Veterans Pro offer (outline button; amber is reserved for the one primary)

### Modules
- **1. App header and PillNav** (`app-header`, AppHeader, header)
  - Logo
  - PillNav with Veterans as navy active pill
  - Notifications IconButton
  - Messages IconButton with unread dot
  - WorkspaceSwitcher 'FEDM8 LLC'
  - Avatar
  - ↳ same as global header
  - states: empty: no unread: dot hidden; loading: avatar skeleton; error: header renders without badges; selected: Veterans pill navy, aria-current=page
  - data: nav items, workspace, avatar, unread flag = real (app config, auth session, messages unread query)
- **2. Level up title** (`page-title`, PageTitle, main)
  - H1 'Level up'
  - SampleChip 'Sample members' (comp only)
  - ↳ none
  - states: empty: n/a; loading: static text; error: static text; selected: n/a
  - data: title text = real (static copy)
- **3. Your path** (`your-path`, ChevronStepper + ProgressCard, main)
  - Heading 'Your path'
  - Progress text '2 of 5 done'
  - Progress bar
  - 5 chevron segments: Profile, SAM, VetCert (current, 'In review' tag), First bid, First win
  - Done segments with check, future segments muted
  - PrimaryButton 'Continue to VetCert' (v2)
  - ↳ click a done step -> opens that step's page (Profile, SAM registration)
  - ↳ click current step -> opens the certification walk-through
  - ↳ future steps are focusable but show what unlocks them
  - ↳ primary button jumps to the current step's task
  - states: empty: new user: 0 of 5 done, first step current; loading: skeleton segments; error: ErrorState with Retry; selected: current step uses accent/navy as where-you-are (R1) with aria-current=step
  - data: Profile done, SAM done = real (profile completeness and SAM registration status of the workspace; TBD fn)
  - data: VetCert 'In review' = real (FEDM8 LLC SDVOSB is In review with SBA VetCert (owner decision); never show certified)
  - data: First bid, First win status = real (pipeline stages (Submitted, Won) from My pipeline)
- **4. Choose your next move** (`next-move`, ChoiceCard x4 + AvatarStack, main) · social
  - Heading (visually part of section)
  - Card 1 'Get certified' - 'VetCert in review' - 'Next up' badge - step number 1 - avatars DR KA +38
  - Card 2 'Find a mentor' - 'Matched to you' - number 2 - avatars DR KA
  - Card 3 'Win your first contract' - 'Open notices now' - count 7,829 - avatars LG MT +16
  - Card 4 'Join a team' - 'On the DHRA notice' - count 13 - avatars PS LG +11
  - Icon/picture area on each card
  - ↳ click Get certified -> /os#/certifications (SDVOSB walk-through)
  - ↳ click Find a mentor -> scrolls to Mentors
  - ↳ click Win your first contract -> Contracts list filtered to set-asides the user qualifies for
  - ↳ click Join a team -> Teaming room for the DHRA notice
  - ↳ hover/focus avatar stack -> names
  - states: empty: avatar stack hidden when no peers; loading: card skeletons; error: cards render without counts; selected: navigating cards have no persistent selected state; 'next up' card has amber ring
  - data: card labels and sublabels = real (static copy; 'On the DHRA notice' must be dynamic (top teaming notice) not hard-coded)
  - data: 7,829 open notices = sample (count of active notices from the opportunities table; show only real number with unit word 'notices')
  - data: 13 on DHRA team = sample (team room member count for the notice; unit 'people')
  - data: '1' and '2' order numbers = real (recommended order, not counts; render as step badge, not stat)
  - data: avatar stacks (38, 16, 11 more) = sample (TBD members following each goal)
- **5. Mentors** (`mentors`, MentorCard x2, main) · social
  - Heading 'Mentors' with 'See all' link
  - Card: Avatar with PresenceDot, name, 'Army vet · Cloud ops', CertBadge SDVOSB, 'Replies in 1 day', 'Ask to connect' button
  - Card 2: Kim A., Air Force PM, VOSB, 'Replies in 2 days'
  - ↳ Ask to connect -> opens message composer pre-filled; sets button to 'Requested'
  - ↳ click name/avatar -> member profile
  - ↳ See all -> mentor directory with filters (branch, skill, cert)
  - states: empty: 'No mentors matched yet' with link to Teaming network; loading: 2 skeleton cards; error: ErrorState inline; selected: after request: 'Requested' disabled
  - data: mentor profiles (Dana R., Kim A.) = sample (TBD mentors flag on member profiles)
  - data: reply time 'Replies in N days' = sample (TBD computed median response time; hide if under 3 data points)
  - data: cert badges = sample (show only if verified)
  - data: online presence dot = placeholder (TBD presence)
- **6. Wins wall** (`wins-wall`, WinCard x3, main) · social
  - Heading 'Wins wall' with SampleChip and 'Share a win' link
  - 3 WinCards: icon, title (First sub role landed / VOSB approved / First bid submitted), author with branch, vote count (42 / 67 / 29), comment count (8 / 15 / 5)
  - ↳ click card -> win post in Community (Wins Space)
  - ↳ Share a win -> Composer pre-set to Win
  - ↳ vote/comment counts link to the thread
  - states: empty: 'Be the first to share a win' EmptyState with Share a win; loading: 3 skeleton cards; error: ErrorState; selected: n/a
  - data: 3 sample wins = sample (TBD; real source = Wins Space posts tagged win, opt-in to appear on wall)
  - data: vote and comment counts = sample (community posts counts)
- **7. Squad** (`squad`, SquadCard + TrackingBar, right) · social
  - Icon and title 'MD/DC/VA vets in IT' with 'Squad' label
  - AvatarStack of 5
  - '214 members'
  - '12 online' with PresenceDot
  - Next meetup row with date
  - 'Join squad' button (outline)
  - ↳ Join squad -> adds user, button becomes 'Joined', opens squad chat
  - ↳ click title -> squad page / chat
  - ↳ meetup row -> add to calendar
  - states: empty: no meetup: row hidden; loading: skeleton; error: ErrorState; selected: joined state shows 'Open squad chat'
  - data: squad name and region = sample (TBD squads table matched by location and industry)
  - data: 214 members, 12 online = sample (squad_members count; presence)
  - data: Next meetup [date] = placeholder (TBD; comp shows literal '[date]', must be hidden or real)
- **8. Veterans Pro offer** (`pro-offer`, PlanCard + ProTag, right)
  - Pro icon and ProTag, title '3 months Pro free'
  - 'For veterans' label
  - Two feature chips: AI proposal writer, Contact rosters
  - 'Claim 3 months' button (outline)
  - ↳ Claim 3 months -> starts trial, no card required (confirm), card changes to 'Trial: N days left'
  - ↳ feature chips link to the feature explainer
  - ↳ hidden once Pro is active and not on trial
  - states: empty: n/a; loading: skeleton; error: claim failure toast with retry; selected: claimed: trial countdown
  - data: 3 months Pro free for veterans = real (owner decision; entitlement table / plan, TBD edge fn to grant)
  - data: veteran eligibility = real (veteran status on profile; verification method TBD)
- **9. Page empty, loading, error** (`page-states`, EmptyState + LoadingSkeleton + ErrorState, main)
  - New-user state (0 of 5 done)
  - Section skeletons
  - Per-section error with Retry
  - ↳ Retry reloads the failed section only
  - states: empty: path 0 of 5; loading: skeletons; error: section-level; selected: n/a
  - data: all = real (composed from modules above)

### Features
- Five-step veteran path with progress and current step
- Four next-move cards with peer avatars and counts
- Recommended next move highlighted with an amber ring
- Mentor matching and Ask to connect
- Wins wall and Share a win
- Join a regional squad and see online members
- Claim 3 months of Pro free (veterans)
- Veterans, Learn and Community always visible in the main nav (never under All tools)
- SDVOSB shown as In review for FEDM8 LLC, never certified

### v2 changes vs comp
- Move 'Your path' ChevronStepper directly under the H1, above the choice cards (page note)
- 'Get certified' card: amber --fm-action ring and 'Next up' badge for next-up; remove navy selected fill (R1: navy only for user-selected)
- Remove 'Grow > Veterans' breadcrumb (R4)
- Choice cards: label first (type-xl), count second (type-md muted with unit word) (R6); '7,829' and '13' get unit words 'notices' and 'people'
- Exactly one primary: 'Continue to VetCert'; 'Join squad', 'Ask to connect' and 'Claim 3 months' are outline (R2)
- Squad 'Next meetup [date]' must be hidden when no real date (R9)
- Meta text (mentor subtitle, reply time, win author) at least 13px (R8)
- Remove SampleChips from shipped build

### Acceptance
1. Given the page loads, When the header renders, Then Veterans is the navy active pill with aria-current=page and visible without opening any menu
2. Given the page loads, When I look above the H1, Then no breadcrumb is rendered, and the H1 reads 'Level up'
3. Given the page renders, When I read top to bottom, Then the order is H1, Your path, Choose your next move, Mentors/Wins wall, with Squad and Pro in the right rail
4. Given the user has done Profile and SAM, When Your path renders, Then it reads '2 of 5 done', the bar is 40%, two segments show done, VetCert is current with the 'In review' tag, and the last two are muted
5. Given FEDM8 LLC SDVOSB is in review, When any cert state appears on this page, Then it says 'In review' and never 'Certified'
6. Given I click a done step, When it opens, Then I land on that step's page; clicking the current step opens the certification walk-through
7. Given the page loads, When I look for amber buttons, Then only 'Continue to VetCert' is amber (R2)
8. Given the four ChoiceCards, When they render, Then each shows label first and count second with a unit word, and Get certified has an amber ring plus 'Next up' with no navy fill
9. Given I click 'Get certified', When it navigates, Then I arrive at the SDVOSB walk-through; 'Find a mentor' scrolls to Mentors; 'Join a team' opens the DHRA team room
10. Given the open-notice count is unknown, When the card renders, Then no number is shown (no '[count]' or 0)
11. Given Mentors has 2 matches, When I click 'Ask to connect', Then a request is created and the button changes to disabled 'Requested'; with 0 matches the EmptyState shows
12. Given a mentor card, When it renders, Then cert badge shows only for verified certs and reply time is hidden if there is not enough data
13. Given the Wins wall, When it has 0 wins, Then EmptyState 'Be the first to share a win' shows; 'Share a win' opens the Composer in Win mode
14. Given Squad, When I click 'Join squad', Then the button becomes 'Joined' and the squad member count increments by 1
15. Given no meetup date exists, When Squad renders, Then the Next meetup row is not shown
16. Given the user is a veteran without Pro, When I click 'Claim 3 months', Then Pro is granted for 3 months and the card shows days left; a non-veteran does not see the card
17. Given Pro is already active, When the page renders, Then the Pro offer card is hidden
18. Given a section request fails, When it errors, Then only that section shows ErrorState with Retry
19. Given text on the page, When measured, Then non-badge text is at least 13px with contrast at least 4.5:1

### Responsive
<=1024: right rail (Squad, Pro) stacks below Wins wall; ChoiceCards go 2x2; ChevronStepper keeps all 5 segments with short labels. <=480: single column; ChevronStepper becomes a compact 'Step 3 of 5: VetCert' bar with a scrollable segment row; ChoiceCards stack full width; BottomTabBar replaces PillNav; the primary button becomes full width under the stepper.

### Accessibility
- Landmarks: header, nav 'Main', main with sections labelled 'Your path', 'Choose your next move', 'Mentors', 'Wins wall'; aside 'Squad and Pro'
- Stepper is an ol with aria-current=step on the current item; done/future conveyed in text for screen readers
- ChoiceCards are links or buttons with accessible names including label and count
- AvatarStacks have role=img with names
- Progress bar has role=progressbar with aria-valuenow
- Focus ring on all controls; focus order follows eye path
- Colour is never the only indicator for next-up (badge text present)

### Open questions
- How is veteran status verified to unlock the 3 months Pro, and is a card required
- What event ends the trial and what happens to data and rosters at trial end
- How are mentors recruited and approved; is there a safety/vetting process
- How are squads formed (location plus industry) and who can create one
- What is the 'First win' definition for the path step (awarded contract entered in pipeline?)
- Whether Wins wall entries are opt-in
- Should steps be reorderable or is the 5-step path fixed
- How is 'Next up' chosen (rules or AI)

## Page: Certifications walk-through (`certifications`)
- Route: /os#/certifications/<track>?step=<n>&q=<n> · Nav: Certifications · Breadcrumb: Grow ▸ Certifications ▸ SDVOSB
- Purpose: Walk the user through one certification track (here SDVOSB), one question per screen, saving answers, so they know whether and how to apply.
- Primary action: Continue — bottom-right of the question card (wizard rule R2); disabled until an answer is chosen
- Screen: https://fedm8.com/design/003/screens/10-certs.png · Comp: https://fedm8.com/design/003/comps/10-certs.dc.txt

### Eye path
1 ChevronBreadcrumb (depth 3: Grow > Certifications > SDVOSB, R4)
2 ChevronStepper of the 6 steps (Eligibility, Ownership, Control, Documents, Review, Submit) with current step
3 Track chip 'SDVOSB · change' (the picker collapses once a track is chosen)
4 Single QuestionCard: 'Question 1 of 3', H1 question, Yes/No ChoiceCards, 'Why we ask'
5 Back / Saved automatically / Continue (amber, bottom-right)
6 'Ask' drawer (collapsed by default) for community help, opened on demand

### Modules
- **1. App header and PillNav** (`app-header`, AppHeader, header)
  - Logo
  - PillNav with Certifications as navy active pill
  - Notifications
  - Messages with unread dot
  - WorkspaceSwitcher 'FEDM8 LLC'
  - Avatar
  - ↳ same as global header
  - states: empty: no unread: dot hidden; loading: avatar skeleton; error: header without badges; selected: Certifications pill navy
  - data: nav, workspace, avatar, unread = real (app config, auth, messages)
- **2. Breadcrumb** (`breadcrumb`, ChevronBreadcrumb, header)
  - Grow (link)
  - Certifications (link)
  - SDVOSB (current)
  - ↳ click Grow -> Veterans/Grow hub
  - ↳ click Certifications -> track list/overview
  - ↳ current segment is not a link
  - states: empty: n/a; loading: static; error: static; selected: current segment navy (where-you-are)
  - data: path labels = real (route)
- **3. Certification steps** (`step-bar`, ChevronStepper, main)
  - 6 segments: Eligibility, 2 Ownership (current), 3 Control, 4 Documents, 5 Review, 6 Submit
  - done / current / upcoming styles
  - ↳ click a completed step -> jumps back to it
  - ↳ upcoming steps not clickable until reached (aria-disabled)
  - ↳ advancing via Continue updates it
  - states: empty: first step current; loading: skeleton segments; error: static labels; selected: current step navy, aria-current=step
  - data: step list for SDVOSB = placeholder (TBD cert_tracks schema; step count and names differ per track)
  - data: completed steps = real (cert checklist progress table (site/cert-checklist-schema.sql))
- **4. Track picker** (`track-picker`, TrackPicker (ChoiceCard x5) collapsing to chip, left) · social
  - Heading 'Track' with SampleChip
  - 5 ChoiceCards: SDVOSB (In progress, DR +22), VOSB (SBA VetCert, KA +31), 8(a) (SBA program, LG +9), HUBZone (SBA program, MT +5), WOSB (Incl. EDWOSB, PS +12)
  - After choice: collapsed chip 'SDVOSB · change'
  - ↳ click a track -> selects it (navy selected card, R1) then collapses to the chip
  - ↳ click 'change' -> expands picker; switching track keeps saved answers per track
  - ↳ avatars show people on that track
  - states: empty: no track chosen: picker expanded, no question shown, prompt 'Pick a track'; loading: 5 skeleton cards; error: ErrorState; chip still usable; selected: selected card navy; collapsed after choice
  - data: track list (SDVOSB, VOSB, 8(a), HUBZone, WOSB incl. EDWOSB) = real (static cert catalog)
  - data: sublabels 'In progress' / 'SBA VetCert' / 'SBA program' = real (In progress derived from user's saved progress; VetCert is the SBA program for SDVOSB/VOSB)
  - data: peer counts 22, 31, 9, 5, 12 = sample (TBD members per track; label with unit 'members' (R6/R9))
- **5. Question** (`question-card`, QuestionCard, main)
  - Counter 'Question 1 of 3' and section tag 'Ownership'
  - H1 'Do service-disabled veterans own at least 51% of the company?'
  - Yes / No ChoiceCards with icons
  - 'Why we ask' link
  - Footer: Back (outline), 'Saved automatically' text, Continue (amber, disabled until answered)
  - ↳ select Yes/No -> card navy (selected), Continue enables, answer autosaves
  - ↳ Continue -> next question; after last question of a step -> next step
  - ↳ Back -> previous question, answer retained
  - ↳ Why we ask -> inline explainer or drawer with the rule and source link
  - ↳ keyboard: Y/N or arrows, Enter to continue
  - ↳ answers that make the user ineligible show an inline notice with next options, not a hard stop
  - states: empty: no answer, Continue disabled; loading: skeleton; Continue disabled; error: 'Not saved. Retry' next to Saved automatically; selected: chosen answer navy
  - data: question text and explainer = placeholder (TBD; must be reviewed against SBA 13 CFR Part 128 / VetCert requirements before shipping)
  - data: Question 1 of 3 counter = real (computed from step's question list)
  - data: answers = real (cert_checklist answers table (site/cert-checklist-schema.sql))
  - data: Saved automatically indicator = real (save status from API call)
- **6. Ask the community** (`ask-drawer`, Drawer + ThreadList + Composer, drawer) · social
  - Trigger button 'Ask' with icon
  - Heading 'Ask the community'
  - AvatarStack (DR KA LG), '23 on SDVOSB', '4 online'
  - SampleChip
  - Thread 1: VoteRail 14, Dana R. Army vet 3h, 'Does ownership through a holding company still count toward the 51%?', Reply, '3 replies', nested reply from Kim A. 2h with 5 votes
  - Thread 2: VoteRail 9, Luis G. Navy vet 1d, 'How long did your review take after you submitted?', Reply, '6 replies'
  - Composer 'Ask…' input and Post button
  - ↳ collapsed by default; opens as right-hand drawer over the page (does not shrink the question)
  - ↳ vote, Reply, nested thread expand
  - ↳ Post -> adds a question tagged to the SDVOSB track and the current step
  - ↳ close with Esc or X, focus returns to trigger
  - ↳ thread list is filtered to the current track and step
  - states: empty: 'No questions on this step yet. Ask one.'; loading: skeleton threads; error: ErrorState inline; selected: open/closed
  - data: threads, replies, votes (14, 9, 3, 6, 5) = sample (TBD community_posts filtered by track tag; shares backend with Community)
  - data: 23 on SDVOSB, 4 online = sample (track member count; presence)
  - data: author branch labels = sample (profile fields)
- **7. Page empty, loading, error** (`page-states`, EmptyState + LoadingSkeleton + ErrorState, main)
  - No track chosen state
  - Completed state after last step ('Ready to submit' summary)
  - Load/save error with Retry
  - ↳ Retry reloads or re-saves
  - states: empty: no track; loading: skeleton; error: retry; selected: n/a
  - data: all = real (composed)

### Features
- Pick a certification track (SDVOSB, VOSB, 8(a), HUBZone, WOSB/EDWOSB)
- Picker collapses to a chip 'SDVOSB · change' after choice
- Six-step chevron progress with jump back to done steps
- One question per screen with Yes/No answers
- Why we ask explainer
- Back and Continue navigation; Continue disabled until answered
- Autosave with visible status
- Community Ask drawer with votes, replies and a composer, scoped to track and step
- Peers on the same track and who is online
- FEDM8 LLC SDVOSB shown as In review, never certified

### v2 changes vs comp
- After a track is chosen, collapse the picker to one chip 'SDVOSB · change' (page note); the left column no longer competes with the question
- Community panel becomes a collapsible 'Ask' drawer, closed by default (page note)
- Continue is the single amber primary at bottom-right (R2); Back is outline
- Selected track and selected Yes/No are the only navy fills (R1); the 'SDVOSB' chip uses chip-selected style (R7)
- Track cards: label first, count second with unit word 'members' (R6); no placeholder counts (R9)
- Meta text (author line, replies, vote counts, sublabels) minimum 13px (R8)
- Breadcrumb kept since depth is 3 (R4); 'Grow' must link to a real page
- Remove SampleChips from shipped build

### Acceptance
1. Given I open /os#/certifications/sdvosb, When the page loads, Then Certifications is the navy nav pill and the breadcrumb reads Grow > Certifications > SDVOSB with SDVOSB as the current non-link segment
2. Given the stepper, When it renders, Then 6 segments show, the current one has aria-current=step and navy fill, completed ones are clickable and upcoming ones are aria-disabled
3. Given I click a completed step, When it opens, Then my saved answers are shown
4. Given no track is selected, When the page loads, Then the 5-card picker is expanded and no question shows; When I click SDVOSB, Then it collapses to the chip 'SDVOSB · change' and the first question shows
5. Given the chip, When I click 'change' and choose VOSB, Then the stepper and questions switch to VOSB and SDVOSB answers remain saved
6. Given track cards, When they render, Then label is first and the peer count has a unit word; with unknown counts the number is hidden
7. Given the question screen, When it renders, Then only one question is visible with 'Question N of M' and the section tag
8. Given no answer is selected, When I look at Continue, Then it is amber and disabled; When I choose Yes or No, Then the card turns navy, Continue enables and 'Saved automatically' changes to 'Saved' within 2 seconds
9. Given I click Continue, When the answer saved, Then the next question loads and Back returns with my answer retained
10. Given a save fails, When the request errors, Then the indicator reads 'Not saved. Retry', Continue stays enabled and Retry resubmits
11. Given I click 'Why we ask', When it opens, Then it shows the explanation and a source link, and Esc closes it
12. Given the answer implies ineligibility, When I choose it, Then an inline notice shows alternatives and I can still continue
13. Given the page loads, When I check the right side, Then the Ask drawer is closed and only an 'Ask' trigger is visible; When I open it, Then it overlays without moving the question card
14. Given the Ask drawer, When it opens, Then it shows threads for the SDVOSB track, vote and Reply work, and Post adds a question tagged with track and step
15. Given I press Esc in the drawer, When it closes, Then focus returns to the Ask trigger
16. Given no threads exist, When the drawer opens, Then the EmptyState prompts to ask the first question
17. Given FEDM8 LLC's SDVOSB is in review, When any status is displayed, Then it says 'In review' and never 'Certified'
18. Given text on the page, When measured, Then non-badge text is at least 13px and contrast at least 4.5:1
19. Given I am on the last step, When I complete it, Then a summary shows and the single amber button reads 'Submit'

### Responsive
<=1024: breadcrumb and stepper stay at top; the track picker is already collapsed to the chip; the Ask drawer slides over from the right at 60% width. <=480: stepper shows 'Step 2 of 6: Ownership' with a scrollable segment row; question card full width; Yes/No cards stack; Back and Continue stay in a sticky bottom bar with Continue on the right; Ask becomes a bottom sheet; BottomTabBar hidden during the wizard to avoid two bottom bars.

### Accessibility
- One question per screen; H1 is the question; focus moves to the H1 on step change and the change is announced via aria-live
- Yes/No are radio-style buttons with aria-pressed or role=radio
- Stepper is an ol with aria-current=step; disabled steps aria-disabled
- Saved status in an aria-live=polite region
- Drawer is role=complementary dialog with focus trap, Esc to close, focus return to the trigger
- Breadcrumb nav with aria-label and aria-current=page
- Visible focus ring; Continue disabled state explained via aria-describedby

### Open questions
- Exact question set for each track and step (the comp shows only SDVOSB Question 1 of 3); needs review against SBA VetCert rules before use
- Does the walk-through only guide, or does it also submit/store documents with SBA
- Document upload requirements and storage for the Documents step
- How are ineligible answers handled (block, warn, route to another track)
- Is 'Eligibility' step numbered (comp shows it unnumbered)
- Should community threads in the drawer be moderated for advice accuracy on legal questions
- How many tracks can one user work on in parallel
- How is 'In progress' on the SDVOSB card derived when FEDM8 LLC already has an application In review

## Page: Learn (`learn`)
- Route: /os#/learn?goal=<slug>&lesson=<n> · Nav: Learn · Breadcrumb: —
- Purpose: Pick a learning goal, resume the current course, browse courses and discuss the current lesson with peers.
- Primary action: Resume — top-right of the Continue card
- Screen: https://fedm8.com/design/003/screens/11-learn.png · Comp: https://fedm8.com/design/003/comps/11-learn.dc.txt

### Eye path
1 H1 'What's your goal?' (no breadcrumb, R4: Learn is top-level)
2 Four goal ChoiceCards: label first, count with unit 'lessons' second (R6); the in-progress goal has a light tint, not navy (page note)
3 Continue card: the one highlighted object on the page, with amber Resume at top-right (R2)
4 Lesson list inside Continue card; current lesson has a light cream tint + bold (R1), not navy
5 Courses list (sortable), then right rail Lesson discussion

### Modules
- **1. App header and PillNav** (`app-header`, AppHeader, header)
  - Logo
  - PillNav with Learn as navy active pill
  - Notifications
  - Messages with unread dot
  - WorkspaceSwitcher 'FEDM8 LLC'
  - Avatar
  - ↳ same as global header
  - states: empty: no unread: dot hidden; loading: avatar skeleton; error: header without badges; selected: Learn pill navy
  - data: nav, workspace, avatar, unread = real (app config, auth, messages)
- **2. What's your goal** (`goal-picker`, ChoiceCard x4 + AvatarStack, main) · social
  - H1 "What's your goal?"
  - SampleChip (comp only)
  - Card 'First contract' - 8 lessons - MT DR +40
  - Card 'Teaming 101' - 6 lessons - 'In progress' tag - LG PS +27
  - Card 'Pricing a bid' - 7 lessons - KA DR +18
  - Card 'Capability statement' - 4 lessons - MT LG +33
  - Picture/icon area per card
  - ↳ click a goal -> filters Courses and sets Continue to the first unfinished lesson of that goal's course
  - ↳ selected goal is the user-chosen one (navy per R1 only for user selection)
  - ↳ in-progress goal shows light tint and tag
  - ↳ avatars show learners
  - states: empty: no goal chosen: Continue card hidden, prompt in its place; loading: 4 skeleton cards; error: cards render without avatars; selected: navy on user-selected card; in-progress uses --fm-row-selected-bg tint
  - data: goals and lesson counts 8, 6, 7, 4 = sample (TBD courses/lessons tables; counts must equal real lesson counts and match the Courses list)
  - data: learner avatars (40, 27, 18, 33 more) = sample (TBD course_enrollments)
  - data: In progress state = real (lesson_progress per user)
- **3. Continue** (`continue-card`, ProgressCard + LessonList, main) · social
  - Icon, label 'Continue', H2 'Teaming 101', 'Lesson 3 · Find a prime'
  - PrimaryButton 'Resume' (amber)
  - Progress bar with '2 of 6'
  - LessonList (4 shown): 1 Why team up (5 min, 12 comments, done), 2 Prime or sub (6 min, 8, done), 3 Find a prime (6 min, 5, current), 4 Write the pitch (7 min, 3)
  - Per-lesson duration and comment count
  - ↳ Resume -> opens the current lesson
  - ↳ click any lesson -> opens it (done lessons replayable; locked lessons not applicable unless sequential rule set)
  - ↳ click comment count -> opens that lesson's discussion in the right rail
  - ↳ list shows 4 lessons with a 'Show all 6' control
  - states: empty: not started: label 'Start', 0 of N; loading: skeleton; error: ErrorState; list still shows; selected: current lesson light cream tint + bold, aria-current=step
  - data: course title, lesson 3 title, progress 2 of 6 = sample (TBD lesson_progress)
  - data: lesson titles and durations = sample (TBD lessons table; list currently shows 4 of 6)
  - data: per-lesson comment counts 12, 8, 5, 3 = sample (lesson comment counts)
  - data: done/current status = real (lesson_progress)
- **4. Courses** (`courses`, CourseRow list + SortTabs, main) · social
  - Heading 'Courses', SampleChip, SortTabs Hot/New/Top
  - 5 CourseRows: Your first federal contract (8 lessons · 42 min, MT DR +40, 46), Teaming 101 (6 · 34 min, LG PS +27, 28), Pricing a bid (7 · 48 min, KA DR +18, 19), Capability statement (4 · 22 min, MT LG +33, 31), Read a Sources Sought (5 · 25 min, DR PS +14, 12)
  - Each row: icon, title, lesson count and duration, learner avatars, discussion count
  - ↳ sort tab re-orders rows
  - ↳ click row -> course page / first unfinished lesson
  - ↳ click discussion count -> course discussion
  - ↳ filtered by the chosen goal
  - states: empty: 'No courses yet'; loading: 5 skeleton rows; error: ErrorState with Retry; selected: selected sort uses chip-selected style (R7); in-progress course has tint
  - data: 5 courses with lesson counts and minutes = sample (TBD courses table; content must be authored, not invented; 'Read a Sources Sought' has no matching goal card)
  - data: learner avatars and the trailing numbers (46, 28, 19, 31, 12) = sample (TBD; the trailing number is unlabelled in the comp (comments or learners) and must carry a unit)
- **5. Lesson discussion** (`lesson-discussion`, ThreadList + ThreadPost + BotPost + Composer, right) · social
  - Heading 'Lesson 3', '5 comments', SortTabs Hot/New/Top
  - ThreadPost: VoteRail 11, Luis G. with RoleBadge 'Prime', 2h, body 'Start with the sources sought list. Primes read the responses.', Reply, Save, nested reply by Marcus T. 1h with 3 votes
  - BotPost: FedM8 AI with 'Bot' ProTag-style badge, 'Stuck on this lesson? Ask FedM8' link
  - Composer 'Comment on this lesson…' with Post button (outline)
  - ↳ vote, Reply (nested), Save
  - ↳ sort tabs re-order
  - ↳ Ask FedM8 -> opens AI chat scoped to the lesson
  - ↳ Post -> adds comment; thread follows the lesson selected in Continue
  - ↳ changing lesson swaps the thread
  - states: empty: 'No comments yet. Start the discussion.'; loading: skeleton comments; error: ErrorState inline; selected: n/a
  - data: comments, votes, roles = sample (TBD lesson_comments; role badge from team-role on profile)
  - data: 5 comments count = sample (count of comments for lesson)
  - data: FedM8 AI bot response = real (AI edge function (TBD); bot posts clearly labelled)
- **6. Page empty, loading, error** (`page-states`, EmptyState + LoadingSkeleton + ErrorState, main)
  - No goal chosen state
  - Course completed state ('All lessons done' with next suggestion)
  - Section errors with Retry
  - ↳ Retry reloads the section
  - states: empty: no goal; loading: skeleton; error: section-level; selected: n/a
  - data: all = real (composed)

### Features
- Choose a learning goal from four paths
- Resume the in-progress course at the current lesson
- Progress bar and lesson list with durations and per-lesson comment counts
- Browse and sort a course catalogue
- See other learners via avatar stacks
- Reddit-style discussion on each lesson with votes, nested replies and Save
- FedM8 AI helper inside the lesson discussion
- Learn is always visible in the main nav

### v2 changes vs comp
- Single highlight: the Continue card is the one emphasised object; goal card 'Teaming 101' gets a light tint and the current lesson row gets cream --fm-row-selected-bg + bold, no navy (page note, R1)
- Resume is the single amber primary at the card's top-right (R2); Post in the discussion is outline
- Remove 'Grow > Learn' breadcrumb (R4)
- Goal cards: label first, then count with unit word 'lessons' (R6)
- Sort tabs use chip-selected style (R7)
- Meta (durations, comment counts, role/time lines) minimum 13px (R8)
- Course row trailing number (46, 28...) must be labelled with a unit or hidden (R9)
- Remove SampleChips from shipped build

### Acceptance
1. Given the page loads, When the header renders, Then Learn is the navy active pill with aria-current=page and no breadcrumb is rendered above the H1
2. Given the page loads, When I count navy-filled elements in the main column, Then there are none unless I selected a goal; the Continue card Resume button is the only amber button
3. Given the goal cards, When they render, Then each shows label first then 'N lessons' second, and the in-progress goal shows an 'In progress' tag with a light tint
4. Given I click a goal card, When it applies, Then Courses filter to that goal, the card shows navy selected state, and Continue points to the first unfinished lesson of that goal
5. Given goal lesson counts, When compared with the Courses list, Then the counts match
6. Given I have progress 2 of 6, When Continue renders, Then the label reads 'Lesson 3 · Find a prime', the bar is 33% and the text reads '2 of 6'
7. Given I click Resume, When it opens, Then lesson 3 loads and the discussion rail shows 'Lesson 3'
8. Given the lesson list, When it renders, Then lessons 1 and 2 show done, lesson 3 has cream tint, bold title and aria-current=step, lesson 4 shows 7 min and 3 comments
9. Given a lesson list longer than 4, When it renders, Then a 'Show all N' control appears and expands the rest
10. Given I click a lesson's comment count, When the rail updates, Then it shows that lesson's thread
11. Given Courses, When I click Hot, New or Top, Then rows re-sort and exactly one tab has aria-pressed=true with chip-selected style
12. Given a course row, When it renders, Then title, 'N lessons · M min', learner avatars and a labelled discussion count are shown; clicking opens the course
13. Given there are no courses, When the section renders, Then EmptyState shows
14. Given the discussion thread, When I click Upvote, Then the score increments once and toggles off on second click
15. Given I click Reply on a comment, When I submit, Then the reply nests under it with a thread line
16. Given I type in 'Comment on this lesson…' and click Post, When saved, Then the comment appears at the top under New and the count increments
17. Given the FedM8 AI row, When I click 'Ask FedM8', Then an AI chat opens scoped to Lesson 3 and the AI messages are labelled 'Bot'
18. Given no comments exist, When the rail renders, Then 'No comments yet' EmptyState shows and the composer is focused on click
19. Given text on the page, When measured, Then non-badge text is at least 13px with contrast at least 4.5:1
20. Given a section fails to load, When it errors, Then only that section shows ErrorState with Retry

### Responsive
<=1024: Lesson discussion moves below Courses as a collapsible section opened from the Continue card's lesson comment counts; goal cards go 2x2. <=480: goal cards become a horizontal scroll row or stack full width; Continue card full width with Resume full-width under the title; discussion opens as a bottom sheet from the lesson row; BottomTabBar replaces PillNav.

### Accessibility
- Landmarks: header, nav 'Main', main with sections 'Pick a goal', 'Continue', 'Courses'; aside 'Lesson discussion'
- H1 is the goal question; course and lesson titles use h2/h3
- Progress bar has role=progressbar with aria-valuenow and text '2 of 6'
- Lesson list is an ol with aria-current=step on the current lesson; done state also in text for screen readers
- Goal cards use aria-pressed; AvatarStacks role=img with names
- Vote buttons have aria-label and aria-pressed
- Bot messages carry an accessible 'Bot' label
- Visible focus ring; tab order: nav, goals, Resume, lessons, courses, discussion

### Open questions
- Who authors the course content (the sample titles, counts and minutes are placeholders); no course content should be shown that does not exist
- Are lessons sequential-locked or freely navigable
- What does the unlabelled trailing number on each course row mean (comments, learners, completions)
- Why does 'Read a Sources Sought' appear in Courses without a goal card
- Does the lesson list show all lessons or four plus 'Show all'
- Is the AI helper Pro-gated or free; what does it have access to
- Is there a certificate or completion state, and does it feed 'Your path' on Veterans
- Should lesson discussion be visible to non-members

## Page: Plan & settings (`plan-settings`)
- Route: /os#/me/settings/plan · Nav: none (Me menu; header avatar) · Breadcrumb: Me ▸ Plan & settings
- Purpose: See and change the plan, tune notifications and privacy, manage billing, and personalise how the app looks and feels.
- Primary action: Start 3 months free — bottom-right of the Pro adds card (veterans only; hidden once the trial is active, then the card shows 'Trial' status instead)
- Screen: https://fedm8.com/design/003/screens/12-settings.png · Comp: https://fedm8.com/design/003/comps/12-settings.dc.txt

### Eye path
1 H1 'Your plan' (breadcrumb Me > Plan & settings sits above it, depth 2 per R4)
2 Plan cards: Free / Pro / Team. Pro is the selected ChoiceCard (navy = selected choice, R1). Label first (type-xl), price second (R6)
3 Pro adds: 4 benefits, then the single amber 'Start 3 months free' (R2)
4 Appearance (new): preset picker first, live preview beside it, fine controls below
5 Notifications toggles
6 Privacy toggle
7 Billing row with outline 'Add card' (secondary)

### Modules
- **0. App header** (`app-header`, AppHeader, header)
  - FedM8 shield logo + wordmark (home link)
  - PillNav with 8 pills: Home, Contracts, Grants, Teaming, Certifications, Veterans, Learn, Community (icon + label each)
  - IconButton Notifications (bell)
  - IconButton Messages with unread dot (amber)
  - WorkspaceSwitcher label 'FEDM8 LLC'
  - Avatar 'JN' (account menu)
  - ↳ click a pill → navigate to that top-level page
  - ↳ click logo → Home
  - ↳ bell/messages → open drawer
  - ↳ workspace label → switch workspace menu
  - ↳ avatar → menu (Plan & settings, Sign out)
  - states: empty: no unread → unread dot hidden; loading: pills render immediately; workspace and avatar show skeleton; error: workspace name fails to load → show 'Workspace' and retry on click; selected: no pill is active on this page (settings is under Me); no navy pill shown (R1)
  - data: nav items = real (static route table)
  - data: unread messages flag = placeholder (TBD messaging table)
  - data: workspace name 'FEDM8 LLC' = real (workspace/profile table)
  - data: avatar initials 'JN' = real (auth user profile)
- **1. Breadcrumb** (`breadcrumb`, ChevronBreadcrumb, header)
  - Segment 'Me' (link, muted)
  - Segment 'Plan & settings' (current, aria-current=page)
  - ↳ click 'Me' → Me / profile hub
  - states: empty: n/a; loading: n/a; error: n/a; selected: last segment is current location
  - data: segment labels = real (static route table)
- **2. Settings navigation** (`settings-nav`, SettingsNav, left)
  - 6 items with icon + label: Plan, Profile, Company, Notifications, Privacy, Billing
  - Active item filled navy (Plan)
  - Appearance item (new) added between Privacy and Billing
  - ↳ click item → scroll to / open that section; URL hash updates
  - ↳ Profile and Company navigate to their own pages
  - ↳ arrow keys move between items
  - states: empty: n/a; loading: static, no loading; error: n/a; selected: active item navy with white ink (R1 where-you-are)
  - data: items = real (static route table)
- **3. Your plan** (`plan-picker`, PlanCard, main)
  - H1 'Your plan'
  - 3 PlanCards in a radio-like group: Free ($0/mo, 'Find, track, discuss'), Pro ('AI and rosters', price, '/mo after'), Team ('Seats for your crew', price, '/mo')
  - Free: person icon on soft-blue circle
  - Pro: sun/spark icon on amber circle, status tag, purple pill 'Veterans: 3 months free'
  - Team: people icon, AvatarStack of 3 (Dana R., Luis G., Kim A.)
  - Pro status tag reads 'Trial' while on the free 3 months (was 'Your plan'); 'Your plan' once paid
  - ↳ select a card → aria-pressed true, card turns selected style, plan change flow starts (confirm step before billing change)
  - ↳ selecting Free from Pro asks to confirm loss of Pro features
  - ↳ Team opens seat-count step
  - states: empty: n/a; loading: cards show skeleton price lines until plan data loads; error: plan fetch fails → ErrorState with Retry; cards stay disabled; selected: current plan card uses --fm-selected-bg (navy); only one selected
  - data: Pro price [PRICE] = placeholder (TBD pricing config; do not ship until set, hide price line if null)
  - data: Team price [PRICE] = placeholder (TBD pricing config)
  - data: Free price $0 = real (plan catalogue)
  - data: current plan and trial status = real (subscriptions table / billing provider (TBD))
  - data: Team avatar stack names = sample (none; comp-only decoration, replace with generic team icon or real teammates)
  - data: veteran eligibility for 3 months = real (profile.veteran flag (TBD))
- **4. Pro adds** (`pro-adds`, SummaryCard, main)
  - H2 'Pro adds' + ProTag
  - 4 benefit rows (icon + label): Contact roster members, AI proposal writer, AI requirement summaries, Past-pay lookups
  - PrimaryButton 'Start 3 months free'
  - Note 'Veterans only'
  - ↳ click Start 3 months free → verify veteran status → activate trial → Pro card flips to 'Trial'
  - ↳ if not verified veteran → button hidden and 'Veterans only' replaced by upgrade link
  - ↳ benefit rows are static, not links
  - states: empty: n/a; loading: button shows spinner and is disabled while activating; error: activation fails → inline error under button, Retry; selected: after trial starts: button replaced by 'Trial ends <date>' text
  - data: benefit list = real (matches CORE Pro definition)
  - data: trial end date = placeholder (TBD subscriptions.trial_ends_at)
  - data: veteran eligibility = real (profile.veteran flag (TBD))
- **5. Appearance** (`appearance`, ThemeSwitcher, main)
  - H2 'Appearance' with paintbrush icon
  - Preset picker: 4 selectable cards with mini thumbnails (Card hub default, High contrast, Dark, Compact); selected card uses selected style
  - Accent colour picker: row of contrast-safe swatches only (each swatch pre-validated >=4.5:1 for action-ink on action and >=3:1 against surface); unsafe swatches are not offered
  - Text size slider: --fm-type-scale 0.9 to 1.3, step 0.05, live value label (e.g. '100%'), min and max end labels
  - Density SegmentedControl: Compact (0.85) / Comfortable (1) / Roomy (1.15) → --fm-density
  - Corner style control: Square / Default / Round / Extra round → --fm-radius-scale 0 / 1 / 1.25 / 1.5
  - Reduce motion Toggle (follows prefers-reduced-motion until user sets it)
  - Live preview panel: sample header pill, ChoiceCard, NoticeRow with UrgencyChips, PrimaryButton, FilterChip pair; updates as controls change
  - Contrast guard message area (inline, role=status) shown when a combination is blocked
  - TextButton 'Reset to default'
  - Footnote: 'Saved to your account and synced across devices'
  - ↳ choose preset → all tokens swap at once, per-control overrides cleared, preview and whole app update without reload
  - ↳ pick accent swatch → sets action/accent tokens; if resulting pair fails 4.5:1 text contrast the change is rejected, previous value kept, guard message names the failing pair
  - ↳ move text-size slider → --fm-type-scale updates live while dragging; clamped to 0.9–1.3; keyboard arrows step 0.05
  - ↳ pick density → --fm-density updates spacing live
  - ↳ pick corner style → --fm-radius-scale updates live
  - ↳ toggle reduce motion → --fm-motion-fast/base set to 0ms and transitions removed
  - ↳ Reset to default → confirms, restores Card hub preset with no overrides
  - ↳ every change auto-saves (debounced 500ms) to the user's theme JSON and applies on other signed-in devices
  - ↳ offline change → applied locally, queued, synced on reconnect
  - states: empty: no saved theme → Card hub preset selected, scale 1, Comfortable, Default corners, reduce motion follows OS; loading: controls disabled with skeleton while saved theme loads; app renders default tokens until it resolves, then swaps once with no flash of wrong theme if cached locally; error: save fails → inline 'Not saved. Retry' with TextButton; local change stays applied; guard-blocked change shows contrast message and reverts the control; selected: selected preset card, selected density/corner segment and chosen swatch each show a visible selected state (ring plus check, not colour alone)
  - data: user theme JSON (preset id, accent, type scale, density, radius scale, reduce motion) = placeholder (TBD table user_theme(user_id, preset, overrides jsonb, updated_at) with RLS own-row; edge fn or supabase client upsert)
  - data: preset definitions = real (static JSON bundled with app (CORE token set per preset))
  - data: safe accent swatch list = real (static list validated at build by contrast script)
  - data: local cache of theme = real (localStorage (convenience only, account copy wins))
- **6. Notifications** (`notifications`, Toggle, main)
  - H2 'Notifications' with bell icon
  - Toggle row 'Friday match summary email' + sub 'Your best matches, once a week' (on)
  - Toggle row 'Auto-enlist on high-confidence matches' + 'Opt-in' badge + sub 'Off until you turn it on' (off)
  - Toggle row 'Replies to my comments' + sub 'In app' (on)
  - ↳ click or Space on a switch → flips, saves immediately, announces state
  - ↳ turning on Auto-enlist shows a one-line consent confirm before saving
  - states: empty: n/a (3 fixed rows); loading: switches show skeleton thumb until prefs load; error: save fails → switch reverts and inline error 'Could not save'; selected: on = --fm-success track; off = neutral track with border (not colour alone: thumb position and 'On/Off' accessible name)
  - data: weekly digest pref = placeholder (TBD notification_prefs table)
  - data: auto-enlist pref = placeholder (TBD notification_prefs; default false)
  - data: reply notification pref = placeholder (TBD notification_prefs)
- **7. Privacy** (`privacy`, Toggle, main)
  - H2 'Privacy' with lock icon
  - Toggle row 'Share CAGE and UEI with teammates' + sub 'Consent. Off by default' (off)
  - ↳ flip switch → saves consent with timestamp; turning off revokes immediately
  - states: empty: n/a; loading: skeleton thumb; error: save fails → revert and message; selected: on/off as Notifications
  - data: share CAGE/UEI consent + consent timestamp = placeholder (TBD privacy_consents table)
- **8. Billing** (`billing`, SettingsNav, main)
  - Receipt icon on soft-blue circle
  - H2 'Billing'
  - Status line 'Trial: 3 months. No card on file'
  - SecondaryButton (outline) 'Add card'
  - ↳ click Add card → hosted payment form (provider TBD); on success status line updates to card brand and last 4
  - states: empty: no card → 'No card on file'; loading: status line skeleton; error: provider error → inline message, button stays; selected: card on file → button becomes 'Update card'
  - data: trial length and end date = placeholder (TBD subscriptions)
  - data: card on file = placeholder (TBD billing provider customer portal)

### Features
- Choose Free / Pro / Team plan
- Veterans start a 3 month Pro trial without a card
- Trial status label on the Pro card
- View what Pro adds (4 benefits)
- Appearance: 4 presets (Card hub default, High contrast, Dark, Compact)
- Appearance: contrast-safe accent colour swatches only
- Appearance: text size slider 0.9–1.3 (--fm-type-scale)
- Appearance: density compact / comfortable / roomy (--fm-density)
- Appearance: corner style (--fm-radius-scale)
- Appearance: reduce motion toggle
- Appearance: live preview
- Appearance: Reset to default
- Appearance: saved per user and synced across devices
- Toggle weekly match summary email
- Toggle auto-enlist on high-confidence matches (opt-in, default off)
- Toggle reply notifications
- Toggle sharing CAGE and UEI with teammates (consent, default off)
- Add a payment card
- Settings side navigation to Plan, Profile, Company, Notifications, Privacy, Appearance, Billing

### v2 changes vs comp
- Pro card tag reads 'Trial' while on the free 3 months, 'Your plan' only when paid
- Tighten bottom space: page bottom padding 24px, no empty min-height; section gaps use --fm-space-6
- R1: navy only on active settings nav item and the selected plan card; Pro adds card and others are white
- R2: one amber primary on the page ('Start 3 months free'); 'Add card' stays outline
- R4: breadcrumb retained because depth is 2 (Me > Plan & settings)
- R6: plan card label first (type-xl) then price (type-md); price not large
- R8: 11px badge text ('Pro', 'Opt-in') raised to 12px badge minimum with ink passing 4.5:1; sub-lines stay 13px
- Toggle off-state track (#8a8576) must reach >=3:1 against its card background; use --fm-toggle-off-bg
- Add Appearance module and Appearance item in SettingsNav (new — not in comp)
- Replace placeholder '[PRICE]' with real price or hide the price line; never ship the literal text
- Team AvatarStack is sample decoration; replace with icon-only or real teammates

### Acceptance
1. Given the page loads When it renders Then the H1 'Your plan' is visible and the breadcrumb shows 'Me' then 'Plan & settings' with the last segment aria-current=page
2. Given the header When viewed Then 8 nav pills show (Home, Contracts, Grants, Teaming, Certifications, Veterans, Learn, Community), none is navy-filled, and Veterans, Learn and Community are visible without an overflow menu
3. Given the header When the user has unread messages Then an unread dot shows on Messages; with none the dot is absent
4. Given settings nav When it renders Then it lists Plan, Profile, Company, Notifications, Privacy, Appearance, Billing and Plan is the only navy item
5. Given the user clicks 'Notifications' in settings nav When the click completes Then the Notifications section is in view and the URL hash updates
6. Given a user on the free 3 month trial When the plan cards render Then Pro is selected (aria-pressed=true), shows tag 'Trial' (not 'Your plan'), and exactly one card is selected
7. Given the user selects Free while on Pro When confirmed Then a confirm dialog states which Pro features are lost before any change is saved
8. Given Pro and Team prices are not configured When cards render Then no '[PRICE]' text appears and the price line is hidden
9. Given Pro price is configured When rendered Then plan label is type-xl and the price is type-md, not larger than the label
10. Given Pro adds When viewed Then exactly 4 benefits are listed: Contact roster members, AI proposal writer, AI requirement summaries, Past-pay lookups
11. Given a verified veteran not yet on trial When they click 'Start 3 months free' Then the trial activates, the Pro card shows 'Trial', and the button is replaced by 'Trial ends <date>'
12. Given a non-veteran user When the page renders Then 'Start 3 months free' is not shown
13. Given the page When counted Then exactly one element is styled --fm-action (Start 3 months free); Add card is outline
14. Given Appearance When the user selects each of Card hub, High contrast, Dark, Compact Then the whole app re-themes immediately with no page reload and the selected card shows a ring and check
15. Given High contrast preset When active Then every text and background pair on this page measures >=7:1 for body text
16. Given the accent picker When opened Then only swatches that give action-ink on action >=4.5:1 are offered; no unsafe swatch is selectable
17. Given a combination that would fail contrast (e.g. a light accent with white ink via preset mix) When the user applies it Then the change is blocked, the previous value is kept, and an inline role=status message names the failing pair and ratio
18. Given the text size slider When dragged to 0.9 and 1.3 Then --fm-type-scale equals 0.9 and 1.3, text across the page rescales live, values outside the range cannot be set, and no horizontal scroll appears at 1.3 on 390px width
19. Given the slider has focus When ArrowRight is pressed Then the value increases by 0.05 and the live label updates
20. Given density When Compact, Comfortable, Roomy are chosen Then --fm-density is 0.85, 1, 1.15 and card padding changes proportionally
21. Given corner style When Square is chosen Then --fm-radius-scale is 0 and cards and buttons have square corners (pills stay pill only if tokenised that way)
22. Given reduce motion When toggled on Then --fm-motion-fast and --fm-motion-base resolve to 0ms and no transition runs; with the OS setting on and no user choice, it starts on
23. Given any Appearance control changes When the preview panel is visible Then the preview reflects the change within 100ms
24. Given Reset to default When confirmed Then preset = Card hub, accent default, scale 1, Comfortable, default corners, reduce motion follows OS, and the saved row is cleared or set to defaults
25. Given the user changes a theme setting When they sign out and sign in on the same device Then the same settings are applied before first paint of the app
26. Given the user changes a theme on device A When they open the app on device B signed in as the same user Then device B applies the same theme within 5 seconds of load without a manual refresh of the setting
27. Given the save request fails When a control changes Then the change stays applied locally, an inline 'Not saved. Retry' appears, and Retry re-sends successfully
28. Given no saved theme exists When Appearance loads Then Card hub is selected with scale 1, Comfortable, default corners
29. Given a keyboard-only user When tabbing through Appearance Then every control is reachable in visual order and each selected state is announced
30. Given Notifications When rendered Then 3 switches show with 'Friday match summary email' on, 'Auto-enlist on high-confidence matches' off with an 'Opt-in' badge, and 'Replies to my comments' on
31. Given a switch is flipped When the save succeeds Then the new state persists after reload; if the save fails the switch reverts and shows an error
32. Given Auto-enlist is turned on When the switch flips Then a consent confirm is shown first and nothing saves until confirmed
33. Given Privacy When rendered Then 'Share CAGE and UEI with teammates' is off by default with sub 'Consent. Off by default'; turning on stores a consent timestamp
34. Given Billing with no card When rendered Then it reads 'Trial: 3 months. No card on file' and an outline 'Add card' button is present
35. Given the page at the bottom When scrolled Then the space under the Billing card is no more than --fm-space-6 plus safe area (no large empty band)
36. Given any meta text on the page When measured Then font size >=13px (badges >=12px) and contrast >=4.5:1

### Responsive
<=1024px: SettingsNav becomes a horizontal scrollable SegmentedControl above the content (44px targets); plan cards stack 1 or 2 per row; Appearance preview moves below the controls. <=480px: single column, 16px gutter, plan cards full width, toggles keep 44px rows, breadcrumb collapses to back arrow plus current segment, header PillNav moves to BottomTabBar (Home, Contracts, Teaming, Community, Me).

### Accessibility
- Landmarks: header (banner), nav 'Main', nav 'Settings', main with a section per module labelled by its H2
- Plan cards are a role=radiogroup (or aria-pressed buttons) with a roving tabindex and a visible --fm-focus-ring
- Switches use role=switch with aria-checked and aria-labelledby; state changes announced
- Slider uses native input range or role=slider with aria-valuemin/max/now and a text value
- Contrast-guard message uses role=status so it is announced
- Selected presets and swatches show a ring plus check mark, never colour alone
- Tab order: header, breadcrumb, settings nav, plan, Pro adds, Appearance, Notifications, Privacy, Billing

### Open questions
- Final Pro and Team prices
- Exact contrast thresholds for the guard (assumed 4.5:1 text, 3:1 UI) and the swatch palette list
- Should theme apply to logged-out marketing pages
- Are Profile and Company separate pages or sections here
- Billing provider and trial-to-paid flow
- Does Team plan need a seat management screen in v1

## Page: Mobile · Home (`mobile-home`)
- Route: /os#/home (390px viewport) · Nav: Home · Breadcrumb: —
- Purpose: See what needs me today (due soon, matches, invites, replies), open the contract due next, and glance at network activity.
- Primary action: Open — left, full-width of the Due tomorrow card actions row (amber)
- Screen: https://fedm8.com/design/003/screens/m1-home.png · Comp: https://fedm8.com/design/003/comps/m1-home.dc.txt

### Eye path
1 H1 'Good morning, <name>' (no breadcrumb at depth 1, R4)
2 'Your day' 2x2 StatCards: Due this week, New matches, Team invites, Replies (big numbers only here, R6)
3 'Due tomorrow' card: urgency chip, title, agency, team strip, amber Open + bookmark icon button
4 Network activity list
5 BottomTabBar (Home active)

### Modules
- **0. App bar** (`app-bar`, AppHeader, header)
  - FedM8 logo and wordmark (home link)
  - IconButton Search
  - IconButton Notifications with red unread dot
  - ↳ Search → full-screen search
  - ↳ Notifications → notifications drawer
  - ↳ logo → Home
  - states: empty: no unread → dot hidden; loading: icons render at once; error: n/a; selected: n/a
  - data: notification unread flag = placeholder (TBD notifications table)
- **1. Greeting and open count** (`greeting`, PageTitle, main)
  - H1 'Good morning, Joey' (time-of-day word and first name)
  - Sub-line '<N> open notices' with tabular number
  - ↳ tap sub-line → Contracts list
  - states: empty: count unknown → hide the sub-line (R9); loading: skeleton line for sub-line; error: hide sub-line; selected: n/a
  - data: first name = real (profile)
  - data: open notices count 7,829 = real (notices table count where status=open (verify the number is live, not hard-coded))
  - data: greeting word (morning/afternoon/evening) = real (device local time)
- **2. Your day** (`your-day`, StatCard, main) · social
  - 2x2 grid of 4 tappable StatCards, min height 140px
  - Due this week: calendar icon, number, label, AvatarStack of 2 (Dana R., Marcus T.)
  - New matches: star icon, label, badge 'Set up profile' (no number until matching is set up)
  - Team invites: people icon, number, label, AvatarStack of 3
  - Replies: chat icon, number, label, AvatarStack of 2
  - Comp-only SampleChip 'Sample members' above the grid (must not ship)
  - ↳ tap Due this week → Contracts filtered to due <=7 days
  - ↳ tap New matches → profile setup if incomplete, else matches list
  - ↳ tap Team invites → Teaming invites
  - ↳ tap Replies → Community replies inbox
  - states: empty: zero count → card shows label and a calm 'None' text, number hidden; no big 0; loading: LoadingSkeleton in number slot; error: card shows label with a dash and retry on tap; selected: no selected state; cards are links. None are navy (R1)
  - data: due this week count 5 = sample (TBD tracked_notices where due within 7 days for user)
  - data: new matches (no number) = placeholder (TBD matching engine; show 'Set up profile' until profile complete)
  - data: team invites count 3 = sample (TBD team_invites)
  - data: replies count 4 = sample (TBD comment replies unread)
  - data: avatar stacks (DR, MT, LG, KA, PS) = sample (none; real: users involved in each count)
- **3. Due tomorrow** (`due-tomorrow`, NoticeRow, main) · social
  - H2 'Due tomorrow'
  - TypeBadge 'Sources Sought'
  - UrgencyChip 'Nov 3, 12:00 PM ET' (<=48h → --fm-urgent per R5)
  - Title 'Web Sustainment/Content Management/Cloud Hosting'
  - Agency line 'DHRA (DoD) · 541511'
  - TrackingBar: AvatarStack of 5, '13 teaming', green dot '5 online'
  - PrimaryButton 'Open' (amber, full width minus icon button)
  - IconButton bookmark (Track), 48px, outline
  - ↳ tap Open or card title → mobile contract detail
  - ↳ tap bookmark → toggles tracking, icon fills, toast 'Tracking'
  - ↳ tap team strip → Team tab of the contract
  - states: empty: nothing due in 48h → card replaced by EmptyState 'Nothing due tomorrow' with link to Contracts; loading: card skeleton; error: ErrorState small with Retry; selected: tracked → bookmark filled
  - data: notice title, agency, NAICS, type, due date = real (notices table (SAM.gov ingest))
  - data: teaming count 13 and online 5 = sample (TBD team_members count; presence service TBD)
  - data: team avatar strip = sample (TBD)
  - data: tracked state = real (tracked_notices (TBD))
- **4. Network** (`network`, ActivityFeedItem, main) · social
  - H2 'Network' with TextButton 'See all'
  - 3 ActivityFeedItems: avatar, bold name + action text, relative time (1h / 40m / 2h)
  - Items: 'Luis G. joined as Prime on DHRA', 'Priya S. replied in Discussion', 'Dana R. is tracking FOMS Software Dev'
  - ↳ tap an item → the related contract or thread
  - ↳ See all → full activity feed
  - states: empty: EmptyState 'No network activity yet' with link to Teaming; loading: 3 skeleton rows; error: ErrorState with Retry; selected: n/a
  - data: activity items and times = sample (TBD activity_events table)
  - data: avatar initials and colours = sample (TBD profiles)
- **5. Bottom tab bar** (`bottom-tabs`, BottomTabBar, bottom)
  - 5 tabs with icon + label: Home, Contracts, Teaming, Community, Me
  - Active tab: navy pill behind icon, bold label
  - ↳ tap tab → top-level page; re-tap active tab scrolls to top
  - states: empty: n/a; loading: n/a; error: n/a; selected: Home navy pill (R1)
  - data: tab list = real (static route table)

### Features
- Time-of-day greeting with first name
- Open notices count
- 4 glanceable counters: Due this week, New matches, Team invites, Replies
- Deep links from each counter
- Due tomorrow contract card with urgency chip
- Open contract in one tap
- Track (bookmark) in one tap
- Team presence on the card (teaming count, online count)
- Network activity feed with See all
- Search and notifications in app bar
- 5 item bottom tab bar

### v2 changes vs comp
- R4: remove the 'Home' ChevronBreadcrumb; top-level page starts with the H1
- R2/page note: 'Open' becomes the single amber PrimaryButton; bookmark becomes an outline IconButton (was amber)
- R1: 'Due this week' StatCard is no longer navy-filled; use surface with --fm-accent-soft icon circle (navy is only for where-you-are)
- R5: urgency chip colour driven by time left (<=48h --fm-urgent-soft/--fm-urgent text); the comp's amber chip becomes the urgent style
- R6/R9: big numbers only on StatCards; New matches shows no number; unknown counts hidden, not shown as 0
- R8: relative timestamps (12px) raised to 13px; badge text min 12px
- Remove 'Sample members' chip from shipped build; real avatar data or hide stacks
- Make the bell dot --fm-urgent and give it a number-free accessible label

### Acceptance
1. Given the page loads at 390px When rendered Then no ChevronBreadcrumb is present and the first heading is H1 'Good morning, <first name>' (word matches local time of day)
2. Given the open notices count is available When rendered Then '<N> open notices' shows with thousands separator; if unavailable the line is hidden
3. Given Your day When rendered Then 4 cards appear in a 2x2 grid in order Due this week, New matches, Team invites, Replies, each at least 140px tall and at least 44px tap target
4. Given a counter is 0 When rendered Then no large '0' is shown on the card
5. Given the profile is incomplete When rendered Then New matches shows badge 'Set up profile' and no number, and tapping goes to profile setup
6. Given the user taps Due this week When navigation completes Then the Contracts list is filtered to due within 7 days
7. Given Your day When inspected Then no StatCard has --fm-nav-active-bg background
8. Given SampleChip 'Sample members' When building for production Then it is not in the DOM
9. Given a notice due within 48 hours When Due tomorrow renders Then the chip uses --fm-urgent colours and shows date and time in ET
10. Given no notice is due within 48 hours When Home renders Then the Due tomorrow card is replaced by an EmptyState with a link to Contracts
11. Given the Due tomorrow card When counted Then exactly one amber button ('Open') exists on the screen and the bookmark button has an outline style
12. Given the user taps Open When navigation completes Then the mobile contract detail for that notice opens
13. Given the user taps the bookmark When the request succeeds Then the icon fills, a toast 'Tracking' shows, and aria-pressed is true; tapping again untracks
14. Given the card team strip When data exists Then it shows avatars, '<n> teaming' and '<n> online' with a green dot plus text (not colour alone)
15. Given Network When it has items Then 3 are shown each at least 56px tall with name in bold, action, and relative time at 13px or larger
16. Given Network has no items When rendered Then an EmptyState is shown
17. Given the user taps See all When navigation completes Then the full activity feed opens
18. Given Network items load slowly When pending Then 3 skeleton rows show
19. Given the bottom tab bar When rendered Then 5 tabs (Home, Contracts, Teaming, Community, Me) show, Home has the navy pill and aria-current=page, and each target is at least 44px
20. Given the app bar When rendered Then Search and Notifications are 44px targets with accessible names, and the unread dot is shown only when unread exists
21. Given content scrolls When the bottom bar is sticky Then the last section is not hidden behind it (bottom padding >= bar height)
22. Given text scale 1.3 When Home renders Then no horizontal scroll and no clipped card text
23. Given any API error When a module fails Then only that module shows an ErrorState with Retry

### Responsive
Designed for 390px. >=768px: switch to the desktop Home layout (PillNav, wider grid). 320px: StatCards stay 2 columns with label wrapping to 2 lines; avatar stacks drop to 2 avatars. Safe-area insets respected on the bottom bar.

### Accessibility
- Landmarks: header, main, nav 'Main' (bottom bar)
- Each StatCard is one link with an accessible name including the count, e.g. 'Due this week, 5'
- Avatar stacks have role=img with names; decorative initials are aria-hidden
- Online status has text '5 online' beside the dot
- Bookmark button uses aria-pressed and label 'Track'
- Focus order: app bar, H1, stat cards, due card (Open then bookmark), network list, bottom tabs
- Tap targets >=44px

### Open questions
- Is 'Good morning' time-of-day aware or fixed
- What if several notices are due tomorrow: carousel, or list with See all
- Definition of 'New matches' and when the number appears
- Does 7,829 refer to all open notices or to those matching the user
- Who counts as 'Network' (teammates, followed users, everyone)

## Page: Mobile · Contract detail (`mobile-contract-detail`)
- Route: /os#/opportunities?notice=<id> (390px viewport) · Nav: Contracts · Breadcrumb: Contracts ▸ <solicitation #>
- Purpose: Decide on a contract: read the summary, say what I can do, join as Prime / Sub / 1099, and talk with the team.
- Primary action: Track — full-width amber button in the notice header (left of the More button)
- Screen: https://fedm8.com/design/003/screens/m2-contract.png · Comp: https://fedm8.com/design/003/comps/m2-contract.dc.txt

### Eye path
1 Back arrow + breadcrumb Contracts > solicitation # (depth 2, R4)
2 Type badge + countdown chip, title, agency line (R3 step 1)
3 Track (the one amber button) + More
4 Who is here strip (18 tracking, 5 online)
5 3-part SegmentedControl: Overview · Talk 24 · Team 13
6 Overview: Summary (BLUF) → Can you do this? (R3)
7 Team: Join as (3 ChoiceCards), then claim bar and Pro Contact
8 Talk: Discussion thread list
9 BottomTabBar (Contracts active)

### Modules
- **0. App bar** (`app-bar`, AppHeader, header)
  - FedM8 logo
  - IconButton Search
  - IconButton Notifications with unread dot
  - ↳ Search → search overlay
  - ↳ Notifications → drawer
  - states: empty: no unread → dot hidden; loading: renders at once; error: n/a; selected: n/a
  - data: unread flag = placeholder (TBD notifications)
- **1. Back and breadcrumb** (`breadcrumb`, ChevronBreadcrumb, header)
  - Back arrow IconButton (label 'Back to Contracts')
  - Segment 'Contracts' (link, accent-soft)
  - Segment solicitation number in mono (current, e.g. H9821027SS0001)
  - ↳ back arrow or 'Contracts' segment → Contracts list keeping prior filters and scroll
  - states: empty: n/a; loading: solicitation segment shows skeleton; error: falls back to 'Contract'; selected: last segment current
  - data: solicitation number = real (notices.solicitation_number)
- **2. Notice header** (`notice-header`, CountdownBanner, main)
  - TypeBadge 'Sources Sought'
  - UrgencyChip 'Due tomorrow · Nov 3, 12:00 PM ET' (<=48h → --fm-urgent per R5)
  - H1 title 'Web Sustainment/Content Management/Cloud Hosting'
  - Agency path 'DoD › Defense Human Resources Activity (DHRA)'
  - PrimaryButton 'Track' (full width, bookmark icon)
  - IconButton 'More actions' (outline, 48px)
  - ↳ Track → toggles tracking; label becomes 'Tracking' with filled icon
  - ↳ More → bottom sheet: Share, Open on SAM.gov, Save, Report
  - ↳ countdown updates live; at <=48h turns urgent, <=7d warn, later neutral
  - states: empty: n/a; loading: skeleton for title and chips; Track disabled; error: ErrorState 'Could not load this contract' with Retry and back; selected: tracked → Track reads 'Tracking' and uses outline style so only one amber action remains where relevant
  - data: type, title, agency path, due date = real (notices table)
  - data: tracked state = real (tracked_notices (TBD))
- **3. Who is here** (`who-is-here`, TrackingBar, main) · social
  - AvatarStack of 3
  - '18 tracking' (bold)
  - green dot + '5 online'
  - ↳ tap → opens Team tab
  - states: empty: 0 tracking → 'Be the first to track' text, no numbers; loading: skeleton; error: hide the strip; selected: n/a
  - data: tracking count 18 = sample (TBD count of tracked_notices for this notice)
  - data: online count 5 = sample (TBD presence service)
  - data: avatars = sample (TBD profiles)
- **4. Sections** (`section-tabs`, SegmentedControl, main) · social
  - 3 segments only: Overview, Talk 24, Team 13 (count on the same line as the label, 13px)
  - Selected segment: chip-selected style (cream), others transparent
  - ↳ tap segment → swaps panel below, keeps scroll at the control
  - ↳ arrow keys move between segments; panel focus moves to panel heading
  - ↳ selected segment kept in URL (?tab=)
  - states: empty: count 0 → label only, no number; loading: counts show a dash; error: counts hidden; selected: exactly one aria-pressed true (default Overview)
  - data: Talk count 24 = sample (TBD comments count for notice)
  - data: Team count 13 = sample (TBD team_members count)
- **5. Summary (BLUF)** (`summary`, SummaryCard, main) · Pro
  - Heading 'Summary'
  - AI BLUF text, 2–4 lines
  - ProTag if AI summary is a Pro feature
  - FactLine: agency · NAICS · set-aside · due date (single collapsed line)
  - Expand control for facts
  - ↳ tap FactLine → expands full facts
  - ↳ Pro gate → ProLock with upgrade link for free users
  - states: empty: no description → 'No description provided'; loading: pending: 'Reading this notice…' skeleton; error: failed: 'Summary unavailable' with Retry; selected: ready: BLUF text shown with 'AI summary' label
  - data: AI summary text = placeholder (TBD edge function summarise-notice; never invent text)
  - data: fact line values = real (notices table)
- **6. I can do this** (`can-do`, RequirementToggle, main) · Pro · social
  - H2 'I can do this' with '2 of 4' and CoverageMeter bar
  - 4 RequirementToggles (checkbox rows, 60px min): Web sustainment (on), Content management (off), Cloud hosting (on), 'NAICS 541511 · Custom programming' (off)
  - ClaimBar: check icon + 'Your claim: Web sustainment, Cloud hosting' + outline 'Contact' button with Pro tag
  - ↳ toggle a requirement → saves at once, meter and claim text update, count in claim bar updates
  - ↳ tap Contact → if Pro, opens contact sheet for roster members who claim the same; if free, shows ProLock upgrade prompt
  - ↳ claim visible to team after save
  - states: empty: no requirements parsed → EmptyState 'No requirements listed yet' and claim bar hidden; loading: skeleton rows; error: toggle save fails → toggle reverts, inline error; selected: checked row: --fm-success-soft background plus check mark (not navy, R1)
  - data: requirement list = sample (TBD parsed from notice by AI; must be derived from the real notice, not invented)
  - data: user's claims = placeholder (TBD notice_claims(user_id, notice_id, requirement_id))
  - data: coverage 2 of 4 = placeholder (derived from claims)
- **7. Join as** (`join-as`, JoinAsPicker, main) · social
  - H2 'Join as'; comp-only SampleChip 'Sample members' (must not ship)
  - 3 stacked ChoiceCards: Prime ('Lead the bid'), Sub ('Join a prime'), 1099 ('Contract by the task')
  - Each card: icon circle, label (type-xl) first, count second (type-md muted with unit word, e.g. '2 people')
  - AvatarStack per role plus top member line (e.g. 'Luis G. · Navy vet', 'Priya S. · Section 508 / UX', 'Marcus T. · USMC · Web dev')
  - 'Recommended 3×' chip on Prime
  - ↳ tap a card → selects role (single choice), updates ClaimBar and roster link
  - ↳ tap avatar stack → roster for that role
  - ↳ tap selected card again → keeps selection (no deselect) unless user taps 'Leave'
  - states: empty: 0 in a role → '0 people' muted, still selectable; loading: 3 skeleton cards; error: ErrorState with Retry; selected: selected card uses --fm-selected-bg navy with white ink (R1 valid for choice cards)
  - data: role counts 2 / 7 / 4 = sample (TBD notice_roster count by role)
  - data: avatars and top member lines = sample (TBD roster + profiles)
  - data: Recommended 3× count = sample (TBD recommendation signal; show only if derivable)
  - data: user's chosen role = placeholder (TBD notice_roster.role)
- **8. Talk (Discussion)** (`talk-panel`, ThreadList, main) · social
  - SortTabs Hot/New/Top
  - ThreadPost list with VoteRail, author, RoleBadge, time, body
  - BotPost slot (FedM8 AI)
  - Composer (sticky, above bottom bar)
  - ↳ vote, reply, share, save
  - ↳ post a comment
  - ↳ tab count updates after posting
  - states: empty: EmptyState 'Start the discussion'; loading: skeleton posts; error: ErrorState with Retry; selected: sort selected via chip-selected style
  - data: comments = placeholder (TBD notice_comments)
- **9. Bottom tab bar** (`bottom-tabs`, BottomTabBar, bottom)
  - 5 tabs: Home, Contracts, Teaming, Community, Me
  - Contracts active with navy pill
  - ↳ tap tab → top-level page; tapping active Contracts returns to the list
  - states: empty: n/a; loading: n/a; error: n/a; selected: Contracts navy pill (R1)
  - data: tabs = real (static route table)

### Features
- Back to Contracts list
- Breadcrumb Contracts > solicitation #
- Type badge and live countdown with urgency colour
- Track / untrack (single amber action)
- More actions sheet
- Who is here: tracking count and online count
- 3-way section switch: Overview, Talk, Team
- AI summary (BLUF) with pending/ready/failed states
- Per-requirement 'I can do this' toggles with coverage meter
- Claim bar summarising claims
- Contact roster members (Pro)
- Join as Prime / Sub / 1099
- Recommended-role chip
- Roster avatar stacks per role
- Discussion thread with voting and composer

### v2 changes vs comp
- Segmented control reduced from 4 to 3: Overview · Talk 24 · Team 13; 'Can do' moves into Team (comp says: put 'Can do' inside Team)
- Flag for owner: R3 orders the detail page Summary → Can you do this → Join as → Discussion; this spec puts Summary + Can do in Overview and Join as + claim in Team per the page note, so the Can do list shows in both Overview preview and Team; confirm
- Add SummaryCard (BLUF) which the comp omits (R3)
- R3: facts collapse to one FactLine
- R5: countdown chip uses urgency colours; comp amber chip becomes --fm-urgent at <=48h
- R6: Join as counts (30px) become type-md muted with unit word, below the label
- R1: only the selected Join-as card is navy; selected segment uses --fm-chip-selected-bg not navy; checked requirement rows use --fm-success-soft
- R2: Track stays the only amber button; the Contact button is outline and its Pro tag uses --fm-pro
- R7: no navy-filled chips
- R8: segment count text 12px raised to 13px; meta lines 13px with 4.5:1
- Remove SampleChip 'Sample members' for production
- Segment labels single line (no two-line cramped labels)

### Acceptance
1. Given the page loads at 390px When rendered Then the back arrow, breadcrumb 'Contracts' > solicitation number, and H1 title appear in that order
2. Given the user taps the back arrow When navigation completes Then the Contracts list opens with its prior filters and scroll position
3. Given the notice header When rendered Then it shows a TypeBadge, a countdown chip, the H1 title and the agency path
4. Given the due date is within 48h When rendered Then the chip uses --fm-urgent; within 7 days --fm-warn; later neutral
5. Given the screen When counted Then exactly one amber PrimaryButton ('Track') exists; More and Contact are outline
6. Given the user taps Track When the request succeeds Then the button shows 'Tracking' with a filled icon and the tracking count increments by 1
7. Given the user taps More When the sheet opens Then it lists Share, Open on SAM.gov, Save, Report and closes on Escape or swipe down
8. Given Who is here When data exists Then it shows an avatar stack, '<n> tracking' and '<n> online' with dot plus text; with 0 tracking it shows 'Be the first to track'
9. Given the section control When rendered Then exactly 3 segments (Overview, Talk, Team) appear on single lines, each at least 44px tall, and Talk and Team show counts of 13px or larger
10. Given the user taps Team When the panel shows Then Join as, the claim bar and the roster are visible and the segment has aria-pressed=true
11. Given Overview is selected When rendered Then Summary appears first, then I can do this
12. Given the AI summary is pending When rendered Then a skeleton with 'Reading this notice…' shows; on failure 'Summary unavailable' with Retry; no invented text ever appears
13. Given a free user When viewing the AI summary Then a ProLock explains the upgrade and the rest of the page stays usable
14. Given I can do this When rendered Then each parsed requirement is a 60px-min row with a 24px checkbox and a visible label
15. Given the user checks a requirement When saved Then the row gets --fm-success-soft with a check, the meter and 'N of M' update, and the claim text lists the checked items
16. Given a requirement save fails When toggled Then it reverts and an inline error shows
17. Given no claims When rendered Then the claim bar shows 'No claim yet' and the meter is empty
18. Given the user taps Contact When on Pro Then a contact sheet for roster members with matching claims opens; when free, a ProLock prompt appears and no contact detail is exposed
19. Given Join as When rendered Then 3 ChoiceCards (Prime, Sub, 1099) show label first at type-xl and count second at type-md muted with a unit word, with no 30px counts
20. Given the user taps Sub When selection changes Then only Sub has aria-pressed=true and navy background, and the ClaimBar role updates
21. Given a role has 0 members When rendered Then '0 people' shows and the card is still selectable
22. Given Recommended chip data is not derivable When rendered Then no 'Recommended N×' chip is shown
23. Given Talk is selected When rendered Then SortTabs, thread list and a Composer show; empty state reads 'Start the discussion'
24. Given the user posts a comment When saved Then Talk count increments by 1 and the post appears at the top of New
25. Given SampleChip 'Sample members' When building for production Then it is absent
26. Given the bottom tab bar When rendered Then Contracts has the navy pill and aria-current=page and the last content is not hidden behind the bar
27. Given text scale 1.3 When rendered Then no horizontal scroll and no clipped labels in the section control
28. Given a module API fails When loading Then only that module shows an ErrorState with Retry

### Responsive
Designed for 390px. >=768px: switch to the desktop contract detail (two-column with right rail). 320px: section control keeps 3 segments with 13px labels; ChoiceCards stay stacked; claim bar wraps Contact below the claim text.

### Accessibility
- Landmarks: header, main, nav 'Main'; breadcrumb is ol with aria-current=page on the last item
- Section control: use role=tablist/tab/tabpanel with arrow-key navigation, or aria-pressed buttons with panels labelled
- Join-as cards are a radiogroup; selection announced
- Requirement rows are native checkboxes inside labels
- Meter has text '2 of 4' (not colour alone)
- Urgency chip includes text, not only colour
- Pro-locked controls explain why they are locked via aria-describedby
- Focus order follows eye path; tap targets >=44px

### Open questions
- Where Summary sits when Overview is not selected (R3 wants it before Can-do and Join-as)
- Does 'Can do' appear only in Team (per page note) or also as a preview in Overview
- How requirements are produced (AI parse vs manual) and who can edit them
- Can a user hold more than one role on the same contract
- Is the AI summary Pro-only on mobile
- Grant detail uses the same mobile pattern?

## Page: Mobile · Community (`mobile-community`)
- Route: /os#/community (390px viewport) · Nav: Community · Breadcrumb: —
- Purpose: Browse and join discussions by space, sort them, vote and comment, and start a new post.
- Primary action: New post — bottom-right, floating above the bottom tab bar (amber, icon-only plus)
- Screen: https://fedm8.com/design/003/screens/m3-community.png · Comp: https://fedm8.com/design/003/comps/m3-community.dc.txt

### Eye path
1 H1 'Community' (top-level tab, no breadcrumb or back arrow, R4)
2 Spaces chips: Contracts, Grants, Certs, Teaming, Wins
3 Sort: Hot / New / Top
4 Post cards: vote rail, space tag, author, time, title, comments, save
5 FAB '+' (the one amber action, R2)
6 BottomTabBar (Community active)

### Modules
- **0. App bar** (`app-bar`, AppHeader, header)
  - FedM8 logo
  - IconButton Search
  - IconButton Notifications with unread dot
  - ↳ Search → searches posts and spaces
  - ↳ Notifications → drawer
  - states: empty: no unread → dot hidden; loading: renders at once; error: n/a; selected: n/a
  - data: unread flag = placeholder (TBD notifications)
- **1. Page title** (`title`, PageTitle, main)
  - H1 'Community' (type-2xl on mobile)
  - states: empty: n/a; loading: static; error: n/a; selected: n/a
  - data: title = real (static)
- **2. Spaces** (`spaces`, FilterChip, main) · social
  - Horizontally scrollable row of 5 chips: Contracts, Grants, Certs, Teaming, Wins
  - Selected chip filled with chip-selected style; others outline
  - Right edge fade hints scroll
  - ↳ tap chip → loads that space's posts, resets sort to Hot, keeps scroll at top
  - ↳ swipe row to reveal more spaces
  - ↳ selected chip scrolls into view
  - states: empty: n/a (5 fixed spaces); loading: chips render at once; posts show skeleton; error: posts show ErrorState, chips stay; selected: one chip aria-pressed=true (default Contracts), --fm-chip-selected-bg, never navy (R7)
  - data: space list = real (static list; backing spaces table TBD)
- **3. Sort** (`sort`, SortTabs, main) · social
  - SegmentedControl Hot / New / Top (44px targets)
  - Comp-only SampleChip 'Sample members' at right (must not ship)
  - ↳ tap → re-sorts the list without page reload; selection remembered per space for the session
  - states: empty: n/a; loading: list skeleton while re-sorting; error: keep previous list and show toast; selected: one option aria-pressed=true (default Hot); chip-selected style
  - data: sort options = real (static; ranking query TBD (Hot formula to define))
- **4. Posts** (`posts`, ThreadPost, main) · social
  - List of PostCards (3 in comp): VoteRail (Upvote, score, Downvote), space tag (Contracts / Teaming / Wins with distinct soft colour plus text), author Avatar + name + relative time, title link (type-md bold), comment count button, Save icon button
  - Post 1: Contracts · Dana R. · 2h · 'Anyone know who holds DHRA web sustainment today?' · 31 votes · 24 comments
  - Post 2: Teaming · Luis G. · 5h · 'Prime seeking a 508 tester for FAA FOMS sources sought' · 18 votes · 9 comments
  - Post 3: Wins · Kim A. · 1d · 'First sub slot landed after a Sources Sought response' · 12 votes · 5 comments
  - Last card has bottom padding equal to FAB height plus bar so actions are never covered
  - ↳ tap title or comment count → thread detail
  - ↳ tap Upvote/Downvote → toggles vote, score updates optimistically, second tap removes vote
  - ↳ tap Save → toggles saved, toast 'Saved'
  - ↳ tap author → profile card
  - ↳ pull to refresh; infinite scroll loads next page
  - states: empty: EmptyState 'No posts in <space> yet' with link to New post; loading: 3 skeleton cards; error: ErrorState with Retry; selected: voted up/down shows filled arrow plus colour change; saved shows filled bookmark
  - data: posts (title, space, author, time) = sample (TBD community_posts)
  - data: vote scores 31 / 18 / 12 = sample (TBD votes aggregate)
  - data: comment counts 24 / 9 / 5 = sample (TBD comments count)
  - data: user's vote and saved state = placeholder (TBD post_votes, post_saves)
- **5. New post** (`new-post-fab`, FAB, overlay) · social
  - 56px amber circular button with plus icon, label 'New post' (accessible name)
  - Sits 16px from the right, above the bottom bar
  - ↳ tap → Composer sheet (space picker defaults to the current space, title, body, Post)
  - ↳ hides while scrolling down, shows when scrolling up (optional)
  - states: empty: n/a; loading: n/a; error: post fails → draft kept, inline error; selected: pressed state uses --fm-action-hover
  - data: draft text = real (localStorage convenience, not authoritative)
- **6. Bottom tab bar** (`bottom-tabs`, BottomTabBar, bottom)
  - 5 tabs: Home, Contracts, Teaming, Community, Me
  - Community active with navy pill
  - ↳ tap tab → top-level page; tapping active Community scrolls to top
  - states: empty: n/a; loading: n/a; error: n/a; selected: Community navy pill (R1)
  - data: tabs = real (static route table)

### Features
- Browse by space (Contracts, Grants, Certs, Teaming, Wins)
- Sort Hot / New / Top
- Upvote and downvote posts
- See comment counts and open threads
- Save a post
- Start a new post
- Author avatar, name and relative time on every post
- Space tag on every post
- Search and notifications in app bar
- Infinite scroll and pull to refresh

### v2 changes vs comp
- R4: remove the 'Home > Community' breadcrumb and back arrow; Community is a bottom-tab destination so the page starts with the H1
- Page note: add bottom padding equal to the FAB height (56px) plus bar clearance so the last card's actions are not overlapped
- R7: space chips and sort control use --fm-chip-selected-bg for selected, not navy fill
- R2: the FAB is the single amber action
- R8: relative timestamps and badge text (12px) raised to 13px (badges 12px with 4.5:1); tag ink for Wins uses --fm-success on --fm-success-soft
- Remove SampleChip 'Sample members' for production
- Space tags carry text, not colour only; ensure each tag pair passes 4.5:1
- Vote arrows: downvote arrow colour uses --fm-ink-3 (4.5:1)

### Acceptance
1. Given the page loads at 390px When rendered Then no breadcrumb or back arrow is present and the first heading is H1 'Community'
2. Given Spaces When rendered Then 5 chips (Contracts, Grants, Certs, Teaming, Wins) show in a horizontally scrollable row with 44px height and Contracts selected by default
3. Given the user taps 'Teaming' When the list loads Then only Teaming posts show, that chip is aria-pressed=true with --fm-chip-selected-bg, and no chip has a navy background
4. Given Sort When rendered Then Hot, New, Top show with Hot selected and each target at least 44px
5. Given the user taps 'New' When the list reloads Then posts are ordered newest first without a page reload
6. Given a post card When rendered Then it shows vote rail with score, space tag, avatar, author name, relative time at 13px or larger, title, comment count button and Save button
7. Given the user taps Upvote When the request succeeds Then the arrow fills, the score increases by 1, and a second tap removes the vote and restores the score
8. Given the user taps Downvote after Upvote When saved Then the vote switches from up to down and the displayed score equals the baseline score minus 1
9. Given the vote request fails When tapped Then the score reverts and a toast explains
10. Given the user taps a post title or its comment count When navigation completes Then the thread detail for that post opens
11. Given the user taps Save When saved Then the icon fills and a toast 'Saved' shows; tapping again unsaves
12. Given a space has no posts When rendered Then an EmptyState 'No posts in <space> yet' with a New post link shows
13. Given posts are loading When pending Then 3 skeleton cards show
14. Given the list request fails When rendered Then an ErrorState with Retry shows and chips and sort stay usable
15. Given the list reaches the bottom When scrolled Then the next page loads and the last card's buttons are fully visible above the FAB and the tab bar (at least 56px clearance)
16. Given the FAB When rendered Then it is the only amber element on the screen, 56px, with accessible name 'New post', and does not cover any card action at rest
17. Given the user taps the FAB When the sheet opens Then a Composer shows with the current space preselected, title, body and a Post button; empty title disables Post
18. Given the post is submitted When it succeeds Then the sheet closes and the new post appears at the top of New; on failure the draft is kept
19. Given SampleChip 'Sample members' When building for production Then it is absent
20. Given the bottom tab bar When rendered Then Community has the navy pill and aria-current=page and the bar does not hide content
21. Given pull to refresh When released Then the list reloads and scroll resets to top
22. Given text scale 1.3 When rendered Then titles wrap, no horizontal scroll except the Spaces row, and vote rail stays 44px wide
23. Given each space tag When measured Then text contrast against its tag background is at least 4.5:1

### Responsive
Designed for 390px. >=768px: switch to desktop Community (left rail of spaces, centre feed, right rail). 320px: author line wraps to two lines; vote rail stays 44px; Spaces row keeps scrolling. Safe-area insets respected for the FAB and bar.

### Accessibility
- Landmarks: header, main, nav 'Main'; posts in a list with each article labelled by its title
- Spaces and Sort are groups with aria-pressed buttons; group labelled 'Spaces' and 'Sort'
- Vote buttons labelled 'Upvote' / 'Downvote' with aria-pressed and the score announced (e.g. 'Score 31')
- Comment button label '24 comments'; Save label 'Save' / 'Saved'
- FAB has an accessible name and is reachable by keyboard in logical order
- Space tag text is read, not only coloured
- Tap targets >=44px; focus ring visible

### Open questions
- Hot ranking formula
- Is 'Certs' space the same as Certifications
- Do downvotes exist for all spaces or only some
- Moderation and reporting entry point on posts
- Should posts show a preview body or only titles
- Does the FAB hide on scroll
