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.
synthesize.css..synth scope
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-learn
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.
| Local | Resolves to | Notes |
|---|---|---|
--accent / --accentD / --accentVL | --phase-learn / -dark / -light | Unified brand red |
--accentL | none — #FCA5A5 | Red-300 step between light and main; no platform token exists |
--ink / --ink2 / --muted / --faint | --color-ink* ramp | Warm ink |
--hair | none — #C4BCB0 | Hairline between faint ink and border |
--bgPage / --surface / --subtle | --bg-page / --bg-surface / --bg-subtle | |
--border / --inner | none — #E8E3DC / #F0EBE4 | Warm borders; the platform only defines a cool border token |
--ok | none — #047857 | Decision green — the one non-red semantic in the app |
--sticky | none — #FEF9C3 | Rendered 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.
--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
| Width | Frame | AI panel |
|---|---|---|
| ≥ 1440px | Three columns | Grid column, 340px |
| 1081–1439px | Two columns | Fixed sheet on the right edge, under the chrome via --synth-stage-top |
| ≤ 1080px | Single column, rail last | Bottom 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.
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.
- Build a "decision lane" into the weekly ritual
- After Thursdays nobody knows who owns what
.theme-points renders nothing rather than an empty list.Alternate layouts
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.
.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.
carried marks an action that came forward from the previous session rather than out of this one.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.
- 1The group is hungry for clearer decisions — three separate threads all point at the same gap.
- 2Status meetings are the most-named drag on the team's time.
- 3The offsite-is-too-long tension is loud enough to flag up.
#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.
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.
- check_circleReading your sources
- radio_button_checkedClustering the notes
- radio_button_uncheckedNaming the themes
- radio_button_uncheckedPulling out decisions and actions
.loading wrapper, which centres the steps in a 56vh grid on the real screen.No decisions captured
Add one below if the group committed to something.
Navigation
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.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.
| Was | Now | What stayed local, and why |
|---|---|---|
.btn, --accent, --sm, --ghost | .ws-btn + --primary/--secondary/--ghost + sizes | Only the Material Symbols sizing — ws_button ships inline SVG, the app emits ligatures |
.sy-skel + its shimmer | .ws-skeleton + 9 variants | Nothing. 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-state | Nothing. 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--underline | Nothing. This also retired the reset Synthesize needed against the global .tab pill rule |
.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.
.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.
| Component | Classes | Why it stays |
|---|---|---|
| App frame | .sy-app, .sy-rail, .sy-main, .sy-tools | Planner and Facilitator have their own shells with different anatomy |
| Theme card | .theme-card, .theme-points, .evidence, .ev-card | Evidence-traced clustering output; nothing else on the platform produces it |
| Priority matrix | .matrix, .dot-card, .prio-tray, .prio-chip | Drag-positioned 2×2 tied to the framework definitions in synthesize.js |
| Source artifact card | .tray-item, .tray-item--session | Shape is dictated by the artifact types Synthesize ingests |
| AI panel | .sy-ai, .ai-msg, .ai-bubble | Chat is Synthesize-only today — revisit if a second app grows one |
| Summary card | .sum-card, .sum-num, .sum-flag | Numbered draft list; close to ws_checklist but semantically different |
Do / Don't
ws_app_bar. The canvas is pure workspace..res-head duplicated the doc-bar. A test asserts it never comes back.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.
| Component | How it gets there | Usage |
|---|---|---|
ws_app_bar() | PHP, synthesize/app.php | Session name, undo/redo, library, AI, Export, Save, New |
ws_header() | PHP, synthesize/app.php | Platform nav above the shell ($ws_header_section = 'synthesize') |
.ws-btn | Class, from JS | Every button in the app — 66 call sites |
.ws-tabs--underline | Class, from JS | The five results tabs |
.ws-badge / .ws-pill | Class, from JS (wsBadge, wsPill) | Priority bands, category tags, the muted marker |
.ws-alert | Class, from JS (wsAlert) | The material-size guardrail; board-import errors |
.ws-empty-state | Class, from JS (wsEmpty) | Empty summary, actions, decisions, workshop list, saved list, detached source |
.ws-skeleton | Class, from JS (wsSkel) | Boot and saved-list skeletons |
Files
| File | Purpose |
|---|---|
synthesize/app.php | Shell: header, app bar, #synth-app mount |
synthesize/synthesize.js | Every screen, rendered from template strings |
synthesize/synthesize.css | All app styles, scoped under .synth |
api/synthesize.php | Synthesis, persistence, imports, and the Synthesizer AI chat action |
synthesize/tests/synthesize.spec.js | Playwright E2E over the shell, tabs, priorities, and the AI panel |