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, and the part most likely to be wrong — so a theme shows what it is made of without being opened. A title alone tells you nothing about whether the grouping holds.

.theme-card — collapsed, with points
Decisions go undocumented3 notes
Ways of Working
  • Build a "decision lane" into the weekly ritual
  • After Thursdays nobody knows who owns what
.theme-card — expanded (.evidence)
Too much synchronous status1 note
P1Ways of Working
format_quote
Honestly half our meetings are status updates. We should make those async.
personMarcus
Expanding swaps the read-only points for editable evidence cards, each traced back to its source.
.theme-card.muted
Parking lot2 notes
visibility_offMuted
Muted keeps a theme on the board but out of the export.
.theme-card — no notes (degenerate)
Untitled theme0 notes
Other
A hand-added theme before anything is filed into it. .theme-points renders nothing rather than an empty list.

Alternate layouts

.kan-card — kanban
Ways of Working2
Decisions go undocumented
3 notes
.ol-theme — outline
expand_moreWays of Working2 themes · 4 notes
chevron_rightDecisions go undocumented3

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

Cards, not table rows. The two facts that decide whether an action happens — who owns it and when it is due — sit on the card so they survive a narrow column.

.act-card — open
2 open · 0 done
Write the decision-log template
personJordaneventFriday
.act-card — done + carried
Book the shadowing rotation
carried
personSameventJuly
carried marks an action that came forward from the previous session rather than out of this one.
.act-card — unassigned, no date
Follow up on the staging access request
personevent
Empty owner and date show their placeholder rather than collapsing — an unowned action should look unfinished.
.dec-card
check
Run a two-week trial of async status starting next sprint
A decision is settled, so the tick is filled — where an action still to do gets an open checkbox.

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.

.sy-ai — empty, with suggestions
The three openers are the questions the board can actually answer — an empty chat box teaches nothing.
.sy-ai — conversation + typing
.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.
Why no entry animation
The whole screen re-renders on every state change, so an animation on .sy-ai replays on each message and reads as a flicker. The panel appears without one deliberately; the typing dots are the only motion, and they are an infinite indicator rather than an entry.

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.
.sy-ai.ws-modal--drawer / --bottom-sheet: both variants exist and match the panel's two sheet forms, but the swap only holds if all three breakpoints survive it.
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 card.theme-card, .theme-points, .evidence, .ev-cardEvidence-traced clustering output; nothing else on the platform produces it
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.sy-ai, .ai-msg, .ai-bubbleChat is Synthesize-only today — revisit if a second app grows one
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