Skip to content

Admin Dashboard: the final design โ€‹

  • Source: the operator's Claude Design project 4598a0dd-667b-42f3-8271-e6ddf05ddcee, pulled 2026-08-30.
  • Status: approved, wired and routed (2026-08-30). /admin/dashboard renders it with real counts and a real changelog, and it replaced the Dashboard that shipped on 2026-08-28. See What is wired, and what is deliberately not.

Why is this committed rather than linked? โ€‹

Because the last set of mockups was lost. Three merchant design directions were rendered on 2026-08-28, served from a dead session's localhost:8099, and never committed. She asked to see them on 2026-08-30 and they no longer existed. Anything a design produces lives in the project folder from now on.

What is here? โ€‹

FileWhat it is
Admin Dashboard Final.dc.htmlThe design to implement. 52 KB, zero em dashes
Admin Dashboard Mockups.dc.htmlThe earlier iteration, 174 KB. Read the delta between the two: it shows what she rejected
  • Not vendored: support.js, Claude Design's own generated runtime. It is theirs, it is marked do-not-edit, and it carries em dashes our lint bans. The design files do not need it to be read.
  • Not vendored: _ds/.../colors_and_type.css. The design links packages/ui/theme.css, which is OUR token file, so the design system copy is reference rather than a dependency.

What does the design already tell us? โ€‹

