@hypersocial/decks-cli 0.9.10 → 0.9.11

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": "@hypersocial/decks-cli",
3
- "version": "0.9.10",
3
+ "version": "0.9.11",
4
4
  "description": "Command-line client for Hypersocial Decks live analytics and share management.",
5
5
  "type": "module",
6
6
  "main": "./src/commands.mjs",
@@ -308,6 +308,20 @@ its `##` heading.
308
308
  | `glass` | dark or busy slides: head and captions on one frosted panel |
309
309
  | `corner` | the slide is the point (a product shot, a chart, a demo): small head bottom-right, subtitles along the bottom |
310
310
 
311
+ **Loading and buffering.** A deck opens on its loading card (the title and
312
+ the presenter's still), and a line still loading shows as a small waiting
313
+ pill. Once the deck has spoken, a line that stalls on the network for more
314
+ than 0.3 s makes the slide play a short
315
+ "signal" glitch in the deck's accents (`--accent-2` / `--accent-3`, else
316
+ `--accent`; set `--hd-signal-1` / `--hd-signal-2` to choose others): the
317
+ title's colour splits, thin scanlines and a slice pass over the slide, and a
318
+ `SIGNAL // BUFFERING` tag sits in the top-left corner.
319
+ Body text never moves; it calms to scanlines after 6 s and stops the moment
320
+ sound plays. It never shows in thumbnails, exports, present mode or while the
321
+ owner presents, and reduced motion gets a still dim and the tag. Where it
322
+ doesn't fit the deck, turn it off with `data-loading="calm"` on the player tag
323
+ (or on `<html>`, or `"loading": "calm"` in the manifest).
324
+
311
325
  Size it per slide with `data-presenter-size="s|m|l|xl"` on the `<section>`
312
326
  (0.7×, 1×, 1.3×, 1.6× the style's head; on the player tag it is the deck's
313
327
  default): `xl` on a slide with empty space, `s` on a busy product slide. Phones
@@ -431,8 +445,8 @@ slide that overflows a 390×844 phone.
431
445
 
432
446
  #### House style (preferences)
433
447
 
434
- Everything below is the owner's house style, not a rule: follow it by
435
- default, and depart from it when the brief asks for something else.
448
+ The owner's house style, not rules: follow it by default; depart from it
449
+ when the brief asks.
436
450
 
437
451
  **One idea per slide.** The title states the idea as a sentence a listener
438
452
  could repeat ("Narration renders in two minutes", not "Speed"). Everything
@@ -451,16 +465,15 @@ new sizes:
451
465
  | `--t-body` | 24px | 15px | list items, card text |
452
466
  | `--t-label` | 17px | 12px | kickers, sources, captions (mono, uppercase) |
453
467
 
454
- Nothing on a slide below 17px at stage size (a screenshot's frame chrome aside). A title over 28 characters needs
455
- `data-title-mobile`; write the short version as a real sentence.
468
+ Nothing on a slide below 17px at stage size (a screenshot's frame chrome aside). A title over
469
+ 28 characters needs `data-title-mobile`, written as a real sentence.
456
470
 
457
471
  **Colour and contrast.** Three colours do the work: `--ink`, `--paper` and one
458
- `--accent` (a second, `--accent-2`, only for a chart's other series, an
459
- illustration or a placeholder face). Text is ink on paper or accent, or paper on ink: never accent
472
+ `--accent` (`--accent-2` only for a chart's second series or a placeholder
473
+ face). Text is ink on paper or accent, or paper on ink: never accent
460
474
  on paper, never grey on a colour. Alternate slide grounds (`on-accent`,
461
475
  plain, `on-ink`) so the slide changes read, and keep every number, source and
462
- label at full contrast. Change the palette and fonts once, in `:root`; every
463
- layout reads them.
476
+ label at full contrast. Set the palette and fonts once, in `:root`.
464
477
 
465
478
  **Start from the brand.** The account keeps a brand: colours, fonts, a logo,
466
479
  a presenter style and a design.md (voice, do and don't). `deck new` and
@@ -468,16 +481,14 @@ a presenter style and a design.md (voice, do and don't). `deck new` and
468
481
  it first (`hyperdecks brand show --json`, `get_brand`) and write in its voice.
469
482
  An explicit request for this deck wins over the brand; `--no-brand`
470
483
  (`brand: false`) starts from the template's look. No brand yet? Offer to
471
- import it from what they already have: `hyperdecks brand import
472
- <url|file.pptx|file.pdf|deck-slug>` (`import_brand`) reads the real colours,
473
- fonts and logo (a PowerPoint's theme exactly) and returns a preview link, a
474
- sample slide in that brand, and a token. Always show them the preview and
475
- ask; save with `brand set --draft <token>` (`set_brand { draft }`) only after
476
- they say yes.
477
- To change it in words ("darker accent, serif titles"), `hyperdecks brand
478
- revise "<change>"` (`revise_brand`) gives a draft and preview the same way.
479
- Every brand save is kept in its history: `brand history` (`brand_history`)
480
- lists the last changes and `brand undo [<version>]` (`undo_brand`) goes back.
484
+ import it: `hyperdecks brand import <url|file.pptx|file.pdf|deck-slug>`
485
+ (`import_brand`) reads the real colours, fonts and logo (a PowerPoint's theme
486
+ exactly) and returns a preview link of a sample slide and a token. Show them
487
+ the preview; save with `brand set --draft <token>` (`set_brand { draft }`)
488
+ only after they say yes.
489
+ To change it in words, `brand revise "<change>"` (`revise_brand`) gives a
490
+ draft and preview the same way; `brand history` (`brand_history`) lists past
491
+ saves and `brand undo [<version>]` (`undo_brand`) goes back.
481
492
 
482
493
  **Fonts: pick a pairing.** Fonts load from Google Fonts. Use one of the
483
494
  curated pairings below (display for titles, body for text, mono for kickers
@@ -489,13 +500,19 @@ saves one.
489
500
  **Textures, icons, pictures:** the `look` topic (one texture, one icon set
490
501
  from `search_icons` or `generate_icon_set`, a picture on every other slide).
491
502
 
492
- **Pick the layout from what the slide says.** Each layout's file in
493
- `templates/layouts/` opens with its slot side, presenter and the cues that
494
- suit it.
503
+ **Pick the layout from what the slide says** (each file in
504
+ `templates/layouts/` opens with its slot, presenter and cues).
505
+
506
+ **The cover never puts text on the picture.** The title and lead sit on a
507
+ solid ground, never on a picture or one faded
508
+ with a gradient `mask-image` (`check` warns).
495
509
 
496
510
  | Layout | Use it for | Presenter, moves |
497
511
  |---|---|---|
498
- | `hero` | the opening, a section opener; the presenter is the visual, no `.art` unless it shows the real subject (check fails while the placeholder `<div class="art">` is in: swap it for an `<img class="art">` or delete it) | stage, bottom-right; `[wave]`, `[look: h1]` |
512
+ | `hero` | the default cover, a section opener: title on the ground, the real subject framed beside it (no `.art`: the presenter is the visual) | stage; `[wave]`, `[look: h1]` |
513
+ | `cover-band` | the picture is the story: full bleed, title on a panel | as `hero` |
514
+ | `cover-poster` | a wide scene: picture on top, title below | as `hero` |
515
+ | `cover-type` | no real picture yet: type only, presenter large | as `hero` |
499
516
  | `number` | one number with its source: proof, traction, a delta | stage; `[look: #<id>-figure]` as it is said |
500
517
  | `product` | a screenshot in a browser frame (`.frame.card` for a phone) with the point beside it | stage; `[look: #<id>-frame]`, `[reveal: #<id>-callout]` |
501
518
  | `split` | the workhorse: a real picture (phone, laptop, 16:9) beside sticker, title, three ticks | stage, bottom-left; `[look: #<id>-frame]`, `[go: #<id>-t1]` … `t3` |
@@ -631,7 +648,7 @@ an app icon or a face), `.ticks`, `.panel`, `.pill`, `.frame.card` (a phone),
631
648
  `<concrete subject>, <setting>, <medium: e.g. editorial photo, soft daylight>,
632
649
  palette <ink hex> <accent hex> <paper hex>, no text, no logos`
633
650
  (`generate_image { brand: true }` adds the palette and the "no text" for
634
- you). Generate at the slide's shape (wide for a hero), in one batch.
651
+ you). Generate at the slide's shape (16:9; near-square for the hero), in one batch.
635
652
 
636
653
  **Slide recipes.** Opener: `on-accent` + `bg-corner`, the logo
637
654
  or a few app-icon stickers, one display line. Product or feature: `split`
@@ -764,6 +781,8 @@ you do).
764
781
  clip that says "Minimoji" but is transcribed as "Moji" needs
765
782
  `normalize: moji=minimoji`, and with a second entry
766
783
  `normalize: moji=minimoji, dash=D.A.S.H.` (one line, not a YAML list).
784
+ British and American spellings of a word (centre/center, colour/color,
785
+ organise/organize, travelled/traveled) already pass: no entry needed.
767
786
  - Write a narrator's name as it is spoken when the two differ, or leave it to
768
787
  the narrator's spoken name: `narrator list` shows it, and narrate reads
769
788
  "D.A.S.H." as "Dash" (captions keep what you wrote).
@@ -825,7 +844,8 @@ with the player's `hyperdecks.ai` script tag. Messages and what to do:
825
844
  | `forbidden phrase` | remove it from the page, script or narration |
826
845
  | `slide <id>: needs a data-note` / ⚠ `no data-note` | add the source (required on a `data-kind="pitch"` deck and with `--strict`) |
827
846
  | `slide <id>: title … needs a data-title-mobile` | add a shorter title |
828
- | `slide <id>: the hero's art` / `the product screenshot` / `the chart … is still the layout's placeholder` | make the real file (`gen-image`, `screenshot <url>`, `chart <spec.json>`) and swap the element for the `<img>` the message names; or delete the hero's `<div class="art">` for no art |
847
+ | `slide <id>: the hero's art` / `the cover's picture` / `the product screenshot` / `the chart … is still the layout's placeholder` | make the real file (`gen-image`, `screenshot <url>`, `chart <spec.json>`) and swap the element for the `<img>` the message names; or delete the hero's `<div class="art">` for no art (on `cover-band` / `cover-poster`, switch to `cover-type` instead) |
848
+ | ⚠ `slide <id>: the … cover fades its picture into the ground with a mask-image` | the old hero's look: the title sits on picture and colour mixed. Move the deck's opening to a current cover (`hero`, `cover-band`, `cover-poster`, `cover-type`), or drop the mask and keep the title off the picture |
829
849
  | `… presenter style "…" is not one of` | use stage, bubble, glass or corner |
830
850
  | `script.md line N: [go: …] matches nothing in slide <id>` | fix the selector, or add the element to that slide |
831
851
  | `… transition "…" is not one of` | use drift, cut, push, wipe, zoom `<selector>`, morph or fly |
@@ -46,9 +46,11 @@ makes the hello deck; continue from step 5 below.
46
46
  `upload_deck_asset` for a picture the user gave you (a URL, or a file they
47
47
  uploaded). Each returns a hosted URL: put that URL in the HTML. A hosted deck
48
48
  has no folder, so a relative path like `img/a.png` is an error. The
49
- layouts' stand-ins (hero's `<div class="art">`, product's
49
+ layouts' stand-ins (the covers' `<div class="art">`, product's
50
50
  `<div class="shot">`, chart's placeholder `<svg>`) fail `check_deck` until
51
51
  you swap each for an `<img>` of its real file, or delete the hero's art.
52
+ Open on a cover (`hero`, `cover-band`, `cover-poster` or `cover-type`,
53
+ deck_guide topic layouts) and never put the title or lead on the picture.
52
54
  Icons: one set per deck (`look` topic): `search_icons { query }` for real
53
55
  line icons as inline `<svg>` in `currentColor`, or one `generate_icon_set`.
54
56
  For motion, `generate_video` (poll `check_video_job`): `loop: true` for a
package/src/brand.mjs CHANGED
@@ -118,6 +118,21 @@ export function cleanBrand(brand) {
118
118
  return { name, colors, fonts, logoUrl, presenterStyle, hasDesignMd };
119
119
  }
120
120
 
121
+ // The templates' own ink, paper and accent (layouts/_deck.html, narrated-deck.html), for a brand that sets only some colours.
122
+ const TEMPLATE_INK = "#0b0b0f", TEMPLATE_PAPER = "#fafaf7", TEMPLATE_ACCENT = "#ffda1b";
123
+ // WCAG relative luminance and contrast ratio of two #rgb / #rrggbb colours.
124
+ function luminance(hex) {
125
+ const h = hex.replace("#", ""), full = h.length === 3 ? [...h].map((c) => c + c).join("") : h.slice(0, 6);
126
+ const [r, g, b] = [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16) / 255).map((c) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4));
127
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
128
+ }
129
+ export const contrast = (a, b) => { const [x, y] = [luminance(a), luminance(b)].sort((m, n) => n - m); return (x + 0.05) / (y + 0.05); };
130
+ // Which of ink and paper the text on the accent is: ink unless it falls under 4.5:1 and paper reads better.
131
+ export function accentInk(accent, ink, paper) {
132
+ const onInk = contrast(accent, ink);
133
+ return onInk < 4.5 && contrast(accent, paper) > onInk ? "paper" : "ink";
134
+ }
135
+
121
136
  const escapeAttr = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
122
137
  const fontStack = (family, role) => `"${family}", ${FALLBACK[role]}`;
123
138
  const LOGO_CSS = `
@@ -141,6 +156,10 @@ export function applyBrand(html, brand) {
141
156
  block = decl.test(block) ? block.replace(decl, (_, k) => `${k}${value}`) : block.replace(/\s*\}$/, (end) => ` ${name}: ${value};${end}`);
142
157
  };
143
158
  for (const [k, v] of Object.entries(b.colors)) set(CSS_VAR[k], v);
159
+ // Text on an accent ground reads --accent-ink (ink by default): a dark accent
160
+ // that ink cannot be read on takes the paper instead, so a cover on the accent stays legible.
161
+ const { accent = TEMPLATE_ACCENT, ink = TEMPLATE_INK, paper = TEMPLATE_PAPER } = b.colors;
162
+ if ((b.colors.accent || b.colors.ink || b.colors.paper) && accentInk(accent, ink, paper) === "paper") set("--accent-ink", "var(--paper)");
144
163
  if (b.fonts.body) set("--font", fontStack(b.fonts.body, "body"));
145
164
  if (b.fonts.mono) set("--mono", fontStack(b.fonts.mono, "mono"));
146
165
  if (b.fonts.display) {
@@ -72,6 +72,7 @@ export const MESSAGES = {
72
72
  transitionStale: (where, script, manifest) => `${where}: script.md says transition "${script}" but the manifest has "${manifest}": run \`hyperdecks narrate\` so the player gets it`,
73
73
  flyTwice: (ids) => `fly is for the deck's one big moment, but slides ${ids.join(", ")} all use it`,
74
74
  // Warnings from the look topic (deck_guide look): the deck passes, but a person should look.
75
+ coverFade: (where, layout) => `${where}: the ${layout} cover fades its picture into the ground with a mask-image, so the title sits on a mix of picture and colour that is hard to read: keep the text off the picture (layouts hero, cover-band, cover-poster or cover-type; deck_guide topic layouts)`,
75
76
  fewPictures: (shown, total) => `only ${shown} of ${total} slides show a picture (a screenshot, a recording, a photo, a chart); aim for a real one on every other slide (deck_guide topic look)`,
76
77
  presenterGutter: (where, decl) => `${where}: ${decl} keeps a right gutter for the presenter; leave that corner to the slide's .narration-slot so a deck without a presenter can use it (deck_guide topic look)`,
77
78
  mixedIcons: (sets) => `the deck mixes icon sets (${sets.join(" and ")}); keep one set per deck (deck_guide topic look)`,
@@ -316,6 +317,19 @@ export function slides(html) {
316
317
  });
