Canvas Design System
Main Site Tokens

Synthesize

The post-session sense-making tool: drop in transcripts, board photos, and recordings, and get back themes, decisions, and action items you would actually send. Reflect phase.

How to use this page

Synthesize has no ws_* helpers of its own. Every screen is built from JavaScript template strings in synthesize/synthesize.js, so the real contract is the class names in synthesize/synthesize.css. This page renders those classes against that stylesheet — change the CSS and these specimens move with the app.

Design here first
Every element appears in all its states — empty, loading, done, muted, overflowing. Most of Synthesize's rough edges are states that were never seen next to each other.
Don't add styles to this page
The only CSS here is gallery chrome. A specimen that needs a style to look right is telling you the style belongs in synthesize.css.
The .synth scope
Everything in synthesize.css is scoped under .synth. Specimens here are wrapped accordingly. If a block on this page renders unstyled, it is missing its wrapper. The three exceptions — .synth-modal, .synth-overlay, .synth-topbar — are top-level and marked where they appear.

App Identity

Phase
Reflect
Accent
--phase-learn
Rendering
Client-side (vanilla JS)

Red Unification: Synthesize sits in the Reflect phase by name only. Like every app on the platform it renders in the Learn red family — the legacy Reflect emerald is retired.

Token Aliases

synthesize.css opens with a block of short local variables aliased onto platform tokens, so the app's rules stay terse. Anything without a platform token behind it is listed as such — those are the honest gaps, not drift.

LocalResolves toNotes
--accent / --accentD / --accentVL--phase-learn / -dark / -lightUnified brand red
--accentLnone#FCA5A5Red-300 step between light and main; no platform token exists
--ink / --ink2 / --muted / --faint--color-ink* rampWarm ink
--hairnone#C4BCB0Hairline between faint ink and border
--bgPage / --surface / --subtle--bg-page / --bg-surface / --bg-subtle
--border / --innernone#E8E3DC / #F0EBE4Warm borders; the platform only defines a cool border token
--oknone#047857Decision green — the one non-red semantic in the app
--stickynone#FEF9C3Rendered sticky-note yellow; functional, mirrors the source board
--rCard / --rModal / --rBadge--radius-xl / -2xl / -sm

App Frame

Every working screen wears the same frame: sources rail, working surface, and the AI panel when open. All three render inside #synth-app so the app's single delegated click/drag handler keeps working.

.sy-app — rail + working surface
Sign in to save drafts.
The working surface carries the drafting grid over --bg-page, so page and surface read as one sheet. Grid cell and line colour both come from platform tokens.

Anatomy

.sy-app — the grid. Two columns; a third 340px column appears above 1440px when the AI panel opens.
.sy-rail — sticky sources rail, --app-rail-width (320px).
.sy-main — working surface. Drafting grid, hairline border, --rCard radius.
.sy-main__bar — row one: where you are (back link or tabs) + how to add material.
.sy-tools / .sy-tool — 38px icon rail; labels live in .sy-tool__tip on hover.
.sy-main__foot — hint on the left, committing action on the right.

Responsive behaviour

WidthFrameAI panel
≥ 1440pxThree columnsGrid column, 340px
1081–1439pxTwo columnsFixed sheet on the right edge, under the chrome via --synth-stage-top
≤ 1080pxSingle column, rail lastBottom sheet, 78vh

Sources

What you gathered. The rail persists through the whole session, so after synthesis it is still how you get back to where a theme came from.

.tray-item — default
description
Q2 offsite transcript
Transcript · 4,210 words
.tray-item--session — workshop record
sensors
Live session signals
14 notes · 3 polls · 8 reactions
Accent border and tint mark the one artifact that keeps changing — it is re-imported, not uploaded.
.tray-title — long name (2-line clamp)
folder_open
Q3 platform strategy workshop — combined notes, whiteboard captures and the follow-up thread
Document · 18 pages
A source is identified by its name, so the rail wraps to two lines rather than truncating mid-word.
.sy-rail__empty
.sy-drop — the empty state's centre

Themes

The clustering output — now rendered by the recap-first surface, not the specimens this section used to show.

Retired 2026-08-09 — see Workspace's Themes tab instead
.theme-card, .themes-grid, .theme-chev, .theme-top, .theme-dot, .theme-title, .theme-notes, .theme-badges, .theme-points, .themes-bar, the bare .evidence/.ev-card containers, the theme card's .menu/.menu-wrap overflow menu, and the "Alternate layouts" .kanban/.kan-* and .outline/.ol-* specimens that lived here were deleted from synthesize.css when Task 11 (recap-first redesign closeout) confirmed zero remaining callers.

The current theme surface is .recap-theme/.recap-theme__head/.recap-theme__title/.recap-evidence/.recap-evidence__toggle (the "View sources (N)" disclosure) — shared verbatim between the Recap document (synthesize/js/modules/recap.js) and Workspace's Themes tab (synthesize/js/modules/workspace.js). The inner evidence-row pieces (.ev-text/.ev-by/.ev-foot/.ev-quote/.src-chip) are unchanged and still live.

Priorities

