@ammduncan/easel 0.2.28 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to easel. This project adheres to [Semantic Versioning](https://semver.org/).
4
4
 
5
+ ## 0.3.0 — 2026-05-26
6
+
7
+ ### Fixed
8
+ - **App-fidelity mode (`kind:"mockup"`/`"app"`) no longer strips the structural primitives — `.window`/`.code`/`.terminal` now render in mockups.** The wrapper has two branches: a normal presentation branch and an app-fidelity branch for UI recreations (which skips presets/tokens/chips/prose-caps/body-bg so the agent controls every pixel). But app-fidelity skipped the *entire* built-in stylesheet — including the self-contained `.window` window chrome and `.code`/`.terminal` code blocks — while the skill and the `push` tool description told agents to use `.window` *for mockups*. So a `kind:"mockup"` push with `<div class="window">` rendered as unstyled serif text with no chrome (the same "I shipped a fix but it doesn't apply here" surprise as the 0.2.29 `.window` washout). Extracted the primitives — which use fixed, theme-independent colours and can't leak the host theme — into a shared `STRUCTURAL_PRIMITIVES_CSS` constant injected into BOTH wrapper branches (single source, no duplication; the normal branch's inline copies and their `@media print` overrides were removed). A mockup can now reach for `.window`/`.code`/`.terminal` and they render, in either kind.
9
+ - **App-fidelity mode now sets a system-sans default font instead of falling back to serif.** It deliberately omits the Inter webfont (so the agent controls typography with no CDN dependency), but it also left no `font-family` at all, so any mockup that didn't set its own font rendered in Times serif. Added a `system-ui, -apple-system, "Segoe UI", sans-serif` floor; the pushed HTML's own `font-family` still wins the cascade.
10
+
11
+ ### Added
12
+ - **Visual-regression test suite (`tests/visual/`).** A fixture battery covering every built-in primitive plus realistic composites, each carrying an injected contrast-audit that walks text nodes, composites translucent backgrounds over the host backdrop, computes WCAG contrast, and reports washout failures. A driver pushes them to a running server; the suite is rendered across the full preset × theme × density matrix (+ print) to catch surface-vs-ink regressions. The two fixes above were both found by this suite. See `tests/visual/README.md`.
13
+
14
+ ### Docs
15
+ - **`push` tool `kind` description and the using-easel skill now state what app-fidelity keeps vs strips** — structural primitives + sans default kept; presentation defaults (preset tokens, chips, prose caps, body bg/color, Inter) stripped — so the "use `.window` for mockups" guidance and the engine no longer contradict each other.
16
+
17
+ ## 0.2.29 — 2026-05-25
18
+
19
+ ### Fixed
20
+ - **`.window` mockups no longer wash out in a dark-mode viewer — pin a stable surface like `.code`/`.terminal` already do.** `.window` set `background: light-dark(#ffffff, #161616)`, so its surface *flipped* with the host theme, and it never pinned text color or re-scoped `color: inherit` to children. In a dark-mode viewer the window resolved to a dark `#161616` panel, and a light-dashboard mockup's subtle gray-on-white labels (pills, captions, KPI sublabels) vanished against it — only explicitly-coloured figures survived. This is the identical surface-vs-ink mismatch the 0.2.28 `.code`/`.terminal` primitive locks against; it was fixed for code blocks but the `.window` chrome was never given the same treatment, and its theme-flipping background made it worse (the same mockup rendered differently per viewer). A mockup renders an app's own UI and should look the same to everyone, so `.window` is now a **stable light canvas**: white bg, pinned `#1a1a1a` ink, `color: inherit` re-scoped to every child. Added an opt-in **`.window.dark`** variant (locked dark surface + light ink + dark chrome + stronger shadow) for genuinely dark-UI mockups, and a `@media print` override so a dark mockup prints legibly (browsers drop background colors by default, which would otherwise strand its light ink on white paper). Skill + `push` tool description updated to document the stable surface and `.window.dark`.
21
+
5
22
  ## 0.2.28 — 2026-05-25
