Canvas Design System
Main Site Tokens

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-col with --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, and llms.txt names it. ws_grid_start, ws_container_start and the app bar, dock and footer demos, which ds:find returns for layout requests, now point to it, and --app-grid-col, --app-rail and --app-content-wide gained 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 its node_modules; it was gitignored while .mcp.json, ds:eval, ds:build-eval and test:ds all depended on it, and .mcp.json now 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 (a ws_* component before HTML, a token before a hex or pixel value) and the two commands that apply it, npm run ds:find and npm run check:ds. Both had said "There is no CI"; lint:docs now fails that claim while .github/workflows/ci.yml exists.
  • New check-drift.php check 6 holds each demo page to its component's CSS. A Tokens table may list only custom properties that the component's rules in components.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-primary and --phase-learn a month after components.css moved to the --brand* tokens, plus --transition-fast where 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-ink and --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 or components.css is staged.
October 5, 2026
v2.53.4: The solutions pages on ws_agenda
  • Improved The five solutions/bytype/ pages draw their sample agenda with ws_agenda. renderSoltAgenda() (includes/data.php) maps a library workshop through soltAgendaItems(): 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 a ws_agenda call 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 from css/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, because css/styles.css colours 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), is ws_agenda through irAgendaItems() in includes/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 without item_type or day_number renders as one plain list. .prv-agenda__mins is gone. The library's workshop page is not moved: its agenda links each exercise to the library, and ws_agenda has 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 with ws_agenda instead of the editor's input classes, which the read-only view had been hiding with overrides; briefingAgendaItems() in planner/includes/briefing-data.php maps 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 loads components.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_end puts 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 sends tab twice. Inside a panel, ws_filter_bar carries that panel's tab in its form and its "Clear all" link, with or without keep, 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 <form inside 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 and form=-owned fields don't count as sending the tab. A form whose action loads another page is skipped: another path, another host or port, or a host with no path. ./view.php, ../dir/view.php and a bare action count as this page, and so does any method other than post or dialog. 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_tabs with numeric item ids opens the tab the URL names; the ids were compared to the query string without casting.
  • Fixed ws_text treats an unknown variant as a caption rather than writing it into the class, and accepts variant and tag in any case.
  • Fixed ws_filter_bar's "Clear all" link builds its query with & explicitly, so a host whose arg_separator.output is &amp; cannot double-escape it. Its guidance says two bars on one page need different searchLabels, since each names its search landmark.
  • Improved ws_chip_input takes optional, like ws_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__required and .ws-form-row__required rules are gone, and the React WsFormRow's required prop is marked as ignored.
  • Improved DESIGN.md 11.6 and ws_filter_bar no 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. title is the card-title step (Fraunces, --text-h4, semibold, an h3 unless tag says otherwise); caption is --text-small in --text-default, which passes contrast on a card where --text-muted does not. live makes 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=notes opens Notes over the items' active flags (an unknown or disabled tab is ignored), ws_tab_panel_end adds the tab as a hidden field to each GET form in its panel, and components.js keeps 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_select and ws_form_row take optional, which appends "(optional)" to the label, and required now 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_state draws its icon from the Material Symbols font when there is no local SVG, as ws_button does. The default, inbox, has none, so the icon circle rendered empty, as did filter_alt_off and bookmark_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 a ws_tabs tab controls: id="{id}-panel", role="tabpanel", named by its tab (or label), spaced --space-4 from 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_bar is the search box above a list, with or without filters. Search-only, its button reads "Search" (with filters, "Filter"; submitLabel sets it), the form is a role="search" landmark named like the field, and showSearchLabel puts the field's name above it as a real <label>. Its guidance no longer sends search-only to ws_input, which keeps search inside other forms and in headers. keep carries 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 from start through each numeric duration; an item's own start resets them and a duration written as text stops them rather than inventing one. Items with a day are 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-agenda and others); builders in the friction run reached for ws_planner_card and 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 as ws_form_row and no output buffering in the caller.
  • New ws_cluster_start(['grow' => 'first']) (or last): that item takes the row's free width and keeps --grid-min-sm before 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 carries role="switch", so screen readers say "switch, on". ws_consent_row, whose control only enables its Send button, passes role checkbox and stays one.
  • Fixed .ws-input is box-sizing: border-box, so width: 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): wsToast when the person stays on the page; a success ws_alert when the save reloaded it, because a toast fired during page load is easily missed; and a failed save never a toast alone. wsToast is the toast to call; window.Toast (js/toast.js) and the Planner's showToast() are legacy, and the build eval no longer loads js/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_accordion and 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 found ws_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 a gap step, as a div, section, list or form), ws_cluster_start (a wrapping row with justify and align), ws_grid_start (as many equal columns as fit above a column floor, no breakpoints) and ws_container_start (a centred --container-* column with gutters), each closed by its _end. They nest (closing one out of order raises a warning), lists get role="list", the stack spaces with gap so 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 in components.css between LAYOUT: START and LAYOUT: END; the demo is Layout. In the first friction run every builder hand-wrote these.
  • New --grid-min-sm, --grid-min-md and --grid-min-lg (200, 280 and 360px): the narrowest a ws_grid column gets before the grid drops one.
  • Fixed ws_form_row's label now names what it wraps. With one control inside it gets for that control (adding an id if it has none), the helper or error joins the control's aria-describedby, an error sets aria-invalid and is announced, and required sets aria-required behind an aria-hidden asterisk. With several controls, or one that already has its own label, the row is a labelled role="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 portable components/php/FormRow.php; components/react/FormRow.tsx takes an htmlFor.
  • Improved The three screen recipes and the component pairs on composition.php lay out with the new helpers, so copying one copies no CSS; .ds-recipe-stack, -grid, -row, -actions and -narrow are gone from recipes.css. The workshop list's loading placeholders share its ws_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 placeholder the 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.json beside 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.css now states its purpose (69 had none), with contrast where it decides use: --text-muted for meta lines, --text-faint decorative only, status colours for icons and fills with their -ink partner for text. Component tokens the ws_* components do not read say so, and name what does.
  • Improved A component's a11y in components.generated.json is now one Concern: behavior line 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.txt gives the same source-of-truth order as START-HERE.md, leads with components.generated.json, and is now held to its claims by npm run lint:docs.
  • Fixed tokens.dtcg.json exported 20 tokens at their @media override instead of their value: every duration and transition as 0s !important (reduced motion), the focus ring as Highlight (forced colours), --text-muted at 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:find and the MCP's suggest_component list every option a helper takes, with its type. Options with no default were dropped, which hid ws_agent_proposal's title, why, adjust and returnFocus; 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.json is 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-waitlist in css/taste.css: a ws_input and a secondary ws_button stacked inside the existing .taste-onward block, 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-billing in css/index2.css: the /pricing/ Monthly / Yearly toggle at card size, two ghost ws_buttons in a --brand-line track 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-busy and .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-soft and 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-enter on <html>), the first nine cards fade up 12px in a 40ms stagger, --duration-slow and --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.php to --brand-tint and --brand-ink in CSS.
September 30, 2026
v2.48.1: Every call site on contract
  • Fixed .ws-field__hint, the help line ws_input renders, had no styles, so every input hint showed at body size; it now shares the .ws-field__helper rule.
  • New ws_input type file is styled (the picker button reads as a secondary button) and documented.
  • Improved ws_section_header takes tag: h1 on 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 status key, activity types through ws_activity_badge, and button attributes through attrs.
