rikiki-deck 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  # Rikiki · LLM reference
2
2
 
3
- This reference documents rikiki v0.4.0.
3
+ This reference documents rikiki v0.6.0.
4
4
 
5
5
  Exhaustive, self-consistent reference for authoring valid **rikiki** decks. Every
6
6
  tag, attribute, slot, and token below was derived from the source in this repo
@@ -107,7 +107,9 @@ debounce, discreet chevrons sit bottom-right (2D-aware), and mouse
107
107
  back/forward buttons map to back/advance. The wheel yields to scrollable
108
108
  content: a wheel over an overflowing descendant (a tall `deck-code` block, a
109
109
  zoomable inline `<svg>`, …) scrolls it natively and only advances the deck once
110
- that element reaches its scroll edge. The bottom-left key-hint chips
110
+ that element reaches its scroll edge. **Ctrl/⌘ + wheel and trackpad pinch zoom
111
+ the slide** (see Slide zoom below); set `no-zoom` to leave zoom to the browser.
112
+ The bottom-left key-hint chips
111
113
  (`← → O P ?`) are also clickable shortcuts for the matching action. Interactive
112
114
  elements (`a`, `button`, inputs, `[contenteditable]`) never trigger navigation;
113
115
  add `data-no-advance` to opt any element out. Configure with the `mouse-nav`
@@ -129,10 +131,58 @@ Chevron styling tokens: `--deck-root-nav-color`, `--deck-root-nav-bg`,
129
131
  | `nav` | `2d` | Opt into 2D (chapter/slide) navigation · see above |
130
132
  | `mouse-nav` | *(absent)* / `none` / subset of `click wheel arrows aux` | Mouse navigation config · see above |
131
133
  | `transition` | `slide` (default-ish) / `slide-up` / `slide-down` / `slide-right` / `fade` / `zoom` / `flip` | Deck-wide slide transition (see table below) |
134
+ | `fluid` | *(boolean)* | Fluid rendering · the deck fills its box and reflows like a web page (no logical canvas, no zoom-to-fit scale, no letterbox). Default is zoom-to-fit · see Rendering & sizing below. Also accepted **per slide** · see below |
135
+ | `width` / `height` | integers (default `1920` / `1080`) | Logical canvas size for zoom-to-fit · only the aspect ratio and the rem baseline depend on these. Ignored in `fluid` mode |
136
+ | `no-hint` | *(boolean)* | Hide the bottom-left key-hint chips (`← → · O · P · ?`) |
137
+ | `no-arrows` | *(boolean)* | Hide the bottom-right on-screen navigation chevrons |
138
+ | `no-zoom` | *(boolean)* | Disable slide zoom (Ctrl/⌘+wheel, pinch, `+`/`-`/`0`) · on by default; restores the browser's own zoom |
139
+ | `no-counter` | *(boolean)* | Hide the slide counter (it is already hidden on cover, overview and blank screens) |
140
+ | `preview` | *(boolean)* | Inert mode · render and letterbox the deck but wire no input, autoplay or presenter handlers. Used internally by the presenter's Current preview as a control surface; rarely set by hand |
132
141
  | `autoplay` | integer ms (e.g. `8000`) | Auto-advance every N ms; pauses on hover, resets on any manual nav. `0`/absent = off |
133
142
  | `loop` | *(boolean)* | With `autoplay`, wraps from the last slide back to the first |
134
143
  | `swipe` | *(boolean)* | Pointer-driven horizontal swipe (touch + mouse): a swipe ≥ 60 px advances / goes back |
135
144
 