It was synced from dev, not from this branch. โ€‹

  • github.md in the project records branch: dev, commit: 130153ca, dated 2026-08-28.
  • So a diff of her uploaded style files against this branch shows OUR 209 commits, not design intent. Diff against origin/dev instead.
  • The design sets a{color:#60a5fa} with #93c5fd on hover, Tailwind blue-400 and blue-300.
  • She reached the same conclusion independently on 2026-08-30 from the token side: --accent-500 and --warning are both #f59e0b, so the link colour and the caution colour are literally the same value. The design is ahead of the codebase here.

It contains the announcements authoring surface. โ€‹

What is the standing decision? โ€‹

  • This REPLACES the Dashboard that shipped on 2026-08-28, her words, asked and answered directly. It is not a patch on DashboardHome.jsx.
  • Scaffold first, wire second. It gets built against fixture data in the admin Storybook on port 6008, she approves the visual, and only then is anything wired.

What changed between Mockups and Final? โ€‹

Nothing was redrawn. The Final file is a SELECTION out of the Mockups file. โ€‹

  • Screen 4a is byte-identical in both files, and so is screen 5b. A line-by-line diff of each pair returns only the surrounding viewer chrome.
  • So the delta is entirely about what she kept and what she dropped, which is the more useful reading anyway.

What she kept: 4a and 5b. โ€‹

KeptWhat it is
4a Editorial + wickThe page. 1c's editorial layout with 3a's wick timeline dropped into the hero
5b Modal in contextThe Post Announcement modal, drawn over 4a rather than on its own

What she dropped, and what each one was. โ€‹

DroppedWhat it wasWhat replaced it
1a By the bookRole-card grammar, vertical timeline, stacked metric cards1c, which is the editorial hero and the collapsed icon rail
1b Mission controlHorizontal phase stepper, condensed metrics panel, grouped changelogSame
2a The wickThe burn-line timeline, first pass3a, which keeps the burn line and takes 2b's label stack
2b Night arcLanterns along a curved street at nightSame
2c Standing lanternsOne beam of light per phaseSame
3a on its ownThe chosen timeline, as a standalone riff4a, which is 3a inside 1c
4b Flat headersThe same page with NO title band on the two cards4a, so the title band survived a direct A/B against its own removal
5a Post announcementThe modal on its own, without the page behind it5b, the same modal in context

Two things the dropped screens settle. โ€‹

  • The title band is deliberate. 4b existed only to show the page without it, and she picked the version that has it. That is worth knowing before anyone "simplifies" the band away later.
  • 5a's label mentions "type pills" and its markup has none. The label is stale; 5a and 5b contain the same modal, character for character. There is no type or category control in the design.

Do her uploaded token copies differ from origin/dev? โ€‹

No. Every value she redeclared matches origin/dev exactly. โ€‹

  • The design does not vendor a token file. What it carries is an inline :root{} block at the top of each .dc.html, and that block is the thing to compare.
  • Checked against origin/dev's apps/admin/src/shared/styles/styles.css:
TokenDesignorigin/dev
--bg#000000#000000
--surface#18181b#18181b
--surface-elevated#1e1e1e#1e1e1e
--text#e6eef8#e6eef8
--muted#9aa6b2#9aa6b2
--borderrgba(255,255,255,0.08)rgba(255,255,255,0.08)
--border-softrgba(255,255,255,0.05)rgba(255,255,255,0.05)
  • --accent-*, --info, --success and --danger are not redeclared at all: the design links our real packages/ui/theme.css and resolves them from it.
  • --font-sans is the one value that reads differently, and it is not a difference in intent: the design lists 'Inter Variable', Inter, system-ui, -apple-system, sans-serif where packages/ui/fonts.css also carries 'Segoe UI', Roboto, 'Helvetica Neue', Arial in the middle. A truncated fallback tail in the viewer's own chrome. The components set no font-family and inherit the real stack.

The one thing the comparison DOES explain: why the design draws title bands as rgba(0,0,0,.35). โ€‹

  • --surface-dark (#131316) exists on origin/dev and is absent from the subset she pasted into the design. The design could not have used the token, because in that document the token did not exist.
  • So the raw rgba is an artifact of the paste, not a decision against --surface-dark. The build uses the token.

What was built, and what is not wired? โ€‹

Where it lives. โ€‹

  • apps/admin/src/admin/dashboard/, beside the Dashboard that shipped on 2026-08-28. Nothing in DashboardHome.jsx or its parts was touched, so the shipped page cannot move under her while she reviews its replacement. (It arrived in a launch/ subfolder; that folder is gone as of 2026-08-30, when the page stopped being called a launch dashboard. It is the admin Dashboard.)
  • Storybook: port 6008 (npm run storybook:admin). The page is Screens/Dashboard/AdminDashboard; its parts are under Components/Dashboard/.
FileWhat it is
AdminDashboard.jsxThe page. Hero, wick, reading column, rail, modal
LaunchTimeline.jsxThe wick, positioned from each phase's at percentage
DashboardCard.jsxThe card shell: title band, full-width rule, body
AnnouncementsPanel.jsxThe reading half of #992
ReleaseChangelogPanel.jsxWhat shipped, one row per change
CountsRail.jsxThe counts, including the "Not available" tile
PostAnnouncementModal.jsxThe writing half of #992
dashboardFixtures.jsEvery number and string on the page. This file is the whole data layer today
AdminDashboard.cssAll of the above

It was a scaffold until 2026-08-30. That paragraph now lives below. โ€‹

One accessibility follow-up, deliberately not taken in a scaffold pass. โ€‹

  • The modal's "Message *" is a <span class="form-label">, not a <label for>, because MarkdownEditor exposes no id or aria-label for its textarea. axe passes it (the textarea has a placeholder, which axe accepts as a name), so this is a quality gap rather than a violation.
  • Closing it properly means adding a textareaId / textareaAriaLabel prop to MarkdownEditor, which is shared with NewIssueModal and SelfHostedDocsEditor. Additive and small, but it is a change to a shipped component, so it belongs with the wiring rather than inside a scaffold.

Five places the build deliberately does not copy the design. โ€‹

The first four are a codified rule beating a detail of the mock; the fifth is her own later call. Each is commented at the point of use.

The design drewThe build doesWhy
Title bands as rgba(0,0,0,.35)--surface-darkHer rule, and the token was simply missing from the design's paste (above)
Rail tiles as a near-transparent white wash--surfaceEvery card background in the admin portal is --surface. Depth comes from the border and the shadow
Dividers inset by the card's body paddingDividers spanning the whole cardHer rule, stated 2026-08-28
rgba(154,166,178,.7) for quiet meta text, and opacity: .65 on the unavailable tile--text-secondary, and the tile at full strength saying "Not available" in words--muted at 70% composites to 4.15:1 on --surface, under the 4.5:1 bar. A quieter token, never an opacity
The pin control as a full-width row at the top of the modal BODYThe pin control in the modal HEADER, sized to its contentHer call on the second look. See What changed after her second look

And one place the build deviates from the PORTAL, on her explicit instruction. โ€‹

  • The title input is not capped at 560px in this modal. input.form-input[type="text"] { max-width: 560px } in the shared stylesheet governs every text input in the portal, NewIssueModal included, and this modal shipped capped because of it.
  • Operator, 2026-08-30: "we didn't need to shorten the title input." An announcement title is the headline people read in the list, so seeing it at the width it will occupy matters more than the tidiness of a short field.
  • Implemented as .launch-modal input.form-input[type='text'] { max-width: none }, scoped to this modal and specific enough to win (the global is (0,2,1), this is (0,3,1)). Do not "restore" the house rule here. Nothing else in the portal changes.
  • --launch-link: #60a5fa / --launch-link-hover: #93c5fd, scoped to this page, exactly as the design sets them.
  • That also frees the card titles to be amber. DashPanel on the shipped page uses --text for its titles precisely because links there are amber and an amber heading made them stop reading as links. With blue links the conflict is gone, so the amber the design draws is correct rather than a regression.
  • The system-wide blue-links pilot is a separate queued task. When it lands, the two locals above point at whatever token it introduces.

What changed after her first look? โ€‹

She approved everything except the Post Announcement modal, 2026-08-30: "everything looks beautiful except the post announcement popup. I think we need a bit more height than what it currently has. And we don't need to full width on the 'pin to top' toggle. Make it fit more comfortable please."

WhatFromTo
Modal height75% of the overlay, the design's value88%, plus max-height: 100% so it stays inside the gutters at any window size. On an 820px viewport that is 579px to 679px
Pin-to-top rowFull width, justify-content: space-betweenwidth: fit-content, max-width: 100%, and a --space-4 gap. The switch now sits beside its label instead of hundreds of pixels away
Modal body padding and field gap--space-3 (16px)--space-4 (24px), which is what every card body in the portal already uses
Pin row padding--space-2 --space-3 (8/16px)--space-3 (16px)

One thing the height change exposed, which is why the two had to ship together. โ€‹

  • MarkdownEditor ships a fixed box: .md-textarea is height: 240px and .md-preview caps at 400px, because its other two callers put it inside a scrolling form.
  • Inside a fixed-height modal that meant raising the modal MOVED the empty space rather than removing it: the editor stayed 240px and a 240px hole opened between it and the footer. Visible in the first capture.
  • Fixed with a block scoped to .launch-modal that lets the editor fill its field, plus resize: none (a drag handle that pushes the footer out of a panel which cannot grow is not a feature). NewIssueModal and SelfHostedDocsEditor are untouched and keep the drag-resizable box they were built around.

Unchanged at that point: the width. โ€‹

  • She named height, not width. The modal is still 70.5% capped at 60rem.

What changed after her second look? โ€‹

Her words, 2026-08-30: "We didn't need to shorten the title input...... Perhaps the 'pin to top' option should be in the header in an elegant fashion."

The title input is full width now. Covered above, as a deliberate override of a house rule. โ€‹

The pin control moved into the header. โ€‹

  • Decided by her: the header. How it looks there was mine, so here is what was chosen and why.
ChoiceWhy
Label, rule and switch in one group, --space-2 apart, with --space-4 before the close buttonThe 3:1 spacing ratio is what makes them read as one control. No divider needed to say it. This is the direct inverse of the body version's mistake, where space-between threw the switch away from the words it belongs to
Text right-aligned above the switch, two linesBoth lines end flush against the switch, so label, rule and control share one edge and form a block rather than three things that happen to be near each other
justify-content: flex-start on the header with margin-right: auto on the titleThe shared .modal-header is space-between, which with three children would spread title, pin and close evenly across the bar. That is the same failure again, one level up
A hairline border on the OFF trackIn the body the switch sat on --surface-dark and a bare --surface-2 track read against it. In the header it sits on --surface, where --surface-2 is barely a shade away and the control would read as a loose dot. The knob still carries the affordance at --muted, 7.15:1; the border is what makes it a switch
The rule shortened to "Only one pin at a time"A header cannot hold the design's full sentence. The dropped half ("this replaces the current pin; the old one stays in the list") is a statement about STORAGE and belongs in #992's data model. What is left is the half that stays true whether the switch is on or off, which is what a permanently visible line has to be
  • The header did not get taller: 65px before, 65px after. The pin group is 31px inside a bar whose height was already set by the 32px close button.

The height stays at 88%, and that is a decision rather than an oversight. โ€‹

  • The body did get space back: the pin row and its gap were about 102px.
  • All of it went to the message editor, which is the only flex: 1 child, so the writing area grew from 249px to 351px on an 820px viewport rather than the modal carrying dead space. Measured, not assumed.
  • Lowering it would partly undo what she asked for on the first look. The pin row was never what made the editor small; a fixed 240px MarkdownEditor was, and that was fixed separately. Dropping back toward 75% now would shrink the editor again for a reason that no longer applies.
  • If she wants it shorter anyway it is one value in .launch-modal.

One thing for the wiring, not for now: what gets focus on open. โ€‹

  • Tab order follows the DOM, so the first focusable in the dialog is now the pin switch, then Close, then Title. Nothing is wrong with it, but on a writing surface focus should land on the Title input when the modal opens, and that is an autofocus and focus-trap concern that belongs with #992.

What did axe say? โ€‹

Run: headless chromium against the admin Storybook on 6008, axe-core over #storybook-root, nine stories. Harness: ../harness/admin-dash-capture.mjs. Full report: ../harness/admin-dash-axe.json.

StoryViolationsIncompletePassesPage errors
admin-dash-page01 rule / 19 nodes230
admin-dash-modal01 rule / 20 nodes270
admin-dash-narrow01 rule / 19 nodes230
admin-dash-empty01 rule / 19 nodes230
admin-dash-timeline01 rule / 8 nodes170
admin-dash-announcements00140
admin-dash-changelog00150
admin-dash-rail00130
admin-dash-modal-alone01 rule / 1 node210

Zero violations. The incompletes are all color-contrast, and incomplete is not a pass. โ€‹

  • The hero (19 to 20 nodes). axe cannot resolve a gradient ground, and the hero carries the design's amber wash. Moving the wash to a ::before or a sibling layer does not help: axe then reports the same nodes incomplete because of the pseudo element instead. Measured by hand against the wash at its darkest point (--accent-900 at 22% over --bg, which composites to #1b0f04):
PairRatioBar
--text on the wash (blurb)16.08:14.5
--accent-500 on the wash (December 2026, section labels)8.75:14.5
--muted on the wash (eyebrow, phase dates)7.59:14.5
--text-secondary on the wash (phases ahead of us)6.11:14.5
--accent-500 on the amber step pill over the wash6.89:14.5
  • The timeline story (8 nodes). Same cause, the radial glow under the current phase. Same measurements.
  • The modal (1 node). Element content contains only non-text characters on MarkdownEditor's Quote toolbar button, whose entire label is the glyph. Pre-existing, in a shared component this work reuses rather than wrote, and it also fires wherever NewIssueModal is rendered.

The modal revision moved this number, and an unexplained improvement gets the same check as an unexplained failure. โ€‹

  • It was 3 nodes before the revision and is 1 after. The two that went away are the footer's Cancel and Post Announcement buttons, previously reported as Element's background color could not be determined because it is overlapped by another element. They are now measured, and they pass.
  • Reproduced both ways to be sure it was the change and not a flaky run: the committed CSS gives 3 nodes on 6 of 6 runs and the revised CSS gives 1 on 6 of 6, same server, same story, same 1340x820 viewport, with and without waiting on document.fonts.ready.
  • The cause is geometric. The revision moved the footer from y 625..699 to y 675..749 and removed the dead gap that sat above it. Nothing overlaps either button's centre in either state (elementsFromPoint returns the button, then .modal-footer, then .modal-content in both), so axe's overlap verdict was coming off the text rect's edges in the cramped layout rather than off a real covering element. The exact axe internal is not pinned; the direction is, and it is toward more coverage, not less.
  • It held through the header move: still 1 node after the pin control moved into the header and the title input was uncapped.

The modal header is a fully measured context, unlike the hero. โ€‹

The header has no gradient and no wash, so axe resolved every pair in it against a solid --surface and reported real numbers rather than an incomplete:

ElementRatioForeground on backgroundSize
#launch-post-announcement-title15.14:1#e6eef8 on #18181b18px
#launch-pin-label15.14:1#e6eef8 on #18181b13px
#launch-pin-note7.14:1#9aa6b2 on #18181b11px
  • The one thing axe does not check here is the OFF switch track, because non-text contrast is not in its default rule set. The knob carries the affordance at --muted, 7.15:1 on --surface, and the track has a --border hairline. If the off state wants to be louder that is a token change on .launch-switch, not a structural one.

How do I look at it? โ€‹

npm run storybook:admin        # port 6008, NOT 6006

Then Screens/Dashboard/AdminDashboard. Four stories: Default, PostAnnouncementOpen, Narrow, Empty.

Captures, committed beside the other project screens:

CaptureStory
../screens/admin-dash-page.pngThe page
../screens/admin-dash-modal.pngPost Announcement over the page
../screens/admin-dash-narrow.png900px, rail under the reading column
../screens/admin-dash-empty.pngNo announcements, nothing shipped
../screens/admin-dash-timeline.pngThe wick on its own
../screens/admin-dash-announcements.pngThe announcements card
../screens/admin-dash-changelog.pngThe changelog card
../screens/admin-dash-rail.pngThe counts rail
../screens/admin-dash-modal-alone.pngThe modal on its own

What is wired, and what is deliberately not? โ€‹

Routing: /admin/dashboard renders it, and /admin redirects there. โ€‹

  • AdminShell imports AdminDashboardPage. The index route now REDIRECTS rather than rendering the page a second time: isActive() matches on the pathname, so /admin lit no nav item at all and you were looking at the Dashboard with nothing marked in the sidebar. Same idiom docs and venues already use.
  • Verified in a browser: landing on /admin ends at /admin/dashboard with the Dashboard nav item active, no .page-header-bar and no leftover .dash-split markup from the page it replaced.

The two real sources were REUSED, not rewritten. โ€‹

SourceWhat feeds itWhere it came from
The counts railuseDashboardRail(), the four calls the screens each count names already makeThe Dashboard this replaced
The changelogloadRecentReleases() through the docs API, reading docs/changelogs/dev/The same
  • What is NEW is two pure adapters beside the reader, because the card's row shape is not the reader's day-and-group shape: flattenReleaseEntries() and changelogKind(), with tests.
  • AdminDashboard.jsx stays a props-in view and AdminDashboardPage.jsx is the only file that knows where the numbers come from. That boundary is load-bearing: useAdminDashboard reaches ../../firebase, which initialises the Firebase app on import, so a view that imported it would make every Storybook story boot Firebase to draw a card.

Three things are deliberately NOT wired, each answered by her. โ€‹

WhatHer answer, 2026-08-30What shipped
"View all" on both cards"we might actually need to build this in somewhere"A <span>, not a link. No href, a title naming #1009, and --text-secondary rather than the page's blue: a blue link that does nothing on click is a defect report waiting to be filed
Alpha's six steps, and the hero blurb"I think these are honestly copy at the moment"Hardcoded, moved OUT of dashboardFixtures.js into dashboardCopy.js so the file the routed page imports is not named "fixtures". Its header says where a real source would come from (loadLaunchStages()) and why wiring it would be worse: the plan gives stage names and dates, and gives neither the at spacing nor the six steps
The Post Announcement modal#992 owns the storage model and the firestore.rules gate, and neither existsUntouched. It opens over the routed page, and onSubmit goes to a caller that passes nothing
  • Announcements have no READ path either, which is the same issue. The card now says "Announcements are not stored yet, so nothing can be posted or read here. Tracked in #992" rather than "No announcements yet", which would claim a working feature nobody had posted to.

What the live data showed that the fixtures hid. โ€‹

  • The flame went missing. useDashboardRail's rows carry a label, a value and a destination; the fixtures carry lit. Nothing failed. The rail simply drew seven identical tiles with a widow in the last row while every story showed the design. Mapped in useAdminDashboard (which tile is the flame is a fact about this PAGE, not about the reader) and re-captured.
  • Refresh was invisible. useDashboardRail sets loading: false when a read settles and never sets it back, so a re-read left the numbers sitting at their old values with no feedback. The button now tracks its own round trip. Flipping the reader's flag instead would blank every number on each refresh, which is worse than no feedback.
  • A changelog heading is free text. The fixtures had three kinds; docs/changelogs/dev/ has Fixed 111, Added 85, Changed 33, Security 29, Removed 7 and a dozen one-offs a person typed once. So the dot vocabulary is a CLOSED set with a neutral other, matched on the heading's first word (the repo contains "Fixed (round 2, re-review of the fix diff)"), and the legend lists only the kinds on screen.
  • A count in flight is not an absent one, and a failed changelog read is not an empty window. Both were single states in the design and are two states each against real services.
  • The card reads dev, not this branch. The docs API serves the repo's dev branch, whose newest changelog on 2026-08-30 is dev-08.21.2026. The 08.24 / 08.27 / 08.30 files exist only on feat/admin-and-merchant-portals, so the live card correctly shows Aug 21 and Aug 20. It will move forward when this branch merges.

The one thing worth reconsidering, flagged rather than acted on. โ€‹

  • The changelog's "View all" HAS a real destination and it was deleted with the old page: docsUrl('/changelogs/README') on the docs site, which the retired ReleaseStream linked as "All changelogs". The decision relayed for this build was that both View alls ship inert, so both do. If she wants the changelog one live it is one line, and docsSite.js comes back out of git history.

What the browser run measured. โ€‹

CaptureWhat it shows
../screens/routed-02-1440-viewport.pngThe routed page, real counts, real changelog, Dashboard lit in the sidebar
../screens/routed-modal-1440.pngPost Announcement open over the routed page
../screens/routed-01-1440-full.pngThe first run, kept as evidence: seven identical tiles, no flame, a widow in the last row. That is what a prop the fixtures carry and the loader forgets looks like
Live valueRead
Venues22,636
Users35
Offers to review3 (the fixture had this one as "Not available")
Lanterns lit now / Waves pending / New signups 24h / Merchant applications0
Changelog8 rows across Aug 21 and Aug 20, every one carrying its PR link, v0.1.0 ยท dev
  • Refresh re-reads both sources and disables while it does. The modal opens over the page, holds its title and message fields, and closes. It was NOT submitted.

What did axe say about the ROUTED page? โ€‹

Zero violations on the page, and the numbers match the scaffold's. โ€‹

ScopeViolationsIncompletePasses
.launch-page, routed01 rule / 19 nodes (color-contrast)22
.launch-page, routed, modal open01 rule / 21 nodes26
The scaffold's admin-dash-page story, for comparison01 rule / 19 nodes23
  • Same cause as the scaffold run: axe cannot resolve the hero's gradient wash, and every pair over it was measured by hand at 6.11:1 or better against a 4.5 bar. Routing did not change it.

The stories were re-swept after the wiring, and that is what found the one real a11y regression. โ€‹

  • 19 stories, 0 violations and 0 page errors on every one. Harness: ../harness/admin-dash-stories-after-wiring.mjs.
  • The sweep was not a formality. The new Loading state put an aria-label on a plain <div>, which has no role to accept a name, and axe failed it as aria-prohibited-attr on all seven pending tiles. The routed page scored 0 the whole time, because by the time anything settles there are no pending tiles left to measure. The same shape was one incomplete node on the truncated tile.
  • Fixed by saying it in a .launch-sr span instead, which is what the file already did for "No value". The lesson generalises: a state that exists only DURING a load cannot be measured on a settled page, so it needs a story or it ships unlooked at.

The whole DOCUMENT reports two violations, and neither is this page's. โ€‹

  • Running axe over the routed document rather than the page region finds color-contrast (4 nodes) and scrollable-region-focusable (1 node). A count on the whole document is not automatically the page's, so the run measured a CONTROL route with no Dashboard on it.
  • /admin/users reports the identical two, node for node: the four sidebar section labels (#666d76 on #18181b, 3.38:1 against a 4.5 bar) and the sidebar <nav> as a scrollable region with no keyboard access. Both are shell-wide and pre-date this work. The <nav> one is the same surface as #922.

What needed a decision before anything was wired? โ€‹

1. Does the sidebar collapse on this route? ANSWERED 2026-08-30: no. โ€‹

  • Her answer was one word. No route-level collapse behaviour was added.
  • The design draws a 68px icon rail; the portal's sidebar is 310px expanded and already collapses to 72px by the user's own control, which is the same idiom and is enough. The page sits in the main column either way.
  • "we might actually need to build this in somewhere." Filed as #1009; both ship INERT and say so at the point of use. See Three things are deliberately NOT wired and the note above it about the changelog's one real candidate destination.

3. Who may post, and what "Pin to top" means. ANSWERED 2026-08-30, no longer open. โ€‹

What the ruling meansWhat the scaffold does
The pin is a SINGLE-USER affordance, not a shared one"Only one pin at a time" is a statement about her own pin, so it reads correctly as a permanent line under the control. It is not a race between authors
Provenance exists on every record and cannot be backfilledEvery announcement in the fixtures carries an author, and AnnouncementsPanel shows it on every row rather than on the rows that happen to have one
  • Still open inside this, and it is a wiring question rather than a design one: withdraw is a THIRD state beside posted and deleted, so the reading panel will eventually need to know not to render a withdrawn record while the record still exists.

4. Alpha's six steps: real state or copy? ANSWERED 2026-08-30: copy. โ€‹

  • "I think these are honestly copy at the moment." Build, Prove and Clear stay marked done and the timeline needs no source at all. They moved to dashboardCopy.js with a header saying not to wire it, and why.

5. The title input cap. ANSWERED 2026-08-30, no longer open. โ€‹

  • "We didn't need to shorten the title input." The cap is overridden for this modal only, and the override is documented above so nobody restores it by tidying.
  • This page ships blue links now because the design does. If the pilot picks a different blue, or a token name, this page follows it.

Built with VitePress