317
318
  }
318
319
 
320
+ // Each slide's titles written short for phones: { id, titles: [{ desktop, phone }] },
321
+ // only where the phone text differs. The player swaps data-title-mobile in below
322
+ // 650px, so review must not read the shorter phone title as cut off.
323
+ export function mobileTitles(html) {
324
+ return slides(html).map(({ id, body }) => ({
325
+ id,
326
+ titles: [...body.matchAll(/<([a-z][\w-]*)\b([^>]*\sdata-title-mobile\s*=[^>]*)>([\s\S]*?)<\/\1>/gi)]
327
+ .map(([, , attrs, inner]) => ({ desktop: textOf(inner), phone: attr(attrs, "data-title-mobile") }))
328
+ .filter((t) => t.phone != null && decode(t.phone).trim() && decode(t.phone).trim() !== t.desktop)
329
+ .map((t) => ({ desktop: t.desktop.slice(0, 200), phone: decode(t.phone).trim().slice(0, 200) })),
330
+ })).filter((s) => s.id && s.titles.length);
331
+ }
332
+
319
333
  // The player tag also carries the deck's declarations: data-silent (no
320
334
  // narration, on purpose) and data-kind="pitch" (every slide sources its facts).
321
335
  // The check reads what the deck says, never guesses from its content.
@@ -535,6 +549,14 @@ const PLACEHOLDERS = [
535
549
  layout: "l-hero", tag: "div", cls: "art", what: "the hero's art", img: '<img class="art" src="…" alt="">, or delete the div for no art (the presenter is the visual)',
536
550
  how: { hosted: "make a picture of the real subject with generate_image", cli: "make a picture of the real subject with `hyperdecks gen-image \"<prompt>\"`" },
537
551
  },
552
+ {
553
+ layout: "l-cover-band", tag: "div", cls: "art", what: "the cover's picture", img: '<img class="art" src="…" alt="">, or use the cover-type layout for no picture',
554
+ how: { hosted: "make a wide picture of the real subject with generate_image", cli: "make a wide picture of the real subject with `hyperdecks gen-image \"<prompt>\"`" },
555
+ },
556
+ {
557
+ layout: "l-cover-poster", tag: "div", cls: "art", what: "the cover's picture", img: '<img class="art" src="…" alt="">, or use the cover-type layout for no picture',
558
+ how: { hosted: "make a wide picture of the real subject with generate_image", cli: "make a wide picture of the real subject with `hyperdecks gen-image \"<prompt>\"`" },
559
+ },
538
560
  {
539
561
  layout: "l-product", tag: "div", cls: "shot", what: "the product screenshot", img: '<img class="shot" src="…" alt="…">',
540
562
  how: { hosted: "capture the real page with capture_url", cli: "capture the real page with `hyperdecks screenshot <url>`" },
@@ -814,10 +836,27 @@ export function lookWarnings(html) {
814
836
  if (iconImg(tag)) sets.add("image icons");
815
837
  }
816
838
  if (sets.size > 1) out.push(MESSAGES.mixedIcons([...sets].sort()));
817
- out.push(...textureWarnings(all, html));
839
+ out.push(...textureWarnings(all, html), ...coverFadeWarnings(all, html));
818
840
  return out;
819
841
  }
820
842
 