145
+ In presenter mode (`P`) the projected (main) window auto-hides its hint chips
146
+ and nav arrows while the speaker popup is open · `no-hint` / `no-arrows` stay
147
+ useful for a clean look outside presenter mode (and for embedded thumbnails).
148
+
149
+ **Rendering & sizing.** By default a deck renders into a fixed logical canvas
150
+ (`width`×`height`, 1920×1080 by default) scaled uniformly to fit the host box,
151
+ so a slide's layout is identical at any window size and the deck is letterboxed
152
+ when the aspect differs. Set `fluid` to opt out: the deck then fills its box and
153
+ reflows like a web page. Either mode is embed-safe · a `<deck-root>` placed
154
+ inside a larger document scales to (or fills) its own container and never
155
+ touches the host page's scroll or typography.
156
+
157
+ **Per-slide fluid escape.** A single slide can opt out of the fixed canvas by
158
+ carrying its own `fluid` attribute · while that slide is active the deck drops
159
+ the zoom-to-fit scale and letterbox and gives the slide the real viewport (the
160
+ rem baseline tracks the viewport too), then restores the canvas on navigation
161
+ back to a fixed slide. Use it for one slide that embeds a live interactive demo
162
+ (an `<iframe>` playground, a mini-game) which should consume the whole screen
163
+ while the rest of the deck stays on the predictable fixed canvas:
164
+
165
+ ```html
166
+ <deck-root>
167
+ <deck-feature><h1 slot="title">Regular fixed slide</h1></deck-feature>
168
+ <deck-feature fluid>
169
+ <iframe src="playground.html" style="position:fixed; inset:0; border:0;"></iframe>
170
+ </deck-feature>
171
+ </deck-root>
172
+ ```
173
+
174
+ (A deck-wide `fluid` already makes every slide fluid · the per-slide attribute
175
+ is only meaningful inside an otherwise fixed deck.)
176
+
177
+ **Slide zoom.** On by default in fixed-canvas mode: Ctrl/⌘ + wheel (and trackpad
178
+ pinch) magnify the active slide around the cursor; `+`/`-` zoom by steps and `0`
179
+ resets to fit. While magnified, a plain wheel and pointer drag pan the slide, and
180
+ slide navigation snaps back to fit. Because it scales the whole stage uniformly,
181
+ fonts and layout grow together (no reflow) · this is also how to "make the fonts
182
+ bigger". Zoom is a no-op in `fluid` mode (nothing fixed to magnify), in overview,
183
+ and with `no-zoom`. It is local to the projected window (not mirrored into the
184
+ presenter popup).
185
+
136
186
  **Transition values** (deck-wide via `transition="…"` on `<deck-root>`, or
137
187
  per-slide via `data-transition="…"` on a slide host). When unset, the effective
138
188
  default is `fade`:
@@ -221,22 +271,30 @@ Direct children of `<deck-root>`. Each is one slide.
221
271
 
222
272
  ### deck-code details
223
273
 
224
- - `lang` · drives highlighting: `js` / `ts` / `json` (default), `html` / `xml` /
225
- `svg`, `css` / `scss` / `less`.
274
+ - `lang` · drives highlighting. The **built-in highlighter understands only**
275
+ `js` / `ts` / `json` (default), `html` / `xml` / `svg`, and `css` / `scss` /
276
+ `less`. **Any other value** (`python`, `rust`, `bash`, `go`, `sql`, …) is **not
277
+ an error and produces no warning** — the block is silently colored with the JS
278
+ rules, so the result looks plausible but is wrong. For any language outside the
279
+ list above, install the Shiki plugin (see below); it adds Shiki's full set of
280
+ grammars and is the only way to highlight non-built-in languages correctly.
226
281
  - `hero` · centers the block vertically as the slide's focal element.
227
282
  - `nested` · lighter border, no shadow (for use inside a `deck-card`).
228
283
  - `step-groups` · a JSON array attribute that turns the snippet into a stepped
229
284
  reveal; the number of groups becomes the slide's step count (see §7).
230
285
 
231
286
  Highlighting is done client-side with a built-in regex highlighter (no build
232
- step). An opt-in **Shiki plugin** can upgrade every `deck-code` block to
233
- Shiki's grammars/themes.
287
+ step), limited to the languages listed under `lang` above. An opt-in **Shiki
288
+ plugin** upgrades every `deck-code` block to Shiki's full grammar/theme set —
289
+ **required** whenever a deck uses a language the built-in highlighter does not
290
+ understand.
234
291
 
235
292
  ### Shiki plugin (optional, opt-in)
236
293
 
237
294
  `src/plugins/shiki.ts` (`dist/shiki.js`) re-renders all `<deck-code>` blocks
