@markdstage/markdstage 3.8.0 → 3.8.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markdstage/markdstage",
3
- "version": "3.8.0",
3
+ "version": "3.8.1",
4
4
  "description": "Present, validate, inspect, capture, and export MarkdStage Markdown decks from the command line — no Copilot canvas required.",
5
5
  "license": "MIT",
6
6
  "author": "runceel",
@@ -116,12 +116,12 @@
116
116
  <div class="presenter-previews">
117
117
  <section class="presenter-preview presenter-preview-current" aria-labelledby="presenterCurrentLabel">
118
118
  <h2 id="presenterCurrentLabel">Current slide</h2>
119
- <iframe id="presenterCurrent" class="presenter-frame" title="Current slide"></iframe>
119
+ <div id="presenterCurrentViewport" class="presenter-frame slide-viewport"></div>
120
120
  </section>
121
121
  <div class="presenter-sidebar">
122
122
  <section class="presenter-preview" aria-labelledby="presenterNextLabel">
123
123
  <h2 id="presenterNextLabel">Next slide</h2>
124
- <iframe id="presenterNext" class="presenter-frame" title="Next slide"></iframe>
124
+ <div id="presenterNextViewport" class="presenter-frame slide-viewport"></div>
125
125
  <p id="presenterNextEmpty" class="presenter-next-empty" hidden>There is no next slide</p>
126
126
  </section>
127
127
  <section class="presenter-notes" aria-labelledby="presenterNotesLabel">
@@ -20,6 +20,7 @@ import {
20
20
  stripSpeakerNotes,
21
21
  } from "./speaker-notes.mjs";
22
22
  import { splitImportPath } from "./import-path.mjs";
23
+ import { createSlideViewport, OUTPUT_WIDTH, OUTPUT_HEIGHT } from "./slide-viewport.mjs";
23
24
 
24
25
  // Client-side slide renderer for the MarkdStage canvas.
25
26
  //
@@ -105,6 +106,11 @@ let previewMode = false;
105
106
  let previewOffset = 0;
106
107
  let navigationEnabled = true;
107
108
  let fixedPreviewMode = false;
109
+ let surfaceMode = false;
110
+ let outputViewport = null;
111
+ let currentViewport = null;
112
+ let nextViewport = null;
113
+ let surfaceDiagnostic = null;
108
114
  let moreControlsOpen = false;
109
115
  // Markdown for the most recently rendered slide, retained for editing-mode rerenders.
110
116
  let lastMarkdown = "";
@@ -122,8 +128,6 @@ let layoutFrame = 0;
122
128
  // past clientHeight, which is invisible but still enough for `overflow:auto` to
123
129
  // draw a scrollbar.
124
130
  const SCROLL_EPSILON = 2;
125
- const OUTPUT_WIDTH = 1280;
126
- const OUTPUT_HEIGHT = 720;
127
131
  const PPTX_LAYOUT_NAMES = ["title", "default", "center", "section", "backcover"];
128
132
  const LAYOUT_HINT_LIMIT = 5;
129
133
 
@@ -268,11 +272,8 @@ function elementPath(element, root) {
268
272
  return parts.join(" > ");
269
273
  }
270
274
 
