fb-slides 0.10.0 → 0.11.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/runtime/edit.js CHANGED
@@ -1,10 +1,13 @@
1
1
  // ---------------------------------------------------------------------------
2
2
  // RevealEdit — edit the slide you are looking at, save it back into its .md.
3
3
  //
4
- // A reveal.js plugin, dev-server only in effect: at init it probes the edit
5
- // API that `fb-slides dev` mounts, and only an answer makes the button appear.
6
- // A built deck is static files, the probe 404s, and this plugin does nothing —
7
- // which is how the same runtime ships in both without configuration.
4
+ // A reveal.js plugin, localhost and dev-server only in effect. At init it first
5
+ // looks at the address bar: anywhere but localhost — a published deck, a phone
6
+ // on the LAN — it stops right there and never asks. On localhost it probes the
7
+ // edit API that `fb-slides dev` mounts, and only an answer makes the button
8
+ // appear: `preview` and any other static server 404 the probe, and this plugin
9
+ // does nothing — which is how the same runtime ships in both without
10
+ // configuration.
8
11
  //
9
12
  // The drawer opens down the right edge with the slide still on stage, one
10
13
  // textarea for the slide and one for the speaker notes. Typing re-renders the
@@ -56,6 +59,12 @@ const joinNotes = ({ content, label, notes }) =>
56
59
  // one without a CORS preflight, which the dev server never answers.
57
60
  const API_HEADERS = { 'x-fb-slides': '1' };
58
61
 
62
+ // Mirror of LOCAL_HOST in lib/edit.mjs: the edit API refuses any other Host,
63
+ // so asking from a published deck — or a phone on the LAN — could only ever
64
+ // cost a 404 in the console. location.hostname carries no port, `[::1]` keeps
65
+ // its brackets, and a file:// page has an empty one.
66
+ const LOCAL_HOST = /^(localhost|127\.0\.0\.1|\[::1\])$/;
67
+
59
68
  const EDIT_ICON = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
60
69
  stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
61
70
  <path d="M11 5H6a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2v-5" />
