Changelog
A log of additions, improvements, and fixes to the Canvas Design System.
October 5, 2026
v2.54.1: The app page grid, where agents look
- Improved The
/app/page grid (.app-page: eight columns of--app-grid-colwith--app-grid-gutter, a six-column main beside the two-column rail) was documented only on App Shell Layouts and in the curated JSON, which no agent entry point reached. DESIGN.md 7 now states its rules (the columns, the spans, which nested grids stay on it, the 1100px step), START-HERE opens with it for/app/work, andllms.txtnames it.ws_grid_start,ws_container_startand the app bar, dock and footer demos, whichds:findreturns for layout requests, now point to it, and--app-grid-col,--app-railand--app-content-widegained search phrases and purposes.
October 5, 2026
v2.54.0: Steering for every agent, and demo pages held to their CSS
- Improved The design-system-enforcer MCP (
.claude/mcp-servers/design-system-enforcer/) is tracked in git, without itsnode_modules; it was gitignored while.mcp.json,ds:eval,ds:build-evalandtest:dsall depended on it, and.mcp.jsonnow starts it from${CLAUDE_PROJECT_DIR}instead of one machine's path. AGENTS.md and GEMINI.md, which other agents read, now carry the design-system rule (aws_*component before HTML, a token before a hex or pixel value) and the two commands that apply it,npm run ds:findandnpm run check:ds. Both had said "There is no CI";lint:docsnow fails that claim while.github/workflows/ci.ymlexists. - New
check-drift.phpcheck 6 holds each demo page to its component's CSS. A Tokens table may list only custom properties that the component's rules incomponents.css(matched by its class block) or its template's own<style>read or set, and a retired alias (--phase-*,--mainsite-*,--coach-*) may appear elsewhere in a demo only where that CSS still uses it or the line says it is retired. It found 82 false claims in 25 demos, mostly--mainsite-primaryand--phase-learna month aftercomponents.cssmoved to the--brand*tokens, plus--transition-fastwhere the CSS reads--transition-interactive; all are corrected. The button demo's four identical phase-themed buttons are one secondary button, and the stat and modal examples pass--brand-inkand--brand. hero, listing-card and tooltip are styled elsewhere and reported as unchecked. The pre-commit hook now runs the drift check when a demo orcomponents.cssis staged.
October 5, 2026
v2.53.4: The solutions pages on ws_agenda
- Improved The five
solutions/bytype/pages draw their sample agenda withws_agenda.renderSoltAgenda()(includes/data.php) maps a library workshop throughsoltAgendaItems(): each item keeps its own start time, breaks stay breaks except the welcome and wrap-up blocks (activities), and an item named or phased as an icebreaker is one. Each page's hard-coded fallback agenda is aws_agendacall with the same copy, its times computed from a start and the durations (checked row for row against the old ones). The fourteen.solt-agenda__item, dot, name, description, duration and day-label rules are gone fromcss/solutions-type.css; the list frame takes the side padding. The per-type dot colours (two of them retired phase colours) are gone with them. Also fixed: the title bar's workshop name inherits the bar's white, becausecss/styles.csscolours every heading--text-dark.
October 5, 2026
v2.53.3: The Impact Report on ws_agenda
- Improved The Impact Report's "What happened" agenda, in the app (
app/views/prove/report.php) and on the share link (prove/r/index.php), isws_agendathroughirAgendaItems()inincludes/impact-report.php, instead of two copies of a bare<ol>. It now writes each item's type and groups a multi-day workshop by day; a stored snapshot withoutitem_typeorday_numberrenders as one plain list..prv-agenda__minsis gone. The library's workshop page is not moved: its agenda links each exercise to the library, andws_agendahas no per-item link yet.
October 5, 2026
v2.53.2: The first page on ws_agenda
- Improved The shared briefing (
planner/briefing-view.php) draws its interim agenda withws_agendainstead of the editor's input classes, which the read-only view had been hiding with overrides;briefingAgendaItems()inplanner/includes/briefing-data.phpmaps its blocks. Blocks carry no start time or type, so the agenda shows neither, and the total is written as the rows are ("3 hr 5 min"). The page now loadscomponents.css, so the AGENDA block must be on the host first. Five hand-rolled agendas remain.
October 5, 2026
v2.53.1: Review fixes for tabs that remember, text steps and optional fields
- Fixed
ws_tab_panel_endputs the tab's hidden field last in each GET form, just before</form>, and leaves it out when the form already submits a field of that name, so the browser never sendstabtwice. Inside a panel,ws_filter_barcarries that panel's tab in its form and its "Clear all" link, with or withoutkeep, rather than the tab the page loaded on, which is stale once someone switches tabs. The tags are read whole and in order, as a browser reads them. So<forminside a script, a comment, a textarea, an attribute value,<form-row>or<style-guide>is never taken for a form, and a form nested in another is ignored. Disabled, unchecked andform=-owned fields don't count as sending the tab. A form whoseactionloads another page is skipped: another path, another host or port, or a host with no path../view.php,../dir/view.phpand a bareactioncount as this page, and so does anymethodother thanpostordialog. The patterns are possessive, so a script of a megabyte or more no longer exhausts PCRE and silently skips the panel. Controls inside scripts, comments and templates no longer stop an otherwise empty panel from being a Tab stop. - Fixed
ws_tabswith numeric item ids opens the tab the URL names; the ids were compared to the query string without casting. - Fixed
ws_texttreats an unknownvariantas a caption rather than writing it into the class, and acceptsvariantandtagin any case. - Fixed
ws_filter_bar's "Clear all" link builds its query with&explicitly, so a host whosearg_separator.outputis&cannot double-escape it. Its guidance says two bars on one page need differentsearchLabels, since each names its search landmark. - Improved
ws_chip_inputtakesoptional, likews_input, and so do its PHP and React mirrors. The rest of the optional fields now use the option instead of "(optional)" typed into the label (prep link URL, deck client logo, Strategist save description, the two cancel questions), the planner's custom activity no longer says "Optional" twice, and Contribute marks Materials, Category and Instructions, which the server never required. The unused.ws-field__requiredand.ws-form-row__requiredrules are gone, and the ReactWsFormRow'srequiredprop is marked as ignored. - Improved DESIGN.md 11.6 and
ws_filter_barno longer disagree. The ban on Reset and Clear buttons now covers forms someone fills in, where one click wipes what they typed, and names the filter bar's "Clear all" link as the exception: it loads the unfiltered list, and Back restores the filters. A builder in the fourth friction run hit the conflict.
October 5, 2026
v2.53.0: Text steps, tabs that remember, and empty states that show their icon
- New
ws_text($text, ['variant' => 'title'|'caption']): the two text steps builders kept writing by hand.titleis the card-title step (Fraunces,--text-h4, semibold, anh3unlesstagsays otherwise);captionis--text-smallin--text-default, which passes contrast on a card where--text-muteddoes not.livemakes it a status for counts a script changes. The filtered-list recipe's card titles use it. Demo: Text. - New
ws_tabs(..., ['param' => 'tab']): the URL says which tab is open.?tab=notesopens Notes over the items'activeflags (an unknown or disabled tab is ignored),ws_tab_panel_endadds the tab as a hidden field to each GET form in its panel, andcomponents.jskeeps the URL in step as tabs change, so saving a note comes back to Notes. - Improved Optional fields are marked in words, not required ones with an asterisk (DESIGN.md 11.3).
ws_input,ws_selectandws_form_rowtakeoptional, which appends "(optional)" to the label, andrequirednow sets only the browser's check, with no red asterisk; the PHP and React mirrors match. On the pages that mix required and optional fields, the 30 genuinely optional ones are marked (Contribute, Connect, prework, planner edit, the planner modals, prep). A field was left unmarked when a script or the server rejects it empty, when it is a select that always sends a value, or when it is pre-filled and a blank falls back to a default. - Fixed
ws_empty_statedraws its icon from the Material Symbols font when there is no local SVG, asws_buttondoes. The default,inbox, has none, so the icon circle rendered empty, as didfilter_alt_offandbookmark_border.
October 4, 2026
v2.52.0: Tab panels that follow their tabs, and one search box above a list
- New
ws_tab_panel_start('{tab id}')/ws_tab_panel_end()write the panel aws_tabstab controls:id="{id}-panel",role="tabpanel", named by its tab (orlabel), spaced--space-4from the row, and open exactly when its tab is the active item, so a page that posts back to a tab opens on it. A panel with nothing focusable is a Tab stop; one holding a control is not. In every friction run, every builder hand-wrote these divs and set nothing from the server. The tabs demo uses the helper. - Improved
ws_filter_baris the search box above a list, with or without filters. Search-only, its button reads "Search" (with filters, "Filter";submitLabelsets it), the form is arole="search"landmark named like the field, andshowSearchLabelputs the field's name above it as a real<label>. Its guidance no longer sends search-only tows_input, which keeps search inside other forms and in headers.keepcarries named query parameters through the search and "Clear all" as hidden fields, so inside/app/(['view', 'step']) a search no longer sends the person back to Home. The filtered-list recipe's way out reads "Show all workshops".
October 4, 2026
v2.51.0: An agenda to read, a switch you can click by its words, and one rule for confirming a save
- New
ws_agenda($items, $options): a read-only running order, each row a start time, the title with its type written out, and the duration. Times run on fromstartthrough each numeric duration; an item's ownstartresets them and a duration written as text stops them rather than inventing one. Items with adayare grouped under Day labels. Breaks are muted, never coloured, so the activity-type colours stay on the Planner's canvas. Six apps hand-roll this today (.solt-agenda,.briefing-agenda,.prv-agendaand others); builders in the friction run reached forws_planner_cardand fought its Swap button. Demo: Agenda. - New
ws_form_row_start/ws_form_row_end: a form row around content that helpers print rather than return, such as a stack of switches, with the same label binding asws_form_rowand no output buffering in the caller. - New
ws_cluster_start(['grow' => 'first'])(orlast): that item takes the row's free width and keeps--grid-min-smbefore the row wraps, as a search field beside its button should. - Fixed
ws_toggle: the whole row is now one<label>, so a click on the text or the description flips the switch (only the slider did), and the input carriesrole="switch", so screen readers say "switch, on".ws_consent_row, whose control only enables its Send button, passesrolecheckboxand stays one. - Fixed
.ws-inputisbox-sizing: border-box, sowidth: 100%plus padding no longer overflows its column on a page without a reset. - Improved One rule for confirming a save (DESIGN.md 10.2, and the Alert and Toast demos):
wsToastwhen the person stays on the page; a successws_alertwhen the save reloaded it, because a toast fired during page load is easily missed; and a failed save never a toast alone.wsToastis the toast to call;window.Toast(js/toast.js) and the Planner'sshowToast()are legacy, and the build eval no longer loadsjs/toast.js. - Fixed The playground runs a fixture's calls in one output buffer, as a page would, so a pair whose start buffers its own content previews in order.
- Fixed Eleven catalog entries (
ws_modal,ws_tabs,ws_toggle,ws_tooltip,ws_accordionand others) now carry their demo's whole accessibility table: the generator took only the first block under the heading, so a demo that opens with a sentence lost its rows. A builder foundws_toggle's role rule missing this way.
October 4, 2026
v2.50.0: Layout helpers, and a form row whose label names its field
- New Four layout helpers own the space between components:
ws_stack_start(one above the other, with agapstep, as adiv,section, list orform),ws_cluster_start(a wrapping row withjustifyandalign),ws_grid_start(as many equal columns as fit above a column floor, no breakpoints) andws_container_start(a centred--container-*column with gutters), each closed by its_end. They nest (closing one out of order raises a warning), lists getrole="list", the stack spaces withgapso a hidden input adds no space and no child's margin leaks in, a button in a stack keeps its own width, and cards in a list grid's row end level. Styles sit last incomponents.cssbetweenLAYOUT: STARTandLAYOUT: END; the demo is Layout. In the first friction run every builder hand-wrote these. - New
--grid-min-sm,--grid-min-mdand--grid-min-lg(200, 280 and 360px): the narrowest aws_gridcolumn gets before the grid drops one. - Fixed
ws_form_row's label now names what it wraps. With one control inside it getsforthat control (adding an id if it has none), the helper or error joins the control'saria-describedby, an error setsaria-invalidand is announced, andrequiredsetsaria-requiredbehind anaria-hiddenasterisk. With several controls, or one that already has its own label, the row is a labelledrole="group", so no control carries two names. Controls in a<template>or a script are not counted, and attribute values are parsed, not pattern-matched. Before, a field labelled only by the row had no accessible name. Same in the portablecomponents/php/FormRow.php;components/react/FormRow.tsxtakes anhtmlFor. - Improved The three screen recipes and the component pairs on
composition.phplay out with the new helpers, so copying one copies no CSS;.ds-recipe-stack,-grid,-row,-actionsand-narroware gone fromrecipes.css. The workshop list's loading placeholders share itsws_grid, so they keep the cards' columns. The settings form's actions now lead with Save, aligned left with the fields (DESIGN.md 11.6). - Improved DESIGN.md 11.7's form example gives each field its own label and hint and lays the form out with a stack and a cluster; it taught the unbound row label, a red asterisk that 11.3 forbids, and a
placeholderthe row ignored. 7 names the layout helpers.
October 4, 2026
v2.49.0: Agents say where they had to guess
- New The build eval asks each builder agent for a friction log: every point where the system left it to guess, find nothing, or choose between sources that disagree. A run merges the logs into a
friction.jsonbeside the builds, ranked by the helpers and tokens they name, so the next fix is the one agents trip on most. It is never scored. - Improved Every semantic token in
platform-tokens.cssnow states its purpose (69 had none), with contrast where it decides use:--text-mutedfor meta lines,--text-faintdecorative only, status colours for icons and fills with their-inkpartner for text. Component tokens thews_*components do not read say so, and name what does. - Improved A component's
a11yincomponents.generated.jsonis now oneConcern: behaviorline per rule; 50 entries read as run-together text before. - Improved The design-system-enforcer MCP reloads the catalog, synonyms and tokens when any of them changes, instead of answering from the catalog the session started with.
- Improved
llms.txtgives the same source-of-truth order as START-HERE.md, leads withcomponents.generated.json, and is now held to its claims bynpm run lint:docs. - Fixed
tokens.dtcg.jsonexported 20 tokens at their@mediaoverride instead of their value: every duration and transition as0s !important(reduced motion), the focus ring asHighlight(forced colours),--text-mutedat its high-contrast step. The generator now keeps the base definition, as the catalog does, and reads values that span lines; all 356 match the catalog. - Fixed A token whose value spans several lines (
--paper-grid-image) kept no purpose in the catalog: the generator looked for its comment above the last line, not the first. - Fixed
ds:findand the MCP'ssuggest_componentlist every option a helper takes, with its type. Options with no default were dropped, which hidws_agent_proposal'stitle,why,adjustandreturnFocus; three of five builders in the first friction run had to dig them out of the JSON. - Removed
figma-tokens.json, untouched since May and read by nothing, still listed the retired blue, purple and emerald phase colours as current.tokens.dtcg.jsonis the Figma and Tokens Studio export.
October 2, 2026
v2.48.3: A waitlist form for the Salt to Taste thank-you page
- New
.taste-waitlistincss/taste.css: aws_inputand a secondaryws_buttonstacked inside the existing.taste-onwardblock, with a status line that keeps its height so the confirmation does not shift the page. Errors read in--brand-ink.
September 30, 2026
v2.48.2: A billing switch on the homepage Pro card
- New
.ctr-billingincss/index2.css: the /pricing/ Monthly / Yearly toggle at card size, two ghostws_buttons in a--brand-linetrack with the chosen one filled--brand-ink. A negative block margin keeps the Pro card's head row at the h3's height so its price lines up with Free's. - New
.ctr-plan__cta.is-busyand.ctr-plan__err: the Pro button dims while Stripe checkout opens, and a failure reads under it in--brand-ink, as on /pricing/.
September 30, 2026
v2.48.1: The listing card moves in sequence
- Improved Listing card hover runs as one sequence in
css/listing-redesign.css: the card lifts, then the category tile warms to--brand-softand grows to 106%, then the title turns--brand-ink, then the actions brighten, 40ms apart. Delays apply on hover only, so leaving returns everything at once. Hover-capable pointers only. - New
listing-card-enter: on the first library listing page of a session (.listing-enteron<html>), the first nine cards fade up 12px in a 40ms stagger,--duration-slowand--ease-spring. - Fixed The listing card's motion now honours reduced motion: no lift, no tile scale, no delays, no entrance. The tile's colours moved from an inline style in
includes/partials/listing-card.phpto--brand-tintand--brand-inkin CSS.
September 30, 2026
v2.48.1: Every call site on contract
- Fixed
.ws-field__hint, the help linews_inputrenders, had no styles, so every input hint showed at body size; it now shares the.ws-field__helperrule. - New
ws_inputtypefileis styled (the picker button reads as a secondary button) and documented. - Improved
ws_section_headertakestag: h1on a page with no hero, where it opens the page. - Fixed No call site passes an option value or name its component does not support. Status colours go through the
statuskey, activity types throughws_activity_badge, and button attributes throughattrs.
September 30, 2026
v2.48.0: Built for agents, and for agent screens
- New A shared set for AI screens:
ws_ai_markand thews_badgeaivariant ("Prepared by Workshopr"),ws_agent_status(thinking, or stages with a retry on a failed one),ws_agent_steps,ws_agent_check,ws_agent_proposal(before and after, Apply, Not now, Undo),ws_source_chipandws_consent_row.js/ws-agent.jsdraws the same markup from JavaScript, and a parity test holds the two together. - New Three screen recipes with default, loading, empty and error states: a filtered workshop list, a settings form and a confirmation before deleting.
ds:findand the MCP return a recipe when the request is for a whole screen. - New
npm run ds:build-evalscores what an agent builds, not just what it finds: components, option values, tokens, legacy patterns, rendering, required states and keyboard access, in a locked-down PHP sandbox. - Improved An option value is valid only when
components.cssstyles it, andgenerate-catalog.php --checkfails on any disagreement. Every entry says when to use it, when not to, what to choose instead, and what accessibility work the caller owns.ws_inputlists the options it really reads (hint, nothelper). - Improved The playground previews most helpers, curated or built from the signature, with visual baselines for each curated fixture. START-HERE.md opens with the five-step agent workflow.
- Improved
components.cssreads the five brand tokens instead of the legacy phase and site aliases, with no change to any resolved value. A new ratchet incheck-page.phpstops pages adding.modal-overlayor hand-rolled card classes. - Fixed Tabs follow the WAI-ARIA pattern: arrow keys, Home and End, and panels labelled by their tab. Filter bar dropdowns and search have accessible names.
- Fixed Secondary button text uses
--brand-inkand thews_empty_statedescription uses--text-default, so both meet contrast guidelines. - Fixed Token cycles that silently broke styling when stylesheets loaded in a different order:
--font-bodyand nine more incss/tokens/_variables.css, self-references in the academy, DNA and auth modal sheets, and the Planner's--color-warning.
October 1, 2026
v2.47.2: One contract for agents
- Improved
components.generated.jsonis now the contract every agent tool reads. Eachws_*function, includingws_header,ws_section_hero, the DNA and app shelf helpers andws_workshop_list, carries its option values and defaults, demo, playground fixture, states and scope. A newtokenssection lists every token with its purpose, group, tier and, for a legacy alias, the--brand*token to use. A newlegacysection, fromlegacy-patterns.json, names the lookalikes to avoid. - Improved
ds:findand the design-system-enforcer MCP rank requests with one shared search over the catalog andsynonyms.json, andds:findnow returns tokens for styling requests. The MCP warns on legacy alias tokens, lookalike classes and option values the catalog does not list. - New
npm run ds:evalscores both finders on plain-language requests and fails if a score drops. Two new gates:check-drift.phpfails when a token is missing from the catalog, andgenerate-catalog.php --checkholds a completeness ratchet incatalog-baseline.json. - Fixed
tokens.dtcg.jsonregenerated with the five brand tokens, sonpm run check:dspasses. START-HERE.md names commands instead of hard-coded counts and is now checked bynpm run lint:docs.
October 1, 2026
v2.47.1: One red, five names
- New Five brand tokens in platform-tokens.css:
--brand,--brand-ink,--brand-line,--brand-softand--brand-tint. - Improved The 53 phase and app reds (
--phase-*,--mainsite-*,--planner-*,--coach-*and the rest) now point at those five instead of repeating the hex, so the red changes in one place. Every token resolves to the value it had before. Error, chart and category colours keep their own values.
September 30, 2026
v2.47.0: Find, preview, verify
- New Component playground with editable states, isolated previews, and matching PHP examples.
- Improved Catalog search and a database-free browser suite give Claude a repeatable component workflow with visual regression checks.
September 2026
v2.46.1: Plan's toolbar is its tools; a wider launcher
- Improved
ws_phase_toolbar('plan')has no lead or trail pill. The canon (includes/components/phase-toolbars.php) drops thetimerread-out and thetone: successtime-left read-out, since the strip above the builder's canvas carries both.tone: successstays supported inws_app_dock(); nothing uses it now. Spec, demo, DESIGN.md §12 andPhaseToolbarTestfollow; Synthesize's count is now the example of a wordless pill. - Improved App launcher (
.app-launcher__panel,app/css/app-shell.css): 720px wide instead of 536, still two columns, so each app's description stays on one line.
September 2026
v2.46.0: Comments from a workshop card
- New Workshop card menu: Comments (
ws_wl_menu_template(),data-wl-act="comments",data-wl-need="comments"), after the phase jumps.app/js/workshop-list-menu.jskeeps it only where the page carries the comments panel, and opens it throughwindow.wsCommentsOpen(id, name, returnTo), which the panel script now exposes: it opensws_side_panel()on a named workshop, with the subtitle naming it, or switches workshop in place when the panel is already open, and hands focus back to the card'smore_verton close. A listing books the panel for its page throughappCommentsBookForList(); a bar that also asked for it keeps its phase. - New Comments panel save step (
.app-comments__save): on Strategize with no saved workshop, a small primaryws_buttonSave to account sits under the note. It carries the bar'sdata-st-save-accounthook, so it opens the same naming dialog.strategist.jsfiresws-plan-savedafter a save, and an open panel reloads on it and moves focus to the comment field.
September 2026
v2.45.1: The header paints signed in
- Fixed
ws_header()renders the auth state its cookie session already resolves for the account menu:.wsh-header__signinarriveshiddenand.wsh-header__account[data-wsh-auth="in"]visible for a signed-in reader, the reverse for everyone else. It rendered Sign In/Register for all and leftwshApplyAuth()to swap in the avatar after load, which flashed on every page.wshApplyAuth()now counts the server session as signed in while localStorage has the user but not yet the Bearer token; clearingworkshopr_userstill signs the header out. Pinned bytests/E2E/header-auth-first-paint.spec.js. - Fixed
ws_header()dropdowns take.wsh-header__link--activeon any page their menu lists, not only the one their own href names, so thehomesection's Company stays lit across all five company pages. Off-site and fragment items never match. Pinned bytests/E2E/header-company-active.spec.js.
September 2026
v2.45.0: Side Panel; comments on every phase
- New
ws_side_panel($body, $options)(includes/components/side-panel.php,.ws-side-panelincomponents.css,wsSidePanelOpen/Close/Toggle()incomponents.js, demo atcomponents/demos/side-panel.php): a non-modal panel that slides in from the right edge.role="dialog"witharia-modal="false"; nothing goes inert and focus is not trapped. Any[data-ws-side-panel-open="{id}"]toggles it and keeps itsaria-expandedin step; Escape (focus in the panel or on its toggle) or the close button put it away and hand focus back; one is open at a time. It firesws-side-panel-openandws-side-panel-close. In on--duration-enterand--ease-spring, out on--duration-exitand--ease-in, transform only. It layers one step under overlays (--z-overlayminus 100), so modals, toasts, header menus and the launcher land on top of it. Placement is--ws-side-panel-top/--ws-side-panel-bottom(the app shell keeps its 60px header and its footer clear); opened from a bar (.ws-app-baror[data-ws-side-panel-anchor]) it starts under that bar and follows it as the page scrolls; full screen below 600px. Added toworkshopr-design-system.jsonand the generated catalog. - New
ws_side_panel()branded variant:'variant' => 'branded'puts the head on the red band the brandedws_modalwears (--phase-learn, white title, subtitle on--overlay-medium, close on--overlay-light), and'icon'adds the icon pill beside the heading (.ws-side-panel__icon, on a light veil in the band). The comments panel wears both. - New
ws_app_bar()action that opens a side panel: white and filled with a red icon while its panel is open ([data-ws-side-panel-open][aria-expanded="true"]). - New Comments panel (
app/includes/comments-panel.php,app/css/comments.css,app/js/comments-panel.js; Figma: Workshopr Teams mockups, Leave a comment, 10:1465) onws_side_panel().appCommentsBarAction($phase)putsmode_commentbeside the save on Strategize's, Plan's, Facilitate's and Synthesize's bars, andappCommentsPanelForPage()renders the panel at body level. The form isws_select,ws_input(textarea) and a full-width primaryws_button; the thread draws.ws-avatar(red for the workshop's owner, grey otherwise),.ws-badgefor Facilitator and a.ws-pillcount. Each thread's first comment carries Resolve beside Reply (.app-comments__actions), shown only to an owner or editor or the thread's author, as the API allows; a resolved thread leaves the list and.app-comments__resolvedoffers Undo, with focus moved onto it. Show resolved (.app-comments__resolved-section, a toggle witharia-expanded) lists the resolved threads under the open ones, their context and text on--color-ink-muted, a neutral Resolved.ws-pillon each first comment, Reopen in place of Resolve and no Reply. The API's list, add and resolve answers carryresolvedandresolved_totalbeside the open threads. It reads and writes throughapi/plan-comments.php(list, add withparent_idfor replies, resolve) and says comments are not switched on where that API or its table is missing. Facilitate's live shortcuts (facilitator/js/modules/keyboard.js) and the builder's undo (planner/js/modules/history.js) stand aside while focus is in a side panel.
September 2026
v2.44.2: One workspace menu; install on every rail
- Improved
ws_header()account menu: the Workspaces section is gone, with.wsh-header__menu-section,.wsh-header__menu-divider,.wsh-header__menu-item--teamand the.wsh-header__team-*rules; the menu is My Stuff, Settings and Sign out. The workspace switcher (.wsh-ws) is the one place to change or create a workspace.page-baseline.jsoncounts the switcher's trigger among the header's raw buttons, as the avatar and app-switcher triggers already were. - Improved
appShellRail(): the Install Workshopr block (.app-rail__install, its toast slot and the secondaryws_button) renders on every rail, straight under the journey card, or in its place on a view without one. It was Home's alone, at the foot of the rail. - Improved Workspace page (
app/css/team.css): Recent files is one card per file on--card-paddingand--card-radius(.app-team__recent-list), replacing the single.app-team__recent-card. A file's Launch menu draws each phase with the side nav's own icon, read from the nav's config and drawn asws_sidenavdraws it (the local SVG,.app-team__menu-icon, else the font glyph). The rail's Invite people button aligns right. - Fixed Favicons:
images/favicon-32x32.pngandimages/favicon-16x16.pngare rendered fromimages/favicon.svg(graph_3), andimages/apple-touch-icon.pngis the app's 180px graph_3 icon. They still carried the retired W, and Chrome takes a PNG whosesizesfits over the SVG, so the tab showed the W.includes/header.phpversions the three links (?v=2) so a cached icon is refetched. The install toast (app/js/app-install.js) draws graph_3 wherewsToast()draws its info circle.
September 2026
v2.44.1: Paper empty phases; no Beta pill
- Improved Empty phases on paper: the Strategize, Plan, Facilitate and Synthesize lists pass
'class' => 'wl--phase'tows_workshop_list(), andapp/css/app-shell.csssets their.ws-empty-stateon--bg-paperin place of the component's darker paper. Home and Prove keep the component's own. - Improved
ws_app_footer()full variant: the 2.0 Beta pill is gone, with.ws-app-footer__betaand the shell's size override for it; the bar now reads the wordmark and the build version. The component demo follows.
September 2026
v2.44.0: A settings page for each workspace
- New
/app/?view=team§ion=settings: the team view is two pages now, set bydata-team-modeon its root. The workspace page keeps the invite notices and Workshops; the settings page puts Members, Plan & seats (#team-billing) and Team settings in the main column, the settings rows side by side there. Both render every section andteam.jsruns the same;[data-team-mode-only]inapp/css/team.csskeeps each section to its own page. The workspace page's rail gains a full-width secondary Workspace settingsws_buttonunder Seats, and Seats' Manage seats opens the settings page at Plan & seats. Switching workspace from the settings page stays on settings. (The switcher had a Workspace settings row too; it was taken off, so the rail's button is the way in.) - Fixed No first-paint flicker: the view resolves the workspace it opens on the server (the address, the current workspace, then the first team) and renders its name, the reader's role line and the full trail, handing team.js the same id through
data-current-workspace. "Work together." is only the no-team state now. The indeterminatews_progressgives way tows_skeletonplaceholders shaped like the sections on their way (the Workshops card's rows, the member tiles, the Plan & seats card), hidden when the section they stand for draws, and the rail's Seats card shows its own skeleton (.is-pending) for the people it will fill for. - Improved Workshop cards: the Workshops container is gone, and with it
.app-team__card--workshopsand its dashed-to-solid red mask; the heading, count and Add a workshop sit on the page (.app-team__head--above) and the list is.app-team__ws-grid, tiles three across on--card-bgwith--shadow-smlike the member cards (icon and options menu on top, the name clamped to two lines, then its details), dropping to two and one by a container query on.app-team__workshops. Plan & seats' bar isws_progress()now rather than hand-written markup. - New Recent files on the workspace page, grown from the Teams build's "New in the library" rail card into a main-column section:
.app-team__recent, the heading above a card of rows (icon tile, name, then kind, workshop, who and when), opening onws_skeletonrows. Fed by/api/teams.php?action=recent_files(wsTeamRecentFiles(),includes/team.php): the workspace's agendas, the reader's own syntheses (anyone else's is private, so it would be a dead link) and, while Prove is on, Impact Reports. Each row carries the page'smore_horizmenu (kindfile) to launch its workshop in Strategize, Plan, Facilitate or Synthesize, on the listing's routes (ws_wl_stage_routes()); each file names itsplan_idfor it. - Improved Workspace page to its Figma (Workshopr Teams mockups, S03):
.app-team__hero--home(the Workspace pill with the role line, the name,.app-team__peoplewith an.ws-avatar-groupand the count of each role);.app-team__month, three counts on a card;.app-team__ws-toolbarwith a searchws_inputand the shared listing's.wl-chipbuttons (workshop-list.cssnow loads on the team view) for All, Mine and Shared with me; and.app-team__ws-card, whose stage chip (.app-team__stage), next-step label and route come from the listing's own rules (ws_wl_cta_for_stage(),ws_wl_stage_href()) through/api/teams.php?action=workshops. Invite people is a railws_cardabove Seats. (The hero's New workshop button and its dialog were built, then taken off.) Not built, because this branch has no data for them: the Library tab, the Editing now chip, the email domain and the Builder and Viewer roles.
September 2026
v2.43.0: Create a workspace in a dialog
- New
appWorkspaceCreateModal()(app/includes/workspace-modal.php, driven byapp/js/workspace-create.js): the switcher's Create workspace row ([data-wsh-ws-create]) opens a brandedws_modalwith onews_form_row()field, whose helper line becomes the error after blur and carries any refusal from/api/teams.php?action=create, and the actions inws_modal_footer(). The app shell renders it on every page the switcher shows on; the row keeps its href to the team page's create card for when script is off. - Fixed Focus in a dialog opened from a menu:
wsModalOpen()'s trap focuses the first control (the close button) once the overlay has faded in, so a dialog that wants its field focused does it after that, on the overlay'stransitionendand a timer past the trap's. Focus moves to the switcher's button before the dialog opens, so closing hands it back there rather than to a row in a hidden menu.
September 2026
v2.42.1: The brand mark is graph_3 again
- Fixed
images/icons/graph_3.svgwas a copy ofpicture_in_picture_alt.svg, so everyicon('graph_3')drew a window: the homepage hero badge (section-hero.php), the/app/sign-in brand, the planner's logo menu and onboarding. It now holds the real Material Symbolsgraph_3path, the oneimages/favicon.svgand the header's font glyph already draw. It keeps its#E54D4Dfill, because onboarding loads it as an<img>wherecurrentColorcannot reach. - Improved The
/app/install icons are the brand mark:app/icons/swaps the four squares for a whitegraph_3on the red tile, with PNG 192 and 512, a full-bleed maskable 512 and a 180apple-touch-icon.png(Safari and iOS ignore SVG touch icons).app/manifest.jsonlists them. - Improved Rail promo cards are light red on every rail:
.app-rail__tips(Tips, Intervention Cards, Academy) rests on--phase-learn-very-light, as the Strategize rail already drew it; hover firms the edge to--phase-learn-light. - Improved Toasts clear the
/app/frame footer: above 900px,body.app-shell > .ws-toast-containersits--app-footer-height(48px, new, also the footer's height) plus--space-6up. Home's install toast opens in.app-rail__install-toast, a live-region slot above its button, rather than in the corner stack.
September 2026
v2.42.0: Cards are warm paper
- New
--card-bgis now#F7F1EA, a warm paper tone between--bg-paper(#FAF7F2) and--bg-paper-dark(#F0EBE3). It was an alias of--bg-surface; it now holds its own value at the semantic layer, the way--toast-bgdoes. White stays for what opens over or inside a card: modals, menus, inputs and checkboxes. Mirrored inworkshopr-design-system.json(colors.backgrounds.card),tokens.dtcg.json(card.bg), the Colors and Tokens pages, andDESIGN.md§7, which no longer calls cards white. - Improved Every component that already read
var(--card-bg)follows without an edit:ws_card,ws_choice_cards,ws_task_checklist, the library.listing-card, and in/app/the workshop card, the settings, contribute and sign-in cards and the stuff rows. - Improved The
/app/cards that painted white directly now read the token:.app-team__card(app/css/team.css), the kanban card.wl-kcardand timeline row.wl-tl__row(app/css/workshop-list.css), and inapp/css/app-shell.cssthe handoff sheet.app-shell__sheet, Plan's Strategist nudge.lp2-nudgeand the Strategize rail's compact checklist. Fallbacks that still said#FFFFFFbeside--card-bgnow say#F7F1EA. Tables, list containers, menus, the launcher and the Synthesize action rail keep their white. - New Figma:
color/card/bgin the Canvas Design System'sPrimitivescollection (fill scopes, WEB code syntaxvar(--card-bg)) with a matching paint style, bound as the fill ofCard,Listing card,Task checklist,wl-card,card-296,rail-todos,rail-film,plan-choice-cardandplan-nudge. Consuming files pick it up after the library is published.
September 2026
v2.41.0: Workspaces on the components
- Improved Workspace page (
app/css/team.css): the shell trail (appShellCrumbs()) now opens it, with the open workspace's name set byteam.js..app-team__card--workshopssits on--bg-paperinside a 1px--phase-learnedge dashed 4 on, 4 off, drawn as an SVG mask on::beforebecauseborder-style: dashedcannot set the dash or the gap; with workshops in it (.has-workshops, set byteam.js) the mask goes and the 1px border shows solid in the same red..app-team__invite-cardfills with--phase-learn-light. Members and pending invites are.app-team__gridtiles three across, on--card-bgwith no container card around them, dropping to two and one by a container query on.app-team__members's width rather than the viewport's. The seat count is a largews_selecton the Seats row, tied to the billing form byform=""and enabled only for the owner once billing is live. - Improved The workspace dialogs are on the components: Invite teammates and Add a workshop to the team now follow the shell's modal pattern (
appPlanTemplateSaveModal(),appStrategizeNewCaseModal()): the branded header with an icon and a subtitle, fields throughws_form_row()withws_input()andws_select()in place of raw<input>and<select>, the row's remove asws_icon_button(), the empty state asws_alert(), and the actions inws_modal_footer(), whose type=button primaryteam.jssubmits along with Enter. Per-row invite errors use.ws-form-row__errorand.ws-form-row--error, witharia-invalidandaria-describedbyon the field.group_addis not inimages/icons/, so the dialog wearsgroupsand its buttonadd. - New Rail Seats card (Figma: Workshopr Teams mockups, Card · Builder seats, 12:1528):
appShellRail()takesafterProfile, HTML set directly under the DNA card, and the workspace page puts aws_cardthere built fromws_badge(status, success),ws_progress, an.ws-avatar-groupofws-avatar--xsand a secondaryws_button, styled as.app-rail__seatsinapp/css/team.css. The frame's copy is the new role model's (Builders take seats; viewers and guests are free); on this branch every member and pending invite takes a seat, and the card says so.ws_card()takesidbut notattrs, and itsdataoption prints nothing, so the card is found by id and kept out of sight by a class untilteam.jsfills it. - Improved Team settings, redesigned and in the rail:
.app-team__settingrows (what it does, then its control; side by side in the main column, stacked under.app-rail), a.app-team__dangerzone on--color-error-very-light, and Leave and Delete in brandedws_modaldialogs with adangerprimary inws_modal_footer()in place ofconfirm()andprompt(). The team root (data-team-root) is now the whole.app-page, rail included, soteam.jsreaches the cards there. - Improved Invite notices: an email-matched invite card carries a close
ws-btn--icon-only(remembered per browser underworkshopr_team_invites_dismissed) and fades after 30 seconds on--duration-slow, the clock pausing on hover and focus aswsToast's does. - New Workspace switcher in
ws_header()(Figma: Workshopr Teams mockups, Workspace menu, 70:6288),.wsh-ws: a trigger beside the logotype and a menu of Personal and every workspace, with Create workspace and Workspace settings. It renders only when the page passes'workspaceSwitcher' => true(the app shell does, through$ws_header_opts) andteamsVisible()is on.window.wshSetWorkspace(teamId)lets a page correct the current workspace after load.
September 2026
v2.40.4: Toasts are plain text
- Fixed
wsToast()wrote its message and action label intoinnerHTML, so any caller passing a server message that carried a user-typed name (a team name inapp/js/team.js, a participant name in the pre-work tool) handed that name to the HTML parser. Both are now set withtextContentinincludes/components/components.js; the icons stay static markup. The legacy inlineToast.show()inincludes/footer/toast-inline.phpdoes the same for its title, message and action text. Pass raw text: the callers that pre-escaped (app/js/plan-template-save.js,plan-templates-page.js,plan-template-picker.jsand the first-save toast inplanner/js/planner-v2.js) no longer do, or they would show&. The planner's “Removed · Undo” toast, the one caller that passed markup, now appends its.toast-undo-btn,.toast-undo-hintand.sr-onlyspoken form as nodes inside.ws-toast__message, so it looks the same. The four pages that loadedcomponents.jswith no?v=(the pre-work tool,dna/about.php,intervention/card.phpand the design-system footer) now cache-bust it. Covered intests/toast-a11y.spec.jsand the planner delete-and-undo test inplanner/tests/planner.spec.js.
September 2026
v2.40.3: Accordion panels have a name
- Fixed
ws_accordionpanels pointedaria-labelledbyat the item wrapper, which holds the panel itself, so screen readers named each region with its heading and its whole body text. The trigger now carries{id}-triggerand the panel points at it, and theexpand_morearrow isaria-hidden. Same change inincludes/components/accordion.php,components/php/Accordion.phpandcomponents/react/Accordion.tsx.
September 2026
v2.40.2: The intro survey on the system
- Improved
/survey/is rebuilt fromws_pill,ws_progress,ws_radio_card_group,ws_inputandws_buttoninside the site header and footer, on a card that matches/start/. Its own stylesheet and inline SVG buttons are gone; layout lives insurvey/survey.csson platform tokens. - Fixed
ws_radio_card_groupicons had no size and painted at about 12px. The survey sizes them at 24px locally; the component itself still leaves it to the page.
September 2026
v2.40.1: Modal footer attrs, focus rings
- Fixed
ws_modal_footer()now passesattrsthrough to its left, cancel and primary buttons (includes/components/modal-footer.php). The docblock always promised it; the template dropped them, so a footer button could not carrydata-modal-closeor adata-*hook. It broke the Cancel subscription dialog in Settings (neither button did anything) and the demo buttons on the modal page. - Fixed Keyboard focus rings are sky-blue site-wide.
*:focus-visibleandbutton:focus-visibleincss/styles.csspainted--primary-red; they now use--focus-ring-color,--focus-ring-width,--focus-ring-offsetand--focus-ring-halo, the one non-red accent Red Unification keeps. - Improved Modal body copy in Settings (
app/css/settings.css): spacing between paragraphs and fields,--text-bodyon--color-ink, and an underlined--phase-learn-darklink, instead of the unstyled 9.8px black textws_modalleaves. - Improved The Cancel subscription dialog is
variant => 'branded', matching every other dialog in the app shell.
September 2026
v2.40.0: Plan templates
- New Plan's toolbar gains a ninth slot,
template(Save as template, glyphbookmark_add), between Export agenda and Slide deck inincludes/components/phase-toolbars.php.images/icons/bookmark_add.svgis new. The builder shows the slot once the workshop is saved.design-system/spec-phase-toolbar.mdlists it. - New The Save as template dialog,
appPlanTemplateSaveModal()inapp/includes/plan-template-modal.php: a brandedws_modalshaped like Name Workshop, onews_form_row,ws_modal_footer. It renames too. A clashing name draws an inline Replace it inside the row's error (.pt-save-replace). - New The template picker and card (
app/views/plan/template-picker.php,app/css/plan-templates.css): a large brandedws_modalwithws_tabs, and.pt-cardonws_card: name, job line, duration and item count, an outline of four with the planner's activity-type dots, aws_badgefor slots to fill, and a Use button. Skeletons, empty and error states arews_skeleton,ws_empty_stateandws_alert. - New The My templates rows (
.pt-rows,.pt-row) with an inline delete confirmation (.pt-del__pop) drawn as the workshop card menu's is. - Improved The workshop card ⋮ menu offers Save as template under Duplicate, for workshops you own.
September 2026
v2.39.4: Control-centre homepage rules
- New
css/index2.css, the.ctr-*rules for /index2.php: the bento rows at the frame's uneven widths, captions below each tile, cropped screenshot tiles, the fanned intervention cards, the plans, the voices and the red closing box, all on platform tokens. It loads the Fraunces 600 and 700 italics the display emphasis uses, since the site loads only the 400 italic.
September 2026
v2.39.3: App analytics admin rules
- New
.aa-*rules incss/admin/admin-v2.cssfor App analytics: period buttons, filter chips, phase dots and the journey timeline, all on platform tokens.
September 2026
v2.39.2: Homepage facts, table and FAQ sections
- New
.tue-factsand.tue-faq(css/index-new.css). Two homepage sections on the plans' rhythm, headed byws_section_header: a two-column fact list, a semantic stage table (.tue-facts__table: Inter,--gray-700header on--gray-50,--gray-200rules, horizontal scroll below 900px), and aws_accordionFAQ. Tokens only; the h2 shares the plans' existing size rule.
September 2026
v2.39.1: Shell-scaled type aliases in app-shell.css
- New
--shell-text-*aliases onhtml.app-shell-root(app-shell.css). The shell root is 70%, so the rem type tokens paint at 70% inside it;--shell-text-h3,-small,-meta,-caption,-microand-nanodivide the token by--shell-scaleso a component the frames draw at library size can say so with a token. The footer, the See All button, the Strategize bar title and the shell breadcrumbs use them in place of px literals. - Improved Icon boxes and negative margins on the rail take tokens:
--icon-xsand--icon-mdfor the tips and DNA arrows,--space-1and--space-1-5for the academy art and medal offsets. The four values no token lands on (the 9px beta tag, the See All button's 9px pad and corner, the kit head's 5px nudge) carry ads-lint-disable-linewith the reason, matching the file's existing convention.
September 2026
v2.39.0: First-run form, rehearsal bar, one checklist label set
- New Home's first-run form (
app-shell.css)..dash__firstrun-title,-lede,-form,-dateand-alt, tokens only, built fromws_inputandws_button. The.app-ob*welcome overlay and.app-tour*coach-mark rules are removed with the JS that drew them. - New
.fac-rehearsal-bar(facilitator/css/facilitator.css): a persistent bar above every console screen in a rehearsal,ws_badgeplus twows_buttonlinks, and a red icon override on the rehearsal-complete screen. - Improved The Display Text Pattern keeps its red in /app/ headers.
.app-crumbs + .app-hero .app-hero__title emtakes--mainsite-darkinstead ofinherit, and the.st-page--home .st-hero__title emink override is removed, so the four phase indexes and the Strategize index match the steps: bold italic dark red on the light red underline. - Improved One label set for the journey checklist (
appRailJourneyCard(), thews_task_checklistdemo, andrail-todos130:332in the Canvas Design System Figma file, whose checks are now circles to match the component). The compact subtitle's step count is derived from the rows.
September 2026
v2.38.2: Session-start dialog cards, Strategize rail and hero on the phase indexes
- Improved One hero for the four phase indexes.
.app-crumbs + .app-hero(Plan, Facilitate, Synthesize) is drawn to.st-hero's measurements: 24 under the hero, the lede at--text-bodyon a 62ch measure in--text-secondary, and the listing heading (.wl__title, plus Plan's.launchpad-choice-screen__title) at--text-h2semibold. All four titles wear the Display Text Pattern: upright bold first line,embold italic--mainsite-darkon an inset 8px--mainsite-lightunderline (underlined words in an /app/ header are always red);.st-page--home .st-hero__titlejoins.st-page--workbenchfor it. Home's.dash__greeting(dashboard.css) moves from--text-h2to--text-display-lgat 1.12, the index title's size;.dash__hero-titleis unchanged. The three views no longer take.app-hero--compactortitleHidden; Prove still does. Synthesize's index joins$barlessIndexand gainsappShellCrumbs(). - New Admin contributions drawer: review findings panel.
.review-findings,.review-finding--blocking | --should | --niceand their parts are styled in the page's own<style>block, in the admin's existing idiom (literal admin colours, not platform tokens; the admin area sits outside the design system). Every server string is inserted astextContent. - Improved Synthesize intake wears the app frame again.
Intake.viewrenders.wrap--app.wrap--intake > .sy-app.sy-intakewithaside.sy-rail.sy-artifacts-railfirst andsection.sy-main.sy-intake-leftafter it, andviewBooting()draws the same classes, so the skeleton and the screen are one layout.synthesize.cssdrops the intake's own1fr 340pxgrid. In the shell (app-shell.css, from 1081 up) the tan column is a--bg-paper-darkgradient on.sy-intaketo--app-rail-width, the rail is transparent,min-height: 0and capped at100dvh - 2 * --app-bar-height - --space-12so it sticks; the rail pads 12 a side (296 of content), the surface24 48 110 32; the wrap's 24 and the stage's 96 dock clearance are zeroed on this screen via:has(.sy-intake).$wearsFooterinapp/index.phpnow includessynthesize. The compact journey card's finished state no longer takes acta..synth(synthesize.css) paints--bg-page,--paper-grid-imageand--paper-grid-sizein longhand; it was thebackground:shorthand, which wiped the grid on every screen that did not draw its own surface. - Improved Session-start dialog, second pass.
#facStartModal-overlay .ws-modal__titlesteps to--text-h2(the agenda check's precedent for a branded header);.fac-start__valuedrops from a literal 22px to--text-h2and its label to--text-meta; cards are 12 by 16; each row carries aws-pill ws-pill--md(.fac-start__item-type: white, the card's edge colour, ink text) before a 4em right-aligned duration column. The visually hidden type prefix is gone, the pill says it. - Improved The session-start dialog (
.fac-start,app-shell.css). New.fac-start__headputs the name left and.fac-start__statsright-aligned on one row (wraps under the name when it runs out of room)..fac-start__agendaloses its paper box and row dividers; each.fac-start__itemis a thin card (8 by 12,--radius-md, 6 gap) whose tint and 1px edge come fromdata-type(exercise,icebreaker,break). The shell does not load the console's sheet, so the three--fac-type-*pairs are restated as--fac-start-*on.fac-startand must move with them.facilitate-start.jsresolves the type the wayFacilitatorAgenda.planToAgendadoes, plus the meal types, and says it in a.ws-visually-hiddenprefix. - New
appShellRail()takes'look' => 'strategize': another view gets Strategize's rail item for item (compact checklist at every stage, no waiting card) and the aside wears.app-rail--strategizebeside its own class so the redrawn cards apply.'newWorkshop' => 'standalone'renders the naming dialog with the state factory, as Home does. Plan's index uses both; Saved agendas keeps the default Plan rail.
September 2026
v2.38.1: Strategize index and steps, one app bar tooltip
- Fixed
ws_app_bar()drew two tooltips on every icon-only action.app-bar.phphad gained the platformdata-tooltip(opening downward) without losing its own.ws-app-bar__tipspan, so both fired on hover in every engine, the span behind and up-left of the pseudo-element. The span and its CSS incomponents.cssare removed; the platform tooltip, with its Esc dismissal, is the one that stays. Every app bar is affected, not only Strategize. - Improved The Strategize toolbar canon is four slots.
reloadis removed fromphase-toolbars.phpand from the shell's call site, along with the$strategizeStateIdit was the last reader of. The dock demo's first slot now wears the canon'sassignmentglyph; it had keptautorenew. - Improved Strategize index and steps.
appStrategizeQueue()is index-only and renders the whole queue;.st-newcase(the "+" tile) and.st-hero--boxedare retired fromapp-shell.cssand the JSON, and the index hero and the sentence hero both drop their 48 top padding under the trail.appStrategizeCrumbs()with no argument is the index's two-item trail. The sentence step's headline (.st-page--workbench .st-hero__title) wears the Display Text Pattern like.st-tools__title: upright bold, thenemin bold italic--mainsite-darkon an inset 8px--mainsite-lightunderline; the shell's section heroes keep the upright red second line.appShellRail()takes'newWorkshop' => 'index' | 'steps': the button on every Strategize rail, and on a step the naming dialog without a second copy ofstorage.js. - Improved One edge for the rail's promo cards.
.app-rail--strategize .app-rail__academyand.app-rail__cardsno longer take--phase-learn-very-lightas their border; all three keep the card rule (--card-border, 1px#E7E5E4), as Tips always did. In Figma, the three variants ofrail-cta(Canvas Design System, set141:516) have their stroke bound tocolor/gray/200; Academy and Intervention Cards were bound tocolor/brand/red-very-light. The library needs publishing for the screen frames in the synthesize file to pick it up. - New
ws_breadcrumb()items taketarget(_top,_parent,_blank,_self;_blankaddsrel="noopener"), for a trail inside an iframe whose links have to leave it. The embedded planner uses it for.agenda-crumbs(Home / Plan / Build your agenda, first in.agenda-main, 13 on 16 with slash separators like.st-crumbs--shell), styled incss/planner/layouts/_planner.css; the bundle's?v=stamps for that sheet and forcomponents.cssare bumped to20260918a. - Improved The embedded planner is on the page grid and the paper.
--embedded-rail(_sidebar.css) is two of.app-page's eight columns at any width:calc((min(100vw - 96px, 1244px) - 140px) / 4 + 20px), 296 at 1440 and wider, 231 at 1180; it applies from 1025 up, under which the planner's own compact palette and drawer are unchanged..embedded-mode .agenda-maintakespadding-right: max(48px, 100vw - 1292px), so the canvas is six columns and stops at the 1244 cap. Both columns drop their opaquegray-50(the palette from 769 up only, where it is not a drawer), so the body's--paper-grid-imageshows. Standalone/planner/is untouched. - Improved
.sidebar-custom-activity-sectiontakes a 1px--planner-redborder and its button reads Custom Activity, witharia-label="Create custom activity"so the accessible name keeps the verb and still contains the visible label. In Figma,plan-custom-activity(160:1102) gained the outer card it had never drawn (white, 16 pad, radius 8, stroke bound tocolor/brand/red) and the new label. - New
.fac-script-empty--break(facilitator.css): the console's guidance card for a break, a solid-edged variant of the dashed empty state with a Fraunces title.facilitatorBreakNote()infacilitator/js/modules/scripts.js(?v=9, both shells) picks the line by the break's name, falls back by keyword only for items typed as a break, and leaves every other item's "No script" message alone. - Improved The planner's three pick menus are one pattern (
css/planner/components/_exercise-builder.css, appended block; stamp20260919a):.icebreaker-swap-menu,.exercise-swap-menuand.break-suggestion__menushare--pick-tint,--pick-edgeand--pick-inkper activity type, a Fraunces title bar (the break menu's drawn by::before, since script fills it with rows only), rows as 1px-edged cards in Inter at--text-ui(they had been--text-h3in the UA button font), and the count or duration as a tabular-figure pill at the right..break-suggestion:has(.is-open)lifts to--planner-z-dropdownand pausesbreakPulse, which had been fading the open menu to 0.85. - Improved
ws_header()draws one brand mark,graph_3, on every section..wsh-header__markno longer reads the section'siconkey, which stays in the registry.header.phpis one of the header trio, so it deploys withhelpers.phpand the mergedcomponents.css. - New
ws_app_bar()takescenterActions: actions in the same shape asactions, drawn in the centre of the bar when no rawcenteris passed. The shell's live console (app/views/facilitate/run.php) uses it for play and stop, and its trail is now the shared cluster: settings (open-options), save (save-session), delete (discard-session) and the arrow. The three handlers live infacilitator/js/facilitator.js(?v=25) and call what the console already does;discardSession()deliberately skipsend_live, so a discarded run is not marked delivered, and deletes nothing on the server.#overlay-optionsreuses the confirm card with.options-card__list(facilitator.css). - New
appShellCrumbs()and.app-crumbs(app/includes/rail.php,app-shell.css): the shell's trail for any page, Home plus the items passed, sharing every rule.st-crumbs--shellwears;.app-crumbs + .app-herodrops the hero's 48 top padding. Facilitate's index is the first caller, and it joins Plan's in$barlessIndex: nows_app_bar()on either. - Improved
appRailWaitingCard($rows, $stage)serves Plan as well as Synthesize.appRailJourneySignals()gainsplan_list(the rows at the Plan stage); with'plan'the card is titled "Waiting for an agenda" and its rows open the builder.appShellRail()takes'profile' => truefor the DNA card on a view that leaves it out by default. Plan's index passes it with'promos' => ['tips', 'cards'], so its rail is DNA card, waiting card, film, Tips, Intervention Cards; Saved agendas keeps the default. Plan's index also callsappShellCrumbs(). - New
ws_wl_see_all()(app/includes/workshop-list.php): the See All Workshops button under a phase index's queue, secondary, in.app-see-all(renamed from.st-queue__all, same px sizing). Strategize, Plan and Facilitate call it; Plan's and Facilitate's listings drop their ghostfooterlink. The shell also stops renderingws_app_bar()on Plan's index ($planIndexinapp/index.php). - Fixed The schedule panel's duration pill is white. A leftover
#calendarDurationTextrule incss/planner/components/_modals.css(600,--planner-gray-900, from the retired calendar modal) outranked.schedule-panel__meta-pillby targeting the id. The rule is removed; the pill now matches its two siblings in colour and weight. - Fixed The Strategist bundle's
?v=follows its newest file. Both shells versioned all eight scripts offstrategist.js's mtime, so a fix to a module alone shipped behind the old query string. - Improved Pitch, practice and check read trail, title, sentence card, panel. The three views call
appStrategizeCrumbs(), their.st-tools__headand thenst_recap_card()directly;appStrategizeSubhead(), which wrapped the trail and the card in.st-subheadabove the title, is removed along with the shell's two.st-subheadrules and.st-tools__head--tight. Inapp/css/app-shell.css, under.st-page--tool: the head's margin is0 0 var(--space-6),.st-recapcarriesvar(--space-6)below it, and.st-split .st-paneldrops its 16 top margin, which a grid item cannot collapse. Measured at 1440: trail 144, title 184, card 252, panel 338 on all three. The kit and the standalone Strategist are untouched.tests/E2E/app-strategize.spec.jspins the three 24s per step.
September 2026
v2.38.0: Strategize, rebuilt from its Figma frames
- New
app/includes/strategize.php: what the shell adds around the Strategist's own markup, which still serves the standalone app untouched.appStrategizeCrumbs()(Home / Strategize / step throughws_breadcrumb(), deliberately not.st-crumbs:bindNav()stamps the open sentence's?id=onto every link in that class, and stamped, Strategize became a link to the sentence),appStrategizeQueue()(the section's workshops throughws_workshop_list(): two rows and the plus tile, primary buttons, heading fromws_wl_phase_title('strategize')),appStrategizeRail()(the promo pair each frame draws) andappStrategizeNewCaseModal(). - New
strategize/home.php, and?view=strategizeopens it.$strategizeStepsleads withhome; a URL carryingid,planorobjectionwith nostepstill means the sentence. The picker renders no document bar, no dock and no Strategist bundle, because there is no open sentence for any of them to act on. Hero:.st-hero--boxed, the library'sapp-herocard (white, 48 all round, 268 tall with the lede at 1.6). - New The naming dialog, on the design system's modal pattern:
ws_modal()in thebrandedvariant the shell's other dialogs wear, onews_form_row()with a helper that becomes the error after blur and says how to fix it, andws_modal_footer()with Go directly to Plan (secondary, left), Cancel and the primary.app/js/strategize-queue.jswrites a freshStStoragestate withworkshopNameandnameTouchedand opens the sentence on its?id=: localStorage only, no server write.?new=1opens it on arrival and is stripped from the URL. The tile is aws_card()link to that URL, so it works without script. - New Two additive options on
ws_workshop_list():cta['variant']sets every card's button, andgridAppendadds one tile to the end of the grid and renders the grid in place of the empty state. Both default off; no other caller moved. - Improved The Strategize rail, scoped by a new
.app-rail--{view}modifier on the aside.appRailJourneyCard($counts, true)is the compact checklist (title, a line, the rows; no button and no badges), which is what let it back onto a view it was kept off because its button outranked the page's primary.'promos'names the promo cards in order;appRailAcademyCard()is the third, its art exported from the library component with its shadows. Sizes are the frames' in literal px (20 / 12, a 14 checkbox, cards at 221 / 258 / 311 / 330), because the library redrew these larger than the shared rail paints them and the shell's 70% root has no token that lands on them; the other views keep their pinned sizes until the redraw is rolled out. - Improved Measured against the frames at 1440: sentence, kit, pitch and practice match on every anchor (trail 144, sentence card 184 or 219 under the kit's save pill, exit strip, queue heading, grid) and down the whole rail (192 / 292 / 529 / 645 / 919 or 991).
tests/E2E/app-strategize.spec.jspins the picker, the rail order and sizes per step, the trail, the naming flow with a no-server-writes assertion, and?new=1. - Improved Home's rail has one shortcut, New workshop (primary,
data-st-newcase), andappShellRail()renders the naming dialog on Home so it opens in place; its href is the same dialog on the picker for when script is off. In the dialog's footer, Cancel and the primary share a line, a divider follows, and under it Go directly to Plan runs the full width of the dialog's content on--phase-learn-very-light.ws_modal_footer()is a padded flex row written for a footer outside a padded body; here it sits inside the form, so this dialog drops its side padding, replaces the row with one full-width grid track and removes the left group's auto margin, which in a grid would outrank the stretch. Bill's calls, 18 Sep; the comment inrail.phprecords the trade against the quieter buttons it replaced.
September 2026
v2.37.4: the app screens in Figma are layers, not pictures
- Improved Fourteen frames in App — Screens rebuilt as native, editable layers. The five Strategize steps, Library, Plan saved / check / build, Facilitate run, Synthesize make, Settings, My Stuff and Contribute were rectangles filled with a screenshot. Each is now the live page walked node by node (
~/.claude/tools/figbuild:extract.mjs→compile.mjs→preview.mjs→builder.jsfetched by the plugin from port 80): real text in Fraunces, Inter and Material Symbols, vector icons, frames with their fills, strokes and radii, the document bar and the dock, all signed in with real data and every write request swallowed by the extractor. No missing fonts, no builder warnings. The six frames drawn from library instances (home and the four phase pickers, plus the Prove report) were already editable and are unchanged. - Fixed The side-nav highlight is right on every frame. All twenty had inherited the layout template's Strategize highlight. Home, Plan, Facilitate, Synthesize and Strategize light their own item; Settings, My Stuff, Library and Prove light none, which is what the live shell does.
September 2026
v2.37.3: the rest of Plan and Facilitate in Figma
- New In Figma:
App / plan / saved,App / plan / checkandApp / facilitate / runin App — Screens, completing both phases beside the existingplan / home,plan / buildandfacilitate / home. Saved agendas is a signed-in 1244 capture of the page grid (three 296 cards a row beside the Plan rail); the check and the console's ready-to-facilitate state are full-bleed 1340 region captures. All three were taken with every non-GET request aborted (~/.claude/tools/figbuild/cap-readonly.mjs), so a capture cannot write to the database the local.envpoints at. The console's live state is not drawn: starting a session writes one.
September 2026
v2.37.2: what the review of the grid rebuild found
- Fixed The shell's Synthesize grid rules are guarded by
@media (min-width: 1081px).synthesize.csscollapses.sy-intakeand.ws-shellto one column insidemax-width: 1080px, and a media query adds no specificity:.app-shell .synth .sy-intakecarries one more class than the selector in that block, so unguarded it won at every width and the collapse could never happen between 900 and 1080.app-tool-rails.spec.jsnow loads the intake at 1000 wide and asserts one track. - Fixed
.app-rail__lead, and the rail's type step moved off the rail itself.appShellRail()wraps a view's'lead'in<div class="app-rail__lead">, and the seven stepped-down--text-*tokens are declared on.app-rail > :not(.app-rail__lead)instead of.app-rail, because a custom property set on a parent cannot be un-set for one child. The Settings account summary and the Contribute preview are page content written against the platform scale; stepped down with the rail's own cards their captions painted at 7.7px. They are back at 8.4 / 12.6, and the rail's pinned sizes (9.8 / 12.6 / 9.1) did not move. - Fixed
app/js/settings.jsbinds section links document-wide. The aside's[data-settings-section]link renders in the rail now, outside.app-settings, so the root-scoped listener missed it and the click became a page load.switchSection()stays root-scoped, so only nav items takeis-active.app-settings.spec.jsclicks it and checks a marker onwindowsurvives. - Fixed
prove/report.php's not-found branch opens the page grid and renders the rail before its earlyreturn, which fired above the wrapper the batch-5 change added. Not browser-verified: Prove is switched off locally. - Improved A dead
max-width: 900pxblock insettings.cssthat un-stuck an aside which no longer sticks is gone, andapp-grid.spec.jsmeasures all six My Stuff steps (twenty routes), so the open-in-_chrome.php, close-in-index.phpsplit is exercised on every path.
September 2026
v2.37.1: the grid, measured signed in
- Fixed
ws_choice_cards()inside the page grid takes the grid's gutter. The component ships--ws-choice-gap: var(--space-4), which put three cards at 299 across a 928 main, 3px off the 296 columns.wl-gridlands on below them..app-page__main .ws-choice-cards { --ws-choice-gap: var(--app-grid-gutter) }inapp-shell.css;components.cssis untouched because the gap was already a custom property. Found by the first signed-in measurement once the database was reachable again. - Fixed
app-rail.spec.jspins its viewport to 1440 × 900. The DNA card now fills a rail that is two columns of a fluid grid, so its width follows the viewport (296 at 1440, 256 at 1280); the old fixed 280 card never needed one. The pixel pins are the 1440 ones, so the viewport is part of the assertion. - Improved Signed-in verification of the whole rebuild:
app-grid(16 route cases),app-rail(5),app-settingsandapp-tool-rails(3) all pass, and the Settings, My Stuff and Contribute frames in Figma are live 1244 captures instead of drawn placeholders. The Prove report frame stays a placeholder whilePROVE_ENABLEDis off locally.
September 2026
v2.37.0: the in-tool left rails join the grid
- Improved The embedded Planner palette is on the page grid.
.embedded-mode .activity-sidebar(css/planner/components/_sidebar.css) iscalc(var(--space-12) + var(--app-rail))wide with the 48 as left padding and no right border, and.embedded-mode .agenda-main(layouts/_planner.css) starts one--app-grid-gutterlater, so the canvas begins on the third grid line (364).body.embedded-modeis whatplanner.php?embedded=1already set; standalone/planner/keeps its bordered 320 column through--planner-sidebar-width. Both import stamps inplanner-new.cssbumped to20260917a. - Improved Synthesize's two material rails are on it too. In the ported-sections block of
app-shell.css:.app-shell .synth .sy-intakeisminmax(0, 1fr) var(--app-rail)at--app-grid-gutterinside 48px insets (928 + 20 + 296, the artifacts rail on the right like every page rail, where standalone draws 1fr + 340), and.app-shell .synth .ws-shellisvar(--app-rail) minmax(0, 1fr)at the same gap and insets with no 1240 cap and no centring (the sources rail on the left, surface from 364).synthesize.cssis untouched and keeps serving the standalone app; its.sy-apprule turned out to have no caller since the August intake rewrite. - New
tests/E2E/app-tool-rails.spec.jsreads both geometries back: the palette at x 0 and 344 wide inside#appPlannerFramewith the canvas at 344 and a 20 left pad, and the intake's tracks 928 + 296 at a 20 gap from x 48 with the artifacts rail at x 996, and (when a local text route opens it) the workspace's first track 296 at x 48. The three standalone projects (planner,synthesize,components) are the control. - New In Figma:
App / plan / buildandApp / synthesize / makein App — Screens, the full-bleed region captured live at 1340 with the palette on columns one and two.
September 2026
v2.36.0: the reading measure and .app-cols retire
- Improved
prove/report.phpand the handoff sheet open.app-page. The report keeps a document measure inside the main,.prv-report { max-width: 770px; margin: 0 }: five grid columns, left-aligned on the first grid line, where the old 720 was off-grid and centred 104px in (the print override still lifts the cap). Theelsebranch inapp/index.phpwraps.app-shell__sheetand rendersappShellRail(…, $view), so Library has the rail; signed out that is the Tips card alone. - Removed
$wideContent,.app-shell__content--wide,.app-cols,.app-cols__main,.app-cols--asideand their 1100px collapse rules..app-shell__contentcaps atcalc(var(--app-content-wide) + var(--space-12) * 2)(1340, the region at 1440) for every non-bleed view, because every one of them opens.app-pageand the six-column main is the measure. The twenty lines of comments that argued each view into the wide set went with the flag. - Improved Docs:
app-shell-layouts.phpdescribes three archetypes (full bleed, page grid, handoff sheet on the page grid) instead of four, and the grid table lost its two retiring rows;workshopr-design-system.jsonlayout.appShelldropsreadingMeasure, rewriteswideas the only column, and its class inventory no longer listsapp-cols. - New
app-grid.spec.jsadds the library route; fifteen routes in all, every non-bleed view but the report (which needs a plan id). - New In Figma:
App / prove / reportandApp / libraryin App — Screens: the report as a 770 document block on the first five columns of the main (a placeholder, since the report is signed-in only and the database was down), the library as the live signed-out sheet captured at 928 with the Tips-only rail.
September 2026
v2.35.0: My Stuff and Settings on the page grid
- Improved My Stuff opens
.app-pageinstuff/_chrome.phpand closes it inapp/index.phpafter the step require, renderingappShellRail(…, 'stuff', ['lead' => $stuffRailLead ?? '']); the six step files stay rail-agnostic. Contribute renders its preview aside into$stuffRailLead(anob_start()block above the form) instead of a 320 column:.app-contribute__layoutis now.app-grid-6with the form on.app-span-4(612, two 296 cards across), andstuff-contribute.cssloses the 1120 cap, its own split and the aside's sticky rule (the rail is the sticky element). - Improved Settings wraps itself in
.app-pageand hands its account summary to the rail as the lead, with'journey' => false..app-settings__layoutis two tracks inside the 928 main,minmax(0, 240px) minmax(0, 1fr)at a 40 gap, so the panel is 648 and.app-settings__grid-2still lands two fields of about 300; the aside's own sticky rule and the 300 third track are gone. - Fixed Nested spans reset at 1100px.
.app-span-3/-4/-6go full-row where the nested grids drop to three and two tracks, for the same reason the page spans already did:span 4on three tracks creates an implicit column. - New
tests/E2E/app-settings.spec.jspins the settings shape (tracks 240 + 648, main 928, rail 296, the aside inside the rail and no journey card) behind a?section=subscriptiondeep link;app-grid.spec.jsadds settings, stuff/favorites and stuff/contribute, fourteen routes in all. - New In Figma:
App / settings,App / stuff(favorites) andApp / stuff / contributein App — Screens, drawn from components and labelled blocks on the grid (240 + 648 nav and panel, 612 form with 296 cards, 296 rail with the lead card first) because both views are signed-in only and the database was unreachable for a live capture; swap in captures when it is back.
September 2026
v2.34.0: the Strategize tools gain the rail
- Improved
strategize/kit.php,pitch.php,practice.phpandcheck.phpopen.app-pageinside#stAppand renderappShellRail(…, 'strategize'), the first views to gain a rail rather than swap one.#stAppand the announcer stay outermost (the JS contract);$wideContentnow covers every Strategize step.strategist.cssis untouched:.st-splitat 5fr/6fr lands 413 + 495 inside 928, and the frame shows the date and cost fields still side by side at that width, so the split stays. - New
app-grid.spec.jsmeasures the four steps beside the sentence, eleven routes in all. - New In Figma:
App / strategize / kit,/ pitch,/ practiceand/ checkin App — Screens, each the standalone tool page captured at 928 (~/.claude/tools/figbuild/cap-st-tool.mjs) under no hero, with the Strategize rail beside it; the pitch frame is 1152 tall to hold the whole split.
September 2026
v2.33.0: the last four .app-cols pages move
- Improved
facilitate/home.php,synthesize/home.php,prove/home.phpandstrategize/sentence.phpopen.app-page. A wrapper swap and nothing else: each view keeps its own<main>, hero and listing, and Synthesize's static.synth-railcommit bar stays inside the main column. With these,.app-colshas no caller under/app/; the rules stay inapp-shell.cssuntil batch 5 deletes them with the reading measure. - New
app-grid.spec.jsmeasures the four routes beside home and plan, seven in all, with Prove gated on the same feature flag the shell reads (proveEnabled()fromtests/helpers/app-routes.js). - New In Figma:
App / facilitate / home,App / synthesize / home,App / prove / homeandApp / strategize / sentencein App — Screens, each a copy of the Plan frame re-filled per view: the compactapp-hero, threewl-cardat 296, and the rail inrail.php's order (Facilitate: DNA, checklist, film, Intervention Cards; Synthesize: waiting list, film, Tips; Prove: DNA, checklist, Tips). Prove has no hero, card or film variant of its own yet, so its frame re-texts the Synthesize ones; the Strategize frame carries the live workbench as a 928-wide capture under the Strategize hero.
September 2026
v2.32.0: Plan on the page grid
- Improved
app/views/plan/home.phpandplan/saved.phpopen.app-pageinstead of.app-cols. Plan home is the batch-1 proof of the arithmetic:ws_choice_cards()and.wl-gridboth land three 296 cards across the 928 main with nothing but the grid deciding it. Saved agendas gains the rail (appShellRail(…, 'plan')) and joins the wide set inapp/index.php; the wrapper lives in the app view, never insaved-body.php, which the standaloneplanner/saved.phpstill renders railless. - Improved The saved body's inline layout is neutralised inside the shell.
.app-shell .saved-workshops-pagein the ported-sections block ofapp-shell.cssdrops the 1200 cap, the72px automargin written for the standalone page's fixed header, and the 24px side padding, so the list sits on the grid line under the app hero. - New
tests/E2E/app-grid.spec.jsnow measures/app/?view=plan&step=homeand&step=savedbeside home: eight tracks of 138, main 928, rail 296 at x 948, and no.app-colsleft on the route. - New In Figma:
App / plan / homein the App — Screens section of the templates page, a copy of theApp / homeframe with the compactapp-hero(Surface=Plan), threeplan-choice-cardat 296,plan-nudge, threewl-card(stage=Plan), andrail-todos,rail-film(view=Plan) and the Daily Tipsrail-ctadown the rail, in the orderrail.phprenders them for Plan.
September 2026
v2.31.0: one page grid for /app/
- New
.app-page, the page grid every non-bleed/app/view will sit on. Eight columns of--app-grid-col(138px) with--app-grid-gutter(20px) between them, 8 × 138 + 7 × 20 =--app-content-wide(1244), the content column at 1440..app-page__mainspans six (928) and the rail spans two (--app-rail, 296);.app-page--rail-leftputs the rail first. The rail is the same<aside class="app-rail">appShellRail()has always rendered, so a view moves by changing its wrapper, not its rail.app/css/app-shell.css, replacing the September grid block. - New Nested grids that close on the page columns:
.app-grid-6,-3and-2inside a 928 main (138 / 296 / 454),.app-grid-4and-2at the full width (296 / 612), and.app-span-2/-3/-4/-6/-full. The two that do not close (-4inside a main at 217,-3at the full width) are documented as off-grid onapp-shell-layouts.phprather than offered..wl-gridinside a 928 main lands on 296 by itself. - New Four tokens under APP SHELL METRICS:
--app-grid-col,--app-grid-gutter,--app-content-wide,--app-rail. Literal px, because the DTCG export andtests/header-shell.spec.jsread them as strings.--app-rail-widthstays at 320: the Facilitator console, the standalone Planner palette and Synthesize intake read it, and none of them are on this grid. - New
tests/E2E/app-grid.spec.jsreads the used track sizes back from the browser on a public wide route: eight tracks of 138, grid 1244, main 928, rail 296 at 1440, and the rail dropping under the main column at 1100. It carries the list of migrated routes and asserts none of them still open.app-cols.header-shell.spec.jsasserts the four new tokens beside the existing 320 pin. - Improved
.app-colsand.app-cols--asidemarked retiring inapp-shell-layouts.phpand the JSON mirror. They stay, unchanged, until the last view has moved: migration is one view at a time against an approved Figma frame, and retuning the old split in place would move six pages in one commit with nothing to approve. The dead sub-900 grid step went with the rewrite; the shell replaces itself with the mobile notice there. - Fixed
app/index.phploadedqr-creator.min.jswithout a cache-bust, the one page-gate failure standing in the shell, and the reason the check-page hook blocked every edit to that file. - Improved Home is the first screen on the grid. Both branches of
app/views/home.phpopen.app-page: the dashboard as before, and the first-run checklist, which had no rail at all, now carries one minus its journey card, because the main column is the checklist there.appShellRail()gained$optsfor exactly that:'journey' => false, and'lead' => $htmlfor the settings and contribute rails to come. Default behaviour unchanged, so no other caller moved. - Improved The DNA card takes the rail. It was drawn at 280 and right-aligned inside the 320 rail, which left its left edge 16px off the first column; at two columns it is 296 wide, matching the design file's
rail-dna.app-rail.spec.jsre-pins the card to 296 × 80 and the pill to 266 × 50; the font-size pins are the rail's own type scale and did not move. - New In Figma:
App / Template / 7 … / no bar(content from y 24, for the views that render no document bar) and the first screen frame,App / home, a detached copy of it filled with instances ofapp-hero,dash-hero, threewl-cardat 296 across the 928 main, andrail-dna,rail-todos,rail-filmand bothrail-ctacards down the 296 rail, in the orderrail.phprenders them for home.
September 2026
v2.30.0: what /app/ is actually built from
- New
app-screen-components.php. Home (238 named nodes, 66 distinct classes) and the plan builder (203 / 67) inventoried from signed-in captures. The good news first: all 66helpers.phpfunctions are already registered in the JSON, so nothing on either screen that belongs to the design system is missing from it. - New
externalComponentsinworkshopr-design-system.jsonnames the three families that are not: workshopList (24ws_wl_*functions, 32KB of CSS, 12.wl-cardinstances on home and the most repeated card on the platform), dashboardAndRail, and planner (the largest surface and the only one with zero helper functions). Deliberately NOT added tocomponents: check 1b requires every key there to resolve to ahelpers.phpfunction.check-drifthas warned about the first family all along; this is the inventory step it asks for. - New Five of them now exist in the Figma library (Canvas Design Sysetm › 🧩 Components › § App screens):
wl-cardas a four-stage variant set,agenda-itemas a four-type variant set, pluswl-stage,item-duration-badgeandcategory-btn. Geometry and colour taken from the captures: the card ships at 284×175, which is exactly what.wl-gridyields in the current 892px main column. Resized to 296×197 on request, 296 being one column of.app-grid-4, so the component now presupposes the four-column layout rather than the shipped one. - New
dash-hero, the red band on home. 892×146 as shipped, spanning the full main column of.app-cols. Rebuilt from the capture rather than by eye: two background layers (linear-gradient(135deg, #E54D4D, #7F1D1D)under a radial white highlight), two decorative 10%-white shapes, the larger of which overflows the top edge so the frame has to clip. Its ghost button measures 46 tall against the solid one's 42 because of a 2px border on a shared 42px content box. - New
card-296, a proposed single card in four variants: library (category icon / title / time / category pill / favourite / share), agenda (checkbox / phase pill / title / time / item count / action), cta, todo. Fixed 296×197. Nothing ships on it: 296 is one column of.app-grid-4, so it presupposes the four-column layout, and 197 is a fixed height that still needs a truncation rule per text slot. - Improved Red Unification verified rather than assumed: all 12 stage pills on home are
#FEE2E2on#C73E3Ewhatever the stage, sowl-stageneeds no colour variants. The agenda row does keep its four type colours, which DESIGN.md sanctions as the exception.
September 2026
v2.29.1: the layout specs get measured against the app
- Fixed The
ws-app-barFigma component was 72 tall; the shipped bar is 60. Measured on/app/?view=synthesize&step=homeat 1440:.ws-app-baris 1340 × 60, matching--app-bar-height. All four variants resized, and because the actions cluster was a fixed-height auto-layout and the title carried aMINvertical constraint, neither recentred on its own and both had to be re-seated. The content slot moves from y 96 to y 84 in every template that wears a bar. - Fixed The handoff sheet was drawn with a document bar. It has none.
app/index.phprenders the bar only forplan,facilitate,synthesizeandprove;/app/?view=libraryreturns no.ws-app-barat all, and its.app-shell__sheetsits at y 84 on the page, which is 24px inside a region that starts at 60. Bar removed and the slot moved to y 24 in both Figma files and in the docs. - Fixed The reading measure went back to 1131. It had been widened to 1244 so every template shared one content width; the app renders 1131 (
.app-shell__sheetmeasured at exactly that, inside a.app-shell__contentcapped at 1227). The templates were reverted rather than the CSS widened, because the cap exists to keep prose and forms off a 1400px line. - Improved
app-shell-layouts.phpdrops its “Known divergences” section for one that records what was measured and what was corrected.layout.appShellin the JSON losesdivergencesand gainsresolved,barRendersForand per-archetype slot origins.
September 2026
v2.29.0: the app shell gets a layout system
- New
app-shell-layouts.php, the/app/layout system. The shell is config-driven: every view declares label, icon, header, eyebrow, title and lede, and two flags inapp/index.php($fullBleed,$wideContent) pick the frame. That yields four archetypes, not twenty-six layouts. Full bleed puts the slot at x 0 w 1340 with no footer; wide is x 48 w 1244; the reading measure is x 48 w 1131 (max-width: calc(1131px + 48px * 2)); the handoff sheet is a reading measure holding.app-shell__sheet. Chrome is constant at header 60, rail 100, main 1340, bar 60, footer 48. All measured from the live DOM at 1440 and cross-checked againstapp/css/app-shell.css. Mirrored intoworkshopr-design-system.jsonunderlayout.appShell, beside the existinglayout.containers, which describes the main site and never applied here. - New
.app-cols--aside,.app-grid-4and.app-grid-8inapp/css/app-shell.css, beside.app-cols. They go there rather than inincludes/components/components.cssfor two reasons: the family they join is already there, and that file is a standing deploy hazard. At the wide column's 1244 the arithmetic is exact:.app-cols892 + 32 + 320,.app-cols--aside320 + 24 + 900,.app-grid-4296 ×4,.app-grid-8138 ×8. Eight subdivides four exactly (138 + 20 + 138 = 296), so a span of two aligns with one of four; that holds only while both keep the 20px gap. Both grids halve at 1100px and again at 900px rather than dropping to one column, so a span keeps meaning the same fraction of the row. - New
.app-grid-8is documented as a spanning grid, not a card grid. 138px is barely half the 260px floor.wl-gridenforces, and that floor exists because an uncappedrepeat(3, 1fr)once drew 214px cards. Neither new grid has a caller yet:.wl-gridstill caps at three on purpose, so reach for these when a design asks, not by default. - Fixed
page-templates.phpclaimed “every page follows one of these templates” while describing only the main-site pattern ofincludes/header.phpplus a.container. Nothing on it applies inside/app/, which has a rail, a document bar, a self-scrolling region and drafting paper. It now scopes itself to the main site and links to the app's own layouts. - Improved Two Figma-versus-code divergences are recorded rather than reconciled in silence. The reading measure is 1131 in CSS and 1244 in the Figma templates, widened deliberately so every template shares one content width, which leaves a screen designed there 113px wider than it renders. The document bar is 60 in CSS (
--app-bar-height) and 72 in thews-app-barFigma component, which puts Figma content slots at y 96 where the app puts them at y 84. Both are listed as open inlayout.appShell.divergences.
September 2026
v2.28.0: the gates start reporting again
- Fixed Four of the five gates were failing on a clean tree and nothing said so.
check-driftstill listedws_lifecycle, retired in v2.9.99, and flagged a card inapp/css/app-shell.csssetting its own geometry;generate-catalog --checkhadws_app_footer(): stringagainst a source that gainedarray $options = [];tokens.dtcg.jsonwas stale. All fixed — the curated JSON drops the dead entry, carries the realws_app_footersignature with itsvariantandlauncheroptions, and both generated files are current at 66 components and 347 tokens. The audit that found them isAUDIT-2026-09-16-system-audit.md. - Fixed
components.generated.jsonreported a11y coverage of 11/66 against a real 54/66. The extractor ingenerate-catalog.phptook the first<p>under an Accessibility heading, and most demos document accessibility as a Concern/Behavior table instead, so 43 components' notes were dropped on the floor. It now takes a<p>or a<table>. This mattered more than the number suggests: START-HERE.md sends AI assistants to that file for per-component usage, so every assisted UI change was being made against a catalog claiming accessibility guidance barely existed. The 12 still blank are the 12 with no demo page to read. - New
check-page.phpis finally wired to something. Its docblock has always said “wired to a PostToolUse hook … exit 1 = new violations (hook turns this into a 2)”; that hook did not exist, so from the day it shipped the checker ran only when someone typed it..claude/hooks/check-page.mjsis the missing half, andds-lint.ymlgains ads-pagesjob. Both check only the files that changed, the way the CSS job does, so the ~4,700 baselined legacy violations stay tolerated while new drift fails. The hook fails open: a bad payload, a missingphp, a crashing checker all exit 0, because a broken hook must never block ordinary work. Self-test:node .claude/hooks/check-page.mjs --self-test. - Improved The page checker no longer scans agent working directories.
.scratch/,.superpowers/,.omc/and.impeccable/hold snapshot copies of real pages —.superpowers/sdd/keeps one per task step — so their drift was being counted twice, once against the copy and once against the page it came from. Eight of the 47 reported violations were this. Real count: 39, across 26 files, 17 of them never committed. - Improved The plan-check modal's agenda card draws its inset from the card family.
padding: var(--space-2) var(--space-3) var(--space-3)becamevar(--space-2) var(--card-padding-compact) var(--card-padding-compact)— same 8px/12px, no visual change, but the sides and floor now move with the card tokens instead of drifting away from them. The tightened top stays deliberate and is commented as such. - Improved
debt-register.mdno longer claims the gates are green. It opened with that line, true when measured on 31 July and untrue since, with no way for a reader to tell. It now carries the one-line command that re-runs all five and says plainly that CI cannot announce a red gate on a branch this far ahead of origin.
September 2026
v2.27.0: the phase toolbars
- New
ws_phase_toolbar()(includes/components/phase-toolbar.php), the dock a phase always wears.ws_app_dock()was the shape; this is the contents. The canon inincludes/components/phase-toolbars.phpfixes which slots Strategize, Plan, Facilitate and Synthesize each show, in which order, with which glyph and name, so the same phase carries the same toolbar on every screen of that phase. Demo atcomponents/demos/phase-toolbar.php, full specification atdesign-system/spec-phase-toolbar.md, DESIGN.md §12.10, and the JSON catalog carries the entry. - Improved The four docks are now four calls that pass only wiring.
app/index.php,planner/planner.php,app/views/facilitate/run.phpandapp/views/synthesize/make.phpeach dropped their hand-assembleditemsarray and name slots by key instead. Every rendered attribute is byte-identical to what shipped before: the same ids, hrefs,data-actions, badges and pills, so no handler inplanner-v2.js,facilitator.js,synthesize.jsorstrategist.jschanges. - Improved The landmark label defaults to the phase's own name (Strategize steps, Agenda tools, Session controls, Add material), so no screen can ship the generic “App dock” fallback by forgetting to replace it. The canon also guarantees every slot has a glyph and a name, which a hand-assembled dock could not:
tests/Unit/PhaseToolbarTest.phpholds both, along with the rule that the caller's order never changes the toolbar's. - New Three sanctioned deviations, and no more: a screen may drop a slot (
'slots' => ['deck' => false]), append one only it has (extra), or reword one whose vocabulary it owns. A pill the canon deliberately leaves wordless (Plan's two clock read-outs, Synthesize's source count) is dropped rather than drawn blank when no label arrives. Anything else is a design change, and the place to make it is the canon. - Improved The pattern library lists the shell's three toolbars under Navigation & Overlays: App Bar, App Dock and Phase Toolbar. The first two had demos but no card, so the only way to find them was to know they existed.
September 2026
v2.26.0: the app dock, and the bar settles on four actions
- New
ws_app_dock()(includes/components/app-dock.php), the floating toolbar a tool shell wears at the foot of its region: a pill of icon-only slots with an optional labelled pill either side. Material 3's floating toolbar supplies the geometry (a 64px pill, 48px slots, a 40px state layer at 8/12/16%, 24px glyphs); Workshopr supplies every colour,--phase-learn-darkunder--text-inverse, lifted by--shadow-smover--shadow-md. It positions against the nearest positioned ancestor and lets clicks through the gaps between its pills. Demo atcomponents/demos/app-dock.php; the JSON catalog carries the entry. - New A slot
badgeonws_app_dock(): a count pinned to the slot's corner, white on the red, for a tool with something waiting behind it. The Facilitator's polls and reactions slots wear it; the app writes the number and shows it with inline display, the same contract its old nav badges had. - New A pill
toneonws_app_dock():successpaints a static read-out--color-success-dark, the one place the dock leaves the red family, and only to report a state (Plan's time-left pill while the agenda fits), never for an action. - New Two layout tokens beside
--app-bar-height:--app-dock-height(64px) and--app-dock-clearance(96px), the room a shell's scroll region keeps clear while the dock is up. - Improved DESIGN.md §12.8 names the shared action cluster every phase bar now wears (home, save, delete, continue), and §12.9 documents the dock, including the rule that it replaces the rail on the screen it appears on rather than stacking above it. Synthesize wears both on its tool step; the app frame wears the dock for Strategize as a child of
.app-shell__body, which is nowposition: relativeand pads the scroll region by the clearance under ahas-app-dockclass. - Improved The
.agenda-gen-6csequence strip and the.agenda-gen-cstep chip are retired fromcss/planner/components/_agenda-generative.css: the generator no longer renders either, so the spine, dots, note and chip rules went with them. - New The moment strip and moment rows in
facilitator/css/facilitator.css:.fac-moment-stripis a paper pill that rises above the dock (its foot sits at--app-dock-clearanceplus--space-2, one z-index step above--z-sticky) holding a 24px drain ring in--fac-redand four ghostws_button()s whose 1–4 key hints are drawn fromdata-keywith::after, because the button template escapes its label..fac-momentrows sit beside.fac-notein the sidebar and the summary with a tabular-nums session clock and aws-pill--tinttag. The orphaned.fac-toastrules are gone; the console\'s confirmations come from the sharedwsToast(). All motion runs on the platform durations and easings and switches off underprefers-reduced-motion. - Improved The shared bar cluster is three actions, not four:
ws_app_bar()'s own defaults drop the home action, and with it every phase bar and picker bar in the platform. DESIGN.md §12.8 carries the rule: the rail names Home on every screen a bar appears on, so a second route to the dashboard was the one action in the cluster that had nothing to do with the document. - Fixed Every
ws_modal()with a coloured header had square top corners. The shell carries--modal-radius, but the header and the body paint their own backgrounds straight into it, so a branded band covered the curve. The header now inherits the top radii and the last child inherits the bottom pair, rather than clipping the whole dialog withoverflow: hidden, which would cut off anything a modal legitimately hangs outside itself. - New The Agenda Check dialog is two columns while you are pasting (
app/css/app-shell.css): the sheet's body becomes a360px+1frgrid, the pitch on the left under an.app-hero--dialogvariant of the section hero, the composer on the right. Both the grid and the left column are keyed off:has(.ac-screen--input), so the moment the module swaps the composer for the report the sheet widens back to the report's1400pxmeasure and the column disappears without a line of JavaScript. One column below1100px. The report screen is condensed in the same scope:ws_planner_card--lggives up its 96px floor and takes tighter padding, the issue rows drop from 20px to 12px, and the energy-arc score steps from--text-h1to--text-display, none of it leaking outside the dialog. The report sheet itself is1120pxrather than the page's1400px, its rail420pxrather than630px(at the module's own1024pxbreakpoint, so narrow widths keep their single column), and it caps at660pxtall and scrolls its body.
September 2026
v2.25.0: a fifth phase, and the surfaces Prove brought with it
- Improved
ws_app_bar()takes a fifth state,prove, beside strategize, plan, facilitate and synthesize. It needs no colour rule of its own: the platform has been unified under Learn Red since July, so the four existing states already paint the same bar and a fifth would only have been a fifth way to write the same declaration. An unknown state still falls back rather than rendering a class nothing defines. - Improved
ws_workshop_list()knows a fifth stage. Addingprovetows_workshop_list_stages()is the whole change: the stage pill, the chips, the kanban's fifth column and the fifth dot onws_wl_journey()all read that one map, which is why they could never disagree about how many stops the journey has. The row meta line gained two bits from the same listing: how many people answered the feedback form, and, on the Prove picker, how many commitments a workshop carries and how many are waiting on the facilitator's word. - New
app/css/prove-report.cssis back, and its print contract now hides.prv-owner-onlyalong with the shell bar, the back link, the gap cards and the missing list. That one class is the whole owner surface: the share panel, the per-answer remove controls, the commitment status strip and the collect card. The stylesheet's header documents why its font sizes are px literals rather than tokens: the root sits at 80%, so a rem token never paints the size its name suggests, and the report is a document with its own reading ramp. - New
prove/r/report-share.cssfor the public share page, which reuses the report's own stylesheet and adds only what a page outside the shell needs. The client's brand accent is scoped to the document rather than set on the header, so a shared report can carry someone else's colour without repainting Workshopr's chrome. - New
facilitator/css/commit.cssfor the two public commitment pages,commit.phpandcheckin.php: a stranger on a phone with one job, built from tokens only. The facilitator's own panel is a separate.wcm-*block appended tofacilitator/css/facilitator.css, because it lives inside the run console's overlay and inherits it.
September 2026
v2.24.0: a stepper, and a danger primary in the modal footer
- New
ws_stepper(): a number input between a decrement and an increment button, for a value edited in fixed jumps that still has to be typeable. Built from the duration control the exercise contribution form hand-rolled as.app-contribute__stepper, which it replaces; the planner's.duration-stepperis the same control and is the next thing to move onto it. As with every component here it ships no JavaScript: the row carriesdata-ws-stepper,data-step,data-minanddata-max, the buttons carrydata-ws-stepper-dec/-inc, and one delegated handler on the page serves every stepper on it. - Improved
ws_modal_footer()'s primary slot acceptsvariant, defaulting toprimary. The one other value with a job isdanger. The Settings cancel-subscription modal had hand-rolled its entire footer on the strength of a comment claiming the component "passes no attrs through to its buttons", which was false, and then boltedws-btn--dangeronto a primary button because there was no honest way to ask for red. The demo's note saying primary was always primary is corrected.
September 2026
v2.23.0: tracking as a ratio, and one tier in the card
- Fixed Every Fraunces heading is tracked at
-0.02em. Eight rules wrote it as a px literal, which has to be recomputed per size, and the spread had drifted from-0.0156emto-0.0469em:.dash__hero-titleand.wl__titleboth render at 25.6px on the same page and carried-0.6pxand-0.512px, while.app-shell__titlesat at more than twice the norm. Written as a ratio it cannot drift again. - Fixed
.app-rail__film-titleis set in the heading font. It was Inter, and the modal the card opens titles the same words at the same size in Fraunces, so the phrase a reader had just clicked changed typeface under them.rules.typographynames card titles as heading-font at severity mandatory, so this was a rule break rather than a preference. - Fixed
.ws-dna-profile-cta__textis tracked at0.08em. Uppercase at0.01emwas the one piece of untracked caps in the frame, against 0.12em on the eyebrows and 0.08em on table headers. Measured after: 175px of text in a 320px pill, 43px of headroom, so thenowrapholds. - Improved The workshop card's tertiary tier is one size. Stage pill, meta line and action were drawn at 9.6px, 9.6px and 10.4px: a spread too small to read as hierarchy and big enough to be three things to keep right. All three take
--text-metanow, which takes the card from five type levels to three. The journey strip stays at--text-microand is the deliberate exception: at three columns it needs 238px of a 234px box at the next step up. - Improved
.lp2-nudge__desctakes the 62ch measure that.app-shell__ledealready uses in the same stylesheet. It ran to 95 characters a line over two lines, which is where a long measure actually costs a reader: the return sweep has to find a line it left nearly 600px ago.
September 2026
v2.22.0: the card check learns what it was missing
- Fixed Check 4 of
check-drift.phpaccepted anyvar()as proof a card was in the system. It exists to keep card padding and radius on--card-padding/--card-radius, but its token test wasstripos($value, 'var('), soborder-radius: var(--radius-2xl)passed.--radius-2xlis a perfectly good token and it puts a card outside the card family, which is the same drift the literals used to cause. The test now asks whether the value draws from that family. - Fixed A BEM element literally named "card" is now treated as a card. The matcher required a
-cardstem, so.app-settings__cardread as an element like.ws-card__titleand was skipped. That is how the Settings panel drew 16px corners in a 12px frame without this check ever looking at it. - New
--card-padding-roomy: var(--space-8), the third and last member of the card inset set, for a full-width panel carrying a form where 20px against 840px of card reads as no inset at all. Settings drew--space-8directly because there was nothing to reach for. - Improved Five more surfaces join the card family:
.app-settings__card(was--radius-2xland a raw--space-8),.app-stuff__row(was--radius-lgwith no shadow),.app-signin__card,.app-ob__card, and.st-exit, which keeps its tinted fill and accent border because it is a callout band rather than a neutral card, and only gives up its asymmetric inset. - Improved Agent scratch directories are out of the CSS sweep.
.superpowers/and.scratch/join.claude/and.omc/on check 3's exclusion list; working copies of stylesheets that were never routes were being counted as platform drift. - Improved The ratchet is rebuilt at 384 entries, up from 124. That growth is the rule getting stricter, not new drift: 260 declarations across Synthesize, Sidekick, the surveys, Academy, Admin, Login, Start and Strategist were always outside the card family and were always invisible to the old test. Nothing in
app/is on the list. Shrink it by repointing a card at the tokens and deleting its entry.
September 2026
v2.21.0: one card, and a card that groups
- Fixed
.ws-task-checklistdraws the card tokens instead of a private copy of them. It hand-rolled--bg-surface,--gray-200,--radius-2xland--space-6where--card-bg,--card-border,--card-radius(12px) and--card-padding(20px) already said the same thing in four of those five cases and something different in the fifth. In a rail besidews_cardsurfaces it was the one object with 16px corners. - Fixed The app rail's film and Tips cards stop overriding
--card-padding. Both set16px 20pxover the.ws-card--contentinset, which put three vertical paddings (16, 20 and the hero's 24) against a constant horizontal 20 across surfaces with the same job. They inherit now. - Improved Rail cards carry
--card-shadow. Card white (#FFFFFF) against the app's page colour (#FAFAF9) is 1.04:1, so a card was separated from the paper by a 1px border at 1.26:1 and by wherever the 24px grid texture fell behind it. A shadow makes that separation even all the way round. Scoped to the app rail:.ws-cardstays flat platform-wide, which is deliberate, and--elevatedremains the opt-in. - Improved The workshop card groups by proximity. Its five rows sat at a uniform 12px, so the pill row, the name, the meta line, the journey strip and the action all read as peers. Name to meta is 4px now, meta to strip 6px, and the action is pushed to the foot with a 16px floor above it: one group, then the thing you do about it.
- Improved The listing's column floor drops from 280px to 260px, restoring three columns at 276px on a 1440px window. The 280 was chosen while the frame painted at 0.64 scale; at true scale it forced two columns of 417px, where the meta line filled 48% of its row and the action 37%. Measured at 1440, 1200, 1024 and 375: names wrap to two lines at most and no journey strip overflows.
- Fixed
.wl-journey__barfloors at 6px rather than 10px. The bar grows to fill whatever the strip has spare and renders at 9px in a 276px card, so the floor only binds when the card is tight, where the three bars' old 30px minimum was the last 5px pushing the strip past the card edge.
September 2026
v2.20.0: the app frame stops taking the 80% twice
- Fixed The app shell was painting at 0.64x the design system, not 0.8x.
html { font-size: 80% }makes every rem token paint at 0.8x its nominal value, which v2.18.0 wrote into each token's comment.body.app-shellthen appliedzoom: 0.8on top, and the two multiplied. A workshop card title set in--text-h4(nominal 18px, documented as painting 14.4px) was reaching the screen at 11.52px;--text-nanoon the journey strip arrived at 6.4px. Both reductions were documented, each in a comment that accounted for itself and not the other.app/css/app-shell.csseven carries a note reasoning about the rem 0.8 from inside the file whose body rule was applying the second one. Thezoomis gone; the root percentage is the single reduction, and it is the 80% the frame was asked for. - Fixed Everything that existed to compensate for the zoom goes with it.
height: 125dvhon the shell body and the two sign-in minimums return to100dvh(the 125 was 100 / 0.8), and.app-shell .synth-shell { zoom: 1.25 }, a counter-zoom whose only job was to cancel the 0.8, is removed rather than left to magnify Synthesize by a quarter. Four stylesheets carried comments explaining layout decisions in terms of the zoom; each now states the reason that is still true, which is that these views scroll inside the shell's own region rather than the window. - Improved
ws_choice_cardfills only the featured card's action. The component has always said at most one card per row is featured, but every card drew the same solid red button, so a row of three paths presented three equal primaries. The featured card keeps the fill; the rest take the 2px outlinews_button's secondary variant draws, with the border on both so the two never differ in size. Affects the Planner home, the builder's empty canvas and Plan inside the app shell. - Fixed The workshop card's journey strip used the decorative ink for essential text.
--color-ink-faintis annotated inplatform-tokens.cssas "decorative only, never essential text" at 2.52:1 on white, and the strip's upcoming stages were painted with it, while naming which phase a workshop has reached, which is the strip's entire purpose. It now takes--color-ink-faint-accessibleat 4.6:1, and--text-microover--text-nano. - Improved The workshop card title moves from
--text-h4to--text-h3. Everything under it (meta line, journey strip, action) is one tertiary tier around 9 to 10px, and at h4 the title sat 1.2x above that tier, inside the range the eye reads as the same level. It is 1.5x clear now. Fraunces stays:rules.typographynames card titles as heading-font, severity mandatory. - New
featureFirstonws_workshop_list(), which outlines the first visible row's action while the rest stay ghost. Off by default, and only safe where the chips are off, since filtering changes which row is first.
September 2026
v2.19.0: one sheet of paper under the whole platform
- Fixed Every in-scope page now paints
--bg-pagewith--paper-grid-image. All 203 real pages were rendered headless, their body children hidden so only the page background survived, and the resulting 240×240 crop measured for dominant colour and 24px line periodicity. 61 pages failed. 41 were drift and are fixed; the other 20 are the deliberately dark surfaces (Coach landing, Live mode, Better Meetings, the five Academy book covers), the email preview, and six files that are partials or generators rather than routes. The measurement is repeatable: it is pixels, not a grep for the token name. - Fixed The
background:shorthand was the cause in almost every case — exactly the failureplatform-tokens.cssdocuments at the token, and the reason it says five pages had already lost the grid this way.css/tips/styles.css,css/academy/styles.css,css/planner/briefing.css,css/planner/design-system.css,facilitator/css/feedback.cssandprework.cssall wrotebackground: var(…)over the global body rule and wiped the grid with it. They are longhand now, each carrying the note. - Improved Five paper stocks collapse into one. The off-canon pages were painting
#FAF7F2,#FEF7F1,#FBF9F5,#FAF6EC,#F5FAF7(a mint green, in Learning Paths),#F8FAFC(a cool slate, in the DNA rating thanks page) and plain white. All now take--bg-page. - Improved The app shell's paper moves to
--bg-pageinapp/css/app-shell.css:.app-shell__main, the plan-check dialog body that deliberately mirrors it, and.app-boot-veil, which has to match the page it covers.body.app-shellstays white by the note already in the file — the paper lives on main, and the body is never visible behind a125dvhframe withoverflow: hidden. - Improved The Admin console joins the platform grid.
css/admin/admin-v2.csshad a bespoke background: its own--paperat#FAF6EC, a 24px grid atrgba(38, 30, 18, 0.045), and a second 96px accent grid over it. It now takes--bg-pageand--paper-grid-image, so it also drops out under forced-colors with every other page. The two warm radial washes stay, layered above the grid. - Fixed Two pages drew the grid twice.
planner/index.phpandplanner/templates.phpcarried a fixedbody::beforepainting a second 24px grid in--color-borderat 50% opacity. With the body rule restored that would have doubled up, so the overlay is gone. - Fixed Three Tips pages had no page background at all.
tips/preferences.php,tips/unsubscribe.phpandtips/vote-confirm.phprender on white: they link/tips/styles.css, which exists on production but not in the repo, so the background cannot depend on it. Each now declares the paper in its own style block, and linksplatform-tokens.cssexplicitly through the same localhost/www switch it already uses forcomponents.css. The tokens did reach these pages before, but only through an@importinside the auth modal's stylesheet, which none of them includes itself — a page background should not depend on a modal shipping with it. - Improved The restored grids are
background-attachment: fixed, matchingcss/styles.cssand_backgrounds.css. Without it the grid scrolls with the content, which on the Academy book readers (up to 34,000px tall) and the 23,000px Tips index reads as a moving texture rather than paper under the page. 26 rules. - Improved Five self-contained pages define the paper tokens locally — the five Academy book readers, Coach coming-soon, the handouts index, the DNA rating thanks page and the Planner landing page. They cannot link
platform-tokens.css, which also setshtml { font-size: 80% }and would rescale their whole layout, so each mirrors the four values in its own:rootwith a comment naming the source.
September 2026
v2.18.0: the type scale says what it paints
- Fixed Every
--text-*token now documents its rendered size, not its nominal one.html { font-size: 80% }sets the root to 12.8px platform-wide and by design, so a rem token paints at 0.8x the size its comment claimed:--text-smallwas labelled 14px and renders 11.2px,--text-h1was labelled 32px and renders 25.6px. Each token now readspaints 11.2px (nominal 14px), and the block carries a warning against swapping a px literal for the token that shares its nominal value. This is almost certainly why stylesheets across the platform still write px literals: the tokens never gave the size the documentation promised. - New
--space-7: 28px, between--space-6and--space-8. Five call sites inapp/css/app-shell.css,css/stsh-shell.cssandincludes/components/components.cssalready referenced it with a 28px fallback, so the step existed in practice and not in the scale. - New
check-drift.phpreports thews_namespace being claimed outside the design system. A function namedws_*announces itself as part of the system, but if it lives anywhere other thanincludes/components/none of this gate's checks reach it. Six files hold 33 such functions;app/includes/workshop-list.phpalone defines 26, including four whole listing view modes. - Fixed The gate passes again. It was failing on
content/plan/synthesize-mockups/mockup 2.css, a duplicate backup whose canonical twin passes.
September 2026
v2.17.1: the check's issue rows on platform cards
- Improved
.ac .energy-arc-recis restated as a platform card (agenda-check/agenda-check.css):var(--bg-surface)behind avar(--gray-200)edge atvar(--radius-xl), replacing the cream-and-orange row it inherited fromcss/planner/prep.css, whose--warm-creamwas defined nowhere on the platform. Scoped to.ac, so the planner's prep page keeps its own. - Fixed The same rule restated its own border. A
border-leftsat under thebordershorthand at the identical width and colour, saying nothing the line above had not already said. The line is gone.
September 2026
v2.17.0: a gold badge, and badges draw font-only icons
- New
.ws-badge--gold(includes/components/components.css): the premium marker, for Pro and anything else that reads as an upgrade. It borrows the amber ramp rather than owning tokens of its own,var(--color-warning-light)behindvar(--color-warning-darker), taking the darker step for text so it reads as metal rather than as a warning. First use is the Pro pill on the planner's Slide deck modal, which wasinfoblue and said "beta feature" where it meant "paid". - Fixed
ws_badge()draws icons the set has no SVG for. It calledicon()alone, andicon()answers a name it cannot find with an empty span, so a font-only name such asworkspace_premiumrendered as a blank gap inside the badge. It now falls back to the Material Symbols ligature, the same guardws_button()already carried. Only names that had no SVG change. - Fixed
ws_app_bar()tested the wrong thing. It branched on whethericon()exists rather than on whether it returned an SVG, which drew Slide deck (co_present) as a blank button in the planner toolbar. Both the action and the centre-control paths now test the result.
August 2026
v2.16.0: the canvas directory gets a recommender and a pager
- New
.cv-find(css/canvas/styles.css): the Canvas Directory's guided recommender, a conversation panel sitting above the grid. The block covers the head and itsAsktoggle, the thread's two bubble sides (.cv-find__msg--agenton white behind avar(--gray-200)edge,.cv-find__msg--userright-aligned onvar(--phase-learn-light)), the starter and answer chips, a three-dot.cv-find__typingindicator that holds still underprefers-reduced-motion, and the result group: a.cv-find__bestlabel, an italic.cv-find__whyline, and the card capped at 420px so a recommendation reads as one answer rather than a second grid. - New
.cv-pager: the index paginates at 15 canvases a page, so it needed a pager. Every control is an existing.cv-chiprather than a new numeral button; the current page is that chip's.is-activestate. The one addition is.cv-chip--disabled, 0.45 opacity withpointer-events: none, for the Previous and Next ends, which the page renders as<span>so a reader never tabs to a link that goes nowhere. - Improved
.cv-note anow carriesvar(--phase-learn-dark)andvar(--font-semibold). The attribution footnote isvar(--gray-100)undervar(--text-muted)body text, and its two links are the whole way a reader reports a wrong credit or suggests a canvas. At the block's own weight they read as prose. Weighted, they read as the actions they are. - Improved The canvas of the day and the find panel take a
var(--phase-learn-dark)border. Both sit on the index above a grid whose cards are edged invar(--gray-200), and a light edge left them reading as two more cards in it. The dark red says they are the page's two offers instead..cv-today__card:hoverkeeps the same border colour and moves only the 1px lift, so the edge never announces itself twice. - Fixed The directory's stylesheet moved from
css/canvases/tocss/canvas/. The pages now live at/canvas/, and the sheet's path was the last thing on the platform still naming the old route.
August 2026
v2.15.0: the hero folds, and the card menu ships once
- New
.app-hero--compact(app/css/app-shell.css): the section hero for a reader who is already using the product. The full hero is a pitch: pill, two display lines and a lede, about 280px of it, which on 1440x900 put the reader's own list below the fold on every visit to Plan, Facilitate, Synthesize and Prove. The modifier drops the lede and the red second line, takes the headline tovar(--text-display)on one line, and closes the gap below tovar(--space-5). The 48px above is deliberately untouched:.app-railopens level with the hero at that same value, and moving one without the other steps the two columns apart. - New
chipsoption onws_workshop_list(): whether to draw the stage filter chips, default true, withfilterskept as its older name rather than added as a second switch that could disagree. With the chips off,filterstops being a control and becomes the listing's subject: the rows are cut to that stage server-side, so the heading's count, the pager, the empty state and what the reader can see all describe one set. Thefooteroption gained avariantkey, for the plain text link the phase pages close their list with. - Improved The card actions menu is one
<template>per listing, not a popover per card. Every row used to ship its whole panel: nine or so links and buttons, in each of the views, none reachable until a trigger is pressed, which came to 421 links and buttons in a home page showing fourteen workshops.ws_wl_menu()now emits the trigger and the row's facts as data attributes (data-wl-owned,data-wl-collab,data-wl-delivered,data-wl-deck,data-wl-stage);ws_wl_menu_template()emits the superset once with{id}where the plan id goes, andworkshop-list-menu.jsclones it on open, drops what the row does not have, and throws the clone away on close. Same document at 148. Every entry, the keyboard traversal, the in-panel confirmation and the focus return are unchanged. - New
meta_extraon a listing row: extra bits for the card's meta line, after the facts every listing states. Prove is the first caller: it says when the room happened and what it captured. Alsocounts, carried through from the Workshop Record so a page can build its own line without a second query. - New
badgeon aws_task_checklist()row (includes/components/task-checklist.php): a short count rendered as a quietws_badgepill after the label, for a step whose name cannot say how much of it is left ("Run the session · 2 ready"). A done row carrying a badge stays actionable rather than going inert, because a completed step with work still queued behind it is somewhere the reader needs to be able to go. - New
shorton aws_sidenav()item (includes/components/sidenav.php): a second, shorter name for widths the full one cannot survive. Both ship in the DOM,.ws-sidenav__label--shortis hidden by default and the layout picks which one shows, and the item takes the full label as itsaria-labelso the accessible name never shortens with the screen. The app shell's bottom bar is the first caller: below 640px Strategize, Facilitate and Synthesize were all running off the end into an ellipsis, and now read Strategy, Run and Synth. - Fixed About 60 lines of dead stylesheet removed.
.prvh-grid,.prvh-cardand their two media queries were the last hand-rolled workshop card on the platform; Prove rendersws_workshop_list()now, so they went, and the card-border rule inapp-shell.cssstopped naming a selector nothing draws.
August 2026
v2.14.12: the home hero's primary button answers the pointer
- Fixed
.dash__hero-btn.ws-btn--primarynow has its own hover and focus state. Rest,:hoverand:focus-visibleshared one declaration inapp/css/dashboard.css, so the one emphatic button on the app home page never moved under the cursor or the keyboard. Hover and focus takevar(--phase-learn-very-light)as the fill and keepvar(--bg-surface)as the border, so the edge stays put while the face darkens.
August 2026
v2.14.11: the frame bar offers the real app window
- New The app footer's full bar carries an "Install the app" action. Rendered hidden and revealed only by Chrome's
beforeinstallprompt— the installed PWA is the one window a browser draws without a URL row, which no popup feature string is permitted to do. The click spends the event's one-shotprompt(); either outcome retires the button for the visit,appinstalledretires it for good, and browsers that never fire the event never show it. Wired in the template's own script (registered at parse, since Chrome can fire beforeDOMContentLoaded); the control reuses.ws-app-footer__link, so no new CSS.
August 2026
v2.14.10: the featured row leads with the front door
- Improved The app shelf's featured row now points at
/start/.ws_app_shelf_featured()sends the Workshopr App row through the app's landing page, which pitches the app and signs the visitor in on one screen, then forwards an already-signed-in reader straight to/app/. No site surface lands anyone on the shell's bare sign-in wall any more.
August 2026
v2.14.9: the app shelf grows a featured row
- Improved The app shelf's consolidated-app row moved above the phases.
ws_app_shelf_featured()returns the Workshopr App as its own row, and the logotype dropdown (.wsh-panel__app--featured), the homepage shelf (.tue-shelf__app--featured) and the features shelf (.ft-shelf__app--featured) all render it full-width above the phase groups. Strategist and Planner left the STRATEGIZE/PLAN group on the same reasoning that removed Facilitator: they are views inside the app the featured row already covers.
August 2026
v2.14.8: the password field's eye is anchored, and stops seeing double
- Fixed
ws_input's password reveal. Two rules the component always needed lived only in one app's stylesheet..ws-password-wrapperhad noposition, so the absolutely-positioned toggle anchored to whatever ancestor happened to be positioned: on the app shell's new sign-in screen it landed at the far right edge of the window, a pair of eyes floating in the paper. And the inline handler swaps the two eyes by toggling ahiddenclass that nothing defined, so both drew at once on every password field on the platform. Both rules now ship with the component, scoped to the toggle rather than claiming a global.hiddenutility.
August 2026
v2.14.7: ws_choice_cards counts columns from its container
- Fixed
ws_choice_cardsis no longer laid out against the window. The set wasrepeat(3, 1fr)with breakpoints at 1100px and 640px, which is a measurement of the browser, not of the grid: inside the app shell the same component sits in a column that gives up 320px to the rail and paints atzoom: 0.8, so at a 1160px window each card was about 200px wide and its time label collided with its button. The track floor is nowmax(240px, (100% - 2 * gap) / 3)withauto-fill, which reads the container: three columns while there is room for three, then two, then one, and never more than three however wide the screen gets. The featured card still takes the full row once the set folds, now via@container (max-width: 760px)rather than a window breakpoint.--2and--1keep their meaning as caps.
August 2026
v2.14.6: ws_app_bar's icon actions get their tooltips
- Fixed Icon-only actions in
ws_app_barnow carry a tooltip. The component's docblock has always claimed tooltip behaviour, but an icon action shipped witharia-labelalone: named for assistive technology, silent to a pointer. Each one now also getsdata-tooltipfrom the same label plusdata-tooltip-position="bottom", since the bar sits at the top of the window and the default top placement would open off-screen. A caller passing its owndata-tooltipinattrskeeps it, and labelled actions are untouched (their text is already the name). No CSS was needed: the bar was alreadyoverflow: visibleand the[data-tooltip]rules were already there.
August 2026
v2.14.5: A lighter gear
- New
icon('settings_light'), the weight-300 cut of the Material Symbols gear atimages/icons/settings_light.svg. The stock weight-400settingsglyph carries about a third more ink than the outline icons it sits beside (measured 10.1% coverage against home's 7.5%), so on a red app bar it read as bold; the 300 cut lands at 7.3%. First used by the planner's Plan settings button; reach for it wherever a gear sits in a row of outline icons.
August 2026
v2.14.4: ws_task_checklist stops crossing things out
- Improved Done rows dim instead of striking through.
.ws-task-checklist__item.is-done .ws-task-checklist__labeldropstext-decoration: line-throughand keeps the--text-mutedshift; the check is already the state, and a stack of struck labels reads as ruled lines between the rows rather than as finished work. The complete state'stext-decoration: nonereset went with it, being redundant now. - Improved The card's action right-aligns.
.ws-task-checklist__ctabecomes a flex row justified toflex-end, so the button lands under the rows' arrows at the card's trailing edge. Consumers that ship nocta(the app home's first-run card, whose action is its own element) are unaffected. - Improved The rail's Getting started card halved in height (
app/includes/rail.php): no subtitle, no per-stepdescriptionorphase, andprogress => false. Every one of those was already optional on the component, so this is a caller change, not a component one.
August 2026
v2.14.3: The Intervention Cards rail card shows the deck, not a diagram of it
- Improved
.app-rail__cards-artcarries two cards instead of five. The fanned-five artwork was recognisable as a deck but rendered each card at roughly 55px wide, too small for the livery or the scene to read, so the card advertised "cards" in the abstract. It now carries a composed pair (images/intervention/cta-cards-pair.png): the hero square-on with a second angled behind it, built from the real fronts inimages/intervention/cards/rather than a Nano Banana imitation, with shadows derived from each card's own alpha so the rounded corners cast correctly. This is the move the DNA rail card already makes, where the medallion is the pitch rather than an icon standing in for one. The sizing rule goes200x150fixed to216pxwide withheight: auto: the pair is portrait, and a fixed height would squash the angled card. Quantised to 38KB from 139KB, visually identical at display size.
August 2026
v2.14.2: The DNA badge label sits on one line
- Improved
.ws-dna-profile-cta__textgoes 14px/17px to 16px/20px semibold on a single line. "Your DNA Profile" and "Discover your DNA Profile" lose their<br>and gainwhite-space: nowrap; tracking eases from 0.02em to 0.01em so the earned pill's 156px text box still holds the phrase beside the medallion. Both states keep their 59px pill; the line simply centres in it. Sizes stay in px for the reason the block documents.
August 2026
v2.14.1: The DNA medallion loses its plate, its label gains a step, and the feedback modal widens
- Fixed The
ws_dna_profile_ctamedallions are transparent. The fiveimages/dna/profile/medallion-*.pngexports carried an opaque#F5F5F5plate, which painted a pale box over the tinted pill the roundel is supposed to overlap. The plate is knocked out by a border-seeded contiguous flood fill rather than a global colour replace, because the artwork holds near-white of its own (the shirt, the scroll, the roundel's inner field) that a global replace would punch through. The anti-aliased rim is unmixed against the plate colour, so no grey halo survives on the tint. No CSS or markup change: the component'sfilemtimecache-bust carries the new files. - Improved
.ws-dna-profile-cta__textgoes 12px/15px to 14px/17px. At the 80% root the frames' 12px read small beside the medallion. Both states keep their geometry: two lines still clear the pill's 59px box, and the start card is unchanged at 59px. Sizes stay in px for the reason the block already documents:--text-*would paint this at 0.8x the size the wireframe frames are drawn at. - Improved The app footer's Send feedback modal moves from
smtodefault(360px to 500px), giving the message textarea a usable measure. A size step the modal component already ships, not a one-off width. The v2.14.0 entry below describes the originalsm.
August 2026
v2.14.0: The app footer's three actions grow up
- Improved The full app footer's right side reworked. Send feedback is now a button opening a
ws_modal(size sm, branded) whose fields arews_selectandws_input(textarea type included), footed byws_modal_footer; it posts JSON to the existingapi/contact.phpwith client-side floors mirroring the endpoint's own. Resources and Company open small link panels anchored above their buttons (disclosure pattern:aria-haspopup/aria-expanded/aria-controls, menu roles inside, noaria-modal), carrying the marketing footer's four Resources and six Company links. Panels close on Escape (focus returns to the trigger), on outside click, and when the other opens; arrow keys, Home and End walk the items. All panel chrome is tokens; the explicit.ws-app-footer__panel[hidden]rule keeps the display gotcha closed. The launcher lost its footer entry point: the rail's More button is its one way in, anddata-app-footer-launcherplus thelauncheroption are gone.
August 2026
v2.13.0: The DNA Profile CTA takes the wireframes' badge design, and [hidden] learns to win
- Improved
ws_dna_profile_cta()rebuilt from the app wireframes' badge frames (file kRqtDXi210tzoXK5uoHQLz: one frame per archetype plus the default). Earned: the archetype's illustrated medallion, its name lettered on the ribbon, overlapping a tinted "Your DNA Profile" pill; the aria-label and the medallion's alt carry the name in text. Start: the pill alone with the DNA glyph and "Discover your DNA Profile". All five medallions exported at 2x toimages/dna/profile/medallion-<slug>.png; a slug without its medallion on disk falls back to the start card. Card chrome is all tokens:--phase-learn-very-lightfill,--phase-learn-darkborder and type,--radius-lg. New demo page atdesign-system/components/demos/dna-profile-cta.php. - Fixed The workshop listing's
[hidden]attribute now always wins..wl-card'sdisplay: flex(and its kanban, timeline and pager siblings) beat the user-agent's[hidden] { display: none }(the exact trap the bulk bar's own comment documents), so a filtered-out card stayed fully painted while the filtered-empty note counted it as gone. The chips have carried this since they shipped; the per-phase default filters made it visible on every phase home. One rule inworkshop-list.csscloses it for every element the filter and the pager govern.
August 2026
v2.12.1: The listing footer link becomes a real button
- Improved The
footerlink onws_workshop_list()now renders throughws_button()as a small secondary variant with a right-handarrow_forwardicon, replacing the hand-rolled red text link and its literal arrow glyph. The listing's exit is an action, and it inherits the button's phase-colour hover and focus behaviour instead of restating them..wl__allsurvives as a layout hook only; the colour, weight and size rules it carried are gone, so the footer no longer redefines what a secondary button already looks like.
August 2026
v2.12.0: The workshop listing's chips learn a default, and the pager learns the chips
- New
filteroption onws_workshop_list()— which chip opens pressed ('all'or a stage key, default'all'), with first-paint row visibility matching it server-side. A value outside the listing's ownstagesscope falls back to'all'rather than opening on a chip that isn't rendered. The four phase homes each open on their own stage; Home opens on All. - Improved Paging composes with the chips instead of refusing them. The old guard existed because two scripts writing
hiddenon the same rows is how a card goes missing; the replacement is single ownership: on a paged listing,workshop-list-pages.jsis the sole writer of row visibility on grid and list, and the chips (workshop-list-views.js) report the active stage to it. The window runs over the rows the active chip leaves visible, a chip change re-pages from page one, and a chip withpageSizerows or fewer shows no pager. Kanban columns and the timeline stay with views.js: whole-collection layouts, never paged. - Fixed A stale stored view preference no longer blanks the listing.
setView()hid every panel before discovering the requested one didn't exist — exactly what a remembered kanban would do on a listing that stopped offering it. It now checks first and leaves the current view standing. - Improved The pattern page (
design-system/app-workshop-list.php) documents both options and demos the chips and pager working together: fourteen static rows, six to a page, opening on the Plan chip. Still renders signed out.
August 2026
v2.11.2:
ws_choice_card loses its tint- Improved
.ws-choice-card(includes/components/components.css): thelinear-gradientfrom--phase-learn-very-lightdown to--card-bgis gone; the card now paints a flatvar(--card-bg)like every other card on the platform. The tint was the component's only deviation from the plain card surface, and it read as a wash across the top of each card rather than as emphasis. Featured cards still separate themselves by border colour and the filled badge, which is where the emphasis belonged all along. Applies everywhere the component is used: Plan's home, the planner home, and the builder's empty canvas. - Improved
.lp2 .launchpad-choice-screen__header(app/css/app-shell.css): the header now setsmargin-bottom: var(--space-6), replacing an inlinestyle="margin-bottom:0"inapp/views/plan/home.phpthat left the choice cards sitting directly under the subtitle. The standalone planner home keeps--space-10there, which is a page-opening gap rather than a heading-to-cards one. Note the shell renders underzoom: 0.8, so 24px of token space is 19.2 physical pixels; author against the tokens, not the ruler.
August 2026
v2.11.2: The Agenda Check dialog joins the paper, and loses a one-off chip
- Improved
#planCheckModal-overlay .ws-modal__bodypaints the drafting paper (app/css/app-shell.css):--bg-paperplus--paper-grid-image, so the dialog stands on the same ground as the region it opens over rather than a white sheet floating above it. The composer and the report cards already paint their own--bg-surface, so they still read as cards on paper. Written longhand for the reasonplatform-tokens.cssgives at the token: thebackground:shorthand wipesbackground-image, which is how five pages have silently lost the grid before. Scoped to this dialog, not to.ws-modal__bodyat large: the other Plan dialogs are forms, and a form on grid paper reads as a mistake. - Improved
.ac-sample-chipis retired (agenda-check/agenda-check.css). "Try a sample agenda" was a dashed pill with its own hover, focus ring and transition, and no second instance anywhere on the platform; it now rendersws-btn ws-btn--secondary ws-btn--md, the same vocabulary as the "Check my agenda" primary two rows above it. The✨became aws-btn__iconholding Material Symbolsauto_awesome, since a raw emoji inside a design-system button was the tell that it was not one.#acSampleChipstays:agenda-check.jstoggles itshiddenas the textarea fills, so the id is contract and only the styling changed.
August 2026
v2.11.1: The rail's DNA card becomes artwork
- New
.app-rail__dna-art(app/css/app-shell.css): the block for the baked DNA cards inimages/dna/profile/. Deliberately chrome-free: the artwork carries its own border, padding and cream ground, so aws_cardwrapper would have drawn a second frame around the first. The image holds its slot withaspect-ratio: 27 / 10rather than a fixed height, so a slow network cannot reflow the rail beneath it, and it caps at 360px under 1100px where the rail goes full width, because the widest cut is 640px and a stretched rail would upscale the artwork into mush. - Improved The old
.app-rail__dnarow is now the fallback, not the default.appRailProfileCard()returns the artwork when both cuts of a card exist and the text row otherwise, so an archetype added without art degrades to a working card rather than a broken image. The block comment says so; do not delete it as dead CSS. - Improved Not to be confused with
ws_dna_cta(includes/components/dna-cta.php), the full-width marketing strip on the homepage and library index. That one is real text with a real button and has no earned state; this one is a 270×100 rail widget whose wording is part of the image. Two different jobs, and worth keeping straight before either grows into the other.ws_dna_ctastill points at the pre-Red-Unification green Energizer badge.
August 2026
v2.11.0: The app frame footer, the checklist's celebration state, and the branded modal earns its styles
- New
ws_app_footer()grows afullvariant: the consolidated shell's 56px beta bar (wordmark, 2.0 Beta pill, build version read live from the changelog, Send feedback, Resources as the launcher trigger, Company). The 24px band stays the default; the composite's automatic routing is unchanged, and the shell picks the variant through$GLOBALS['ws_app_footer_options']. The template also honours$appFooterStylesOnlyso a documentation page can borrow the styles without consuming its one footer. - New
ws_task_checklist()gainsctaand a designed complete state. The in-card action renders throughws_buttonbelow the items; with every item done the card wears.ws-task-checklist--complete: red-tinted surface, red filled checks, plain ink labels. The app shell's rail journey card (app/includes/rail.php) is the reference consumer, with past-tense labels and a dismiss that persists by cookie. - Fixed
ws_modal'sbrandedvariant renders. The JSON has documentedvariant: brandedsince the catalog was written, but only the per-callheaderColorpath had styles, so the variant fell through to the default. It now paints the red header band the planner dialogs and the app launcher wear; reach for the variant beforeheaderColor. - New The workshop listing pages.
ws_workshop_list()acceptspageSize: rows past page one shiphidden,workshop-list-pages.jsdraws the strip (numbers to seven pages, arrows and a count past that), and pagination refuses to engage alongside the stage filters: both hide rows, and two hands on one attribute loses cards. Grid and list only. - New The app shell's two-column frame.
.app-cols+.app-rail(app-shell.css): the 320px sticky aside Home, Plan, Facilitate and Synthesize share, carrying the DNA badge card, the journey checklist, the film cards and the promo cards from the app wireframes. - Improved The shell's hero second line goes upright. Every app wireframe draws the hero's red line without the italic or the tint underline, so
.app-hero__title em(and the Strategist's.st-herocopy, inside the shell) retire the Display Text Pattern there. The pattern stays the marketing surfaces' signature per DESIGN.md §7; the<em>stays in the markup because the emphasis is real, only the slant is not.
August 2026
v2.10.1: The bulk bar honours
[hidden]; the card menu honours ownership- Fixed
.wl-bulkbarignored its ownhiddenattribute. The block setsdisplay: flex, which beats the user agent's[hidden] { display: none }, so the bar the script hides by toggling that attribute stayed on screen under every workshop listing, Delete button and all. Clicking that Delete did nothing, becauseworkshop-list-select.jsreturns early on an empty selection. An explicit.wl-bulkbar[hidden] { display: none }now closes it, and the block header says why the rule is not redundant. - Fixed Arrow keys inside the card menu's confirmation stayed inside it.
hiddensits on the list and confirmation wrappers, not on the buttons within them, so the popover-wide focus query inworkshop-list-menu.jswalked ArrowDown from Cancel onto the invisible Delete and appeared to swallow the key. The traversal now reads from whichever panel is showing. - Improved
ws_wl_menu()renders per ownership. Duplicate, Copy share link, Slide deck and Delete are owner-only inapi/plans.php, every one of them scoping its query touser_id = ?, so the menu now omits them on a shared card and offers Remove from my workshops instead, backed byremove_collaborator, which gained a plan-scoped self-service branch: one account can hold more than one accepted row on a plan (the unique key isplan_id+email), and dropping a single row would leave the workshop on the list. A row with noownedkey reads as not owned, matchingws_wl_select_box(), which has applied the same rule to the selection checkbox since it shipped.
August 2026
v2.10.0: The app launcher becomes the journey, five phases across
- Improved
.app-launcher__groupsis a five-column grid. The launcher now lists Learn, Strategize, Plan, Facilitate and Synthesize, and the grid was stillrepeat(4, 1fr), so the fifth phase wrapped to a second row and stopped reading as a step in a sequence. The column count is fixed rather than auto-fitting, for that reason. - Improved
.app-launcher__panelwidens to 1360px (from 1040), which holds five columns at 246px each — the width the longest app name plus its leaves-the-frame mark needs. The break to three columns moved from 860px to 1130px to match. Both numbers are window measurements against a frame that paints atzoom: 0.8, and the block says so: 1130px of window is 1412 CSS pixels inside the shell, which is the panel plus its padding. - Fixed A launcher row's name no longer runs under its leaves-the-frame mark. The mark is positioned out of flow, so nothing held the name back from it; "Post-Workshop Toolkit" finished two pixels short of touching.
.app-launcher__app:has(.app-launcher__out) .app-launcher__namenow reservesvar(--space-6)on the right.
August 2026
v2.9.99:
ws_lifecycle() retired; My Stuff gains a submission form- Fixed
ws_lifecycle()is gone. The helper (includes/components/helpers.php), its template (includes/components/lifecycle.php), its demo page (design-system/components/demos/lifecycle.php) and its entry inworkshopr-design-system.jsonwere all removed with the MyWorkshopr consolidation. It shipped as a second, older answer to a questionws_wl_journey()(app/includes/workshop-list.php) already answered, and it never gained a caller in eleven months: a documented component nobody uses is a component the next person builds against by mistake. The.ws-lifecycleCSS incomponents.cssstays —planner/includes/views/saved-body.phpstill hand-rolls that markup in JS for the saved-agenda cards, and is now the block's only consumer. Retire it there and the CSS goes with it; the block header says so. - New
/app/?view=stuff&step=contribute— the exercise submission form, ported out ofmyworkshopr/add-exercise.php(742 lines, an inline IIFE, its own orange accent family) into the shell. Everything the old page hand-rolled is a component now:ws_inputfor the eight fields,ws_radio_card_groupfor difficulty and the category picker,ws_chip_inputfor materials,ws_icon_buttonfor the duration stepper and the step deletes, and the platform Toast in place of a hand-built.ws-alerttemplate string. New sheet atapp/css/stuff-contribute.css, scoped to.app-contributeand painting entirely from platform tokens — the retired--exercise-accentorange is the one Learn red now, per Red Unification. The fixed right-hand drawer became a sticky column: the shell draws atzoom: 0.8inside its own scroll region, where a viewport-anchored drawer lands against the wrong edges. - Improved The
myworkshoprheader section is retired._wsh_sections()inincludes/components/header.phplost the key and its[data-section="myworkshopr"]tag-width rule,WS_APP_PATHSinhelpers.phplost the path, and the nav inventory atincludes/nav-example.phplost the row. Every/myworkshopr/URL is a 301 that renders no header at all, and an unknown section key already falls back tohome, which is what the shell uses.
August 2026
v2.9.98: The colour input becomes a control
- Fixed
ws_input($name, ['type' => 'color'])rendered as a hairline. A colour input's intrinsic content box is a few pixels tall, so.ws-input's shared padding left a ~6px swatch inside a 26px field — a control that read as broken rather than as a colour.input[type="color"].ws-inputnow carries the rendered height of a text field (44px, 36px at--sm), 4px of padding, and a borderless swatch inset to the field's radius. The Planner's new Slide deck modal is the first place two of them sit side by side, which is how it surfaced.
August 2026
v2.9.97: Closing out the 23 August audit
- Fixed
--mainsite-gradientstarts at#D13A3A, not#E54D4D(WCAG SC 1.4.3, Level AA). This gradient backs--button-bg-primary, whose label is--text-small: 11.2px at the 80% root, so normal text with a 4.5:1 bar. White measured 3.82:1 at the light stop. It is now 4.80:1, with the dark stop unchanged at 5.02:1. Flat brand red is untouched and still lives in--mainsite-primary. - Fixed
--planner-gradientand--planner-topbar-bgnow alias--mainsite-gradient. The audit named only the primary button, but the planner carried the identical failure with white text on it in two places: the topbar and.recommend-item__select:hover. Worse,css/planner/base/_variables.cssrebuilt the gradient from--mainsite-primaryrather than sharing it, so the planner would have kept the failing stop after the button was fixed. One definition now, one place to fix. - Fixed
.ws-audio-player__btnhovered purple. It readvar(--color-primary-dark, #651FFF), and neither--color-primarynor--color-primary-darkexists inplatform-tokens.css, so the rest state fell back to red while the hover fell back to a pre-Red-Unification violet. Both now read the real phase tokens. - Improved
ws_dna_cta's CSS moved intocomponents.cssand was tokenised. It also referencedvar(--text, ...)andvar(--display, ...), neither of which is a token, so both always rode their literal fallbacks; they now read--font-bodyand--font-heading. Every rendered value was verified byte-identical afterwards. Font sizes stay in px deliberately: at the 80% root,var(--text-body)would paint 12.8px, not the 16px the banner is drawn at. - Improved Six bare
#FFFFFFliterals tokenised in the pill and toggle components. The audit called these ambiguous, and they are in the abstract, but the property settles it: five are text on a filled pill (--text-inverse) and one is a toggle knob surface (--bg-surface). - Improved Dead CSS removed:
.action-buttons-standaloneand the.pv-toggleblock, the latter a transitional planner v1/v2 switch whose own comment asked for its deletion once the rollout landed. Neither had any markup left. - New Demo pages for
ws_audio_player,ws_audio_recorderandws_sidenav, added to the sidebar so they are reachable rather than direct-URL only. - New Second audit at
AUDIT-2026-08-23-css-foundations.md, covering the foundation layer specifically. Its headline finding is that 59 custom properties across five stylesheets are defined as themselves, which under the CSS cycles rule destroys them rather than inheriting the platform value, discarding 335 declarations on live pages. Not yet fixed: each change is visible and wants review.
v2.9.96: The focus ring the system described but never applied
- Fixed Baseline
:focus-visibleindicator inplatform-tokens.css(WCAG 2.2 SC 2.4.7, Level AA). The file defined--focus-ringand documented at length why it is sky blue, but nothing applied it: the site's only global rule lives incss/styles.css, which a page gets only throughincludes/header.php. Every page that hand-rolls its own<head>had no visible keyboard focus outside forced-colors mode, including the homepage, all of/tips/, and every page of this design system. The new rule uses:where()so its specificity is zero: a floor that never fights a component's own focus style. - Fixed
.ws-app-bar__switch-optionand.ws-choice-carddrew no focus ring at all. Both wroteoutline: 2px solid var(--focus-ring, #38BDF8), but--focus-ringis the whole shorthand, so the value expanded to2px solid 2px solid #0284C7and was discarded as invalid. Both now readoutline: var(--focus-ring, ...)like the eleven correct usages elsewhere in the file. - Fixed Two pointer targets under 24x24 (WCAG 2.2 SC 2.5.8, Level AA).
.ws-chip__removewas 18x18 with no padding and now carries an::after { inset: -3px }hit area, which reaches 24x24 without changing the chip's height..ws-planner-activity-inspector__closewent from 22x22 to 24x24.--target-min: 24pxalready existed, annotated with the SC number, and nothing was using it. - Improved
css/tips/styles.cssimportsplatform-tokens.css, ascss/dna/styles.cssand the academy bundles already did. It declared its own palette and imported nothing, so no token ever reached/tips/. - New Audit at
AUDIT-2026-08-23-system-audit.md: inventory, naming, completeness, WCAG 2.2 AA, token coverage and theming. Registry mirrors cleanly at 64 = 64, token coverage is 92.7%, theming is deliberately single-theme and documented as such, and reduced motion is handled by a universal gate. One finding needs a product decision: white on.ws-btn--primaryis 3.82:1 at the light end of the brand gradient, below the 4.5:1 that 11.2px button text requires.
August 2026
v2.9.95: The homepage gives up its inline CSS
- New
/css/homepage.css, holding what were two<style>blocks inindex.php. The page was 1,591 lines, 1,305 of them CSS; it is now 287. Markup is byte-identical and every surviving rule moved unchanged. - Fixed 892 dead lines removed, 156 rules. The page renders its hero with
ws_section_hero()and its DNA band withws_dna_cta(), which emit.wsh-hero*and.ws-dna-cta*. The hand-rolled.home-hero*,.hp-hero*and.hp-dna-band*families they replaced were still declared, along with.hp-product-card*,.hp-solution*,.hp-tool-card*,.hp-journey-grid,.hp-final-cta*,.hp-mini-agenda*and.hp-phase-pill. Three hero designs were declared; one is rendered. - Fixed The retired phase palette is gone from the homepage. Plan blue
#0284C7, Facilitate purple#7C4DFFand Reflect emerald#10B981survived as hardcoded fallbacks behind--phase-plan,--phase-facilitateand--phase-reflect. Those tokens are#E54D4Dtoday so nothing painted them, but a fallback is a value waiting for its token to move. All of it sat inside the dead rules. - Improved
bodytakes--paper-grid-image/--paper-grid-sizeinstead of re-typing the two gradients.--grid-line-colorisrgba(44, 36, 22, 0.06)and the size is24px 24px, exactly what the page had, so this is identical pixels; the token also resolves tononeunder forced-colors, which a literal gradient does not. The token's own comment names this as how five pages lost the grid. - Improved
.hp-quote-cardonto the card tokens. Moving the CSS into a.cssfile made it visible tocheck-drift.phpfor the first time, which immediately failed it onpadding: 24px. It now reads--card-padding(20px, a real 4px change), plus--card-radiusand--card-shadowin place of--radius-xland--shadow-sm, which are renames at identical values. - Fixed Known gaps, not closed here: five component templates still carry their own
<style>blocks rather than usingcomponents.css(footer.php269 lines,header.php159,section-hero.php109,dna-cta.php39,app-footer.php5), and the homepage still speaks a private token alias layer (--paper,--ink,--learn,--display,--text) over the platform tokens.
August 2026
v2.9.94: The app shell's views give up their inline CSS
- Improved Four
<style>blocks moved out ofapp/views/*.phpintoapp/css/app-shell.css: the.modal-overlayFOUC guard,.lp2-nudge*,.prvh-*and.pwa-install-banner*. Inline CSS in a PHP file is invisible tocheck-drift.php, which filters on the.cssextension, so the card rules in those blocks had drifted toborder-radius: 12px; padding: 18pxwith nothing to catch them. They now read--card-radiusand--card-padding, and the card hover reads--card-shadow-hoverinstead of a hand-rolled warm shadow that no token defined. - Fixed About 660 lines of dead stylesheet removed.
.lp2-card*,.lp2-life*,.lp2-btn*,.lp2-recent*, the whole.fach-*vocabulary, all ofapp/css/synthesize-home.css, anddashboard.css's.dash__rowsection were each orphaned by the same event: the page moved tows_workshop_list()and its old styles stayed.app-shell.css's workshop-card border rule and section-heading rule were carrying nine of those names. - Fixed
body.launchpad-pagedeleted from Plan's home view. It re-declared nine--mainsite-*and--planner-*tokens as red, and it could never match: the shell sets$body_class = 'app-shell'. Those were eight of the elevenhardcoded-hexviolations the file carried inpage-baseline.json; the entry is now{"raw-component": 4}. - Improved Prove's picker joins
.app-hero. It was the one section with a hand-rolled heading and its ownclamp()ramp, and the one page missing from the shell's page-frame neutraliser, so it indented its content further than every other view. - Fixed Known gap, not yet closed:
check-page.phpreads<style>blocks but only scans them for hex literals, so card geometry written inside a PHP file is still ungoverned. SeeAUDIT-2026-08-22-app-css-localization.md.
August 2026
v2.9.93: The 6C sequence strip and step chips
- New
.agenda-gen-6cincss/planner/components/_agenda-generative.css: the 6C sequence strip in the generator's rationale panel. Six rows on a shared vertical spine drawn with a::beforehairline in--planner-gray-200, trimmed to half height on the first and last rows so the line starts and ends at the dots rather than overshooting. A covered step gets a filled--phase-learn-darkdot with a--planner-bg-whitering punching it out of the spine; a missing step gets a hollow dashed--planner-gray-400dot and its minutes column turns red. Grid is16px 1fr autoso the minutes stay right-aligned andtabular-numskeeps them from dancing. - New
.agenda-gen-c: the per-row step chip (Connect, Collect, Choose, Create, Commit, Celebrate) sitting next tows_activity_badgein the generated agenda row head. Brand red on--phase-learn-very-lightwith a--phase-learn-lightborder. - New Two colour systems, deliberately kept apart. The activity-type badge keeps its sanctioned functional coding (exercise blue, icebreaker green, break grey, custom purple) because item types must stay distinguishable on the canvas. The 6C steps are a sequence, not categories, so they are not colour-coded against each other: one brand-red accent marks "covered" and a hollow dot marks a gap. Giving the steps their own six-colour scale would have put a second colour axis in direct competition with the type colours in the same row.
August 2026
v2.9.92: Plan Settings goes tabbed, and the app-bar gear turns white
- New
.planner-settings-tabsincss/planner/components/_modals.css: the modal'sws_tabsnav, flush under the modal header with a--planner-borderrule beneath it. The panes are stock.ws-tab-pane, and switching runs on the existingwsTabsInit/wsTabsSwitchpair inincludes/components/components.jsrather than a second implementation. Note thatcomponents.jsalready auto-runswsTabsInitover every.ws-tabsonDOMContentLoaded, so callers should only ever callwsTabsSwitch; callingwsTabsInitagain double-binds the click handlers. - Fixed Ghost icon buttons in an app-bar trail were reading dark on the red bar.
ws_icon_buttonrenders a.ws-btn--ghost, which inherits the default warm ink, while every.ws-app-bar__actionbeside it is--text-inversewhite. Added.workshop-planner .ws-app-bar__trail .ws-btn--ghostto_component-bridge.cssgiving the glyph, hover and focus ring the same white treatment as its neighbours. Scoped to the planner deliberately: Synthesize's gear sits in the same slot and has the same problem, but sweeping it is a separate call. - Removed
.planner-settings-sectionand.planner-settings-section__titlefromcss/planner/components/_modals.css. Tabs replaced the stacked sections, so there is no adjacent-sibling divider to draw and no section heading to style..planner-settings-notestays (it still styles the signed-out and no-workshop captions) and.deck-settings-hintis untouched. - Fixed Paired fields in modals must be direct children of
.ws-form-row.ws_form_row($a . $b)nests its content in a single.ws-form-row__field, which is a block, so the two.ws-fieldelements never become flex siblings and.modal-overlay .ws-form-row .ws-field { flex: 1 }has nothing to act on. Use a raw<div class="ws-form-row">with the fields inside it, as the Create New Workshop modal does. No CSS change was needed; the rule was already correct.
August 2026
v2.9.91: Planner settings modal styles
- New Planner settings-modal section styles in
css/planner/components/_modals.css:.planner-settings-section(adjacent sections divided by a--planner-borderrule),.planner-settings-section__title(mirrors.export-label),.planner-settings-note(muted caption), and.deck-settings-hint(the export modal's pointer to Planner Settings). All values are platform tokens; the modal itself is the standardmodal-planner.phpshell withws_input/ws_select/ws_buttonfields, and the topbar gear is a stockws_icon_button.
August 2026
v2.9.90: The lifecycle strip catches up to the fourth phase
- Improved
ws_lifecycle()now renders all four workshop phases: Strategize, Plan, Facilitate, Synthesize, matching the vocabularyws_wl_journey()already uses in the workshop listing. The component previously still spoke the retired three-step reading (Designed, Delivered, Synthesized) even though nothing calls it: a documented design-system component teaching a lifecycle the product no longer has would mislead anyone building from it. The retired three values are kept as aliases (designed→plan,delivered→synthesize,synthesized→synthesize), cheap insurance since this repository's working tree carries substantial uncommitted work that a plain grep of committed code cannot see. - Fixed The strip wraps instead of overflowing at narrow widths.
.ws-lifecycleand.ws-lifecycle__stepsgainedflex-wrap: wrap, amax-width: 100%ceiling, and arow-gaptoken for the wrapped state. The old single-row layout held three steps at any width but pushed a fourth off the edge of a 375px viewport.
August 2026
v2.9.89 — The unified shell, rehearsed in mockups
- New Record-strand mockup set: 24 tokens-only HTML states at
synthesize/mockups/record-strand/(contact sheet atindex.html) rehearsing the unified app shell: white global header, dark icon rail, red per-app bar, and a bottom action rail. Every value is avar()againstplatform-tokens.css, so the mockups cannot drift from the system. The shell patterns (ws-global-header, rail-variant sidenav,ws-app-bar,synth-action-bar) are candidates for promotion tows_*components when the strand is built.
August 2026
v2.9.88 — Every card is one size, and the check that keeps it that way
- New
ws_choice_card()andws_choice_cards(): one way into a task, offered beside the others. Icon, title, a sentence of what you get, and a foot carrying the time cost and the action. Three pages were each carrying their own copy of this markup and CSS — Plan's home, the planner home, and the builder's empty canvas — and the copies had drifted: 20px padding against 18px, a link-styled call to action against a filled button. One component now, with a demo page. - Fixed The card is a real control. The old markup was a
<div role="button">, which looks like a button and does nothing on Enter unless you write the key handling yourself, and nobody had. It renders as a<button>, or an<a>when you passhref, and brings the behaviour with it. - Improved
--card-paddingnow means something. It was declared at 24px and read by nothing while every page-level card drew itself at 20. The token moves to 20 — the value the cards were actually at — and--card-padding-compact(12px) joins it for dense rows like the planner's library..wl-card,.listing-card,.library-cardand.ws-card--contentnow read the card tokens instead of hardcoding their own geometry. - New Check 4 in
check-drift.php: a card rule that setspaddingorborder-radiuswith a literal instead of a token fails the build. Ratcheted againstcard-geometry-baseline.json, which grandfathers the 124 literals that predate the tokens — they show as a WARN count, and shrink as cards are repointed. Nine self-test fixtures cover what counts as a card: BEM modifiers and pseudo-classes do,__elementclasses and.card-shelfdo not.
August 2026
v2.9.87 — The Synthesize action rail, and a note that changes its mind
- New
ws_synth_action_bar(): the stone rail that closes an empty Synthesize surface. A note pinned at the left says what the room should have brought back; the one way in sits at the right, rendered throughws_button()rather than hand-cut. Built from thesynth-action-barframe, which is now a component in Figma too. - New The note rotates. One nudge covers one situation, and a reader with nothing to synthesize is rarely stuck for the reason the first note names, so the bar cycles its set every 12 seconds instead of repeating itself. Hovering or focusing holds the current note, a hidden tab stops the clock, and under
prefers-reduced-motionnothing moves at all. Pass your ownnotes, or one note to make it stand still. - Improved The note is paper, not handwriting. The mock set it in Marker Felt; the platform has two families and this is not one of them, so the sticky reads as a sticky through the tilt, the amber paper, and the soft shadow, with the text in Inter. Colours all resolve to existing tokens:
--color-warning-very-lightpaper,--color-warning-lightedge,--color-ink-secondaryink,--color-ink-faintrail.
August 2026
v2.9.86 — The app bar learns which app it is in
- New
'state'option onws_app_bar():strategize,plan,facilitateorsynthesize. Each supplies that app's centre (nothing for Strategize, the duration/date/start-time read-out for Plan, start-session and join-code for Facilitate, the Recap/Workspace switch for Synthesize) plus the shared action cluster, with Share replacing Continue in Synthesize. Built from the four bars drawn in Figma, which are now one component set there with a matchingstatevariant. - New
.ws-app-bar__controland.ws-app-bar__switch: the read-out and the segmented switch, promoted out of the planner's and Synthesize's own markup into the component. Both are translucent white, because they sit on the red bar rather than on a surface. - Improved A named state fills only what the caller left empty. Pass your own
centeroractionsand they survive untouched. A state inferred from the shell's?view=is more cautious still, adding only the.ws-app-bar--{state}class: the existing callers pass their own actions, and a bar must never grow controls the page has no code behind. - New App Bar demo page at
/design-system/components/demos/app-bar.php, showing the four states together.
August 2026
v2.9.85 — The workshop listing learns its stage scope, and all three pages now share it
- New
'stages'option onws_workshop_list(): the stages a given listing can actually contain, e.g.['plan', 'facilitate']. It drives the filter chips and the kanban columns together, so neither offers a stage that can never appear there. It deliberately does not touch a card's journey strip — that shows the workshop's whole life, and truncating it would misreport where the workshop is. Added when Facilitate exposed the inconsistency: hiding its chips because only two stages apply, while the kanban still drew four columns with two that could never fill. - Improved All three list pages now render from this one component, each configured to its own job rather than forked: Plan's "Recent workshop agendas" (in-planning only, six, grid and list), Facilitate's "Choose a workshop" (runnable only, scoped to two stages, with the kanban standing in for the retired Ready-to-run / In-progress tabs), and Synthesize's picker (all stages, per-row action by stage). Between them this retired three hand-cut card templates, three copies of the same fetch-and-render JavaScript, and the skeleton placeholders all three used to paint while waiting.
August 2026
v2.9.84 — Synthesize gets a bottom rail: one place to commit
- New The Synthesize bottom rail (
app/css/synth-rail.css, filled per screen bysyncRail()insynthesize.js): context on the left, the screen's one commit action on the right. Intake's "Start synthesis" left the artifacts rail, review's "build the synthesis" left the end of a long scroll, and the extraction screen's Cancel left the content — all three now share one address. The rail owns no logic: its buttons re-dispatch thedata-actvalues the delegated handler already had, so each action still has exactly one implementation. - Fixed A surface outside
#synth-appneeds its owndata-actlistener.render()owns that node's innerHTML, so anything that must survive a re-render lives outside it — and the delegation bound to the root never sees those clicks. The topbar already carried its own binding for this reason; the rail shipped without one and its buttons rendered perfectly while doing nothing. Caught by the existing Playwright suite, and now covered by a test that asserts the click reaches the handler rather than that a screen changed. - Improved Its own stylesheet rather than a block in
synthesize.css, because the picker page does not load the tool's stylesheet and two copies of the rules would drift. Not fixed-position either:.synth-shellis already a flex column with a scrolling stage, so the rail is that column's last child and never covers the content above it.
August 2026
v2.9.83 — The workshop listing gets its data layer, and Synthesize adopts it first
- New
ws_workshop_list_fetch($pdo, $userId)returns every workshop a user can see, already normalized. It calls the same functions the two endpoints wrap —getWorkshopList()for lifecycle stage and the room's counts, plus one grouped join for duration and item count — so a page renders its rows in the HTML instead of painting a skeleton and filling it a round-trip later. The three list pages can now share one server-side read. - Fixed An unknown item count renders as nothing rather than "0 items":
ws_workshop_list_normalize()keeps a missing count asnull, and the meta line omits it. Claiming zero when you mean "not known" is a different and wrong statement. - New
ws_wl_duration()formats minutes as "2h" / "1h 45m" locally rather than borrowingincludes/data.php'sformatDuration()— that file callsloadFromDatabase()at the bottom, so requiring it for a string helper would fire a full content load on every page that lists workshops. - Improved Synthesize's home page is the first adopter: its "Ready to synthesize", "Working from something else?" and "Recent recaps" sections are replaced by the listing, and the page's list JavaScript is gone with them. Timeline is deliberately not among the offered views there — it groups by scheduled date and the Workshop Record carries no scheduled-at stamp yet, so every row would land in one undated bucket.
August 2026
v2.9.82 — The workshop listing becomes one shared pattern
- New
ws_workshop_list()(app/includes/workshop-list.php, styles inapp/css/workshop-list.css, behaviour inapp/js/workshop-list-views.js): one listing with four interchangeable views — grid, list, kanban by stage, and a timeline grouped by scheduled / recent / drafts — under one chrome of eyebrow, title, stage filter chips, view switcher and an "All workshops" footer. Built from the Figma Recent workshops frames; the file's three variables map exactly onto existing tokens (paper#FAF7F2=--bg-paper,#C73E3E=--phase-learn-dark,#FEE2E2=--phase-learn-light), so it introduced no new colour. - New Server-rendered, unlike the three hand-cut lists it is meant to replace: every view is in the HTML and the switcher only toggles which is visible, so changing view or filtering by stage costs no fetch and no skeleton flash.
ws_workshop_list_normalize()takes a/api/plans.phprow pluslifecycle_statusand maps it to the stage a workshop is ready for (designed→ Plan,delivered→ Facilitate,synthesized→ Synthesize). - New Demo and tweaking surface at
/design-system/app-workshop-list.php, carrying the Figma frames' own sample content plus the three per-page configurations. Not yet adopted by Plan, Facilitate or Synthesize — those three swaps are the next step.
August 2026
v2.9.81 — Tokens must exist: the drift check resolves every var() reference
- New check-drift.php check 3: every
var(--x)in shipped CSS must name a custom property defined somewhere real. The universe is all scanned sheets plus runtime-set properties (inline--x:in markup,setProperty, React computed style keys), because app-local token sheets and per-item inline values are legitimate here; what fails is a no-fallback reference to a property defined nowhere at all, the class where the declaration is invalid at computed-value time and silently resets. References carrying a fallback warn instead.--self-testruns the parser fixtures, including the historicalvar(--space-7)case that shipped a collapsed padding; accepted one-offs go in$KNOWN_DRIFTascss-var:<file>:--<token>. - Fixed The 26 no-fallback references the first run caught:
--space-7(taste buy strip),--space-9(workshop-record artifact icons), Facilitator's never-defined--font-size-base/sm/xsscale (29 declarations were inheriting instead of sizing), the orange card/badge family plus two legacy surface and ink names instyles.css, admin's--admin-accent-*variants, MyWorkshopr v3's neutral vocabulary,--planner-primary-dark→--planner-red-dark,--color-amber-700defined beside its existing--color-amber-100, the unused.mw-svg-iconmask block removed, and the bounce-in animation riding--ease-outnow that the overshoot easing is retired. - Fixed Ported Synthesize renders at native scale inside the
/app/frame: the frame's 0.8 zoom had shrunk the app's literal-px type and its rem-tokenws_*labels alike, so.app-shell .synth-shell { zoom: 1.25 }composes back to 1:1 rather than compensating per component.
August 2026
v2.9.80 — The stsh shell becomes a shared component
- New The phase-journey shell (top bar + dark rail + ruled canvas) is now shared chrome: markup in
includes/components/stsh.php(stsh_topbar(),stsh_rail(),stsh_sym()), styles incss/stsh-shell.css, and a document-head layout atincludes/components/stsh-header.php(registered in check-page's LAYOUT_FILES). Born in the Strategist; extracted when Synthesize adopted it. The Strategist'sst_*helpers now delegate here, and the/app/frame deliberately never loads it — its own sidenav is the chrome there. - New
.stsh-canvas--appvariant for viewport-locked tools: the canvas stops scrolling and hands its box to the app inside, keeping the app's own#main-contentsizing rules working unchanged.
August 2026
v2.9.79 — Synthesize's results screen finishes adopting shared platform chrome
- Fixed The metadata-first "New synthesis" modal, source toolbar, flat/triage review, five-tab result board, Kanban/Outline theme layouts, persistent source rail, and side-panel AI are removed with their dead CSS — each had no remaining caller once the recap-first redesign shipped (Tasks 2–10). The legacy internal home/gallery is not among them: it is retired from the in-shell experience only (the "Your syntheses" control and the Back-past-earliest-stage path now redirect to the
/app/product home instead of rendering it) and is retained solely as the standalonesynthesize/app.phpentry's legacy/anonymous landing and boot-failure fallback. What's left carrying the in-shell screen is entirely shared platform pattern: thews_app_bar, the.ws-tabscomponent driving Workspace's own tab bar, the.recap-*/.ev-*evidence-card family, the platform focus-ring token on every disclosure and tab control, and the existing responsive rules. No new shared component was added here — this closes out adoption of ones already in the system, not a new pattern. - Fixed
.ol-themeand the rest of the retired Outline/Kanban/theme-card/card-menu selectors are deleted rather than left as unreferenced rules for the next design audit to re-flag.
August 2026
v2.9.78 — The Workshop Thread graduates to a production page
- New The connected-workshop direction now renders at
/index-app.phpwith production components. The root-owned responsive stylesheet and interaction script retain the Thread mockup's two-page hero, four-stage workflow, and accessible tab behavior while the realws_header(),ws_button(), andws_footer()supply the shared shell.
August 2026
v2.9.77 — The run-sheet preview is a two-page stack
- Improved The Workshop Thread hero layers the facilitator-notes page behind the agenda. Opposing token-based offsets expose the back page's dark rail while keeping the front page dominant; mobile narrows the spread so the stack stays inside the viewport.
August 2026
v2.9.76 — The Workshop Thread hero shows the finished run sheet
- Improved The landing mockup's hero artifact now uses the real Planner run-sheet preview. A token-based paper mount and restrained offset layer keep the tall export legible in the asymmetric hero, then remove the rotation and narrow the preview on smaller screens.
August 2026
v2.9.75 — The Workshop Thread begins before the agenda
- Improved The landing mockup's lifecycle now supports four connected stages. Strategize precedes Plan, Facilitate, and Reflect; the mobile stage switcher becomes a two-by-two grid so all four labels remain readable without shrinking the type.
August 2026
v2.9.74 — app pages close on a 24px rail, not on the marketing footer
- New
ws_app_footer()— a 24px--gray-400rail (templateincludes/components/app-footer.php) that ends a page inside a sub-application. It holds nothing: no links, no signup, no copyright. A tool the user opened deliberately should stop when the content stops, and the previous ending was 2,500px of Level Up grid, newsletter CTA and dark brand bar below every workspace. Self-contained like the composite it replaces — it emits the token sheet, its own style block, and the toast/auth core, each once — because the Planner and Facilitator run their own CSS stacks and don't all loadcomponents.css. Carriesaria-hidden="true": an emptycontentinfolandmark is one more thing a screen-reader user has to check and discard. - Improved The routing is one test in one file, not a flag on every page.
ws_footer(), a directcomponents/footer.phpinclude and the legacyincludes/footer.phpall funnel through the composite template, soif (ws_is_app_page())at its top covers roughly 120 pages across sixteen apps without touching any of them.ws_is_app_page()matches the first request-path segment againstWS_APP_PATHS, which mirrors the app inventory in PRODUCT.md §5 — adding an app is one string. Reading the request path rather than the script path is deliberate: under the built-in dev server the router ownsSCRIPT_NAME, so the real page is only visible viaREQUEST_URI. - Improved Marketing surfaces are untouched. The homepage,
/about/,/pro/,/solutions/,/help-center/and/changelog/keep the full composite — the Level Up grid and the newsletter CTA are how those pages earn their keep, and the rail would cost signups. The split is by path, so an app's own landing page (/planner/,/facilitator/) takes the rail with the rest of its app. - New Documented at
components/demos/app-footer.php, with the Footer demo now linking to it instead of repeating the stale claim that app shells run footer-less. Both demos are in the sidebar; Footer had never been listed. DESIGN.md §12.7 replaces its "app-shell exception" with the app rule.
August 2026
v2.9.73 — /app/ renders at 80% via zoom, not via redeclared tokens
- Improved
body.app-shelltakeszoom: 0.8. This is the second attempt at a smaller frame and it is deliberately the other mechanism.zoomscales the rendered result, so every proportion inside stays the one the design system specifies and the frame is simply drawn smaller; redeclaring tokens at a fraction (v2.9.68, reverted in v2.9.71) rounds each token independently, so a 2px hairline and a 48px gutter shrink by different ratios and the relationships drift — which is what made 0.67 read as broken rather than small. It also composes with the reader's own zoom instead of replacing it: a reader at 125% sees this frame at 100%. And one property reverses it, where the token pass needed two stylesheets, 128var()fallbacks and 45 literals put back. - Fixed The body height is
125dvh, not100dvh.zoomscales the box after layout, so100dvhis drawn at 80% of the viewport and leaves a fifth of the screen blank under the frame; 100 / 0.8 fills it exactly. This is the one real cost of the approach, and any viewport unit added to these stylesheets needs the same treatment. Verified: the frame bottom sits flush with the window, and the launcher scrim still covers the full viewport despite the zoomed containing block. - Improved The workshop card takes a
#A8A29Eborder in the shell —--gray-400, four steps up from the--gray-200it had, so the card reads as a drawn edge on the drafting paper rather than a hairline that dissolves into it..dash__row,.lp2-card,.fach-card,.synh-cardand.synh-pickupare one object seen from five places, so they are named together inapp-shell.cssrather than edited in four files — which also leaves the standalone pages on the--gray-200they share with the rest of the site..dash__viz,.dash__ftrackand.dash__emptykeep the neutral border: they are panels, not workshops. - Fixed The dashboard row's hover no longer runs backwards. That hover was tuned to lift
--gray-200to--gray-300; against a--gray-400rest state the same rule lightens the edge, so the card receded at the moment it lifted..app-shell .dash__row:hovertakes--gray-500and keeps the direction the interaction was designed around. It is the only one of the five whose hover touches the border; the rest move transform and shadow. - New Synthesize's hero eyebrow carries a BETA marker —
.app-hero__beta, inside the.app-hero__eyebrowpill rather than beside it. As a sibling pill it read as a second eyebrow: same outline, same fill, so the eye had to read both to learn one was a status. Inverted and tucked inside, it is unmistakably a marker on the section name, and solid--phase-learn-darkcarries white at 4.98:1 where the eyebrow's own light fill would have put 10px text at 3.49:1.
August 2026
v2.9.72 — The shell's header bar is 48px, and it is a layout decision, not a scalar
- Improved
body.app-shell > .wsh-headeris 48px with 20px gutters, down from the component's 64px and 24px. Inside the shell the bar carries a brand, two icon buttons and an avatar — the nav, section tag and switcher are hidden — so it is an app title bar, not the site's navigation band, and 64px gave a row of chrome the height of a content card. - Improved The controls come down to 32px (mark, icon buttons, avatar), radius 9→8px, mark glyph 22→20px, wordmark 18→16px. 36px boxes in a 48px bar leave 6px of clearance, which reads as a bar too short for its contents; 32px restores a clean 8px, measured. The icon glyphs inside the buttons are untouched — the target tightens, not the icon.
- Note This is the shape v2.9.71 said a density change should take. One region is resized and its contents fitted to it, so the bar reads as deliberately compact; the 0.67 pass that preceded it applied a scalar to the whole token set and read as a zoomed-out page. Restated in
app-shell.cssrather than the component becausews_headerdraws itself in literal px with no height token to theme.
August 2026
v2.9.71 — The shell's 0.67 density is reverted; /app/ renders at platform scale
- Fixed The
body.app-shelltoken block is gone. v2.9.68 redeclared spacing, radii, the type scale and icon sizes at 0.67 to correct a frame judged too large. What it produced read as a permanently zoomed-out page:--text-bodyresolved to 0.67rem against the platform's 80% root, so body copy painted at 8.6px. The tokens are inherited fromplatform-tokens.cssagain, which restores everyws_*component rendered inside the app at once. - Fixed The literals that were scaled to match are back too — 128
var()fallbacks acrossapp-shell.cssanddashboard.css, plus 45 bare values in the dashboard and the shell's launcher, hero and onboarding. Borders, outlines, focus offsets and breakpoints are untouched, because they were never scaled down. - Fixed The
ws_headeroverride block is deleted rather than rewritten. It existed only to restate the header at 0.67 in the literal px the component draws itself in; with the shell at platform scale the header renders at its own 64px, which is the point. The nav, section tag and switcher stay hidden — that is composition, not sizing. - Improved The rail returns to its documented 108px with a 56px indicator, 64px destination height, 24dp icon and 12px label — the figures its own comment describes, which the 0.67 pass had left the comment out of step with.
- Improved The content column returns to 1131px, and the sentence step's 992px override is removed with it: that cap existed to widen the step against a 758px column and would now be pinning it narrower than the page it sits on. The sparkline keeps its 316px cap — that makes a user unit a pixel and is independent of the density question — and its labels go back to 10.5 and 12 units.
August 2026
v2.9.70 — The section hero opens on 48px, not on the content padding
- Improved
.app-herotakespadding-top: 48px, and Strategize's.st-herotakes it inside the shell, so all four sections open on the same gap above the pill. The pill had been sitting on.app-shell__content's 16px alone, which read as the page starting mid-scroll rather than at its top. - Note 48 is a literal, not
var(--space-12). The shell declares its spacing scale at 0.67, so the token would have rendered 32px; the figure here is the one measured on screen. Any future convergence of.st-heroonto.app-heroinherits it from the one rule.
August 2026
v2.9.69 — The sentence step opts out of the shell's reading measure
- Improved
.app-shell__contentwidens from 822px to 1056px on the Strategize sentence step, taking the madlib card from 758px to the 992px it renders at standalone (the app's own 1040px page less its 48px gutters). The cap is a reading measure and the sentence is not prose — it is six inline controls read across a line, and at 758px it wrapped mid-clause into short ragged lines. Selected with:has(#stSentencePage)rather than a new body class, so the other four Strategize steps, which are prose and cards, keep the 822px measure untouched. - Improved The hero sub on that step takes a 62ch measure — the same figure
.app-shell__ledeuses — so the copy above the card does not stretch to the card's new width. Widening a container widens everything in it; the measure has to move down to the prose that still wants one.
August 2026
v2.9.68 — The app shell encodes its density instead of relying on browser zoom
- Improved
/app/renders at 0.67 of the platform scale, stated in CSS rather than reached by zooming out. The shell had been drawn ~1.5× too large and only read correctly at 67% browser zoom. A forcedzoomwas rejected: it overrides the reader's own zoom instead of composing with it, and media queries keep measuring the unscaled window, so every breakpoint fires at the wrong width. Insteadbody.app-shellredeclares the spacing, radius, type and icon tokens at 0.67, which carries everyvar(--space-4, …)inapp/cssand everyws_*component rendered inside the app; the 229 literal px in those two files are scaled to match. Scoped to the shell's own<body>, so nothing outside/app/moves. - Improved
ws_headeris scaled to match inside the shell. The component draws itself in literal px in its own<style>, so the token scale cannot reach it and it would otherwise sit a third larger than the frame under it.app-shell.cssrestates the pieces the shell actually shows — height 64→43px, brand mark and icon buttons 36→24px, the account menu — and skips the nav, tag and switcher, which the shell already hides. - Fixed The dashboard sparkline's labels were painting at twice the surrounding text. Its sizes are SVG presentation attributes in user units, so a stretched chart magnifies them: at full column width a 10.5-unit month label rendered at 15.9px beside 8px body copy. The attributes are scaled and the SVG is capped at its own 316-unit viewBox so it never renders above 1:1. This is the one place encoded density and real browser zoom genuinely differ — the grid columns are
fr, so they keep their pixel width while their contents shrink, where zoom would have enlarged the CSS viewport instead. - Note Breakpoints, border widths and focus rings are deliberately unscaled. Breakpoints map to real device widths and shrinking them would hand a 390px phone the desktop layout; a 0.67px hairline renders as a smudge; the focus ring is an affordance, not decoration. The cost of leaving breakpoints alone is that the shell collapses to its narrow layout slightly earlier than a zoomed browser would, which costs nothing because it collapses to a layout that works.
August 2026
v2.9.67 — The app launcher adopts the coloured modal header
- Improved The shell rail's active indicator inverts —
--ws-sidenav-indicatormoves from--phase-learnto--phase-learn-lightand--ws-sidenav-on-indicatorfrom--phase-learn-lightto--phase-learn-dark, so a filled dark red glyph sits on a light red pill. The rail's active label takes the pill's light red too, so both halves of the state read as one thing. Themed entirely through the component's own token API inapp-shell.css—ws_sidenavitself is untouched, and its default light-rail theme still applies wherever else it renders. Numbers: glyph 4.06:1 on the pill (past the 3:1 for a filled shape), rail label 12.6:1 on ink, up from the 4.01:1 it used to run at. The expanded variant wraps the whole row in the pill, so its 15px label sits at 4.06:1 — under the 4.5 text floor, up from the 3.13:1 this replaced, and recorded in the stylesheet rather than adjusted away. - Improved The All applications launcher takes
ws_modal's coloured header. Same--phase-learnthe Strategist passes toheaderColor, same white title, same translucent close. It is bespoke markup rather than aws_modal— it carries its own scrim, focus handling and rise animation — so this matches the component's treatment rather than replacing the dialog wholesale. Structure follows too: the panel drops its padding and clips, so the band runs full width and the corners cut it, and the scroll moves off the panel onto.app-launcher__groupsso a long list travels under a header that stays put, which is the splitws_modalmakes with aflex-shrink:0header over a scrolling body. The panel widens from 880px to 1040px at the same time: at 880 every one of the four phase columns wrapped its descriptions, which made four short lists read as one dense block. - Fixed The launcher's close is the platform's close again. It is a
ws_icon_button, so it arrived in ghost chrome — grey border, near-white hover fill — which on a red band read as a white box with a white glyph lost inside it. It now matches.ws-modal__closeon a coloured header exactly: no border, no fill at rest, a 70% white glyph, and a 10% white wash on hover. The quarter turn isws_modal's as well, and it turns the glyph rather than the button, so the X becomes a + and the button's own square does not move. Overriding the ghost hover needs:hover:not(:disabled)—.ws-btn--ghost:hover:not(:disabled)outranks a plain:hover.
August 2026
v2.9.66 — Dashboard rows: stacked, on paper, marked by phase
- Improved
.dash__rowis a full-width row again rather than a 199px two-up card. One column of long rectangles gives a "what next" queue a single left edge to scan down; the two-up grid made each entry a destination to weigh against its neighbour..dash__listis a flex column, the row is one line of icon → body → pill with the pill taking the slack onmargin-left:auto,.dash__phasesruns horizontally and wraps, and the title is one ellipsised line at 16px. The narrow-screen grid that keeps the icon beside the title is unchanged, but its.dash__ctarule now matches the one-line rule's specificity — an auto margin beatsjustify-selfand had stranded the pill on the far right of its own row. - Improved Every dashboard surface moves to
--bg-paperfrom--bg-surface—.dash__row,.dash__vizand.dash__empty. The shell already paints paper, so white cards floated a layer above the page; on paper they read as sheets on that desk, with borders and the hover lift doing the separating.ws_task_checklistcarries its own white card, which reinstated the white inside the two panels that hold one, so it drops its background, border, radius and padding inside.dash__journeyand.dash__firstrunand lets the panel be the card. Scoped to those two containers rather than changed at the component — MyWorkshopr's overview panel uses the checklist standalone and still wants the card, and its demo page is unchanged. - Improved
.dash__ctareads "Open" on every row, in both the recent and attention lists, replacing "Synthesize it", "Build the agenda" and "Run it". The destination is unchanged — each row still opens on its own unfinished step — but the phase strip beside the button already names where the workshop is, and a button naming it again in different words could only disagree. - Improved The row's icon names the phase the workshop has reached — the furthest
donein journey order, not its lifecycle stage. Marks are the shell rail's own, so a row and the rail agree: Strategizeknight, Plancalendar_view_day, Facilitateco_present, Synthesizelightbulb. Strategize waslightbulb_2in the rail, a near-twin of Synthesize's bulb; it is the knight in both now, which also picks up theknight-fill.svgactive state added in v2.9.64. - Fixed Facilitate rows rendered an empty icon tile.
co_presentis font-only andicon()returns an empty placeholder for it — the same trapws_buttonhit in v2.9.65.appDashboardIcon()inapp/includes/dashboard.phptakes the SVG when there is one and lets the font draw the ligature otherwise. - Retired
.dash__qis retired and its CSS deleted. The quick action was a tinted borderless pill, deliberately quieter than the hero, sized for a row of three. The row is two now — New workshop and Run a session, after "Pull a rescue card" moved out to the rail where it belongs — and at two they are the only ways into the day the page offers, which makes them calls to action rather than shortcuts past it. Both arews_buttonprimary, so the dashboard carries no bespoke button CSS at all..dash__quickstays as the flex row that lays them out; the promotion ledger ondesign-system/redesign/dashboard.phprecords the pattern as retired rather than candidate.
August 2026
v2.9.65 — ws_button: an icon name without an SVG no longer renders a gap
- Fixed
ws_button()drew a blank space wherever the icon name had no local asset. The Material Symbols set is far larger thanimages/icons/, andicon()returns an empty<span>placeholder for anything it cannot find — soco_present,lightbulb_2and the rest of the font-only names produced a button with a hole where its icon should be. The button now takes the SVG when one exists and lets the font draw the ligature otherwise, which is the same fallbackws_sidenav()has used since v2.9.64. Template:includes/components/button.php; no new classes, no markup change for icons that already resolved.
August 2026
v2.9.64 — ws_sidenav: the active destination's icon fills in
- New The active item's glyph now renders in Material's filled style, per the M3 rail spec. The local SVG set is outline-only and its cutouts are wound for the default fill rule, so CSS cannot solid-fill it (tried, verified); instead a
{name}-fill.svgasset wins when one exists, and everything else falls back to the Material Symbols font glyph with theFILLaxis on, which the platform's font request already loads.images/icons/knight-fill.svgships for the custom knight. Template:$renderActiveIconinincludes/components/sidenav.php; one new class,.ws-sidenav__icon--filled.
August 2026
v2.9.63 — Dashboard patterns, registered before they are components
- New Redesign › Dashboard Patterns: the promotion ledger for the shell home's vocabulary. The consolidated shell's dashboard ran ahead of the ws_* rule while its patterns were still moving; this page is the decision that they are design-system material. Four candidates — quick-action pill, attention row, hero card, funnel card — each shown live from
app/css/dashboard.css, each mapped against the ws_* component it overlaps, with a stated graduation rule: a pattern that earns a second consumer becomes aws_*helper; one that stays dashboard-only stays a pattern. - New The measurements travel with the patterns. Worst text contrast on the dashboard is 5.30:1 (dark red on the tint), the hero's decorative shapes sit at
z-index: 0because::afterotherwise paints over the title, and every font size is stated in px because the platform's 80% root font-size renders the rem type tokens a fifth small.
August 2026
v2.9.62 — The Material 3 navigation rail, in Workshopr's colours
- New A Redesign section in the design system. The app-shell redesign track: global, shipping components that belong to a direction still being worked out, documented apart from the established set. Same gates as everything else — Workshopr tokens, measured contrast, every state specified, enforcer clean.
- New
ws_sidenav()— the vertical rail an app shell wears down its left edge. A standalone global component: it ships fromincludes/components/helpers.phplike every otherws_*and works on any page, not just the Strategist. Built for the chromeless window, where there is no browser chrome and no horizontal platform nav, so the rail is the only way between apps. Template atincludes/components/sidenav.php, styles under.ws-sidenav, documented at Redesign › Side Navigation. It pairs withws_app_bar(): the rail moves you between apps, the bar acts on the open document. - New Follows the Material 3 navigation rail for geometry and interaction — a pill-shaped active indicator, 24dp icons, a 56×32 indicator in the collapsed rail and a 56dp-tall pill when expanded, 60dp destinations, and state layers at 8% / 10% rather than flat hover swatches. Colour is not Material's. Red Unification and
platform-tokens.cssoutrank an imported palette, so M3's roles map onto ours:secondaryContainer→--phase-learn-light,onSurfaceVariant→--color-ink-secondary. - New Two variants.
rail(80px, label under the icon, indicator around the icon alone) andexpanded(240px, icon beside label, the pill is the item container, inset 20px). Labels show in both, as M3 prescribes; the rail keeps a tooltip on hover and focus because Workshopr's destination names are longer than the short words an M3 rail assumes, and a truncated label must stay recoverable from the keyboard. - Fixed Two colour pairings failed AA and were caught by measuring rather than assuming. M3's
onSecondaryContainerwas first set to--phase-learn-dark, which measures 4.11:1 on the indicator — under the 4.5 floor for text this size. It is now--color-inkat 12.5:1, which is also the more faithful reading of M3, whose light theme puts a near-black tone on the tonal pill. The badge was--phase-learnat 3.82:1 against white; it is now--phase-learn-darkat 5.0:1. - New Every state is specified and demoed: rest, hover, focus, pressed, active, disabled, badge, disclosure open/closed, and the rail tooltip. M3 ships no visible focus ring, so the platform's sky-blue one is kept on top of the state layer — a keyboard user needs it. Current-page never rests on colour alone: the indicator is a filled shape, backed by a weight change,
aria-current="page", and aforced-colorsmapping toHighlight. - New
images/icons/knight.svg— a chess knight for the Strategize entry, drawn to the house 24px /0 -960 960 960convention and checked for legibility down to 20px. The platform's own Strategist glyph isstrategy, which has no local SVG; the knight is a deliberate substitution for this nav, not a replacement for it. - Fixed A closed disclosure was still rendering its children.
.ws-sidenav__sublist { display: flex }is a class selector, so it outranks the user-agent's[hidden]rule and the attribute silently did nothing. Restored with an explicit.ws-sidenav__sublist[hidden] { display: none }— worth remembering for any component that setsdisplayon a element it also toggles withhidden. - New Every M3 metric is now a CSS custom property —
--ws-sidenav-item-height,--ws-sidenav-indicator-w/h,--ws-sidenav-radius,--ws-sidenav-inset,--ws-sidenav-icon-size, the label sizes, the state-layer opacities, and the four colour roles. A shell can retune the rail without forking it, and the defaults remain the M3 spec values. - New A playground and a bare preview. The playground drives those properties live and emits only the ones you changed as a copyable override block; it also reports the active and inactive contrast ratios as you pick colours, so a pretty pairing cannot ship unmeasured. The preview renders the rail full-height with no design-system chrome — the context it was actually built for — via
?variant=rail,?variant=expandedor?both=1. - Fixed The rail clipped its own tooltips and grew a stray horizontal scrollbar. The list was set to
overflow-y: autowith a comment claimingoverflow-xstayed visible — CSS does not allow that, and computes the second axis toautoas soon as the first scrolls. Only the expanded variant scrolls now; the rail isoverflow: visible, since a rail holding more destinations than fit is a content problem rather than something to hide behind a scrollbar. - Improved Icon sizing uses
--icon-sm/md/lgrather than the type scale — icons are not type, and the tokens already existed for exactly this.
August 2026
v2.9.61 — The first illustrated hero, a rule about the mascot, and a background the model would not match
- New Strategist gains a hero illustration in the platform house style at
/images/hero/strategist_hero.png— a facilitator filling the last blank in her sentence, the unbooked room waiting empty behind the glass. Filed with the rest of the set inimages/hero/under the<app>_hero.pngconvention. Matches the Facilitator / Pre-Read / Post-Workshop heroes: 1344×768 (16:9), full-bleed monochrome red scene, populated workspace, presented full width under the hero copy at--radius-2xlwith the same soft shadow as.prk-hero-image. - Improved The house hero spec, written down: ~16:9 (1344×768 dominant, 1024×576 also in use), one warm red hue family over cream, mean saturation 0.33–0.47, full bleed, a real populated room. Measure a candidate against that saturation band before shipping — the image model consistently lands near 0.20, roughly half as saturated as the existing set, and reads washed out beside them.
- Improved New rule, narrowing DESIGN.md §9: Nano Banana never stands in for the user. The first version of this hero cast the banana as the facilitator making the pitch, and was rejected — "it should never be the banana." §9's character remains right for ebook covers, chapter openers and emotional beats, but where an illustration depicts the person using Workshopr, draw a person. Prompts need an explicit "no fruit, no banana, no animal or food characters" ban or the model reintroduces one.
- Improved Two prompt lessons worth reusing: raster words must be drawn as abstract rounded bars of varying length to read as prose (uniform pills read as a form or progress bar, and any attempt at real lettering garbles — one draft produced "ru ama oi"); and the palette needs naming as a closed set, since an open brief let amber and gold in beside the salmon.
- Fixed The image model will not honour an exact background hex. Asked repeatedly for a flat
#FAFAF9ground, it returned a warm cream#FBEDD9every time — which would have read as a pasted rectangle against--bg-page. Resolved by knocking the background out to alpha with a border-seeded flood fill (contiguous only, tolerance 12), so the character's interior cream highlights and the ivory light wedge survive untouched. The asset now composites onto the page ground with no fringe. Reuse this approach for the next one — do not trust a hex in the prompt. - Improved
.st-land__herobecomes a two-column grid (minmax(0,1fr)+minmax(0,26rem)); the oldmax-widthmoved to the new.st-land__hero-copy. Art is bare on the page — no card, border or shadow — and collapses to one column under 900px. - Improved 369 KB → 170 KB via
pngquant --quality 70-95; palette PNG keeps full alpha. The illustration carriesalt="": it restates theh1, so announcing it would only repeat the headline.
August 2026
v2.9.60 — The modal close twists, and the Strategist joins the component runtime
- Improved
.ws-modal__closenow twists its icon a quarter turn on hover —--duration-base/--ease-outmicro-motion, guarded underprefers-reduced-motion. Applies to everyws_modalplatform-wide. - New The Strategist header now loads
components.js— it always hadcomponents.cssbut not the runtime, sowsModalOpen/wsModalClose(focus trap, inert background) didn't exist there. Its "Other…" sentence modal is the first consumer, using theheaderColorred header per the modal demo. - Improved
strategist.csspolish pass: panel hints and the practice page's reading text sit at--text-body(labels stay small);.st-toolnavretired with its markup; the exit strip steps up to--text-h2with a--mainsite-darkborder; the generated 60-second pitch renders as a--mainsite-very-lightcard; tool-card CTAs anchor bottom-right;.st-panel__hint--widelets full-width panels run one line.
August 2026
v2.9.59 — Strategist becomes six pages, and a max-width in the wrong unit
- New
strategist.cssgains a marketing block (.st-land) for the new overview page, plus.st-page--tool,.st-subhead,.st-navlinkand.st-toolnavfor the tool pages. Platform tokens throughout; the only functional color is the unified Learn red. - Fixed A
max-width: 40chon the landing hero crushed the whole column to roughly 320px.chresolves against the element's own font-size, and the hero wrapper inherits body text (~16px) — not the display size its heading renders at. The cap now sits on.st-land__title, wherechmeasures in the display font and 17ch is the three-line hero it was meant to be. - Improved Dead rules retired with the panel-toggle behaviour they served:
.st-tool[data-open],.st-viewport[hidden],.st-kit[hidden]. Panels are pages now, so nothing toggles. - Improved
.st-panel__titleis theh1on a tool page and is sized as one there; the print sheet learned the three new blocks.
August 2026
v2.9.58 — A bare app-shell token collapsed the Facilitator layout in production
- Fixed The Facilitator live dashboard rendered as one long scrolling column on production while looking correct on localhost.
--app-rail-widtharrived with the app-shell metrics and was consumed bare —grid-template-columns: var(--app-rail-width) 1fr 300px. A barevar()that resolves to nothing is invalid at computed-value time, so the whole declaration is dropped andgrid-template-columnsreverts tonone; the agenda rail, script pane, and interventions rail then auto-placed into rows instead of columns. Reproduced in Chromium and verified fixed: with the token file blocked entirely the grid still resolves to320px 804px 300pxwith all three columns side by side. - Fixed The same bare use existed in
synthesize.cssandcss/planner/base/_variables.css, so Synthesize and Planner carried the identical latent collapse. All four uses now carry the, 320pxfallback. - Improved Why production only: the token reaches these files through
@import '/design-system/platform-tokens.css?v=…', and that hand-written stamp was never bumped when the app-shell metrics landed. Production serves CSS withmax-age=604800, so a browser can hold a seven-day-old token file under an unchanged URL; the dev server sends no such header. The Facilitator stamp is bumped, but the fallback is what makes the layout independent of it. Pages that stamp withfilemtime()instead of a hand-edited date do not have this problem.
August 2026
v2.9.57 — Every component page in the sidebar was a dead link
- Fixed All 40 component links in the design-system sidebar were dead — and because
ds-header.phpis included on every design-system page, they were dead from every page, not just one. The nav listed each component ascomponents/<name>while the pages actually live atcomponents/demos/<name>; the router bounced the missing path to/, so clicking any component in the sidebar silently returned you to the homepage. The card grid oncomponent-library.phpwas unaffected — it already used thedemos/path, which is why the break was easy to miss. - Improved The nav
slugnow matches the real page path for components, as it already did for every other entry. It does double duty as both the link target and the active-state key, so all 47 demo pages had their$ds_pageupdated in step — verified that each of the 40 still highlights itself in the sidebar. - Fixed The Button demo's example linked to
/workshops, which does not exist; both the live button and its copyable code sample now point at/library/workshops/.
August 2026
v2.9.56 — Toasts that wait for you, and a truly modal modal
- Fixed Toast auto-dismiss now pauses while hovered or focused (WCAG 2.2.1 Timing Adjustable) — the remaining time is kept across pauses and resumes when the pointer and focus both leave. Previously a toast with an action button could vanish mid-read or mid-Tab. Covered by
tests/toast-a11y.spec.js. - Fixed
wsModalOpen()now makes the page behind the dialoginert— the focus trap stopped Tab, but screen-reader virtual cursors could still wander the background. Restoration is stack-safe (each modal restores only what it set) and the toast container stays live so toasts fired from modal actions still announce. Covered intests/modal-focus.spec.js. - Improved
ws_alert()/ws_banner()now document their HTML-slot contract:$messagerenders unescaped by design (callers pass<strong>, links) — user-supplied data must be escaped withhtmlspecialchars()before passing. The call-site audit found every dynamic caller already does.
July 2026
v2.9.55 — Sixty unnamed buttons, and a ratchet that was counting its own convention
- Fixed 60 icon-only buttons had no accessible name — a WCAG 4.1.2 Level A failure across 24 files. Every one now carries an
aria-label. Worst concentrations:includes/footer/modals.php(11),admin/banners.phpandadmin/system/banners.php(6 each),facilitator/app.php(6). Noteicon()emits a bare<svg>with no<title>, so<?= icon('close') ?>inside a button leaves it unnamed. - Fixed
check-page.phpcountedvar(--token, #hex)fallbacks as hardcoded hex — the same defectds-lint.mjshad fixed in 8dcdacfe, so the page ratchet was firing on the codebase's own defensive convention. The rule now strips fallbacks before counting; a chunk carrying both a real hex and a fallback still reports the real one. Baseline re-derived: 5,601 → 4,723 violations across 320 → 311 files, with zero drift absorbed. - Fixed The one genuinely alt-less image on the product surface — a JS-built reviewer avatar in
includes/ratings-ui.php— now carriesalt=""(decorative; the reviewer's name renders beside it). The audit's "94 missing alt" figure was a regex artifact:<img\b[^>]*>truncates at the>inside?>, hiding anyaltthat follows a PHP attribute. Real numbers: 1,133<img>, 40 withoutalt, 39 of them in ad creative, ebook HTML, or regex literals that merely contain the text<img. - Improved All eight
TODO: no token matchmarkers cleared fromcomponents.csswith no visual change:#7F1D1D→--toast-bg,#15803D→--color-green-700,#B45309→--color-warning-darker, plus three comment-only deletions on lines already using tokens. Two were not stale but real token gaps (amber-50#FFFBEBvs the orange-50--color-warning-surface; the checkbox-card purple) and are now documented as such.
July 2026
v2.9.54 — The shell arc's missing record, and the review fixes
- New Retroactive record of the app-shell arc (this entry should have shipped with the branch, per the §15 rule the code review caught):
ws_app_bar— the shared document bar underws_headeracross Planner, Facilitator, and Synthesize; tokens--app-bar-height,--app-rail-width(320px unified rails),--header-bg,--paper-grid-image;--bg-pagemoved to#FAFAF9with the drafting grid; ~575 lines of shell CSS incomponents.css. - New DESIGN.md §12.8 written.
synthesize.cssand thews_app_bardocblock cited a §12.8 that didn't exist. It now does: one anatomy, component-owns-the-shell ownership rule, title modes, the single save-state pill, action variants, and the slots-are-for-legacy-JS rule. - New
tests/header-shell.spec.js— the header migration's missing coverage: seven migrated pages assert exactly onews_header, zero references to the retired header stack, zero stylesheet 404s; plus shell-token resolution (--app-rail-width= 320px). - Improved The identical seven-property interactive transition, repeated verbatim eight times in
components.css, is now one definition:--ws-transition-interactive. Byte-identical expansion, so no visual change is possible. - Fixed Change-narration comments rewritten as constraints per CLAUDE.md §0 in
academy/training/index.php,podcasts/index.php,includes/components/header.php(×2), andplatform-tokens.css. - Fixed Stale references to the deleted
global-header.phpremoved from the Navigation demo's Files table and corrected in CLAUDE.md §11 Key Files. - Fixed Synthesize docs squared with reality: EVAL-BENCHMARK is now the true contract (36 fixtures, new category P row for P-01…P-04 photo/CV traps, High-severity threshold re-based); ROADMAP no longer claims the v2-cv baseline is "pending" (it's committed, from the 16 Jul pre-fix run) and flags that the post-C-01-fix eval run is not yet committed to
eval/reports/. - Improved
synthesize.js: both synthesis success paths now enter results through oneenterResults()— the duplicated setState + draft-autosave tail can no longer drift apart.
July 2026
v2.9.53 — The defects the doc pass turned up
- Fixed Modal focus never entered the dialog.
wsTrapFocus()calledfocus()in the same tick as the.is-openclass, but.ws-modal-overlayanimates in fromvisibility: hiddenand an unrendered element can't take focus — so the call was a silent no-op and focus stayed on the trigger, outside the trap it had just installed. NewwsFocusWhenVisible()waits for the transition. One frame isn't enough: the overlay reportsvisibleat frame 2 while still atopacity: 0.01and only becomes focusable once painted. Confirmed in headless Chromium, not the preview pane — the pane is itselfvisibility:hiddenand can't measure this. - Fixed
wsModalClose()now restores focus to the element that opened the modal, guarded onisConnected. Previously focus dropped to<body>. - Fixed
closable: falsenow blocks Escape — the template emitsdata-closable="false"and the document handler honours it. Backdrop dismissal stays governed bybackdrop. - Fixed
wsFilterBarRemove()is now defined incomponents.js. The filter-bar template had called it since it shipped; nothing implemented it, so every chip remove threw a ReferenceError. Drops one query param, resetspage, preserves the rest of the query string and the hash. - Fixed
ws_progressomitsaria-valuenowwhenindeterminate, per the ARIA spec — it was announcing a definite percentage for work of unknown length. Both progress animations now honourprefers-reduced-motion, matchingws-skeleton. - Fixed
aria-atomicmoved from the toast container onto each toast. On the container it told assistive tech to re-read the whole stack whenever one arrived. - Fixed
ws_prompt_selectrenders a real<label for>instead of a<span>— the select had no accessible name, and clicking the prompt did nothing. - Fixed
ws_chip_input's add button falls back to anaria-labelwhenaddTextis omitted, so the icon-only form isn't an unnamed button. - New
tests/modal-focus.spec.jscovers all three modal fixes. Notestrategist/tests/sections.spec.jsis separately broken — it requiresjs/modules/sections.js, retired in v2.9.51 — so a barenpx playwright testfails before reaching anything else. - Improved Corrected a claim in the previous entry's doc pass:
ws_chip_input's label is properly bound viafor. The unbound-label finding stands for Form Row, Prompt Select, and the two card groups.
July 2026
v2.9.52 — Every component page, one structure
- Improved All 47 pages in
components/demos/rebuilt to the section order intemplates/README.md: When to Use · Variants · States · Real-World Usage · Options · Accessibility · Tokens · CSS Classes · Files. Adoption was 0/47 before this pass — the template existed, nothing followed it. - New Accessibility and Tokens sections on every component. Accessibility was present on 5 pages; Tokens on none. Each is written from the template and CSS source, not assumed — including the gaps.
- Fixed Input documented
icon,iconPosition,state, andhelper— none are read byinput.php. Its select example also passed flatvalue => labelpairs, throwing Cannot access offset of type string on string;ws_input(type: 'select')wants a list of arrays, unlikews_select(). - Fixed Navigation described a
data-propertyattribute and.property-switcher-*/.ps-*classes that exist nowhere inheader.php. Rewritten againstws_header(?string $section, array $opts), the 17 section keys, and the real.wsh-prefix. - Fixed Accordion and Tabs documented arrow-key, Home/End, and roving-tabindex navigation; both JS inits bind
clickonly. Modal claimed focus return to trigger and an inert background; neither is implemented. All now recorded as known gaps. - Fixed Filter Bar chips call
wsFilterBarRemove(), which is defined nowhere in the codebase — every remove button throws a ReferenceError. Documented, with a working implementation in the usage example. - Fixed Unbound labels recorded on Form Row, Prompt Select, Chip Input, Radio Card Group, and Checkbox Card Group — each renders a visible label with no
for,fieldset/legend, oraria-labelledby, leaving the control unnamed. - Fixed Retired phase hexes removed from Level Up Grid (
#0284C7,#7C4DFF,#10B981), Navigation (plus#F87171,#BE123C), Button'svar()fallbacks, and Planner Inspector Rail's theming advice. Every phase token resolves to#E54D4D. - Fixed Emitted-but-unstyled classes flagged where found:
.ws-badge__text,.ws-avatar--initials,.ws-tabs__label, and Input's orphaned.ws-input-wrap__icon--*rules. - New Undocumented options surfaced:
showHeaderandscoreBadge.varianton Energy Arc Card,group_by_phaseandphase_copyon Level Up Grid,metaon Planner Card, and the phase variants on Banner. Energy Arc Card'sthemedefault corrected fromdarktolight.
July 2026
v2.9.51 — Strategist madlib rebuild
- New
strategist.cssrewritten for the madlib screen: hero on the Display Text Pattern and a display-scale six-dropdown sentence own the first viewport, with three tool cards + full-width panels and a soft very-light-red exit bar below the fold. Platform tokens throughout; the page sits on the standard paper-and-grid background. - New
.st-blankis a documented strategist-local control (likest-tabswas): an inline sentence blank thatws_input/ws_selectcan't express — they render labeled block fields. Baselined incheck-page; if a second app ever needs inline blanks, promote it to aws_*component. - Fixed Retired the three-phase shell CSS (docbar, phase tabs, steplist, wizard nav, readiness meter, rail). The
st-brief/st-docdocument blocks stay —brief-view.phprenders with them.
July 2026
v2.9.50 — The icon-ligature sweep, finished (and a guard so it stays finished)
- Fixed The remaining ligature-named controls in Synthesize: theme disclosure chevrons and theme overflow menus, which announced as
chevron_rightandmore_vert— 22 of the 58 controls on the results screen, all sharing two names. NewdisclosureAttrs(what, open)emits the label andaria-expandedtogether, so a toggle can't ship a name without its state. - Improved A count rendered in its own span concatenates into the button's name —
<span>Themes</span><span>11</span>is announced "Themes11". Where a label and a badge sit in one control, give the control an explicit name with the separator restored. - New Regression guard in
synthesize.spec.js: it sweeps every visible control on the results screen and fails if any accessible name is a bare snake_case token. Catches new icon-only controls without needing a test per button. - Improved Sweep result across the input, review and results screens: 162 controls, none named by an icon. The rule this leaves behind — when a control's only content is
icon(), it needs an explicit name; the ligature is not one.
July 2026
v2.9.49 — Icon ligatures are not labels; disabled controls owe a reason
- Fixed
icon()emits the Material Symbols ligature as real text, so any button whose only content isicon('close')announces as "close" — the wrong verb for a destructive control, and identical on every row of a list. NewremoveLabel(kind, text)insynthesize.jsgives each of the seven per-row remove buttons a name carrying its own row's text, trimmed to 60 chars because these are read aloud in sequence. - Improved Deliberately not fixed by marking every icon
aria-hidden: plenty of controls here carry no other text, so hiding the ligature would leave them nameless. Label the control, don't silence the icon. - Improved A disabled control owes the reason it is disabled, in the place people look. Synthesize's Start button has two blocking conditions — too much material already explained itself in an alert; an empty tray explained nothing, and the adjacent hint talked about saving drafts, so the greyed button read as broken. The hint is now conditional and wired via
aria-describedby. - Improved Placeholder names should look unfinished. "Workshop synthesis" reads like a real title, so a library of them was unsortable; the fallback now borrows the first source's name.
July 2026
v2.9.48 — Synthesize overlays become real dialogs
- Improved
overlay()insynthesize.jsnow emitsrole="dialog"+aria-modal="true"on.synth-modal— the panel, not the backdrop, since the backdrop is the click-to-dismiss surface. One helper, so every Synthesize overlay gained it at once. - New
wireOverlayA11y()names each dialog from its own heading viaaria-labelledbyrather than a hand-maintained name→label map, and pulls focus in. Two traps worth knowing: several overlays lead with a hidden file input, which silently refuses focus — pick the first visible field and verify focus landed, or the dialog keeps focus on<body>. And becauserender()replacesroot.innerHTMLwholesale, focus must be restored on every render behind an open overlay, not just on open. - Improved Escape closes the top overlay, with no exemption for focused fields. The usual "let the input's own Escape win" rule backfires here: focus lands in a field on open, so exempting fields meant Escape never closed the dialogs that have one — which is most of them.
July 2026
v2.9.47 — Planner: name the keyboard model, and the recovery route per audience
- New
.toast-undo-hintincss/planner/components/_modals.css— the keyboard shortcut shown beside the toast's Undo button, quieter than the button so it reads as a hint rather than a competing action. Paired with an.sr-onlyspoken form, since⌘Zdoes not read well aloud. - Improved A toast action button is a mouse affordance. It mounts at the end of the document, focus is elsewhere, and it auto-dismisses — so a keyboard user cannot realistically reach it. When a toast offers recovery, state the shortcut too.
- Improved
#agendaItemsnow carriesrole="list"+aria-label, and each item anaria-describedbynaming the keyboard model. The description is deliberately one sentence: it is read on every focus, so the fuller wording lives in the visible Instructions panel instead.
July 2026
v2.9.46 — ws_header: a nav href's fragment is part of its identity
- Fixed
ws_header()compared nav items on path alone, viaparse_url(..., PHP_URL_PATH), which discards the fragment. Nine sections build anchor navs from absolute paths —/synthesize/,/synthesize/#how,/synthesize/#app— so all three collapsed to/synthesizeand anactiveHrefof/synthesize/matched every one, lighting the whole nav. Matching now uses path + fragment. - Improved Fragment items are excluded from path matching outright: the server never receives the anchor, so it cannot know which one the reader is on. Dropping them also leaves the fragment-less sibling as the unique match, where the group previously cancelled out as ambiguous and nothing highlighted.
- Improved Bare
#anchorhrefs still resolve to""and never match — that is why sections written that way (/sidekick/) were already correct, and why the bug looked inconsistent across the platform. - Fixed
activeHrefmust name the nav item's href, not the current page's URL. Both Facilitator session pages passed/facilitator/app.php, which appears nowhere in that nav, so they highlighted nothing despite a comment saying "highlight Run Session here".
July 2026
v2.9.45 — ws_header: aria-current, and 'active' means CTA not location
- New
ws_header()emitsaria-current="page"on the nav item matching the current path or the page'sactiveHref. The red highlight was visual-only, so "you are here" never reached assistive tech. - Fixed Only one item can ever carry it. Anchor navs whose hrefs all resolve to a single path (
/synthesize/,/synthesize/#how) light several items at once; the highlight tolerates that, the accessibility tree does not. - Improved The registry's
'active' => trueflag paints a section's call to action red — it is not a location marker, andaria-currentdeliberately does not follow it. Setting it on a plain destination lights two items with two different meanings, which is what happened ontemplates.php. UseactiveHreffor location. - Improved The
plannersection's default nav is now Planner / Templates / Saved Agendas, matching the four planner surfaces. It previously said "Launchpad", a name retired when that page became a 301 tobrief/.
July 2026
v2.9.44 — Planner: named the Energy score, retired the mixed badge row
- New
.energy-arc__score-labelincss/planner/components/_canvas.css— a quiet caption naming the score pill, which otherwise rendered as a bare number discoverable only on hover. Carriesaria-hidden, since the button's ownaria-labelalready names the score; without it screen readers hear "Energy" twice. - Improved
.lp2-option__badgenow appears once per row, on the featured card only. Three badges in the same slot encoded three unrelated axes — a recommendation, a method, and a price — so the row read as one comparison it wasn't. One slot, one meaning. - Improved Quoted library counts must come from
workshopr_count_label()(includes/library-counts.php), never a literal. Four planner pages had drifted to 300+/380+ against a 440-activity library; the helper's docblock already claimed to be the single source of truth, but the call sites had not adopted it.
July 2026
v2.9.43 — Planner: undo control in the toast, named dialog close buttons
- New
.toast-undo-btnincss/planner/components/_modals.css— the Undo control inside the agenda delete toast. Inherits the toast's white-on-blue (~8.6:1), and usesdisplay: inline-flexsomin-height/min-width: 24pxactually applies; on a plaininlinebutton the min-height is ignored and the target lands under the WCAG 2.5.8 floor. - Improved
modal-planner.phpnow emitsaria-label="Close {title}"andtype="button"on.red-header-modal-close. One template, so all 15 planner dialogs gained a named close button; they announced as bare "button" before. The overlay already carriedrole="dialog"/aria-modal. - Improved Client-side field errors should reuse
ws_input()'s own error markup —.ws-field--error,.ws-input--error,aria-invalid, and<p class="ws-field__error" role="alert">.setFieldError()inplanner-home.jsmirrors it exactly rather than inventing a second error style.
July 2026
v2.9.42 — Synthesize modals: entry animation belongs to opening
- Fixed
.synth-overlay'ssynth-fadeand.synth-modal'ssynth-poprestarted on every state change behind an open modal, because Synthesize re-renders by replacing the whole ofroot.innerHTML— the modal visibly flashed and reopened. New.synth-overlay--settledsetsanimation: noneon an overlay that was already on screen when the render started (snapshotOverlays()insynthesize.js), so the animation only plays on the render that opened it. - Improved Worth copying wherever a string-rendered surface re-mounts animated chrome.
.synth .menuand the AI log (which already carries a comment about this) have the same exposure.
July 2026
v2.9.41 — Synthesize frame: full-height rail, flush surface
- Improved
.sy-raildrops its border and radius and stretches to the full height of the frame (.sy-appmoves fromalign-items: starttostretch). It is no longersticky: a stretched element has nothing to stick to, so keeping the rail fixed under a long board would mean moving it outside.synth-stage. - Fixed Full height needed a definite-height ancestor chain. A percentage
min-heightresolves against nothing when every ancestor isheight: auto, so.synth-stage .synthis now a flex column and.wrap--app/.sy-apppass height down withflex: 1. - Improved
.wrap--apploses its top padding and trims the inherited 110px bottom pad to--space-6;.sy-maingoes topadding: var(--space-6) 0so content runs the full width of the surface. - Fixed The zero top padding has to be restated inside
@media (max-width: 640px), where.synth .wrap'spaddingshorthand resets it at equal specificity. - Improved
.prio-traymoved below the matrix. Noted as a watch item rather than a clean win: the 2×2 is 543px tall, so a tray of several rows can fall below the fold on a 900px viewport.
July 2026
v2.9.40 — Synthesize adopts the shared components
- Improved Synthesize's private re-implementations of button, tab, badge, pill, alert, empty state and skeleton are deleted; the app emits the platform class names instead.
components.csswas already on every page viaincludes/header.php— the components were present all along, just unused. - New
wsAlert,wsEmpty,wsSkel,wsBadge,wsPillinsynthesize.js, beside the existingicon(). They mirror the markupincludes/components/*.phpproduces, because the PHP helpers can't be called from a client-rendered body. Keep them in step if those templates change. - Fixed The
.tabcollision is gone rather than worked around. Synthesize needed an explicit reset of the global pill rule's radius, shadow, shimmer::beforeand hover lift — same specificity, decided only by source order. Adopting.ws-tabs--underlineremoved the reason for the reset. - Improved Two loading messages were wearing
.empty. They are transient status, not empty states, and now use.load-hint. - Improved What stayed local is now only what carries data-driven colour:
.pri-badgeand.cat-pillset a background inline from app state, so the rules that remain do nothing but keep the text legible on top of it. - Fixed
.ws-skeletonsets nodisplay, so it must be emitted as a<div>— the<span>the old skeleton used would collapse to nothing.
July 2026
v2.9.39 — Synthesize app guide + component gallery
- New
design-system/app-synthesize.php— the fourteenth App Guide, and the first to double as a component gallery. Synthesize renders every screen from JavaScript template strings, so it has nows_*helpers to demo; the page renders its class names directly againstsynthesize/synthesize.css, which means editing that stylesheet moves the specimens with the app. - New Forty specimens across nine groups, each shown in its real states — empty, loading, done, muted, error, and the degenerate cases (no notes, no owner, an overlong source name). Specimens are wrapped in
.synth, since the whole stylesheet is scoped under it. - New A “Swap Candidates” section pairing each duplicated element with its platform equivalent:
.btn↔.ws-btn,.badge/.pill↔.ws-badge/.ws-pill,.empty↔.ws-empty-state,.guardrail↔.ws-alert--warning,.sy-skel↔.ws-skeleton. - Improved
ds-header.phpgains a Synthesize entry under App Guides. Strategist is still missing from that nav — noted, not fixed here. - Fixed Recorded the one genuine platform gap the gallery surfaced: there is no shared segmented control, and Synthesize, Planner, and Facilitator each ship their own
.seg. Flagged as the strongest candidate for a realws_segmented.
July 2026
v2.9.38 — Synthesize app frame: rail, working surface, AI panel
- New
.sy-appreplaces.sy-workas the shell every Synthesize working screen wears:var(--app-rail-width)rail +minmax(0, 1fr)surface, widening to a third340pxcolumn for the AI panel above1440px..sy-work__mainis now.sy-mainand keeps the drafting grid onvar(--bg-page). - New
.sy-ai— a panel that is a grid column on wide screens, a fixed right-edge sheet below1440px, and a bottom sheet below1080px. The sheet's top comes from--synth-stage-top, set once per open, so a fixed element can sit under chrome it can't measure. - New
.sy-tools/.sy-tool— a 38px icon rail with a hover tooltip, replacing the spelled-out.sy-rail--addcolumn. - Fixed
.synth .tabnow resets the global.tabrule's pill radius, shadow, shimmer::before, and hover lift. Same specificity, so only source order was keeping them apart — Synthesize's tabs are an underline row and now say so explicitly. - Improved Retired
.uni-drop,.act-table, and.dec-rowin favour of.sy-drop,.act-card, and.dec-card— the card forms survive a narrow column, the table did not.
July 2026
v2.9.37 — Synthesize work surface on the page paper
- Improved
.sy-work__mainkeeps the drafting grid but takesvar(--bg-page)(#FAFAF9) instead of the warmervar(--bg-paper), so the centre column and the page behind it read as one sheet rather than two paper stocks. - New
.sy-rail--addis now a panel onvar(--bg-paper-dark)(#F0EBE3) — the same wash the global header wears. - Improved Both rails (
.sy-rail) picked up the centre column's panel geometry —1px solid var(--border),--rCardradius,--space-5padding — so the work screen reads as three panels of one family. Fill is the only thing that distinguishes them: grid paper in the middle, the darker wash on the controls, nothing behind the sources list.
July 2026
v2.9.36 — Planner progress-header buttons on the red token
- Improved
.progress-header-btn(the History / Clean up / AI / Share / Export / Schedule / Delete cluster on the planner's agenda bar) takes its background fromvar(--planner-very-light)instead of a hardcodedrgba(229, 77, 77, 0.08). Same red family, now a solid#FEE2E2that matches the soft fills used elsewhere on the platform. - Improved Shared
?v=stamp on every planner@importbumped to20260726a, per the rule inplanner-new.css— the imports are cached for 7 days and the entry file's own stamp does not invalidate them.
July 2026
v2.9.35 — Global header background token
- New
--header-bg: #F0EBE3inplatform-tokens.css(mirrored asbackgrounds.headerin the JSON). The global header now sits a step deeper than the#FEF7F1page cream, so the chrome reads as its own band instead of bleeding into content. Same value the apps already carry as--color-paper-dark. - Improved
ws_header()takes the colour from the token in both places it set the old hex — the.wsh-headerrule and the inlinestyleattribute the component emits (the inline one wins, so they have to move together). Thebgoption still overrides per call.
July 2026
v2.9.34 — ws_skeleton workshop-card variant + reduced-motion guard
- New
ws_skeleton(['variant' => 'workshop', 'count' => 3])— a placeholder that mirrors the saved-workshop card (name, meta row, lifecycle strip, footer action) at the same 12px radius / 18px padding / 12px stack gap the real card uses.countemits sibling cards so it drops straight into an app's own grid. Adopted onplanner/planner-home.php,facilitator/facilitator-home.php, andsynthesize/synthesize-home.php, replacing three "Loading your workshops…" text lines. New.ws-skeleton-gridhelper for pages whose skeleton isn't already inside an app grid. - Improved The skeleton shimmer now stops under
prefers-reduced-motion: reduce(the placeholder stays visible, holding its "content is coming" status) — an infinite loop is decorative motion per DESIGN.md §7.
July 2026
v2.9.33 — ws_app_bar, one document bar for every tool shell
- New
ws_app_bar()— the bar every app shell wears underws_header. One anatomy: app label or document title, contextual controls, save state, action cluster. The component owns the shell (60px min-height, red gradient, Fraunces title, 36px action geometry, tooltips); apps pass only their own markup through theleadSlot,center,trailLead, andtrailSlotslots. Template inincludes/components/app-bar.php, CSS inincludes/components/components.css, registered inworkshopr-design-system.json. - Improved Adopted by all three tool shells, replacing
.planner-topbar,.synth-topbar, and.fac-app-bar. Every id and class the apps' JS binds to was preserved; the superseded layout, title, action, and tooltip rules were deleted from the three app stylesheets rather than left to fight the component on cascade order. - Fixed Removed the dead
$header_variant = 'app'/$header_app_title/$header_app_actionsblock fromfacilitator/app.php— an unfinished app-mode header nothing ever read.
July 2026
v2.9.32 — Workshop Record architecture note linked from Decisions
- New The Design Decisions page gained a Workshop Record section linking the architecture note (
docs/workshop-record.md) and the self-contained, theme-aware diagram (docs/workshop-record-diagram.html). Explains the plan spine, the derived Designed → Delivered → Synthesized lifecycle thatws_lifecyclerenders, and the federated read model — so the component's stages have a documented source of truth.
July 2026
v2.9.31 — ws_lifecycle workshop journey strip
- New
ws_lifecycle()component — a three-step workshop journey strip (Designed → Delivered → Synthesized). Options:status(designed|delivered|synthesized),variant(compactfor card badges,fullwith an optional next-stepcta). Template inincludes/components/lifecycle.php, CSS inincludes/components/components.css, demo atdesign-system/components/demos/lifecycle.php, registered inworkshopr-design-system.json. Red Unification compliant: completed steps use the very-light red fill with full-strength border, pending steps are neutral outline — state is expressed by fill/weight, never by additional hues. - New Adopted on MyWorkshopr workshop cards and the Planner's saved-workshops list, hydrated client-side from
GET /api/workshop-record.php?list=1(lifecycle derived server-side; strips are absent, not broken, when the call fails).
July 2026
v2.9.30 — Facilitator start-agenda adopts the Planner card
- New
.fac-skeletonshimmer primitive (transform-only animation, reduced-motion-safe) plus per-part skeleton layouts —.picker-skeleton,.fac-sidebar-skeleton,.start-agenda-skeleton__*,.start-stat-skeleton,.fac-script-skeleton,.fac-panel-skeleton, and generic.fac-skel-linewidth helpers. Emitted by the sharedFacilitatorSkeletonsJS builders so the live app and the/facilitator/loading-statesgallery render identical markup. - Fixed
.fac-live__bodygainedgrid-template-rows: 1fr+min-height: 0(andmin-height: 0on the three columns) so the left/right columns stretch full height instead of collapsing to a content-sized row. - Improved
facilitator/css/facilitator.css— the Session Start review list (#start-agenda) now reuses the Planner's.agenda-itemcard anatomy (type-coloured.item-iconsquare,.item-time,.item-title,.item-duration-badge) plus the sharedws_activity_badge. Class names mirrorcss/planner/components/_cards.cssbut the rules are scoped under#start-agendaso they never leak into the rest of the facilitator. Read-only: none of the Planner card's editing chrome (drag/delete/swap/edit) is carried over. - New Facilitator type tint tokens
--fac-type-{exercise,icebreaker,break}-very-light(#EFF6FF/#ECFDF5/--gray-50, mirroring the Planner's--planner-*-verylight). Applied as per-type card shading on#start-agenda .agenda-item[data-type]— very-light tinted background + full-strength type-colour border, matching the Planner canvas cards. - Fixed
.start-cta__btn--compactno longer shrinks the button — the agenda-header "Start Session" now matches the hero CTA's size (the variant only keeps its header placement, not a smaller footprint)..start-ctais nowtext-align: rightso the hero button shares the agenda's right edge with the header button. - Improved
--fac-hero-gradientretinted from maroon (#3D0C0C→#6B1717) to an ink gradient (var(--color-ink)→var(--color-ink-secondary),#2C2416→#5C5242) for the start screen's dark hero banner;--fac-hero-dark(live dashboard timer bar) likewise retinted from#3D0C0Ctovar(--color-ink)..fac-nav(live dashboard bottom bar) background changed from--bg-surfaceto--fac-ink-faint, with its tool buttons / note button / position / shortcuts text set to--fac-inkfor contrast. Added the missing--fac-ink-fainttoken (var(--color-ink-faint),#A8A29E) to the facilitator ink scale..fac-nav__tool-btn(Polls/Reactions) and.fac-nav__note-btn(Quick Note) now inherit the.fac-nav__btn(Previous) treatment — white--bg-surfacepill, solid--gray-300border,--radius-lg,--text-meta— so all nav buttons match. Free-floating bar text (.fac-nav__shortcuts,.fac-nav__position) set to white; the pill buttons keep ink labels since their backgrounds are white. Added.fac-nav__btn:not(.fac-nav__btn--primary):hover— the Previous button now hovers to the same red-accent state (--fac-purpleborder/text,--fac-purple-very-lightfill) as the other nav pills; the primary Next button keeps its own red-fill hover. - Improved
.fac-main(live dashboard script panel) now paints the shared graph-paper grid (ink @ 6%, 24px cells over--bg-page) fromcss/base/_backgrounds.css, instead of a flat--bg-pagefill.
July 2026
v2.9.29 — Agenda Health Check app styles
- New
agenda-check/agenda-check.css— a deliberately thin app-shell sheet (.ac-*) covering only the topbar/shell layout, input screen, dropzone, analyzing spinner, and sign-in gate wrapper. The report itself reuses the already-loadedcomponents.css,_agenda-generative.css, and the.energy-arc-*card fromprep.css— no new CSS for the grade card, dimension bars, recommendation rows, or badges. - New
.ac-grade__letter— an additive Fraunces letter-grade flourish layered on the reused energy-arc card, with a one-time fade/settle entrance (--duration-reveal-lg+--ease-reveal) gated behindprefers-reduced-motion.
July 2026
v2.9.28 — Icon set: calendar_view_day + right_panel_open
- New Added
calendar_view_day.svgandright_panel_open.svgtoimages/icons/(official Material Symbols outlines,fill="currentColor") — theicon()helper renders an empty span for missing files, which had left the Strategist doc-bar's rail toggle blank. - Improved Strategist's "Open in Planner" CTA is now a
ws_icon_button(primary, md) with a CSSdata-tooltip+aria-labelinstead of a labeledws_button.
July 2026
v2.9.27 — Strategist joins the global header
- New
ws_headergained astrategistsection (Plan phase,strategyicon, "Strategist" tag) with Strategist / Plan Agenda nav, and a matching app-switcher entry under Plan. - Improved The Strategist page now renders
ws_header('strategist')above its locked shell; the shell height accounts for the header (calc(100dvh - 65px)) so the window still never scrolls, and the header joins the app chrome hidden in print.
July 2026
v2.9.26 — Strategist app-shell rework
- New The Strategist shell is now locked to
100dvhwith a grid-paper canvas: a doc-bar (editable workshop name, readiness pill + breakdown popover, save chip, Planner CTA) pins above an always-visible phase journey, with three independently scrolling bands beneath — no window scroll at any width. - New Every phase (Buy-In, Scope, Details) now drives a master–detail step list from
StSectionsbeside a single-section editor, with tick marks for complete/touched sections replacing the old inline stepper. - New The brief rail became a tabbed collapsible panel (
.st-rail__tabs— Brief / Documents) that switches to a fixed off-canvas drawer below 1100px, with roving-tabindex keyboard support (arrows/Home/End) on the tab pair. - New First-run guidance is now an in-shell start card rather than an overlay.
- Improved Below 760px the step list becomes a horizontal, scrollable chip row instead of disappearing off-screen.
- Fixed Swept the dead
.st-wizard*rule family (stepper dots, footer, print hide-list entries) left over from the pre-rework layout, plus the inert.st-wizard__next--lastclass toggle instrategist.js— neither had any surviving markup or styling effect.
July 2026
v2.9.25 — Synthesize on the platform motion system
- Improved Synthesize micro-motion now pulls from the shared tokens: buttons, icon buttons, action cards, add buttons, and dropzones transition with
--duration-fast/base+--ease-out; modals enter with--duration-enter+--ease-spring; menus pop with--duration-fast/base. Reduced-motion zeroing comes free via the token overrides. - New The Synthesize landing hero uses the page-reveal regime (
--ease-reveal,--duration-reveal(-lg)): title → description → CTAs cascade on load, transform+opacity only, with ananimation-durationkill added to the landing's reduced-motion block. - New
.uni-dropvariant of the Synthesize dropzone — a slim horizontal drop-anything target on the input screen (token-spaced, tscale-aware).
July 2026
v2.9.24 — Synthesize landing + app fully tokenized; Red Unification comments corrected
- Fixed The Synthesize landing (
.sy-land) palette now aliases platform tokens (--phase-learnfamily,--color-ink(-secondary),--bg-page/surface) instead of duplicating hex values; stale "red is a deliberate exception, like Intervention" comments insynthesize/index.phpandsynthesize.cssrewritten to reflect Red Unification. - Fixed Remaining off-scale values in
synthesize.cssmoved onto tokens: shadow/radius vars alias--shadow-sm/--radius-sm/xl/2xl, off-scale 7/28/36px spacing snapped to the 4px scale, the roadmap goal-card selection ring uses--accentVL, and the roadmap detail modal's red side-tab border became a quiet bordered wash. - Fixed Kanban card titles reference
var(--font-heading)instead of a hardcoded font-family. All three Synthesize files now pass the enforcer with zero violations.
July 2026
v2.9.23 — Footer consolidation: ws_footer(), tokenized canon, forks retired
- New
ws_footer()wraps the global footer composite (Level Up grid · Letter CTA · brand bar) and is registered inworkshopr-design-system.json; the composite gained a render-once guard so any include combination yields one footer. Documented in DESIGN.md §12.7 with the footer-less app-shell exception (Planner, Strategist) and the BEM naming rule. - Fixed The canonical footer's 18 enforcer violations: hardcoded hexes replaced with
--bg-paper,--phase-learn(-light),--bg-muted,--color-ink(-muted),--text-inverse,--bg-surface, and--mainsite-gradient; the raw Subscribe button is nowws_button(). - Fixed Footer legal line contrast:
rgba(255,255,255,.4)→.55(3.67:1 → ≈5.8:1, AA pass) in both the composite and the legacy stylesheet. - Improved
includes/footer.phpnow renders the composite viaws_footer(), putting all legacy-footer pages on-canon; the survey fossil footers (3 files) and the Facilitator Bootcamp footer fork were retired.
July 2026
v2.9.22 — Critique fixes: navy aliases retired, toast side-stripes removed, mobile nav
- Fixed Red Unification: the legacy
--primary-navyand--uh-color-navyaliases now resolve to warm ink (--color-ink), retiring cold navy headings platform-wide; library category chips render brand red instead of raw DB Material hexes; the header's per-phase active-link colors and the purple BETA pill collapsed to the red family. - Fixed Toast variants and the facilitator callout dropped the banned side-stripe accent for full light borders on semantic surface tokens.
- New Both global headers (
gn-shell,wsh-header) gained a mobile menu sheet (hamburger toggle, scroll lock, Escape/scrim dismiss, reduced-motion aware) replacing the nav that wasdisplay:nonebelow their breakpoints. - New
includes/library-counts.php: canonical library counts with floors — the single source for every "N+ exercises" claim.
July 2026
v2.9.21 — Strategist app CSS brought fully on-system
- Fixed
strategist/css/strategist.cssreferenced text-size tokens that don't exist (--text-sm,--text-lg,--text-2xl…), so its font sizing silently inherited. Rebuilt on the real scale (--text-h2/h3,--text-ui/small/meta/caption/micro) during the document-centric redesign. - Fixed Strategist focus styles used red outlines; now the platform
--focus-ring(sky-blue), the sole sanctioned non-red accent. - Improved Strategist status/verdict surfaces now use the semantic surface tokens (
--color-warning-surface,--color-success-surface) with text labels, per the color-has-meaning rule.
July 2026
v2.9.20 — Motion cleanup: retire the bounce curve, add press feedback, scope transitions
- Fixed Purged the banned overshoot curve
cubic-bezier(0.34, 1.56, 0.64, 1)(removed July 2026) from circulation: neutralized--transition-bounceincss/tokens/_variables.css, corrected per-app--ease-springoverrides (Tips, Podcasts, Academy, Tips v2) that had subverted the sanctioned token, and replaced the raw literals incss/styles.css, both_modals.csssheets,participant.css, and two planner pages withvar(--ease-spring). - Improved
ws_buttonnow has a:activepress scale on the base class (covers all variants); global header icon buttons gained a press scale + transition. - Improved Removed
transition: allfrom the shared component library and the planner card/sidebar/modal sheets (~71 rules), scoping each totransform/opacity/color/shadow so state changes composite on the GPU.
July 2026
v2.9.19 — Fix footer overriding secondary button borders
- Fixed Scoped the newsletter subscribe-button styles in
includes/components/footer.phpto.cta. An unscoped.ws-btn { border: none }was loading aftercomponents.cssand stripping the red border from every.ws-btn--secondaryon any page that renders the footer.
July 2026
v2.9.18 — Red Unification: retire "plan blue"
- Fixed Repointed the legacy
--planner-blue/--planner-blue-dark/--planner-blue-lightaliases inplatform-tokens.cssto the Learn Red family, so surfaces that load only the platform tokens (e.g.planner/index.php) no longer render the retired plan-blue. - Fixed Cleared leftover sky-blue / violet accents in
css/planner/launchpad.css,css/planner/briefing.css, andplanner/email-preview.phpthat had only been half-converted to red.
July 2026
v2.9.17 — iOS Companion app added to the design system
- New Added
app-companion.php, the per-app guide for the Workshopr Companion iOS app (React Native / Expo) — mobile palette with swatches, type scale, an iOS component inventory grouped by primitives / Run Mode / Capture / Cards, and an iOS patterns section covering safe areas, tactile press, Dynamic Type, and the "one home, two escapes" navigation model. - New Added a
companionentry toappsinworkshopr-design-system.json, mirroring the shape of the existing app entries plus aplatformnote flagging it as a standalone native codebase with no CSS token. - New Added
mobile-ios.md, a concise reference mirroring the tone oftheming.md/motion-system.md, pointing toworkshopr-companion/DESIGN.mdandsrc/theme/as the code source of truth.
July 2026
v2.9.16 — Strategist form-field borders
- Improved Scoped a darker resting border on strategist form fields in
strategist/css/strategist.css:.st-app .ws-input/.ws-selectmove from the platform-default--gray-200to--gray-400, with--gray-500on hover. Scoped to.st-appso the sharedws-input/ws-selectcomponents are unchanged platform-wide.
July 2026
v2.9.15 — Strategist Buy-In wizard
- New Added
.st-wizardstepper nav and.st-wizard__footerBack/Next bar tostrategist/css/strategist.css, splitting the Buy-In phase's five.st-sectionblocks (instrategist/includes/phase-buyin.php) into wizard steps via.st-step/data-st-buyin-step. Active step uses--phase-learn-darktext on--phase-learn-very-lightchip fill; a touched-but-inactive step gets a quieter--gray-200chip. Step transitions are a 200ms fade+4px rise (.st-step--entering), disabled underprefers-reduced-motion. Labels hide under 860px, numbers remain. Print stylesheet forces[data-st-buyin-step]visible and hides the wizard chrome, matching the existing hidden-phase-panel print pattern.
July 2026
v2.9.14 — Strategist generated documents section
- New Added
.st-sidebar__section/.st-sidebar__headingcard and.st-docsbutton stack tostrategist/css/strategist.css, hosting the new Documents section (Executive Summary, Facilitator Briefing, Risk Report, Agenda Blueprint) instrategist/includes/sidebar.phpafter the Workshop Brief preview. Generated output reuses the existing.st-script/.st-skeletonpatterns from the Objection Coach; the section is hidden from print via the existing print stylesheet.
July 2026
v2.9.13 — Strategist recommendation banner
- New Added
.st-recogo/caution/not-yet recommendation banner tostrategist/css/strategist.css, hosted at#stRecommendationinstrategist/includes/sidebar.phpdirectly above the readiness meter. Status is conveyed by a text label plus a bordered card (--color-success/--color-warning/--gray-200), never by color alone, per Red Unification.
July 2026
v2.9.12 — Global footer Letter CTA form restyle
- Improved Restyled
.cta__inputinincludes/components/footer.php: solidvar(--bg-surface)fields with ink text andvar(--shadow-xs)replace the translucent white-on-brown inputs, and field focus now uses the platform sky-blue ring (--focus-ring-color) instead of a blush glow, per the focus-ring exception to Red Unification. Added:focus-visiblering to the footer's local.ws-btn. - Fixed The footer now carries a scoped
.cta .sr-onlyfallback so the form's screen-reader labels can't render as visible text on host pages that don't define the class.
July 2026
v2.9.11 — Planner topbar Strategist readiness badge
- New Added
.topbar-strategist/.topbar-strategist-icon/.topbar-strategist-badgerules tocss/planner/components/_topbar.cssfor the planner topbar's Strategist handoff link — a quiet white link with a light-red readiness pill (--phase-learn-very-lightground,--phase-learn-darktext) that appears when the strategist has left a readiness pointer. Tokens only (--space-1,--space-2,--text-micro,--radius-full,--planner-transition); no hardcoded values beyond the topbar's existing white-on-red link convention.
July 2026
v2.9.10 — Strategist standalone shared-brief page styles
- New Added
.st-app--brief-viewand.st-brief--standalonerules tocss/strategist.cssfor the public read-only Workshop Brief page (strategist/brief-view.php): centered 720px column with token-based spacing and an unclamped brief body. Tokens only (--space-6,--space-4); no hardcoded values.
July 2026
v2.9.9 — Tips category tints unified to brand red
- Improved Completed Red Unification (D1) for Tips: the six
--tips-cat-1..6category tints inplatform-tokens.csswere changed from the legacy purple ramp (#4527A0…#B39DDB) to a brand-red ramp (#7F1D1D · #991B1B · #C73E3E · #E54D4D · #F87171 · #FCA5A5), matching the Intervention/DNA red tint ramps and the already-redapp-tips.phpguide. Regeneratedtokens.dtcg.json; drift check passes. - Fixed Tips app: the local
--cat-*tokens incss/tips/styles.css(half-purple, half-red) and the--tips-cat-*fallbacks intips/index.phpare now the full brand-red ramp. - Fixed Migrated the live category colours: the 13 rows in the
tip_categoriesDB table stored 7 distinct purples (the source of the category card/chip colours) — these were remapped to the brand-red ramp so the Tips feed and category pages render red instead of purple.
July 2026
v2.9.8 — Launchpad choice screen heading alignment
- Improved Launchpad heading: upgraded
.launchpad-choice-screen__title(the "How would you like to build?" h1 page title) fromvar(--text-h2, 1.5rem)to standardvar(--text-h1, 32px), matching the premium page typography of other index headings across the platform. - Improved Semantic Card Headings: changed launchpad choice card titles (
.launchpad-choice-card__title, e.g. "Build from scratch" and "Generate with AI") fromdivtags to semantich1tags and zeroed their margin-top in CSS to maintain design consistency and search engine metadata structure.
July 2026
v2.9.7 — Saved Workshop & Schedule modal improvements
- Improved Applied "Ink Muted" (
var(--color-ink-muted, #78716C)) icon styling and neutralvar(--bg-muted, #F5F5F4)backgrounds to saved workshop/agenda cards across both the Saved Workshops page (planner/saved.php) and the My Library section of MyWorkshopr (myworkshopr/myworkshops.php). Added precise CSS filters for inlineimg.iconelements to prevent raw black SVG fallbacks. - Improved Meta Pills color: set all calendar meta-pills text and icon colors in the "Schedule Workshop" slide-out modal to pure white.
- Improved Energy Details panel: reduced the width of the expanded energy details dropdown panel (
.energy-detail) by 25% (from 920px to 690px) to make the overall canvas layout feel more compact and polished.
July 2026
v2.9.6 — Red Unification cleanup (guide pages + stray gradients)
- Fixed
app-tips.phpguide rewritten from the retired Facilitate purple (#7C4DFF) to the Learn Red family — identity swatches, philosophy cards, core palette, the six category tints, tab-strip demo, and Do/Don't now all read red. Notes that four legacy purple--cat-*tints still ship incss/tips/styles.cssand are flagged for migration. - Fixed
app-hybrid.phpguide rewritten from the retired Plan blue (#0284C7) to the Learn Red family; the "Do" now points atvar(--phase-learn), matching the shipped/toolkits/hybrid/page. - Fixed Finished two half-migrated gradients —
collections-tabs.cssand_sidebar.cssno longer blend brand red into leftover indigo (#6366F1); both are now#E54D4D → #C73E3E.
July 2026
v2.9.5 — ws_task_checklist component (activation flow)
- New
ws_task_checklist— progress-tracked task list for activation/setup flows (helper inhelpers.php, templatetask-checklist.php, CSS incomponents.css, demo atcomponents/demos/task-checklist.php). Items carry real-data done states, optional phase eyebrows, links on undone items, a dismiss hook (data-checklist-dismiss), anddata-taskkeys for JS enhancement. Sky-blue focus ring,:activetactile feedback, 375px-safe. First consumer: the MyWorkshopr Getting Started checklist. - New
.topbar-unsaved-pill(planner_topbar.css) — quiet "Not saved" affordance for anonymous builders. - Fixed JSON catalog + generated roster updated (55 components);
check-drift.phppasses.
July 2026
v2.9.4 — Documentation consistency pass, round 2 (canonical docs + motion pair)
- Fixed
/DESIGN.md: frontmatterelevation.coloredre-synced to the all-red shipped shadows; motion frontmatter aligned to shipped easing tokens (deleted--ease-bounceremoved,--ease-reveal/--ease-springnamed correctly); §3 Coach moved to the Reflect row; §7 phantom--shadow-learn→--{app}-shadow; §11.8 and §12.6 pre-D1 color copy corrected; §14.3 "cooler product palette" removed; §10.3 gained the scannability and inclusive-by-default rules recovered from taste.md. - Improved
/motion.mdconverted from generic template to the dated rank-1 Workshopr mandate: two-regime scoping in the header and Standard Values (page reveals here; 150–300ms UI micro-motion permotion-system.md), stray wrapping code fences removed. - Fixed
motion-system.mdpost-shipping prose: reveal ease is a token (--ease-reveal, P1);--ease-bouncemoved out of the live spec table (deleted, P2); the--ws-ease-bouncestray corrected from "defines" to "uses — defined nowhere (dangling)"; the participant reduced-motion spin exception marked as documented-intent-only (not implemented inparticipant.css). - Improved
theming.mdexceptions table records the planner activity-type colors as a sanctioned functional-coding exception;ADOPTION.mdfont matrix corrected (Tips/Intervention/Podcasts ship Fraunces + Inter; Priority 1 closed); style guide toast snippet (--text-caption, 12px) and radius decision rule (8px buttons per DESIGN.md, shipped 10px flagged) reconciled.
July 2026
v2.9.3 — Documentation consistency pass (post-D1 reconciliation)
- Fixed
workshopr-design-system.jsonphase mapping: Coach moved fromfacilitatetoreflect(it had inverted the "Coach = Reflect, never swap" rule); DNA markedstandalone.check-drift.phppasses (54/54 components, 9 apps). - Fixed
workshopr-style-guide.mdreconciled to rank-4: deference banner to/DESIGN.mdadded; stale pre-unification content corrected (per-phase blue/purple/emerald map, Tips/Podcasts/Coach app blocks, colored shadows, app-tinted focus rings → sky-blue ring tokens, z-index scale → shipped tokens, coach legacy mode tokens marked historical). - Fixed
platform-color-system.mdrewritten to post-D1 reality (was presenting blue/purple/emerald phases and a per-app "exceptions" model as live while labeled Status: Current). - Fixed
illustration-guidelines.md: wrong ink hex (#1F2937→#2C2416), per-app color-variant table collapsed to the unified red family, stale Planner-blue prompt example. - Fixed
README.mdarchitecture line and counts (54ws_*helpers, 44 demos incomponents/demos/);ADOPTION.mdinternal cool-gray contradiction resolved (≈25 remain in planner trees, not "all replaced");DOC-TEMPLATE.mdstale "Tips purple" exception example;START-HERE.mdcounts, §15 changelog pointer, and pre-D1 "phase color discipline" wording;theming.mddead motion.md link. - Fixed
/motion.mdillustrative token block aligned to shipped tokens (--duration-reveal800ms,--duration-reveal-lg1200ms,--stagger-reveal; deleted-overshoot curve removed); deprecatedtaste.md/principles.md/forms.mdreduced to pointer stubs.
July 2026
v2.9.2 — Audit B1 participant bridge + lint-blind indigo cleanup
- Fixed Audit B1:
css/facilitator/participant.cssreferenced 22 undefined tokens surviving on hardcoded fallbacks (a future platform token of the same name would silently restyle the page). Added a participant theme bridge (pertheming.md's app-alias pattern) that defines them once — neutrals alias exact-match platform tokens, activity/status ramps are participant-specific. Zero visual change (verified via computed values). - Fixed Three lint-blind indigo gradients (pre-unification "AI button" styling) that
ds-lintmisses because indigo isn't a signature phase hex:briefing.css(#536dfe/#4338ca),_sidebar.cssand_modals.css(#6366F1) — all repointed to the red family. Zero indigo/violet hexes remain in planner + facilitator CSS.
July 2026
v2.9.1 — Red Unification executed (D1) + audit decisions D2/D3 + motion tokens P1–P4
- Improved D1 — the entire platform is red, everywhere the system speaks. Aligned the last holdouts:
platform-tokens.csssection comments and stale/* #7C4DFF */annotations, the JSONphasesblock (was still blue/purple/emerald), and theds-lint.mjsphase map (tips/coach/live now enforce red — lint is 0 violations across 103 files, down from 13 false positives). DESIGN.md §3 and START-HERE.md rewritten: phases are naming-only. - Improved D2 — the sky-blue focus ring is documented as the one intentional non-red accent (keyboard visibility against red UI). Do not "fix".
- Improved D3 — deprecation register added to the tokens header (delete after Aug 2026: podcast platform brands, planner blues, unused gradients/shadows, cool-text tokens, transition shorthands). The semantic component layer +
--animate-*are kept as theming/motion hooks. - Fixed Five tokens defined twice in
:rootdeduped (warm backgrounds + status inks — audit F2); stale--z-dialog/facilitator annotations corrected (F3). - New Motion P1: page-reveal regime tokenized (
--ease-reveal,--duration-reveal/-lg,--stagger-reveal), zeroed under reduced motion. P2:--ease-bouncedeleted (zero consumers;--animate-scalerepointed to--ease-spring). P3:--transition-*deprecated. P4:--ease-springdocumented as non-overshooting. DTCG mirror regenerated (347 tokens); drift + DTCG + ds-lint all green.
July 2026
v2.9.0 — Consolidation pass: token audit, templates, theming + motion specs, pattern library
- New
token-audit-2026-07.md— full token audit (344 tokens, 33% unused, shadow vocabularies in participant/planner CSS, 5 duplicate definitions). Three decisions pending (phase-abstraction color, focus ring, unused-token disposition); DTCG mirror regenerated. - New
templates/— standard documentation templates (component page, foundation doc) based on the card.php exemplar, plus a.ds-statusmaturity badge in the ds-header chrome. - New
theming.md— three-layer theming architecture, the blessed app-alias bridge pattern, and the six-step new-app checklist. D1-dependent sections marked. - New
motion-system.md— reconciles /motion.md with the animation tokens (two regimes), catalogs the shipped named patterns (enter/exit/pop/live-pulse/progress/nudge/reveal), proposes four motion-token changes (P1–P4). - Improved
component-library.phpis now the Pattern Library: 50 components organized into five problem categories with jump links, honest "docs pending" states for the 7 helpers without demo pages, and a documented app-patterns promotion queue. - Improved START-HERE.md links all consolidation artifacts.
July 2026
v2.8.11 — Facilitator wrong-phase purple purge
- Fixed
css/facilitator/facilitator.cssstill had 78 references to the facilitate-purple tokens (var(--phase-facilitate*, #7C4DFF/#6D28D9)and stale purple fallbacks) that bypassed the app's red-resolving--fac-purple*aliases. All now route through--fac-purple*(→--mainsite-*red) per the documented all-red exception inapps.facilitator.colorNote; the poll-bar gradient's raw#A78BFAwas replaced withvar(--fac-purple-dark).ds-lintis now clean for the file.
July 2026
v2.8.10 — Facilitator confirm dialog + panel typography
- Fixed The Facilitator's reusable confirm dialog (
.confirm-card) had no styles at all — it now follows the established modal anatomy (accent icon tile → Fraunces title → muted subtitle →ws_buttonrow), matching the end-session card. - Improved Slide-in panel titles (Polls, Reactions) adopt the modal heading treatment (Fraunces,
--text-h4); poll form validation renders as an inline.poll-form__errornotice (error tokens) instead of a nativealert(); nativeconfirm()calls (session resume, agenda jump) replaced with the styled dialog.
July 2026
v2.8.9 — Audit follow-through: lint exceptions, focus traps, demo pages, facilitator red completion
- Fixed
scripts/ds-lint.mjsphase map now encodes the documented app color exceptions (Planner, DNA, Intervention, Facilitator → red; Tips → purple) instead of flagging shipped red aswrong-phase. Full-repo violations dropped from 156 (mostly false positives) to 83 genuine legacy items. - New Pre-commit hook (
.git/hooks/pre-commit) runsds-lint.mjs --stagedso new CSS drift is blocked at commit time. Note: git hooks are per-clone — re-create after a fresh clone. - New Shared
wsTrapFocus()/wsReleaseFocus()utility incomponents.js;ws_modalrefactored onto it and the Coach panel + Intervention drawer wired up (guarded, no-op where components.js isn't loaded). - New Demo pages for
ws_pill,ws_toggle,ws_cta_section,ws_filter_bar,ws_how_stepsunderdesign-system/components/demos/, registered on the component-library index (whose cards now link todemos/— the old links 404'd). - Improved Facilitator red completed on remaining surfaces:
css/facilitator/facilitator.css,participant.css, andsummary.cssnow resolve accents to--mainsite-*tokens (purple fallbacks removed). Stale "facilitator = purple" comment inplatform-tokens.csscorrected.
July 2026
v2.8.8 — Facilitator app red confirmed + live poll cards
- Improved Corrected
apps.facilitator.colorNoteinworkshopr-design-system.json: the ENTIRE Facilitator app (landing and app.php dashboard) is mainsite/Learn red (#E54D4D) — the facilitate purple in the Facilitator PRD is deprecated for this app.facilitator.csskeeps the legacy--fac-purple*variable names but resolves them to red. Also updated thephaseColors.facilitatenote. - New Poll result card styles (
.fac-poll,.fac-poll__bar, etc.) infacilitator/css/facilitator.cssfor the live Polls panel — token-based spacing/color, GPU-friendly (transform: scaleX) vote bars. - Improved Facilitator Reactions + Polls panels visual pass: counts and percentages adopt the timer bar's mono voice (
'SF Mono', 'Fira Code', tabular numerals); reaction bubbles pulse (scale + dissipating ring,--ease-spring, reduced-motion guarded) when a count rises; poll cards gained LIVE/FINAL chips and leader emphasis — only the leading option renders a fully saturated bar (trailing fills at 30% opacity), with in-place bar updates so votes animate between refreshes.
July 2026
v2.8.7 — Generative agenda renders from ws_* components
- Improved The Planner's AI Workshop Generator now composes its output entirely from design-system components. A new client renderer (
planner/js/modules/agenda-renderer.js) mirrors thews-card,ws-activity-badge,ws-badge(duration),ws-suggestion-card,ws-stat,ws-alert,ws-empty-stateandws-skeletonmarkup instead of bespoke.generator-itemHTML — and drops all inlineonclickin favor of delegated listeners. - New Layout-only stylesheet
css/planner/components/_agenda-generative.css(imported byplanner-new.css) — all spacing/color/radius via platform tokens, type signalled by the activity badge (no colored side-tab), with a reduced-motion-aware on-canvas assembly animation and a GPU-friendly (transform: scaleX) streaming progress bar. - Improved The planner's inline "Swap with AI" alternatives now render as
ws-suggestion-cardviaAgendaRenderer.swapAltCard()— removed the bespoke.ai-swap-alt-cardmarkup and its hardcoded inline styles.
July 2026
v2.8.6 — Self-contained global footer + facilitator landing exception
- Improved The global footer composite (
includes/components/footer.php) now self-loads its fonts (Fraunces / Inter / JetBrains Mono) and emits its semantic tokens (--paper,--ink,--display, …) as zero-specificity:where(:root)defaults. Host pages no longer need to copy-paste the token block; pages that define the tokens on:rootstill win. - Improved Documented in
workshopr-design-system.jsonthat the Facilitator landing page (/facilitator/index.php) ships mainsite/Learn red (#E54D4D) as a deliberate exception, while the Facilitator app dashboard stays Facilitate purple (#7C4DFF).
July 2026
v2.8.5 — Synthesize header section (red)
- New Registered a
synthesizesection in the global header (ws_header) — iconhub, tag "Synthesize", under the REFLECT app-switcher group. The header ships the mainsite red, so Synthesize adopts red as a deliberate accent exception (like Intervention and Tips).
July 2026
v2.8.4 — Audit close-out (JSON component tiers)
- Improved Promoted the 10 helpers that were only documented as nested
convenience/alternatives/relatednotes to full top-level component entries inworkshopr-design-system.json:ws_card_start,ws_card_end,ws_modal_start,ws_modal_end,ws_modal_footer,ws_duration,ws_category,ws_status,ws_icon_button,ws_stats. The drift check now catalogues 49/49 helpers at top level (was 39), so they're discoverable and won't get re-implemented as raw HTML. Closes the last open finding (P1-3) fromAUDIT-2026-07-01-app-vs-design-system.md.
July 2026
v2.8.3 — app-tips.php color fix
- Fixed
app-tips.phpstill showed coral/red. Tips has shipped Facilitate purple (#7C4DFF) as a deliberate, documented exception since at least v2.8.0 (which corrected the JSON's category colors to match), but the app guide page itself still displayed the old coral/red palette. Updated all color swatches, hex displays, the gradient/shadow, the 6 category tints, and prose ("warm coral" / "coral red" / "coral") to the purple values inplatform-tokens.css(#7C4DFF/#6930C3/#E8DEFF/#F3EEFF+--tips-cat-1..6). Clarified the exception note: Tips is Learn-phase content but deliberately ships Facilitate purple, not Learn red.
July 2026
v2.8.2 — app-planner.php color fix
- Fixed
app-planner.phpstill showed sky blue. The v2.8.0 Planner recolor (blue → Learn red) updatedplatform-tokens.cssand the JSON but missed the Planner app guide page itself. Updated all color swatches, hex displays, the gradient, and prose ("blue" → "red") to#E54D4D/#C73E3E/#FEE2E2/#FEF2F2. Added an explicit note that the Plan phase token (--phase-plan) stays#0284C7— only the app itself ships red.
July 2026
v2.8.1 — Documentation cleanup
- Fixed
platform-color-system.mdwas badly stale (last synced Feb 2026) and contradicted the code on five apps (tips shown red, intervention purple, planner blue, dna green, training green). Rewrote it as a lean ~130-line color-architecture doc that points toplatform-tokens.cssfor hex values instead of mirroring them (mirroring was the drift source). Fixed the same stale planner/DNA colors inworkshopr-style-guide.md. - New
DOC-TEMPLATE.md— standard copy-paste templates (Component / Pattern / Foundation) plus shared doc conventions (status + last-updated header, TOC for long docs, second person, maturity indicators) so every DS doc is structured consistently. - Improved De-duplicated the entry docs.
START-HERE.mdis the single router (source-of-truth hierarchy + on-ramps);README.mdis now purely the detailed file map and no longer repeats the "what to read" guidance. Fixed stale claims (JSON is hand-maintained not generated; DESIGN.md is intent truth; 12 app guides not 8). - Improved Added standardized status / last-updated headers to START-HERE, README, platform-color-system, illustration-guidelines, and a Contents TOC to the long docs. Updated START-HERE's documented color exceptions to include Planner, DNA, and Intervention.
- Fixed Refreshed the per-app guide pages to match the recolor —
app-planner.php→ red,app-dna.php→ red,app-tips.php→ purple — corrected the remaining stale app-color blocks inworkshopr-style-guide.md(tips categories → purple, intervention → red, training → red), and fixedapp-hybrid.php's stale "aligned with Planner" note (Hybrid uses the Plan phase blue; the Planner app is now red).
July 2026
v2.8.0 — App-vs-design-system reconciliation (audit)
- Fixed Planner recolored blue → red. The Planner app shipped mainsite/Learn red (
#E54D4D) since June 2026 but the DS still said sky blue. Updated--planner-*inplatform-tokens.css,apps.plannerin the JSON (with acolorNote), and the phase table + a new exception bullet inDESIGN.md. The Plan phase (--phase-plan) stays blue; only the app overrides. - Fixed DNA recolored green → red. Same treatment for DNA (
--dna-*+ 5 category tints, JSON, DESIGN.md). The Reflect phase (--phase-reflect) stays emerald because Coach still uses it. - Fixed Tips category colors. The JSON listed a red scale for
apps.tips.categories; corrected to the purple scale thatplatform-tokens.cssand the app actually use. - Fixed Motion easing contradiction. Canonical reveal/spring ease is now
cubic-bezier(0.22, 1, 0.36, 1)acrossDESIGN.md, the JSON, andmotion.md(was split between two curves). Addedstandard(0.4, 0, 0.2, 1) for micro UI transitions. - Improved
motion.mdrewritten for the real stack — vanilla CSS transitions/@keyframes+IntersectionObserver(it previously documented Next.js/React/Framer Motion/GSAP, none of which this platform uses). - New Z-index scale extended to cover what the app actually uses:
--z-overlay: 1000,--z-header: 1100, and a high band--z-dialog: 10000/-high: 10001/-highest: 10004(planner live-mode/kiosk). Mirrored in the JSONzIndex. - New Extended utility color tokens for values the app was hardcoding:
--color-orange-600,--color-green-700,--color-auth-primary,--color-auth-gradient-dark, and purple/orange 100–200 pastels. - Improved Aligned
css/facilitator/facilitator.css--phase-facilitatefallbacks from stale#7C3AEDto the real#7C4DFF. BumpedDESIGN.mdlast-reconciledto 2026-07-01. Full audit atdesign-system/AUDIT-2026-07-01-app-vs-design-system.md.
June 2026
v2.7.18 — Intervention accent: purple → red
- Improved Intervention now uses red (Learn red
#E54D4D) as its accent instead of facilitate purple. Updated the--intervention-*tokens (primary/dark/light/very-light/gradient/shadow + 6 category tints) inplatform-tokens.css, theapps.interventionblock inworkshopr-design-system.json(with acolorNote), and theapp-intervention.phpguide. Drift check passes. Intervention stays in the Facilitate phase but is a deliberate color exception, like Tips.
June 2026
v2.7.17 — My Workshopr header section + self-contained footer
- New Added a
myworkshoprsection tows_header(iconspace_dashboard, tag “My Workshopr”, single Dashboard nav link). The title band is enlarged for this section (15px, wider) via a[data-section="myworkshopr"]-scoped rule so other apps are unaffected. - Improved The global footer (
includes/components/footer.php) now self-loadsplatform-tokens.cssso it renders correctly on host pages (e.g. MyWorkshopr) that don't already include the token sheet. Browsers dedupe the identical href elsewhere.
June 2026
v2.7.16 — ws_header account menu + wired controls
- New
ws_header(includes/components/header.php) gained a logged-in account dropdown (.wsh-header__account/.wsh-header__menu) with avatar, name/email, and links to My Workshopr, My Workshops, Team, Settings, and Sign out. - Improved Header controls are now functional: search button opens the site-search overlay (
data-search-trigger), the apps grid icon became a gear linking to/myworkshopr/, and Sign In opens the auth modal in place. The component now self-loads its auth-modal and search-overlay dependencies.
June 2026
v2.7.15 — Letter CTA form polish
- Improved Refined the footer Letter CTA form: input
border-radiusnow matches the button (10px), inputs bumped to 16px / roomier padding (no iOS zoom), added input hover states, a.cta__noteprivacy line, and a single-column stack under 480px.
June 2026
v2.7.14 — Letter CTA form wired to newsletter API
- Fixed The Letter CTA subscribe form (global footer +
academy/components/letter-cta.php) now submits to/api/newsletter.phpwith loading/success/error states. Added a.cta__hphoneypot field and.cta__msg/.cta__msg--errorstatus-message styles.
June 2026
v2.7.13 — Larger .section-title display headings
- Improved
.section-titledisplay headings increased ~25%: theclamp(28px, 4vw, 38px)rule (Intervention, Podcasts, Tips) is nowclamp(35px, 5vw, 48px); DNA moved--text-3xl→--text-4xl; shared-components and Academy usecalc(token * 1.25). Small uppercase sidebar/handout labels were intentionally left unchanged.
June 2026
v2.7.12 — Global header label left-aligned
- Improved
ws_header()app/section label (.wsh-header__tag) is now left-aligned (justify-content: flex-start; text-align: left) instead of centered, so multi-line labels like “Facilitator Style Quiz” read cleanly.
June 2026
v2.7.11 — Global header sticky to top
- Improved
ws_header()(.wsh-header) is nowposition: sticky; top: 0instead ofposition: relative, so the global navigation pins to the top of the viewport on scroll. Existingz-index: 1050(above planner topbar/slide panels, below modals) is unchanged.
June 2026
v2.7.10 — Section hero: book-cover carousel
- New
ws_section_hero()now supports a per-sectioncarouselconfig (array of{img, href, title}) that renders an auto-advancing book-cover carousel in the visual slot instead of the static illustration. Includes clickable covers, prev/next arrows, dot navigation, pause-on-hover/focus, andprefers-reduced-motionsupport. CSS/JS are namespaced under.wsh-carouseland self-emitted once. First used by the Academy hero to showcase all six ebook covers. Each slide also supports an optional top-right statuspill({label, tone}—availablegreen /soonred).
June 2026
v2.7.9 — Section hero component
- New
ws_section_hero($section)(includes/components/section-hero.php) — section-aware hero from the Figma "Hero — *" frames, with a per-section registry (home, library, academy, tips, planner). Self-emits its CSS under the.wsh-heronamespace so it renders on any page regardless of bundle. Illustrations live in/images/heroes/. Placed on the home, library, academy, tips, and planner-landing pages. - New
section-hero.phpisrequire_once'd by bothhelpers.phpandcomponents/header.php, so any page with the global header can render a section hero without pulling in the full helpers. - Fixed
ws_headernow establishes a stacking context (position: relative; z-index: 1050) so its app-switcher dropdown clears app chrome (planner topbar/panels) while staying below modals. - Improved Tuned
ws_section_heroto match the Figma "Hero — Home" frame exactly — light-weight Fraunces title in warm ink (#4D453B) with the red italic accent, bordered badge, gradient primary CTA, and dark (not red) stat figures. Pulled the exact illustration from Figma. - Improved Recreated the Library hero from the Figma "Hero — Library" frame — added optional
imageRounded(16px) andvisualChips(category quick-links under the illustration) to the component; Library now shows Workshops / Icebreakers / Collections / Goals chips. - New
includes/page-grid.php— a reusable snippet that lays the house graph-paper grid (24px, warm ink,background-attachment: fixed) over the pagebodybackground, preserving each page'sbackground-color. Added to the home, library, academy, tips, and planner landing index pages;ws_section_heroalso carries the grid so it reads behind the hero. - New
ws_dna_cta()(includes/components/dna-cta.php) — Facilitator-DNA quiz promo strip from the Figma "dna cta" frame: light-red bar with an overlapping circular archetype badge, red icon chip, title/subtitle, and a "Take the quiz" button linking to/dna/. Self-emits its CSS; ships with the global header. - Improved
ws_section_herobuttons now accept an optionalicon(the Academy hero's "Get the Books" uses it). Synced the Academy hero to its Figma frame (badge "Workshopr Academy", book icon on the primary CTA). - Fixed Hero/CTA icons rendered as literal text on pages (e.g. Academy) that use
ws_headerbut don't loadincludes/icon.php— the header component now loads the inline-SVGicon()helper so component icons always render. - Improved The global footer's "Level Up Your Facilitation" section now animates in on scroll — each row fades + rises as it enters the viewport, with cards staggered 70ms within a row (ease-out, ~0.5s). Progressive enhancement via
IntersectionObserver: no-JS andprefers-reduced-motionusers get the section with no hidden state, and the revealed state is explicit so content is never stuck hidden. Doesn't interfere with the existing card hover-lift.
June 2026
v2.7.8 — Planner recolored to mainsite red
- Improved The planner phase color is now mainsite red, not the legacy plan-phase blue.
_component-bridge.csspreviously remapped--mainsite-primary → --planner-primary(blue), forcing the whole planner blue. It now points the planner accent tokens (--planner-primary/dark/light/very-light, plus the--planner-interactive*and--planner-blue*aliases) at the native--mainsite-*reds, scoped tobody.planner-page/.planner-app-activeso it beats the:roottoken re-imports. Note: aliases declared: var(--planner-primary)at:rootare computed there and must be re-declared in the scoped block to recolor. - Improved AI Workshop Generator loading state (
_generator-modal.css) — heading/subtitle dropped from--text-display/--text-h2to--text-h3/--text-small, with a pulsingauto_awesomesparkle layered inside the spinner.
June 2026
v2.7.7 — Generator modal type scale
- Improved AI Workshop Generator modal (
css/planner/components/_generator-modal.css) — form labels/inputs/selects were using heading tokens (--text-h2/--text-h3); right-sized to--text-ui/--text-small/--text-metaso controls match the planner UI scale. Added a:-webkit-autofilloverride so autofilled fields stay on-theme instead of browser blue. Shared planner@importstamp bumped20260610 → 20260615. - Improved Generator output view got the same treatment — agenda item names dropped from
--text-h2to--text-h4, start times / headers / rationale / refine input from--text-h3to--text-ui/--text-small, the "total" badge became a pill, and the loud salmon (#FCA5A5) refine strip was softened to a light-red bar with a subtle border. - Improved Generator error state now matches the loading state — title/body dropped from
--text-display/--text-h2to--text-h3/--text-small, and the icon (which was targeting a never-rendered.material-symbols-rounded) is now a 44px circular error chip. All four generator states — form, output, loading, error — share one type scale and layout language.
June 2026
v2.7.6 — Versioned @imports rolled out platform-wide
- Fixed Extended the v2.7.5 stale-cache fix to every other CSS bundle with unversioned
@imports:css/styles.css(main site),css/coach/coach.css,css/dna/styles.css,css/academy/training.css,css/intervention/styles.css,css/participant/participant.css,css/facilitator/*.css,css/planner/analytics.css, plus the nested token imports (css/tokens/_variables.css,css/coach/base/_variables.css,assets/css/auth-modal.css,facilitator/css/facilitator.css). All imports now carry the shared?v=YYYYMMDDstamp. Bump the stamp whenever any imported file changes. External imports (Google Fonts) and bundler-managed apps (dna/mobile) deliberately excluded.
June 2026
v2.7.5 — Versioned @imports in the planner bundle
- Fixed Stale-cache breakage after CSS deploys: the
?v=stamp onplanner-new.cssnever invalidated the files it@imports, and the server caches CSS for 7 days — so production visitors kept week-old partials (collapsed.planner-bodyflex layout, exposed card edit panels). Every@importinplanner-new.css(and theplatform-tokens.cssimport insidebase/_variables.css) now carries a shared?v=YYYYMMDDstamp. Bump the stamp on all imports whenever any planner CSS changes.
June 2026
v2.7.4 — Planner stylesheet consolidation
- Fixed Made
css/planner/planner-new.cssthe single canonical planner bundle. The partially-guttedplanner-v2.cssmonolith (1,042DUPLICATE/EXTRACTEDmarkers) was retired and deleted; every selector it still held was already present in the imported component set, except the.sr-onlya11y utility which was migrated intoplanner-new.css.planner/includes/header.phpnow loads the one bundle on the modular path (the deadplanner.csslink and the temporary standalone_modals.csspatch are gone), and the kiosk/embedded headers were repointed off the retired monolith. Token values unchanged — drift check passes.
June 2026
v2.7.3 — Global 80% text scale
- Improved Platform text now renders at 80%. Font sizes are
remand spacing ispx, so a roothtml { font-size: 80% }shrinks all rem text while leaving layout intact. Added toplatform-tokens.cssand both global headers (unified-header.css,academy/global-header.css) to reach pages outside the token chain; the per-app html bases (mainsite, planner, academy, participant, facilitator) were scaled by the same0.8so it stays uniform. No token values changed, so the JSON mirror stays in sync (drift check passes). Inlinepxfont sizes on bespoke landing pages are not covered.
June 2026
v2.7.2 — Invalid negative-margin token syntax
- Fixed Repaired 8 invalid
-var(--space-*)negative margins acrosscss/detail-immersive.css,css/styles.css,css/planner/planner-v2.css,css/planner/live-mode.css, andcss/academy/styles.css.-var()is not valid CSS, so browsers dropped the entire declaration — most visibly leaving the library detail-page stats card (.immersive-stats-float) fully left-aligned because itsmargin: -var(--space-8) auto 0(and with it theautocentering) was ignored. The correct form for negating a token iscalc(-1 * var(--space-8)).
June 2026
v2.7.1 — Planner phase-color fix
- Fixed The planner’s component bridge (
css/planner/components/_component-bridge.css) never remapped--mainsite-primary, so the global header andws_*components fell back to mainsite red (#E54D4D) instead of the planner Plan-phase blue (#0284C7). Added a body-scoped remap (body.planner-app-active) that beats:rootand survives the lateplatform-tokens.css@importinassets/css/auth-modal.css. Semantic danger reds use their own tokens and are unaffected.
June 2026
v2.7.0 — Portable component library (PHP + React)
- New
design-system/components/php/anddesign-system/components/react/— portable implementations of everyws_*component (49) and all 6 documented patterns. PHP ships as namespacedWorkshopr\DS\*standalone functions (collision-free with the globalws_*helpers); React ships as typed.tsxfunction components (Ws<Name>+Ws<Name>Props). - Improved Both flavors render the existing
ws-*class names fromcomponents.css(single source of truth) — no duplicated CSS, no hardcoded colors/sizes/fonts. Shared helpers:_icon.php(icon/cx/attrs/e) andIcon.tsx/cx.ts. - New Barrels:
php/index.php(requires all 50 components, 59Workshopr\DSfunctions) andreact/index.ts(52 exports), plus aREADME.mdwith usage and a patterns-CSS caveat. All PHP verified withphp -l+ render smoke tests.
June 2026
v2.6.0 — AI-enablement: enforcer fix, enriched catalog, DTCG export
- Fixed The
design-system-enforcerMCP pointed at a stale/sites/newphpdir where the server file doesn’t exist, so it never launched — the AI’s real-time compliance oracle was dead. Repointed to the absolute platform path (now reads 325 tokens + 49ws_*helpers). Wired “validate before finishing any UI work” (check_snippet/validate_file/suggest_component) into CLAUDE.md, GEMINI.md, and START-HERE. - New
generate-catalog.phpnow enrichescomponents.generated.jsonwith a canonicalexample,whenToUse/whenNotToUse, ana11ynote, and a derivedantiPatternperws_*helper — extracted from thecomponents/*.phpdemos. It’s now the “how do I use this component” reference for AI (example 41/49, whenToUse 20/49, a11y 6/49). - New
generate-tokens-dtcg.php→tokens.dtcg.json: all tokens in W3C DTCG format with$typeinference,$descriptionfrom CSS comments, andvar()→{alias}references. Standard format for Style Dictionary, Figma, Tokens Studio, and AI tooling. - New
design-system/llms.txt— an llms.txt-convention index pointing AI agents at the canonical files, machine-readable sources, rules, and validators. Both new generators have--checkmodes wired into npm, the CIds-driftjob, and the pre-commit hook.
June 2026
v2.5.2 — Display + timer tokens (font-size scale complete)
- New Extended the type scale with five above-base tokens:
--text-display-lg(3.5rem/56px),--text-display-xl(4.5rem/72px) for large hero headings, and--text-timer-sm/md/lg(4/5/6rem) for the live & run-mode countdown numerals. - Improved Tokenized the final 12 held hero/timer declarations. The platform now has zero bare font-size values — every
font-sizeresolves through a--text-*token. check-drift passes; all apps verified HTTP 200.
June 2026
v2.5.1 — Off-scale font-size normalization
- Improved Snapped 619 off-scale font-sizes to the nearest type-scale step via
ds-tokenize-fontsize.mjs --snap. Each becomesvar(--text-*, <token-rem>)so the fallback matches the snapped size. Most deltas are sub-pixel; the visible ones are28px→24px(62×),22px→20px(46×),36px→32px, and40px→46px. - Held 12 hero declarations (56–96px / 3.5–6rem) are above the top scale step (46px); the snapper holds anything whose nearest step is >8px away rather than collapsing hero text. These await dedicated display tokens.
June 2026
v2.5.0 — Type-scale tokenization
- Improved Tokenized 1,635 hardcoded font-sizes to
--text-*across 104 stylesheets. Only exact on-scale values were converted (px and rem, parsed unit-agnostically), each retaining its original value as a standalone fallback — e.g.font-size: 14px→font-size: var(--text-small, 14px). Value-preserving, idempotent, and safe on pages that don't loadplatform-tokens.css. - New
scripts/ds-tokenize-fontsize.mjs— exact-match font-size tokenizer. Reports off-scale values rather than snapping them, so no size silently changes. - Note ~631 off-scale sizes remain (e.g.
0.8rem,0.85rem,17px,22px,28px, hero36–96px). These need a typography decision (snap to the nearest scale step = a visual change) and were deliberately not auto-converted.
June 2026
v2.4.0 — Compliance gate + token cleanup
- New
scripts/ds-lint.mjs— a low-noise compliance gate that fails on three high-signal regressions: cool-gray hex, banned font stacks (Source Sans 3, legacy Bitter/DM Sans), and wrong-phase brand-color leakage. Scopes phase rules carefully (purple is allowed in the blue planner for activity/icebreaker accents; green is never flagged because it doubles as success/complete). Runs vianpm run lint:ds, the pre-commit hook on staged files, and a CI job. - New
scripts/ds-tokenize.mjs— value-preserving hex→token rewriter. Maps grays (warm Stone), ink, semantic status, and brand primaries; resolves#fffby context (color:→--text-inverse, else--bg-surface); and skips hexes already invar()fallback position so it never produces redundantvar(--x, var(--x)). - Improved
components.css— 55 bare hardcoded colors tokenized and all 27 cool-grays removed. Several were dead-token fallbacks (var(--color-border, #e5e7eb),var(--color-text, #111827)) referencing undefined variables, so the cool gray was actually rendering; these now resolve to warm Stone / ink tokens. Component demo page verified rendering with zero PHP errors. - Fixed Phase-color leakage — Coach mobile app
--primaryand note-icons moved off mainsite red / planner blue onto Facilitate purple; Tips footer accents moved off red onto Tips purple. Coach, Tips, and Facilitator pages verified at HTTP 200. - Fixed
Source Sans 3→ Inter across facilitator, myworkshopr, participant, and prep stylesheets (12 declarations), enforcing the Fraunces/Inter mandate. Microsite--font-*definitions confirmed already on Fraunces/Inter (Bitter/DM Sans only survive in comments). - Improved Remaining cool-grays cleared from
library-goals.css, the shared_sidebar.css,listing-redesign.css, and the DNA mobile reading sheet, then from the consolidated planner tree — zero cool-grays platform-wide. - Fixed Resolved the last 22 wrong-phase lint hits. Investigation found these were all intentional danger/CTA reds caught only by their redundant literal
#E54D4Dfallbacks (facilitator--fac-red=--color-error; planner--planner-red= documented mainsite-CTA alias;--phase-learnstatus colors). Tokenized them (drop literal, keep the var) for zero visual change; mapped the live-mode--run-dangertoken to--color-error; and marked the standalone-capable auth-modal CTA with a documented lint exception. ds-lint now clean across all 112 scanned files.
June 2026
v2.3.0 — Consolidation: single entry point + drift guard
- New
START-HERE.md— one entry point with a stated source-of-truth hierarchy (/DESIGN.md+/motion.mdfor intent;platform-tokens.css+helpers.phpfor implementation; JSON as a machine mirror) and audience on-ramps for designers, developers, and Claude. - New
check-drift.php— guards the JSON mirror against drift: fails if anyws_*helper is undocumented in the JSON, or if phase-color tokens disagree betweenplatform-tokens.cssand the JSON. Known source contradictions (tips, training) are allowlisted as visible warnings pending a design decision. - Improved JSON
metanow references the canonical/DESIGN.mdand/motion.md, declares itself a hand-maintained mirror (correcting the earlier “auto-generated” claim), and points to the drift guard. - Improved
README.mdnow defers toSTART-HERE.mdand corrects the JSON provenance note. - Fixed Phase-color records reconciled to the shipped apps: Tips documented as a deliberate Facilitate-purple exception (Learn content, purple brand), and Academy/Training documented as Learn red (
#E54D4D) — the unimplemented “training = green” mapping was retired from the JSON,DESIGN.md, and CLAUDE.md §11. Each app now carries acolorNotein the JSON, and both passcheck-drift.phpwithout an allowlist. - New Added the
--facilitator-primarytoken family (aliased to--phase-facilitate,#7C4DFF) — facilitator was the only app missing its--{app}-primarytoken. The JSON facilitator color was corrected from the stray#7C3AEDto the phase purple, matching coach/intervention. - Improved
check-drift.phpnow resolves one level of CSSvar()indirection, so phase-aliased app tokens validate. Wired into governance:npm run lint:ds:drift, ads-driftPHP job in the Design System Lint GitHub workflow, and a staged-file gate in the pre-commit hook. - Improved
check-drift.phpgained a reverse check — a JSON component with no matchingws_*function (renamed/removed) now fails the build — and reports component counts. Corrected stale counts inREADME.md(39 demo pages, 49 helpers, not “34”/“40+”), and alignedGEMINI.mdwith the consolidatedDESIGN.md(it still pointed at the deprecated taste/principles/forms files). - New
generate-catalog.php+components.generated.json— a real generator that derives aws_*component roster (signature, summary, declared options) straight from thehelpers.phpdocblocks. The curated JSON keeps its rich hand-authored option metadata; the generated file is the can’t-drift companion.--checkmode fails on a stale roster or a curated signature that no longer matches source, wired intonpm run lint:ds:catalog, the CIds-driftjob, and the pre-commit hook. Also added the canonical-source banner tovoice.php/philosophy.phppointing atDESIGN.md.
May 2026
v2.2.0 — Token compliance: extended accent + brand tokens
- Improved Tokenized recurring hardcoded colors across
css/academy/styles.css,css/facilitator/facilitator.css,css/planner-new/*,css/styles.css, andcss/unified-header.css— raw hex values replaced with platform tokens. Exact color values preserved. - New Teal accent tokens for planner secondary actions:
--color-teal,--color-teal-dark. - New Extended state shades:
--color-warning-darker,--color-error-strong,--color-error-darker,--color-info-strong,--planner-accent-dark,--color-success-surface,--color-success-darker, and the soft warning callout surface (--color-warning-surface+-border/-ink). - New Meeting-tool brand tokens mirroring the podcast platform set:
--platform-meet,--platform-zoom,--platform-inperson(each with-ink/-bgvariants where used).
May 2026
v2.1.1 — Planner sidebar: prototype-matched toolbar + flat suggested list
- New
.sidebar-toolbarcomponent for the planner's activity sidebar — sticky search input + pill-style type tabs. Lives incss/planner/components/_sidebar.css(mirrored atcss/components/_sidebar.css). - New
.side__group/.side__group-title/.side__list/.side__more/.side__group-emptystructural classes for the flat "Suggested for X" list and "Browse all activities" footer link. - New
.library-card--libvariant — full-bleed flat draggable card with.lib-card__top(dot + title),.lib-card__meta(pill + duration),.lib-card__dot--{exercise|icebreaker|break}, and.lib-card__pill--{exercise|icebreaker|break}type modifiers. Scoped under.activity-sidebarso it doesn't affect other surfaces that use.library-card. - Improved All new card / pill colors pull from existing planner tokens (
--planner-exercise-color,--planner-icebreaker-color,--planner-break-color) viacolor-mix()for tinted backgrounds — no new hex values introduced. - New Utility hide rules:
.sidebar-section[hidden],.library-card[hidden],.sidebar-sections[hidden],.sidebar-custom-activity-section[hidden],.side__group[hidden],.side__group-empty[hidden]. Lets the filter module + legacy-section wrapper hide nodes via the native HTMLhiddenattribute.
May 2026
v2.1.0 — Level Up Grid: phase grouping + button --half
- New
ws_level_up_grid()now supports an optionalgroup_by_phase => truemode that renders Learn / Plan / Facilitate / Reflect as their own sub-sections, each with an italic phase title and a half-paragraph intro in Workshopr voice (overridable viaphase_copy). - New CSS classes for the grouped layout:
.ws-level-up__phase,.ws-level-up__phase-head,.ws-level-up__phase-title,.ws-level-up__phase-desc, plus phase-color modifiers (--learn / --plan / --facilitate / --reflect) on the phase block itself. - New
.ws-level-up__grid--3modifier for 3-up phase rows (used by Learn and Plan phases). Existing 4-up grid stays the default. - New
.ws-btn--halfwidth modifier (50%) — sized for narrow card columns where--fullreads as too dominant. - Improved Responsive overrides updated so both
.ws-level-up__gridand.ws-level-up__grid--3step down 4 → 3 → 2 → 1 column, and.ws-level-up__phase-headstacks below 1024px. - Improved Demo:
/design-system/components/demos/level-up-grid.phpnow showcases the grouped variant. Button demo cheatsheet lists--half.
April 2026
v2.0.0 — Planner ws_* Component Migration (Phase 5)
- Improved Migrated
planner/edit.phpbuttons and form inputs from raw HTML tows_button()andws_input() - Improved Migrated remaining raw inputs in
planner/includes/modals.php— invite email, role select, search inputs, category filter - Improved Replaced raw type badges and duration spans in
planner/view.phpwithws_badge()andws_duration() - Improved Standardized
planner/saved.phpbuttons withws_button() - Improved Migrated schedule panel in
planner/planner.php— date/time inputs, timezone select, footer buttons - Improved Tokenized inline checkbox styles in save modal to use
var(--space-*)andvar(--text-sm)
April 2026
v1.9.1 — Planner Brand Color Tokenization
- New Added
--planner-overlay-light/medium/heavyand--planner-backdrop/backdrop-lighttokens to planner variables - Improved Tokenized standalone
#E54D4D(red),#22C55E(green),#9333EA(purple),#F59E0B(warning) hex values across planner CSS to usevar(--planner-*)andvar(--color-*)tokens - Improved Replaced standalone
#2563EBin planner cards and generator modal withvar(--planner-exercise-color) - Improved Tokenized status/semantic colors (warning, error, success backgrounds) in planner-v2.css, analytics.css, exercise-builder.css, briefing.css, live-mode.css
April 2026
v1.9.0 — Cool Gray Eradication & Structural Adoption
- Improved Eradicated all 354 hardcoded cool gray hex values across 23 CSS files. Entire platform now uses warm stone grays (
--gray-50through--gray-900) exclusively - Improved Added
helpers.phpto globalheader.php— all pages now have automatic access tows_*component helpers without manual includes - Improved Migrated
/contact/form from raw HTML tows_input(),ws_select(),ws_button()components - Fixed Replaced 6 hardcoded
font-family: 'Bitter'in MyWorkshopr CSS withvar(--font-heading) - Fixed Cleaned up 2
DM Sansfont fallbacks in Academy CSS - Improved Coach variables now reference
var(--gray-*)tokens for text colors instead of cool gray hex values - Improved Landing page, auth modal, content gating, and typography base CSS fallbacks updated from cool to warm stone
- New Added "Touch It, Fix It" convention to
Conventions.mdfor ongoing adoption enforcement
April 2026
v1.8.4 — Live-Mode Font-Size Normalization
- Improved Normalized all hardcoded
font-sizevalues inlive-mode.cssto design system tokens: compact type scale variables for UI chrome,remunits for presentation display elements, icon sizes preserved aspx
April 2026
v1.8.3 — Planner Font-Weight & Font-Size Normalization
- Improved Converted 9 hardcoded
font-sizepx values toremunits inrun-mode.cssandworkshop-studio.css - Improved Replaced all hardcoded
font-weightnumeric values (400/500/600/700) with design tokens (--font-normal,--font-medium,--font-semibold,--font-bold) across 7 planner CSS files
April 2026
v1.8.2 — Facilitator CSS Hardening
- Improved Replaced ~25 instances of
transition: allin facilitator.css with specific property transitions using design tokens (--duration-fast,--duration-base) - Improved Tokenized ~40 standalone hardcoded hex colors to CSS variables with fallbacks (
--color-warning-*,--color-error-*,--color-success-*,--color-info-*,--phase-facilitate-*,--gray-*) - New Added
:focus-visibleoutlines to all 45+ interactive elements in the Facilitator app (buttons, tabs, cards, inputs) - New Added
@media (prefers-reduced-motion: reduce)block scoped to.facilitator-app
March 2026
v1.8.1 — Secondary Button Refinement
- Improved
.ws-btn--secondaryhover style updated: now uses a subtle background tint (--phase-learn-very-light) and darker text instead of a full solid color fill, matching the academy pattern for a more refined interaction - Improved
.ws-btn--secondarydefault background changed from opaque white (--bg-surface) totransparentfor better layering on non-white backgrounds
March 2026
v1.8.0 — Semantic Tokens & Cross-App Polish
- New 40+ semantic component tokens added to
platform-tokens.css— buttons, cards, inputs, modals, badges, tooltips, toasts, links, and dividers now have dedicated token sets - New Standard suffix convention documented in style guide:
-dark,-light,-very-light,-secondary,-muted,-faint - New Semantic component token documentation in
tokens.phpwith full usage tables for each category - Improved Google Font imports cleaned across 24 PHP templates — removed unused Bitter and DM Sans font families from
<link>tags - Improved Coach landing pages updated: inline CSS font references migrated from DM Sans to Inter
- Improved Token count updated from 180+ to 280+ across documentation and hero stats
- Improved Changelog CSS now uses design tokens instead of hardcoded hex values
- Fixed Audit score: 87 → 92/100
March 2026
v1.7.0 — Comprehensive Audit & Migration
- New 6 overlay/backdrop tokens added:
--backdrop-light,--backdrop-medium,--backdrop-heavy,--backdrop-brand,--overlay-white,--overlay-surface - New Accessible contrast alternatives:
--color-primary-on-white(#C73E3E, 5.02:1) and--color-ink-faint-accessible(#78716C, 4.6:1) - New Contrast reference table added to
colors.phpdocumentation - New Deprecation timeline for legacy v1.x tokens — v2.0 sunset plan with console warning phase
- New
components/components.css— dedicated component stylesheet separated from token file - Improved Full ARIA + keyboard navigation documentation added to Tabs, Accordion, Tooltip, and Modal components
- Improved Font migration complete across all microsites: Bitter → Fraunces, DM Sans → Inter in 9 CSS files
- Improved Planner hardcoded color sweep: 156 hex → token replacements across 9 CSS files (86% reduction, 361 → 49)
- Improved Modal component documentation now includes parameters table
- Improved Breakpoint documentation updated from 4 to 6 breakpoints matching
platform-tokens.css - Fixed 8 cool gray hex values in
overview.phpand 4 inchangelog.phpreplaced with warm stone palette - Fixed
--phase-reflect-lightcorrected to #A7F3D0; missing-very-lightvariants added - Fixed Hero stats corrected: "15 Components" → "33", "100+ Design Tokens" → "280+"
- Fixed Missing half-step spacing tokens and typography tokens added to documentation
- Fixed
.app-badgecomponent CSS moved out ofplatform-tokens.cssintocomponents.css - Deprecated Legacy token aliases (v1.x names) — scheduled for removal in v2.0
- Fixed Audit score: 62 → 78 → 87/100
March 2026
v1.6.0 — Specialized Components
- New
ws_chip_input()— Multi-value input with removable chip tokens for email lists and tag inputs - New
ws_radio_card_group()— Single-select card grid with icon, title, and description per option - New
ws_checkbox_card_group()— Multi-select card grid for choosing multiple options - New
ws_suggestion_card()— Action-oriented card for AI suggestions and contextual prompts - New
ws_modal_footer()— Standardized modal footer with primary/secondary action buttons - New
ws_activity_badge()— Color-coded activity type badge (exercise/icebreaker/break) - New
ws_results_overlay()— Full-screen/inline overlay for results, scores, and completion states - New
.ws-toggle— Reusable toggle switch CSS extracted from planner patterns - New
.ws-chip— Chip markup pattern with remove button for JS-rendered chips - Improved Component library grid updated with all 7 new component entries and demo pages
- Improved CSS uses platform tokens with hardcoded fallbacks so components work outside the planner
- Improved CLAUDE.md updated with mandatory design system usage rules and full component list
February 2026
v1.5.0 — Developer Experience
- New Copy-to-clipboard button on all
.ds-codeblocks (hover to reveal) - New Responsive preview toolbar (Desktop / Tablet / Mobile) on component demos
- New Live demo for
ws_hero()— full, minimal, and accent variants with rendered output - New Live demo for
ws_listing_card()— workshop, exercise, and icebreaker card examples - Improved Code blocks now show copy button on hover with "Copied" confirmation feedback
- Improved Demo containers auto-wrap with responsive preview controls via shared DS footer
February 2026
v1.4.0 — New Components
- New
ws_section_header()— Page/section titles with subtitle, label, and alignment options - New
ws_feature_card()— Icon + title + description cards with compact and coming-soon variants - New
ws_stat()/ws_stats()— Single metric display and grouped stat rows with dividers - New
ws_checklist()— Icon-prefixed lists with custom icons and compact variant - New
ws_empty_state()— Placeholder with icon, description, and CTA button - New
ws_breadcrumb()— Accessible navigation trail with separator icons - Improved Added animation/transition tokens to
platform-tokens.css - Improved Swept
index.phpandabout/index.phpto use component library - Improved Added storybook-style documentation pages for all new components
- Improved Updated component library grid and sidebar navigation
January 2026
v1.3.0 — Component Library
- New
ws_badge(),ws_duration(),ws_category(),ws_status()— Badge system - New
ws_button(),ws_icon_button()— Button with variants, sizes, loading state - New
ws_input(),ws_select()— Form field components - New
ws_card_start()/ws_card_end()— Card containers - New
ws_avatar()— User avatars with initials fallback - New
ws_modal_start()/ws_modal_end()— Dialogs, drawers, bottom sheets - New
ws_tabs()— Tab navigation - New
ws_tooltip()— Contextual hints on hover/focus - New
ws_accordion()— Collapsible content sections - New
ws_progress()— Progress bars and indeterminate loading - New
ws_alert()— Notification banners with actions - New
ws_skeleton()— Loading placeholder system - New Toast notification JS (
wsToastSuccess,wsToastError, etc.) - New Component documentation pages for each component
December 2025
v1.2.0 — Design System Foundations
- New Design system microsite with sidebar navigation
- New Color palette, typography, spacing, and elevation documentation
- New Platform design tokens (
platform-tokens.css) - New Philosophy, voice & tone, and design decisions pages
- New Icons reference and accessibility guidelines