September 30, 2026
v2.48.0: Built for agents, and for agent screens
  • New A shared set for AI screens: ws_ai_mark and the ws_badge ai variant ("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_chip and ws_consent_row. js/ws-agent.js draws 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:find and the MCP return a recipe when the request is for a whole screen.
  • New npm run ds:build-eval scores 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.css styles it, and generate-catalog.php --check fails 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_input lists the options it really reads (hint, not helper).
  • 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.css reads the five brand tokens instead of the legacy phase and site aliases, with no change to any resolved value. A new ratchet in check-page.php stops pages adding .modal-overlay or 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-ink and the ws_empty_state description uses --text-default, so both meet contrast guidelines.
  • Fixed Token cycles that silently broke styling when stylesheets loaded in a different order: --font-body and nine more in css/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.json is now the contract every agent tool reads. Each ws_* function, including ws_header, ws_section_hero, the DNA and app shelf helpers and ws_workshop_list, carries its option values and defaults, demo, playground fixture, states and scope. A new tokens section lists every token with its purpose, group, tier and, for a legacy alias, the --brand* token to use. A new legacy section, from legacy-patterns.json, names the lookalikes to avoid.
  • Improved ds:find and the design-system-enforcer MCP rank requests with one shared search over the catalog and synonyms.json, and ds:find now 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:eval scores both finders on plain-language requests and fails if a score drops. Two new gates: check-drift.php fails when a token is missing from the catalog, and generate-catalog.php --check holds a completeness ratchet in catalog-baseline.json.
  • Fixed tokens.dtcg.json regenerated with the five brand tokens, so npm run check:ds passes. START-HERE.md names commands instead of hard-coded counts and is now checked by npm 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-soft and --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 the timer read-out and the tone: success time-left read-out, since the strip above the builder's canvas carries both. tone: success stays supported in ws_app_dock(); nothing uses it now. Spec, demo, DESIGN.md §12 and PhaseToolbarTest follow; 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.js keeps it only where the page carries the comments panel, and opens it through window.wsCommentsOpen(id, name, returnTo), which the panel script now exposes: it opens ws_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's more_vert on close. A listing books the panel for its page through appCommentsBookForList(); 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 primary ws_button Save to account sits under the note. It carries the bar's data-st-save-account hook, so it opens the same naming dialog. strategist.js fires ws-plan-saved after 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__signin arrives hidden and .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 left wshApplyAuth() 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; clearing workshopr_user still signs the header out. Pinned by tests/E2E/header-auth-first-paint.spec.js.
  • Fixed ws_header() dropdowns take .wsh-header__link--active on any page their menu lists, not only the one their own href names, so the home section's Company stays lit across all five company pages. Off-site and fragment items never match. Pinned by tests/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-panel in components.css, wsSidePanelOpen/Close/Toggle() in components.js, demo at components/demos/side-panel.php): a non-modal panel that slides in from the right edge. role="dialog" with aria-modal="false"; nothing goes inert and focus is not trapped. Any [data-ws-side-panel-open="{id}"] toggles it and keeps its aria-expanded in 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 fires ws-side-panel-open and ws-side-panel-close. In on --duration-enter and --ease-spring, out on --duration-exit and --ease-in, transform only. It layers one step under overlays (--z-overlay minus 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-bar or [data-ws-side-panel-anchor]) it starts under that bar and follows it as the page scrolls; full screen below 600px. Added to workshopr-design-system.json and the generated catalog.
  • New ws_side_panel() branded variant: 'variant' => 'branded' puts the head on the red band the branded ws_modal wears (--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) on ws_side_panel(). appCommentsBarAction($phase) puts mode_comment beside the save on Strategize's, Plan's, Facilitate's and Synthesize's bars, and appCommentsPanelForPage() renders the panel at body level. The form is ws_select, ws_input (textarea) and a full-width primary ws_button; the thread draws .ws-avatar (red for the workshop's owner, grey otherwise), .ws-badge for Facilitator and a .ws-pill count. 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__resolved offers Undo, with focus moved onto it. Show resolved (.app-comments__resolved-section, a toggle with aria-expanded) lists the resolved threads under the open ones, their context and text on --color-ink-muted, a neutral Resolved .ws-pill on each first comment, Reopen in place of Resolve and no Reply. The API's list, add and resolve answers carry resolved and resolved_total beside the open threads. It reads and writes through api/plan-comments.php (list, add with parent_id for 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--team and 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.json counts 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 secondary ws_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-padding and --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 as ws_sidenav draws 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.png and images/favicon-16x16.png are rendered from images/favicon.svg (graph_3), and images/apple-touch-icon.png is the app's 180px graph_3 icon. They still carried the retired W, and Chrome takes a PNG whose sizes fits over the SVG, so the tab showed the W. includes/header.php versions the three links (?v=2) so a cached icon is refetched. The install toast (app/js/app-install.js) draws graph_3 where wsToast() 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' to ws_workshop_list(), and app/css/app-shell.css sets their .ws-empty-state on --bg-paper in 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__beta and 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&section=settings: the team view is two pages now, set by data-team-mode on 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 and team.js runs the same; [data-team-mode-only] in app/css/team.css keeps each section to its own page. The workspace page's rail gains a full-width secondary Workspace settings ws_button under 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 indeterminate ws_progress gives way to ws_skeleton placeholders 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--workshops and 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-bg with --shadow-sm like 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 is ws_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 on ws_skeleton rows. 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's more_horiz menu (kind file) to launch its workshop in Strategize, Plan, Facilitate or Synthesize, on the listing's routes (ws_wl_stage_routes()); each file names its plan_id for 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__people with an .ws-avatar-group and the count of each role); .app-team__month, three counts on a card; .app-team__ws-toolbar with a search ws_input and the shared listing's .wl-chip buttons (workshop-list.css now 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 rail ws_card above 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 by app/js/workspace-create.js): the switcher's Create workspace row ([data-wsh-ws-create]) opens a branded ws_modal with one ws_form_row() field, whose helper line becomes the error after blur and carries any refusal from /api/teams.php?action=create, and the actions in ws_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's transitionend and 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.svg was a copy of picture_in_picture_alt.svg, so every icon('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 Symbols graph_3 path, the one images/favicon.svg and the header's font glyph already draw. It keeps its #E54D4D fill, because onboarding loads it as an <img> where currentColor cannot reach.
  • Improved The /app/ install icons are the brand mark: app/icons/ swaps the four squares for a white graph_3 on the red tile, with PNG 192 and 512, a full-bleed maskable 512 and a 180 apple-touch-icon.png (Safari and iOS ignore SVG touch icons). app/manifest.json lists 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-container sits --app-footer-height (48px, new, also the footer's height) plus --space-6 up. 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-bg is 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-bg does. White stays for what opens over or inside a card: modals, menus, inputs and checkboxes. Mirrored in workshopr-design-system.json (colors.backgrounds.card), tokens.dtcg.json (card.bg), the Colors and Tokens pages, and DESIGN.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-kcard and timeline row .wl-tl__row (app/css/workshop-list.css), and in app/css/app-shell.css the handoff sheet .app-shell__sheet, Plan's Strategist nudge .lp2-nudge and the Strategize rail's compact checklist. Fallbacks that still said #FFFFFF beside --card-bg now say #F7F1EA. Tables, list containers, menus, the launcher and the Synthesize action rail keep their white.
  • New Figma: color/card/bg in the Canvas Design System's Primitives collection (fill scopes, WEB code syntax var(--card-bg)) with a matching paint style, bound as the fill of Card, Listing card, Task checklist, wl-card, card-296, rail-todos, rail-film, plan-choice-card and plan-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 by team.js. .app-team__card--workshops sits on --bg-paper inside a 1px --phase-learn edge dashed 4 on, 4 off, drawn as an SVG mask on ::before because border-style: dashed cannot set the dash or the gap; with workshops in it (.has-workshops, set by team.js) the mask goes and the 1px border shows solid in the same red. .app-team__invite-card fills with --phase-learn-light. Members and pending invites are .app-team__grid tiles three across, on --card-bg with 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 large ws_select on the Seats row, tied to the billing form by form="" 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 through ws_form_row() with ws_input() and ws_select() in place of raw <input> and <select>, the row's remove as ws_icon_button(), the empty state as ws_alert(), and the actions in ws_modal_footer(), whose type=button primary team.js submits along with Enter. Per-row invite errors use .ws-form-row__error and .ws-form-row--error, with aria-invalid and aria-describedby on the field. group_add is not in images/icons/, so the dialog wears groups and its button add.
  • New Rail Seats card (Figma: Workshopr Teams mockups, Card · Builder seats, 12:1528): appShellRail() takes afterProfile, HTML set directly under the DNA card, and the workspace page puts a ws_card there built from ws_badge (status, success), ws_progress, an .ws-avatar-group of ws-avatar--xs and a secondary ws_button, styled as .app-rail__seats in app/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() takes id but not attrs, and its data option prints nothing, so the card is found by id and kept out of sight by a class until team.js fills it.
  • Improved Team settings, redesigned and in the rail: .app-team__setting rows (what it does, then its control; side by side in the main column, stacked under .app-rail), a .app-team__danger zone on --color-error-very-light, and Leave and Delete in branded ws_modal dialogs with a danger primary in ws_modal_footer() in place of confirm() and prompt(). The team root (data-team-root) is now the whole .app-page, rail included, so team.js reaches the cards there.
  • Improved Invite notices: an email-matched invite card carries a close ws-btn--icon-only (remembered per browser under workshopr_team_invites_dismissed) and fades after 30 seconds on --duration-slow, the clock pausing on hover and focus as wsToast'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) and teamsVisible() 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 into innerHTML, so any caller passing a server message that carried a user-typed name (a team name in app/js/team.js, a participant name in the pre-work tool) handed that name to the HTML parser. Both are now set with textContent in includes/components/components.js; the icons stay static markup. The legacy inline Toast.show() in includes/footer/toast-inline.php does 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.js and the first-save toast in planner/js/planner-v2.js) no longer do, or they would show &amp;. The planner's “Removed · Undo” toast, the one caller that passed markup, now appends its .toast-undo-btn, .toast-undo-hint and .sr-only spoken form as nodes inside .ws-toast__message, so it looks the same. The four pages that loaded components.js with no ?v= (the pre-work tool, dna/about.php, intervention/card.php and the design-system footer) now cache-bust it. Covered in tests/toast-a11y.spec.js and the planner delete-and-undo test in planner/tests/planner.spec.js.
September 2026
v2.40.3: Accordion panels have a name
  • Fixed ws_accordion panels pointed aria-labelledby at 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}-trigger and the panel points at it, and the expand_more arrow is aria-hidden. Same change in includes/components/accordion.php, components/php/Accordion.php and components/react/Accordion.tsx.
September 2026
v2.40.2: The intro survey on the system
  • Improved /survey/ is rebuilt from ws_pill, ws_progress, ws_radio_card_group, ws_input and ws_button inside the site header and footer, on a card that matches /start/. Its own stylesheet and inline SVG buttons are gone; layout lives in survey/survey.css on platform tokens.
  • Fixed ws_radio_card_group icons 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 passes attrs through 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 carry data-modal-close or a data-* 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-visible and button:focus-visible in css/styles.css painted --primary-red; they now use --focus-ring-color, --focus-ring-width, --focus-ring-offset and --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-body on --color-ink, and an underlined --phase-learn-dark link, instead of the unstyled 9.8px black text ws_modal leaves.
  • 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, glyph bookmark_add), between Export agenda and Slide deck in includes/components/phase-toolbars.php. images/icons/bookmark_add.svg is new. The builder shows the slot once the workshop is saved. design-system/spec-phase-toolbar.md lists it.
  • New The Save as template dialog, appPlanTemplateSaveModal() in app/includes/plan-template-modal.php: a branded ws_modal shaped like Name Workshop, one ws_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 branded ws_modal with ws_tabs, and .pt-card on ws_card: name, job line, duration and item count, an outline of four with the planner's activity-type dots, a ws_badge for slots to fill, and a Use button. Skeletons, empty and error states are ws_skeleton, ws_empty_state and ws_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 in css/admin/admin-v2.css for 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-facts and .tue-faq (css/index-new.css). Two homepage sections on the plans' rhythm, headed by ws_section_header: a two-column fact list, a semantic stage table (.tue-facts__table: Inter, --gray-700 header on --gray-50, --gray-200 rules, horizontal scroll below 900px), and a ws_accordion FAQ. 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 on html.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, -micro and -nano divide the token by --shell-scale so 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-xs and --icon-md for the tips and DNA arrows, --space-1 and --space-1-5 for 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 a ds-lint-disable-line with 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, -date and -alt, tokens only, built from ws_input and ws_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_badge plus two ws_button links, 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 em takes --mainsite-dark instead of inherit, and the .st-page--home .st-hero__title em ink 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(), the ws_task_checklist demo, and rail-todos 130:332 in 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-body on a 62ch measure in --text-secondary, and the listing heading (.wl__title, plus Plan's .launchpad-choice-screen__title) at --text-h2 semibold. All four titles wear the Display Text Pattern: upright bold first line, em bold italic --mainsite-dark on an inset 8px --mainsite-light underline (underlined words in an /app/ header are always red); .st-page--home .st-hero__title joins .st-page--workbench for it. Home's .dash__greeting (dashboard.css) moves from --text-h2 to --text-display-lg at 1.12, the index title's size; .dash__hero-title is unchanged. The three views no longer take .app-hero--compact or titleHidden; Prove still does. Synthesize's index joins $barlessIndex and gains appShellCrumbs().
  • New Admin contributions drawer: review findings panel. .review-findings, .review-finding--blocking | --should | --nice and 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 as textContent.
  • Improved Synthesize intake wears the app frame again. Intake.view renders .wrap--app.wrap--intake > .sy-app.sy-intake with aside.sy-rail.sy-artifacts-rail first and section.sy-main.sy-intake-left after it, and viewBooting() draws the same classes, so the skeleton and the screen are one layout. synthesize.css drops the intake's own 1fr 340px grid. In the shell (app-shell.css, from 1081 up) the tan column is a --bg-paper-dark gradient on .sy-intake to --app-rail-width, the rail is transparent, min-height: 0 and capped at 100dvh - 2 * --app-bar-height - --space-12 so it sticks; the rail pads 12 a side (296 of content), the surface 24 48 110 32; the wrap's 24 and the stage's 96 dock clearance are zeroed on this screen via :has(.sy-intake). $wearsFooter in app/index.php now includes synthesize. The compact journey card's finished state no longer takes a cta. .synth (synthesize.css) paints --bg-page, --paper-grid-image and --paper-grid-size in longhand; it was the background: 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__title steps to --text-h2 (the agenda check's precedent for a branded header); .fac-start__value drops from a literal 22px to --text-h2 and its label to --text-meta; cards are 12 by 16; each row carries a ws-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__head puts the name left and .fac-start__stats right-aligned on one row (wraps under the name when it runs out of room). .fac-start__agenda loses its paper box and row dividers; each .fac-start__item is a thin card (8 by 12, --radius-md, 6 gap) whose tint and 1px edge come from data-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-start and must move with them. facilitate-start.js resolves the type the way FacilitatorAgenda.planToAgenda does, plus the meal types, and says it in a .ws-visually-hidden prefix.
  • 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--strategize beside 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.php had gained the platform data-tooltip (opening downward) without losing its own .ws-app-bar__tip span, so both fired on hover in every engine, the span behind and up-left of the pseudo-element. The span and its CSS in components.css are 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. reload is removed from phase-toolbars.php and from the shell's call site, along with the $strategizeStateId it was the last reader of. The dock demo's first slot now wears the canon's assignment glyph; it had kept autorenew.
  • Improved Strategize index and steps. appStrategizeQueue() is index-only and renders the whole queue; .st-newcase (the "+" tile) and .st-hero--boxed are retired from app-shell.css and 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, then em in bold italic --mainsite-dark on an inset 8px --mainsite-light underline; 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 of storage.js.
  • Improved One edge for the rail's promo cards. .app-rail--strategize .app-rail__academy and .app-rail__cards no longer take --phase-learn-very-light as their border; all three keep the card rule (--card-border, 1px #E7E5E4), as Tips always did. In Figma, the three variants of rail-cta (Canvas Design System, set 141:516) have their stroke bound to color/gray/200; Academy and Intervention Cards were bound to color/brand/red-very-light. The library needs publishing for the screen frames in the synthesize file to pick it up.
  • New ws_breadcrumb() items take target (_top, _parent, _blank, _self; _blank adds rel="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 in css/planner/layouts/_planner.css; the bundle's ?v= stamps for that sheet and for components.css are bumped to 20260918a.
  • 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-main takes padding-right: max(48px, 100vw - 1292px), so the canvas is six columns and stops at the 1244 cap. Both columns drop their opaque gray-50 (the palette from 769 up only, where it is not a drawer), so the body's --paper-grid-image shows. Standalone /planner/ is untouched.
  • Improved .sidebar-custom-activity-section takes a 1px --planner-red border and its button reads Custom Activity, with aria-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 to color/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() in facilitator/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; stamp 20260919a): .icebreaker-swap-menu, .exercise-swap-menu and .break-suggestion__menu share --pick-tint, --pick-edge and --pick-ink per 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-h3 in 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-dropdown and pauses breakPulse, which had been fading the open menu to 0.85.
  • Improved ws_header() draws one brand mark, graph_3, on every section. .wsh-header__mark no longer reads the section's icon key, which stays in the registry. header.php is one of the header trio, so it deploys with helpers.php and the merged components.css.
  • New ws_app_bar() takes centerActions: actions in the same shape as actions, drawn in the centre of the bar when no raw center is 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 in facilitator/js/facilitator.js (?v=25) and call what the console already does; discardSession() deliberately skips end_live, so a discarded run is not marked delivered, and deletes nothing on the server. #overlay-options reuses 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--shell wears; .app-crumbs + .app-hero drops the hero's 48 top padding. Facilitate's index is the first caller, and it joins Plan's in $barlessIndex: no ws_app_bar() on either.
  • Improved appRailWaitingCard($rows, $stage) serves Plan as well as Synthesize. appRailJourneySignals() gains plan_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' => true for 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 calls appShellCrumbs().
  • 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 ghost footer link. The shell also stops rendering ws_app_bar() on Plan's index ($planIndex in app/index.php).
  • Fixed The schedule panel's duration pill is white. A leftover #calendarDurationText rule in css/planner/components/_modals.css (600, --planner-gray-900, from the retired calendar modal) outranked .schedule-panel__meta-pill by 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 off strategist.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__head and then st_recap_card() directly; appStrategizeSubhead(), which wrapped the trail and the card in .st-subhead above the title, is removed along with the shell's two .st-subhead rules and .st-tools__head--tight. In app/css/app-shell.css, under .st-page--tool: the head's margin is 0 0 var(--space-6), .st-recap carries var(--space-6) below it, and .st-split .st-panel drops 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.js pins 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 through ws_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 through ws_workshop_list(): two rows and the plus tile, primary buttons, heading from ws_wl_phase_title('strategize')), appStrategizeRail() (the promo pair each frame draws) and appStrategizeNewCaseModal().
  • New strategize/home.php, and ?view=strategize opens it. $strategizeSteps leads with home; a URL carrying id, plan or objection with no step still 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's app-hero card (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 the branded variant the shell's other dialogs wear, one ws_form_row() with a helper that becomes the error after blur and says how to fix it, and ws_modal_footer() with Go directly to Plan (secondary, left), Cancel and the primary. app/js/strategize-queue.js writes a fresh StStorage state with workshopName and nameTouched and opens the sentence on its ?id=: localStorage only, no server write. ?new=1 opens it on arrival and is stripped from the URL. The tile is a ws_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, and gridAppend adds 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.js pins 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), and appShellRail() 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 in rail.php records 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.js fetched 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 / check and App / facilitate / run in App — Screens, completing both phases beside the existing plan / home, plan / build and facilitate / 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 .env points 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.css collapses .sy-intake and .ws-shell to one column inside max-width: 1080px, and a media query adds no specificity: .app-shell .synth .sy-intake carries 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.js now 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.js binds 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 take is-active. app-settings.spec.js clicks it and checks a marker on window survives.
  • Fixed prove/report.php's not-found branch opens the page grid and renders the rail before its early return, which fired above the wrapper the batch-5 change added. Not browser-verified: Prove is switched off locally.
  • Improved A dead max-width: 900px block in settings.css that un-stuck an aside which no longer sticks is gone, and app-grid.spec.js measures all six My Stuff steps (twenty routes), so the open-in-_chrome.php, close-in-index.php split 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-grid lands on below them. .app-page__main .ws-choice-cards { --ws-choice-gap: var(--app-grid-gutter) } in app-shell.css; components.css is 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.js pins 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-settings and app-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 while PROVE_ENABLED is 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) is calc(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-gutter later, so the canvas begins on the third grid line (364). body.embedded-mode is what planner.php?embedded=1 already set; standalone /planner/ keeps its bordered 320 column through --planner-sidebar-width. Both import stamps in planner-new.css bumped to 20260917a.
  • Improved Synthesize's two material rails are on it too. In the ported-sections block of app-shell.css: .app-shell .synth .sy-intake is minmax(0, 1fr) var(--app-rail) at --app-grid-gutter inside 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-shell is var(--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.css is untouched and keeps serving the standalone app; its .sy-app rule turned out to have no caller since the August intake rewrite.
  • New tests/E2E/app-tool-rails.spec.js reads both geometries back: the palette at x 0 and 344 wide inside #appPlannerFrame with 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 / build and App / synthesize / make in 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.php and 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). The else branch in app/index.php wraps .app-shell__sheet and renders appShellRail(…, $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--aside and their 1100px collapse rules. .app-shell__content caps at calc(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-page and 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.php describes 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.json layout.appShell drops readingMeasure, rewrites wide as the only column, and its class inventory no longer lists app-cols.
  • New app-grid.spec.js adds the library route; fifteen routes in all, every non-bleed view but the report (which needs a plan id).
  • New In Figma: App / prove / report and App / library in 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-page in stuff/_chrome.php and closes it in app/index.php after the step require, rendering appShellRail(…, 'stuff', ['lead' => $stuffRailLead ?? '']); the six step files stay rail-agnostic. Contribute renders its preview aside into $stuffRailLead (an ob_start() block above the form) instead of a 320 column: .app-contribute__layout is now .app-grid-6 with the form on .app-span-4 (612, two 296 cards across), and stuff-contribute.css loses the 1120 cap, its own split and the aside's sticky rule (the rail is the sticky element).
  • Improved Settings wraps itself in .app-page and hands its account summary to the rail as the lead, with 'journey' => false. .app-settings__layout is 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-2 still 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/-6 go full-row where the nested grids drop to three and two tracks, for the same reason the page spans already did: span 4 on three tracks creates an implicit column.
  • New tests/E2E/app-settings.spec.js pins the settings shape (tracks 240 + 648, main 928, rail 296, the aside inside the rail and no journey card) behind a ?section=subscription deep link; app-grid.spec.js adds settings, stuff/favorites and stuff/contribute, fourteen routes in all.
  • New In Figma: App / settings, App / stuff (favorites) and App / stuff / contribute in 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.php and check.php open .app-page inside #stApp and render appShellRail(…, 'strategize'), the first views to gain a rail rather than swap one. #stApp and the announcer stay outermost (the JS contract); $wideContent now covers every Strategize step. strategist.css is untouched: .st-split at 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.js measures the four steps beside the sentence, eleven routes in all.
  • New In Figma: App / strategize / kit, / pitch, / practice and / check in 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.php and strategize/sentence.php open .app-page. A wrapper swap and nothing else: each view keeps its own <main>, hero and listing, and Synthesize's static .synth-rail commit bar stays inside the main column. With these, .app-cols has no caller under /app/; the rules stay in app-shell.css until batch 5 deletes them with the reading measure.
  • New app-grid.spec.js measures the four routes beside home and plan, seven in all, with Prove gated on the same feature flag the shell reads (proveEnabled() from tests/helpers/app-routes.js).
  • New In Figma: App / facilitate / home, App / synthesize / home, App / prove / home and App / strategize / sentence in App — Screens, each a copy of the Plan frame re-filled per view: the compact app-hero, three wl-card at 296, and the rail in rail.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.php and plan/saved.php open .app-page instead of .app-cols. Plan home is the batch-1 proof of the arithmetic: ws_choice_cards() and .wl-grid both 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 in app/index.php; the wrapper lives in the app view, never in saved-body.php, which the standalone planner/saved.php still renders railless.
  • Improved The saved body's inline layout is neutralised inside the shell. .app-shell .saved-workshops-page in the ported-sections block of app-shell.css drops the 1200 cap, the 72px auto margin 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.js now measures /app/?view=plan&step=home and &step=saved beside home: eight tracks of 138, main 928, rail 296 at x 948, and no .app-cols left on the route.
  • New In Figma: App / plan / home in the App — Screens section of the templates page, a copy of the App / home frame with the compact app-hero (Surface=Plan), three plan-choice-card at 296, plan-nudge, three wl-card (stage=Plan), and rail-todos, rail-film (view=Plan) and the Daily Tips rail-cta down the rail, in the order rail.php renders 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__main spans six (928) and the rail spans two (--app-rail, 296); .app-page--rail-left puts 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, -3 and -2 inside a 928 main (138 / 296 / 454), .app-grid-4 and -2 at the full width (296 / 612), and .app-span-2/-3/-4/-6/-full. The two that do not close (-4 inside a main at 217, -3 at the full width) are documented as off-grid on app-shell-layouts.php rather than offered. .wl-grid inside 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 and tests/header-shell.spec.js read them as strings. --app-rail-width stays 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.js reads 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.js asserts the four new tokens beside the existing 320 pin.
  • Improved .app-cols and .app-cols--aside marked retiring in app-shell-layouts.php and 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.php loaded qr-creator.min.js without 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.php open .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 $opts for exactly that: 'journey' => false, and 'lead' => $html for 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.js re-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 of app-hero, dash-hero, three wl-card at 296 across the 928 main, and rail-dna, rail-todos, rail-film and both rail-cta cards down the 296 rail, in the order rail.php renders 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 66 helpers.php functions are already registered in the JSON, so nothing on either screen that belongs to the design system is missing from it.
  • New externalComponents in workshopr-design-system.json names the three families that are not: workshopList (24 ws_wl_* functions, 32KB of CSS, 12 .wl-card instances 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 to components: check 1b requires every key there to resolve to a helpers.php function. check-drift has 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-card as a four-stage variant set, agenda-item as a four-type variant set, plus wl-stage, item-duration-badge and category-btn. Geometry and colour taken from the captures: the card ships at 284×175, which is exactly what .wl-grid yields 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 #FEE2E2 on #C73E3E whatever the stage, so wl-stage needs 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-bar Figma component was 72 tall; the shipped bar is 60. Measured on /app/?view=synthesize&step=home at 1440: .ws-app-bar is 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 a MIN vertical 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.php renders the bar only for plan, facilitate, synthesize and prove; /app/?view=library returns no .ws-app-bar at all, and its .app-shell__sheet sits 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__sheet measured at exactly that, inside a .app-shell__content capped 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.php drops its “Known divergences” section for one that records what was measured and what was corrected. layout.appShell in the JSON loses divergences and gains resolved, barRendersFor and 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 in app/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 against app/css/app-shell.css. Mirrored into workshopr-design-system.json under layout.appShell, beside the existing layout.containers, which describes the main site and never applied here.
  • New .app-cols--aside, .app-grid-4 and .app-grid-8 in app/css/app-shell.css, beside .app-cols. They go there rather than in includes/components/components.css for 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-cols 892 + 32 + 320, .app-cols--aside 320 + 24 + 900, .app-grid-4 296 ×4, .app-grid-8 138 ×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-8 is documented as a spanning grid, not a card grid. 138px is barely half the 260px floor .wl-grid enforces, and that floor exists because an uncapped repeat(3, 1fr) once drew 214px cards. Neither new grid has a caller yet: .wl-grid still caps at three on purpose, so reach for these when a design asks, not by default.
  • Fixed page-templates.php claimed “every page follows one of these templates” while describing only the main-site pattern of includes/header.php plus 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 the ws-app-bar Figma component, which puts Figma content slots at y 96 where the app puts them at y 84. Both are listed as open in layout.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-drift still listed ws_lifecycle, retired in v2.9.99, and flagged a card in app/css/app-shell.css setting its own geometry; generate-catalog --check had ws_app_footer(): string against a source that gained array $options = []; tokens.dtcg.json was stale. All fixed — the curated JSON drops the dead entry, carries the real ws_app_footer signature with its variant and launcher options, and both generated files are current at 66 components and 347 tokens. The audit that found them is AUDIT-2026-09-16-system-audit.md.
  • Fixed components.generated.json reported a11y coverage of 11/66 against a real 54/66. The extractor in generate-catalog.php took 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.php is 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.mjs is the missing half, and ds-lint.yml gains a ds-pages job. 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 missing php, 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) became var(--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.md no 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 in includes/components/phase-toolbars.php fixes 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 at components/demos/phase-toolbar.php, full specification at design-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.php and app/views/synthesize/make.php each dropped their hand-assembled items array 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 in planner-v2.js, facilitator.js, synthesize.js or strategist.js changes.
  • 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.php holds 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-dark under --text-inverse, lifted by --shadow-sm over --shadow-md. It positions against the nearest positioned ancestor and lets clicks through the gaps between its pills. Demo at components/demos/app-dock.php; the JSON catalog carries the entry.
  • New A slot badge on ws_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 tone on ws_app_dock(): success paints 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 now position: relative and pads the scroll region by the clearance under a has-app-dock class.
  • Improved The .agenda-gen-6c sequence strip and the .agenda-gen-c step chip are retired from css/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-strip is a paper pill that rises above the dock (its foot sits at --app-dock-clearance plus --space-2, one z-index step above --z-sticky) holding a 24px drain ring in --fac-red and four ghost ws_button()s whose 1–4 key hints are drawn from data-key with ::after, because the button template escapes its label. .fac-moment rows sit beside .fac-note in the sidebar and the summary with a tabular-nums session clock and a ws-pill--tint tag. The orphaned .fac-toast rules are gone; the console\'s confirmations come from the shared wsToast(). All motion runs on the platform durations and easings and switches off under prefers-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 with overflow: 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 a 360px + 1fr grid, the pitch on the left under an .app-hero--dialog variant 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's 1400px measure and the column disappears without a line of JavaScript. One column below 1100px. The report screen is condensed in the same scope: ws_planner_card--lg gives up its 96px floor and takes tighter padding, the issue rows drop from 20px to 12px, and the energy-arc score steps from --text-h1 to --text-display, none of it leaking outside the dialog. The report sheet itself is 1120px rather than the page's 1400px, its rail 420px rather than 630px (at the module's own 1024px breakpoint, so narrow widths keep their single column), and it caps at 660px tall 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. Adding prove to ws_workshop_list_stages() is the whole change: the stage pill, the chips, the kanban's fifth column and the fifth dot on ws_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.css is back, and its print contract now hides .prv-owner-only along 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.css for 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.css for the two public commitment pages, commit.php and checkin.php: a stranger on a phone with one job, built from tokens only. The facilitator's own panel is a separate .wcm-* block appended to facilitator/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-stepper is the same control and is the next thing to move onto it. As with every component here it ships no JavaScript: the row carries data-ws-stepper, data-step, data-min and data-max, the buttons carry data-ws-stepper-dec / -inc, and one delegated handler on the page serves every stepper on it.
  • Improved ws_modal_footer()'s primary slot accepts variant, defaulting to primary. The one other value with a job is danger. 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 bolted ws-btn--danger onto 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.0156em to -0.0469em: .dash__hero-title and .wl__title both render at 25.6px on the same page and carried -0.6px and -0.512px, while .app-shell__title sat at more than twice the norm. Written as a ratio it cannot drift again.
  • Fixed .app-rail__film-title is 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.typography names card titles as heading-font at severity mandatory, so this was a rule break rather than a preference.
  • Fixed .ws-dna-profile-cta__text is tracked at 0.08em. Uppercase at 0.01em was 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 the nowrap holds.
  • 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-meta now, which takes the card from five type levels to three. The journey strip stays at --text-micro and is the deliberate exception: at three columns it needs 238px of a 234px box at the next step up.
  • Improved .lp2-nudge__desc takes the 62ch measure that .app-shell__lede already 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.php accepted any var() 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 was stripos($value, 'var('), so border-radius: var(--radius-2xl) passed. --radius-2xl is 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 -card stem, so .app-settings__card read as an element like .ws-card__title and 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-8 directly because there was nothing to reach for.
  • Improved Five more surfaces join the card family: .app-settings__card (was --radius-2xl and a raw --space-8), .app-stuff__row (was --radius-lg with 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-checklist draws the card tokens instead of a private copy of them. It hand-rolled --bg-surface, --gray-200, --radius-2xl and --space-6 where --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 beside ws_card surfaces it was the one object with 16px corners.
  • Fixed The app rail's film and Tips cards stop overriding --card-padding. Both set 16px 20px over the .ws-card--content inset, 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-card stays flat platform-wide, which is deliberate, and --elevated remains 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__bar floors 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-shell then applied zoom: 0.8 on 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-nano on 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.css even carries a note reasoning about the rem 0.8 from inside the file whose body rule was applying the second one. The zoom is 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: 125dvh on the shell body and the two sign-in minimums return to 100dvh (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_card fills 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 outline ws_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-faint is annotated in platform-tokens.css as "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-accessible at 4.6:1, and --text-micro over --text-nano.
  • Improved The workshop card title moves from --text-h4 to --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.typography names card titles as heading-font, severity mandatory.
  • New featureFirst on ws_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-page with --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 failure platform-tokens.css documents 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.css and prework.css all wrote background: 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-page in app/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-shell stays white by the note already in the file — the paper lives on main, and the body is never visible behind a 125dvh frame with overflow: hidden.
  • Improved The Admin console joins the platform grid. css/admin/admin-v2.css had a bespoke background: its own --paper at #FAF6EC, a 24px grid at rgba(38, 30, 18, 0.045), and a second 96px accent grid over it. It now takes --bg-page and --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.php and planner/templates.php carried a fixed body::before painting a second 24px grid in --color-border at 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.php and tips/vote-confirm.php render 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 links platform-tokens.css explicitly through the same localhost/www switch it already uses for components.css. The tokens did reach these pages before, but only through an @import inside 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, matching css/styles.css and _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 sets html { font-size: 80% } and would rescale their whole layout, so each mirrors the four values in its own :root with 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-small was labelled 14px and renders 11.2px, --text-h1 was labelled 32px and renders 25.6px. Each token now reads paints 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-6 and --space-8. Five call sites in app/css/app-shell.css, css/stsh-shell.css and includes/components/components.css already referenced it with a 28px fallback, so the step existed in practice and not in the scale.
  • New check-drift.php reports the ws_ namespace being claimed outside the design system. A function named ws_* announces itself as part of the system, but if it lives anywhere other than includes/components/ none of this gate's checks reach it. Six files hold 33 such functions; app/includes/workshop-list.php alone 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-rec is restated as a platform card (agenda-check/agenda-check.css): var(--bg-surface) behind a var(--gray-200) edge at var(--radius-xl), replacing the cream-and-orange row it inherited from css/planner/prep.css, whose --warm-cream was 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-left sat under the border shorthand 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) behind var(--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 was info blue and said "beta feature" where it meant "paid".
  • Fixed ws_badge() draws icons the set has no SVG for. It called icon() alone, and icon() answers a name it cannot find with an empty span, so a font-only name such as workspace_premium rendered as a blank gap inside the badge. It now falls back to the Material Symbols ligature, the same guard ws_button() already carried. Only names that had no SVG change.
  • Fixed ws_app_bar() tested the wrong thing. It branched on whether icon() 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 its Ask toggle, the thread's two bubble sides (.cv-find__msg--agent on white behind a var(--gray-200) edge, .cv-find__msg--user right-aligned on var(--phase-learn-light)), the starter and answer chips, a three-dot .cv-find__typing indicator that holds still under prefers-reduced-motion, and the result group: a .cv-find__best label, an italic .cv-find__why line, 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-chip rather than a new numeral button; the current page is that chip's .is-active state. The one addition is .cv-chip--disabled, 0.45 opacity with pointer-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 a now carries var(--phase-learn-dark) and var(--font-semibold). The attribution footnote is var(--gray-100) under var(--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 in var(--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:hover keeps 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/ to css/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 to var(--text-display) on one line, and closes the gap below to var(--space-5). The 48px above is deliberately untouched: .app-rail opens level with the hero at that same value, and moving one without the other steps the two columns apart.
  • New chips option on ws_workshop_list(): whether to draw the stage filter chips, default true, with filters kept as its older name rather than added as a second switch that could disagree. With the chips off, filter stops 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. The footer option gained a variant key, 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, and workshop-list-menu.js clones 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_extra on 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. Also counts, carried through from the Workshop Record so a page can build its own line without a second query.
  • New badge on a ws_task_checklist() row (includes/components/task-checklist.php): a short count rendered as a quiet ws_badge pill 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 short on a ws_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--short is hidden by default and the layout picks which one shows, and the item takes the full label as its aria-label so 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-card and their two media queries were the last hand-rolled workshop card on the platform; Prove renders ws_workshop_list() now, so they went, and the card-border rule in app-shell.css stopped 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--primary now has its own hover and focus state. Rest, :hover and :focus-visible shared one declaration in app/css/dashboard.css, so the one emphatic button on the app home page never moved under the cursor or the keyboard. Hover and focus take var(--phase-learn-very-light) as the fill and keep var(--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-shot prompt(); either outcome retires the button for the visit, appinstalled retires 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 before DOMContentLoaded); 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-wrapper had no position, 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 a hidden class 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 .hidden utility.
August 2026
v2.14.7: ws_choice_cards counts columns from its container
  • Fixed ws_choice_cards is no longer laid out against the window. The set was repeat(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 at zoom: 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 now max(240px, (100% - 2 * gap) / 3) with auto-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. --2 and --1 keep 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_bar now carry a tooltip. The component's docblock has always claimed tooltip behaviour, but an icon action shipped with aria-label alone: named for assistive technology, silent to a pointer. Each one now also gets data-tooltip from the same label plus data-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 own data-tooltip in attrs keeps it, and labelled actions are untouched (their text is already the name). No CSS was needed: the bar was already overflow: visible and 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 at images/icons/settings_light.svg. The stock weight-400 settings glyph 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__label drops text-decoration: line-through and keeps the --text-muted shift; 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's text-decoration: none reset went with it, being redundant now.
  • Improved The card's action right-aligns. .ws-task-checklist__cta becomes a flex row justified to flex-end, so the button lands under the rows' arrows at the card's trailing edge. Consumers that ship no cta (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-step description or phase, and progress => 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-art carries 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 in images/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 goes 200x150 fixed to 216px wide with height: 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__text goes 14px/17px to 16px/20px semibold on a single line. "Your DNA Profile" and "Discover your DNA Profile" lose their <br> and gain white-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_cta medallions are transparent. The five images/dna/profile/medallion-*.png exports carried an opaque #F5F5F5 plate, 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's filemtime cache-bust carries the new files.
  • Improved .ws-dna-profile-cta__text goes 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 sm to default (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 original sm.
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 are ws_select and ws_input (textarea type included), footed by ws_modal_footer; it posts JSON to the existing api/contact.php with 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, no aria-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, and data-app-footer-launcher plus the launcher option 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 to images/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-light fill, --phase-learn-dark border and type, --radius-lg. New demo page at design-system/components/demos/dna-profile-cta.php.
  • Fixed The workshop listing's [hidden] attribute now always wins. .wl-card's display: 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 in workshop-list.css closes 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 footer link on ws_workshop_list() now renders through ws_button() as a small secondary variant with a right-hand arrow_forward icon, 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__all survives 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 filter option on ws_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 own stages scope 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 hidden on the same rows is how a card goes missing; the replacement is single ownership: on a paged listing, workshop-list-pages.js is 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 with pageSize rows 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): the linear-gradient from --phase-learn-very-light down to --card-bg is gone; the card now paints a flat var(--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 sets margin-bottom: var(--space-6), replacing an inline style="margin-bottom:0" in app/views/plan/home.php that left the choice cards sitting directly under the subtitle. The standalone planner home keeps --space-10 there, which is a page-opening gap rather than a heading-to-cards one. Note the shell renders under zoom: 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__body paints the drafting paper (app/css/app-shell.css): --bg-paper plus --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 reason platform-tokens.css gives at the token: the background: shorthand wipes background-image, which is how five pages have silently lost the grid before. Scoped to this dialog, not to .ws-modal__body at large: the other Plan dialogs are forms, and a form on grid paper reads as a mistake.
  • Improved .ac-sample-chip is 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 renders ws-btn ws-btn--secondary ws-btn--md, the same vocabulary as the "Check my agenda" primary two rows above it. The ✨ became a ws-btn__icon holding Material Symbols auto_awesome, since a raw emoji inside a design-system button was the tell that it was not one. #acSampleChip stays: agenda-check.js toggles its hidden as 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 in images/dna/profile/. Deliberately chrome-free: the artwork carries its own border, padding and cream ground, so a ws_card wrapper would have drawn a second frame around the first. The image holds its slot with aspect-ratio: 27 / 10 rather 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__dna row 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_cta still 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 a full variant: 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 $appFooterStylesOnly so a documentation page can borrow the styles without consuming its one footer.
  • New ws_task_checklist() gains cta and a designed complete state. The in-card action renders through ws_button below 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's branded variant renders. The JSON has documented variant: branded since the catalog was written, but only the per-call headerColor path 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 before headerColor.
  • New The workshop listing pages. ws_workshop_list() accepts pageSize: rows past page one ship hidden, workshop-list-pages.js draws 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-hero copy, 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-bulkbar ignored its own hidden attribute. The block sets display: 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, because workshop-list-select.js returns 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. hidden sits on the list and confirmation wrappers, not on the buttons within them, so the popover-wide focus query in workshop-list-menu.js walked 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 in api/plans.php, every one of them scoping its query to user_id = ?, so the menu now omits them on a shared card and offers Remove from my workshops instead, backed by remove_collaborator, which gained a plan-scoped self-service branch: one account can hold more than one accepted row on a plan (the unique key is plan_id + email), and dropping a single row would leave the workshop on the list. A row with no owned key reads as not owned, matching ws_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__groups is a five-column grid. The launcher now lists Learn, Strategize, Plan, Facilitate and Synthesize, and the grid was still repeat(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__panel widens 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 at zoom: 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__name now reserves var(--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 in workshopr-design-system.json were all removed with the MyWorkshopr consolidation. It shipped as a second, older answer to a question ws_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-lifecycle CSS in components.css stays — planner/includes/views/saved-body.php still 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 of myworkshopr/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_input for the eight fields, ws_radio_card_group for difficulty and the category picker, ws_chip_input for materials, ws_icon_button for the duration stepper and the step deletes, and the platform Toast in place of a hand-built .ws-alert template string. New sheet at app/css/stuff-contribute.css, scoped to .app-contribute and painting entirely from platform tokens — the retired --exercise-accent orange is the one Learn red now, per Red Unification. The fixed right-hand drawer became a sticky column: the shell draws at zoom: 0.8 inside its own scroll region, where a viewport-anchored drawer lands against the wrong edges.
  • Improved The myworkshopr header section is retired. _wsh_sections() in includes/components/header.php lost the key and its [data-section="myworkshopr"] tag-width rule, WS_APP_PATHS in helpers.php lost the path, and the nav inventory at includes/nav-example.php lost the row. Every /myworkshopr/ URL is a 301 that renders no header at all, and an unknown section key already falls back to home, 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-input now 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-gradient starts 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-gradient and --planner-topbar-bg now 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.css rebuilt the gradient from --mainsite-primary rather 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__btn hovered purple. It read var(--color-primary-dark, #651FFF), and neither --color-primary nor --color-primary-dark exists in platform-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 into components.css and was tokenised. It also referenced var(--text, ...) and var(--display, ...), neither of which is a token, so both always rode their literal fallbacks; they now read --font-body and --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 #FFFFFF literals 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-standalone and the .pv-toggle block, 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_recorder and ws_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-visible indicator in platform-tokens.css (WCAG 2.2 SC 2.4.7, Level AA). The file defined --focus-ring and documented at length why it is sky blue, but nothing applied it: the site's only global rule lives in css/styles.css, which a page gets only through includes/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-option and .ws-choice-card drew no focus ring at all. Both wrote outline: 2px solid var(--focus-ring, #38BDF8), but --focus-ring is the whole shorthand, so the value expanded to 2px solid 2px solid #0284C7 and was discarded as invalid. Both now read outline: 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__remove was 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__close went from 22x22 to 24x24. --target-min: 24px already existed, annotated with the SC number, and nothing was using it.
  • Improved css/tips/styles.css imports platform-tokens.css, as css/dna/styles.css and 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--primary is 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 in index.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 with ws_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 #7C4DFF and Reflect emerald #10B981 survived as hardcoded fallbacks behind --phase-plan, --phase-facilitate and --phase-reflect. Those tokens are #E54D4D today 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 body takes --paper-grid-image / --paper-grid-size instead of re-typing the two gradients. --grid-line-color is rgba(44, 36, 22, 0.06) and the size is 24px 24px, exactly what the page had, so this is identical pixels; the token also resolves to none under 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-card onto the card tokens. Moving the CSS into a .css file made it visible to check-drift.php for the first time, which immediately failed it on padding: 24px. It now reads --card-padding (20px, a real 4px change), plus --card-radius and --card-shadow in place of --radius-xl and --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 using components.css (footer.php 269 lines, header.php 159, section-hero.php 109, dna-cta.php 39, app-footer.php 5), 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 of app/views/*.php into app/css/app-shell.css: the .modal-overlay FOUC guard, .lp2-nudge*, .prvh-* and .pwa-install-banner*. Inline CSS in a PHP file is invisible to check-drift.php, which filters on the .css extension, so the card rules in those blocks had drifted to border-radius: 12px; padding: 18px with nothing to catch them. They now read --card-radius and --card-padding, and the card hover reads --card-shadow-hover instead 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 of app/css/synthesize-home.css, and dashboard.css's .dash__row section were each orphaned by the same event: the page moved to ws_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-page deleted 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 eleven hardcoded-hex violations the file carried in page-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 own clamp() 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.php reads <style> blocks but only scans them for hex literals, so card geometry written inside a PHP file is still ungoverned. See AUDIT-2026-08-22-app-css-localization.md.
August 2026
v2.9.93: The 6C sequence strip and step chips
  • New .agenda-gen-6c in css/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 ::before hairline 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-dark dot with a --planner-bg-white ring punching it out of the spine; a missing step gets a hollow dashed --planner-gray-400 dot and its minutes column turns red. Grid is 16px 1fr auto so the minutes stay right-aligned and tabular-nums keeps them from dancing.
  • New .agenda-gen-c: the per-row step chip (Connect, Collect, Choose, Create, Commit, Celebrate) sitting next to ws_activity_badge in the generated agenda row head. Brand red on --phase-learn-very-light with a --phase-learn-light border.
  • 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-tabs in css/planner/components/_modals.css: the modal's ws_tabs nav, flush under the modal header with a --planner-border rule beneath it. The panes are stock .ws-tab-pane, and switching runs on the existing wsTabsInit/wsTabsSwitch pair in includes/components/components.js rather than a second implementation. Note that components.js already auto-runs wsTabsInit over every .ws-tabs on DOMContentLoaded, so callers should only ever call wsTabsSwitch; calling wsTabsInit again double-binds the click handlers.
  • Fixed Ghost icon buttons in an app-bar trail were reading dark on the red bar. ws_icon_button renders a .ws-btn--ghost, which inherits the default warm ink, while every .ws-app-bar__action beside it is --text-inverse white. Added .workshop-planner .ws-app-bar__trail .ws-btn--ghost to _component-bridge.css giving 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-section and .planner-settings-section__title from css/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-note stays (it still styles the signed-out and no-workshop captions) and .deck-settings-hint is 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-field elements 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-border rule), .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 standard modal-planner.php shell with ws_input/ws_select/ws_button fields, and the topbar gear is a stock ws_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 vocabulary ws_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-lifecycle and .ws-lifecycle__steps gained flex-wrap: wrap, a max-width: 100% ceiling, and a row-gap token 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 at index.html) rehearsing the unified app shell: white global header, dark icon rail, red per-app bar, and a bottom action rail. Every value is a var() against platform-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 to ws_* 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() and ws_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 pass href, and brings the behaviour with it.
  • Improved --card-padding now 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-card and .ws-card--content now read the card tokens instead of hardcoding their own geometry.
  • New Check 4 in check-drift.php: a card rule that sets padding or border-radius with a literal instead of a token fails the build. Ratcheted against card-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, __element classes and .card-shelf do 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 through ws_button() rather than hand-cut. Built from the synth-action-bar frame, 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-motion nothing moves at all. Pass your own notes, 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-light paper, --color-warning-light edge, --color-ink-secondary ink, --color-ink-faint rail.
August 2026
v2.9.86 — The app bar learns which app it is in
  • New 'state' option on ws_app_bar(): strategize, plan, facilitate or synthesize. 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 matching state variant.
  • New .ws-app-bar__control and .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 center or actions and 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 on ws_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 by syncRail() in synthesize.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 the data-act values the delegated handler already had, so each action still has exactly one implementation.
  • Fixed A surface outside #synth-app needs its own data-act listener. 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-shell is 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 as null, 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 borrowing includes/data.php's formatDuration() — that file calls loadFromDatabase() 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 in app/css/workshop-list.css, behaviour in app/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.php row plus lifecycle_status and 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-test runs the parser fixtures, including the historical var(--space-7) case that shipped a collapsed padding; accepted one-offs go in $KNOWN_DRIFT as css-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/xs scale (29 declarations were inheriting instead of sizing), the orange card/badge family plus two legacy surface and ink names in styles.css, admin's --admin-accent-* variants, MyWorkshopr v3's neutral vocabulary, --planner-primary-dark → --planner-red-dark, --color-amber-700 defined beside its existing --color-amber-100, the unused .mw-svg-icon mask block removed, and the bounce-in animation riding --ease-out now 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-token ws_* 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 in css/stsh-shell.css, and a document-head layout at includes/components/stsh-header.php (registered in check-page's LAYOUT_FILES). Born in the Strategist; extracted when Synthesize adopted it. The Strategist's st_* helpers now delegate here, and the /app/ frame deliberately never loads it — its own sidenav is the chrome there.
  • New .stsh-canvas--app variant for viewport-locked tools: the canvas stops scrolling and hands its box to the app inside, keeping the app's own #main-content sizing 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 standalone synthesize/app.php entry's legacy/anonymous landing and boot-failure fallback. What's left carrying the in-shell screen is entirely shared platform pattern: the ws_app_bar, the .ws-tabs component 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-theme and 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.php with 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 real ws_header(), ws_button(), and ws_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-400 rail (template includes/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 load components.css. Carries aria-hidden="true": an empty contentinfo landmark 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 direct components/footer.php include and the legacy includes/footer.php all funnel through the composite template, so if (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 against WS_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 owns SCRIPT_NAME, so the real page is only visible via REQUEST_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-shell takes zoom: 0.8. This is the second attempt at a smaller frame and it is deliberately the other mechanism. zoom scales 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, 128 var() fallbacks and 45 literals put back.
  • Fixed The body height is 125dvh, not 100dvh. zoom scales the box after layout, so 100dvh is 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 #A8A29E border in the shell — --gray-400, four steps up from the --gray-200 it 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-card and .synh-pickup are one object seen from five places, so they are named together in app-shell.css rather than edited in four files — which also leaves the standalone pages on the --gray-200 they share with the rest of the site. .dash__viz, .dash__ftrack and .dash__empty keep 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-200 to --gray-300; against a --gray-400 rest state the same rule lightens the edge, so the card receded at the moment it lifted. .app-shell .dash__row:hover takes --gray-500 and 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__eyebrow pill 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-dark carries 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-header is 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.css rather than the component because ws_header draws 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-shell token 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-body resolved to 0.67rem against the platform's 80% root, so body copy painted at 8.6px. The tokens are inherited from platform-tokens.css again, which restores every ws_* component rendered inside the app at once.
  • Fixed The literals that were scaled to match are back too — 128 var() fallbacks across app-shell.css and dashboard.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_header override 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-hero takes padding-top: 48px, and Strategize's .st-hero takes 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-hero onto .app-hero inherits it from the one rule.
August 2026
v2.9.69 — The sentence step opts out of the shell's reading measure
  • Improved .app-shell__content widens 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__lede uses — 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 forced zoom was 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. Instead body.app-shell redeclares the spacing, radius, type and icon tokens at 0.67, which carries every var(--space-4, …) in app/css and every ws_* 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_header is 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.css restates 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-indicator moves from --phase-learn to --phase-learn-light and --ws-sidenav-on-indicator from --phase-learn-light to --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 in app-shell.css — ws_sidenav itself 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-learn the Strategist passes to headerColor, same white title, same translucent close. It is bespoke markup rather than a ws_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__groups so a long list travels under a header that stays put, which is the split ws_modal makes with a flex-shrink:0 header 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__close on a coloured header exactly: no border, no fill at rest, a 70% white glyph, and a 10% white wash on hover. The quarter turn is ws_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__row is 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__list is a flex column, the row is one line of icon → body → pill with the pill taking the slack on margin-left:auto, .dash__phases runs 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__cta rule now matches the one-line rule's specificity — an auto margin beats justify-self and had stranded the pill on the far right of its own row.
  • Improved Every dashboard surface moves to --bg-paper from --bg-surface — .dash__row, .dash__viz and .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_checklist carries 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__journey and .dash__firstrun and 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__cta reads "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 done in journey order, not its lifecycle stage. Marks are the shell rail's own, so a row and the rail agree: Strategize knight, Plan calendar_view_day, Facilitate co_present, Synthesize lightbulb. Strategize was lightbulb_2 in the rail, a near-twin of Synthesize's bulb; it is the knight in both now, which also picks up the knight-fill.svg active state added in v2.9.64.
  • Fixed Facilitate rows rendered an empty icon tile. co_present is font-only and icon() returns an empty placeholder for it — the same trap ws_button hit in v2.9.65. appDashboardIcon() in app/includes/dashboard.php takes the SVG when there is one and lets the font draw the ligature otherwise.
  • Retired .dash__q is 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 are ws_button primary, so the dashboard carries no bespoke button CSS at all. .dash__quick stays as the flex row that lays them out; the promotion ledger on design-system/redesign/dashboard.php records 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 than images/icons/, and icon() returns an empty <span> placeholder for anything it cannot find — so co_present, lightbulb_2 and 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 fallback ws_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.svg asset wins when one exists, and everything else falls back to the Material Symbols font glyph with the FILL axis on, which the platform's font request already loads. images/icons/knight-fill.svg ships for the custom knight. Template: $renderActiveIcon in includes/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 a ws_* 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: 0 because ::after otherwise 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 from includes/components/helpers.php like every other ws_* 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 at includes/components/sidenav.php, styles under .ws-sidenav, documented at Redesign › Side Navigation. It pairs with ws_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.css outrank 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) and expanded (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 onSecondaryContainer was 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-ink at 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-learn at 3.82:1 against white; it is now --phase-learn-dark at 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 a forced-colors mapping to Highlight.
  • New images/icons/knight.svg — a chess knight for the Strategize entry, drawn to the house 24px / 0 -960 960 960 convention and checked for legibility down to 20px. The platform's own Strategist glyph is strategy, 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 sets display on a element it also toggles with hidden.
  • 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=expanded or ?both=1.
  • Fixed The rail clipped its own tooltips and grew a stray horizontal scrollbar. The list was set to overflow-y: auto with a comment claiming overflow-x stayed visible — CSS does not allow that, and computes the second axis to auto as soon as the first scrolls. Only the expanded variant scrolls now; the rail is overflow: 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/lg rather 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 in images/hero/ under the <app>_hero.png convention. 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-2xl with 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 #FAFAF9 ground, it returned a warm cream #FBEDD9 every 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__hero becomes a two-column grid (minmax(0,1fr) + minmax(0,26rem)); the old max-width moved 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 carries alt="": it restates the h1, 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__close now twists its icon a quarter turn on hover — --duration-base / --ease-out micro-motion, guarded under prefers-reduced-motion. Applies to every ws_modal platform-wide.
  • New The Strategist header now loads components.js — it always had components.css but not the runtime, so wsModalOpen/wsModalClose (focus trap, inert background) didn't exist there. Its "Other…" sentence modal is the first consumer, using the headerColor red header per the modal demo.
  • Improved strategist.css polish pass: panel hints and the practice page's reading text sit at --text-body (labels stay small); .st-toolnav retired with its markup; the exit strip steps up to --text-h2 with a --mainsite-dark border; the generated 60-second pitch renders as a --mainsite-very-light card; tool-card CTAs anchor bottom-right; .st-panel__hint--wide lets 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.css gains a marketing block (.st-land) for the new overview page, plus .st-page--tool, .st-subhead, .st-navlink and .st-toolnav for the tool pages. Platform tokens throughout; the only functional color is the unified Learn red.
  • Fixed A max-width: 40ch on the landing hero crushed the whole column to roughly 320px. ch resolves 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, where ch measures 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__title is the h1 on 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-width arrived with the app-shell metrics and was consumed bare — grid-template-columns: var(--app-rail-width) 1fr 300px. A bare var() that resolves to nothing is invalid at computed-value time, so the whole declaration is dropped and grid-template-columns reverts to none; 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 to 320px 804px 300px with all three columns side by side.
  • Fixed The same bare use existed in synthesize.css and css/planner/base/_variables.css, so Synthesize and Planner carried the identical latent collapse. All four uses now carry the , 320px fallback.
  • 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 with max-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 with filemtime() 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.php is included on every design-system page, they were dead from every page, not just one. The nav listed each component as components/<name> while the pages actually live at components/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 on component-library.php was unaffected — it already used the demos/ path, which is why the break was easy to miss.
  • Improved The nav slug now 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_page updated 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 dialog inert — 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 in tests/modal-focus.spec.js.
  • Improved ws_alert() / ws_banner() now document their HTML-slot contract: $message renders unescaped by design (callers pass <strong>, links) — user-supplied data must be escaped with htmlspecialchars() 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.php and admin/system/banners.php (6 each), facilitator/app.php (6). Note icon() emits a bare <svg> with no <title>, so <?= icon('close') ?> inside a button leaves it unnamed.
  • Fixed check-page.php counted var(--token, #hex) fallbacks as hardcoded hex — the same defect ds-lint.mjs had 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 carries alt="" (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 any alt that follows a PHP attribute. Real numbers: 1,133 <img>, 40 without alt, 39 of them in ad creative, ebook HTML, or regex literals that merely contain the text <img.
  • Improved All eight TODO: no token match markers cleared from components.css with 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 #FFFBEB vs 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 under ws_header across Planner, Facilitator, and Synthesize; tokens --app-bar-height, --app-rail-width (320px unified rails), --header-bg, --paper-grid-image; --bg-page moved to #FAFAF9 with the drafting grid; ~575 lines of shell CSS in components.css.
  • New DESIGN.md §12.8 written. synthesize.css and the ws_app_bar docblock 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 one ws_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), and platform-tokens.css.
  • Fixed Stale references to the deleted global-header.php removed 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 one enterResults() — 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() called focus() in the same tick as the .is-open class, but .ws-modal-overlay animates in from visibility: hidden and 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. New wsFocusWhenVisible() waits for the transition. One frame isn't enough: the overlay reports visible at frame 2 while still at opacity: 0.01 and only becomes focusable once painted. Confirmed in headless Chromium, not the preview pane — the pane is itself visibility:hidden and can't measure this.
  • Fixed wsModalClose() now restores focus to the element that opened the modal, guarded on isConnected. Previously focus dropped to <body>.
  • Fixed closable: false now blocks Escape — the template emits data-closable="false" and the document handler honours it. Backdrop dismissal stays governed by backdrop.
  • Fixed wsFilterBarRemove() is now defined in components.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, resets page, preserves the rest of the query string and the hash.
  • Fixed ws_progress omits aria-valuenow when indeterminate, per the ARIA spec — it was announcing a definite percentage for work of unknown length. Both progress animations now honour prefers-reduced-motion, matching ws-skeleton.
  • Fixed aria-atomic moved 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_select renders 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 an aria-label when addText is omitted, so the icon-only form isn't an unnamed button.
  • New tests/modal-focus.spec.js covers all three modal fixes. Note strategist/tests/sections.spec.js is separately broken — it requires js/modules/sections.js, retired in v2.9.51 — so a bare npx playwright test fails before reaching anything else.
  • Improved Corrected a claim in the previous entry's doc pass: ws_chip_input's label is properly bound via for. 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 in templates/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, and helper — none are read by input.php. Its select example also passed flat value => label pairs, throwing Cannot access offset of type string on string; ws_input(type: 'select') wants a list of arrays, unlike ws_select().
  • Fixed Navigation described a data-property attribute and .property-switcher-* / .ps-* classes that exist nowhere in header.php. Rewritten against ws_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 click only. 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, or aria-labelledby, leaving the control unnamed.
  • Fixed Retired phase hexes removed from Level Up Grid (#0284C7, #7C4DFF, #10B981), Navigation (plus #F87171, #BE123C), Button's var() 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: showHeader and scoreBadge.variant on Energy Arc Card, group_by_phase and phase_copy on Level Up Grid, meta on Planner Card, and the phase variants on Banner. Energy Arc Card's theme default corrected from dark to light.
July 2026
v2.9.51 — Strategist madlib rebuild
  • New strategist.css rewritten 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-blank is a documented strategist-local control (like st-tabs was): an inline sentence blank that ws_input/ws_select can't express — they render labeled block fields. Baselined in check-page; if a second app ever needs inline blanks, promote it to a ws_* component.
  • Fixed Retired the three-phase shell CSS (docbar, phase tabs, steplist, wizard nav, readiness meter, rail). The st-brief/st-doc document blocks stay — brief-view.php renders 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_right and more_vert — 22 of the 58 controls on the results screen, all sharing two names. New disclosureAttrs(what, open) emits the label and aria-expanded together, 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 is icon('close') announces as "close" — the wrong verb for a destructive control, and identical on every row of a list. New removeLabel(kind, text) in synthesize.js gives 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() in synthesize.js now emits role="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 via aria-labelledby rather 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 because render() replaces root.innerHTML wholesale, 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-hint in css/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-only spoken form, since ⌘Z does 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 #agendaItems now carries role="list" + aria-label, and each item an aria-describedby naming 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, via parse_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 /synthesize and an activeHref of /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 #anchor hrefs 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 activeHref must 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() emits aria-current="page" on the nav item matching the current path or the page's activeHref. 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' => true flag paints a section's call to action red — it is not a location marker, and aria-current deliberately does not follow it. Setting it on a plain destination lights two items with two different meanings, which is what happened on templates.php. Use activeHref for location.
  • Improved The planner section'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 to brief/.
July 2026
v2.9.44 — Planner: named the Energy score, retired the mixed badge row
  • New .energy-arc__score-label in css/planner/components/_canvas.css — a quiet caption naming the score pill, which otherwise rendered as a bare number discoverable only on hover. Carries aria-hidden, since the button's own aria-label already names the score; without it screen readers hear "Energy" twice.
  • Improved .lp2-option__badge now 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-btn in css/planner/components/_modals.css — the Undo control inside the agenda delete toast. Inherits the toast's white-on-blue (~8.6:1), and uses display: inline-flex so min-height/min-width: 24px actually applies; on a plain inline button the min-height is ignored and the target lands under the WCAG 2.5.8 floor.
  • Improved modal-planner.php now emits aria-label="Close {title}" and type="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 carried role="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() in planner-home.js mirrors it exactly rather than inventing a second error style.
July 2026
v2.9.42 — Synthesize modals: entry animation belongs to opening
  • Fixed .synth-overlay's synth-fade and .synth-modal's synth-pop restarted on every state change behind an open modal, because Synthesize re-renders by replacing the whole of root.innerHTML — the modal visibly flashed and reopened. New .synth-overlay--settled sets animation: none on an overlay that was already on screen when the render started (snapshotOverlays() in synthesize.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 .menu and 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-rail drops its border and radius and stretches to the full height of the frame (.sy-app moves from align-items: start to stretch). It is no longer sticky: 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-height resolves against nothing when every ancestor is height: auto, so .synth-stage .synth is now a flex column and .wrap--app / .sy-app pass height down with flex: 1.
  • Improved .wrap--app loses its top padding and trims the inherited 110px bottom pad to --space-6; .sy-main goes to padding: var(--space-6) 0 so 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's padding shorthand resets it at equal specificity.
  • Improved .prio-tray moved 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.css was already on every page via includes/header.php — the components were present all along, just unused.
  • New wsAlert, wsEmpty, wsSkel, wsBadge, wsPill in synthesize.js, beside the existing icon(). They mirror the markup includes/components/*.php produces, because the PHP helpers can't be called from a client-rendered body. Keep them in step if those templates change.
  • Fixed The .tab collision is gone rather than worked around. Synthesize needed an explicit reset of the global pill rule's radius, shadow, shimmer ::before and hover lift — same specificity, decided only by source order. Adopting .ws-tabs--underline removed 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-badge and .cat-pill set a background inline from app state, so the rules that remain do nothing but keep the text legible on top of it.
  • Fixed .ws-skeleton sets no display, 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 no ws_* helpers to demo; the page renders its class names directly against synthesize/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.php gains 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 real ws_segmented.
July 2026
v2.9.38 — Synthesize app frame: rail, working surface, AI panel
  • New .sy-app replaces .sy-work as the shell every Synthesize working screen wears: var(--app-rail-width) rail + minmax(0, 1fr) surface, widening to a third 340px column for the AI panel above 1440px. .sy-work__main is now .sy-main and keeps the drafting grid on var(--bg-page).
  • New .sy-ai — a panel that is a grid column on wide screens, a fixed right-edge sheet below 1440px, and a bottom sheet below 1080px. 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--add column.
  • Fixed .synth .tab now resets the global .tab rule'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-row in 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__main keeps the drafting grid but takes var(--bg-page) (#FAFAF9) instead of the warmer var(--bg-paper), so the centre column and the page behind it read as one sheet rather than two paper stocks.
  • New .sy-rail--add is now a panel on var(--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), --rCard radius, --space-5 padding — 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 from var(--planner-very-light) instead of a hardcoded rgba(229, 77, 77, 0.08). Same red family, now a solid #FEE2E2 that matches the soft fills used elsewhere on the platform.
  • Improved Shared ?v= stamp on every planner @import bumped to 20260726a, per the rule in planner-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: #F0EBE3 in platform-tokens.css (mirrored as backgrounds.header in the JSON). The global header now sits a step deeper than the #FEF7F1 page 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-header rule and the inline style attribute the component emits (the inline one wins, so they have to move together). The bg option 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. count emits sibling cards so it drops straight into an app's own grid. Adopted on planner/planner-home.php, facilitator/facilitator-home.php, and synthesize/synthesize-home.php, replacing three "Loading your workshops…" text lines. New .ws-skeleton-grid helper 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 under ws_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 the leadSlot, center, trailLead, and trailSlot slots. Template in includes/components/app-bar.php, CSS in includes/components/components.css, registered in workshopr-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_actions block from facilitator/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 that ws_lifecycle renders, 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 (compact for card badges, full with an optional next-step cta). Template in includes/components/lifecycle.php, CSS in includes/components/components.css, demo at design-system/components/demos/lifecycle.php, registered in workshopr-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-skeleton shimmer 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-line width helpers. Emitted by the shared FacilitatorSkeletons JS builders so the live app and the /facilitator/loading-states gallery render identical markup.
  • Fixed .fac-live__body gained grid-template-rows: 1fr + min-height: 0 (and min-height: 0 on 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-item card anatomy (type-coloured .item-icon square, .item-time, .item-title, .item-duration-badge) plus the shared ws_activity_badge. Class names mirror css/planner/components/_cards.css but the rules are scoped under #start-agenda so 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--compact no 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-cta is now text-align: right so the hero button shares the agenda's right edge with the header button.
  • Improved --fac-hero-gradient retinted 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 #3D0C0C to var(--color-ink). .fac-nav (live dashboard bottom bar) background changed from --bg-surface to --fac-ink-faint, with its tool buttons / note button / position / shortcuts text set to --fac-ink for contrast. Added the missing --fac-ink-faint token (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-surface pill, solid --gray-300 border, --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-purple border/text, --fac-purple-very-light fill) 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) from css/base/_backgrounds.css, instead of a flat --bg-page fill.
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-loaded components.css, _agenda-generative.css, and the .energy-arc-* card from prep.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 behind prefers-reduced-motion.
July 2026
v2.9.28 — Icon set: calendar_view_day + right_panel_open
  • New Added calendar_view_day.svg and right_panel_open.svg to images/icons/ (official Material Symbols outlines, fill="currentColor") — the icon() 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 CSS data-tooltip + aria-label instead of a labeled ws_button.
July 2026
v2.9.27 — Strategist joins the global header
  • New ws_header gained a strategist section (Plan phase, strategy icon, "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 100dvh with 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 StSections beside 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--last class toggle in strategist.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 an animation-duration kill added to the landing's reduced-motion block.
  • New .uni-drop variant 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-learn family, --color-ink(-secondary), --bg-page/surface) instead of duplicating hex values; stale "red is a deliberate exception, like Intervention" comments in synthesize/index.php and synthesize.css rewritten to reflect Red Unification.
  • Fixed Remaining off-scale values in synthesize.css moved 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 in workshopr-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 now ws_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.php now renders the composite via ws_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-navy and --uh-color-navy aliases 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 was display:none below 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.css referenced 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-bounce in css/tokens/_variables.css, corrected per-app --ease-spring overrides (Tips, Podcasts, Academy, Tips v2) that had subverted the sanctioned token, and replaced the raw literals in css/styles.css, both _modals.css sheets, participant.css, and two planner pages with var(--ease-spring).
  • Improved ws_button now has a :active press scale on the base class (covers all variants); global header icon buttons gained a press scale + transition.
  • Improved Removed transition: all from the shared component library and the planner card/sidebar/modal sheets (~71 rules), scoping each to transform/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.php to .cta. An unscoped .ws-btn { border: none } was loading after components.css and stripping the red border from every .ws-btn--secondary on 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-light aliases in platform-tokens.css to 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, and planner/email-preview.php that 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 companion entry to apps in workshopr-design-system.json, mirroring the shape of the existing app entries plus a platform note flagging it as a standalone native codebase with no CSS token.
  • New Added mobile-ios.md, a concise reference mirroring the tone of theming.md/motion-system.md, pointing to workshopr-companion/DESIGN.md and src/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-select move from the platform-default --gray-200 to --gray-400, with --gray-500 on hover. Scoped to .st-app so the shared ws-input/ws-select components are unchanged platform-wide.
July 2026
v2.9.15 — Strategist Buy-In wizard
  • New Added .st-wizard stepper nav and .st-wizard__footer Back/Next bar to strategist/css/strategist.css, splitting the Buy-In phase's five .st-section blocks (in strategist/includes/phase-buyin.php) into wizard steps via .st-step / data-st-buyin-step. Active step uses --phase-learn-dark text on --phase-learn-very-light chip fill; a touched-but-inactive step gets a quieter --gray-200 chip. Step transitions are a 200ms fade+4px rise (.st-step--entering), disabled under prefers-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__heading card and .st-docs button stack to strategist/css/strategist.css, hosting the new Documents section (Executive Summary, Facilitator Briefing, Risk Report, Agenda Blueprint) in strategist/includes/sidebar.php after the Workshop Brief preview. Generated output reuses the existing .st-script / .st-skeleton patterns 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-reco go/caution/not-yet recommendation banner to strategist/css/strategist.css, hosted at #stRecommendation in strategist/includes/sidebar.php directly 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__input in includes/components/footer.php: solid var(--bg-surface) fields with ink text and var(--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-visible ring to the footer's local .ws-btn.
  • Fixed The footer now carries a scoped .cta .sr-only fallback 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-badge rules to css/planner/components/_topbar.css for the planner topbar's Strategist handoff link — a quiet white link with a light-red readiness pill (--phase-learn-very-light ground, --phase-learn-dark text) 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-view and .st-brief--standalone rules to css/strategist.css for 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..6 category tints in platform-tokens.css were 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-red app-tips.php guide. Regenerated tokens.dtcg.json; drift check passes.
  • Fixed Tips app: the local --cat-* tokens in css/tips/styles.css (half-purple, half-red) and the --tips-cat-* fallbacks in tips/index.php are now the full brand-red ramp.
  • Fixed Migrated the live category colours: the 13 rows in the tip_categories DB 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) from var(--text-h2, 1.5rem) to standard var(--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") from div tags to semantic h1 tags 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 neutral var(--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 inline img.icon elements 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.php guide 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 in css/tips/styles.css and are flagged for migration.
  • Fixed app-hybrid.php guide rewritten from the retired Plan blue (#0284C7) to the Learn Red family; the "Do" now points at var(--phase-learn), matching the shipped /toolkits/hybrid/ page.
  • Fixed Finished two half-migrated gradients — collections-tabs.css and _sidebar.css no 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 in helpers.php, template task-checklist.php, CSS in components.css, demo at components/demos/task-checklist.php). Items carry real-data done states, optional phase eyebrows, links on undone items, a dismiss hook (data-checklist-dismiss), and data-task keys for JS enhancement. Sky-blue focus ring, :active tactile 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.php passes.
July 2026
v2.9.4 — Documentation consistency pass, round 2 (canonical docs + motion pair)
  • Fixed /DESIGN.md: frontmatter elevation.colored re-synced to the all-red shipped shadows; motion frontmatter aligned to shipped easing tokens (deleted --ease-bounce removed, --ease-reveal/--ease-spring named 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.md converted 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 per motion-system.md), stray wrapping code fences removed.
  • Fixed motion-system.md post-shipping prose: reveal ease is a token (--ease-reveal, P1); --ease-bounce moved out of the live spec table (deleted, P2); the --ws-ease-bounce stray corrected from "defines" to "uses — defined nowhere (dangling)"; the participant reduced-motion spin exception marked as documented-intent-only (not implemented in participant.css).
  • Improved theming.md exceptions table records the planner activity-type colors as a sanctioned functional-coding exception; ADOPTION.md font 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.json phase mapping: Coach moved from facilitate to reflect (it had inverted the "Coach = Reflect, never swap" rule); DNA marked standalone. check-drift.php passes (54/54 components, 9 apps).
  • Fixed workshopr-style-guide.md reconciled to rank-4: deference banner to /DESIGN.md added; 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.md rewritten 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.md architecture line and counts (54 ws_* helpers, 44 demos in components/demos/); ADOPTION.md internal cool-gray contradiction resolved (≈25 remain in planner trees, not "all replaced"); DOC-TEMPLATE.md stale "Tips purple" exception example; START-HERE.md counts, §15 changelog pointer, and pre-D1 "phase color discipline" wording; theming.md dead motion.md link.
  • Fixed /motion.md illustrative token block aligned to shipped tokens (--duration-reveal 800ms, --duration-reveal-lg 1200ms, --stagger-reveal; deleted-overshoot curve removed); deprecated taste.md/principles.md/forms.md reduced to pointer stubs.
July 2026
v2.9.2 — Audit B1 participant bridge + lint-blind indigo cleanup
  • Fixed Audit B1: css/facilitator/participant.css referenced 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 (per theming.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-lint misses because indigo isn't a signature phase hex: briefing.css (#536dfe/#4338ca), _sidebar.css and _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.css section comments and stale /* #7C4DFF */ annotations, the JSON phases block (was still blue/purple/emerald), and the ds-lint.mjs phase 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 :root deduped (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-bounce deleted (zero consumers; --animate-scale repointed to --ease-spring). P3: --transition-* deprecated. P4: --ease-spring documented 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-status maturity 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.php is 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.css still 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 in apps.facilitator.colorNote; the poll-bar gradient's raw #A78BFA was replaced with var(--fac-purple-dark). ds-lint is 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_button row), 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__error notice (error tokens) instead of a native alert(); native confirm() 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.mjs phase map now encodes the documented app color exceptions (Planner, DNA, Intervention, Facilitator → red; Tips → purple) instead of flagging shipped red as wrong-phase. Full-repo violations dropped from 156 (mostly false positives) to 83 genuine legacy items.
  • New Pre-commit hook (.git/hooks/pre-commit) runs ds-lint.mjs --staged so 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 in components.js; ws_modal refactored 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_steps under design-system/components/demos/, registered on the component-library index (whose cards now link to demos/ — the old links 404'd).
  • Improved Facilitator red completed on remaining surfaces: css/facilitator/facilitator.css, participant.css, and summary.css now resolve accents to --mainsite-* tokens (purple fallbacks removed). Stale "facilitator = purple" comment in platform-tokens.css corrected.
July 2026
v2.8.8 — Facilitator app red confirmed + live poll cards
  • Improved Corrected apps.facilitator.colorNote in workshopr-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.css keeps the legacy --fac-purple* variable names but resolves them to red. Also updated the phaseColors.facilitate note.
  • New Poll result card styles (.fac-poll, .fac-poll__bar, etc.) in facilitator/css/facilitator.css for 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 the ws-card, ws-activity-badge, ws-badge (duration), ws-suggestion-card, ws-stat, ws-alert, ws-empty-state and ws-skeleton markup instead of bespoke .generator-item HTML — and drops all inline onclick in favor of delegated listeners.
  • New Layout-only stylesheet css/planner/components/_agenda-generative.css (imported by planner-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-card via AgendaRenderer.swapAltCard() — removed the bespoke .ai-swap-alt-card markup 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 :root still win.
  • Improved Documented in workshopr-design-system.json that 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 synthesize section in the global header (ws_header) — icon hub, 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/related notes to full top-level component entries in workshopr-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) from AUDIT-2026-07-01-app-vs-design-system.md.
July 2026
v2.8.3 — app-tips.php color fix
  • Fixed app-tips.php still 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 in platform-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.php still showed sky blue. The v2.8.0 Planner recolor (blue → Learn red) updated platform-tokens.css and 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.md was 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 to platform-tokens.css for hex values instead of mirroring them (mirroring was the drift source). Fixed the same stale planner/DNA colors in workshopr-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.md is the single router (source-of-truth hierarchy + on-ramps); README.md is 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 in workshopr-style-guide.md (tips categories → purple, intervention → red, training → red), and fixed app-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-* in platform-tokens.css, apps.planner in the JSON (with a colorNote), and the phase table + a new exception bullet in DESIGN.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 that platform-tokens.css and the app actually use.
  • Fixed Motion easing contradiction. Canonical reveal/spring ease is now cubic-bezier(0.22, 1, 0.36, 1) across DESIGN.md, the JSON, and motion.md (was split between two curves). Added standard (0.4, 0, 0.2, 1) for micro UI transitions.
  • Improved motion.md rewritten 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 JSON zIndex.
  • 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-facilitate fallbacks from stale #7C3AED to the real #7C4DFF. Bumped DESIGN.md last-reconciled to 2026-07-01. Full audit at design-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) in platform-tokens.css, the apps.intervention block in workshopr-design-system.json (with a colorNote), and the app-intervention.php guide. 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 myworkshopr section to ws_header (icon space_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-loads platform-tokens.css so 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-radius now matches the button (10px), inputs bumped to 16px / roomier padding (no iOS zoom), added input hover states, a .cta__note privacy 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.php with loading/success/error states. Added a .cta__hp honeypot field and .cta__msg / .cta__msg--error status-message styles.
June 2026
v2.7.13 — Larger .section-title display headings
  • Improved .section-title display headings increased ~25%: the clamp(28px, 4vw, 38px) rule (Intervention, Podcasts, Tips) is now clamp(35px, 5vw, 48px); DNA moved --text-3xl → --text-4xl; shared-components and Academy use calc(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 now position: sticky; top: 0 instead of position: relative, so the global navigation pins to the top of the viewport on scroll. Existing z-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-section carousel config (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, and prefers-reduced-motion support. CSS/JS are namespaced under .wsh-carousel and self-emitted once. First used by the Academy hero to showcase all six ebook covers. Each slide also supports an optional top-right status pill ({label, tone} — available green / soon red).
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-hero namespace 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.php is require_once'd by both helpers.php and components/header.php, so any page with the global header can render a section hero without pulling in the full helpers.
  • Fixed ws_header now 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_hero to 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) and visualChips (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 page body background, preserving each page's background-color. Added to the home, library, academy, tips, and planner landing index pages; ws_section_hero also 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_hero buttons now accept an optional icon (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_header but don't load includes/icon.php — the header component now loads the inline-SVG icon() 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 and prefers-reduced-motion users 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.css previously 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 to body.planner-page/.planner-app-active so it beats the :root token re-imports. Note: aliases declared : var(--planner-primary) at :root are 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-h2 to --text-h3/--text-small, with a pulsing auto_awesome sparkle 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-meta so controls match the planner UI scale. Added a :-webkit-autofill override so autofilled fields stay on-theme instead of browser blue. Shared planner @import stamp bumped 20260610 → 20260615.
  • Improved Generator output view got the same treatment — agenda item names dropped from --text-h2 to --text-h4, start times / headers / rationale / refine input from --text-h3 to --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-h2 to --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=YYYYMMDD stamp. 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 on planner-new.css never invalidated the files it @imports, and the server caches CSS for 7 days — so production visitors kept week-old partials (collapsed .planner-body flex layout, exposed card edit panels). Every @import in planner-new.css (and the platform-tokens.css import inside base/_variables.css) now carries a shared ?v=YYYYMMDD stamp. 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.css the single canonical planner bundle. The partially-gutted planner-v2.css monolith (1,042 DUPLICATE/EXTRACTED markers) was retired and deleted; every selector it still held was already present in the imported component set, except the .sr-only a11y utility which was migrated into planner-new.css. planner/includes/header.php now loads the one bundle on the modular path (the dead planner.css link and the temporary standalone _modals.css patch 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 rem and spacing is px, so a root html { font-size: 80% } shrinks all rem text while leaving layout intact. Added to platform-tokens.css and 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 same 0.8 so it stays uniform. No token values changed, so the JSON mirror stays in sync (drift check passes). Inline px font 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 across css/detail-immersive.css, css/styles.css, css/planner/planner-v2.css, css/planner/live-mode.css, and css/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 its margin: -var(--space-8) auto 0 (and with it the auto centering) was ignored. The correct form for negating a token is calc(-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 and ws_* 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 :root and survives the late platform-tokens.css @import in assets/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/ and design-system/components/react/ — portable implementations of every ws_* component (49) and all 6 documented patterns. PHP ships as namespaced Workshopr\DS\* standalone functions (collision-free with the global ws_* helpers); React ships as typed .tsx function components (Ws<Name> + Ws<Name>Props).
  • Improved Both flavors render the existing ws-* class names from components.css (single source of truth) — no duplicated CSS, no hardcoded colors/sizes/fonts. Shared helpers: _icon.php (icon/cx/attrs/e) and Icon.tsx / cx.ts.
  • New Barrels: php/index.php (requires all 50 components, 59 Workshopr\DS functions) and react/index.ts (52 exports), plus a README.md with usage and a patterns-CSS caveat. All PHP verified with php -l + render smoke tests.
June 2026
v2.6.0 — AI-enablement: enforcer fix, enriched catalog, DTCG export
  • Fixed The design-system-enforcer MCP pointed at a stale /sites/newphp dir 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 + 49 ws_* 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.php now enriches components.generated.json with a canonical example, whenToUse / whenNotToUse, an a11y note, and a derived antiPattern per ws_* helper — extracted from the components/*.php demos. 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 $type inference, $description from CSS comments, and var() → {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 --check modes wired into npm, the CI ds-drift job, 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-size resolves 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 becomes var(--text-*, <token-rem>) so the fallback matches the snapped size. Most deltas are sub-pixel; the visible ones are 28px→24px (62×), 22px→20px (46×), 36px→32px, and 40px→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 load platform-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, hero 36–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 via npm 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 #fff by context (color:→--text-inverse, else --bg-surface); and skips hexes already in var() fallback position so it never produces redundant var(--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 --primary and 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 #E54D4D fallbacks (facilitator --fac-red = --color-error; planner --planner-red = documented mainsite-CTA alias; --phase-learn status colors). Tokenized them (drop literal, keep the var) for zero visual change; mapped the live-mode --run-danger token 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.md for intent; platform-tokens.css + helpers.php for 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 any ws_* helper is undocumented in the JSON, or if phase-color tokens disagree between platform-tokens.css and the JSON. Known source contradictions (tips, training) are allowlisted as visible warnings pending a design decision.
  • Improved JSON meta now references the canonical /DESIGN.md and /motion.md, declares itself a hand-maintained mirror (correcting the earlier “auto-generated” claim), and points to the drift guard.
  • Improved README.md now defers to START-HERE.md and 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 a colorNote in the JSON, and both pass check-drift.php without an allowlist.
  • New Added the --facilitator-primary token family (aliased to --phase-facilitate, #7C4DFF) — facilitator was the only app missing its --{app}-primary token. The JSON facilitator color was corrected from the stray #7C3AED to the phase purple, matching coach/intervention.
  • Improved check-drift.php now resolves one level of CSS var() indirection, so phase-aliased app tokens validate. Wired into governance: npm run lint:ds:drift, a ds-drift PHP job in the Design System Lint GitHub workflow, and a staged-file gate in the pre-commit hook.
  • Improved check-drift.php gained a reverse check — a JSON component with no matching ws_* function (renamed/removed) now fails the build — and reports component counts. Corrected stale counts in README.md (39 demo pages, 49 helpers, not “34”/“40+”), and aligned GEMINI.md with the consolidated DESIGN.md (it still pointed at the deprecated taste/principles/forms files).
  • New generate-catalog.php + components.generated.json — a real generator that derives a ws_* component roster (signature, summary, declared options) straight from the helpers.php docblocks. The curated JSON keeps its rich hand-authored option metadata; the generated file is the can’t-drift companion. --check mode fails on a stale roster or a curated signature that no longer matches source, wired into npm run lint:ds:catalog, the CI ds-drift job, and the pre-commit hook. Also added the canonical-source banner to voice.php/philosophy.php pointing at DESIGN.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, and css/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 / -bg variants where used).
May 2026
v2.1.1 — Planner sidebar: prototype-matched toolbar + flat suggested list
  • New .sidebar-toolbar component for the planner's activity sidebar — sticky search input + pill-style type tabs. Lives in css/planner/components/_sidebar.css (mirrored at css/components/_sidebar.css).
  • New .side__group / .side__group-title / .side__list / .side__more / .side__group-empty structural classes for the flat "Suggested for X" list and "Browse all activities" footer link.
  • New .library-card--lib variant — 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-sidebar so 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) via color-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 HTML hidden attribute.
May 2026
v2.1.0 — Level Up Grid: phase grouping + button --half
  • New ws_level_up_grid() now supports an optional group_by_phase => true mode 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 via phase_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--3 modifier for 3-up phase rows (used by Learn and Plan phases). Existing 4-up grid stays the default.
  • New .ws-btn--half width modifier (50%) — sized for narrow card columns where --full reads as too dominant.
  • Improved Responsive overrides updated so both .ws-level-up__grid and .ws-level-up__grid--3 step down 4 → 3 → 2 → 1 column, and .ws-level-up__phase-head stacks below 1024px.
  • Improved Demo: /design-system/components/demos/level-up-grid.php now showcases the grouped variant. Button demo cheatsheet lists --half.
April 2026
v2.0.0 — Planner ws_* Component Migration (Phase 5)
  • Improved Migrated planner/edit.php buttons and form inputs from raw HTML to ws_button() and ws_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.php with ws_badge() and ws_duration()
  • Improved Standardized planner/saved.php buttons with ws_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-*) and var(--text-sm)
April 2026
v1.9.1 — Planner Brand Color Tokenization
  • New Added --planner-overlay-light/medium/heavy and --planner-backdrop/backdrop-light tokens to planner variables
  • Improved Tokenized standalone #E54D4D (red), #22C55E (green), #9333EA (purple), #F59E0B (warning) hex values across planner CSS to use var(--planner-*) and var(--color-*) tokens
  • Improved Replaced standalone #2563EB in planner cards and generator modal with var(--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-50 through --gray-900) exclusively
  • Improved Added helpers.php to global header.php — all pages now have automatic access to ws_* component helpers without manual includes
  • Improved Migrated /contact/ form from raw HTML to ws_input(), ws_select(), ws_button() components
  • Fixed Replaced 6 hardcoded font-family: 'Bitter' in MyWorkshopr CSS with var(--font-heading)
  • Fixed Cleaned up 2 DM Sans font 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.md for ongoing adoption enforcement
April 2026
v1.8.4 — Live-Mode Font-Size Normalization
  • Improved Normalized all hardcoded font-size values in live-mode.css to design system tokens: compact type scale variables for UI chrome, rem units for presentation display elements, icon sizes preserved as px
April 2026
v1.8.3 — Planner Font-Weight & Font-Size Normalization
  • Improved Converted 9 hardcoded font-size px values to rem units in run-mode.css and workshop-studio.css
  • Improved Replaced all hardcoded font-weight numeric 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: all in 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-visible outlines 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--secondary hover 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--secondary default background changed from opaque white (--bg-surface) to transparent for 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.php with 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.php documentation
  • 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.php and 4 in changelog.php replaced with warm stone palette
  • Fixed --phase-reflect-light corrected to #A7F3D0; missing -very-light variants 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-badge component CSS moved out of platform-tokens.css into components.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-code blocks (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.php and about/index.php to 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