6
23
 
7
24
  ### Added
@@ -110,6 +110,127 @@
110
110
  :root[data-theme="light"] .chip.info { background:#ecfeff; color:#0e7490; border-color:#a5f3fc; box-shadow:0 0 12px -6px rgba(14,116,144,.45); }
111
111
  :root[data-theme="dark"] .chip.info { background:#0a1c22; color:#67e8f9; border-color:#155060; box-shadow:0 0 12px -4px rgba(103,232,249,.4); }
112
112
  .chip.accent { background:var(--ds-accent-soft); color:var(--ds-accent); border-color:transparent; box-shadow:0 0 12px -4px color-mix(in srgb, var(--ds-accent) 40%, transparent); }
113
+ `;
114
+
115
+ /* Self-contained structural primitives — window chrome + locked code/terminal.
116
+ These pin their own background and ink with fixed (theme-independent) colours,
117
+ so unlike the preset-token presentation styles they CAN'T leak the host theme.
118
+ Injected in BOTH the normal wrapper AND app-fidelity (mockup/app) mode: a UI
119
+ recreation is exactly where an agent reaches for a window frame or a code
120
+ block, so stripping these in fidelity mode left the skill's own guidance
121
+ ("wrap a mockup in .window") producing unstyled output. */
122
+ const STRUCTURAL_PRIMITIVES_CSS = `
123
+ /* Skeuomorphic macOS-style window chrome for UI mockups. Usage:
124
+ <div class="window" data-title="App name"> …mockup content… </div>
125
+ Draws a 40px title bar with the three traffic-light dots (red/yellow/green)
126
+ and an optional centred title from data-title. Content sits below the bar.
127
+ Add the desktop class for a full desktop-screen canvas — min-height 900px,
128
+ i.e. the 1440x900 (16:10) standard design canvas — so a screen mockup looks
129
+ like a real window with viewport breathing room rather than a short strip.
130
+ Omit desktop for dialogs / small components so they stay content-sized.
131
+
132
+ A mockup renders an app's own UI, so it owns a STABLE surface that does NOT
133
+ flip with the host theme — otherwise a light dashboard mockup renders on a dark
134
+ window in a dark-mode viewer and every subtle gray label washes out (same
135
+ surface-vs-ink mismatch the .code primitive locks against). Default is a light
136
+ canvas with pinned dark ink and color:inherit re-scoped to every child so the
137
+ host's light-dark() ink can never leak in. Add the dark class
138
+ (class="window dark") for a genuinely dark-UI mockup. */
139
+ .window {
140
+ position: relative;
141
+ padding-top: 40px;
142
+ border-radius: 12px;
143
+ border: 1px solid #e2e2e2;
144
+ box-shadow: 0 14px 48px rgba(0, 0, 0, 0.16);
145
+ overflow: hidden;
146
+ background: #ffffff;
147
+ color: #1a1a1a;
148
+ }
149
+ .window * { color: inherit; }
150
+ .window::before {
151
+ content: "";
152
+ position: absolute;
153
+ top: 0;
154
+ left: 0;
155
+ right: 0;
156
+ height: 40px;
157
+ background-color: #f1f1f1;
158
+ border-bottom: 1px solid #e2e2e2;
159
+ background-image:
160
+ radial-gradient(circle at 19px 20px, #ff5f57 6px, transparent 6.5px),
161
+ radial-gradient(circle at 39px 20px, #febc2e 6px, transparent 6.5px),
162
+ radial-gradient(circle at 59px 20px, #28c840 6px, transparent 6.5px);
163
+ background-repeat: no-repeat;
164
+ }
165
+ .window::after {
166
+ content: attr(data-title);
167
+ position: absolute;
168
+ top: 0;
169
+ left: 0;
170
+ right: 0;
171
+ height: 40px;
172
+ display: flex;
173
+ align-items: center;
174
+ justify-content: center;
175
+ font-size: 13px;
176
+ font-weight: 500;
177
+ color: #6b6b6b;
178
+ pointer-events: none;
179
+ }
180
+ .window.dark {
181
+ border-color: #2a2a2a;
182
+ background: #161616;
183
+ color: #e6edf3;
184
+ box-shadow: 0 14px 48px rgba(0, 0, 0, 0.4);
185
+ }
186
+ .window.dark::before {
187
+ background-color: #1f1f1f;
188
+ border-bottom-color: #2a2a2a;
189
+ }
190
+ .window.dark::after { color: #9b9b9b; }
191
+ .window.desktop {
192
+ min-height: 900px;
193
+ }
194
+ /* Locked-dark code / terminal primitive. Reach for this instead of hand-rolling
195
+ a dark code container — the recurring failure is a custom dark <div> that sets
196
+ its own background but lets base text inherit .wrap's light-dark() ink, which
197
+ resolves to near-black in light host mode and vanishes against the dark panel.
198
+ This class locks BOTH background and ink, and re-scopes color:inherit to every
199
+ child so the host theme can never leak in. Ships the verified github-dark token
200
+ palette so syntax highlighting reads against #0f172a without per-token tuning.
201
+ Usage: <div class="code"><span class="kw">gcloud</span> services enable …</div>
202
+ .terminal is an alias; add .terminal for a prompt feel (same colors). */
203
+ .code, .terminal {
204
+ background: #0f172a;
205
+ color: #e6edf3;
206
+ border-radius: 12px;
207
+ padding: 18px 22px;
208
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
209
+ font-size: 13.5px;
210
+ line-height: 1.7;
211
+ overflow: auto;
212
+ margin: 16px 0 24px;
213
+ }
214
+ .code *, .terminal * { color: inherit; }
215
+ .code .kw, .terminal .kw { color: #ff7b72; } /* keywords, control flow */
216
+ .code .string, .terminal .string { color: #a5d6ff; } /* strings, attr values */
217
+ .code .fn, .terminal .fn { color: #d2a8ff; } /* function names */
218
+ .code .prop, .terminal .prop { color: #79c0ff; } /* identifiers, properties */
219
+ .code .num, .terminal .num { color: #ffa657; } /* numbers, constants */
220
+ .code .comment, .terminal .comment { color: #8b949e; } /* comments */
221
+ .code .muted, .terminal .muted { color: #94a3b8; } /* dim / secondary */
222
+ .code .accent, .terminal .accent { color: #6ee7b7; } /* highlight / success */
223
+ @media print {
224
+ /* Force the locked-dark primitives light for print — browsers drop background
225
+ colours by default, which would otherwise strand their light ink on white
226
+ paper. Applies in both normal and app-fidelity mode. The !important here
227
+ also (intentionally) overrides the normal branch's non-print-gated pre/code
228
+ theming, so code reads as dark-on-light on paper regardless of host theme. */
229
+ pre, code, .code, .terminal { background: #f4f3ed !important; color: #111 !important; border: 1px solid #ddd; }
230
+ .code *, .terminal * { color: #111 !important; }
231
+ .window.dark { background: #ffffff !important; color: #111 !important; }
232
+ .window.dark * { color: #111 !important; }
233
+ }
113
234
  `;
114
235
 
115
236
  const unreadIds = new Set();
@@ -653,7 +774,11 @@
653
774
  <script src="https://cdn.jsdelivr.net/npm/html-to-image@1.11.13/dist/html-to-image.js"></script>
654
775
  <style>
655
776
  *, *::before, *::after { box-sizing: border-box; }
656
- html, body { margin: 0; padding: 0; }
777
+ /* Sane sans-serif floor so an unstyled mockup doesn't fall back to Times serif.
778
+ No CDN font here (that's the agent's call in fidelity mode) — the pushed HTML
779
+ can override font-family inline / in its own <style>, which wins the cascade. */
780
+ html, body { margin: 0; padding: 0; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; }
781
+ ${STRUCTURAL_PRIMITIVES_CSS}
657
782
  </style>
658
783
  </head>
659
784
  <body>
@@ -671,9 +796,10 @@ ${body}
671
796
  <link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
672
797
  <script src="https://cdn.jsdelivr.net/npm/html-to-image@1.11.13/dist/html-to-image.js"></script>
673
798
  <style>
799
+ *, *::before, *::after { box-sizing: border-box; }
674
800
  ${PRESET_TOKENS_CSS}
675
801
  ${SEMANTIC_CHIPS_CSS}
676
- *, *::before, *::after { box-sizing: border-box; }
802
+ ${STRUCTURAL_PRIMITIVES_CSS}
677
803
  html, body {
678
804
  margin: 0;
679
805
  background: var(--ds-bg-elev);
@@ -721,56 +847,6 @@ body > *:last-child { margin-bottom: 0 !important; }
721
847
  margin-left: 0;
722
848
  margin-right: 0;
723
849
  }
724
- /* Skeuomorphic macOS-style window chrome for UI mockups. Usage:
725
- <div class="window" data-title="App name"> …mockup content… </div>
726
- Draws a 40px title bar with the three traffic-light dots (red/yellow/green)
727
- and an optional centred title from data-title. Content sits below the bar.
728
- Add the desktop class for a full desktop-screen canvas — min-height 900px,
729
- i.e. the 1440x900 (16:10) standard design canvas — so a screen mockup looks
730
- like a real window with viewport breathing room rather than a short strip.
731
- Omit desktop for dialogs / small components so they stay content-sized. */
732
- .window {
733
- position: relative;
734
- padding-top: 40px;
735
- border-radius: 12px;
736
- border: 1px solid light-dark(#e2e2e2, #2a2a2a);
737
- box-shadow: 0 14px 48px rgba(0, 0, 0, 0.16);
738
- overflow: hidden;
739
- background: light-dark(#ffffff, #161616);
740
- }
741
- .window::before {
742
- content: "";
743
- position: absolute;
744
- top: 0;
745
- left: 0;
746
- right: 0;
747
- height: 40px;
748
- background-color: light-dark(#f1f1f1, #1f1f1f);
749
- border-bottom: 1px solid light-dark(#e2e2e2, #2a2a2a);
750
- background-image:
751
- radial-gradient(circle at 19px 20px, #ff5f57 6px, transparent 6.5px),
752
- radial-gradient(circle at 39px 20px, #febc2e 6px, transparent 6.5px),
753
- radial-gradient(circle at 59px 20px, #28c840 6px, transparent 6.5px);
754
- background-repeat: no-repeat;
755
- }
756
- .window::after {
757
- content: attr(data-title);
758
- position: absolute;
759
- top: 0;
760
- left: 0;
761
- right: 0;
762
- height: 40px;
763
- display: flex;
764
- align-items: center;
765
- justify-content: center;
766
- font-size: 13px;
767
- font-weight: 500;
768
- color: light-dark(#6b6b6b, #9b9b9b);
769
- pointer-events: none;
770
- }
771
- .window.desktop {
772
- min-height: 900px;
773
- }
774
850
  .wrap { display: block; }
775
851
  .kicker {
776
852
  display: block;
@@ -837,35 +913,6 @@ pre {
837
913
  margin: 16px 0 24px;
838
914
  }
839
915
  pre code { background: transparent; padding: 0; color: inherit; font-size: inherit; }
840
- /* Locked-dark code / terminal primitive. Reach for this instead of hand-rolling
841
- a dark code container — the recurring failure is a custom dark <div> that sets
842
- its own background but lets base text inherit .wrap's light-dark() ink, which
843
- resolves to near-black in light host mode and vanishes against the dark panel.
844
- This class locks BOTH background and ink, and re-scopes color:inherit to every
845
- child so the host theme can never leak in. Ships the verified github-dark token
846
- palette so syntax highlighting reads against #0f172a without per-token tuning.
847
- Usage: <div class="code"><span class="kw">gcloud</span> services enable …</div>
848
- .terminal is an alias; add .terminal for a prompt feel (same colors). */
849
- .code, .terminal {
850
- background: #0f172a;
851
- color: #e6edf3;
852
- border-radius: 12px;
853
- padding: 18px 22px;
854
- font-family: ui-monospace, "SF Mono", Menlo, monospace;
855
- font-size: 13.5px;
856
- line-height: 1.7;
857
- overflow: auto;
858
- margin: 16px 0 24px;
859
- }
860
- .code *, .terminal * { color: inherit; }
861
- .code .kw, .terminal .kw { color: #ff7b72; } /* keywords, control flow */
862
- .code .string, .terminal .string { color: #a5d6ff; } /* strings, attr values */
863
- .code .fn, .terminal .fn { color: #d2a8ff; } /* function names */
864
- .code .prop, .terminal .prop { color: #79c0ff; } /* identifiers, properties */
865
- .code .num, .terminal .num { color: #ffa657; } /* numbers, constants */
866
- .code .comment, .terminal .comment { color: #8b949e; } /* comments */
867
- .code .muted, .terminal .muted { color: #94a3b8; } /* dim / secondary */
868
- .code .accent, .terminal .accent { color: #6ee7b7; } /* highlight / success */
869
916
  blockquote {
870
917
  border-left: 3px solid var(--ds-accent);
871
918
  margin: 18px 0;
@@ -912,8 +959,7 @@ img { max-width: 100%; height: auto; border-radius: 10px; }
912
959
  body { padding: 24px !important; max-width: none !important; }
913
960
  body > p, body > .deck, body > .lede, body > ul, body > ol, body > blockquote,
914
961
  body > h1, body > h2, body > h3, body > h4 { max-width: none !important; }
915
- pre, code, .code, .terminal { background: #f4f3ed !important; color: #111 !important; border: 1px solid #ddd; }
916
- .code *, .terminal * { color: #111 !important; }
962
+ /* .code/.terminal/.window.dark print overrides live in STRUCTURAL_PRIMITIVES_CSS. */
917
963
  .card, .panel { background: #fff !important; border: 1px solid #ddd !important; box-shadow: none !important; }
918
964
  a { color: #111 !important; text-decoration: underline; border-bottom: 0 !important; }
919
965
  }
package/dist/mcp.js CHANGED
@@ -72,7 +72,7 @@ const inputSchema = {
72
72
  },
73
73
  kind: {
74
74
  type: "string",
75
- description: "Freeform tag: mockup, app, diff, explanation, comparison, diagram, status, progress, etc. SPECIAL: 'mockup' and 'app' switch the iframe into APP-FIDELITY mode — the wrapper skips its presentation defaults (Inter body font, design-token CSS, semantic chips, prose width constraints, body bg/color). Only the box-sizing reset and the html-to-image bridge stay. Use this when the push is a recreation of real UI (app screen, component instance, embedded preview) and you want full control over every pixel without the host theme leaking in. For presentation content (explanations, comparisons, status reports), omit kind or use a non-fidelity value.",
75
+ description: "Freeform tag: mockup, app, diff, explanation, comparison, diagram, status, progress, etc. SPECIAL: 'mockup' and 'app' switch the iframe into APP-FIDELITY mode — the wrapper skips its PRESENTATION defaults (preset design-token CSS, semantic chips, prose width constraints, body bg/color, the Inter webfont) so the host theme can't leak in and you control every pixel. It KEEPS the self-contained structural primitives (.window/.window.dark window chrome, .code/.terminal code blocks — all fixed-colour, theme-independent) and a neutral system-sans default font, so you can still reach for <div class=\"window\"> in a mockup and it renders. Set your own font-family/colours in the pushed HTML to override the sans default. Use this kind when the push is a recreation of real UI (app screen, component instance, embedded preview). For presentation content (explanations, comparisons, status reports), omit kind or use a non-fidelity value.",
76
76
  },
77
77
  },
78
78
  required: ["html"],
@@ -145,7 +145,7 @@ export async function main() {
145
145
  "• Stack desktop mockups VERTICALLY with labels ('Now', 'Proposed') — don't squeeze them side-by-side. The iframe is ~900px wide; two desktop screens at half-width crush columns, wrap headings to 3 lines, and turn tables unreadable.\n" +
146
146
  "• Side-by-side is fine only for narrow mobile mockups, small cards, or short text columns that genuinely fit in half-width.\n" +
147
147
  "• Mockup embedded mid-explanation? Prose is left-aligned and capped ~880px; wrap JUST the mockup in <div class=\"full-bleed\">…</div> and it fills the content column from the SAME left edge (wider than the prose, sharing one left margin; the body padding stays as a gutter so nothing touches the card edge). (If the WHOLE push is a UI recreation, use kind:'mockup'/'app' instead.)\n" +
148
- "• Window chrome: wrap a mockup in <div class=\"window\" data-title=\"App name\">…</div> for a macOS window frame (title bar + red/yellow/green traffic-light dots + centred title). Add the `desktop` class (class=\"window desktop\") for the 1440x900 (16:10) desktop canvas via min-height:900px; omit it so dialogs/components size to content. Combine with .full-bleed to fill the column. NOTE: .window sets overflow:hidden (to clip its rounded corners) — so NEVER put a fixed `height` on .window or any inner stage, or content past that height is silently guillotined. It's built to grow via min-height.\n" +
148
+ "• Window chrome: wrap a mockup in <div class=\"window\" data-title=\"App name\">…</div> for a macOS window frame (title bar + red/yellow/green traffic-light dots + centred title). Add the `desktop` class (class=\"window desktop\") for the 1440x900 (16:10) desktop canvas via min-height:900px; omit it so dialogs/components size to content. Combine with .full-bleed to fill the column. .window is a STABLE LIGHT canvas — it pins white bg + dark ink + re-scopes color:inherit to children (like .code/.terminal), so it does NOT flip with the host theme and subtle gray-on-white labels stay legible even in a dark-mode viewer; a mockup is a screenshot, it should look the same to everyone. For a genuinely dark-UI mockup add the `dark` class (class=\"window dark\") — don't hand-roll a dark .window. NOTE: .window sets overflow:hidden (to clip its rounded corners) — so NEVER put a fixed `height` on .window or any inner stage, or content past that height is silently guillotined. It's built to grow via min-height.\n" +
149
149
  "• BUILD MOCKUPS FLUID, not fixed-width. Lay the inside out with flex / % / fr widths, not hardcoded width:1440px columns. 1440 is a MAX, not a target. A fluid mockup reflows to fit when the viewer's window is squeezed — no horizontal scroll, nothing clipped, exports stay complete. A fixed-pixel-width mockup gets cut off or needs an awkward horizontal scrollbar when narrowed.\n" +
150
150
  "• NEVER CLIP CONTENT — no fixed `height` + `overflow:hidden` on any container that holds content (cards, panels, device/browser/phone frames, stages, slideovers, toasts). That combo guillotines anything taller than your guessed height — buttons sliced through text, lists cut mid-item. Containers size to their CONTENT: use `min-height` for a floor, NEVER a fixed `height`. `overflow:hidden` is allowed ONLY for genuine cosmetic crops where clipping IS the intent (rounded-corner image masks, decorative bleed) — never on a content region. Decorative frames must grow with their content. When unsure, leave height unset. Mentally render the tallest card: if any text/button could exceed the box, the box is wrong.\n" +
151
151
  "• MATCH THE SOURCE'S REAL FRAME — faithful height, not minimal, in both directions. Mocking a COMPONENT (card, modal, row, toolbar)? Size to content — do NOT pad with min-height:560px to feel 'desktop-y'; that floats content in dead whitespace. Mocking a FULL DESKTOP SCREEN (login page, dashboard)? Give it realistic viewport proportions via MIN-HEIGHT (e.g. min-height:760px or a 16:10 floor — never a fixed `height`, which clips) and lay content out inside as the real screen does (centred form, top nav). Either way copy the source's exact dimensions if it has them, as a min-height. Test: cropped the same way, would your mock look like a screenshot of the real thing? Empty bands = over-padded; a screen squashed to a strip = under-sized.\n" +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ammduncan/easel",
3
- "version": "0.2.28",
3
+ "version": "0.3.0",
4
4
  "description": "A live browser tab for every Claude Code (and MCP) session. The push MCP tool appends HTML cards to a scrolling feed you keep open in split-screen.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -299,7 +299,7 @@ Most mockups appear *inside* an explanation push — prose intro, embedded UI mo
299
299
  `.full-bleed` is injected into every presentation push. Prose is left-aligned and capped at ~880px; `.full-bleed` fills the **content column's full width from the same left edge** — wider than the prose, sharing one left margin down the card. It does *not* bleed to the card's physical edge: the body padding stays as a gutter, so neither the mockup nor the text ever touches the card border.
300
300
 
301
301
  Two cases, two tools:
302
- - **Whole push is a mockup / app recreation** → `kind: "mockup"` (or `"app"`) on the push. Strips the entire presentation frame; content owns the canvas.
302
+ - **Whole push is a mockup / app recreation** → `kind: "mockup"` (or `"app"`) on the push. Strips the *presentation* frame (preset tokens, semantic chips, prose-width caps, body bg/color, the Inter webfont) so the content owns the canvas — but **keeps the structural primitives** (`.window`/`.window.dark`, `.code`/`.terminal`) and a neutral system-sans default font, so `.window` and friends still render in a mockup. To match the real app's typeface, **inject its webfont right in the pushed HTML** — a `<link rel="stylesheet" href="…">` or an `@font-face` block loads fine in the sandbox — then set `font-family` on the content; that wins over the sans default.
303
303
  - **Mockup embedded in an explanation** → leave the push as-is and wrap the mockup section in `<div class="full-bleed">`.
304
304
 
305
305
  ### Window chrome for UI mockups
@@ -317,6 +317,8 @@ Wrap a mockup in `.window` to give it a macOS window frame — a title bar with
317
317
  - `data-title` sets the centred title text (omit for a blank bar).
318
318
  - Add the **`desktop`** class for a full desktop-screen canvas — `min-height: 900px`, the standard 1440×900 (16:10) design canvas — so a screen mockup looks like a real window with viewport breathing room. **Omit `desktop`** for a dialog or small component so the chrome sizes to its content (don't pad a small thing to screen height).
319
319
 
320
+ **`.window` is a stable LIGHT canvas — it does not flip with the host theme.** A mockup is a screenshot of an app; it should look the same to every viewer regardless of their OS/viewer mode. So `.window` pins a white surface with dark ink and re-scopes `color: inherit` to children (same locked-surface treatment as `.code`/`.terminal`) — you can use subtle gray-on-white labels inside and they stay legible even when the viewer is in dark mode. For a genuinely **dark-UI** mockup, add the **`dark`** class (`class="window dark"`) — it locks a dark surface + light ink + dark chrome instead. Don't hand-roll a dark `.window` with inline backgrounds.
321
+
320
322
  **Build the mockup fluid, not fixed-width.** Lay the inside out with flex / `%` / `fr` widths, not hardcoded `width: 1440px` columns. The content column caps at a desktop-realistic width, but when the viewer's window is narrower (a "squeezed" screen) a fluid mockup simply **reflows to fit** — no horizontal scroll, nothing clipped, and PNG/PDF export still captures the whole thing. A fixed-pixel-width mockup gets cut off or needs an awkward horizontal scrollbar when squeezed; a fluid one never does. The 1440 is a *max*, not a target.
321
323
 
322
324
  **`.window` sets `overflow: hidden`** (to clip its own rounded corners) and grows via `min-height`, never a fixed height. So **never put a fixed `height` on `.window` itself, or on an inner "stage" element** — content past that height gets silently guillotined, and because the overflow is on the frame the crop is invisible until you export or scroll. Let it grow.