@@ -84,10 +93,14 @@ const RevealEdit = () => ({
84
93
  id: 'edit',
85
94
 
86
95
  init(deck) {
87
- // Nothing is built until the API answers: on a static host the fetch fails
88
- // or comes back as someone's 404 page, the JSON parse throws, and the deck
89
- // never knows this plugin existed. Init itself stays synchronous so a slow
90
- // or absent server cannot hold up the whole deck.
96
+ // Not on localhost: not even a question. The server would refuse the
97
+ // answer anyway, and a published deck should not log a 404 on every load.
98
+ if (!LOCAL_HOST.test(location.hostname)) return;
99
+
100
+ // Nothing is built until the API answers: under `preview` or any static
101
+ // server the fetch comes back as a 404 page, the JSON parse throws, and
102
+ // the deck never knows this plugin existed. Init itself stays synchronous
103
+ // so a slow or absent server cannot hold up the whole deck.
91
104
  fetch('/api/editable', { headers: API_HEADERS, cache: 'no-cache' })
92
105
  .then((response) => response.json())
93
106
  .then((answer) => {
@@ -132,6 +145,12 @@ const build = (deck, assetsDir) => {
132
145
  <button type="button" class="deck-edit-nav" data-go="1" title="Next slide" aria-label="Next slide">${NEXT_ICON}</button>
133
146
  <button type="button" class="deck-edit-close" aria-label="Close">${CLOSE_ICON}</button>
134
147
  </header>
148
+ <div class="deck-edit-actions">
149
+ <button type="button" class="deck-edit-add" title="Add a slide after this one">Add page</button>
150
+ <button type="button" class="deck-edit-delete" title="Delete this slide">Delete page</button>
151
+ <span class="deck-edit-status" role="status"></span>
152
+ <button type="button" class="deck-edit-save">Save</button>
153
+ </div>
135
154
  <div class="deck-edit-row">
136
155
  <label class="deck-edit-label" for="deck-edit-source">slide</label>
137
156
  <span class="deck-edit-row-acts">
@@ -143,12 +162,6 @@ const build = (deck, assetsDir) => {
143
162
  <textarea id="deck-edit-source" spellcheck="false"></textarea>
144
163
  <label class="deck-edit-label" for="deck-edit-notes">speaker notes</label>
145
164
  <textarea id="deck-edit-notes" spellcheck="false"></textarea>
146
- <footer>
147
- <button type="button" class="deck-edit-add" title="Add a slide after this one">Add page</button>
148
- <button type="button" class="deck-edit-delete" title="Delete this slide">Delete page</button>
149
- <span class="deck-edit-status" role="status"></span>
150
- <button type="button" class="deck-edit-save">Save</button>
151
- </footer>
152
165
  <div class="deck-edit-confirm" hidden>
153
166
  <p>Delete this slide from <code></code>?<br />There is no undo.</p>
154
167
  <div>
@@ -303,10 +316,12 @@ const build = (deck, assetsDir) => {
303
316
  deck.layout();
304
317
  };
305
318
 
306
- // `focus` names the box that gets the room and the cursor — navigation and
307
- // save-reloads pass the one the drawer already had, so following the deck
308
- // never folds the notes away under a writing hand.
309
- const open = async (focus = 'source') => {
319
+ // `focus` names the box that gets the room — navigation and save-reloads
320
+ // pass the one the drawer already had, so following the deck never folds
321
+ // the notes away under a writing hand. `takeFocus` says whether that box
322
+ // also gets the cursor: opening by hand always does, following the deck
323
+ // only when the cursor was already in the drawer.
324
+ const open = async (focus = 'source', { takeFocus = true } = {}) => {
310
325
  const slide = deck.getCurrentSlide();
311
326
  const src = SRC(slide);
312
327
  if (!src) return;
@@ -354,7 +369,7 @@ const build = (deck, assetsDir) => {
354
369
  // stand this drawer down in turn when one of them arms (see below).
355
370
  document.dispatchEvent(new CustomEvent('deck:tool-armed', { detail: 'edit' }));
356
371
  setOpen(true);
357
- (focus === 'notes' ? notesArea : sourceArea).focus();
372
+ if (takeFocus) (focus === 'notes' ? notesArea : sourceArea).focus();
358
373
  };
359
374
 
360
375
  const toggle = () => (editing ? close() : open());
@@ -442,6 +457,11 @@ const build = (deck, assetsDir) => {
442
457
  // stage thing — and the drawer follows through `slidechanged` like any other
443
458
  // navigation.
444
459
  for (const nav of panel.querySelectorAll('.deck-edit-nav')) {
460
+ // The press itself would move the focus onto the button (Chrome does),
461
+ // and the drawer could no longer tell a writing hand from a browsing one
462
+ // when the new slide lands. Held back, the cursor stays where it was —
463
+ // in a box, or out on the deck.
464
+ nav.addEventListener('mousedown', (event) => event.preventDefault());
445
465
  nav.addEventListener('click', () => {
446
466
  if (Number(nav.dataset.go) < 0) deck.left({ skipFragments: true });
447
467
  else deck.right({ skipFragments: true });
@@ -541,12 +561,16 @@ const build = (deck, assetsDir) => {
541
561
 
542
562
  // The drawer follows the deck: landing on another slide loads that one,
543
563
  // and whatever was typed on the old one goes the way of a cancel. The
544
- // overview is a different surface altogether, so there it does close.
564
+ // cursor only comes back to the box it was in — arrowing through the deck
565
+ // from outside the drawer must not pull it in. The overview is a different
566
+ // surface altogether, so there it does close.
545
567
  deck.on('slidechanged', async () => {
546
568
  if (!editing) return;
547
569
  const focus = panel.dataset.focus;
570
+ const active = document.activeElement;
571
+ const takeFocus = active === sourceArea || active === notesArea;
548
572
  discard();
549
- await open(focus);
573
+ await open(focus, { takeFocus });
550
574
  // The new slide would not load — a drawer left standing would lie about
551
575
  // what it edits.
552
576
  if (!editing) setOpen(false);
@@ -192,6 +192,49 @@ const BUILT_IN = [
192
192
  keys: 'appear step reveal element attach',
193
193
  body: '${content}\n<!-- .element: class="fragment" -->',
194
194
  },
195
+ {
196
+ group: 'Text',
197
+ label: 'Eyebrow chips',
198
+ hint: 'the line above the title — pills, or a // rule, depending on the theme',
199
+ keys: 'tag label kicker pill badge topic',
200
+ top: true,
201
+ body: '<p class="chips"><span>${Angular}</span><span>Signals</span></p>',
202
+ },
203
+ {
204
+ group: 'Text',
205
+ label: 'Rows',
206
+ hint: 'two or three things, each with a glyph and one sentence',
207
+ keys: 'cards list compare items icon',
208
+ body:
209
+ '<div class="rows">\n' +
210
+ ' <div class="row">\n' +
211
+ ' <span class="row-icon">${1}</span>\n' +
212
+ ' <div><strong>Name</strong><p>What it is for.</p></div>\n' +
213
+ ' </div>\n' +
214
+ ' <div class="row">\n' +
215
+ ' <span class="row-icon">2</span>\n' +
216
+ ' <div><strong>Name</strong><p>What it is for.</p></div>\n' +
217
+ ' </div>\n' +
218
+ '</div>',
219
+ },
220
+ {
221
+ group: 'Text',
222
+ label: 'Call to action',
223
+ hint: 'the last slide — the first one is filled, both grow an arrow',
224
+ keys: 'cta end follow subscribe outro links',
225
+ body:
226
+ '<div class="rows rows--cta">\n' +
227
+ ' <div class="row"><div><strong>${Save the post}</strong></div></div>\n' +
228
+ ' <div class="row"><div><strong>Follow me</strong></div></div>\n' +
229
+ '</div>',
230
+ },
231
+ {
232
+ group: 'Text',
233
+ label: 'Code, with its file name',
234
+ hint: 'the marker draws the header bar — the language comes from the extension',
235
+ keys: 'fence filename snippet header tab',
236
+ body: '<!-- file: ${cart.component.ts} -->\n```ts\nconst total = computed(() => 0);\n```',
237
+ },
195
238
  {
196
239
  group: 'Text',
197
240
  label: 'Table',
@@ -122,7 +122,14 @@ body,
122
122
  -webkit-background-clip: text;
123
123
  background-clip: text;
124
124
  color: transparent;
125
- margin-bottom: 0.15em;
125
+ /* The gradient only paints inside the element's box, and with
126
+ `background-clip: text` a glyph is invisible wherever it is not painted.
127
+ A pack that tightens line-height under a display serif (both shipped
128
+ packs do) leaves descenders and tall accents outside the line box, so
129
+ they were cut. Padding widens the painted box; the margins take the
130
+ same amount back so nothing on the slide moves. */
131
+ padding: 0.15em 0.1em 0.3em 0;
132
+ margin: -0.15em -0.1em -0.15em 0;
126
133
  }
127
134
  .reveal .slides section.deck-title h2 {
128
135
  color: var(--muted);
@@ -266,6 +273,10 @@ html[data-reveal-theme] .reveal strong { color: var(--r-heading-color, var(--tex
266
273
  .reveal .slides section > table tbody tr:nth-child(even) { background: rgb(255 255 255 / 0.018); }
267
274
  .reveal .slides section > table td:first-child { color: var(--muted); white-space: nowrap; }
268
275
  .reveal .slides section > table th:first-child:empty { min-width: 8ch; }
276
+ /* A table written with an empty header row (`| | |`) is a two-column layout —
277
+ a term and what it means — not a table with nameless columns. Drawing the bar
278
+ anyway puts an empty grey strip across the slide, so it goes. */
279
+ .reveal .slides section > table thead:not(:has(th:not(:empty))) { display: none; }
269
280
 
270
281
  /* --- fragments ----------------------------------------------------------- */
271
282
 
@@ -424,6 +435,163 @@ html[data-reveal-theme] .reveal strong { color: var(--r-heading-color, var(--tex
424
435
  }
425
436
  .reveal .slides .box > :last-child { margin-bottom: 0; }
426
437
 
438
+ /* --- eyebrow chips ------------------------------------------------------- */
439
+ /* `<p class="chips"><span>Angular</span><span>Signals</span></p>` — the line
440
+ above the title that says which talk this is, and where in it you are. The
441
+ first chip is the loud one; the rest ride along quieter, because a row of
442
+ equally loud labels says nothing. One span is simply one pill.
443
+
444
+ A theme pack is free to make this a `// ANGULAR · SIGNALS` rule instead —
445
+ same markup, same meaning, different voice. */
446
+ .reveal .slides .chips {
447
+ display: flex;
448
+ flex-wrap: wrap;
449
+ align-items: center;
450
+ gap: 0.45em;
451
+ margin: 0 0 1.1em;
452
+ font-size: 0.46em;
453
+ line-height: 1;
454
+ }
455
+ .reveal .slides .chips span {
456
+ padding: 0.62em 1em;
457
+ border: 1px solid var(--line);
458
+ border-radius: 999px;
459
+ background: color-mix(in srgb, var(--card) 60%, transparent);
460
+ color: var(--muted);
461
+ font-family: var(--mono);
462
+ letter-spacing: 0.01em;
463
+ white-space: nowrap;
464
+ }
465
+ .reveal .slides .chips span:first-child {
466
+ background: var(--accent);
467
+ border-color: var(--accent);
468
+ color: var(--bg);
469
+ font-weight: 650;
470
+ }
471
+
472
+ /* --- rows ---------------------------------------------------------------- */
473
+ /* A list that reads as cards: one line each, an optional glyph in a tile at the
474
+ left, the name and one sentence about it. It is the shape a comparison slide
475
+ keeps wanting — two things, what each is for — and a <ul> cannot hold it
476
+ without turning into a table nobody can read from the back of the room.
477
+
478
+ `<div class="rows">`, then per item `<div class="row">` holding an optional
479
+ `<span class="row-icon">`, a `<strong>` and a `<p>`. */
480
+ .reveal .slides .rows {
481
+ display: flex;
482
+ flex-direction: column;
483
+ gap: 0.55em;
484
+ margin: 0 0 0.7em;
485
+ }
486
+ .reveal .slides .row {
487
+ display: flex;
488
+ align-items: flex-start;
489
+ gap: 0.75em;
490
+ padding: 0.75em 0.9em;
491
+ border: 1px solid var(--line);
492
+ border-radius: 14px;
493
+ background: color-mix(in srgb, var(--card) 70%, transparent);
494
+ }
495
+ .reveal .slides .row-icon {
496
+ flex: none;
497
+ display: grid;
498
+ place-items: center;
499
+ width: 1.9em;
500
+ height: 1.9em;
501
+ border-radius: 12px;
502
+ background: var(--accent);
503
+ color: var(--bg);
504
+ font-family: var(--mono);
505
+ font-size: 0.85em;
506
+ font-weight: 700;
507
+ line-height: 1;
508
+ }
509
+ /* The text column, whether or not it was wrapped in one. */
510
+ .reveal .slides .row > :not(.row-icon) { min-width: 0; }
511
+ .reveal .slides .row strong {
512
+ display: block;
513
+ margin-bottom: 0.1em;
514
+ font-size: 0.92em;
515
+ }
516
+ .reveal .slides .row p {
517
+ margin: 0;
518
+ color: var(--muted);
519
+ font-size: 0.7em;
520
+ line-height: 1.4;
521
+ }
522
+
523
+ /* `<div class="rows rows--cta">` — the last slide's two ways out. Each row
524
+ grows an arrow at its right edge, and the first one is the one you actually
525
+ want pressed, so it is the one that is filled. Same rule as the chips: in a
526
+ row of equals nothing is being asked. */
527
+ .reveal .slides .rows--cta .row { align-items: center; }
528
+ .reveal .slides .rows--cta .row::after {
529
+ content: '\2192';
530
+ margin-left: auto;
531
+ padding-left: 0.7em;
532
+ font-size: 0.85em;
533
+ opacity: 0.7;
534
+ }
535
+ .reveal .slides .rows--cta .row:first-child {
536
+ background: var(--accent);
537
+ border-color: var(--accent);
538
+ color: var(--bg);
539
+ }
540
+ .reveal .slides .rows--cta .row:first-child p,
541
+ .reveal .slides .rows--cta .row:first-child::after {
542
+ color: color-mix(in srgb, var(--bg) 72%, var(--accent));
543
+ opacity: 1;
544
+ }
545
+ /* `.reveal strong` is white, which on a filled accent row is the one colour it
546
+ must not be: the label has to read against the fill, not against the deck. */
547
+ .reveal .slides .rows--cta .row:first-child strong { color: inherit; }
548
+
549
+ /* --- a code block that says which file it is ----------------------------- */
550
+ /* `<!-- file: cart.component.ts -->` on the line above a fence (deck.js lifts
551
+ it onto the <pre>) draws a header bar: the name on the left, the language on
552
+ the right. The language is not written anywhere — it is the `language-*`
553
+ class the highlighter already applied, uppercased.
554
+
555
+ The bar is the <pre>'s own ::before rather than an element, so the markup a
556
+ deck carries stays a plain fenced code block and Marp still renders it. */
557
+ .reveal pre[data-file] { padding-top: 2.1em; }
558
+ .reveal pre[data-file]::before {
559
+ content: attr(data-file);
560
+ position: absolute;
561
+ top: 0;
562
+ left: 0;
563
+ right: 0;
564
+ height: 2.1em;
565
+ display: flex;
566
+ align-items: center;
567
+ padding: 0 1.2em;
568
+ box-sizing: border-box;
569
+ border-bottom: 1px solid var(--line);
570
+ background: color-mix(in srgb, var(--card) 70%, #14181f);
571
+ color: var(--muted);
572
+ font-family: var(--mono);
573
+ font-size: 0.85em;
574
+ /* Above the stack of absolutely positioned <code> steps, which start at the
575
+ top of the block and would otherwise slide under the bar. */
576
+ z-index: 1;
577
+ }
578
+ .reveal pre[data-lang]::after {
579
+ content: attr(data-lang);
580
+ position: absolute;
581
+ top: 0;
582
+ right: 1.2em;
583
+ height: 2.1em;
584
+ display: flex;
585
+ align-items: center;
586
+ color: var(--accent);
587
+ font-family: var(--mono);
588
+ font-size: 0.72em;
589
+ letter-spacing: 0.08em;
590
+ z-index: 2;
591
+ }
592
+ /* The header sits in the <pre>'s padding; the code keeps its own. */
593
+ .reveal pre[data-file] code { border-radius: 0 0 10px 10px; }
594
+
427
595
  /* --- the author slide ---------------------------------------------------- */
428
596
  /* `<!-- .slide: class="author-slide" -->` turns a slide into the speaker page:
429
597
  photos filling the left edge to edge, the bio centred on the right. Who you
@@ -630,7 +798,9 @@ html[data-reveal-theme] .reveal strong { color: var(--r-heading-color, var(--tex
630
798
 
631
799
  /* Hidden until it is wanted: on stage the slide is the point, so the toolbar
632
800
  sleeps just above the top edge of the screen. Parking the mouse along that
633
- edge — or arming a tool, or tabbing into it — slides it back down. */
801
+ edge — or arming a tool, or tabbing into it — slides it back down, and down
802
+ it stays: `is-open` is a latch annotate.js sets, and only the X chip (or the
803
+ X key) lifts it. Neither the mouse wandering off nor a tool disarming does. */
634
804
  #deck-tools {
635
805
  position: fixed;
636
806
  top: 14px;
@@ -686,19 +856,13 @@ html[data-reveal-theme] .reveal strong { color: var(--r-heading-color, var(--tex
686
856
  0%, 100% { transform: translateX(-50%) translateY(0); }
687
857
  50% { transform: translateX(-50%) translateY(3px); }
688
858
  }
689
- #deck-tools:hover,
690
- #deck-tools:focus-within,
691
- #deck-tools.is-armed {
859
+ #deck-tools.is-open {
692
860
  transform: translateX(-50%) translateY(0);
693
861
  }
694
862
  /* Once the toolbar is out, the chevron has done its job — the strip stays as
695
- a silent hover target. */
696
- #deck-tools:hover::before,
697
- #deck-tools:focus-within::before,
698
- #deck-tools.is-armed::before { cursor: default; }
699
- #deck-tools:hover::after,
700
- #deck-tools:focus-within::after,
701
- #deck-tools.is-armed::after { opacity: 0; }
863
+ a silent surface. */
864
+ #deck-tools.is-open::before { cursor: default; }
865
+ #deck-tools.is-open::after { opacity: 0; }
702
866
 
703
867
  .deck-tool {
704
868
  position: relative;
@@ -739,6 +903,21 @@ html[data-reveal-theme] .reveal strong { color: var(--r-heading-color, var(--tex
739
903
  background: color-mix(in srgb, var(--text) 10%, transparent);
740
904
  color: var(--text);
741
905
  }
906
+ /* The X: last on the strip whatever order the plugins hung their chips in,
907
+ and set a little apart from the tools it puts away. */
908
+ .deck-tools-close {
909
+ order: 1;
910
+ margin-left: 6px;
911
+ }
912
+ .deck-tools-close::before {
913
+ content: '';
914
+ position: absolute;
915
+ left: -5px;
916
+ top: 8px;
917
+ bottom: 8px;
918
+ width: 1px;
919
+ background: var(--line);
920
+ }
742
921
  /* An armed tool wears the colour it draws in. annotate.js owns the pen's
743
922
  palette — it is a list the toolbar enumerates, not a theme token — and sets
744
923
  `--pen` and its readable foreground on #deck-tools as the colour changes. */
@@ -791,6 +970,90 @@ html[data-reveal-theme] .reveal strong { color: var(--r-heading-color, var(--tex
791
970
  0 0 14px color-mix(in srgb, var(--ink) 65%, transparent);
792
971
  }
793
972
 
973
+ /* --- the theme switcher (themes.js, dev only) ---------------------------- */
974
+ /* Hung below the toolbar like the swatches, for the same reason: the toolbar
975
+ comes down from the top edge, so down is where the room is. It exists only
976
+ under `fb-slides dev` — a built deck is rendered without the list this reads,
977
+ and the plugin never gets as far as creating any of it. */
978
+ #deck-themes {
979
+ position: absolute;
980
+ top: calc(100% + 9px);
981
+ left: 50%;
982
+ transform: translateX(-50%);
983
+ min-width: 200px;
984
+ padding: 6px;
985
+ /* Reveal ships fifteen themes and the packs sit above them: the list is
986
+ taller than a toolbar menu should be, so it scrolls rather than running off
987
+ the bottom of a laptop screen. */
988
+ max-height: min(60vh, 420px);
989
+ overflow-y: auto;
990
+ overscroll-behavior: contain;
991
+ background: color-mix(in srgb, var(--card) 92%, transparent);
992
+ border: 1px solid var(--line);
993
+ border-radius: 12px;
994
+ backdrop-filter: blur(6px);
995
+ box-shadow: 0 14px 40px rgb(0 0 0 / 0.4);
996
+ scrollbar-width: thin;
997
+ scrollbar-color: var(--outline-bar) transparent;
998
+ }
999
+ #deck-themes::-webkit-scrollbar { width: 5px; }
1000
+ #deck-themes::-webkit-scrollbar-track { background: transparent; }
1001
+ #deck-themes::-webkit-scrollbar-thumb { background: var(--outline-bar); border-radius: 999px; }
1002
+ #deck-themes::-webkit-scrollbar-thumb:hover { background: var(--outline-bar-hover); }
1003
+
1004
+ /* The two kinds, told apart. Not sticky, unlike the outline's deck headers:
1005
+ there are only two of them, and two sticky headings at the same offset pile
1006
+ up on each other the moment the list is scrolled. */
1007
+ #deck-themes > h2 {
1008
+ margin: 12px 0 4px;
1009
+ padding: 6px 10px 5px;
1010
+ font: 600 9.5px/1 var(--sans);
1011
+ letter-spacing: 0.12em;
1012
+ text-transform: uppercase;
1013
+ color: var(--muted);
1014
+ background: var(--bg-2);
1015
+ border: 0;
1016
+ }
1017
+ #deck-themes[hidden] { display: none; }
1018
+
1019
+ /* Open, not armed — the pressed look belongs to the tools that draw. */
1020
+ #tool-theme[aria-expanded='true'] {
1021
+ background: color-mix(in srgb, var(--text) 12%, transparent);
1022
+ color: var(--text);
1023
+ }
1024
+
1025
+ .deck-theme-row {
1026
+ display: block;
1027
+ width: 100%;
1028
+ padding: 7px 10px;
1029
+ border: 0;
1030
+ border-radius: 8px;
1031
+ background: transparent;
1032
+ color: var(--muted);
1033
+ font: 400 12.5px/1.2 var(--mono);
1034
+ text-align: left;
1035
+ cursor: pointer;
1036
+ transition: background 0.14s ease, color 0.14s ease;
1037
+ }
1038
+ .deck-theme-row:hover { background: color-mix(in srgb, var(--text) 10%, transparent); color: var(--text); }
1039
+ .deck-theme-row[aria-selected='true'] {
1040
+ background: color-mix(in srgb, var(--accent) 22%, transparent);
1041
+ color: var(--text);
1042
+ }
1043
+
1044
+ /* What it takes to keep the choice: the switcher previews, slides.config.js
1045
+ decides. Saying so here is cheaper than being asked. */
1046
+ .deck-theme-note {
1047
+ position: sticky;
1048
+ bottom: -6px;
1049
+ margin: 8px 4px 0;
1050
+ padding: 5px 0 2px;
1051
+ background: var(--bg-2);
1052
+ color: var(--muted);
1053
+ font: 400 10.5px/1.35 var(--mono);
1054
+ opacity: 0.75;
1055
+ }
1056
+
794
1057
  /* --- the outline sidebar (outline.js) ------------------------------------ */
795
1058
 
796
1059
  /* The panel does not float over the deck, it takes room from it: the slide
@@ -1321,11 +1584,14 @@ body.edit-open #deck-edit { transform: none; }
1321
1584
  .deck-edit-ink .hljs-comment,
1322
1585
  .deck-edit-ink .hljs-meta { color: var(--muted); font-weight: 400; }
1323
1586
 
1324
- #deck-edit > footer {
1587
+ /* The file operations sit in a bar under the header, over the boxes: what a
1588
+ save or a delete acts on is the whole slide, not the box the cursor is in. */
1589
+ .deck-edit-actions {
1325
1590
  display: flex;
1326
1591
  align-items: center;
1327
1592
  gap: 10px;
1328
- padding: 12px 16px 14px;
1593
+ padding: 10px 16px;
1594
+ border-bottom: 1px solid var(--line);
1329
1595
  }
1330
1596
  .deck-edit-status {
1331
1597
  flex: 1;