oficina / slides Public

Clone
git clone https://amadan.net/oficina/slides

Plain git — no account needed to clone.

Download

Download this file

1/* slides.css — the look of a slide.
2
3 internal/slides/render builds the tree; this file decides where the
4 regions sit and how big the words are. Nothing here knows what a deck
5 says, and nothing in the renderer writes a rule, so a template's
6 layout changes here and only here.
7
8 It is a separate stylesheet from tokens.css and theme.css on purpose.
9 Those two dress the app — the chrome around a deck, in the reader's
10 own light or dark. A slide is not chrome: it is a document with a
11 fixed look that has to be the same on the presenter's laptop and on
12 the projector, so its palette comes from the deck's theme
13 (internal/slides/render/themes.go, which says why it does not follow
14 the OS scheme) and arrives as custom properties inline on the
15 section. Everything below reads those; it never names a colour except
16 as a fallback for a section that arrived without them.
17
18 ── How a slide scales ───────────────────────────────────────────────
19
20 The section is a container (`container-type: inline-size`) and every
21 size inside it is in `cqw`, so a slide is one design that works at any
22 width: 1920 on a projector, 900 in the editor, 380 on a phone. There
23 is no second stylesheet and no breakpoint doing the scaling.
24
25 The type scale lives as custom properties ON the section and is used
26 BY its descendants. That order is load-bearing: `cqw` in a property
27 resolves against the element's own query container, and an element is
28 never its own container, so a `cqw` written directly on the section
29 would measure the page around it instead. Held in a custom property
30 it is substituted into the descendant's declaration and measured
31 there, against the section. The section's own padding is a percentage
32 for the same reason.
33
34 And that is why the template grid is not on the section but on the
35 `[slides-frame]` inside it. `gap: var(--slides-gap)` is a `cqw`
36 length USED on the element it is written on, which is the one place
37 the rule above bites: written on the section it measured whatever
38 container was outside — the space between a heading and a body was
39 49.92px on a 1920 screen, 20.27px in a 780px window, and a different
40 number again in the editor's stage. On the frame it is a descendant
41 declaration like every other one, so it is 2.6% of the section's
42 content box wherever the slide is drawn (43.68px at 1920), and
43 internal/slides/layout can state one number for it.
44
45 ── The phone ────────────────────────────────────────────────────────
46
47 Three mechanisms, and they do different jobs:
48
49 - `@container slide (max-width: 600px)` collapses the template grids
50 to one column, honours the `data-phone-*` layer, and puts a rem
51 floor under the type scale so a 320px reflow is still readable.
52 It fires for a narrow container anywhere — a phone, a two-up
53 editor, a preview pane.
54 - `[data-vp="phone"]`, which the renderer writes when it was asked
55 for a phone, drops the 16:9 frame on a flow slide so it grows as
56 tall as its content. A container query cannot do this: a query
57 styles a container's descendants, never the container itself.
58 - `@media (max-width: 600px)` does the same for a slide that was
59 rendered for a desktop and opened on a phone anyway — the
60 scriptless viewer has no way to know which it is serving, and §8's
61 320px reflow is not optional.
62
63 Modern CSS is the floor (rastrillo SKILL.md): container queries,
64 `oklch()`, `color-mix()` and `light-dark()` are all free here.
65
66 ── The fonts ────────────────────────────────────────────────────────
67
68 The one thing this file does fetch: the twelve Liberation faces the
69 app serves itself, declared below. They are not decoration. The Go
70 layout engine (internal/slides/layout) measures a slide with these
71 exact font programs to produce the SVG thumbnail and the PDF, so a
72 browser laying the same slide out with whatever the machine calls
73 `system-ui` would break its lines somewhere else — and a deck that
74 paginates one way on screen and another way in its own export is two
75 decks. internal/slides/fonts holds the same twelve files for the
76 engine's side of that promise.
77
78 Each URL carries the file's content hash so the asset handler serves
79 it immutable (rastrillo's Assets: a hashed name that matches the
80 current content is cacheable forever). A hash that has gone stale is
81 not a 404 — the handler strips it, finds the file and serves it
82 no-cache — so the failure mode is a slower fetch, not a missing
83 font. internal/slidestest's TestSlidesCSSFontURLsCarryTheirContentHash
84 is what keeps them current. */
85
86/* ── @font-face ──────────────────────────────────────────────────────
87 Liberation Serif, Sans and Mono, SIL OFL 1.1 — the licence ships
88 beside them at /static/fonts/LICENSE-OFL-1.1.txt, as it requires.
89 TrueType rather than woff2 (about twice the bytes) because
90 converting needs a build step this repo's committed-artifact static
91 tree does not have; a browser fetches only the faces a page uses.
92 `swap`, because a slide with invisible text is worse for three
93 seconds than a slide that reflows once. */
94
95@font-face {
96 font-family: "Liberation Sans";
97 font-style: normal;
98 font-weight: 400;
99 font-display: swap;
100 src: url("fonts/LiberationSans-Regular.bade59d822652f76.ttf") format("truetype");
101}
102@font-face {
103 font-family: "Liberation Sans";
104 font-style: normal;
105 font-weight: 700;
106 font-display: swap;
107 src: url("fonts/LiberationSans-Bold.1b5f2da6f4cadce4.ttf") format("truetype");
108}
109@font-face {
110 font-family: "Liberation Sans";
111 font-style: italic;
112 font-weight: 400;
113 font-display: swap;
114 src: url("fonts/LiberationSans-Italic.93ff5260834141b6.ttf") format("truetype");
115}
116@font-face {
117 font-family: "Liberation Sans";
118 font-style: italic;
119 font-weight: 700;
120 font-display: swap;
121 src: url("fonts/LiberationSans-BoldItalic.352e9dc2be89f210.ttf") format("truetype");
122}
123
124@font-face {
125 font-family: "Liberation Serif";
126 font-style: normal;
127 font-weight: 400;
128 font-display: swap;
129 src: url("fonts/LiberationSerif-Regular.9caef765d2e891c1.ttf") format("truetype");
130}
131@font-face {
132 font-family: "Liberation Serif";
133 font-style: normal;
134 font-weight: 700;
135 font-display: swap;
136 src: url("fonts/LiberationSerif-Bold.443a4dbd694b6e46.ttf") format("truetype");
137}
138@font-face {
139 font-family: "Liberation Serif";
140 font-style: italic;
141 font-weight: 400;
142 font-display: swap;
143 src: url("fonts/LiberationSerif-Italic.de18fe26337e0324.ttf") format("truetype");
144}
145@font-face {
146 font-family: "Liberation Serif";
147 font-style: italic;
148 font-weight: 700;
149 font-display: swap;
150 src: url("fonts/LiberationSerif-BoldItalic.4b2635d2ec4d9923.ttf") format("truetype");
151}
152
153@font-face {
154 font-family: "Liberation Mono";
155 font-style: normal;
156 font-weight: 400;
157 font-display: swap;
158 src: url("fonts/LiberationMono-Regular.5883330d94debd99.ttf") format("truetype");
159}
160@font-face {
161 font-family: "Liberation Mono";
162 font-style: normal;
163 font-weight: 700;
164 font-display: swap;
165 src: url("fonts/LiberationMono-Bold.ddce09ee3483b2d5.ttf") format("truetype");
166}
167@font-face {
168 font-family: "Liberation Mono";
169 font-style: italic;
170 font-weight: 400;
171 font-display: swap;
172 src: url("fonts/LiberationMono-Italic.016743fc425a1b61.ttf") format("truetype");
173}
174@font-face {
175 font-family: "Liberation Mono";
176 font-style: italic;
177 font-weight: 700;
178 font-display: swap;
179 src: url("fonts/LiberationMono-BoldItalic.2ec6f3e5bd9b24c7.ttf") format("truetype");
180}
181
182[slides-slide] {
183 /* The palette. These values are what a section renders as when it
184 arrived without inline custom properties — a fragment pasted into
185 a page, a test harness. A real section overrides every one of them
186 from the deck's theme. */
187 --slides-bg: #faf7f0;
188 --slides-fg: #1c1a17;
189 --slides-accent: #8a5410;
190 --slides-accent-2: #46617a;
191 --slides-font-heading: Liberation Serif, Georgia, serif;
192 --slides-font-body: Liberation Sans, system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
193 /* Code is one face on every theme, so it is a variable here and
194 not a theme property. */
195 --slides-font-code: Liberation Mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
196
197 /* The type scale, in cqw so it is a fraction of the slide's own
198 width. Calibrated on a 1920-wide frame: 6.6cqw is ~127px, about a
199 95pt title, and the body steps land near 32, 40 and 49pt — slide
200 sizes, not document sizes. */
201 --slides-t-1: 6.6cqw;
202 --slides-t-2: 5cqw;
203 --slides-t-3: 4cqw;
204 --slides-body-l: 3.4cqw;
205 --slides-body-m: 2.8cqw;
206 --slides-body-s: 2.2cqw;
207 --slides-code: 2.3cqw;
208
209 --slides-gap: 2.6cqw;
210 --slides-rule: 0.5cqw;
211 /* Percentage, not cqw: see the header. 6.25% of a 16:9 frame is
212 120px at 1920, the margin a slide template usually keeps. */
213 --slides-pad: 6.25%;
214
215 box-sizing: border-box;
216 container-type: inline-size;
217 container-name: slide;
218 aspect-ratio: 16 / 9;
219 inline-size: 100%;
220 margin: 0;
221 padding: var(--slides-pad);
222 background-color: var(--slides-bg);
223 color: var(--slides-fg);
224 font-family: var(--slides-font-body);
225 line-height: 1.3;
226 text-wrap: pretty;
227 /* A slide that overflows scrolls rather than clipping. Losing the
228 end of a sentence to `overflow: hidden` is a WCAG 1.4.10 failure
229 nobody would ever see reported; a scrollbar is visible, and it is
230 the estilo allowance §8 already makes for the canvas stage. */
231 overflow: auto;
232
233 /* One cell, which the frame fills. The template grid is on the
234 frame (see the header); all the section does is give it the
235 content box to be laid out in. */
236 display: grid;
237 grid-template-columns: minmax(0, 1fr);
238 grid-template-rows: minmax(0, 1fr);
239}
240
241[slides-slide] * {
242 box-sizing: border-box;
243}
244
245/* The frame: the template grid, one cell of the section, inside its
246 padding. Every `cqw` written here is a descendant's and resolves
247 against the section, which is the whole reason it exists. A canvas
248 slide has no frame — its blocks are placed by hand on the section
249 itself, and there is no gap to get wrong. */
250[slides-frame] {
251 display: grid;
252 gap: var(--slides-gap);
253 grid-template-columns: minmax(0, 1fr);
254 align-content: start;
255 min-inline-size: 0;
256 min-block-size: 0;
257}
258
259/* A slide's own backdrop (§2.2): a colour arrives as --slides-bg and
260 needs no rule; an image arrives as a URL to paint. */
261[slides-slide][data-background="image"] {
262 background-image: var(--slides-bg-image);
263 background-size: cover;
264 background-position: center;
265}
266
267/* ── Regions ─────────────────────────────────────────────────────────
268 A region is a grid area and a column of blocks. A region a template
269 has no area for falls through to auto placement rather than
270 disappearing, which is what makes an aside on a title slide still
271 readable. */
272
273[slides-region] {
274 display: flex;
275 flex-direction: column;
276 gap: var(--slides-gap);
277 min-inline-size: 0;
278 min-block-size: 0;
279}
280
281[slides-region][data-region="heading"] {
282 grid-area: heading;
283}
284[slides-region][data-region="body"] {
285 grid-area: body;
286}
287[slides-region][data-region="aside"] {
288 grid-area: aside;
289}
290[slides-region][data-region="media"] {
291 grid-area: media;
292}
293
294/* ── Templates ───────────────────────────────────────────────────────
295 Each is a grid, and each names its areas so the region rules above
296 land in the right cell. */
297
298/* title — the opening slide: everything centred, in the middle. */
299[slides-slide][data-template="title"] > [slides-frame] {
300 grid-template-areas: "heading" "body";
301 grid-template-rows: auto auto;
302 align-content: center;
303 justify-items: center;
304 text-align: center;
305}
306[slides-slide][data-template="title"] [slides-region] {
307 align-items: center;
308 max-inline-size: 80%;
309}
310
311/* title-body — the workhorse: a heading, then everything else. */
312[slides-slide][data-template="title-body"] > [slides-frame] {
313 grid-template-areas: "heading" "body";
314 grid-template-rows: auto 1fr;
315}
316
317/* two-column — the heading spans, body and aside sit side by side. */
318[slides-slide][data-template="two-column"] > [slides-frame] {
319 grid-template-areas:
320 "heading heading"
321 "body aside";
322 grid-template-columns: 1fr 1fr;
323 grid-template-rows: auto 1fr;
324 column-gap: calc(var(--slides-gap) * 1.6);
325}
326
327/* section — a chapter break: one heading, centred, over an accent bar
328 that is the only decoration in the whole stylesheet. */
329[slides-slide][data-template="section"] > [slides-frame] {
330 grid-template-areas: "heading";
331 align-content: center;
332}
333[slides-slide][data-template="section"] [data-region="heading"] {
334 align-items: center;
335}
336[slides-slide][data-template="section"] [data-region="heading"]::after {
337 content: "";
338 inline-size: 18cqw;
339 block-size: var(--slides-rule);
340 background-color: var(--slides-accent);
341 border-radius: var(--slides-rule);
342}
343
344/* image-full — the media fills the frame and the heading sits over its
345 bottom-left corner, on a scrim so the words survive a light picture.
346 The heading is still first in the document: the picture is not what
347 the slide is about. */
348[slides-slide][data-template="image-full"] {
349 /* No padding, so one cqw on this template is 19.2px and not 16.8
350 — internal/slides/layout carries both scales for it. */
351 padding: 0;
352}
353[slides-slide][data-template="image-full"] > [slides-frame] {
354 grid-template-areas: "media";
355 grid-template-rows: 1fr;
356 gap: 0;
357}
358[slides-slide][data-template="image-full"] [data-region="heading"] {
359 grid-area: media;
360 z-index: 1;
361 align-self: end;
362 justify-self: start;
363 max-inline-size: 80%;
364 margin: var(--slides-pad);
365 padding: calc(var(--slides-gap) / 2) var(--slides-gap);
366 border-radius: 1cqw;
367 /* The scrim is the slide's own background, mixed towards
368 transparent: it belongs to whichever theme is on. */
369 background-color: color-mix(in oklab, var(--slides-bg) 82%, transparent);
370 backdrop-filter: blur(0.6cqw);
371}
372[slides-slide][data-template="image-full"] [data-region="media"],
373[slides-slide][data-template="image-full"] [data-block="image"] {
374 block-size: 100%;
375 min-block-size: 0;
376}
377
378/* quote — one pull quote, centred, and nothing competing with it. */
379[slides-slide][data-template="quote"] > [slides-frame] {
380 grid-template-areas: "body";
381 align-content: center;
382 justify-items: center;
383}
384[slides-slide][data-template="quote"] [slides-region] {
385 align-items: center;
386 max-inline-size: 84%;
387 text-align: center;
388}
389
390/* blank — a single body region, stacked from the top. It is also what
391 a canvas slide reflowed onto a phone becomes (§2.3). */
392[slides-slide][data-template="blank"] > [slides-frame] {
393 grid-template-areas: "body";
394}
395
396/* ── Blocks ──────────────────────────────────────────────────────────
397 Semantic elements, sized off the scale, with the browser's own
398 margins removed: spacing on a slide is the region's gap, so two
399 blocks are never a gap plus a margin. */
400
401[slides-block] {
402 margin: 0;
403 min-inline-size: 0;
404}
405
406[slides-block][data-block="heading"] {
407 font-family: var(--slides-font-heading);
408 font-weight: 600;
409 line-height: 1.1;
410 text-wrap: balance;
411 letter-spacing: -0.01em;
412}
413[slides-block][data-level="1"] {
414 font-size: var(--slides-t-1);
415}
416[slides-block][data-level="2"] {
417 font-size: var(--slides-t-2);
418}
419[slides-block][data-level="3"] {
420 font-size: var(--slides-t-3);
421 color: var(--slides-accent);
422}
423[slides-slide][data-template="section"] [data-block="heading"] {
424 text-align: center;
425}
426
427[slides-block][data-block="text"],
428[slides-block][data-block="list"] {
429 font-size: var(--slides-body-m);
430}
431[slides-block][data-size="s"] {
432 font-size: var(--slides-body-s);
433}
434[slides-block][data-size="l"] {
435 font-size: var(--slides-body-l);
436}
437[slides-block][data-align="center"] {
438 text-align: center;
439}
440[slides-block][data-align="end"] {
441 text-align: end;
442}
443[slides-block][data-align="justify"] {
444 text-align: justify;
445}
446
447[slides-block] p {
448 margin: 0;
449}
450[slides-block] p + p {
451 margin-block-start: 0.6em;
452}
453
454[slides-block][data-block="list"] {
455 display: flex;
456 flex-direction: column;
457 gap: 0.4em;
458 padding-inline-start: 1.4em;
459}
460[slides-block][data-block="list"][data-indent="1"] {
461 padding-inline-start: 3em;
462}
463[slides-block][data-block="list"][data-indent="2"] {
464 padding-inline-start: 4.6em;
465}
466[slides-block][data-block="list"][data-indent="3"] {
467 padding-inline-start: 6.2em;
468}
469[slides-block][data-block="list"] li::marker {
470 color: var(--slides-accent);
471}
472
473[slides-block][data-block="quote"] {
474 font-size: var(--slides-body-l);
475 display: flex;
476 flex-direction: column;
477 gap: 0.6em;
478}
479[slides-block][data-block="quote"] blockquote {
480 margin: 0;
481 font-style: italic;
482 text-wrap: balance;
483}
484[slides-block][data-block="quote"] figcaption {
485 font-size: 0.62em;
486 font-style: normal;
487 color: var(--slides-accent);
488}
489[slides-block][data-block="quote"] figcaption::before {
490 content: "— ";
491}
492[slides-block][data-block="quote"] cite {
493 font-style: normal;
494}
495
496[slides-block][data-block="image"] {
497 display: flex;
498 min-block-size: 0;
499}
500[slides-block][data-block="image"] img {
501 inline-size: 100%;
502 block-size: 100%;
503 max-inline-size: 100%;
504 border-radius: var(--slides-radius, 0);
505 object-fit: cover;
506}
507[slides-block][data-block="image"][data-fit="contain"] img {
508 object-fit: contain;
509}
510/* An image with no alt yet is not an error the reader should meet, but
511 the editor stage wants to see it. The outline shows only where the
512 host page asks for it. */
513[slides-editing] [slides-block] img[data-missing-alt] {
514 outline: var(--slides-rule) dashed var(--slides-accent);
515 outline-offset: calc(var(--slides-rule) * -1);
516}
517/* A block nothing reaches — named as a child by a columns block that
518 is itself inside one of its children, a shape the ops refuse but a
519 merge of two people's edits can still make (codex 7). The renderer
520 puts it after everything else instead of dropping it; the content is
521 what the reader sees, and the editor stage sees the outline too. The
522 wrapper is a region with no grid area, so it lands in a fresh row
523 under the template's own. */
524[slides-region][data-region="unreached"] {
525 grid-column: 1 / -1;
526}
527[slides-editing] [slides-block][data-unreached] {
528 outline: var(--slides-rule) dashed var(--slides-accent-2);
529 outline-offset: calc(var(--slides-rule) * -1);
530}
531
532[slides-block][data-block="columns"] {
533 display: grid;
534 gap: var(--slides-gap);
535 grid-template-columns: repeat(2, minmax(0, 1fr));
536}
537[slides-block][data-block="columns"][data-count="3"] {
538 grid-template-columns: repeat(3, minmax(0, 1fr));
539}
540
541[slides-block][data-block="code"] {
542 font-size: var(--slides-code);
543 font-family: var(--slides-font-code);
544 line-height: 1.45;
545 padding: 1em;
546 border-radius: 0.6em;
547 overflow-x: auto;
548 tab-size: 2;
549 /* A panel a shade off the slide, in whichever direction the theme
550 runs — no second colour needed, and no assumption about light or
551 dark. */
552 background-color: color-mix(in oklab, var(--slides-fg) 8%, var(--slides-bg));
553}
554[slides-block][data-block="code"] code {
555 font: inherit;
556}
557[slides-block] :not(pre) > code {
558 font-family: var(--slides-font-code);
559 font-size: 0.88em;
560 padding: 0.1em 0.3em;
561 border-radius: 0.25em;
562 background-color: color-mix(in oklab, var(--slides-fg) 10%, var(--slides-bg));
563}
564
565[slides-block] mark {
566 background-color: color-mix(in oklab, var(--slides-accent) 26%, var(--slides-bg));
567 color: inherit;
568 padding: 0 0.15em;
569 border-radius: 0.2em;
570}
571[slides-block] a {
572 color: var(--slides-accent);
573 /* Never colour alone (§8): a link is underlined as well. */
574 text-decoration: underline;
575 text-underline-offset: 0.15em;
576 text-decoration-thickness: 0.07em;
577}
578
579[slides-block][data-block="table"] {
580 inline-size: 100%;
581 font-size: var(--slides-body-s);
582 border-collapse: collapse;
583}
584[slides-block][data-block="table"] th,
585[slides-block][data-block="table"] td {
586 text-align: start;
587 vertical-align: top;
588 padding: 0.45em 0.7em;
589 border-block-end: 1px solid color-mix(in oklab, var(--slides-fg) 22%, var(--slides-bg));
590}
591[slides-block][data-block="table"] th {
592 font-weight: 600;
593 border-block-end-color: var(--slides-accent);
594}
595
596[slides-block][data-block="divider"] {
597 border: 0;
598 inline-size: 100%;
599 block-size: var(--slides-rule);
600 background-color: color-mix(in oklab, var(--slides-fg) 30%, var(--slides-bg));
601 border-radius: var(--slides-rule);
602}
603
604[slides-block][data-block="spacer"] {
605 block-size: var(--slides-gap);
606 flex: none;
607}
608[slides-block][data-block="spacer"][data-size="s"] {
609 block-size: calc(var(--slides-gap) / 2);
610}
611[slides-block][data-block="spacer"][data-size="l"] {
612 block-size: calc(var(--slides-gap) * 2);
613}
614
615[slides-block][data-block="shape"] {
616 inline-size: 100%;
617 block-size: auto;
618 min-block-size: 8cqw;
619 overflow: visible;
620}
621
622/* ── Canvas ──────────────────────────────────────────────────────────
623 Blocks placed by hand (§2.3). The boxes are percentages of the frame,
624 so a canvas slide needs no breakpoint at all: it scales as a whole,
625 text included, because the type scale is in cqw. */
626
627[slides-slide][data-layout="canvas"] {
628 display: block;
629 position: relative;
630 padding: 0;
631 overflow: hidden;
632}
633[slides-slide][data-layout="canvas"] > [slides-block] {
634 position: absolute;
635 display: flex;
636 flex-direction: column;
637 /* Top-aligned and never clipped: a box is where text STARTS, not a
638 viewport onto it. Centring plus overflow: hidden cut the first and
639 last lines of any block taller than its box (seen on the sample
640 deck's canvas heading, 2026-09-07) — the same WCAG 1.4.10 failure
641 the slide-level rule above refuses. Text that outgrows its box
642 runs past it; the person shrinks the type or grows the box. */
643 justify-content: flex-start;
644 overflow: visible;
645}
646[slides-slide][data-layout="canvas"] > [data-block="shape"] {
647 display: block;
648 block-size: 100%;
649 min-block-size: 0;
650 overflow: hidden;
651}
652[slides-slide][data-layout="canvas"] > [data-block="image"] {
653 display: block;
654 overflow: hidden;
655}
656
657/* ── The phone layer ─────────────────────────────────────────────────
658 Everything below is scoped to a narrow container, and it is last in
659 the file so a phone rule beats the desktop rule it ties with. */
660
661@container slide (max-width: 600px) {
662 /* A rem floor under the scale: at 380px a slide's 2.8cqw body would
663 be 10px, and §8 takes 320px reflow with no exemptions. The floors
664 are set on the descendants rather than on the section, because a
665 container query styles a container's contents and never the
666 container itself. */
667 [slides-frame],
668 [slides-region],
669 [slides-slide] > [slides-block] {
670 --slides-t-1: max(1.6rem, 9cqw);
671 --slides-t-2: max(1.35rem, 7cqw);
672 --slides-t-3: max(1.15rem, 5.6cqw);
673 --slides-body-l: max(1.1rem, 4.6cqw);
674 --slides-body-m: max(1rem, 4cqw);
675 --slides-body-s: max(0.9rem, 3.4cqw);
676 --slides-code: max(0.85rem, 3.2cqw);
677 --slides-gap: max(0.8rem, 4cqw);
678 }
679
680 /* Every template collapses to one column, in document order. Both
681 halves matter: `grid-area: auto` alone is not enough, because a
682 template that still carries an explicit multi-column track (only
683 `two-column` does, at 1fr 1fr) auto-places its items into that
684 track instead of stacking them — the region rule below undoes the
685 named placement, this one undoes the columns underneath it.
686
687 The region rule has to be `[slides-region][data-region]`, not
688 `[slides-region]`: the named placements above are (0,2,0), and a
689 (0,1,0) rule here lost to them however late it came, so every
690 region kept an area the collapsed grid no longer has and all of
691 them landed in one implicit cell — the heading drawn over the
692 body on every flow slide (polish audit B4). Equal specificity and
693 later in the file is what wins; the browser drive
694 TestBrowserPhoneStacksFlowRegions holds it. */
695 [slides-slide][data-layout="flow"] > [slides-frame] {
696 grid-template-columns: 1fr;
697 grid-template-areas: none;
698 }
699 [slides-region][data-region] {
700 grid-area: auto;
701 }
702
703 /* image-full stops overlaying: the heading goes under the picture,
704 where a phone-sized scrim would have been unreadable anyway. */
705 [slides-slide][data-template="image-full"] [data-region="heading"] {
706 grid-area: auto;
707 max-inline-size: none;
708 margin: 0;
709 background: none;
710 backdrop-filter: none;
711 }
712 [slides-slide][data-template="image-full"] [data-block="image"] img {
713 block-size: auto;
714 object-fit: contain;
715 }
716
717 [slides-slide][data-template="title"] [slides-region],
718 [slides-slide][data-template="quote"] [slides-region] {
719 max-inline-size: none;
720 }
721
722 /* A columns block is columns no longer. */
723 [slides-block][data-block="columns"] {
724 grid-template-columns: minmax(0, 1fr);
725 }
726
727 /* §2.5, the phone layer. `hidden` and `size` are attributes the
728 selector can read; `order` and `box` are values, because CSS
729 cannot pull a number out of a data attribute — the renderer sends
730 those as custom properties on the same element. */
731 [slides-block][data-phone-hidden] {
732 display: none;
733 }
734 [slides-block] {
735 order: var(--slides-phone-order, 0);
736 }
737 [slides-block][data-phone-size="s"] {
738 font-size: var(--slides-body-s);
739 }
740 [slides-block][data-phone-size="m"] {
741 font-size: var(--slides-body-m);
742 }
743 [slides-block][data-phone-size="l"] {
744 font-size: var(--slides-body-l);
745 }
746 [slides-slide][data-layout="canvas"] > [slides-block][data-phone-box] {
747 left: var(--slides-phone-left);
748 top: var(--slides-phone-top);
749 width: var(--slides-phone-width);
750 height: var(--slides-phone-height);
751 }
752}
753
754/* A flow slide rendered for a phone (§2.3's reflow, and every template
755 at a phone width) is as tall as what is on it. The 16:9 frame is a
756 projector's shape, not a page's, and keeping it here would mean
757 either a scrollbar per slide or words falling off the bottom.
758
759 Two selectors for one rule, because there are two ways to arrive:
760 the renderer was told it was a phone, or a desktop render was opened
761 on one — which is every scriptless viewer, since a server rendering
762 HTML has no honest way to know the screen. */
763[slides-slide][data-vp="phone"]:not([data-layout="canvas"]) {
764 aspect-ratio: auto;
765 overflow: visible;
766}
767
768@media (max-width: 600px) {
769 [slides-slide]:not([data-layout="canvas"]) {
770 aspect-ratio: auto;
771 overflow: visible;
772 }
773}
774