271
- // The live 16:9 preview scales #stage with a CSS transform, so getBoundingClientRect()
272
- // reports scaled pixels while scrollWidth/clientWidth stay in the untransformed 1280x720
273
- // layout space. Every rect-derived delta is divided by this factor before the two kinds of
274
- // measurement are combined, keeping the preview, PDF, and PNG diagnostics in one coordinate
275
- // system.
275
+ // Normalize any content-authored transform before combining rect and scroll metrics.
276
+ // Display scaling happens outside the fixed browsing context and is not measured here.
276
277
  function layoutScale(deck) {
277
278
  const width = deck.offsetWidth;
278
279
  if (!width) return 1;
@@ -430,7 +431,9 @@ function updateFixedPreviewWarning() {
430
431
  const warning = document.getElementById("layoutWarning");
431
432
  const button = document.getElementById("navFixedPreview");
432
433
  const empty = document.body.classList.contains("markdstage-empty");
433
- if (!fixedPreviewMode || !layoutTarget || empty) {
434
+ const diagnostic = fixedPreviewMode && !presenterMode &&
435
+ (surfaceDiagnostic || (layoutTarget && collectSlideLayout(layoutTarget, navIndex)));
436
+ if (!fixedPreviewMode || !diagnostic || empty || presenterMode) {
434
437
  document.body.classList.remove("fixed-preview-overflow");
435
438
  if (warning) {
436
439
  warning.hidden = true;
@@ -441,7 +444,6 @@ function updateFixedPreviewWarning() {
441
444
  return;
442
445
  }
443
446
 
444
- const diagnostic = collectSlideLayout(layoutTarget, navIndex);
445
447
  document.body.classList.toggle("fixed-preview-overflow", diagnostic.pdfClipped);
446
448
  if (button) button.dataset.state = diagnostic.pdfClipped ? "error" : "active";
447
449
  syncMoreControls();
@@ -462,20 +464,17 @@ function updateFixedPreviewWarning() {
462
464
  warning.hidden = false;
463
465
  }
464
466
 
465
- function updateFixedPreviewScale() {
466
- if (!fixedPreviewMode) return;
467
- const availableWidth = Math.max(1, window.innerWidth - 24);
468
- const availableHeight = Math.max(1, window.innerHeight - 88);
469
- const scale = Math.min(availableWidth / OUTPUT_WIDTH, availableHeight / OUTPUT_HEIGHT, 1);
470
- document.body.style.setProperty("--fixed-preview-scale", String(scale));
471
- }
472
-
473
467
  function refreshLayout() {
474
468
  const target = layoutTarget;
475
469
  if (!target || !target.deck.isConnected) return;
476
470
  if (target.autoSize) applyAutoSize(target.deck, target.bodyEl);
477
471
  updateBodyScroll(target.bodyEl);
478
472
  updateFixedPreviewWarning();
473
+ if (surfaceMode && !document.body.classList.contains("mermaid-loading")) {
474
+ window.frameElement?.dispatchEvent(new CustomEvent("slide-layout", {
475
+ detail: collectSlideLayout(target, navIndex),
476
+ }));
477
+ }
479
478
  }
480
479
 
481
480
  // Coalesce the (re)layout into one frame: several triggers — render, resize,
@@ -965,6 +964,19 @@ function renderSlide(markdown) {
965
964
  architectureEditors = [];
966
965
  applyCustomThemeCss(customThemeCss);
967
966
  const token = ++renderToken;
967
+ if (!surfaceMode && (fixedPreviewMode || presenterViewOpen)) {
968
+ const { meta } = splitFrontMatter(lastMarkdown);
969
+ document.title = meta.title || meta.deck || "Slide";
970
+ document.documentElement.setAttribute("data-theme",
971
+ deckThemeLocked ? deckTheme : normalizeTheme(meta.theme || deckTheme));
972
+ layoutTarget = null;
973
+ surfaceDiagnostic = null;
974
+ if (layoutFrame) cancelAnimationFrame(layoutFrame);
975
+ layoutFrame = 0;
976
+ if (!presenterViewRequested || !presenterViewAvailable || navTotal <= 0) updateSlideViewports();
977
+ updateFixedPreviewWarning();
978
+ return;
979
+ }
968
980
  const slide = createSlide(markdown, deckTheme);
969
981
  document.title = slide.title;
970
982
  document.documentElement.setAttribute("data-theme", slide.theme);
@@ -1000,13 +1012,23 @@ function renderSlide(markdown) {
1000
1012
  .catch((error) => {
1001
1013
  if (token !== renderToken) return;
1002
1014
  console.error(error.message);
1003
- showExportNotification("error", error.message);
1015
+ if (surfaceMode) {
1016
+ window.frameElement?.dispatchEvent(new CustomEvent("slide-error", { detail: error.message }));
1017
+ } else {
1018
+ showExportNotification("error", error.message);
1019
+ }
1004
1020
  });
1005
- const mermaid = runMermaid(slide.bodyEl, slide.deck, token, false).finally(() => {
1021
+ const mermaid = Promise.resolve(document.fonts?.ready).then(() => {
1022
+ if (token === renderToken) return runMermaid(slide.bodyEl, slide.deck, token, false);
1023
+ }).finally(() => {
1006
1024
  if (token === renderToken) scheduleLayoutRefresh();
1007
1025
  });
1008
- Promise.all([mermaid, images]).finally(() => {
1009
- if (token === renderToken) document.body.classList.remove("mermaid-loading");
1026
+ Promise.all([mermaid, images]).finally(async () => {
1027
+ await afterLayout();
1028
+ if (token === renderToken) {
1029
+ document.body.classList.remove("mermaid-loading");
1030
+ refreshLayout();
1031
+ }
1010
1032
  });
1011
1033
  }
1012
1034
 
@@ -3149,7 +3171,7 @@ async function fetchDeck() {
3149
3171
  function setArchitectureEditMode(enabled) {
3150
3172
  const next = Boolean(enabled) && architectureEditAvailable && !presenterMode;
3151
3173
  if (next === architectureEditMode) return false;
3152
- if (next && fixedPreviewMode) setFixedPreviewMode(false);
3174
+ if (next && fixedPreviewMode) setFixedPreviewMode(false, { rerender: false });
3153
3175
  architectureEditMode = next;
3154
3176
  document.body.classList.toggle("architecture-edit-mode", next);
3155
3177
  updateArchitectureEditButton(next);
@@ -3255,7 +3277,8 @@ async function requestArchitectureEditMode(enabled) {
3255
3277
  }
3256
3278
 
3257
3279
  function architectureDiagramOptions() {
3258
- return [...document.querySelectorAll(".architecture-diagram[data-architecture-block]")].map(
3280
+ const slideDocument = document.getElementById("outputFrame")?.contentDocument || document;
3281
+ return [...slideDocument.querySelectorAll(".architecture-diagram[data-architecture-block]")].map(
3259
3282
  (wrapper, index) => ({
3260
3283
  block: Number(wrapper.dataset.architectureBlock),
3261
3284
  title: wrapper.dataset.architectureTitle || `Diagram ${index + 1}`,
@@ -3581,6 +3604,10 @@ async function fetchState() {
3581
3604
  // re-fetches /state for an instant update (without waiting for the SSE nudge).
3582
3605
  async function navigate(payload) {
3583
3606
  if (!navigationEnabled) return;
3607
+ if (surfaceMode) {
3608
+ window.frameElement?.dispatchEvent(new CustomEvent("slide-navigate", { detail: payload }));
3609
+ return;
3610
+ }
3584
3611
  try {
3585
3612
  const res = await fetch("./navigate", {
3586
3613
  method: "POST",
@@ -3900,26 +3927,29 @@ async function exportFromCanvas(format, { mermaidImageFallback = false } = {}) {
3900
3927
  }
3901
3928
  }
3902
3929
 
3903
- function setFixedPreviewMode(enabled) {
3904
- fixedPreviewMode = Boolean(enabled);
3930
+ function setFixedPreviewMode(enabled, { rerender = true } = {}) {
3931
+ fixedPreviewMode = presenterMode || Boolean(enabled);
3905
3932
  document.body.classList.toggle("fixed-preview-mode", fixedPreviewMode);
3906
- document.body.classList.toggle("fixed-output-mode", fixedPreviewMode);
3933
+ document.body.classList.toggle("responsive-preview-mode", !fixedPreviewMode);
3907
3934
  const button = document.getElementById("navFixedPreview");
3908
3935
  if (button) {
3909
3936
  button.setAttribute("aria-pressed", fixedPreviewMode ? "true" : "false");
3910
3937
  button.dataset.state = fixedPreviewMode ? "active" : "";
3911
3938
  button.title = fixedPreviewMode
3912
- ? "Return to responsive canvas layout"
3939
+ ? "Switch to responsive editing layout (not output-equivalent)"
3913
3940
  : "Preview PDF layout at 16:9";
3941
+ button.querySelector(".nav-more-label").textContent =
3942
+ fixedPreviewMode ? "Output preview" : "Responsive editing";
3914
3943
  }
3915
3944
  syncMoreControls();
3916
- if (fixedPreviewMode) {
3917
- updateFixedPreviewScale();
3918
- } else {
3919
- document.body.style.removeProperty("--fixed-preview-scale");
3945
+ if (!fixedPreviewMode) {
3946
+ outputViewport?.dispose();
3947
+ outputViewport = null;
3948
+ document.getElementById("stage").classList.remove("slide-viewport");
3920
3949
  document.body.classList.remove("fixed-preview-overflow");
3921
3950
  }
3922
- scheduleLayoutRefresh();
3951
+ surfaceDiagnostic = null;
3952
+ if (rerender && currentVersion >= 0) renderSlide(lastMarkdown);
3923
3953
  updateFixedPreviewWarning();
3924
3954
  }
3925
3955
 
@@ -3927,6 +3957,103 @@ function toggleFixedPreviewMode() {
3927
3957
  setFixedPreviewMode(!fixedPreviewMode);
3928
3958
  }
3929
3959
 
3960
+ function slideViewportState(markdown, index, interactive) {
3961
+ return {
3962
+ markdown, index, total: navTotal, theme: deckTheme, themeLocked: deckThemeLocked,
3963
+ customThemeCss, customThemeMeta, navigationEnabled: interactive,
3964
+ };
3965
+ }
3966
+
3967
+ function mountSlideViewport(host, id, title, interactive) {
3968
+ return createSlideViewport(host, {
3969
+ id, title,
3970
+ onNavigate: (payload) => { if (interactive) navigate(payload); },
3971
+ onPointer: () => { if (interactive) setMoreControlsOpen(false); },
3972
+ onError: (message) => showExportNotification("error", message),
3973
+ onKey: (detail) => {
3974
+ if (!interactive) return;
3975
+ const event = new KeyboardEvent("keydown", { ...detail, bubbles: true, cancelable: true });
3976
+ document.dispatchEvent(event);
3977
+ detail.handled = event.defaultPrevented;
3978
+ },
3979
+ onLayout: (diagnostic) => {
3980
+ if (id === "presenterNext") return;
3981
+ surfaceDiagnostic = diagnostic;
3982
+ document.body.classList.remove("mermaid-loading");
3983
+ updateFixedPreviewWarning();
3984
+ },
3985
+ });
3986
+ }
3987
+
3988
+ function updateSlideViewports() {
3989
+ const stage = document.getElementById("stage");
3990
+ document.body.classList.add("mermaid-loading");
3991
+ if (presenterViewOpen) {
3992
+ stage.replaceChildren();
3993
+ currentViewport ??= mountSlideViewport(
3994
+ document.getElementById("presenterCurrentViewport"), "presenterCurrent", "Current slide", true,
3995
+ );
3996
+ currentViewport.setState(slideViewportState(lastMarkdown, navIndex, true));
3997
+ const hasNext = navMode !== "deck" || navIndex < navTotal - 1;
3998
+ if (hasNext) {
3999
+ const index = Math.min(navIndex + 1, navTotal - 1);
4000
+ nextViewport ??= mountSlideViewport(
4001
+ document.getElementById("presenterNextViewport"), "presenterNext", "Next slide", false,
4002
+ );
4003
+ nextViewport.setState(slideViewportState(deckSlides[index] || "", index, false));
4004
+ }
4005
+ } else {
4006
+ if (!outputViewport) {
4007
+ stage.replaceChildren();
4008
+ stage.classList.add("slide-viewport");
4009
+ outputViewport = mountSlideViewport(stage, "outputFrame", "Slide", navigationEnabled);
4010
+ }
4011
+ outputViewport.setState(slideViewportState(lastMarkdown, navIndex, navigationEnabled));
4012
+ }
4013
+ }
4014
+
4015
+ function initSlideSurface() {
4016
+ surfaceMode = true;
4017
+ presenterMode = true;
4018
+ document.body.classList.add("surface-mode", "fixed-output-mode", "presenter-mode", "preview-mode");
4019
+ let previousState = "";
4020
+ window.__markdstageSurface = {
4021
+ render(state) {
4022
+ const key = JSON.stringify(state);
4023
+ if (key === previousState) {
4024
+ refreshLayout();
4025
+ return;
4026
+ }
4027
+ previousState = key;
4028
+ deckTheme = state.theme;
4029
+ deckThemeLocked = state.themeLocked;
4030
+ applyCustomThemeCss(state.customThemeCss);
4031
+ customThemeMeta = state.customThemeMeta;
4032
+ navIndex = state.index;
4033
+ navTotal = state.total;
4034
+ navigationEnabled = state.navigationEnabled;
4035
+ renderSlide(state.markdown);
4036
+ },
4037
+ };
4038
+ wirePointerNavigation();
4039
+ document.addEventListener("pointerdown", () => {
4040
+ window.frameElement?.dispatchEvent(new CustomEvent("slide-pointer"));
4041
+ });
4042
+ document.addEventListener("keydown", (event) => {
4043
+ if (!navigationEnabled || event.defaultPrevented) return;
4044
+ const target = event.target;
4045
+ if (target?.closest("input, textarea, select, [contenteditable], button, a")) return;
4046
+ const detail = {
4047
+ key: event.key, code: event.code, repeat: event.repeat,
4048
+ ctrlKey: event.ctrlKey, altKey: event.altKey, metaKey: event.metaKey, shiftKey: event.shiftKey,
4049
+ };
4050
+ window.frameElement?.dispatchEvent(new CustomEvent("slide-key", { detail }));
4051
+ if (detail.handled) event.preventDefault();
4052
+ });
4053
+ window.addEventListener("resize", scheduleLayoutRefresh);
4054
+ document.fonts?.addEventListener("loadingdone", scheduleLayoutRefresh);
4055
+ }
4056
+
3930
4057
  function updateNav() {
3931
4058
  const nav = document.getElementById("nav");
3932
4059
  if (!nav) return;
@@ -3956,14 +4083,9 @@ function openPresenterView() {
3956
4083
  document.body.classList.add("presenter-view-mode");
3957
4084
  const view = document.getElementById("presenterView");
3958
4085
  if (view) view.hidden = false;
3959
- const current = document.getElementById("presenterCurrent");
3960
- const next = document.getElementById("presenterNext");
3961
- if (current && !current.getAttribute("src")) {
3962
- current.setAttribute("src", "./?preview=1&offset=0&navigate=1");
3963
- }
3964
- if (next && !next.getAttribute("src")) {
3965
- next.setAttribute("src", "./?preview=1&offset=1");
3966
- }
4086
+ outputViewport?.dispose();
4087
+ outputViewport = null;
4088
+ renderSlide(lastMarkdown);
3967
4089
  updatePresenterView();
3968
4090
  }
3969
4091
 
@@ -3972,6 +4094,11 @@ function closePresenterView() {
3972
4094
  document.body.classList.remove("presenter-view-mode");
3973
4095
  const view = document.getElementById("presenterView");
3974
4096
  if (view) view.hidden = true;
4097
+ currentViewport?.dispose();
4098
+ nextViewport?.dispose();
4099
+ currentViewport = null;
4100
+ nextViewport = null;
4101
+ renderSlide(lastMarkdown);
3975
4102
  document.getElementById("navMore")?.focus();
3976
4103
  }
3977
4104
 
@@ -3984,7 +4111,7 @@ function updatePresenterView() {
3984
4111
  if (prev) prev.disabled = navMode === "deck" && navIndex <= 0;
3985
4112
  if (next) next.disabled = navMode === "deck" && navIndex >= navTotal - 1;
3986
4113
  const hasNext = navMode !== "deck" || navIndex < navTotal - 1;
3987
- const nextFrame = document.getElementById("presenterNext");
4114
+ const nextFrame = document.getElementById("presenterNextViewport");
3988
4115
  const nextEmpty = document.getElementById("presenterNextEmpty");
3989
4116
  if (nextFrame) nextFrame.hidden = !hasNext;
3990
4117
  if (nextEmpty) nextEmpty.hidden = hasNext;
@@ -4512,6 +4639,10 @@ function init() {
4512
4639
  } catch (_) {}
4513
4640
 
4514
4641
  const params = new URLSearchParams(window.location.search);
4642
+ if (params.get("surface") === "1") {
4643
+ initSlideSurface();
4644
+ return;
4645
+ }
4515
4646
  presenterViewRequested = params.get("presenter") === "1";
4516
4647
  if (params.get("pptx") === "1") {
4517
4648
  initPptx(params).catch(reportPptxBootstrapFailure);
@@ -4546,15 +4677,12 @@ function init() {
4546
4677
  requestArchitectureEditMode(true);
4547
4678
  }
4548
4679
 
4549
- // Canvas and CLI preview start on the fixed 16:9 output surface. Presenter
4550
- // views keep their purpose-built layouts, and the control still lets users
4551
- // switch back to the responsive canvas layout.
4552
- if (
4553
- !presenterMode &&
4554
- !presenterViewRequested &&
4555
- params.get("responsive") !== "1"
4556
- ) {
4680
+ // Only the explicitly requested editing layout is responsive. Audience and
4681
+ // Desktop previews use the same isolated surface as Canvas and CLI previews.
4682
+ if (presenterMode || params.get("responsive") !== "1") {
4557
4683
  setFixedPreviewMode(true);
4684
+ } else {
4685
+ setFixedPreviewMode(false);
4558
4686
  }
4559
4687
 
4560
4688
  updateArchitectureEditButton();
@@ -4564,7 +4692,6 @@ function init() {
4564
4692
  wirePreviewKeyboardNavigation();
4565
4693
  }
4566
4694
  window.addEventListener("resize", () => {
4567
- updateFixedPreviewScale();
4568
4695
  scheduleLayoutRefresh();
4569
4696
  });
4570
4697
  if (document.fonts?.ready) {
@@ -0,0 +1,48 @@
1
+ export const OUTPUT_WIDTH = 1280;
2
+ export const OUTPUT_HEIGHT = 720;
3
+
4
+ // Scale the browsing context, not its contents: SVG text measurement and viewport
5
+ // units must see the same untransformed geometry at every display size.
6
+ export function createSlideViewport(host, { id, title, onNavigate, onKey, onPointer, onLayout, onError }) {
7
+ const frame = document.createElement("iframe");
8
+ frame.id = id;
9
+ frame.title = title;
10
+ frame.className = "slide-viewport-frame";
11
+ frame.width = OUTPUT_WIDTH;
12
+ frame.height = OUTPUT_HEIGHT;
13
+ frame.src = "./?surface=1";
14
+ let state = null;
15
+ let loaded = false;
16
+ const render = () => {
17
+ if (loaded && state) frame.contentWindow.__markdstageSurface.render(state);
18
+ };
19
+ frame.addEventListener("load", () => {
20
+ loaded = true;
21
+ render();
22
+ });
23
+ frame.addEventListener("slide-navigate", (event) => onNavigate?.(event.detail));
24
+ frame.addEventListener("slide-key", (event) => onKey?.(event.detail));
25
+ frame.addEventListener("slide-pointer", () => onPointer?.());
26
+ frame.addEventListener("slide-layout", (event) => onLayout?.(event.detail));
27
+ frame.addEventListener("slide-error", (event) => onError?.(event.detail));
28
+ const resize = () => {
29
+ const scale = Math.min(host.clientWidth / OUTPUT_WIDTH, host.clientHeight / OUTPUT_HEIGHT);
30
+ frame.style.transform = `translate(-50%, -50%) scale(${scale})`;
31
+ };
32
+ const observer = new ResizeObserver(resize);
33
+ host.appendChild(frame);
34
+ observer.observe(host);
35
+ resize();
36
+ return {
37
+ setState(next) {
38
+ state = next;
39
+ frame.tabIndex = next.navigationEnabled ? 0 : -1;
40
+ render();
41
+ },
42
+ dispose() {
43
+ observer.disconnect();
44
+ frame.remove();
45
+ state = null;
46
+ },
47
+ };
48
+ }
@@ -95,10 +95,14 @@
95
95
  html,body{height:100%;margin:0;overflow:hidden}
96
96
  #stage{height:100%}
97
97
  body.fixed-preview-mode #stage{
98
- position:fixed;left:50%;top:calc(50% - 24px);width:1280px;height:720px;
99
- transform:translate(-50%,-50%) scale(var(--fixed-preview-scale,1));
100
- transform-origin:center;box-shadow:0 18px 60px rgba(0,0,0,.38);
98
+ position:fixed;inset:12px 12px 76px;height:auto;
101
99
  }
100
+ .slide-viewport{position:relative;overflow:hidden;}
101
+ .slide-viewport-frame{position:absolute;left:50%;top:50%;width:1280px;height:720px;
102
+ max-width:none;max-height:none;border:0;transform-origin:center;display:block;}
103
+ body.presenter-mode.fixed-preview-mode #stage{inset:0;background:#000;}
104
+ body.surface-mode #stage{width:1280px;height:720px;}
105
+ body.surface-mode #nav,body.surface-mode .layout-warning{display:none!important;}
102
106
  .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;
103
107
  clip:rect(0,0,0,0);white-space:nowrap;border:0;}
104
108
  body{
@@ -493,6 +497,7 @@ body.presenter-mode #navPresenterView{display:none;}
493
497
  body.presenter-mode #navExport,body.presenter-mode #navExportPptx{display:none;}
494
498
  body.presenter-mode #navImport{display:none;}
495
499
  body.presenter-mode #navSourceMode{display:none;}
500
+ body.presenter-mode #navFixedPreview{display:none;}
496
501
  body.preview-mode .nav,body.preview-mode .overview{display:none!important;}
497
502
  /* When no deck is loaded, leave only the load button as the available action. */
498
503
  .nav.nav-empty .nav-main,.nav.nav-empty #navMore{display:none;}
@@ -634,7 +639,7 @@ body.presenter-view-mode .layout-warning{display:none;}
634
639
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
635
640
 
636
641
  /* ===== Fixed 16:9 output surface ===== */
637
- /* PDF, PNG capture, and the in-canvas fixed preview share these rules. At the
642
+ /* PDF, PNG capture, and all fixed slide viewports share these rules. At the
638
643
  CSS reference pixel density, 1280x720 equals the PDF's 13.333333in x 7.5in. */
639
644
  body.fixed-output-mode .deck{
640
645
  width:1280px;height:720px;min-width:1280px;max-width:1280px;