Three lenses on the same themes. A theme sits in the tray until it has been placed under the current lens — an empty grid is honest about the fact that nobody has judged anything yet.

.prio-bar + .prio-tray — nothing placed yet
Drag onto the grid
The lens is a named choice, not a carousel position. The tray sits below the grid, so reading order runs lens → grid → what is still unplaced. Watch the vertical budget: the 2×2 is 543px tall, so with enough unplaced themes the tray wraps to several rows and falls below the fold on a 900px viewport.
.prio-tray — everything placed
check_circle Every theme is on the grid.
.matrix — with .dot-card
High impactLow impactLow effortHigh effortQuick winsBig betsFill-insPark
Decisions go undocumented
Whose voice gets heard
.dot-card is drag-positioned in percentages, so the grid stays responsive.

Action Items & Decisions

The two facts that decide whether an action happens — who owns it and when it is due — now render inline in the recap document, not as the grid cards this section used to show.

Retired 2026-08-09 — see the recap document's Decisions/Actions sections instead
.act-grid, .act-card (open/done/unassigned), .act-progress, .dec-grid, and .dec-card were deleted from synthesize.css when Task 11 confirmed zero remaining callers — the card-grid layout they demoed is gone, replaced by a one-document read (#recap-actions/#recap-decisions in synthesize/js/modules/recap.js).

The reusable, non-hover-scoped inner pieces are unchanged and still live, just nested in the new .recap-action/.recap-decision containers instead: .act-body, .act-text, .act-meta, .act-field, .act-check, .carried-chip, .dec-check, .dec-text.

Quick Summary

The first tab, and the first thing anyone reads. Numbered so it reads as an ordered take rather than a bag of bullets, and flagged as a draft in words rather than by implication.

.sum-head + .sum-list
bolt DraftNot a final summary. Edit any line, or ask for a fresh pass.
  1. 1The group is hungry for clearer decisions — three separate threads all point at the same gap.
  2. 2Status meetings are the most-named drag on the team's time.
  3. 3The offsite-is-too-long tension is loud enough to flag up.
Redraft lives in the app bar, not here
bolt DraftNot a final summary. Edit any line, or ask for a fresh pass.
The head states the draft status and nothing else. Redraft is a chrome action (#synthTopbarRedraft), shown only while Quick Summary is the active tab and disabled while a rewrite is in flight. It runs over the board as it stands after your edits, not the material the first pass saw.

Synthesizer AI

A chat grounded in the board you have open. It reads the themes, decisions, and actions; nothing it says is written into the synthesis unless you copy it there yourself.

Retired 2026-08-09 — the fixed side flyout is gone, the chat log stays
.sy-ai (the fixed-position side panel, both its base rule and its <1080px sheet override) and its .ai-head/.ai-head__icon/.ai-title title-bar chrome were deleted from synthesize.css when Task 11 confirmed zero remaining callers — Synthesizer AI is now Workspace's own "Ask Synthesizer" tab, embedded inline in the content column (synthesize/js/modules/workspace.js) rather than a flyout, so it needs no fixed positioning or its own close-button header.

Everything else below is unchanged and reused verbatim by that tab: .ai-log, .ai-empty, .ai-suggestions/.ai-suggest, .ai-msg/.ai-avatar, .ai-bubble (including --typing and .is-error), .ai-composer. The specimen below still renders on the live stylesheet.
.ai-bubble.is-error
auto_awesome
Couldn't reach the assistant. Check your connection and try again.
A failed turn stays in the log as a message rather than a toast, so the thread still reads in order.

Review & Loading

The steps between dropping material and seeing a board.

.load-steps
  • check_circleReading your sources
  • radio_button_checkedClustering the notes
  • radio_button_uncheckedNaming the themes
  • radio_button_uncheckedPulling out decisions and actions
This usually takes a few seconds. We're drafting — you'll edit everything next.
Shown without its .loading wrapper, which centres the steps in a 56vh grid on the real screen.
.ws-skeleton — boot skeleton
Platform skeletons, which bring their own shimmer; a still screenshot catches it mid-sweep.
.ws-alert--warning
.ws-empty-state

No decisions captured

Add one below if the group committed to something.

Navigation

.ws-tabs--underline
Platform tabs. components.js only auto-binds .ws-tabs on DOMContentLoaded, and Synthesize renders these later — so the active state stays the app's to manage, which it already did.
.seg — segmented control
No platform equivalent exists. Synthesize, Planner, and Facilitator each have their own segmented control. This is the strongest candidate for a real ws_segmented.

Adopted from the Platform

includes/header.php loads components.css on every page, so all 58 platform components were already present inside Synthesize — the app just re-implemented a handful of them privately. Those private copies are gone. The PHP ws_* helpers still can't be called from a client-rendered app, so synthesize.js emits the same class names directly; small mirrors of the templates live next to icon() in that file.

WasNowWhat stayed local, and why
.btn, --accent, --sm, --ghost.ws-btn + --primary/--secondary/--ghost + sizesOnly the Material Symbols sizing — ws_button ships inline SVG, the app emits ligatures
.sy-skel + its shimmer.ws-skeleton + 9 variantsNothing. Emitted as <div>: the component sets no display, so a span would collapse
.guardrail.ws-alert--warning / --error.synth-alert--spaced, for an alert sitting inside a modal body
.empty.ws-empty-stateNothing. Loading lines moved to .load-hint — they were never empty states
.badge, .pill.ws-badge, .ws-pill.pri-badge and .cat-pill: both carry a colour set inline from app state, so they only make the text legible on top of it
.tabrow / .tab.ws-tabs--underlineNothing. This also retired the reset Synthesize needed against the global .tab pill rule
Not adopted yet
.synth-modal / .synth-overlay.ws-modal: needs the focus trap in components.js checked against Synthesize's own handling first.
(The .sy-ai.ws-modal--drawer row that used to sit here is moot: .sy-ai was deleted 2026-08-09 — Synthesizer AI is a Workspace tab now, not a panel to adopt a drawer shell for. See the Synthesizer AI section above.)
The one real gap: no shared segmented control
Synthesize, Planner, and Facilitator each ship their own .seg. There is no ws_segmented to adopt, which makes it the strongest candidate for the next real component — not a swap, a promotion.

Stays Local

These have exactly one consumer. Promoting them to ws_* would be speculative generality and would add drift-check burden for no reuse — so they stay in synthesize.css, documented here instead.

ComponentClassesWhy it stays
App frame.sy-app, .sy-rail, .sy-main, .sy-toolsPlanner and Facilitator have their own shells with different anatomy
Theme/evidence card.recap-theme, .recap-evidence, .ev-text/.ev-by/.ev-foot/.ev-quoteEvidence-traced clustering output; nothing else on the platform produces it. (Renamed 2026-08-09 from .theme-card/.theme-points/.evidence/.ev-card, retired with the old five-tab board.)
Priority matrix.matrix, .dot-card, .prio-tray, .prio-chipDrag-positioned 2×2 tied to the framework definitions in synthesize.js
Source artifact card.tray-item, .tray-item--sessionShape is dictated by the artifact types Synthesize ingests
AI panel.ai-msg, .ai-bubble, .ai-composerChat is Synthesize-only today — revisit if a second app grows one. (No longer .sy-ai: that fixed side-panel container was retired 2026-08-09; these classes now live inline in Workspace's Ask Synthesizer tab.)
Summary card.sum-card, .sum-num, .sum-flagNumbered draft list; close to ws_checklist but semantically different

Do / Don't

Do: say it is a draft
Synthesize outputs a first pass over someone's workshop. The Draft flag and the editable-everything rule are the product's honesty about that.
Don't: present output as an answer
A confident summary of a session the model half-understood is worse than no summary. Never hide the seams.
Do: show what a grouping is made of
Themes render their first notes collapsed, and evidence traces back to its source. The user is checking the machine's work.
Don't: hide evidence behind a click
A theme title alone gives no way to judge whether the clustering held.
Do: start empty when nothing has been judged
The priority grid opens empty with every theme in the tray. Auto-Sort is one click away when you want the machine's read.
Don't: pre-place items and call it a judgement
A grid that looks filled in implies agreement nobody expressed — the same reason unplaced themes are excluded from the cross-lens consensus read.
Do: keep chrome in the app bar
Session name, undo/redo, save state, AI, Export, Save and New all live in ws_app_bar. The canvas is pure workspace.
Don't: add a second toolbar in the canvas
The retired .res-head duplicated the doc-bar. A test asserts it never comes back.
Do: animate only what persists
The app re-renders wholesale on every state change, so animation belongs on infinite indicators, not on entry.
Don't: add entry animations to re-rendered nodes
They replay on every keystroke-free update and read as flicker.

Shared Components Used

Two ws_* helpers are called from the PHP shell. The rest of the platform components reach the app as classes, emitted from synthesize.js — the helpers can't be called from a client-rendered body, but the CSS is already on the page either way.

ComponentHow it gets thereUsage
ws_app_bar()PHP, synthesize/app.phpSession name, undo/redo, library, AI, Export, Save, New
ws_header()PHP, synthesize/app.phpPlatform nav above the shell ($ws_header_section = 'synthesize')
.ws-btnClass, from JSEvery button in the app — 66 call sites
.ws-tabs--underlineClass, from JSThe five results tabs
.ws-badge / .ws-pillClass, from JS (wsBadge, wsPill)Priority bands, category tags, the muted marker
.ws-alertClass, from JS (wsAlert)The material-size guardrail; board-import errors
.ws-empty-stateClass, from JS (wsEmpty)Empty summary, actions, decisions, workshop list, saved list, detached source
.ws-skeletonClass, from JS (wsSkel)Boot and saved-list skeletons

Files

FilePurpose
synthesize/app.phpShell: header, app bar, #synth-app mount
synthesize/synthesize.jsEvery screen, rendered from template strings
synthesize/synthesize.cssAll app styles, scoped under .synth
api/synthesize.phpSynthesis, persistence, imports, and the Synthesizer AI chat action
synthesize/tests/synthesize.spec.jsPlaywright E2E over the shell, tabs, priorities, and the AI panel