238
- through [Shiki](https://shiki.style), loaded from a CDN on first use. Install it
239
- after the rikiki bundle:
295
+ through [Shiki](https://shiki.style), loaded from the vendored
296
+ `dist/vendor/shiki.js` bundle on first use (offline · no CDN). Install it after
297
+ the rikiki bundle:
240
298
 
241
299
  ```html
242
300
  <script type="module" src="./dist/index.js"></script>
@@ -252,19 +310,23 @@ API:
252
310
  async function installShiki(opts?: {
253
311
  theme?: string; // any Shiki theme name (https://shiki.style/themes) · default 'one-dark-pro'
254
312
  langs?: string[]; // grammars to preload · default ['ts', 'js', 'html', 'css', 'json']
255
- cdn?: string; // CDN base to load Shiki from · default 'https://esm.sh/'
256
313
  }): Promise<void>
257
314
  ```
258
315
 
259
316
  - Any Shiki theme/language works (not just the built-in highlighter's set); set
260
317
  `langs` to whatever your deck uses.
261
- - Shiki's inline token colors are stripped so the deck's `--rik-code__syntax-*`
262
- tokens still theme the output.
318
+ - Shiki owns the palette under this plugin: it colors each token with an inline
319
+ style from the chosen `theme`, so pick a `theme` that suits your code
320
+ background (e.g. `one-dark-pro` on a dark deck). The `--deck-code-syntax-*`
321
+ tokens only affect the built-in highlighter, not Shiki output.
263
322
  - A language not loaded falls back silently to the built-in regex highlighter
264
323
  (no error).
265
- - **Trade-off:** pulls ~300 KB of Shiki + requested grammars from the CDN. That
266
- is why it is opt-in · the core bundle stays ~12 KB gzip. Use `cdn` to point at
267
- a self-hosted mirror.
324
+ - **How it hooks in:** it registers a highlighter on the shared `<deck-code>`
325
+ class via `setDeckCodeHighlighter` (resolved through `customElements.get`), so
326
+ it never patches the component's internals · see *Writing a plugin* below.
327
+ - **Trade-off:** the vendored Shiki bundle is large (every grammar + theme, JS
328
+ engine, no wasm). That is why it is opt-in and lazy · the core bundle stays
329
+ ~14 KB gzip.
268
330
 
269
331
  ---
270
332
 
@@ -333,9 +395,11 @@ Morph pairs that swap on the same click need explicit steps
333
395
  (`data-click-hide="1"` on the outgoing element, `data-click="1"` on the
334
396
  incoming one) · bare attributes would put them on two sequential clicks.
335
397
 
336
- The plugin patches `deck-root` so its step counter accounts for `[data-click]`
337
- elements, and stepping toggles their visibility. Going back cancels pending
338
- auto/stagger timers. Deep links and back-navigation settle instantly (no
398
+ The plugin registers on each `<deck-root>` through the plugin hook API (see
399
+ *Writing a plugin* below): its `steps` hook makes the step counter account for
400
+ `[data-click]` elements, its `applyStep` hook toggles their visibility, and its
401
+ `navigate` hook wraps cross-slide `data-morph` transitions. Going back cancels
402
+ pending auto/stagger timers. Deep links and back-navigation settle instantly (no
339
403
  replayed delays). It respects `prefers-reduced-motion`. When a `data-morph`
340
404
  navigation runs, the deck-wide `transition="…"` animation is skipped for that
341
405
  navigation so the two don't fight, and pending auto/stagger reveals start
@@ -343,6 +407,42 @@ once the morph settles instead of firing mid-transition. On auto/stagger
343
407
  elements, `data-anim-delay` is folded into the timer (delays add up once,
344
408
  they don't apply twice).
345
409
 
410
+ ### Writing a plugin (the hook API)
411
+
412
+ Both opt-in plugins extend the deck through a small **public** contract rather
413
+ than reaching into engine internals. A plugin is a `DeckPlugin` object; register
414
+ it on a deck instance with `deckRoot.use(plugin)`, which returns an unregister
415
+ function.
416
+
417
+ ```ts
418
+ import type { DeckPlugin } from './dist/index.js';
419
+
420
+ const myPlugin: DeckPlugin = {
421
+ name: 'my-plugin', // unique · use() is idempotent by name
422
+ setup(ctx) { /* … */ return () => {}; }, // on register · optional teardown
423
+ steps(slide, ctx) { return 0; }, // contribute step count (combined as a max)
424
+ applyStep(step, slide, ctx) { /* … */ }, // after the engine applied the step
425
+ navigate(to, ctx, proceed) { return false; }, // around-advice · call proceed() to navigate
426
+ };
427
+ document.querySelector('deck-root').use(myPlugin);
428
+ ```
429
+
430
+ The `ctx` (`DeckContext`) is the only surface a plugin touches:
431
+ `ctx.host` (the element), `ctx.current`, `ctx.step`, `ctx.slides`,
432
+ `ctx.requestUpdate()`. Notes:
433
+
434
+ - **`navigate` is around-advice** · return a truthy value to take ownership and
435
+ call `proceed()` yourself (e.g. inside a `startViewTransition`); return falsy
436
+ to let the engine navigate. Only the **first** registered plugin with a
437
+ `navigate` hook owns navigation (a second one warns and is ignored).
438
+ - **`steps` from all plugins are combined with the engine's own as a maximum.**
439
+ - **`<deck-code>` highlighting** is a separate hook: `setDeckCodeHighlighter(fn)`
440
+ registers a `(code, lang) => string | null` highlighter on the shared
441
+ `<deck-code>` class (return `null` to fall back to the built-in regex one).
442
+ - `installClickStages()` / `installShiki()` are **back-compat shims** that attach
443
+ the plugin to every `<deck-root>` already on the page · a deck created
444
+ dynamically *after* the call must register the plugin itself with `use()`.
445
+
346
446
  ---
347
447
 
348
448
  ## 8 · Presenter mode
@@ -351,6 +451,28 @@ Press **`P`** to open a speaker window. It mirrors the current slide and the
351
451
  next slide (rendered live via the rikiki bundle), shows a timer/clock, and
352
452
  displays the speaker notes for the current slide.
353
453
 
454
+ On a Chromium browser with a second screen, pressing **`P`** sends the **deck
455
+ fullscreen to the external screen** (the projector) and opens the **speaker
456
+ window on the speaker's current screen** · the audience gets the slides, the
457
+ speaker keeps the presenter view on their laptop. The deck fullscreen is
458
+ released when the presenter closes. Without the Window Management API
459
+ (Firefox/Safari), the permission, or a second screen, the deck stays in its
460
+ window and the popup uses the default placement.
461
+
462
+ Notes on the first use: the **first** `P` press prompts for the Window
463
+ Management permission; the speaker window always opens regardless (it is never
464
+ blocked on the prompt). The slides move to the projector as soon as the screen
465
+ layout is known · because that layout is only available after the permission is
466
+ granted, the very first time may take a second `P` press (close + reopen) before
467
+ the slides go fullscreen; afterwards it happens on the press itself (the cached
468
+ layout lets the fullscreen ride the keypress activation, and `window.open`
469
+ relies on popups being allowed for the origin · which the presenter already
470
+ requires). The Current/Next previews are constrained to a 16:9 box so the
471
+ thumbnail matches the projected slide's geometry regardless of the window shape.
472
+ While the speaker window is open, the projected deck auto-hides its key-hint
473
+ chips and nav arrows (the same effect as `no-hint` / `no-arrows`), restoring
474
+ them on close.
475
+
354
476
  Speaker notes live in a `<deck-notes>` element placed inside any slide host.
355
477
  It is hidden in the deck itself; only the presenter window reads its text.
356
478
 
package/llms.txt CHANGED
@@ -5,7 +5,7 @@
5
5
  > stylesheet, then the component bundle, then write a `<deck-root>` wrapping
6
6
  > `deck-*` slide elements. No build step to author or run a deck.
7
7
 
8
- This reference documents rikiki v0.4.0.
8
+ This reference documents rikiki v0.6.0.
9
9
 
10
10
  The framework is opinionated:
11
11
 
@@ -33,9 +33,11 @@ Authoring tags fall into four buckets (see the reference for the exhaustive attr
33
33
  ## Plugins (opt-in, lazy)
34
34
 
35
35
  - **Transitions**: `transition="slide|slide-up|slide-down|slide-right|fade|zoom|flip"` on `<deck-root>` (per-slide `data-transition`).
36
- - **Presenter window**: press `P` · mirrors current + next slide, timer, and `<deck-notes>`.
37
- - **Shiki highlighting**: `import { installShiki } from './dist/shiki.js'` then `await installShiki({ theme, langs })`.
36
+ - **Presenter window**: press `P` · mirrors current + next slide, timer, and `<deck-notes>`. With a second screen it sends the slides fullscreen to the projector and keeps the presenter view on the speaker's screen; previews are 16:9; the projected deck auto-hides its chrome while it's open.
37
+ - **Shiki highlighting**: the built-in `deck-code` highlighter only knows js/ts/json/html/xml/svg/css/scss/less; any other `lang` is silently colored as JS (no error). For any other language, install Shiki **after** `dist/index.js`: `import { installShiki } from './dist/shiki.js'` then `await installShiki({ theme, langs })` (list every language the deck uses in `langs`).
38
38
  - **Click-stages**: `import { installClickStages } from './dist/click-stages.js'` then `installClickStages()` · per-element `data-click` / `data-anim` / `data-morph` reveals.
39
+ - **Slide zoom**: Ctrl/⌘+wheel / pinch / `+`/`-`/`0` magnify a slide (fixed canvas, on by default, pan by drag/wheel); `no-zoom` on `<deck-root>` opts out.
40
+ - **Writing a plugin**: `deckRoot.use(plugin)` with a `DeckPlugin` (`steps` / `applyStep` / `navigate` / `setup` hooks); `setDeckCodeHighlighter(fn)` overrides `<deck-code>` highlighting. Both re-exported from `dist/index.js`. `installShiki` / `installClickStages` are back-compat shims.
39
41
  - **Livereload (authoring only)**: append `?live` to the deck URL.
40
42
 
41
43
  ## Build & assembly
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "rikiki-deck",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "A tiny Lit Web Components framework for technical presentations · zero-build for consumers, TypeScript for contributors.",
5
+ "author": "Cédric Chariere Fiedler <cedric@siliceum.com>",
5
6
  "homepage": "https://rikiki.tordu-jardin.fr",
6
7
  "repository": {
7
8
  "type": "git",
@@ -36,10 +37,13 @@
36
37
  "fonts",
37
38
  "README.md",
38
39
  "llms.txt",
39
- "docs/llms"
40
+ "docs/llms",
41
+ ".claude/skills/rikiki-deck",
42
+ ".claude/skills/rikiki-theme",
43
+ ".claude/skills/rikiki-debug"
40
44
  ],
41
45
  "scripts": {
42
- "build": "node build-vendor.mjs && node build.mjs && node build-standalone.mjs && tsc --emitDeclarationOnly",
46
+ "build": "node build-vendor.mjs && node build.mjs && node build-standalone.mjs && tsc --emitDeclarationOnly -p tsconfig.build.json",
43
47
  "vendor": "node build-vendor.mjs",
44
48
  "watch": "node build.mjs --watch",
45
49
  "typecheck": "tsc --noEmit",
@@ -48,9 +52,16 @@
48
52
  "deck": "node build/vite-deck.mjs",
49
53
  "deck:example": "node build/vite-deck.mjs decks/example/deck.config.js",
50
54
  "test": "vitest run",
51
- "bump": "node scripts/bump-version.mjs"
55
+ "test:e2e": "playwright test",
56
+ "test:smoke": "playwright test --grep @smoke",
57
+ "lint": "biome check src scripts",
58
+ "format": "biome format --write src scripts",
59
+ "bump": "node scripts/bump-version.mjs",
60
+ "prepublishOnly": "npm run build && npm test"
52
61
  },
53
62
  "devDependencies": {
63
+ "@biomejs/biome": "^2.4.16",
64
+ "@playwright/test": "^1.60.0",
54
65
  "esbuild": "^0.24.0",
55
66
  "lit": "^3.3.3",
56
67
  "marked": "^12.0.2",
package/themes/rikiki.css CHANGED
@@ -177,7 +177,8 @@
177
177
  --rik-font-mono: 'Space Mono', 'JetBrains Mono', ui-monospace, monospace;
178
178
 
179
179
  /* Typography · body scale · modular ratio 1.25 (major third) from 1rem.
180
- html root-size is `clamp(14px, 2.35vh, 42px)` so rems scale with viewport.
180
+ html root-size is constant (`canvas-h × 0.0235` ≈ 25px); the deck-root
181
+ transform scales the whole canvas to fit, so rems never touch the viewport.
181
182
  New `--rik-text-*` aliases follow a documented t-shirt scale; the
182
183
  historical `--rik-font-size-*` names map to the same values for back-compat. */
183
184
  --rik-text-2xs: 0.64rem; /* 1 / 1.25^2 */
@@ -196,10 +197,10 @@
196
197
  order is now monotonic (strong < big < mega < hook < stat). */
197
198
  --rik-font-size-xs: var(--rik-text-2xs);
198
199
  --rik-font-size-sm: var(--rik-text-xs);
199
- --rik-font-size-body: 0.92rem; /* between -xs and -sm */
200
- --rik-font-size-lead: var(--rik-text-sm);
201
- --rik-font-size-h2: var(--rik-text-base);
202
- --rik-font-size-h1: var(--rik-text-md);
200
+ --rik-font-size-body: var(--rik-text-base); /* ≈32px · sized for projection */
201
+ --rik-font-size-lead: var(--rik-text-md);
202
+ --rik-font-size-h2: var(--rik-text-md);
203
+ --rik-font-size-h1: var(--rik-text-lg);
203
204
  --rik-font-size-section: var(--rik-text-lg);
204
205
  --rik-font-size-display: var(--rik-text-xl);
205
206
  --rik-font-size-mono: 0.88rem;
@@ -277,14 +278,10 @@
277
278
  *, *::before, *::after { box-sizing: border-box; }
278
279
  body, h1, h2, h3, h4, p, ul, ol, li, blockquote, pre, table, td, th,
279
280
  figure, dl, dd { margin: 0; padding: 0; }
280
- html { font-size: clamp(14px, 2.35vh, 42px); }
281
- /* Fixed-viewport decks (<deck-root fixed>) anchor the root font-size to the
282
- logical canvas height instead of the window, so every rem-based size scales
283
- with the letterboxed stage rather than the screen · no reflow off-ratio. */
284
- html:has(deck-root[fixed]) {
285
- font-size: calc(min(100vh, 100vw * var(--deck-canvas-h, 1080) / var(--deck-canvas-w, 1920)) * 0.0235);
286
- }
287
- html, body { width: 100%; height: 100%; overflow: hidden; background: var(--rik-surface-page); }
281
+ /* The rem baseline and the no-scroll reset are injected globally by deck-root
282
+ (theme-independent · see _injectGlobals). The theme only owns the page surface
283
+ and the type below. */
284
+ html, body { background: var(--rik-surface-page); }
288
285
  body { font-family: var(--rik-font-sans); color: var(--rik-text-default); }
289
286
 
290
287
  ::selection { background: var(--rik-selection__bg); color: var(--rik-selection__text); }
@@ -164,10 +164,12 @@
164
164
 
165
165
  --rik-font-size-xs: var(--rik-text-2xs);
166
166
  --rik-font-size-sm: var(--rik-text-xs);
167
- --rik-font-size-body: 0.92rem;
168
- --rik-font-size-lead: var(--rik-text-sm);
169
- --rik-font-size-h2: var(--rik-text-base);
170
- --rik-font-size-h1: var(--rik-text-md);
167
+ /* Slide body sized for projection (≈32px on the 1080 canvas) · the headings
168
+ step up with it so the hierarchy (incl. markdown ## → h2) stays above body. */
169
+ --rik-font-size-body: var(--rik-text-base);
170
+ --rik-font-size-lead: var(--rik-text-md);
171
+ --rik-font-size-h2: var(--rik-text-md);
172
+ --rik-font-size-h1: var(--rik-text-lg);
171
173
  --rik-font-size-section: var(--rik-text-lg);
172
174
  --rik-font-size-display: var(--rik-text-xl);
173
175
  --rik-font-size-mono: 0.88rem;
@@ -236,14 +238,10 @@
236
238
  *, *::before, *::after { box-sizing: border-box; }
237
239
  body, h1, h2, h3, h4, p, ul, ol, li, blockquote, pre, table, td, th,
238
240
  figure, dl, dd { margin: 0; padding: 0; }
239
- html { font-size: clamp(14px, 2.35vh, 42px); }
240
- /* Fixed-viewport decks (<deck-root fixed>) anchor the root font-size to the
241
- logical canvas height instead of the window, so every rem-based size scales
242
- with the letterboxed stage rather than the screen · no reflow off-ratio. */
243
- html:has(deck-root[fixed]) {
244
- font-size: calc(min(100vh, 100vw * var(--deck-canvas-h, 1080) / var(--deck-canvas-w, 1920)) * 0.0235);
245
- }
246
- html, body { width: 100%; height: 100%; overflow: hidden; background: var(--rik-surface-page); }
241
+ /* The rem baseline and the no-scroll reset are injected globally by deck-root
242
+ (theme-independent · see _injectGlobals). The theme only owns the page surface
243
+ and the type below. */
244
+ html, body { background: var(--rik-surface-page); }
247
245
  body { font-family: var(--rik-font-sans); color: var(--rik-text-default); }
248
246
 
249
247
  ::selection { background: var(--rik-selection__bg); color: var(--rik-selection__text); }