843
+ // Covers never put text on the picture. Where the text overlaps a picture can only be
844
+ // measured by rendering, so this reads the one source pattern that always does: a cover
845
+ // layout whose CSS fades its picture with a gradient mask-image (the old hero's look,
846
+ // still inlined in decks made from it). A deck's own picture and text placement beyond
847
+ // that is left to the layouts and the review.
848
+ export const COVER_LAYOUTS = ["l-hero", "l-cover-band", "l-cover-poster", "l-cover-type"];
849
+ function coverFadeWarnings(all, html) {
850
+ const css = [...uncommented(html).matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi)].map((m) => m[1].replace(/\/\*[\s\S]*?\*\//g, "")).join("\n");
851
+ const faded = new Set();
852
+ for (const [, sel, body] of css.matchAll(/([^{}]*)\{([^{}]*)\}/g)) {
853
+ if (!/(?:^|;|\s)(?:-webkit-)?mask(?:-image)?\s*:[^;]*gradient\(/i.test(body)) continue;
854
+ for (const l of COVER_LAYOUTS) if (new RegExp(`\\.${l}(?![\\w-])`).test(sel)) faded.add(l);
855
+ }
856
+ if (!faded.size) return [];
857
+ return all.flatMap((s, i) => classes(s.tag).filter((c) => faded.has(c)).map((c) => MESSAGES.coverFade(`slide ${s.id || i + 1}`, c.slice(2))));
858
+ }
859
+
821
860
  // Backgrounds (the look topic): the kit's textures are confined to the object they
822
861
  // back. The retired whole-slide textures warn rather than vanish silently (a deck made
823
862
  // before keeps their rules in its own inlined shell, where they still paint). Read from each slide's class attribute; a texture on a layout whose shell
@@ -832,10 +871,11 @@ const RETIRED_TEXTURES = {
832
871
  const SIGNATURES = ["bg-corner", "bg-mark"];
833
872
  export const MAX_SIGNATURE_SLIDES = 3;
834
873
  const BUSY_LAYOUTS = ["l-number", "l-features", "l-team", "l-timeline", "l-compare"]; // full-width content: nothing to back
874
+ const PICTURE_COVERS = ["l-cover-band", "l-cover-poster"]; // the picture is the ground: a texture would only sit under it
835
875
  export const TEXTURE_OFF = {
836
- "bg-corner": ["l-split", "l-product", "l-chart", "l-quote", ...BUSY_LAYOUTS],
837
- "bg-stage": ["l-split", "l-chart", "l-quote", "l-closer", ...BUSY_LAYOUTS],
838
- "bg-mark": ["l-split", "l-product", "l-chart", "l-phones", ...BUSY_LAYOUTS],
876
+ "bg-corner": ["l-split", "l-product", "l-chart", "l-quote", ...BUSY_LAYOUTS, ...PICTURE_COVERS],
877
+ "bg-stage": ["l-split", "l-chart", "l-quote", "l-closer", ...BUSY_LAYOUTS, ...PICTURE_COVERS, "l-cover-type"],
878
+ "bg-mark": ["l-split", "l-product", "l-chart", "l-phones", ...BUSY_LAYOUTS, ...PICTURE_COVERS],
839
879
  };
840
880
  function textureWarnings(all, html) {
841
881
  const css = [...uncommented(html).matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi)].map((m) => m[1].replace(/\/\*[\s\S]*?\*\//g, "")).join("\n");
package/src/files.mjs CHANGED
@@ -13,6 +13,9 @@ const FILES = {
13
13
  "templates/layouts/closer.html": () => new URL("../templates/layouts/closer.html", import.meta.url),
14
14
  "templates/layouts/compare.html": () => new URL("../templates/layouts/compare.html", import.meta.url),
15
15
  "templates/layouts/features.html": () => new URL("../templates/layouts/features.html", import.meta.url),
16
+ "templates/layouts/cover-band.html": () => new URL("../templates/layouts/cover-band.html", import.meta.url),
17
+ "templates/layouts/cover-poster.html": () => new URL("../templates/layouts/cover-poster.html", import.meta.url),
18
+ "templates/layouts/cover-type.html": () => new URL("../templates/layouts/cover-type.html", import.meta.url),
16
19
  "templates/layouts/hero.html": () => new URL("../templates/layouts/hero.html", import.meta.url),
17
20
  "templates/layouts/number.html": () => new URL("../templates/layouts/number.html", import.meta.url),
18
21
  "templates/layouts/product.html": () => new URL("../templates/layouts/product.html", import.meta.url),
package/src/layouts.mjs CHANGED
@@ -4,8 +4,9 @@
4
4
  import { packageText } from "./files.mjs";
5
5
 
6
6
  export const NARRATED_FILES = { "index.html": "narrated-deck.html", "script.md": "script.md", "evidence.md": "evidence.md" };
7
- // The library, in a sensible deck order; `--layouts` takes any order and repeats.
8
- export const LAYOUTS = ["hero", "number", "product", "split", "phones", "quote", "compare", "timeline", "features", "chart", "team", "closer"];
7
+ // The library: the four covers first (hero is the default; a deck opens on one), then
8
+ // the rest in a sensible deck order. `--layouts` takes any order and repeats.
9
+ export const LAYOUTS = ["hero", "cover-band", "cover-poster", "cover-type", "number", "product", "split", "phones", "quote", "compare", "timeline", "features", "chart", "team", "closer"];
9
10
  const LAYOUT_ALIASES = { "big-number": "number", "product-shot": "product", "phone-row": "phones", comparison: "compare", "three-up": "features", "image-copy": "split", "image-text": "split" };
10
11
 
11
12
  const escapeHtml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
@@ -4,11 +4,17 @@
4
4
  // (lib/loadingBlock.test.ts keeps the three in step). A JS string, not a read of
5
5
  // the template, because Convex (the hosted draft's write) cannot read package files.
6
6
  // No Node imports: the CLI and Convex (and so the MCP route's writes) use it.
7
+ // The opening card (opening.mjs) is the loader: its rules in the style, and the
8
+ // <script data-hd-opening> after it that builds it once the page is parsed. A
9
+ // frame or ?thumb (data-hd-frame) gets the quiet dots instead; an export neither.
10
+ import { OPENING_CSS, OPENING_PAGE_SCRIPT } from "./opening.mjs";
11
+
7
12
  export const LOADING_BLOCK = [
8
13
  "<!-- Loading state (keep verbatim; `hyperdecks check` requires it). Until the",
9
14
  " player marks body[data-hd-ready] the page shows only the slide card, at its",
10
15
  " final size, in the deck's --hd-ground (its first slide's colour) on the",
11
- " page's brand colour, with a quiet loader in --hd-ground-ink; then the",
16
+ " page's brand colour, with the opening card over it (the title and the",
17
+ " presenter's still), or in a frame quiet dots in --hd-ground-ink; then the",
12
18
  " slides fade in. If the player never arrives, the slides show after 10 s",
13
19
  " rather than never. Put data-layout=\"overlay\" on <html> for the overlay. -->",
14
20
  "<link rel=\"modulepreload\" href=\"https://hyperdecks.ai/player/v1/player.js\">",
@@ -22,7 +28,7 @@ export const LOADING_BLOCK = [
22
28
  " /* The card as the player draws it (player.css, framed): 16:9, scaled to fit",
23
29
  " between the 64px masthead and the 56px dock, 24px in from the sides. */",
24
30
  " body::before { inset: 64px 24px 56px; margin: auto; width: min(100vw - 48px, (100dvh - 120px) * 16 / 9); height: min(100dvh - 120px, (100vw - 48px) * 9 / 16); border-radius: calc(min(100vw - 48px, (100dvh - 120px) * 16 / 9) * .01125); background: var(--hd-ground, #fff); box-shadow: 0 0 0 1px color-mix(in srgb, var(--hd-ground-ink, #0b0b0f) 16%, transparent); }",
25
- " body::after { left: 50%; top: 50%; width: 12px; height: 12px; margin: -6px 0 0 -6px; border-radius: 50%; color: var(--hd-ground-ink, #0b0b0f); background: currentColor; box-shadow: -24px 0 currentColor, 24px 0 currentColor; opacity: 0; animation: hd-dots .7s ease-in-out .6s infinite alternate; }",
31
+ " [data-hd-frame] body::after { left: 50%; top: 50%; width: 12px; height: 12px; margin: -6px 0 0 -6px; border-radius: 50%; color: var(--hd-ground-ink, #0b0b0f); background: currentColor; box-shadow: -24px 0 currentColor, 24px 0 currentColor; opacity: 0; animation: hd-dots .7s ease-in-out .6s infinite alternate; }",
26
32
  " body[data-hd-ready]::before, body[data-hd-ready]::after { opacity: 0; animation: none; }",
27
33
  " /* The overlay (embedded, ?thumb, or data-layout=\"overlay\"): the stage fills the window. */",
28
34
  " :is([data-hd-frame], [data-layout=\"overlay\"]) body::before { inset: 0; width: min(100vw, 100dvh * 16 / 9); height: min(100dvh, 100vw * 9 / 16); border-radius: calc(min(100vw, 100dvh * 16 / 9) * .01125); box-shadow: none; }",
@@ -42,21 +48,28 @@ export const LOADING_BLOCK = [
42
48
  " @keyframes hd-in { from { opacity: 0; } }",
43
49
  " @keyframes hd-dots { from { opacity: .15; } to { opacity: .6; } }",
44
50
  " @keyframes hd-fail-open { to { visibility: visible; overflow: auto; } }",
51
+ ...OPENING_CSS.replace(/\n$/, "").split("\n"),
45
52
  "</style>",
53
+ OPENING_PAGE_SCRIPT,
46
54
  ].join("\n");
47
55
 
48
56
  const STYLE = /<style\b[^>]*\sdata-hd-loading(?=[\s>=])[^>]*>[\s\S]*?<\/style>/i;
49
57
  const HEAD = /<head\b[^>]*>[\s\S]*?<\/head>/i;
50
58
  const PLAYER = /<script\b[^>]*\/player\/v\d+\/player\.js"[^>]*>/;
51
59
  const LOADING_STYLE = STYLE.exec(LOADING_BLOCK)[0];
52
- // The block's pieces besides its style, on any player host: the two preloads and
53
- // the frame flag (each kept once), and its own comment (matched only as a whole
60
+ const OPENING_SCRIPT = /<script data-hd-opening>[\s\S]*?<\/script>/;
61
+ const OPENING_SCRIPT_PIECE = new RegExp(`${OPENING_SCRIPT.source}[ \\t]*\\n?`, "g");
62
+ // The script with the line break before it (as the block writes it after the style).
63
+ const OPENING_SCRIPT_LINE = new RegExp(`\\n?[ \\t]*${OPENING_SCRIPT.source}`, "g");
64
+ // The block's pieces besides its style, on any player host: the two preloads,
65
+ // the frame flag and the card's <script data-hd-opening> (each kept once), and its own comment (matched only as a whole
54
66
  // comment, so a person's comments in <head> are never touched).
55
67
  const PIECES = [
56
68
  /<link rel="modulepreload" href="[^"]*\/player\/v\d+\/player\.js">[ \t]*\n?/g,
57
69
  /<link rel="preload" as="style" href="[^"]*\/player\/v\d+\/player\.css">[ \t]*\n?/g,
58
70
  /<script>if \(self !== top \|\| [^<]*document\.documentElement\.dataset\.hdFrame = "";<\/script>[ \t]*\n?/g,
59
71
  new RegExp(`${STYLE.source}[ \\t]*\\n?`, "gi"),
72
+ OPENING_SCRIPT_PIECE,
60
73
  ];
61
74
  const BLOCK_COMMENT = /^<!-- Loading state \(keep verbatim/;
62
75
 
@@ -93,11 +106,13 @@ export function withLoadingBlock(html) {
93
106
  const at = title ? title.index + title[0].length : /^<head\b[^>]*>/i.exec(cleaned)[0].length;
94
107
  return { html: splice(`${cleaned.slice(0, at)}\n${LOADING_BLOCK}${cleaned.slice(at)}`), fixed: "inserted" };
95
108
  }
96
- const deduped = trimPieces(rawHead, 1);
109
+ // The card's script goes right after the style, as the template's (a block
110
+ // from before the card gets it; a stray copy elsewhere goes).
111
+ const deduped = trimPieces(rawHead, 1).split(/(<!--[\s\S]*?-->)/).map((part, i) => (i % 2 ? part : part.replace(OPENING_SCRIPT_LINE, ""))).join("");
97
112
  // The one style left, outside comments, made the template's.
98
113
  const blank = deduped.replace(/<!--[\s\S]*?-->/g, (c) => " ".repeat(c.length));
99
114
  const style = STYLE.exec(blank);
100
- const fresh = deduped.slice(0, style.index) + LOADING_STYLE + deduped.slice(style.index + style[0].length);
115
+ const fresh = deduped.slice(0, style.index) + LOADING_STYLE + "\n" + OPENING_PAGE_SCRIPT + deduped.slice(style.index + style[0].length);
101
116
  if (fresh === rawHead) return { html, fixed: null };
102
- return { html: splice(fresh), fixed: deduped !== rawHead ? "deduped" : "refreshed" };
117
+ return { html: splice(fresh), fixed: trimPieces(rawHead, 1) !== rawHead ? "deduped" : "refreshed" };
103
118
  }
@@ -0,0 +1,117 @@
1
+ // The opening card a deck shows while its player loads (#207): the first
2
+ // slide blurred and breathing where the slide will be, and a glass card with
3
+ // the presenter's still hopping, the title and the player's real boot steps
4
+ // (body[data-hd-boot], player.js). When the player marks body[data-hd-ready]
5
+ // the presenter jumps, the card fades and the slide sharpens.
6
+ //
7
+ // One card, two ways in: a route that serves the deck adds it with the deck's
8
+ // poster and narrator (lib/deckLoadingOverlay.ts), and every deck's own
9
+ // loading state (loadingBlock.mjs, the templates) builds it in the page from
10
+ // what the file has (its <title>, the player tag's data-presenter-image), so a
11
+ // raw deck file opens on it too. The page's own card steps aside when a route
12
+ // already added one. Never in a frame, a thumbnail (?thumb,
13
+ // data-hyperdecks-thumb) or an export (?export); a deck ready within FAST_MS
14
+ // never shows it; if the player never says ready it goes after FAIL_OPEN_MS.
15
+ // No Node imports: the CLI, Convex and the app all use it.
16
+
17
+ export const FAST_MS = 150;
18
+ export const FAIL_OPEN_MS = 10000;
19
+
20
+ // The boot steps player.js reports on body[data-hd-boot], in the order it
21
+ // reaches them: [progress %, copy]. {name} is the presenter's.
22
+ export const BOOT_STEPS = Object.freeze({
23
+ page: [10, "Opening the deck…"],
24
+ player: [38, "Loading the player…"],
25
+ presenter: [62, "Waking up {name}…"],
26
+ narration: [84, "Getting the narration…"],
27
+ });
28
+
29
+ // The card's rules (the route wraps them in <style data-hd-opening>; the
30
+ // loading block carries them in its <style data-hd-loading>).
31
+ export const OPENING_CSS = `#hd-open{position:fixed;inset:0;z-index:2147483000;pointer-events:none;visibility:visible!important;animation:none!important;opacity:0;font-family:var(--hd-ui-font,"Space Grotesk",system-ui,sans-serif);color:#0b0b0f}
32
+ #hd-open.is-shown{opacity:1;transition:opacity .2s ease-out}
33
+ #hd-open.is-gone{opacity:0;transition:opacity .45s ease-out}
34
+ #hd-open .hdo-slide{position:absolute;inset:64px 24px 56px;margin:auto;width:min(100vw - 48px,(100dvh - 120px)*16/9);height:min(100dvh - 120px,(100vw - 48px)*9/16);border-radius:calc(min(100vw - 48px,(100dvh - 120px)*16/9)*.01125);overflow:hidden;container-type:inline-size;background:var(--hd-ground,#fff)}
35
+ #hd-open .hdo-slide img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;filter:blur(2.2cqi) saturate(1.15);transform:scale(1.12);animation:hdo-breathe 2.4s ease-in-out infinite}
36
+ #hd-open .hdo-slide::after{content:"";position:absolute;inset:0;background:linear-gradient(100deg,transparent 30%,rgba(255,255,255,.3) 50%,transparent 70%);background-size:250% 100%;animation:hdo-shim 1.8s ease-in-out infinite;mix-blend-mode:soft-light}
37
+ #hd-open.is-done .hdo-slide img{animation:hdo-deblur .5s cubic-bezier(.2,.8,.2,1) .5s forwards}
38
+ #hd-open.is-done .hdo-slide::after{opacity:0;transition:opacity .3s}
39
+ .hdo-card{position:absolute;left:50%;top:50%;translate:-50% -50%;width:min(86vw,420px);display:grid;justify-items:center;gap:.6em;padding:1.3em 1.5em 1.2em;border-radius:22px;background:rgba(255,255,255,.74);-webkit-backdrop-filter:blur(14px) saturate(1.3);backdrop-filter:blur(14px) saturate(1.3);box-shadow:4px 4px 0 #0b0b0f;border:1.5px solid #0b0b0f;text-align:center;box-sizing:border-box}
40
+ #hd-open.is-done .hdo-card{opacity:0;transform:scale(.96);transition:opacity .25s .5s,transform .25s .5s}
41
+ .hdo-buddy{position:relative;width:92px;height:96px;margin-top:-62px;display:grid;justify-items:center}
42
+ .hdo-hop{width:86px;height:80px;transform-origin:50% 100%;animation:hdo-hop 1.15s cubic-bezier(.3,0,.4,1) infinite}
43
+ .hdo-look{width:100%;height:100%;animation:hdo-look 4.6s ease-in-out infinite}
44
+ .hdo-buddy img{width:100%;height:100%;object-fit:contain;display:block;filter:drop-shadow(0 3px 0 rgba(11,11,15,.18))}
45
+ .hdo-shadow{position:absolute;bottom:4px;width:46px;height:8px;border-radius:50%;background:rgba(11,11,15,.22);animation:hdo-shadow 1.15s cubic-bezier(.3,0,.4,1) infinite}
46
+ .hdo-think{position:absolute;right:-30px;top:-6px;display:flex;gap:3px;padding:5px 8px;border-radius:14px;background:#fff;border:1.5px solid #0b0b0f;box-shadow:2px 2px 0 #0b0b0f}
47
+ .hdo-think::after{content:"";position:absolute;left:4px;bottom:-7px;width:7px;height:7px;border-radius:50%;background:#fff;border:1.5px solid #0b0b0f}
48
+ .hdo-think i{width:5px;height:5px;border-radius:50%;background:#0b0b0f;animation:hdo-tdot 1s ease-in-out infinite}
49
+ .hdo-think i:nth-child(2){animation-delay:.15s}.hdo-think i:nth-child(3){animation-delay:.3s}
50
+ .hdo-spark{position:absolute;left:50%;top:40%;width:8px;height:8px;margin:-4px;opacity:0}
51
+ .hdo-spark::before{content:"";position:absolute;inset:0;background:#0b0b0f;clip-path:polygon(50% 0,62% 38%,100% 50%,62% 62%,50% 100%,38% 62%,0 50%,38% 38%)}
52
+ #hd-open.is-done .hdo-hop{animation:hdo-yay .55s cubic-bezier(.2,1.4,.4,1) forwards}
53
+ #hd-open.is-done :is(.hdo-look,.hdo-shadow){animation:none}
54
+ #hd-open.is-done .hdo-think{opacity:0;transition:opacity .15s}
55
+ #hd-open.is-done .hdo-spark{animation:hdo-spark .6s ease-out forwards}
56
+ .hdo-title{font-weight:700;font-size:18px;line-height:1.2;text-wrap:balance;overflow-wrap:anywhere}
57
+ .hdo-by{font:12.5px/1.3 var(--hd-ui-mono,"JetBrains Mono",ui-monospace,monospace);color:#3d3b35}
58
+ .hdo-bar{width:100%;height:5px;border-radius:999px;background:rgba(11,11,15,.12);overflow:hidden}
59
+ .hdo-bar i{display:block;height:100%;width:8%;background:#0b0b0f;border-radius:999px;transition:width .35s ease-out}
60
+ .hdo-step{font:12px/1.3 var(--hd-ui-mono,"JetBrains Mono",ui-monospace,monospace);color:#55534b;min-height:1.3em}
61
+ @keyframes hdo-breathe{50%{filter:blur(2.6cqi) saturate(1.25) brightness(1.05)}}
62
+ @keyframes hdo-shim{0%{background-position:120% 0}100%{background-position:-120% 0}}
63
+ @keyframes hdo-deblur{to{filter:blur(0);transform:none}}
64
+ @keyframes hdo-hop{0%,100%{transform:translateY(0) scale(1.06,.92)}12%{transform:translateY(0) scale(.95,1.06)}45%{transform:translateY(-14px) scale(1,1)}70%{transform:translateY(0) scale(1.04,.95)}82%{transform:translateY(0) scale(1,1)}}
65
+ @keyframes hdo-shadow{0%,100%{transform:scale(1.05);opacity:.9}45%{transform:scale(.62);opacity:.45}70%{transform:scale(1);opacity:.85}}
66
+ @keyframes hdo-look{0%,18%{transform:rotate(0)}24%,38%{transform:rotate(-9deg) translateX(-2px)}44%,58%{transform:rotate(8deg) translateX(2px)}64%,100%{transform:rotate(0)}}
67
+ @keyframes hdo-tdot{0%,100%{opacity:.25;transform:translateY(0)}40%{opacity:1;transform:translateY(-2px)}}
68
+ @keyframes hdo-yay{0%{transform:translateY(0) scale(1.08,.9)}40%{transform:translateY(-26px) scale(.94,1.08) rotate(-6deg)}100%{transform:translateY(-6px) scale(1) rotate(0)}}
69
+ @keyframes hdo-spark{0%{opacity:0;transform:translate(0,0) scale(.4)}30%{opacity:1}100%{opacity:0;transform:translate(var(--x),var(--y)) scale(1.2) rotate(90deg)}}
70
+ :is([data-hd-frame],[data-layout="overlay"]) #hd-open .hdo-slide{inset:0;width:min(100vw,100dvh*16/9);height:min(100dvh,100vw*9/16)}
71
+ @media (max-width:650px){#hd-open .hdo-slide{inset:52px 8px calc(56px + env(safe-area-inset-bottom));width:auto;height:auto;border-radius:14px}.hdo-card{width:min(84vw,320px);padding:1em 1.1em .95em}.hdo-title{font-size:15.5px}.hdo-buddy{transform:scale(.85);margin-top:-58px}}
72
+ @media (prefers-reduced-motion:reduce){#hd-open *,#hd-open *::before,#hd-open *::after{animation:none!important;transition:none!important}#hd-open .hdo-slide::after{display:none}#hd-open.is-done .hdo-card{opacity:1;transform:none}}
73
+ @media print{#hd-open{display:none!important}}
74
+ `;
75
+
76
+ // Runs the card `o` (#hd-open): shows it after FAST_MS unless the deck is
77
+ // ready, follows data-hd-boot, and on data-hd-ready jumps, fades and removes
78
+ // itself. In a frame, a thumbnail or an export it removes itself at once.
79
+ export const OPENING_RUN = `function(o){var b=document.body,d=document.documentElement;if(!o||!b)return;
80
+ if(self!==top||/[?&](thumb|export)\\b/.test(location.search)||d.hasAttribute("data-hyperdecks-thumb")){o.remove();return}
81
+ var S=${JSON.stringify(BOOT_STEPS)},bar=o.querySelector(".hdo-bar i"),step=o.querySelector(".hdo-step"),who=o.getAttribute("data-name")||"the presenter",at=0,shown=false,done=false,mo;
82
+ var rm=window.matchMedia&&matchMedia("(prefers-reduced-motion: reduce)").matches;
83
+ function boot(){var s=S[b.getAttribute("data-hd-boot")];if(!s||s[0]<=at)return;at=s[0];bar.style.width=s[0]+"%";step.textContent=s[1].split("{name}").join(who)}
84
+ function finish(){if(done)return;done=true;if(mo)mo.disconnect();clearTimeout(t1);clearTimeout(t2);
85
+ if(!shown){o.remove();return}bar.style.width="100%";step.textContent="Ready";
86
+ o.classList.add("is-done");setTimeout(function(){o.classList.add("is-gone")},rm?0:500);setTimeout(function(){o.remove()},rm?500:1000)}
87
+ function check(){if(b.hasAttribute("data-hd-ready"))finish();else boot()}
88
+ var t1=setTimeout(function(){if(!done){shown=true;o.classList.add("is-shown")}},${FAST_MS});
89
+ var t2=setTimeout(finish,${FAIL_OPEN_MS});
90
+ step.textContent=S.page[1];
91
+ if(window.MutationObserver){mo=new MutationObserver(check);mo.observe(b,{attributes:true,attributeFilter:["data-hd-ready","data-hd-boot"]})}
92
+ check()}`;
93
+
94
+ // Five sparks around the presenter for the jump at ready.
95
+ export const OPENING_SPARKS = [[-40, -30], [42, -34], [-30, 10], [36, 6], [0, -46]]
96
+ .map(([x, y]) => `<span class="hdo-spark" style="--x:${x}px;--y:${y}px"></span>`).join("");
97
+ // The presenter's still in the card (its src set by whoever builds it).
98
+ export const OPENING_BUDDY = `<div class="hdo-buddy"><div class="hdo-hop"><div class="hdo-look"><img src="" alt=""></div></div><div class="hdo-shadow"></div><div class="hdo-think"><i></i><i></i><i></i></div>${OPENING_SPARKS}</div>`;
99
+
100
+ // The page's own card (the loading block's <script data-hd-opening>): once the
101
+ // page is parsed (readyState "interactive", before the player module runs),
102
+ // unless a route already added a card, the player is ready, or the page is
103
+ // framed, a thumbnail or an export, it puts a card at the top of <body> with
104
+ // the page's <title> and the player tag's presenter still (none when it
105
+ // presents nobody), with no poster and no narrator's name, and runs it.
106
+ export const OPENING_PAGE_SCRIPT = `<script data-hd-opening>(function(){var d=document;
107
+ if(self!==top||/[?&](thumb|export)\\b/.test(location.search))return;
108
+ function go(){if(d.readyState==="loading")return;d.removeEventListener("readystatechange",go);
109
+ var b=d.body,p=d.querySelector('script[src*="/player/v"][src*="/player.js"]');
110
+ if(!b||!p||d.getElementById("hd-open")||b.hasAttribute("data-hd-ready")||d.documentElement.hasAttribute("data-hyperdecks-thumb"))return;
111
+ var o=d.createElement("div"),img=p.getAttribute("data-presenter")!=="none"&&p.getAttribute("data-presenter-image");
112
+ o.id="hd-open";o.setAttribute("aria-hidden","true");
113
+ o.innerHTML='<div class="hdo-slide"></div><div class="hdo-card">'+(img?${JSON.stringify(OPENING_BUDDY)}:'')+'<div class="hdo-title"></div><div class="hdo-bar"><i></i></div><div class="hdo-step"></div></div>';
114
+ if(img){var i=o.querySelector(".hdo-buddy img");i.addEventListener("error",function(){var y=o.querySelector(".hdo-buddy");if(y)y.remove()});i.src=img}
115
+ o.querySelector(".hdo-title").textContent=(d.title||"").replace(/\\s+/g," ").trim();
116
+ b.insertBefore(o,b.firstChild);(${OPENING_RUN})(o)}
117
+ d.addEventListener("readystatechange",go);go()})();</script>`;
package/src/runtime.mjs CHANGED
@@ -8,7 +8,7 @@ import { fileURLToPath } from "node:url";
8
8
  import { ConvexHttpClient } from "convex/browser";
9
9
  import { makeFunctionReference } from "convex/server";
10
10
 
11
- export const VERSION = "0.9.10";
11
+ export const VERSION = "0.9.11";
12
12
  const UPLOAD_TIMEOUT_MS = 125_000;
13
13
 
14
14
  // The app's home. HYPERDECKS_HOST points the prod target's broker and share
package/src/scaffold.mjs CHANGED
@@ -32,7 +32,7 @@ export async function deckNew(pos, opts = {}, { log = (line) => console.log(line
32
32
  if (!SLUG.test(slug)) throw new Error("slug must be lowercase letters, digits and dashes (max 64)");
33
33
  const dir = resolve(/*turbopackIgnore: true*/ cwd, typeof opts.dir === "string" ? opts.dir : join("decks", slug));
34
34
  if (existsSync(dir)) throw new Error(`${dir} already exists; pick another slug or --dir`);
35
- if (opts.layouts === true) throw new Error(`--layouts needs a list, e.g. --layouts ${LAYOUTS.slice(0, 3).join(",")},closer`);
35
+ if (opts.layouts === true) throw new Error(`--layouts needs a list, e.g. --layouts hero,product,number,closer`);
36
36
  if (opts.layouts !== undefined) parseLayouts(opts.layouts);
37
37
  // The account's brand goes on the template (--no-brand: the template's look);
38
38
  // an explicit --presenter-style wins over the brand's.
@@ -7,7 +7,8 @@
7
7
  <!-- Loading state (keep verbatim; `hyperdecks check` requires it). Until the
8
8
  player marks body[data-hd-ready] the page shows only the slide card, at its
9
9
  final size, in the deck's --hd-ground (its first slide's colour) on the
10
- page's brand colour, with a quiet loader in --hd-ground-ink; then the
10
+ page's brand colour, with the opening card over it (the title and the
11
+ presenter's still), or in a frame quiet dots in --hd-ground-ink; then the
11
12
  slides fade in. If the player never arrives, the slides show after 10 s
12
13
  rather than never. Put data-layout="overlay" on <html> for the overlay. -->
13
14
  <link rel="modulepreload" href="https://hyperdecks.ai/player/v1/player.js">
@@ -21,7 +22,7 @@
21
22
  /* The card as the player draws it (player.css, framed): 16:9, scaled to fit
22
23
  between the 64px masthead and the 56px dock, 24px in from the sides. */
23
24
  body::before { inset: 64px 24px 56px; margin: auto; width: min(100vw - 48px, (100dvh - 120px) * 16 / 9); height: min(100dvh - 120px, (100vw - 48px) * 9 / 16); border-radius: calc(min(100vw - 48px, (100dvh - 120px) * 16 / 9) * .01125); background: var(--hd-ground, #fff); box-shadow: 0 0 0 1px color-mix(in srgb, var(--hd-ground-ink, #0b0b0f) 16%, transparent); }
24
- body::after { left: 50%; top: 50%; width: 12px; height: 12px; margin: -6px 0 0 -6px; border-radius: 50%; color: var(--hd-ground-ink, #0b0b0f); background: currentColor; box-shadow: -24px 0 currentColor, 24px 0 currentColor; opacity: 0; animation: hd-dots .7s ease-in-out .6s infinite alternate; }
25
+ [data-hd-frame] body::after { left: 50%; top: 50%; width: 12px; height: 12px; margin: -6px 0 0 -6px; border-radius: 50%; color: var(--hd-ground-ink, #0b0b0f); background: currentColor; box-shadow: -24px 0 currentColor, 24px 0 currentColor; opacity: 0; animation: hd-dots .7s ease-in-out .6s infinite alternate; }
25
26
  body[data-hd-ready]::before, body[data-hd-ready]::after { opacity: 0; animation: none; }
26
27
  /* The overlay (embedded, ?thumb, or data-layout="overlay"): the stage fills the window. */
27
28
  :is([data-hd-frame], [data-layout="overlay"]) body::before { inset: 0; width: min(100vw, 100dvh * 16 / 9); height: min(100dvh, 100vw * 9 / 16); border-radius: calc(min(100vw, 100dvh * 16 / 9) * .01125); box-shadow: none; }
@@ -41,7 +42,75 @@
41
42
  @keyframes hd-in { from { opacity: 0; } }
42
43
  @keyframes hd-dots { from { opacity: .15; } to { opacity: .6; } }
43
44
  @keyframes hd-fail-open { to { visibility: visible; overflow: auto; } }
45
+ #hd-open{position:fixed;inset:0;z-index:2147483000;pointer-events:none;visibility:visible!important;animation:none!important;opacity:0;font-family:var(--hd-ui-font,"Space Grotesk",system-ui,sans-serif);color:#0b0b0f}
46
+ #hd-open.is-shown{opacity:1;transition:opacity .2s ease-out}
47
+ #hd-open.is-gone{opacity:0;transition:opacity .45s ease-out}
48
+ #hd-open .hdo-slide{position:absolute;inset:64px 24px 56px;margin:auto;width:min(100vw - 48px,(100dvh - 120px)*16/9);height:min(100dvh - 120px,(100vw - 48px)*9/16);border-radius:calc(min(100vw - 48px,(100dvh - 120px)*16/9)*.01125);overflow:hidden;container-type:inline-size;background:var(--hd-ground,#fff)}
49
+ #hd-open .hdo-slide img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;filter:blur(2.2cqi) saturate(1.15);transform:scale(1.12);animation:hdo-breathe 2.4s ease-in-out infinite}
50
+ #hd-open .hdo-slide::after{content:"";position:absolute;inset:0;background:linear-gradient(100deg,transparent 30%,rgba(255,255,255,.3) 50%,transparent 70%);background-size:250% 100%;animation:hdo-shim 1.8s ease-in-out infinite;mix-blend-mode:soft-light}
51
+ #hd-open.is-done .hdo-slide img{animation:hdo-deblur .5s cubic-bezier(.2,.8,.2,1) .5s forwards}
52
+ #hd-open.is-done .hdo-slide::after{opacity:0;transition:opacity .3s}
53
+ .hdo-card{position:absolute;left:50%;top:50%;translate:-50% -50%;width:min(86vw,420px);display:grid;justify-items:center;gap:.6em;padding:1.3em 1.5em 1.2em;border-radius:22px;background:rgba(255,255,255,.74);-webkit-backdrop-filter:blur(14px) saturate(1.3);backdrop-filter:blur(14px) saturate(1.3);box-shadow:4px 4px 0 #0b0b0f;border:1.5px solid #0b0b0f;text-align:center;box-sizing:border-box}
54
+ #hd-open.is-done .hdo-card{opacity:0;transform:scale(.96);transition:opacity .25s .5s,transform .25s .5s}
55
+ .hdo-buddy{position:relative;width:92px;height:96px;margin-top:-62px;display:grid;justify-items:center}
56
+ .hdo-hop{width:86px;height:80px;transform-origin:50% 100%;animation:hdo-hop 1.15s cubic-bezier(.3,0,.4,1) infinite}
57
+ .hdo-look{width:100%;height:100%;animation:hdo-look 4.6s ease-in-out infinite}
58
+ .hdo-buddy img{width:100%;height:100%;object-fit:contain;display:block;filter:drop-shadow(0 3px 0 rgba(11,11,15,.18))}
59
+ .hdo-shadow{position:absolute;bottom:4px;width:46px;height:8px;border-radius:50%;background:rgba(11,11,15,.22);animation:hdo-shadow 1.15s cubic-bezier(.3,0,.4,1) infinite}
60
+ .hdo-think{position:absolute;right:-30px;top:-6px;display:flex;gap:3px;padding:5px 8px;border-radius:14px;background:#fff;border:1.5px solid #0b0b0f;box-shadow:2px 2px 0 #0b0b0f}
61
+ .hdo-think::after{content:"";position:absolute;left:4px;bottom:-7px;width:7px;height:7px;border-radius:50%;background:#fff;border:1.5px solid #0b0b0f}
62
+ .hdo-think i{width:5px;height:5px;border-radius:50%;background:#0b0b0f;animation:hdo-tdot 1s ease-in-out infinite}
63
+ .hdo-think i:nth-child(2){animation-delay:.15s}.hdo-think i:nth-child(3){animation-delay:.3s}
64
+ .hdo-spark{position:absolute;left:50%;top:40%;width:8px;height:8px;margin:-4px;opacity:0}
65
+ .hdo-spark::before{content:"";position:absolute;inset:0;background:#0b0b0f;clip-path:polygon(50% 0,62% 38%,100% 50%,62% 62%,50% 100%,38% 62%,0 50%,38% 38%)}
66
+ #hd-open.is-done .hdo-hop{animation:hdo-yay .55s cubic-bezier(.2,1.4,.4,1) forwards}
67
+ #hd-open.is-done :is(.hdo-look,.hdo-shadow){animation:none}
68
+ #hd-open.is-done .hdo-think{opacity:0;transition:opacity .15s}
69
+ #hd-open.is-done .hdo-spark{animation:hdo-spark .6s ease-out forwards}
70
+ .hdo-title{font-weight:700;font-size:18px;line-height:1.2;text-wrap:balance;overflow-wrap:anywhere}
71
+ .hdo-by{font:12.5px/1.3 var(--hd-ui-mono,"JetBrains Mono",ui-monospace,monospace);color:#3d3b35}
72
+ .hdo-bar{width:100%;height:5px;border-radius:999px;background:rgba(11,11,15,.12);overflow:hidden}
73
+ .hdo-bar i{display:block;height:100%;width:8%;background:#0b0b0f;border-radius:999px;transition:width .35s ease-out}
74
+ .hdo-step{font:12px/1.3 var(--hd-ui-mono,"JetBrains Mono",ui-monospace,monospace);color:#55534b;min-height:1.3em}
75
+ @keyframes hdo-breathe{50%{filter:blur(2.6cqi) saturate(1.25) brightness(1.05)}}
76
+ @keyframes hdo-shim{0%{background-position:120% 0}100%{background-position:-120% 0}}
77
+ @keyframes hdo-deblur{to{filter:blur(0);transform:none}}
78
+ @keyframes hdo-hop{0%,100%{transform:translateY(0) scale(1.06,.92)}12%{transform:translateY(0) scale(.95,1.06)}45%{transform:translateY(-14px) scale(1,1)}70%{transform:translateY(0) scale(1.04,.95)}82%{transform:translateY(0) scale(1,1)}}
79
+ @keyframes hdo-shadow{0%,100%{transform:scale(1.05);opacity:.9}45%{transform:scale(.62);opacity:.45}70%{transform:scale(1);opacity:.85}}
80
+ @keyframes hdo-look{0%,18%{transform:rotate(0)}24%,38%{transform:rotate(-9deg) translateX(-2px)}44%,58%{transform:rotate(8deg) translateX(2px)}64%,100%{transform:rotate(0)}}
81
+ @keyframes hdo-tdot{0%,100%{opacity:.25;transform:translateY(0)}40%{opacity:1;transform:translateY(-2px)}}
82
+ @keyframes hdo-yay{0%{transform:translateY(0) scale(1.08,.9)}40%{transform:translateY(-26px) scale(.94,1.08) rotate(-6deg)}100%{transform:translateY(-6px) scale(1) rotate(0)}}
83
+ @keyframes hdo-spark{0%{opacity:0;transform:translate(0,0) scale(.4)}30%{opacity:1}100%{opacity:0;transform:translate(var(--x),var(--y)) scale(1.2) rotate(90deg)}}
84
+ :is([data-hd-frame],[data-layout="overlay"]) #hd-open .hdo-slide{inset:0;width:min(100vw,100dvh*16/9);height:min(100dvh,100vw*9/16)}
85
+ @media (max-width:650px){#hd-open .hdo-slide{inset:52px 8px calc(56px + env(safe-area-inset-bottom));width:auto;height:auto;border-radius:14px}.hdo-card{width:min(84vw,320px);padding:1em 1.1em .95em}.hdo-title{font-size:15.5px}.hdo-buddy{transform:scale(.85);margin-top:-58px}}
86
+ @media (prefers-reduced-motion:reduce){#hd-open *,#hd-open *::before,#hd-open *::after{animation:none!important;transition:none!important}#hd-open .hdo-slide::after{display:none}#hd-open.is-done .hdo-card{opacity:1;transform:none}}
87
+ @media print{#hd-open{display:none!important}}
44
88
  </style>
89
+ <script data-hd-opening>(function(){var d=document;
90
+ if(self!==top||/[?&](thumb|export)\b/.test(location.search))return;
91
+ function go(){if(d.readyState==="loading")return;d.removeEventListener("readystatechange",go);
92
+ var b=d.body,p=d.querySelector('script[src*="/player/v"][src*="/player.js"]');
93
+ if(!b||!p||d.getElementById("hd-open")||b.hasAttribute("data-hd-ready")||d.documentElement.hasAttribute("data-hyperdecks-thumb"))return;
94
+ var o=d.createElement("div"),img=p.getAttribute("data-presenter")!=="none"&&p.getAttribute("data-presenter-image");
95
+ o.id="hd-open";o.setAttribute("aria-hidden","true");
96
+ o.innerHTML='<div class="hdo-slide"></div><div class="hdo-card">'+(img?"<div class=\"hdo-buddy\"><div class=\"hdo-hop\"><div class=\"hdo-look\"><img src=\"\" alt=\"\"></div></div><div class=\"hdo-shadow\"></div><div class=\"hdo-think\"><i></i><i></i><i></i></div><span class=\"hdo-spark\" style=\"--x:-40px;--y:-30px\"></span><span class=\"hdo-spark\" style=\"--x:42px;--y:-34px\"></span><span class=\"hdo-spark\" style=\"--x:-30px;--y:10px\"></span><span class=\"hdo-spark\" style=\"--x:36px;--y:6px\"></span><span class=\"hdo-spark\" style=\"--x:0px;--y:-46px\"></span></div>":'')+'<div class="hdo-title"></div><div class="hdo-bar"><i></i></div><div class="hdo-step"></div></div>';
97
+ if(img){var i=o.querySelector(".hdo-buddy img");i.addEventListener("error",function(){var y=o.querySelector(".hdo-buddy");if(y)y.remove()});i.src=img}
98
+ o.querySelector(".hdo-title").textContent=(d.title||"").replace(/\s+/g," ").trim();
99
+ b.insertBefore(o,b.firstChild);(function(o){var b=document.body,d=document.documentElement;if(!o||!b)return;
100
+ if(self!==top||/[?&](thumb|export)\b/.test(location.search)||d.hasAttribute("data-hyperdecks-thumb")){o.remove();return}
101
+ var S={"page":[10,"Opening the deck…"],"player":[38,"Loading the player…"],"presenter":[62,"Waking up {name}…"],"narration":[84,"Getting the narration…"]},bar=o.querySelector(".hdo-bar i"),step=o.querySelector(".hdo-step"),who=o.getAttribute("data-name")||"the presenter",at=0,shown=false,done=false,mo;
102
+ var rm=window.matchMedia&&matchMedia("(prefers-reduced-motion: reduce)").matches;
103
+ function boot(){var s=S[b.getAttribute("data-hd-boot")];if(!s||s[0]<=at)return;at=s[0];bar.style.width=s[0]+"%";step.textContent=s[1].split("{name}").join(who)}
104
+ function finish(){if(done)return;done=true;if(mo)mo.disconnect();clearTimeout(t1);clearTimeout(t2);
105
+ if(!shown){o.remove();return}bar.style.width="100%";step.textContent="Ready";
106
+ o.classList.add("is-done");setTimeout(function(){o.classList.add("is-gone")},rm?0:500);setTimeout(function(){o.remove()},rm?500:1000)}
107
+ function check(){if(b.hasAttribute("data-hd-ready"))finish();else boot()}
108
+ var t1=setTimeout(function(){if(!done){shown=true;o.classList.add("is-shown")}},150);
109
+ var t2=setTimeout(finish,10000);
110
+ step.textContent=S.page[1];
111
+ if(window.MutationObserver){mo=new MutationObserver(check);mo.observe(b,{attributes:true,attributeFilter:["data-hd-ready","data-hd-boot"]})}
112
+ check()})(o)}
113
+ d.addEventListener("readystatechange",go);go()})();</script>
45
114
  <link rel="preconnect" href="https://fonts.googleapis.com">
46
115
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
47
116
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;700&family=Space+Mono:wght@400;700&display=swap">
@@ -141,20 +210,20 @@
141
210
  -webkit-mask-image: radial-gradient(circle at 100% 0, #000 45%, rgba(0,0,0,.4) 100%); mask-image: radial-gradient(circle at 100% 0, #000 45%, rgba(0,0,0,.4) 100%); }
142
211
  .l-closer.bg-corner::before { --corner-r: 460px; }
143
212
  .on-ink.bg-corner::before { --corner-a: 22%; }
144
- :is(.l-split, .l-product, .l-chart, .l-quote, .l-number, .l-features, .l-team, .l-timeline, .l-compare).bg-corner::before { content: none; }
213
+ :is(.l-split, .l-product, .l-chart, .l-quote, .l-number, .l-features, .l-team, .l-timeline, .l-compare, .l-cover-band, .l-cover-poster).bg-corner::before { content: none; }
145
214
  /* bg-stage: a rounded dot plate exactly the picture's column plus 32px, so the frame or the phones stand on it; on the hero a tile behind the stickers. */
146
215
  .slide.bg-stage::before { inset: auto; background: radial-gradient(color-mix(in srgb, currentColor var(--stage-a, 24%), transparent) 2.4px, transparent 2.9px) 0 0 / 22px 22px; border-radius: 40px; }
147
216
  .l-product.bg-stage::before { position: relative; grid-area: frame; align-self: stretch; margin: var(--stage-m, -32px -24px -32px -40px); }
148
217
  .l-phones.bg-stage::before { position: relative; grid-area: row; align-self: stretch; margin: var(--stage-m, -32px); }
149
218
  .l-hero.bg-stage::before { right: 110px; top: 110px; width: 410px; height: 350px; }
150
- :is(.l-split, .l-chart, .l-quote, .l-closer, .l-number, .l-features, .l-team, .l-timeline, .l-compare).bg-stage::before { content: none; }
219
+ :is(.l-split, .l-chart, .l-quote, .l-closer, .l-number, .l-features, .l-team, .l-timeline, .l-compare, .l-cover-band, .l-cover-poster, .l-cover-type).bg-stage::before { content: none; }
151
220
  /* bg-mark: the slide number, set as style="--mark:'04'" on the section, huge in --display, faint,
152
221
  cropped by the top edge; right on the hero, in the indent on slot-left slides. A signature: cover, an opener, closer. */
153
222
  .slide.bg-mark::before { content: var(--mark, ""); inset: auto; right: 24px; top: -.26em; font: var(--display-weight, 700) var(--mark-size, 560px)/1 var(--display, var(--font)); font-synthesis: none; letter-spacing: var(--display-tracking, -.05em); white-space: nowrap;
154
223
  color: color-mix(in srgb, currentColor var(--mark-a, 8%), transparent); }
155
224
  .slide.on-ink.bg-mark::before { --mark-a: 6%; }
156
225
  .slide.slot-left.bg-mark::before { right: auto; left: -.08em; font-size: var(--mark-size, 440px); } /* ends inside the 500px indent, clear of the title */
157
- :is(.l-split, .l-product, .l-chart, .l-phones, .l-number, .l-features, .l-team, .l-timeline, .l-compare).bg-mark::before { content: none; }
226
+ :is(.l-split, .l-product, .l-chart, .l-phones, .l-number, .l-features, .l-team, .l-timeline, .l-compare, .l-cover-band, .l-cover-poster).bg-mark::before { content: none; }
158
227
  /* Pieces. .sticker: a logo or icon tile, tilted (--tilt), ink edge, hard offset
159
228
  shadow (.sticker.photo for a face or an app icon that fills it). .ticks: a short
160
229
  list with accent-square bullets. .panel: a quiet card. .pill: a small tag. */
@@ -170,7 +239,7 @@
170
239
  .pill { display: inline-flex; align-items: center; gap: 8px; width: fit-content; padding: 8px 16px; border-radius: 999px; background: var(--card); color: var(--ink); font: 700 var(--t-label) var(--mono); letter-spacing: .08em; text-transform: uppercase; box-shadow: 4px 4px 0 var(--accent); }
171
240
  .on-accent .pill { box-shadow: 4px 4px 0 var(--accent-ink); }
172
241
  /* A laptop for a desktop screenshot or clip: a .frame.laptop holding an img.shot or video.shot. */
173
- .slide .frame.laptop { position: relative; width: 860px; max-width: 100%; padding: 14px 14px 22px; border-radius: 20px 20px 6px 6px; background: #1a1a1d; box-shadow: 0 30px 70px color-mix(in srgb, var(--ink) 22%, transparent); }
242
+ .slide .frame.laptop { position: relative; width: min(860px, 100%); padding: 14px 14px 22px; border-radius: 20px 20px 6px 6px; background: #1a1a1d; box-shadow: 0 30px 70px color-mix(in srgb, var(--ink) 22%, transparent); }
174
243
  .slide .frame.laptop::after { content: ""; position: absolute; left: -6%; right: -6%; bottom: -16px; height: 16px; border-radius: 0 0 16px 16px; background: linear-gradient(#d9d9de, #a9a9b0); }
175
244
  .slide .frame.laptop > .shot { display: block; width: 100%; aspect-ratio: 16 / 10; border-radius: 6px; object-fit: cover; object-position: top; background: var(--paper); }
176
245
  /* Phones: the slide is the 374×736 card; the presenter is a small head in
@@ -195,7 +264,7 @@
195
264
  .panel { padding: 10px 12px; border-radius: 12px; }
196
265
  .pill { padding: 4px 10px; box-shadow: 2px 2px 0 var(--accent); }
197
266
  .slide .frame.laptop { padding: 5px 5px 9px; border-radius: 10px 10px 3px 3px; }
198
- .slide .frame.laptop::after { bottom: -7px; height: 7px; border-radius: 0 0 8px 8px; }
267
+ .slide .frame.laptop::after { left: -3%; right: -3%; bottom: -7px; height: 7px; border-radius: 0 0 8px 8px; }
199
268
  }
200
269
  </style>
201
270
  <!-- LAYOUT STYLES -->
@@ -12,6 +12,8 @@ script: That's it. [go: #__ID__-cta] Here is the one thing to do next. Thanks [g
12
12
  .l-closer .content { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 32px; padding-left: 500px; }
13
13
  .l-closer .tt { font-size: var(--t-display); line-height: .9; }
14
14
  .l-closer .cta { display: inline-flex; align-items: center; gap: 16px; width: fit-content; max-width: 100%; padding: 22px 34px; border-radius: 22px; background: var(--accent); color: var(--accent-ink); font: 700 34px var(--mono); letter-spacing: -.01em; text-decoration: none; }
15
+ /* The arrow is an SVG, not "→": Space Mono's (and Space Grotesk's) web subsets have no U+2192, so the glyph fell back to another face and read as malformed. */
16
+ .l-closer .cta svg { flex: none; width: 1em; height: 1em; }
15
17
  .l-closer .contact { font: var(--t-label) var(--mono); letter-spacing: .12em; text-transform: uppercase; }
16
18
  @media (max-width: 650px) {
17
19
  .l-closer .content { padding-left: 0; justify-content: flex-start; gap: 16px; }
@@ -26,7 +28,7 @@ script: That's it. [go: #__ID__-cta] Here is the one thing to do next. Thanks [g
26
28
  <div class="content">
27
29
  <h2 class="tt" data-title-mobile="Thanks.">Thanks.</h2>
28
30
  <p class="lead">One sentence on the one thing to do next.</p>
29
- <a class="cta" id="__ID__-cta" href="https://example.com" target="_blank" rel="noopener">example.com →</a>
31
+ <a class="cta" id="__ID__-cta" href="https://example.com" target="_blank" rel="noopener">example.com <svg class="lucide-arrow-right" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg></a>
30
32
  <div class="contact muted">your@email · your.site</div>
31
33
  </div>
32
34
  <div class="narration-slot"></div>
@@ -0,0 +1,44 @@
1
+ <!--
2
+ layout: cover-band
3
+ summary: A cover with the picture full bleed and the title on a solid, inked panel of the theme's paper in the bottom-left, so the title's contrast never depends on the picture.
4
+ use: the first slide when the picture is the story (a place, an object, a crowd) and wants the whole frame. Its .art is a placeholder check fails on: put in a wide picture of the real subject (gen-image / generate_image, 16:9) as <img class="art">; keep the subject top or top-right: the panel covers the bottom-left and the presenter the bottom-right. The panel grows with the title; never move the title off the panel onto the picture. Without art the panel sits on the slide's ground.
5
+ slot: bottom-right
6
+ presenter: stage
7
+ nopresenter: The panel may widen into the corner the presenter held.
8
+ cues: [wave] at the hello, [look: h1] when the narration says the title.
9
+ script: Hi, [wave] this is __TITLE__. [look: h1] One idea, in a few minutes.
10
+ -->
11
+ <style>
12
+ /* The picture fills the slide; the title sits on a solid panel of the theme's paper in the bottom-left, never on the picture. */
13
+ .l-cover-band { justify-content: flex-end; align-items: flex-start; overflow: hidden; }
14
+ /* A brand logo (applyBrand puts it first in the slide) sits top-left on the picture, not on the panel's edge. */
15
+ .l-cover-band:has(> .art) > .brand-logo { position: absolute; top: 28px; left: 72px; }
16
+ .l-cover-band .art { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
17
+ .l-cover-band div.art { background: color-mix(in srgb, var(--ink) 6%, var(--paper)) repeating-linear-gradient(135deg, color-mix(in srgb, var(--ink) 10%, transparent) 0 2px, transparent 2px 22px); }
18
+ .l-cover-band div.art::after { content: "Art goes here"; position: absolute; left: 70%; top: 34%; transform: translate(-50%, -50%); padding: 14px 26px; border: 3px dashed var(--ink); border-radius: 16px; background: var(--paper); color: var(--ink); font: 700 24px var(--mono); letter-spacing: .08em; text-transform: uppercase; white-space: nowrap; }
19
+ .l-cover-band .content { position: relative; z-index: 1; display: flex; flex-direction: column; gap: 18px; max-width: 980px; padding: 36px 44px 40px; border: 4px solid var(--ink); border-radius: 28px; background: var(--paper); color: var(--ink); box-shadow: 14px 14px 0 var(--ink); }
20
+ .l-cover-band .tt { font-size: min(var(--t-display), 96px); line-height: .92; }
21
+ .l-cover-band:not(:has(> .art)) { justify-content: center; }
22
+ @media (max-width: 650px) {
23
+ /* Phones: the picture across the top, the panel below it, down to the presenter's corner. */
24
+ .l-cover-band { justify-content: flex-start; align-items: stretch; gap: 0; }
25
+ .l-cover-band .art { position: relative; inset: auto; flex: none; width: auto; height: 210px; margin: -28px -20px 0; }
26
+ .l-cover-band div.art::after { left: 50%; top: 50%; padding: 8px 14px; border-width: 2px; font-size: 13px; }
27
+ .l-cover-band .content { gap: 12px; margin-top: -36px; padding: 18px 18px 20px; border-width: 2px; border-radius: 16px; box-shadow: 6px 6px 0 var(--ink); }
28
+ .l-cover-band .tt { font-size: var(--t-display); }
29
+ .l-cover-band:has(> .art) > .brand-logo { top: 12px; left: 20px; }
30
+ }
31
+ /* No presenter (:root.hd-no-presenter: no face and no captions, deck_guide look): the corner is free. Phones keep this layout's rules. */
32
+ @media (min-width: 651px) {
33
+ :root.hd-no-presenter .l-cover-band .content { max-width: 1200px; }
34
+ }
35
+ </style>
36
+ <section class="slide l-cover-band" id="__ID__" data-chapter="Hello" data-note="Title slide: no facts.">
37
+ <div class="art" role="presentation"></div>
38
+ <div class="content">
39
+ <div class="kicker">__SLUG__</div>
40
+ <h1 class="tt" data-title-mobile="__TITLE__">__TITLE__</h1>
41
+ <p class="lead">One sentence on what this deck is about.</p>
42
+ </div>
43
+ <div class="narration-slot"></div>
44
+ </section>
@@ -0,0 +1,42 @@
1
+ <!--
2
+ layout: cover-poster
3
+ summary: A poster cover: the picture across the top, edge to edge, and the title below it on the slide's ground. The picture takes whatever height the title leaves, so a long title shortens the picture instead of covering it.
4
+ use: the first slide when the picture is a scene or a landscape that crops well to a wide strip. Its .art is a placeholder check fails on: put in a wide picture of the real subject (gen-image / generate_image, 16:9 or wider) as <img class="art">; keep the subject in the middle band, the strip crops top and bottom. Never move the title up onto the picture. Without art the title fills the slide.
5
+ slot: bottom-right
6
+ presenter: stage
7
+ nopresenter: The title and lead run the slide's full width instead of stopping short of the corner.
8
+ cues: [wave] at the hello, [look: h1] when the narration says the title.
9
+ script: Hi, [wave] this is __TITLE__. [look: h1] One idea, in a few minutes.
10
+ -->
11
+ <style>
12
+ /* The picture is a flexible strip in the slide's flow; the text below it keeps its natural height. */
13
+ .l-cover-poster { gap: 0; }
14
+ /* A brand logo (applyBrand puts it first in the slide) sits on the picture's top-left, out of the flow, so the strip still starts at the top edge. */
15
+ .l-cover-poster:has(> .art) > .brand-logo { position: absolute; top: 28px; left: 72px; }
16
+ .l-cover-poster .art { display: block; flex: 1 1 0; min-height: 240px; width: auto; margin: -64px -72px 0; object-fit: cover; border-bottom: 4px solid var(--ink); }
17
+ .l-cover-poster div.art { position: relative; background: color-mix(in srgb, var(--ink) 6%, var(--paper)) repeating-linear-gradient(135deg, color-mix(in srgb, var(--ink) 10%, transparent) 0 2px, transparent 2px 22px); }
18
+ .l-cover-poster div.art::after { content: "Art goes here"; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); padding: 14px 26px; border: 3px dashed var(--ink); border-radius: 16px; background: var(--paper); color: var(--ink); font: 700 24px var(--mono); letter-spacing: .08em; text-transform: uppercase; white-space: nowrap; }
19
+ .l-cover-poster .content { flex: none; display: flex; flex-direction: column; gap: 18px; padding: 40px 400px 0 0; }
20
+ .l-cover-poster .tt { font-size: min(var(--t-display), 104px); line-height: .92; }
21
+ .l-cover-poster:not(:has(> .art)) .content { flex: 1; justify-content: center; padding-top: 0; }
22
+ @media (max-width: 650px) {
23
+ .l-cover-poster .art { flex: none; height: 196px; min-height: 0; margin: -28px -20px 0; border-bottom-width: 2px; }
24
+ .l-cover-poster div.art::after { padding: 8px 14px; border-width: 2px; font-size: 13px; }
25
+ .l-cover-poster .content { gap: 12px; padding: 18px 0 0; }
26
+ .l-cover-poster:has(> .art) > .brand-logo { top: 12px; left: 20px; }
27
+ .l-cover-poster .tt { font-size: var(--t-display); }
28
+ }
29
+ /* No presenter (:root.hd-no-presenter: no face and no captions, deck_guide look): the corner is free. Phones keep this layout's rules. */
30
+ @media (min-width: 651px) {
31
+ :root.hd-no-presenter .l-cover-poster .content { padding-right: 0; }
32
+ }
33
+ </style>
34
+ <section class="slide l-cover-poster" id="__ID__" data-chapter="Hello" data-note="Title slide: no facts.">
35
+ <div class="art" role="presentation"></div>
36
+ <div class="content">
37
+ <div class="kicker">__SLUG__</div>
38
+ <h1 class="tt" data-title-mobile="__TITLE__">__TITLE__</h1>
39
+ <p class="lead">One sentence on what this deck is about.</p>
40
+ </div>
41
+ <div class="narration-slot"></div>
42
+ </section>
@@ -0,0 +1,31 @@
1
+ <!--
2
+ layout: cover-type
3
+ summary: A type-only cover: the title, big, on the theme's own ground and texture, with the presenter large in the corner as the visual. No picture to fill.
4
+ use: the first slide when there is no picture that shows the real subject yet, or the deck is about an idea more than a thing. Swap the ground (on-ink, on-accent, on-tint) and keep one signature texture (bg-corner or bg-mark) at most; the title stays clear of it. Nothing to fill: it passes check as scaffolded.
5
+ slot: bottom-right
6
+ presenter: stage
7
+ nopresenter: The title widens into the corner the presenter held.
8
+ cues: [wave] at the hello, [look: h1] when the narration says the title.
9
+ script: Hi, [wave] this is __TITLE__. [look: h1] One idea, in a few minutes.
10
+ -->
11
+ <style>
12
+ .l-cover-type .content { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 30px; max-width: 980px; }
13
+ .l-cover-type .tt { font-size: calc(var(--t-display) * 1.12); line-height: .88; letter-spacing: -.05em; }
14
+ .l-cover-type .lead { max-width: 24em; }
15
+ @media (max-width: 650px) {
16
+ .l-cover-type .content { justify-content: flex-start; gap: 16px; padding-top: 24px; }
17
+ .l-cover-type .tt { font-size: calc(var(--t-display) * 1.08); }
18
+ }
19
+ /* No presenter (:root.hd-no-presenter: no face and no captions, deck_guide look): the corner is free. Phones keep this layout's rules. */
20
+ @media (min-width: 651px) {
21
+ :root.hd-no-presenter .l-cover-type .content { max-width: 1240px; }
22
+ }
23
+ </style>
24
+ <section class="slide on-ink l-cover-type bg-corner" id="__ID__" data-chapter="Hello" data-presenter-size="l" data-note="Title slide: no facts.">
25
+ <div class="content">
26
+ <div class="kicker">__SLUG__</div>
27
+ <h1 class="tt" data-title-mobile="__TITLE__">__TITLE__</h1>
28
+ <p class="lead">One sentence on what this deck is about.</p>
29
+ </div>
30
+ <div class="narration-slot"></div>
31
+ </section>
@@ -1,29 +1,44 @@
1
1
  <!--
2
2
  layout: hero
3
- summary: The opening. One display-size line, a visual on the right, the presenter's hello.
4
- use: the first slide; a section opener. Its .art is a placeholder check fails on: put in a picture of the real subject (gen-image / generate_image) as <img class="art">, or delete the div and let the presenter be the visual.
3
+ summary: The opening, as a split cover: the title on the slide's own ground on the left, the picture in a clean frame top-right, the presenter's hello. Text never sits on the picture.
4
+ use: the first slide (the default cover); a section opener. Its .art is a placeholder check fails on: put in a picture of the real subject (gen-image / generate_image, landscape or near-square: the frame is about 7:6) as <img class="art">, or delete the div and the title takes the full frame with the presenter as the visual. Never lay the title over the picture or fade the picture behind it. Other covers: cover-band (the picture full bleed, the title on a solid panel), cover-poster (the picture on top, the title below) and cover-type (type only).
5
5
  slot: bottom-right
6
6
  presenter: stage
7
- nopresenter: With art (the placeholder div counts), unchanged: the art already fills the right. Delete the art and the line re-centres in the full frame.
7
+ nopresenter: With art, the picture grows to the full height of its column. Delete the art and the line re-centres in the full frame.
8
8
  cues: [wave] at the hello, [look: h1] when the narration says the title.
9
9
  script: Hi, [wave] this is __TITLE__. [look: h1] One idea, in a few minutes.
10
10
  -->
11
11
  <style>
12
- .l-hero { overflow: hidden; }
13
- .l-hero .content { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 28px; max-width: 980px; position: relative; z-index: 1; }
12
+ /* The title and the picture are two columns: whatever the title's length, it never reaches the picture. */
13
+ .l-hero .content { flex: 1; display: flex; flex-direction: column; justify-content: center; gap: 28px; max-width: 980px; }
14
14
  .l-hero .tt { font-size: var(--t-display); line-height: .9; }
15
- /* Swap the div for an <img class="art"> of your file (gen-image, wide), or delete it for no art. */
16
- .l-hero .art { position: absolute; top: 0; right: 0; width: 820px; height: 100%; object-fit: cover; -webkit-mask-image: linear-gradient(to right, transparent, #000 32%); mask-image: linear-gradient(to right, transparent, #000 32%); }
15
+ /* With art: the title on the left on the slide's ground; the picture framed top-right, the presenter's corner below it. */
16
+ .l-hero:has(> .art) { display: grid; grid-template-columns: minmax(0, 1fr) 560px; grid-template-rows: minmax(0, 1fr); column-gap: 72px; }
17
+ .l-hero:has(> .art) .content { grid-column: 1; grid-row: 1; max-width: none; }
18
+ .l-hero:has(> .art) .tt { font-size: min(var(--t-display), 112px); }
19
+ /* A brand logo (applyBrand puts it first in the slide) heads the title column; the picture spans both rows. */
20
+ .l-hero:has(> .art):has(> .brand-logo) { grid-template-rows: auto minmax(0, 1fr); }
21
+ .l-hero:has(> .art) > .brand-logo { grid-column: 1; grid-row: 1; }
22
+ .l-hero:has(> .art):has(> .brand-logo) .content { grid-row: 2; }
23
+ .l-hero:has(> .art):has(> .brand-logo) .art { grid-row: 1 / -1; }
24
+ /* Swap the div for an <img class="art"> of your file (gen-image, landscape or square), or delete it for no art. */
25
+ .l-hero .art { grid-column: 2; grid-row: 1; align-self: start; display: block; width: 100%; height: 480px; margin-top: 8px; border: 4px solid var(--ink); border-radius: 28px; object-fit: cover; box-shadow: 14px 14px 0 var(--ink); }
26
+ .on-ink.l-hero .art { border-color: var(--paper); box-shadow: 14px 14px 0 var(--accent); }
17
27
  /* Until then a labelled, hatched stand-in, so it reads as a slot to fill; check fails while it is here. */
18
- .l-hero div.art { background: repeating-linear-gradient(135deg, color-mix(in srgb, var(--ink) 10%, transparent) 0 2px, transparent 2px 22px); }
19
- .l-hero div.art::after { content: "Art goes here"; position: absolute; left: 64%; top: 40%; transform: translate(-50%, -50%); padding: 14px 26px; border: 3px dashed var(--ink); border-radius: 16px; background: var(--paper); color: var(--ink); font: 700 24px var(--mono); letter-spacing: .08em; text-transform: uppercase; white-space: nowrap; }
28
+ .l-hero div.art { position: relative; background: var(--paper) repeating-linear-gradient(135deg, color-mix(in srgb, var(--ink) 10%, transparent) 0 2px, transparent 2px 22px); }
29
+ .l-hero div.art::after { content: "Art goes here"; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); padding: 14px 26px; border: 3px dashed var(--ink); border-radius: 16px; background: var(--paper); color: var(--ink); font: 700 24px var(--mono); letter-spacing: .08em; text-transform: uppercase; white-space: nowrap; }
20
30
  @media (max-width: 650px) {
21
- .l-hero .content { justify-content: flex-start; gap: 16px; padding-top: 200px; }
22
- .l-hero .art { width: 100%; height: 230px; -webkit-mask-image: linear-gradient(to bottom, #000 60%, transparent); mask-image: linear-gradient(to bottom, #000 60%, transparent); }
23
- .l-hero div.art::after { left: 50%; top: 45%; padding: 8px 14px; border-width: 2px; font-size: 13px; }
31
+ /* Phones: the picture above, the title below it on the ground. */
32
+ .l-hero:has(> .art) { display: flex; }
33
+ .l-hero:has(> .art) .tt { font-size: var(--t-display); }
34
+ .l-hero .content { flex: none; justify-content: flex-start; gap: 16px; }
35
+ .l-hero .art { flex: none; height: 176px; margin-top: 0; border-width: 2px; border-radius: 16px; box-shadow: 6px 6px 0 var(--ink); }
36
+ .on-ink.l-hero .art { box-shadow: 6px 6px 0 var(--accent); }
37
+ .l-hero div.art::after { padding: 8px 14px; border-width: 2px; font-size: 13px; }
24
38
  }
25
39
  /* No presenter (:root.hd-no-presenter: no face and no captions, deck_guide look): the corner is free. Phones keep this layout's rules. */
26
40
  @media (min-width: 651px) {
41
+ :root.hd-no-presenter .l-hero:has(> .art) .art { align-self: stretch; height: auto; margin-top: 0; }
27
42
  :root.hd-no-presenter .l-hero:not(:has(> .art)) .content { align-self: center; align-items: center; text-align: center; max-width: 1200px; }
28
43
  }
29
44
  </style>
@@ -9,13 +9,13 @@ cues: [look: #__ID__-frame] when the narration turns to the picture; [go: #__ID_
9
9
  script: Here it is, doing the job. [look: #__ID__-frame] It starts with [go: #__ID__-t1] the first thing. Then [go: #__ID__-t2] the second. And [go: #__ID__-t3] the third. [go: slot]
10
10
  -->
11
11
  <style>
12
- .l-split { display: grid; grid-template-columns: 680px 1fr; column-gap: 64px; align-items: center; }
12
+ .l-split { display: grid; grid-template-columns: 680px minmax(0, 1fr); column-gap: 64px; align-items: center; }
13
13
  .l-split .text { align-self: start; display: flex; flex-direction: column; gap: 22px; padding-top: 24px; }
14
14
  .l-split .head { display: flex; align-items: center; gap: 22px; }
15
15
  .l-split .tt { font-size: var(--t-sub); line-height: 1.02; }
16
16
  .l-split .lead { font-size: var(--t-body); }
17
17
  .l-split .ticks { margin-top: 6px; }
18
- .l-split .media { display: grid; place-items: center; height: 100%; }
18
+ .l-split .media { display: grid; grid-template-columns: minmax(0, 1fr); place-items: center; height: 100%; min-width: 0; }
19
19
  .l-split .frame.card { width: 300px; --tilt: 4deg; }
20
20
  .l-split .shot.wide { display: block; width: 100%; aspect-ratio: 16 / 9; border-radius: 18px; object-fit: cover; box-shadow: 14px 14px 0 var(--accent); }
21
21
  .on-accent.l-split .shot.wide { box-shadow: 14px 14px 0 var(--ink); }
@@ -7,7 +7,8 @@
7
7
  <!-- Loading state (keep verbatim; `hyperdecks check` requires it). Until the
8
8
  player marks body[data-hd-ready] the page shows only the slide card, at its
9
9
  final size, in the deck's --hd-ground (its first slide's colour) on the
10
- page's brand colour, with a quiet loader in --hd-ground-ink; then the
10
+ page's brand colour, with the opening card over it (the title and the
11
+ presenter's still), or in a frame quiet dots in --hd-ground-ink; then the
11
12
  slides fade in. If the player never arrives, the slides show after 10 s
12
13
  rather than never. Put data-layout="overlay" on <html> for the overlay. -->
13
14
  <link rel="modulepreload" href="https://hyperdecks.ai/player/v1/player.js">
@@ -21,7 +22,7 @@
21
22
  /* The card as the player draws it (player.css, framed): 16:9, scaled to fit
22
23
  between the 64px masthead and the 56px dock, 24px in from the sides. */
23
24
  body::before { inset: 64px 24px 56px; margin: auto; width: min(100vw - 48px, (100dvh - 120px) * 16 / 9); height: min(100dvh - 120px, (100vw - 48px) * 9 / 16); border-radius: calc(min(100vw - 48px, (100dvh - 120px) * 16 / 9) * .01125); background: var(--hd-ground, #fff); box-shadow: 0 0 0 1px color-mix(in srgb, var(--hd-ground-ink, #0b0b0f) 16%, transparent); }
24
- body::after { left: 50%; top: 50%; width: 12px; height: 12px; margin: -6px 0 0 -6px; border-radius: 50%; color: var(--hd-ground-ink, #0b0b0f); background: currentColor; box-shadow: -24px 0 currentColor, 24px 0 currentColor; opacity: 0; animation: hd-dots .7s ease-in-out .6s infinite alternate; }
25
+ [data-hd-frame] body::after { left: 50%; top: 50%; width: 12px; height: 12px; margin: -6px 0 0 -6px; border-radius: 50%; color: var(--hd-ground-ink, #0b0b0f); background: currentColor; box-shadow: -24px 0 currentColor, 24px 0 currentColor; opacity: 0; animation: hd-dots .7s ease-in-out .6s infinite alternate; }
25
26
  body[data-hd-ready]::before, body[data-hd-ready]::after { opacity: 0; animation: none; }
26
27
  /* The overlay (embedded, ?thumb, or data-layout="overlay"): the stage fills the window. */
27
28
  :is([data-hd-frame], [data-layout="overlay"]) body::before { inset: 0; width: min(100vw, 100dvh * 16 / 9); height: min(100dvh, 100vw * 9 / 16); border-radius: calc(min(100vw, 100dvh * 16 / 9) * .01125); box-shadow: none; }
@@ -41,7 +42,75 @@
41
42
  @keyframes hd-in { from { opacity: 0; } }
42
43
  @keyframes hd-dots { from { opacity: .15; } to { opacity: .6; } }
43
44
  @keyframes hd-fail-open { to { visibility: visible; overflow: auto; } }
45
+ #hd-open{position:fixed;inset:0;z-index:2147483000;pointer-events:none;visibility:visible!important;animation:none!important;opacity:0;font-family:var(--hd-ui-font,"Space Grotesk",system-ui,sans-serif);color:#0b0b0f}
46
+ #hd-open.is-shown{opacity:1;transition:opacity .2s ease-out}
47
+ #hd-open.is-gone{opacity:0;transition:opacity .45s ease-out}
48
+ #hd-open .hdo-slide{position:absolute;inset:64px 24px 56px;margin:auto;width:min(100vw - 48px,(100dvh - 120px)*16/9);height:min(100dvh - 120px,(100vw - 48px)*9/16);border-radius:calc(min(100vw - 48px,(100dvh - 120px)*16/9)*.01125);overflow:hidden;container-type:inline-size;background:var(--hd-ground,#fff)}
49
+ #hd-open .hdo-slide img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;filter:blur(2.2cqi) saturate(1.15);transform:scale(1.12);animation:hdo-breathe 2.4s ease-in-out infinite}
50
+ #hd-open .hdo-slide::after{content:"";position:absolute;inset:0;background:linear-gradient(100deg,transparent 30%,rgba(255,255,255,.3) 50%,transparent 70%);background-size:250% 100%;animation:hdo-shim 1.8s ease-in-out infinite;mix-blend-mode:soft-light}
51
+ #hd-open.is-done .hdo-slide img{animation:hdo-deblur .5s cubic-bezier(.2,.8,.2,1) .5s forwards}
52
+ #hd-open.is-done .hdo-slide::after{opacity:0;transition:opacity .3s}
53
+ .hdo-card{position:absolute;left:50%;top:50%;translate:-50% -50%;width:min(86vw,420px);display:grid;justify-items:center;gap:.6em;padding:1.3em 1.5em 1.2em;border-radius:22px;background:rgba(255,255,255,.74);-webkit-backdrop-filter:blur(14px) saturate(1.3);backdrop-filter:blur(14px) saturate(1.3);box-shadow:4px 4px 0 #0b0b0f;border:1.5px solid #0b0b0f;text-align:center;box-sizing:border-box}
54
+ #hd-open.is-done .hdo-card{opacity:0;transform:scale(.96);transition:opacity .25s .5s,transform .25s .5s}
55
+ .hdo-buddy{position:relative;width:92px;height:96px;margin-top:-62px;display:grid;justify-items:center}
56
+ .hdo-hop{width:86px;height:80px;transform-origin:50% 100%;animation:hdo-hop 1.15s cubic-bezier(.3,0,.4,1) infinite}
57
+ .hdo-look{width:100%;height:100%;animation:hdo-look 4.6s ease-in-out infinite}
58
+ .hdo-buddy img{width:100%;height:100%;object-fit:contain;display:block;filter:drop-shadow(0 3px 0 rgba(11,11,15,.18))}
59
+ .hdo-shadow{position:absolute;bottom:4px;width:46px;height:8px;border-radius:50%;background:rgba(11,11,15,.22);animation:hdo-shadow 1.15s cubic-bezier(.3,0,.4,1) infinite}
60
+ .hdo-think{position:absolute;right:-30px;top:-6px;display:flex;gap:3px;padding:5px 8px;border-radius:14px;background:#fff;border:1.5px solid #0b0b0f;box-shadow:2px 2px 0 #0b0b0f}
61
+ .hdo-think::after{content:"";position:absolute;left:4px;bottom:-7px;width:7px;height:7px;border-radius:50%;background:#fff;border:1.5px solid #0b0b0f}
62
+ .hdo-think i{width:5px;height:5px;border-radius:50%;background:#0b0b0f;animation:hdo-tdot 1s ease-in-out infinite}
63
+ .hdo-think i:nth-child(2){animation-delay:.15s}.hdo-think i:nth-child(3){animation-delay:.3s}
64
+ .hdo-spark{position:absolute;left:50%;top:40%;width:8px;height:8px;margin:-4px;opacity:0}
65
+ .hdo-spark::before{content:"";position:absolute;inset:0;background:#0b0b0f;clip-path:polygon(50% 0,62% 38%,100% 50%,62% 62%,50% 100%,38% 62%,0 50%,38% 38%)}
66
+ #hd-open.is-done .hdo-hop{animation:hdo-yay .55s cubic-bezier(.2,1.4,.4,1) forwards}
67
+ #hd-open.is-done :is(.hdo-look,.hdo-shadow){animation:none}
68
+ #hd-open.is-done .hdo-think{opacity:0;transition:opacity .15s}
69
+ #hd-open.is-done .hdo-spark{animation:hdo-spark .6s ease-out forwards}
70
+ .hdo-title{font-weight:700;font-size:18px;line-height:1.2;text-wrap:balance;overflow-wrap:anywhere}
71
+ .hdo-by{font:12.5px/1.3 var(--hd-ui-mono,"JetBrains Mono",ui-monospace,monospace);color:#3d3b35}
72
+ .hdo-bar{width:100%;height:5px;border-radius:999px;background:rgba(11,11,15,.12);overflow:hidden}
73
+ .hdo-bar i{display:block;height:100%;width:8%;background:#0b0b0f;border-radius:999px;transition:width .35s ease-out}
74
+ .hdo-step{font:12px/1.3 var(--hd-ui-mono,"JetBrains Mono",ui-monospace,monospace);color:#55534b;min-height:1.3em}
75
+ @keyframes hdo-breathe{50%{filter:blur(2.6cqi) saturate(1.25) brightness(1.05)}}
76
+ @keyframes hdo-shim{0%{background-position:120% 0}100%{background-position:-120% 0}}
77
+ @keyframes hdo-deblur{to{filter:blur(0);transform:none}}
78
+ @keyframes hdo-hop{0%,100%{transform:translateY(0) scale(1.06,.92)}12%{transform:translateY(0) scale(.95,1.06)}45%{transform:translateY(-14px) scale(1,1)}70%{transform:translateY(0) scale(1.04,.95)}82%{transform:translateY(0) scale(1,1)}}
79
+ @keyframes hdo-shadow{0%,100%{transform:scale(1.05);opacity:.9}45%{transform:scale(.62);opacity:.45}70%{transform:scale(1);opacity:.85}}
80
+ @keyframes hdo-look{0%,18%{transform:rotate(0)}24%,38%{transform:rotate(-9deg) translateX(-2px)}44%,58%{transform:rotate(8deg) translateX(2px)}64%,100%{transform:rotate(0)}}
81
+ @keyframes hdo-tdot{0%,100%{opacity:.25;transform:translateY(0)}40%{opacity:1;transform:translateY(-2px)}}
82
+ @keyframes hdo-yay{0%{transform:translateY(0) scale(1.08,.9)}40%{transform:translateY(-26px) scale(.94,1.08) rotate(-6deg)}100%{transform:translateY(-6px) scale(1) rotate(0)}}
83
+ @keyframes hdo-spark{0%{opacity:0;transform:translate(0,0) scale(.4)}30%{opacity:1}100%{opacity:0;transform:translate(var(--x),var(--y)) scale(1.2) rotate(90deg)}}
84
+ :is([data-hd-frame],[data-layout="overlay"]) #hd-open .hdo-slide{inset:0;width:min(100vw,100dvh*16/9);height:min(100dvh,100vw*9/16)}
85
+ @media (max-width:650px){#hd-open .hdo-slide{inset:52px 8px calc(56px + env(safe-area-inset-bottom));width:auto;height:auto;border-radius:14px}.hdo-card{width:min(84vw,320px);padding:1em 1.1em .95em}.hdo-title{font-size:15.5px}.hdo-buddy{transform:scale(.85);margin-top:-58px}}
86
+ @media (prefers-reduced-motion:reduce){#hd-open *,#hd-open *::before,#hd-open *::after{animation:none!important;transition:none!important}#hd-open .hdo-slide::after{display:none}#hd-open.is-done .hdo-card{opacity:1;transform:none}}
87
+ @media print{#hd-open{display:none!important}}
44
88
  </style>
89
+ <script data-hd-opening>(function(){var d=document;
90
+ if(self!==top||/[?&](thumb|export)\b/.test(location.search))return;
91
+ function go(){if(d.readyState==="loading")return;d.removeEventListener("readystatechange",go);
92
+ var b=d.body,p=d.querySelector('script[src*="/player/v"][src*="/player.js"]');
93
+ if(!b||!p||d.getElementById("hd-open")||b.hasAttribute("data-hd-ready")||d.documentElement.hasAttribute("data-hyperdecks-thumb"))return;
94
+ var o=d.createElement("div"),img=p.getAttribute("data-presenter")!=="none"&&p.getAttribute("data-presenter-image");
95
+ o.id="hd-open";o.setAttribute("aria-hidden","true");
96
+ o.innerHTML='<div class="hdo-slide"></div><div class="hdo-card">'+(img?"<div class=\"hdo-buddy\"><div class=\"hdo-hop\"><div class=\"hdo-look\"><img src=\"\" alt=\"\"></div></div><div class=\"hdo-shadow\"></div><div class=\"hdo-think\"><i></i><i></i><i></i></div><span class=\"hdo-spark\" style=\"--x:-40px;--y:-30px\"></span><span class=\"hdo-spark\" style=\"--x:42px;--y:-34px\"></span><span class=\"hdo-spark\" style=\"--x:-30px;--y:10px\"></span><span class=\"hdo-spark\" style=\"--x:36px;--y:6px\"></span><span class=\"hdo-spark\" style=\"--x:0px;--y:-46px\"></span></div>":'')+'<div class="hdo-title"></div><div class="hdo-bar"><i></i></div><div class="hdo-step"></div></div>';
97
+ if(img){var i=o.querySelector(".hdo-buddy img");i.addEventListener("error",function(){var y=o.querySelector(".hdo-buddy");if(y)y.remove()});i.src=img}
98
+ o.querySelector(".hdo-title").textContent=(d.title||"").replace(/\s+/g," ").trim();
99
+ b.insertBefore(o,b.firstChild);(function(o){var b=document.body,d=document.documentElement;if(!o||!b)return;
100
+ if(self!==top||/[?&](thumb|export)\b/.test(location.search)||d.hasAttribute("data-hyperdecks-thumb")){o.remove();return}
101
+ var S={"page":[10,"Opening the deck…"],"player":[38,"Loading the player…"],"presenter":[62,"Waking up {name}…"],"narration":[84,"Getting the narration…"]},bar=o.querySelector(".hdo-bar i"),step=o.querySelector(".hdo-step"),who=o.getAttribute("data-name")||"the presenter",at=0,shown=false,done=false,mo;
102
+ var rm=window.matchMedia&&matchMedia("(prefers-reduced-motion: reduce)").matches;
103
+ function boot(){var s=S[b.getAttribute("data-hd-boot")];if(!s||s[0]<=at)return;at=s[0];bar.style.width=s[0]+"%";step.textContent=s[1].split("{name}").join(who)}
104
+ function finish(){if(done)return;done=true;if(mo)mo.disconnect();clearTimeout(t1);clearTimeout(t2);
105
+ if(!shown){o.remove();return}bar.style.width="100%";step.textContent="Ready";
106
+ o.classList.add("is-done");setTimeout(function(){o.classList.add("is-gone")},rm?0:500);setTimeout(function(){o.remove()},rm?500:1000)}
107
+ function check(){if(b.hasAttribute("data-hd-ready"))finish();else boot()}
108
+ var t1=setTimeout(function(){if(!done){shown=true;o.classList.add("is-shown")}},150);
109
+ var t2=setTimeout(finish,10000);
110
+ step.textContent=S.page[1];
111
+ if(window.MutationObserver){mo=new MutationObserver(check);mo.observe(b,{attributes:true,attributeFilter:["data-hd-ready","data-hd-boot"]})}
112
+ check()})(o)}
113
+ d.addEventListener("readystatechange",go);go()})();</script>
45
114
  <link rel="preconnect" href="https://fonts.googleapis.com">
46
115
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
47
116
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;700&family=Space+Mono:wght@400;700&display=swap">