/* ============================================================================
   SLIDE TEMPLATES - the shared vocabulary every deck body is composed from.

   WHY THIS FILE EXISTS. The first session-1 deck was 130 slides each carrying
   its own inline <style> block: 130 private definitions of "a card", "a heading",
   "an eyebrow". Two things followed, and both were measured rather than guessed.
   The deck did not look like one deck. And 80 of the 130 slides overflowed the
   1920x1080 canvas, silently, because `.slide__body` scrolls: on a projector the
   surplus is simply not on the screen and nothing anywhere says so.

   So a body now composes template classes and writes almost no CSS of its own.
   A template that fits, fits on every slide that uses it.

   THE COORDINATE SYSTEM is the one slides.css already establishes: a slide lives
   inside `.cl-stage__canvas`, a fixed 1920x1080 box with `container-type:
   inline-size`, transform-scaled to whatever screen it is on. Every rule here
   sizes itself in `cqw` (1cqw = 19.2px) with a `clamp()` floor, never in `vw`,
   never in a `@media` query. A `vw` rule looks perfect on the venue screen and
   breaks the phone preview, which is why it survives casual review.

   THE PHONE SURFACE is ordinary responsive CSS and shares these class names, so
   every template has a `.slide--phone` branch beside it rather than in a second
   file. slides.css says why: the open-day app split one block's rules across two
   files and its own docs now read "change one, change the other".

   NAMING. Everything here is `tpl-`. `cl-` belongs to the app chrome, `s-` to the
   handful of legacy per-slide blocks, and a parallel deck kit owns its own file.

   SKINS ARE OPTED INTO FROM THE BODY. A body cannot set a class on `.slide`, so
   the skins below select upward with `:has()`. `.slide:has(.tpl--dark)` is
   specificity (0,2,0) and beats `.slide` (0,1,0), which is what lets a body
   repaint its own slide, title included.
   ============================================================================ */

/* ── Tokens the templates share ───────────────────────────────────────────── */

.slide {
  /* One scale knob per slide. A template sets its type in `em` off these, so a
     density class can shrink a whole slide by one number rather than by a
     rewrite. This is the lever the overflow pass pulls. */
  --tpl-scale: 1;
  --tpl-gap: calc(1.6cqw * var(--tpl-scale));
  --tpl-pad: calc(2.2cqw * var(--tpl-scale));
  --tpl-radius: 1.1cqw;
  --tpl-line: var(--border-default);
  --tpl-ink: var(--text-color);
  --tpl-ink-soft: var(--text-secondary);
  --tpl-ink-faint: var(--text-tertiary);
  --tpl-surface: #ffffff;
  --tpl-accent: var(--primary-color);
  /* THE TWO STATE INKS, and their washes. Amber and red were added to the slide
     palette on 2026-09-13 because the four inks above are all "how loud", never
     "what kind": a warning line and an error line had no colour of their own and
     came out as Accent, the same blue as an emphasis.

     They are TOKENS on `.slide`, like every other ink, so `.tpl--dark` can
     re-point them: the site's amber is a dark #A86000 tuned for white, which on
     charcoal is nearly unreadable. The wash pair is separate from the ink pair
     because a fill and a letterform want opposite ends of the same hue - the ink
     has to carry 4.5:1 against the ground, the fill must not.

     BOTH SITES MUST DECLARE THE SITE TOKENS BELOW. class-app/public/css/tokens.css
     had neither --warning-color nor --alert-color until this change, and a slide
     ink resolving to an undefined token paints the inherited colour on the
     projector with nothing anywhere saying so. tests/slideTextFormat.spec.ts pins
     both files against every fallback written in this sheet. */
  --tpl-warning: var(--warning-color);
  --tpl-error: var(--alert-color);
  --tpl-warning-wash: var(--warning-light);
  --tpl-error-wash: var(--alert-light);
  --tpl-brand: linear-gradient(90deg, #0894ff 0%, #bd32d6 50%, #ffa304 100%);
  /* HOW FAR THE TERMINAL CHEVRON REACHES INTO THE CONTENT COLUMN. The one rule
     about that corner is written off this number, and it is derived, not chosen:

       chevron's far edge, from the canvas's inline-start (right) edge
                        = 3cqw inset + 6.528cqw inline-size = 9.528cqw
       the content column's own inline-start edge
                        = `.slide--display`'s 7cqw padding
       overhang         = 9.528cqw - 7cqw = 2.528cqw = 48.54px on the canvas

     ROUNDED UP TO 2.6cqw, and the 1.4px is the whole point of the rounding: at
     the exact overhang a card's border-box edge lands on the same sub-pixel as
     the mark's box edge (1737.06 against 1737.08), and a zero-clearance
     abutment is how this comes back the next time anything moves by a pixel.

     All three inputs live in slides.css (`.slide--display`,
     `.slide__brandmark--term`, `.slide__brandmark .logo-10x-terminal`). Nothing
     computes this for you: move the chevron or resize it there and this number
     is wrong here, silently, and content starts printing over the mark again.

     IN cqw AND NEVER IN A MEDIA QUERY, like everything else in this file. The
     canvas is a 1920px `container-type: inline-size` box on BOTH surfaces, so
     this is 49.92px on the projector, in the presenter's preview and in the
     student area alike, and the clamp only ever bites somewhere none of them
     goes: the floor is its value at a 462px container, near where the chevron's
     own clamp floors take over from its cqw sizes (6.528cqw = 31px at 475px),
     and the ceiling is its value on the 1920px canvas.

     WHAT IT DOES NOT COVER: a card's ambient shadow. `--shadow-md` is a 16px
     blur, so it reaches about 8px past the border box it belongs to and still
     grazes the outermost pixels of the arm's cap on order 27 - measured at 2
     pixels, 7/255 at worst, against 501 before this rule. Buying those back
     would cost every closing footnote in the deck another 8px of indent, and
     shadows spilling over their neighbours is what this deck's cards do
     everywhere else too. */
  --tpl-mark-reserve: clamp(12px, 2.6cqw, 49.92px);
}

/* Three density steps. `--d2` and `--d3` exist so a slide that is one line too
   tall is fixed by adding a class, not by deleting the line the lecturer wanted.
   Nothing below 0.78: past that the back row cannot read it. */
.tpl--d2 { --tpl-scale: 0.88; }
.tpl--d3 { --tpl-scale: 0.78; }

/* Centre on the CROSS axis too. `.tpl` is a flex column, so its
   `justify-content: center` only centres vertically and every child still
   stretches to the full width - which is why a title card's mascot and lockup
   had to carry `align-self: center` by hand. `tpl--figure` and `tpl--quote`
   centre themselves; this is for the rest. */
.tpl--center {
  align-items: center;
  text-align: center;
}

/* ── The template root ────────────────────────────────────────────────────── */

.tpl {
  position: relative;
  block-size: 100%;
  min-block-size: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: var(--tpl-gap);
  font-family: var(--font-body);
  color: var(--tpl-ink);
  /* The body scrolls by default (slides.css). Inside a template it must not:
     a template that needs to scroll is a template that has been overfilled, and
     the overflow harness is what catches it. Hiding here rather than scrolling
     makes the failure visible in a screenshot instead of only in a measurement. */
  overflow: hidden;
}

.slide--display .slide__body:has(.tpl) {
  /* The template owns the vertical rhythm; the body stops being a scroll port. */
  overflow: visible;
  display: flex;
  min-block-size: 0;
  flex: 1 1 auto;
}

.slide--display .slide__body:has(.tpl) > .tpl {
  inline-size: 100%;
}

.slide--phone .tpl {
  block-size: auto;
  gap: var(--space-md);
  overflow: visible;
}

/* ── Type size steps ─────────────────────────────────────────────────────── */
/* The five relative sizes of the formatting vocabulary, `TEXT_SIZE_EM` in
   src/data/slideTextFormat.ts. The rest of that vocabulary sits near the foot
   of this file; these five sit ABOVE EVERY PART ON PURPOSE, and the position is
   the mechanism rather than tidiness.

   A step is written in two places and has to be right in both:

   - AS A MARK on a span inside a line, `font-size` in `em` is the whole job. The
     span is sized off the line it sits in, so it follows the part's own cqw
     size, the density rungs and the phone's px with no branch of its own.
   - ON A PART'S OWN ELEMENT, `em` IS WRONG. `font-size` resolves `em` against
     the PARENT, so `<p class="tpl-point tpl-t--lg">` would come out at 1.25x the
     body's size rather than 1.25x the point's. So a step also sets
     `--tpl-type-step`, and every part's own absolute `font-size` in this file,
     projector and phone branch alike, multiplies it in
     (`* var(--tpl-type-step, 1)`). The part's own rule is later in the file and
     never lighter than (0,1,0), so it wins the `font-size` declaration outright
     and the step reaches the text through the multiplier instead.

   BEING FIRST IS WHAT LETS THE PART WIN AT EQUAL WEIGHT. Move these below the
   parts and the `em` beats every part rule it ties: every block-level step
   would silently re-base its text on the parent, with nothing erroring.

   The property inherits, so a step on a card grows its head, body and number
   together, and a mark inside a stepped block compounds with it the way nested
   type should. A step on a part inside a stepped container REPLACES the outer
   step for that part rather than multiplying it. Parts sized in `em` themselves
   (`.tpl-code`, `.tpl-figure__unit`, `th`, the `<small>` in a step) do not read
   the property: they already follow their parent, and reading it as well would
   apply an enclosing step twice. A NEW PART WITH AN ABSOLUTE SIZE MULTIPLIES IT
   TOO, or block-level steps on it paint nothing - tests/slideTextFormat.spec.ts
   reds on one that does not. */
.tpl-t--xs  { font-size: 0.75em;  --tpl-type-step: 0.75; }
.tpl-t--sm  { font-size: 0.875em; --tpl-type-step: 0.875; }
.tpl-t--lg  { font-size: 1.25em;  --tpl-type-step: 1.25; }
.tpl-t--xl  { font-size: 1.5em;   --tpl-type-step: 1.5; }
.tpl-t--2xl { font-size: 2em;     --tpl-type-step: 2; }

/* ── Shared parts ─────────────────────────────────────────────────────────── */

/* The kicker over a title: which chapter this slide belongs to. Mono, letter
   spaced, quiet. Hebrew is case-less, so no text-transform - it would only ever
   have shouted at the one Latin word in the line. */
.tpl-eyebrow {
  font-family: var(--font-mono);
  font-size: calc(clamp(12px, 1.25cqw, 25px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  letter-spacing: 0.12em;
  line-height: calc(1.3 * var(--tpl-plh, 1));
  color: var(--tpl-ink-faint);
  display: flex;
  align-items: center;
  gap: 0.9cqw;
}

.tpl-eyebrow::before {
  content: '';
  inline-size: calc(2.6cqw * var(--tpl-scale));
  block-size: 2px;
  border-radius: 2px;
  background: var(--tpl-brand);
  flex: none;
}

.slide--phone .tpl-eyebrow { font-size: calc(11px * var(--tpl-type-step, 1)); }

/* A leading eyebrow belongs to the TITLE, not to the body's centred block. In a
   centred flex column it otherwise drifts to the middle of the slide and reads
   as a subtitle floating in space, which is what the smoke test showed. Pin it
   to the top of the body box and reserve its line so centred content cannot
   ride up into it. */
/* `:not(.tpl-free)` for the same reason the mascot rule carries it, and it was
   found the same way - in a browser, not in a spec. This rule is (0,4,0) and the
   free-placement rule at the foot of this file is (0,3,0), so WITHOUT the
   exclusion this one wins outright on a free eyebrow that happens to be the
   first child of its root. What the owner then sees is not a box that ignored
   its `--tpl-fx`: `inset-inline-start: 0` under `direction: rtl` resolves to
   `right: 0`, so the box pins itself to the OPPOSITE corner from the one the
   numbers name, and the panel and the slide disagree with nothing on screen to
   say why. */
.slide--display .tpl > .tpl-eyebrow:first-child:not(.tpl-free) {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  z-index: 1;
}

.slide--display .tpl:has(> .tpl-eyebrow:first-child:not(.tpl-free)) {
  padding-block-start: calc(3.4cqw * var(--tpl-scale));
}

/* The lede: one sentence under the title that carries the claim. Never a
   paragraph - a paragraph on a projector is read instead of listened to. */
.tpl-lede {
  font-size: calc(clamp(16px, 2.05cqw, 41px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  font-weight: 400;
  line-height: calc(1.4 * var(--tpl-plh, 1));
  color: var(--tpl-ink-soft);
  max-inline-size: 62ch;
}

.slide--phone .tpl-lede { font-size: calc(15px * var(--tpl-type-step, 1)); }

/* The closing line: the principle, stated once, in ink rather than grey. */
.tpl-point {
  font-size: calc(clamp(17px, 2.3cqw, 46px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  font-weight: 400;
  line-height: calc(1.35 * var(--tpl-plh, 1));
  color: var(--tpl-ink);
  max-inline-size: 58ch;
}

.slide--phone .tpl-point { font-size: calc(16px * var(--tpl-type-step, 1)); }

.tpl-note {
  font-size: calc(clamp(13px, 1.5cqw, 30px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  line-height: calc(1.4 * var(--tpl-plh, 1));
  color: var(--tpl-ink-faint);
}

.slide--phone .tpl-note { font-size: calc(13px * var(--tpl-type-step, 1)); }

/* ── The closing block yields the chevron's corner ────────────────────────── */
/*
   THE CORNER BELONGS TO THE MARK, AND CONTENT STEPS AROUND IT. One declaration,
   one number, and the only thing it is ever allowed to touch is the LAST block
   of a template - because that is the only block that can reach the bottom of a
   column `.tpl` centres.

   WHAT IT FIXES. An ink measurement of all 147 rendered states of the session-1
   deck - counting pixels where the mark's own strokes come out different with
   the slide's content drawn over them, not element boxes - found the chevron
   struck on fourteen of them and the 10x mark on none. Eleven were one shape: a
   closing `.tpl-note` or `.tpl-point` on 12/2, 18/1, 42/1, 55/1, 87, 91, 94, 95,
   96, 99/1 and 102, whose last line begins at the column's inline-start edge -
   which in this `dir="rtl"` deck is the physical RIGHT, the very edge the
   chevron is pinned to - and printed straight through its upper arm, 9 to 121
   pixels each. Two more were not prose but a surface landing ON the mark
   and cutting it: 27's layer stack (501px, on all 8 of its steps) and 63's
   closing card row (435px). Same cause, same cure: the block that owns the
   bottom of the slide steps back by the overhang, and the mark is drawn whole
   inside the gap. (The fourteenth, 7, had a different cause and is fixed at
   `.tpl-steps--2col` above.)

   WHY NOT MOVE THE MARK. Its corner is an owner decision of 2026-09-06 and the
   only insets that would clear a full-bleed card are 0.4cqw horizontally or
   0.8cqw vertically - rendered, both read as the mark having fallen off the
   slide rather than sitting in a corner. Fading it where a card covers it fixes
   nothing: an opaque card hides whatever is under it at any opacity, so the mark
   stays cut, only more faintly. Content yielding is also what slides.css already
   says should happen ("a collision an author can see is a collision that gets
   fixed").

   WHY NOT VERTICALLY. Lifting the content instead - `padding-block-end` on
   `.tpl` - is a tempting one-liner and wrong twice over. `.tpl` centres on the
   block axis, so 55.8px of bottom padding moves EVERY display slide up by
   27.9px; and it takes 55.8px out of the height budget of exactly the slides
   that collide, which collide BECAUSE they are already full. 27, 63 and 87
   overflow the moment you try it.

   WHY FOUR NAMED SELECTORS AND NOT `.tpl > :last-child`. The blunt version
   reaches bar scales, tables, comparison columns and section subtitles - content
   whose geometry carries meaning - so each closing shape that can actually reach
   the corner is named instead, and each name resolves to exactly the states that
   need it: `.tpl-layers:last-child` is order 27 alone, and a template that
   stacks a terminal transcript over a closing card row is order 63 alone. The
   nine other `.tpl-grid:last-child` slides close 150px higher and are
   deliberately left flush. Order 7, the fourteenth state, is not here at all:
   its collision was a row rule a two-column step list should never have drawn,
   and it is deleted at `.tpl-steps--2col` above rather than dodged.

   `:last-child` CARRIES THE REST OF THE CONDITION. A block with anything after
   it cannot reach the bottom of a column `.tpl` centres, and the 25 bodies that
   close on a `.tpl-mascot--corner` are out of flow and clear the chevron by
   arithmetic (slides.css).

   `.slide--display` SCOPES IT TO WHERE THE MARK ACTUALLY PAINTS. Both marks are
   `display: none` on `.slide--phone` (slides.css), and the phone branch of this
   file is still live markup on the phone REWRITE a slide can carry, so an
   unscoped rule would indent a phone footnote to clear a mark that is not there.

   `:not(.tpl-free)` BECAUSE A PLACED BLOCK IS NOT A CLOSING BLOCK. A free box is
   out of the flow, drawn exactly where its own numbers say, and being the last
   child of its root means nothing about where it sits. Without the exclusion a
   placed point or note that happens to be written last grew 2.6cqw of start
   padding its box never asked for, so its text sat 50px in from the edge the
   properties panel reports - the same panel-and-slide disagreement the eyebrow
   and stepper pins carry the same exclusion for.
*/
/* A CENTRED TEMPLATE TAKES NO RESERVE, and this is not a nicety. These five are
   the `.tpl` roots that set `align-items: center`, so their closing block is
   shrink-wrapped and centred in the column rather than stretched to it: it never
   begins at the inline-start edge, so it cannot reach the corner, and padding
   would not indent it - it would widen a centred box and slide its line
   half the reserve OFF CENTRE. Measured before this rule existed: 25px on orders
   1, 73 and 85, whose closing note is centred text, and on 36, 49, 56 and 69,
   whose is a shrink-wrapped block. Zeroing the token rather than bolting
   `:not()` onto all four selectors keeps the rule below readable, and follows what
   `--d2` already does with `--tpl-scale`. A NEW TEMPLATE THAT CENTRES ITS
   CHILDREN BELONGS ON THIS LIST. */
.tpl--center,
.tpl--section,
.tpl--figure,
.tpl--shot,
.tpl--tree {
  --tpl-mark-reserve: 0px;
}

.slide--display .tpl > .tpl-note:last-child:not(.tpl-free),
.slide--display .tpl > .tpl-point:last-child:not(.tpl-free),
.slide--display .tpl > .tpl-layers:last-child:not(.tpl-free),
.slide--display .tpl:has(> .tpl-term) > .tpl-grid:last-child:not(.tpl-free) {
  padding-inline-start: var(--tpl-mark-reserve);
}

/* The 3-stop brand hairline, in canvas units. The site's `.brand-hairline` is
   96x2 px; this is the same gradient at the deck's scale. */
.tpl-rule {
  inline-size: calc(9cqw * var(--tpl-scale));
  block-size: 2px;
  border: 0;
  border-radius: 2px;
  background: var(--tpl-brand);
  opacity: 0.9;
  flex: none;
}

.tpl-rule--wide { inline-size: calc(16cqw * var(--tpl-scale)); }
.tpl-rule--full { inline-size: 100%; opacity: 0.55; }

.slide--phone .tpl-rule { inline-size: 72px; }

/* A pill. Counts, labels, "step 4 of 13". */
.tpl-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.5cqw;
  padding: calc(0.35cqw * var(--tpl-scale)) calc(1.1cqw * var(--tpl-scale));
  border-radius: var(--radius-full);
  border: 1px solid var(--primary-border);
  background: var(--primary-light);
  color: var(--tpl-accent);
  font-family: var(--font-mono);
  font-size: calc(clamp(12px, 1.3cqw, 26px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  line-height: calc(1.2 * var(--tpl-plh, 1));
}

/* A badge is a pill, and a pill that stretches is a bar. In a flex column the
   default `align-items: stretch` makes it full width, which is what the smoke
   test drew. */
.tpl-badge { align-self: flex-start; }

.tpl-badge--quiet {
  border-color: var(--tpl-line);
  background: transparent;
  color: var(--tpl-ink-faint);
}

.slide--phone .tpl-badge { font-size: calc(12px * var(--tpl-type-step, 1)); padding: 3px 10px; }

/* Inline code and keys. `unicode-bidi: isolate` is not decoration: a Latin
   identifier inside a Hebrew sentence is reordered without it, and a mis-ordered
   identifier is a line of code nobody in the room can copy. */
.tpl-code {
  font-family: var(--font-mono);
  direction: ltr;
  unicode-bidi: isolate;
  font-size: 0.92em;
  padding: 0.06em 0.34em;
  border-radius: 0.35em;
  background: var(--bg-secondary);
  color: var(--tpl-ink);
}

.tpl-kbd {
  font-family: var(--font-mono);
  direction: ltr;
  unicode-bidi: isolate;
  font-size: 0.9em;
  padding: 0.1em 0.45em;
  border-radius: 0.3em;
  border: 1px solid var(--tpl-line);
  border-block-end-width: 2px;
  background: var(--tpl-surface);
}

/* A definition row: the term on the start side, what it means beside it. The
   chapters on Claude Code and on Git are largely vocabulary, and `tpl--table`
   is a heavy instrument for two columns of short text - it draws rules, reserves
   a header row and asks the author for `<thead>`. This is a grid of pairs.

   The children alternate: k, v, k, v. There is no row element on purpose, so a
   pair cannot drift out of alignment with the pair above it. */
.tpl-kv {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: baseline;
  column-gap: calc(1.4cqw * var(--tpl-scale));
  row-gap: calc(0.6cqw * var(--tpl-scale));
  min-inline-size: 0;
}

/* Four tracks, so twice as many pairs fit the same height. Still k, v, k, v in
   source order; the grid wraps them two pairs to a row. */
.tpl-kv--2col {
  grid-template-columns: auto minmax(0, 1fr) auto minmax(0, 1fr);
  column-gap: calc(1.2cqw * var(--tpl-scale));
}

.tpl-kv__k {
  font-family: var(--font-mono);
  direction: ltr;
  unicode-bidi: isolate;
  font-size: calc(clamp(13px, 1.6cqw, 32px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  line-height: calc(1.35 * var(--tpl-plh, 1));
  color: var(--tpl-ink);
  white-space: nowrap;
}

.tpl-kv__v {
  font-size: calc(clamp(13px, 1.55cqw, 31px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  line-height: calc(1.4 * var(--tpl-plh, 1));
  color: var(--tpl-ink-soft);
  min-inline-size: 0;
}

.slide--phone .tpl-kv,
.slide--phone .tpl-kv--2col {
  grid-template-columns: minmax(0, 1fr);
  row-gap: 2px;
}

.slide--phone .tpl-kv__k { font-size: calc(14px * var(--tpl-type-step, 1)); white-space: normal; }
.slide--phone .tpl-kv__k:not(:first-child) { margin-block-start: 10px; }
.slide--phone .tpl-kv__v { font-size: calc(14px * var(--tpl-type-step, 1)); }

/* The closing slide's heart. Carried over from the open-day deck literally,
   colour included, because the two closers are meant to look like the same
   gesture. Not sized off `--tpl-scale`: it is one glyph on a slide with almost
   nothing else on it, so there is nothing for a density class to rescue. */
.tpl-heart {
  text-align: center;
  font-size: calc(clamp(22px, 2.6cqw, 52px) * var(--tpl-type-step, 1));
  line-height: calc(1 * var(--tpl-plh, 1));
  color: #e0245e;
}

.slide--phone .tpl-heart { font-size: calc(22px * var(--tpl-type-step, 1)); }

/* ── Glass card ───────────────────────────────────────────────────────────── */
/* The main site's fold card, in canvas units: a translucent white pane over the
   slide's own background, capped by the brand hairline. The cap is the site's
   one sanctioned decorative gradient and it never sits behind text. */

.tpl-card {
  position: relative;
  padding: var(--tpl-pad);
  border-radius: var(--tpl-radius);
  border: 1px solid var(--border-light);
  background: rgba(255, 255, 255, 0.82);
  backdrop-filter: blur(16px) saturate(160%);
  -webkit-backdrop-filter: blur(16px) saturate(160%);
  box-shadow: var(--shadow-md);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: calc(0.7cqw * var(--tpl-scale));
  min-inline-size: 0;
}

.tpl-card--capped::before {
  content: '';
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  block-size: 2px;
  background: var(--tpl-brand);
}

.tpl-card--flat {
  background: var(--tpl-surface);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  box-shadow: var(--shadow-sm);
}

.tpl-card--tint {
  background: var(--primary-light);
  border-color: var(--primary-border);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  box-shadow: none;
}

.tpl-card__head {
  font-size: calc(clamp(15px, 1.85cqw, 37px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  font-weight: 500;
  line-height: calc(1.25 * var(--tpl-plh, 1));
  color: var(--tpl-ink);
}

.tpl-card__body {
  font-size: calc(clamp(13px, 1.55cqw, 31px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  font-weight: 400;
  line-height: calc(1.45 * var(--tpl-plh, 1));
  color: var(--tpl-ink-soft);
}

.tpl-card__num {
  font-family: var(--font-display);
  font-size: calc(clamp(24px, 3.6cqw, 72px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  font-weight: 300;
  line-height: calc(1 * var(--tpl-plh, 1));
  letter-spacing: -0.02em;
  color: var(--tpl-accent);
}

.slide--phone .tpl-card { padding: var(--space-md); }
.slide--phone .tpl-card__head { font-size: calc(15px * var(--tpl-type-step, 1)); }
.slide--phone .tpl-card__body { font-size: calc(14px * var(--tpl-type-step, 1)); }
.slide--phone .tpl-card__num { font-size: calc(26px * var(--tpl-type-step, 1)); }

/* ── T1  lead: one claim, optionally built ───────────────────────────────── */

.tpl--lead {
  justify-content: center;
  gap: calc(2cqw * var(--tpl-scale));
}

.tpl--lead .tpl-point {
  font-size: calc(clamp(20px, 3cqw, 60px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  font-weight: 400;
  line-height: calc(1.28 * var(--tpl-plh, 1));
}

.slide--phone .tpl--lead .tpl-point { font-size: calc(19px * var(--tpl-type-step, 1)); }

/* The LAST slide of a deck. Centred, and quiet. Centring is `tpl--center` on the
   body; everything else has to be here, because `.slide__title` is the h1
   OUTSIDE the template and a slide body may not ship a `<style>` block to reach
   it. The selector is (0,2,0) and beats the base `.slide__title` (0,1,0) on all
   four properties.

   Owner decision, 2026-09-06, replacing the blue-and-500 of two days earlier:
   the closing sentence is a courtesy, not the message. The BRAND LOCKUP below it
   is what the room should be left looking at, so the words above it give way -
   near-black instead of the site's blue, weight 200 instead of 500, two thirds
   of the size, and the display face's negative tracking traded for a little air.
   Noto Sans Hebrew is a 100-900 variable face, so 200 is a real drawn weight
   here rather than a synthesised one; at this size 300 still reads as a heading
   and 200 reads as a sign-off, which is the whole point of the change.

   The brand lockup keeps its own colours on purpose: it is an SVG painted with
   `url(#...)` gradient fills, which `color` cannot reach. */
.slide:has(.tpl--closing) .slide__title {
  color: var(--tpl-ink);
  font-weight: 200;
  font-size: clamp(20px, 2.8cqw, 56px);
  letter-spacing: 0.01em;
}

.tpl--closing .tpl-eyebrow,
.tpl--closing .tpl-point { color: var(--tpl-accent); }

/* ── T2  section: the card that opens a module, chapter or subject ───────── */
/* Zero minutes on the clock. It exists so the room can tell a new part of the
   day has started, which the run of show otherwise only tells the lecturer. */

/* CENTRED, like every other slide title in the deck.
   `slides.css` centres `.slide__title`, and this template hides that h1 and
   prints its own title instead - so leaving the card at `flex-start` would make
   the module, chapter and subject openers the only left-ragged headings in a
   deck of centred ones, which is the inconsistency the centring was asked for.
   The kicker, the sub and the badge row centre with it: a title slide whose
   heading is centred over left-aligned metadata reads as a mistake. */
.tpl--section {
  justify-content: center;
  align-items: center;
  gap: calc(1.4cqw * var(--tpl-scale));
  text-align: center;
}

.tpl--section .tpl-section__kicker {
  font-family: var(--font-mono);
  font-size: calc(clamp(13px, 1.5cqw, 30px) * var(--tpl-type-step, 1));
  letter-spacing: 0.16em;
  color: var(--tpl-accent);
}

.tpl--section .tpl-section__title {
  font-family: var(--font-display);
  font-size: calc(clamp(30px, 5.4cqw, 108px) * var(--tpl-type-step, 1));
  font-weight: 300;
  line-height: calc(1.08 * var(--tpl-plh, 1));
  letter-spacing: -0.03em;
  color: var(--tpl-ink);
  max-inline-size: 22ch;
}

.tpl--section .tpl-section__sub {
  font-size: calc(clamp(16px, 2.2cqw, 44px) * var(--tpl-type-step, 1));
  font-weight: 400;
  line-height: calc(1.4 * var(--tpl-plh, 1));
  color: var(--tpl-ink-soft);
  max-inline-size: 46ch;
}

.tpl--section .tpl-section__meta {
  display: flex;
  gap: 1.2cqw;
  flex-wrap: wrap;
  margin-block-start: 0.6cqw;
}

/* A section card hides the slide's own H1: the card IS the title, at its own
   scale, and the two together read as the same words printed twice. */
.slide--display:has(.tpl--section) .slide__title { display: none; }
.slide--phone:has(.tpl--section) .slide__title { display: none; }

.slide--phone .tpl--section .tpl-section__title { font-size: calc(26px * var(--tpl-type-step, 1)); }
.slide--phone .tpl--section .tpl-section__sub { font-size: calc(15px * var(--tpl-type-step, 1)); }

/* ── T3  cards: two to four panes across ─────────────────────────────────── */

.tpl--cards .tpl-grid {
  display: grid;
  grid-template-columns: repeat(var(--tpl-cols, 3), minmax(0, 1fr));
  gap: var(--tpl-gap);
  align-items: stretch;
  min-block-size: 0;
}

.tpl-grid--2 { --tpl-cols: 2; }
.tpl-grid--3 { --tpl-cols: 3; }
.tpl-grid--4 { --tpl-cols: 4; }

.slide--phone .tpl--cards .tpl-grid {
  grid-template-columns: 1fr;
  gap: var(--space-sm);
}

/* ── T4  split: text beside an artefact ──────────────────────────────────── */

.tpl--split .tpl-split {
  display: grid;
  grid-template-columns: var(--tpl-split, 1fr 1fr);
  gap: calc(2.4cqw * var(--tpl-scale));
  align-items: center;
  min-block-size: 0;
}

.tpl-split--wide-start { --tpl-split: 1.35fr 1fr; }
.tpl-split--wide-end { --tpl-split: 1fr 1.35fr; }

.tpl-split__col {
  display: flex;
  flex-direction: column;
  gap: calc(0.9cqw * var(--tpl-scale));
  min-inline-size: 0;
}

.slide--phone .tpl--split .tpl-split {
  grid-template-columns: 1fr;
  gap: var(--space-md);
}

/* ── T5  figure: one number, one caption ─────────────────────────────────── */

.tpl--figure {
  align-items: center;
  text-align: center;
  justify-content: center;
}

.tpl-figure__num {
  font-family: var(--font-display);
  /* 500+ weight is forbidden on the brand gradient, numerals included, so this
     is a solid colour at a light weight rather than a gradient fill. */
  font-size: calc(clamp(56px, 12cqw, 240px) * var(--tpl-type-step, 1));
  font-weight: 300;
  line-height: calc(0.95 * var(--tpl-plh, 1));
  letter-spacing: -0.04em;
  color: var(--tpl-accent);
  direction: ltr;
}

/* Its own tracking, overriding the display number's -0.04em. That negative value
   is right for a big Latin numeral and wrong for the Hebrew word glued to it:
   Hebrew letterforms have no side bearings to spare, and on order 73 the two
   vavs of the unit word fused into a single stroke - the word read as a
   different word. Tuned on that slide at 0.06 / 0.08 / 0.10em: 0.06 separates
   them, 0.10 reads best but widens the word enough to start drifting off the
   number, 0.08 is both. Positive, and only just. */
.tpl-figure__unit {
  font-size: 0.34em;
  font-weight: 400;
  letter-spacing: 0.08em;
  color: var(--tpl-ink-soft);
  padding-inline-start: 0.2em;
}

.tpl-figure__cap {
  font-size: calc(clamp(16px, 2.3cqw, 46px) * var(--tpl-type-step, 1));
  font-weight: 400;
  line-height: calc(1.35 * var(--tpl-plh, 1));
  color: var(--tpl-ink);
  max-inline-size: 44ch;
}

.slide--phone .tpl-figure__num { font-size: calc(54px * var(--tpl-type-step, 1)); }
.slide--phone .tpl-figure__cap { font-size: calc(16px * var(--tpl-type-step, 1)); }

/* ── T6  anatomy: a string taken apart, every part labelled ──────────────── */
/* The template the owner asked for by name: print the URL and map protocol,
   host, domain, TLD, port, path, query. It is also the right shape for a
   command line, a file path and an HTTP request line, so it is generic. */

.tpl--anatomy {
  justify-content: center;
  gap: calc(2cqw * var(--tpl-scale));
}

.tpl-anat {
  direction: ltr;
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: center;
  gap: 0;
  font-family: var(--font-mono);
}

.tpl-anat__part {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: calc(0.55cqw * var(--tpl-scale));
  padding-block-end: 0.2cqw;
}

/* The same field written in the base the machine actually stores, printed ABOVE
   the decimal INSIDE the part rather than as a second row of its own: the part is
   already a flex column, so the two writings of one byte share a column and stay
   registered no matter how wide either gets. A separator part carries a blank one
   (`&nbsp;`) for the same reason - `.tpl-anat` aligns to `flex-start`, so a part
   one line shorter floats its dot up out of the row. */
.tpl-anat__bin {
  font-size: calc(clamp(9px, 1.15cqw, 23px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  line-height: calc(1.25 * var(--tpl-plh, 1));
  letter-spacing: 0.05em;
  color: var(--tpl-ink-faint);
  white-space: pre;
}

.tpl-anat__part.is-on .tpl-anat__bin { color: var(--tpl-accent); }

.tpl-anat__txt {
  font-size: calc(clamp(18px, 3.1cqw, 62px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  line-height: calc(1.15 * var(--tpl-plh, 1));
  color: var(--tpl-ink-faint);
  padding: 0.06em 0.1em;
  border-block-end: 3px solid transparent;
  white-space: pre;
}

/* A lit part. `--on` is the one the slide is talking about at this build step;
   everything else stays grey so the eye has one place to go. */
.tpl-anat__part.is-on .tpl-anat__txt {
  color: var(--tpl-accent);
  border-block-end-color: var(--tpl-accent);
  background: var(--primary-light);
  border-start-start-radius: 0.2em;
  border-start-end-radius: 0.2em;
}

.tpl-anat__lbl {
  font-family: var(--font-body);
  direction: rtl;
  font-size: calc(clamp(11px, 1.25cqw, 25px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  line-height: calc(1.2 * var(--tpl-plh, 1));
  color: var(--tpl-ink-faint);
  text-align: center;
  max-inline-size: 14ch;
  opacity: 0;
}

.tpl-anat__part.is-on .tpl-anat__lbl {
  color: var(--tpl-accent);
  opacity: 1;
}

/* Every label showing at once, for the summary step. */
.tpl-anat--all .tpl-anat__lbl { opacity: 1; }

.tpl-anat__legend {
  display: grid;
  grid-template-columns: repeat(var(--tpl-cols, 4), minmax(0, 1fr));
  gap: calc(0.9cqw * var(--tpl-scale)) calc(1.6cqw * var(--tpl-scale));
}

.tpl-anat__legend--2 { --tpl-cols: 2; }
.tpl-anat__legend--3 { --tpl-cols: 3; }
.tpl-anat__legend--4 { --tpl-cols: 4; }
.tpl-anat__legend--5 { --tpl-cols: 5; }

.tpl-anat__row {
  display: flex;
  align-items: baseline;
  gap: 0.7cqw;
  font-size: calc(clamp(12px, 1.45cqw, 29px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  line-height: calc(1.35 * var(--tpl-plh, 1));
  color: var(--tpl-ink-soft);
  min-inline-size: 0;
}

.tpl-anat__row b {
  font-family: var(--font-mono);
  direction: ltr;
  unicode-bidi: isolate;
  font-weight: 400;
  color: var(--tpl-ink);
  flex: none;
}

.slide--phone .tpl-anat__bin { font-size: calc(8px * var(--tpl-type-step, 1)); letter-spacing: 0; }
.slide--phone .tpl-anat__txt { font-size: calc(15px * var(--tpl-type-step, 1)); }
.slide--phone .tpl-anat__lbl { font-size: calc(11px * var(--tpl-type-step, 1)); }
.slide--phone .tpl-anat__legend { grid-template-columns: 1fr; }

/* ── T7  steps: a numbered sequence ──────────────────────────────────────── */

.tpl--steps .tpl-steps {
  list-style: none;
  display: grid;
  gap: calc(0.9cqw * var(--tpl-scale));
  padding: 0;
  margin: 0;
  min-block-size: 0;
}

.tpl-steps--2col { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: var(--tpl-gap); }

.tpl-step {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: baseline;
  gap: calc(1.1cqw * var(--tpl-scale));
  padding-block: calc(0.55cqw * var(--tpl-scale));
  border-block-end: 1px solid var(--border-light);
  min-inline-size: 0;
}

.tpl-step:last-child { border-block-end: 0; }

/* A TWO-COLUMN LIST HAS TWO BOTTOM ROWS, and `:last-child` only reaches one of
   them: on order 7 the right-hand column ended on a rule and the left-hand one
   did not, an asymmetry visible from the back of the room. `-n+2` is the last
   cell of each column whether the list is odd or even - with ten steps it is 9
   and 10, with nine it is 8 and 9, and in both cases that is exactly the pair
   that sits at the foot of a column.

   It is also the whole of that slide's collision with the bottom-right
   brandmark: the stray rule ran 13px across the chevron's upper arm at y 948,
   one pixel tall. Deleting a rule that should not have been drawn is the honest
   fix; reserving the mark's corner from a ten-item course outline, the way the
   prose blocks do below, would have inset the entire grid by 50px and left the
   two columns visibly unbalanced. */
.tpl-steps--2col > .tpl-step:nth-last-child(-n + 2) { border-block-end: 0; }

.tpl-step__n {
  font-family: var(--font-mono);
  font-size: calc(clamp(13px, 1.6cqw, 32px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  color: var(--tpl-accent);
  min-inline-size: 2.2ch;
  direction: ltr;
}

.tpl-step__t {
  font-size: calc(clamp(14px, 1.75cqw, 35px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  line-height: calc(1.35 * var(--tpl-plh, 1));
  color: var(--tpl-ink);
  min-inline-size: 0;
}

.tpl-step__t small {
  display: block;
  font-size: 0.82em;
  color: var(--tpl-ink-faint);
  line-height: calc(1.3 * var(--tpl-plh, 1));
}

.slide--phone .tpl-steps--2col { grid-template-columns: 1fr; }
.slide--phone .tpl-step__t { font-size: calc(14px * var(--tpl-type-step, 1)); }

/* ── T8  terminal: a command and what it printed ─────────────────────────── */

.tpl-term {
  direction: ltr;
  border-radius: var(--tpl-radius);
  border: 1px solid rgba(255, 255, 255, 0.09);
  background: #14151d;
  overflow: hidden;
  box-shadow: var(--shadow-lg);
  min-inline-size: 0;
}

.tpl-term__bar {
  display: flex;
  align-items: center;
  gap: 0.6cqw;
  padding: calc(0.7cqw * var(--tpl-scale)) calc(1.1cqw * var(--tpl-scale));
  background: rgba(255, 255, 255, 0.04);
  border-block-end: 1px solid rgba(255, 255, 255, 0.07);
}

.tpl-term__dot {
  inline-size: 0.75cqw;
  block-size: 0.75cqw;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.18);
}

.tpl-term__name {
  font-family: var(--font-mono);
  font-size: calc(clamp(11px, 1.2cqw, 24px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  color: rgba(255, 255, 255, 0.45);
  padding-inline-start: 0.6cqw;
}

.tpl-term__body {
  padding: calc(1.2cqw * var(--tpl-scale));
  font-family: var(--font-mono);
  font-size: calc(clamp(13px, 1.6cqw, 32px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  line-height: calc(1.55 * var(--tpl-plh, 1));
  color: rgba(255, 255, 255, 0.82);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* The prompt sign and the command the person typed, told apart from the output
   they did not. A room that cannot tell those apart cannot copy either. */
.tpl-term__cmd { color: #ffffff; }
.tpl-term__cmd::before { content: '$ '; color: var(--ccdd-gold, #d4a574); }
.tpl-term__out { color: rgba(255, 255, 255, 0.55); }
.tpl-term__ok { color: #7ddba3; }
.tpl-term__err { color: #ff9d9d; }
.tpl-term__dim { color: rgba(255, 255, 255, 0.34); }

.slide--phone .tpl-term__body { font-size: calc(12px * var(--tpl-type-step, 1)); padding: 12px; }
.slide--phone .tpl-term__dot { inline-size: 8px; block-size: 8px; }

/* ── T9  compare: two columns, headed ────────────────────────────────────── */

.tpl--compare .tpl-cmp {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--tpl-gap);
  min-block-size: 0;
}

.tpl-cmp__col {
  display: flex;
  flex-direction: column;
  gap: calc(0.7cqw * var(--tpl-scale));
  padding: var(--tpl-pad);
  border-radius: var(--tpl-radius);
  border: 1px solid var(--tpl-line);
  background: var(--tpl-surface);
  min-inline-size: 0;
}

.tpl-cmp__col--on {
  border-color: var(--primary-border);
  background: var(--primary-light);
}

.tpl-cmp__head {
  font-size: calc(clamp(15px, 1.95cqw, 39px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  font-weight: 500;
  line-height: calc(1.2 * var(--tpl-plh, 1));
  color: var(--tpl-ink);
  display: flex;
  align-items: baseline;
  gap: 0.6cqw;
}

.tpl-cmp__list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: calc(0.45cqw * var(--tpl-scale));
  font-size: calc(clamp(13px, 1.55cqw, 31px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  line-height: calc(1.4 * var(--tpl-plh, 1));
  color: var(--tpl-ink-soft);
}

.tpl-cmp__list li {
  /* RTL list indentation is padding-RIGHT, per the repo convention. */
  padding-inline-start: 1.1em;
  position: relative;
}

.tpl-cmp__list li::before {
  content: '';
  position: absolute;
  inset-inline-start: 0.35em;
  inset-block-start: 0.62em;
  inline-size: 0.32em;
  block-size: 0.32em;
  border-radius: 50%;
  background: var(--tpl-ink-faint);
}

.slide--phone .tpl--compare .tpl-cmp { grid-template-columns: 1fr; }

/* ── T10  table: a compact matrix ────────────────────────────────────────── */

.tpl-table {
  inline-size: 100%;
  border-collapse: collapse;
  font-size: calc(clamp(13px, 1.55cqw, 31px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  line-height: calc(1.35 * var(--tpl-plh, 1));
}

.tpl-table th,
.tpl-table td {
  padding: calc(0.7cqw * var(--tpl-scale)) calc(1cqw * var(--tpl-scale));
  text-align: start;
  border-block-end: 1px solid var(--border-light);
  vertical-align: top;
}

.tpl-table th {
  font-weight: 500;
  color: var(--tpl-ink-faint);
  font-size: 0.86em;
  border-block-end-color: var(--tpl-line);
}

.tpl-table td { color: var(--tpl-ink-soft); }
.tpl-table td:first-child { color: var(--tpl-ink); }
.tpl-table tr:last-child td { border-block-end: 0; }

.slide--phone .tpl-table { font-size: calc(13px * var(--tpl-type-step, 1)); }

/* ── T11  quote: a line somebody said, with their face ───────────────────── */

.tpl--quote { justify-content: center; }

.tpl-quote {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: calc(2.2cqw * var(--tpl-scale));
}

.tpl-quote__portrait {
  inline-size: clamp(56px, 9cqw, 180px);
  block-size: clamp(56px, 9cqw, 180px);
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid var(--tpl-line);
  background: var(--bg-secondary);
  flex: none;
}

.tpl-quote__text {
  font-size: calc(clamp(18px, 2.8cqw, 56px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  font-weight: 300;
  line-height: calc(1.3 * var(--tpl-plh, 1));
  color: var(--tpl-ink);
  max-inline-size: 40ch;
}

.tpl-quote__src {
  font-size: calc(clamp(12px, 1.4cqw, 28px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  color: var(--tpl-ink-faint);
  margin-block-start: 0.7cqw;
}

.tpl-quote__src span[dir='ltr'] { unicode-bidi: isolate; }

.slide--phone .tpl-quote { grid-template-columns: 1fr; gap: var(--space-sm); }
.slide--phone .tpl-quote__text { font-size: calc(17px * var(--tpl-type-step, 1)); }

/* ── T12  timeline: dated stops along one line ───────────────────────────── */
/* A history, drawn: operating systems, the browser, a commit log.

   THE EARLIEST STOP IS ON THE RIGHT. The strip inherits the deck's RTL and a
   grid with `grid-auto-flow: column` fills from the start side, which in RTL is
   the right edge, so source order IS chronological order. Do not add
   `direction: ltr` here to make the gradient run the other way: it would put
   the first year on the left of a Hebrew slide, and the room reads the slide
   before it reads the line. */

.tpl--timeline { justify-content: center; }

.tpl-tl {
  /* The dot diameter, named once: the track aligns to its centre. */
  --tpl-tl-dot: calc(1.5cqw * var(--tpl-scale));
  position: relative;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  align-items: start;
  gap: calc(1.2cqw * var(--tpl-scale));
  padding-block: calc(0.4cqw * var(--tpl-scale));
  min-block-size: 0;
}

/* The rule the stops sit on. Absolutely positioned, so a stop still hidden by a
   build step leaves the line whole instead of punching a hole in it. The offset
   is the block padding plus half a dot; it is written out rather than derived
   because a `var()` holding a `calc()` divided inside another `calc()` is the
   kind of expression that fails silently in one engine and not another. */
.tpl-tl__track {
  position: absolute;
  inset-inline: 0;
  inset-block-start: calc(1.15cqw * var(--tpl-scale) - 1px);
  block-size: 2px;
  border-radius: 2px;
  background: linear-gradient(90deg, #0894ff 0%, #bd32d6 50%, #ffa304 100%);
  opacity: 0.9;
}

.tpl-tl__stop {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: calc(0.5cqw * var(--tpl-scale));
  min-inline-size: 0;
}

/* Filled with the slide's own ground, which is what hides the track behind it.
   The dark skin overrides that fill; nothing else needs to. */
.tpl-tl__dot {
  inline-size: var(--tpl-tl-dot);
  block-size: var(--tpl-tl-dot);
  border-radius: 50%;
  border: 2px solid var(--tpl-accent);
  background: var(--tpl-surface);
  flex: none;
}

.tpl-tl__stop.is-on .tpl-tl__dot { background: var(--tpl-accent); }

.tpl-tl__year {
  font-family: var(--font-mono);
  direction: ltr;
  unicode-bidi: isolate;
  font-size: calc(clamp(13px, 1.55cqw, 31px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  line-height: calc(1.2 * var(--tpl-plh, 1));
  color: var(--tpl-accent);
}

.tpl-tl__t {
  font-size: calc(clamp(12px, 1.45cqw, 29px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  line-height: calc(1.35 * var(--tpl-plh, 1));
  color: var(--tpl-ink-soft);
  max-inline-size: 16ch;
}

.tpl-tl__stop.is-on .tpl-tl__t { color: var(--tpl-ink); }

/* A 9:16 card has no room for six columns, and a horizontal line down a phone
   is a line nobody follows. It becomes a list, oldest first, still in source
   order. */
.slide--phone .tpl-tl { grid-auto-flow: row; gap: var(--space-sm); }
.slide--phone .tpl-tl__track { display: none; }
.slide--phone .tpl-tl__dot { display: none; }
.slide--phone .tpl-tl__stop { flex-direction: row; align-items: baseline; text-align: start; gap: 8px; }
.slide--phone .tpl-tl__year { font-size: calc(13px * var(--tpl-type-step, 1)); flex: none; min-inline-size: 5ch; }
.slide--phone .tpl-tl__t { font-size: calc(14px * var(--tpl-type-step, 1)); max-inline-size: none; }

/* ── T13  layers: the abstraction tower, bottom floor first ──────────────── */
/* `column-reverse` so the FIRST child in the source is the BOTTOM floor. A
   tower authored top-down reads backwards to whoever edits it next, and the
   build steps that light one floor at a time have to run in source order:
   transistors, then the language, then the application, upward. */

.tpl--layers { justify-content: center; }

.tpl-layers {
  display: flex;
  flex-direction: column-reverse;
  gap: calc(0.5cqw * var(--tpl-scale));
  min-block-size: 0;
}

/* The glass slab of `.tpl-card`, laid out as one row instead of a pane. The
   padding is tuned to a MEASURED ceiling, not a guessed one: the body box under
   a one-line title is 718px, an eyebrow reserves 65px of it, and eight floors
   have to fit what is left. They do, with about 20px to spare. A ninth floor
   does not; that is what `--d2` is for. */
.tpl-layer {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: baseline;
  gap: calc(1.1cqw * var(--tpl-scale));
  padding: calc(0.68cqw * var(--tpl-scale)) calc(1.5cqw * var(--tpl-scale));
  border-radius: var(--tpl-radius);
  border: 1px solid var(--border-light);
  background: rgba(255, 255, 255, 0.82);
  backdrop-filter: blur(16px) saturate(160%);
  -webkit-backdrop-filter: blur(16px) saturate(160%);
  box-shadow: var(--shadow-md);
  min-inline-size: 0;
  transition: border-color 0.25s ease, box-shadow 0.25s ease, background 0.25s ease;
}

/* The floor the lecturer is standing on. Readable from the back row is the
   whole point, so the border is the accent itself rather than the 18%-alpha
   hairline the cards use, and the shadow lifts a step. */
.tpl-layer.is-on {
  border-color: var(--tpl-accent);
  background: rgba(255, 255, 255, 0.94);
  box-shadow: var(--shadow-lg);
}

.tpl-layer__n {
  font-family: var(--font-mono);
  direction: ltr;
  font-size: calc(clamp(12px, 1.4cqw, 28px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  line-height: calc(1.3 * var(--tpl-plh, 1));
  color: var(--tpl-accent);
  min-inline-size: 2.2ch;
}

.tpl-layer__t {
  font-size: calc(clamp(14px, 1.7cqw, 34px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  line-height: calc(1.25 * var(--tpl-plh, 1));
  color: var(--tpl-ink);
  min-inline-size: 0;
}

/* The concrete instance of the floor, quiet, on the end side. Usually a Latin
   name, so it is isolated. */
.tpl-layer__ex {
  font-family: var(--font-mono);
  direction: ltr;
  unicode-bidi: isolate;
  font-size: calc(clamp(11px, 1.3cqw, 26px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  line-height: calc(1.3 * var(--tpl-plh, 1));
  color: var(--tpl-ink-faint);
  justify-self: end;
  text-align: end;
}

.slide--phone .tpl-layers { gap: 6px; }
.slide--phone .tpl-layer { padding: 10px 12px; gap: 10px; border-radius: var(--radius-md); }
.slide--phone .tpl-layer__n { font-size: calc(12px * var(--tpl-type-step, 1)); }
.slide--phone .tpl-layer__t { font-size: calc(14px * var(--tpl-type-step, 1)); }
.slide--phone .tpl-layer__ex { font-size: calc(11px * var(--tpl-type-step, 1)); }

/* ── T14  logos: official marks in a row, each captioned ─────────────────── */
/* THE APP SCALES NOTHING. An `<img>` with no block-size renders at its intrinsic
   pixels, which for a mark downloaded from a vendor is whatever that vendor
   shipped: one press-kit SVG is 512px tall and the next is 24. So the height is
   set here and the width follows it. */

.tpl--logos { justify-content: center; }

.tpl-logos {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(calc(13cqw * var(--tpl-scale)), 1fr));
  gap: calc(2.2cqw * var(--tpl-scale)) calc(2.4cqw * var(--tpl-scale));
  align-items: end;
  justify-items: center;
  min-block-size: 0;
}

.tpl-logo {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: calc(0.7cqw * var(--tpl-scale));
  min-inline-size: 0;
}

.tpl-logo__img {
  display: block;
  block-size: calc(7cqw * var(--tpl-scale));
  inline-size: auto;
  max-inline-size: 100%;
  object-fit: contain;
}

.tpl-logo--sm .tpl-logo__img { block-size: calc(5cqw * var(--tpl-scale)); }
.tpl-logo--lg .tpl-logo__img { block-size: calc(9.5cqw * var(--tpl-scale)); }

.tpl-logo__name {
  font-size: calc(clamp(14px, 1.8cqw, 36px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  font-weight: 500;
  line-height: calc(1.2 * var(--tpl-plh, 1));
  color: var(--tpl-ink);
}

.tpl-logo__role {
  font-size: calc(clamp(12px, 1.35cqw, 27px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  line-height: calc(1.35 * var(--tpl-plh, 1));
  color: var(--tpl-ink-faint);
  max-inline-size: 18ch;
}

.slide--phone .tpl-logos { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-md); }
.slide--phone .tpl-logo { gap: 6px; }
.slide--phone .tpl-logo__img { block-size: 40px; }
.slide--phone .tpl-logo__name { font-size: calc(14px * var(--tpl-type-step, 1)); }
.slide--phone .tpl-logo__role { font-size: calc(12px * var(--tpl-type-step, 1)); }

/* ── T15  shot: a framed screenshot, optionally pinned ───────────────────── */

.tpl--shot {
  justify-content: center;
  align-items: center;
}

.tpl-shot {
  position: relative;
  /* The pin coordinates below are the SCREENSHOT's own, and a screenshot of a
     tool is laid out left to right whatever the slide around it does. The
     caption is a sibling, so it keeps the slide's RTL. */
  direction: ltr;
  /* MEASURED, not guessed. The body box under a one-line title is 718px tall
     (1080 canvas, less 2 x 6cqw slide padding, less a 93px title, less the
     slide's 2cqw gap). Take the caption and the template's gap off that and a
     frame has about 649px. A 16:9 capture 649px tall is 1154px wide, so 56cqw
     (1075px) is the widest frame that still leaves headroom for an eyebrow or a
     second caption line. Widen it and the slide fits the canvas while the
     harness reports the template overflowing itself, which is the failure mode
     nobody sees on a projector. 52cqw (998px, so 561px tall at 16:9) is that
     ceiling with an eyebrow and a one-line caption still on the slide: 50cqw
     is 960px, so 540px tall at 16:9, and 44px are left over. */
  max-inline-size: calc(50cqw * var(--tpl-scale));
  border-radius: var(--tpl-radius);
  border: 1px solid var(--border-light);
  box-shadow: var(--shadow-lg);
  background: var(--tpl-surface);
  overflow: hidden;
  /* An `<img>` is an inline box: without this the frame carries a descender's
     worth of ground under the picture. */
  line-height: calc(0 * var(--tpl-plh, 1));
  min-inline-size: 0;
}

/* THE ONE PLACE IN THIS FILE WHERE AN IMAGE IS FLUID, and it is deliberate.
   Everywhere else an `<img>` is given an explicit size because the app scales
   nothing and an intrinsic-size image is whatever the vendor shipped. A
   screenshot is the exception: it is a picture of a rectangle, it has no
   correct size of its own, and the frame is what decides how large it is. */
.tpl-shot__img {
  display: block;
  inline-size: 100%;
  block-size: auto;
  /* The guard rail. `overflow: hidden` on the frame would hide a too-tall
     capture from the eye but not from the canvas: the image element itself
     still reaches past the slide, which is exactly what the preview harness
     flags. Capping the image is what actually keeps it inside. A capture taller
     than 16:9 letterboxes here rather than cropping; that is the signal to move
     it to `--tall`. */
  max-block-size: calc(29cqw * var(--tpl-scale));
  object-fit: contain;
}

/* A portrait capture. Keeps the top of the window and drops the bottom, which
   on a long page is where there is least to say. `cover` plus a top
   `object-position` is what makes the crop happen inside the image box instead
   of by clipping an oversized element. */
.tpl-shot--tall {
  max-inline-size: calc(27cqw * var(--tpl-scale));
}

.tpl-shot--tall .tpl-shot__img {
  max-block-size: calc(27cqw * var(--tpl-scale));
  object-fit: cover;
  object-position: top center;
}

/* Placed from the body with `style="--x:42%;--y:18%"`. Those are computed
   values, which is the one thing an inline style is still for. */
.tpl-shot__pin {
  position: absolute;
  inset-inline-start: var(--x, 50%);
  inset-block-start: var(--y, 50%);
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  justify-content: center;
  inline-size: calc(2.6cqw * var(--tpl-scale));
  block-size: calc(2.6cqw * var(--tpl-scale));
  border-radius: 50%;
  /* Solid, never the brand gradient: this numeral is weight 500 and the brand
     rule forbids the gradient at 500 and above, numerals included. The white
     ring is what keeps the pin legible on a dark screenshot as well as a light
     one, so it stays even on the charcoal skin. */
  background: var(--primary-color);
  color: #ffffff;
  font-family: var(--font-mono);
  font-size: calc(clamp(11px, 1.35cqw, 27px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  font-weight: 500;
  line-height: calc(1 * var(--tpl-plh, 1));
  box-shadow: 0 0 0 calc(0.22cqw * var(--tpl-scale)) rgba(255, 255, 255, 0.92), var(--shadow-md);
}

.tpl-shot__cap {
  font-size: calc(clamp(13px, 1.5cqw, 30px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  line-height: calc(1.35 * var(--tpl-plh, 1));
  color: var(--tpl-ink-faint);
  text-align: center;
  max-inline-size: 52ch;
}

/* Off the canvas there is no container, so a `cqw` cap resolves against the
   small viewport and would crush the picture to a stamp. Every cap the frame
   depends on is restated in px here. */
.slide--phone .tpl-shot,
.slide--phone .tpl-shot--tall { max-inline-size: 100%; }
.slide--phone .tpl-shot__img { max-block-size: none; }
.slide--phone .tpl-shot--tall .tpl-shot__img { max-block-size: 320px; }
.slide--phone .tpl-shot__pin { inline-size: 20px; block-size: 20px; font-size: calc(11px * var(--tpl-type-step, 1)); box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.92), var(--shadow-md); }
.slide--phone .tpl-shot__cap { font-size: calc(13px * var(--tpl-type-step, 1)); }

/* ── T16  cmd: one command, the same shape every time ────────────────────── */
/* Used 26 times across the terminal, Claude Code and Git chapters. The value is
   that it never moves: the room learns where the name is, where the gloss is
   and where the example is, and stops re-reading the layout on every slide.

   The name and the form are `justify-self: start` rather than full-width blocks
   with `text-align`. An LTR block inside an RTL column aligns its text to the
   LEFT edge of the slide, which is the wrong side; shrinking the box to its
   content puts it back on the start side where the Hebrew begins. */

.tpl--cmd { justify-content: center; }

.tpl-cmd {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-content: center;
  gap: calc(0.75cqw * var(--tpl-scale));
  min-block-size: 0;
}

.tpl-cmd__name {
  justify-self: start;
  font-family: var(--font-mono);
  direction: ltr;
  unicode-bidi: isolate;
  /* Below the slide's own H1 on purpose. The title names the subject; this
     names the command, and two lines at the same size compete. */
  font-size: calc(clamp(22px, 3.4cqw, 68px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  font-weight: 400;
  line-height: calc(1.1 * var(--tpl-plh, 1));
  letter-spacing: -0.02em;
  color: var(--tpl-ink);
}

.tpl-cmd__gloss {
  font-size: calc(clamp(16px, 2.1cqw, 42px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  font-weight: 400;
  line-height: calc(1.35 * var(--tpl-plh, 1));
  color: var(--tpl-ink-soft);
  max-inline-size: 58ch;
}

.tpl-cmd__form {
  justify-self: start;
  font-family: var(--font-mono);
  direction: ltr;
  unicode-bidi: isolate;
  font-size: calc(clamp(13px, 1.5cqw, 30px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  line-height: calc(1.35 * var(--tpl-plh, 1));
  color: var(--tpl-ink-faint);
}

/* The example is a `.tpl-term`, not a second terminal. Put both classes on the
   same element and use `.tpl-term__cmd` / `__out` / `__ok` / `__err` / `__dim`
   inside it; all this adds is the slot's own type size, which is one step down
   so that six output lines still fit under everything above. */
.tpl-cmd__ex { min-inline-size: 0; }

.tpl-cmd__ex .tpl-term__body {
  padding: calc(1cqw * var(--tpl-scale));
  font-size: calc(clamp(12px, 1.4cqw, 28px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  line-height: calc(1.45 * var(--tpl-plh, 1));
}

.tpl-cmd__note {
  font-size: calc(clamp(13px, 1.5cqw, 30px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  line-height: calc(1.4 * var(--tpl-plh, 1));
  color: var(--tpl-ink-faint);
  max-inline-size: 62ch;
}

.slide--phone .tpl-cmd { gap: var(--space-sm); }
.slide--phone .tpl-cmd__name { font-size: calc(22px * var(--tpl-type-step, 1)); }
.slide--phone .tpl-cmd__gloss { font-size: calc(15px * var(--tpl-type-step, 1)); }
.slide--phone .tpl-cmd__form { font-size: calc(13px * var(--tpl-type-step, 1)); }
.slide--phone .tpl-cmd__ex .tpl-term__body { font-size: calc(12px * var(--tpl-type-step, 1)); }
.slide--phone .tpl-cmd__note { font-size: calc(13px * var(--tpl-type-step, 1)); }

/* ── T17  tree: a file system, printed ───────────────────────────────────── */
/* The indentation IS the content, so `white-space: pre` and no wrapping. A row
   that runs past the frame is a row that needs shortening; folding it onto a
   second line would leave something that no longer looks like a tree.

   The `pre` sits on the ROW, not on the block. Put it on the block and the
   newline an author leaves between two `<div class="tpl-tree__row">` tags is
   preserved as a blank line, and a hand-indented tree renders at double
   spacing: measured at 73px of overflow on a 6-row tree, which is a slide the
   preview harness fails and nobody can see the cause of. Between block-level
   rows the whitespace now collapses away as it should. */

/* One tree is an artefact, not a column of text: centred, it sits under a
   centred title instead of hugging the left edge of an RTL slide. A pair of
   trees is a layout and stays full width. */
.tpl--tree {
  justify-content: center;
  align-items: center;
}

.tpl--tree .tpl-trees { align-self: stretch; }

.tpl-tree {
  font-family: var(--font-mono);
  direction: ltr;
  unicode-bidi: isolate;
  font-size: calc(clamp(12px, 1.5cqw, 30px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  line-height: calc(1.5 * var(--tpl-plh, 1));
  color: var(--tpl-ink-soft);
  min-inline-size: 0;
  overflow: hidden;
}

.tpl-tree__row {
  display: block;
  white-space: pre;
}

.tpl-tree__dir {
  font-weight: 500;
  color: var(--tpl-ink);
}

.tpl-tree__file { color: var(--tpl-ink-soft); }

/* A Hebrew gloss inside an LTR block. Without the isolate the block's own
   direction reorders it and the punctuation lands on the wrong end, which is
   the same failure `.tpl-code` exists to prevent, running the other way.

   The gap is on inline-END, and that is not a typo. A logical property resolves
   against the ELEMENT's own direction, and this element is RTL inside an LTR
   line: its inline-start is its RIGHT edge, which in the line is the far side,
   away from the file name. Written the obvious way the measured gap is 0px. */
.tpl-tree__note {
  font-family: var(--font-body);
  direction: rtl;
  unicode-bidi: isolate;
  font-size: 0.86em;
  color: var(--tpl-ink-faint);
  padding-inline-end: 1.4em;
}

/* Two trees side by side: Windows beside Unix. Column flow over two tracks, so
   the source reads head, tree, head, tree and needs no wrapper per side. */
.tpl-trees {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-template-rows: auto minmax(0, 1fr);
  grid-auto-flow: column;
  gap: calc(0.9cqw * var(--tpl-scale)) var(--tpl-gap);
  min-block-size: 0;
}

.tpl-trees__head {
  font-family: var(--font-mono);
  font-size: calc(clamp(12px, 1.4cqw, 28px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  letter-spacing: 0.08em;
  line-height: calc(1.3 * var(--tpl-plh, 1));
  color: var(--tpl-ink-faint);
  padding-block-end: calc(0.5cqw * var(--tpl-scale));
  border-block-end: 1px solid var(--tpl-line);
}

.slide--phone .tpl--tree { align-items: stretch; }
.slide--phone .tpl-tree { font-size: calc(12px * var(--tpl-type-step, 1)); overflow-x: auto; }
.slide--phone .tpl-trees {
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: none;
  grid-auto-flow: row;
  gap: var(--space-sm);
}
.slide--phone .tpl-trees__head { font-size: calc(12px * var(--tpl-type-step, 1)); padding-block-end: 4px; }

/* ── T18  scale: orders of magnitude, as bars ────────────────────────────── */
/* Logarithmic by construction: the author writes the power of ten AND the bar
   length, because a linear bar for 10 cubed beside 10 to the twelfth is one
   pixel and a blank row. The gradient is allowed on the fill - the brand rule
   is about letterforms, and a bar is not a letter. */

.tpl--scale { justify-content: center; }

.tpl-scale {
  display: grid;
  gap: calc(0.55cqw * var(--tpl-scale));
  min-block-size: 0;
}

.tpl-scale__row {
  display: grid;
  grid-template-columns:
    minmax(0, calc(18cqw * var(--tpl-scale)))
    calc(7cqw * var(--tpl-scale))
    minmax(0, 1fr);
  align-items: center;
  gap: calc(1.2cqw * var(--tpl-scale));
  /* Every row carries the highlight's padding, so lighting one shifts nothing
     on the slide beside it. */
  padding: calc(0.3cqw * var(--tpl-scale)) calc(0.9cqw * var(--tpl-scale));
  border-radius: calc(var(--tpl-radius) * 0.7);
  min-inline-size: 0;
}

.tpl-scale__row.is-on { background: var(--primary-light); }

.tpl-scale__lbl {
  font-size: calc(clamp(13px, 1.6cqw, 32px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  line-height: calc(1.25 * var(--tpl-plh, 1));
  color: var(--tpl-ink);
  min-inline-size: 0;
}

.tpl-scale__num {
  font-family: var(--font-mono);
  direction: ltr;
  unicode-bidi: isolate;
  text-align: end;
  font-size: calc(clamp(12px, 1.4cqw, 28px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  line-height: calc(1.2 * var(--tpl-plh, 1));
  color: var(--tpl-ink-faint);
}

/* The track. In RTL the fill starts at the right edge and grows leftwards,
   which is the direction the room is already reading in.

   THIN, AND THAT IS THE POINT. The bar was 1.1cqw (21px on the canvas), which
   is a slab, and the fill carries the brand gradient - which the house rule
   allows on hairlines and thin bars and forbids on any large area. 0.22cqw is
   a fifth of the old thickness, so 4.2px on the 1920 canvas: a hairline with a
   colour ramp rather than a painted block. The BAR LENGTHS ARE THE DATA and
   none of them change; this is thinner, never shorter. The phone's hand-set 6px
   below is deliberately NOT re-derived from this number - it is a bar read at
   arm's length rather than from the back of a room, and under about 5px it
   stops reading as a bar there at all. Leave it alone. */
.tpl-scale__bar {
  block-size: calc(0.22cqw * var(--tpl-scale));
  border-radius: var(--radius-full);
  background: var(--bg-secondary);
  overflow: hidden;
  min-inline-size: 0;
}

.tpl-scale__fill {
  display: block;
  block-size: 100%;
  inline-size: var(--w, 0%);
  border-radius: var(--radius-full);
  background: var(--tpl-brand);
}

/* Three columns of a magnitude chart do not survive a phone card, so the bar
   drops to its own full-width row under the label and the number. */
.slide--phone .tpl-scale { gap: 6px; }
.slide--phone .tpl-scale__row { grid-template-columns: minmax(0, 1fr) auto; gap: 6px 10px; padding: 4px 6px; }
.slide--phone .tpl-scale__bar { grid-column: 1 / -1; block-size: 6px; }
.slide--phone .tpl-scale__lbl { font-size: calc(14px * var(--tpl-type-step, 1)); }
.slide--phone .tpl-scale__num { font-size: calc(12px * var(--tpl-type-step, 1)); }

/* ── T19  stepper: where the room is in the thirteen-step workshop ───────── */
/* The root exists so a whole slide can be the strip, but the strip is really a
   component: `.tpl-stepper` works as the first child of ANY template root. */

.tpl--stepper { justify-content: center; }

.tpl-stepper {
  display: flex;
  align-items: center;
  gap: calc(0.6cqw * var(--tpl-scale));
  min-inline-size: 0;
}

/* Written first in a body, the strip belongs to the TITLE, exactly as the
   eyebrow does: in a centred flex column it would otherwise drift to the middle
   of the slide and read as content. One class plus one pseudo-class, the same
   weight as the eyebrow's rule above, deliberately. */
/* `:not(.tpl-free)` for the same reason the mascot rule carries it, and it was
   found the same way - in a browser, not in a spec. This rule is (0,4,0) and the
   free-placement rule at the foot of this file is (0,3,0), so WITHOUT the
   exclusion this one wins outright on a free eyebrow that happens to be the
   first child of its root. What the owner then sees is not a box that ignored
   its `--tpl-fx`: `inset-inline-start: 0` under `direction: rtl` resolves to
   `right: 0`, so the box pins itself to the OPPOSITE corner from the one the
   numbers name, and the panel and the slide disagree with nothing on screen to
   say why. */
.slide--display .tpl > .tpl-stepper:first-child:not(.tpl-free) {
  position: absolute;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: 1;
}

.slide--display .tpl:has(> .tpl-stepper:first-child:not(.tpl-free)) {
  padding-block-start: calc(4cqw * var(--tpl-scale));
}

.tpl-stepper__label {
  flex: none;
  font-family: var(--font-mono);
  font-size: calc(clamp(12px, 1.3cqw, 26px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  letter-spacing: 0.06em;
  line-height: calc(1.3 * var(--tpl-plh, 1));
  color: var(--tpl-ink-faint);
  padding-inline-end: calc(0.8cqw * var(--tpl-scale));
}

/* Thirteen pips plus the label is about 640px of a 1651px content column, so
   the strip never needs to wrap on the canvas. */
.tpl-stepper__pip {
  flex: none;
  inline-size: calc(1.6cqw * var(--tpl-scale));
  block-size: calc(0.2cqw * var(--tpl-scale));
  border-radius: var(--radius-full);
  background: var(--border-default);
  transition: inline-size 0.25s ease, background 0.25s ease;
}

.tpl-stepper__pip.is-done {
  background: var(--tpl-accent);
  opacity: 0.45;
}

/* The live step. The gradient is fine here for the same reason as the scale
   bar: it is a pill, not a letterform. */
.tpl-stepper__pip.is-now {
  inline-size: calc(3.4cqw * var(--tpl-scale));
  block-size: calc(0.24cqw * var(--tpl-scale));
  background: var(--tpl-brand);
  opacity: 1;
}

/* On a phone card thirteen dashes are thirteen things nobody counts, so the
   strip becomes what it was standing in for all along: the label on its own
   line and one thin bar of segments under it. */
.slide--phone .tpl-stepper { flex-wrap: wrap; gap: 3px; }
.slide--phone .tpl-stepper__label {
  inline-size: 100%;
  font-size: calc(12px * var(--tpl-type-step, 1));
  padding-inline-end: 0;
  margin-block-end: 4px;
}
.slide--phone .tpl-stepper__pip { flex: 1 1 0; inline-size: auto; block-size: 3px; }
.slide--phone .tpl-stepper__pip.is-now { inline-size: auto; block-size: 3px; }

/* ── Mascot ──────────────────────────────────────────────────────────────── */
/* 33 poses, 117 files, at /mascot/<pose>-<bust|full>-<light|dark>.svg, the same
   set in both apps' public/mascot. `MASCOT_POSES` in src/data/slideTemplates.ts
   is the roster, and tests/slideTemplatesMascot.spec.ts checks it against both
   directories, so read the count there rather than trusting this one. 240x240
   gradient line art, so it reads at any size and needs no background behind it.
   `light` and `dark` name the SURFACE the pose is drawn for, not the figure: use
   `-dark` on the Claude Code skin. */

.tpl-mascot {
  display: block;
  inline-size: clamp(60px, 11cqw, 220px);
  block-size: auto;
  flex: none;
}

.tpl-mascot--sm { inline-size: clamp(44px, 7cqw, 140px); }
.tpl-mascot--lg { inline-size: clamp(90px, 17cqw, 330px); }
.tpl-mascot--xl { inline-size: clamp(120px, 26cqw, 500px); }

/* Parked in a corner of the slide, out of the text's way. Absolute against the
   slide, not the body, so it does not take part in the template's layout and
   therefore cannot push anything off the canvas. */
/* Deliberately smaller than an in-flow mascot. Every corner mascot costs the
   slide a reserved band (below), and at the earlier 11cqw that band was 173px
   out of a roughly 546px content budget - a third of the slide, spent on
   decoration, on a deck whose whole problem was that it did not fit. */
.tpl-mascot--corner {
  position: absolute;
  inset-block-end: 3.2cqw;
  inset-inline-start: 3.2cqw;
  inline-size: clamp(50px, 8cqw, 160px);
  z-index: 0;
  /* Behind the text and quiet enough that the last line of a full slide reads
     cleanly across it. The reserved band below keeps them apart on most slides;
     this is what makes the remaining few look deliberate rather than collided. */
  opacity: 0.7;
  pointer-events: none;
}

/* The mirror of `--corner`: `inline-end` is the LEFT side in this deck's RTL.
   It carries its own size, because a size modifier like `--lg` would otherwise
   win from earlier in the file and put a 326px figure in a corner sized for
   160px - which is what it did, floating it into the middle of the slide. */
.tpl-mascot--corner-end {
  position: absolute;
  inset-block-end: 3.2cqw;
  inset-inline-end: 3.2cqw;
  inline-size: clamp(50px, 8cqw, 160px);
  z-index: 0;
  opacity: 0.7;
  pointer-events: none;
}

/* An IN-FLOW mascot that ends a template column, which is a different animal
   from either corner variant above. `.tpl` is a flex COLUMN, so a child with its
   own `inline-size` is placed at the inline START of its line - the physical
   RIGHT in this RTL deck - and a trailing one therefore lands in the bottom
   right corner, which the terminal brandmark has owned since 2026-09-06
   (slides.css). On order 2 the checklist mascot's clipboard had the chevron's
   arm drawn straight through it, because the brandmark is positioned and an
   in-flow sibling is not, so the mark paints on top.

   Centring is the fix rather than a nudge: it needs no magic number, it cannot
   collide with EITHER bottom corner, and on a template that already centres its
   children (`tpl--section`, `tpl--center`, `tpl--figure`) it changes nothing.
   `align-self` is ignored on an absolutely positioned child, so neither corner
   variant nor the watermark can be reached by this rule. */
.slide--display .tpl > .tpl-mascot:last-child {
  align-self: center;
}

/* A watermark rather than a character: very large, very faint, behind the text. */
.tpl-mascot--watermark {
  position: absolute;
  inset-block-end: -4cqw;
  inset-inline-start: -3cqw;
  inline-size: 34cqw;
  opacity: 0.07;
  z-index: 0;
  pointer-events: none;
}

/* A PLACED mascot (`tpl-mascot tpl-free`) goes with them. On the phone nothing is
   placed, so it would come back as a figure the width of the reading column,
   in DOM order, between two lines of body copy: decoration where the reading
   is, which is what the three placements above are hidden to avoid. */
.slide--phone .tpl-mascot--corner,
.slide--phone .tpl-mascot--corner-end,
.slide--phone .tpl-mascot--watermark,
.slide--phone .tpl-mascot.tpl-free { display: none; }

/* Everything a template draws sits above a corner mascot or a watermark.
   `:not(.tpl-mascot)` is load-bearing, not tidiness. `.tpl > *` and
   `.tpl-mascot--corner` are both specificity (0,1,0), and this rule comes later
   in the file, so without the exclusion it OVERRODE the mascot's
   `position: absolute` and dropped it back into the flex column - as a full row,
   pushing the slide's content up and out. Three authors hit it independently and
   each worked around it with an inline style attribute; the bug is here, and
   this is where it is fixed.

   `:not(.tpl-free)` IS THE SAME BUG, ONE VOCABULARY LATER, and it is the first
   of three defences the free-placement section at the end of this file relies
   on. A free box is positioned too (`absolute`, or `fixed` under a
   `tpl--slide-frame` root), placed from percentages in its own style
   attribute. Without this exclusion the rule above would drop every one of
   them back into the centred flex column AS A FULL-WIDTH ROW, pushing the
   slide's own content off the canvas, with nothing erroring and nothing on the
   projector to say so. The other two defences are that the free rule is written
   at (0,3,0), which beats this line on specificity, and that it is placed at the
   very end of this file, which beats it on order. Three, because one "tidy-up"
   must not be able to reopen it. tests/editorDeckFreeCss.spec.ts asserts all
   three, in this file and in its mirror. */
.tpl > *:not(.tpl-mascot):not(.tpl-free) { position: relative; z-index: 1; }

/* A template's blocks NEVER shrink below their content.
   This is not tidiness. `.tpl` is a centred flex column, so a direct child with
   the default `flex-shrink: 1` is squashed when the column is over budget - and
   a squashed `.tpl-split` or `.tpl-grid` still lays its own children out at full
   height and, because it centres them, paints them OUTSIDE its box, on top of
   the blocks above and below. Measured on the workshop's `git diff` slide: a
   grid reporting 62px tall holding a 446px terminal, printing over the badge
   above it and the closing line below it. Nothing about that reads as an
   overflow - the slide still fits the canvas - which is why the rule is here
   and why the preview harness now checks for escaping children too. Over
   budget must mean CLIPPED at the bottom, which is visible, never overlapping,
   which is not. */
.tpl > * { flex: 0 0 auto; }

/* `flex: 0 0 auto` needs no mascot exclusion: an absolutely positioned element
   is not a flex item, so this rule cannot reach one. The `position` rule above
   is the one that had to say `:not(.tpl-mascot)`, and does. */

/* A corner mascot occupies a band the centred content must not reach into.
   Reserving it on the template rather than asking every author to remember is
   the whole point: the smoke test put a mascot's head through the bottom card
   of a three-card grid, and nothing about the markup said it would. */
.slide--display:has(.tpl-mascot--corner) .tpl,
.slide--display:has(.tpl-mascot--corner-end) .tpl {
  padding-block-end: calc(6cqw * var(--tpl-scale));
}

/* A watermark sits behind the text on purpose and reserves nothing. */

/* ── Skins ───────────────────────────────────────────────────────────────── */
/* Opted into from the body, so one authored file decides the whole slide. */

/* Soft blue wash - the site's own page ground. */
.slide--display:has(.tpl--tint) {
  background: linear-gradient(180deg, #f7fbff 0%, #eaf2fd 100%);
}

/* A faint grid, for anything structural: layers, files, a network. */
.slide--display:has(.tpl--grid) {
  background-color: var(--bg-surface);
  background-image:
    linear-gradient(rgba(0, 113, 227, 0.055) 1px, transparent 1px),
    linear-gradient(90deg, rgba(0, 113, 227, 0.055) 1px, transparent 1px);
  background-size: 4cqw 4cqw;
}

/* A brand glow in the corner. The deck's one ambient decoration. */
.slide--display:has(.tpl--glow) {
  background:
    radial-gradient(ellipse 60% 55% at 92% 6%, rgba(189, 50, 214, 0.09) 0%, transparent 62%),
    radial-gradient(ellipse 70% 60% at 6% 96%, rgba(8, 148, 255, 0.1) 0%, transparent 60%),
    var(--bg-surface);
}

/* ── The Claude Code skin ────────────────────────────────────────────────── */
/* The palette of the site's Claude Code deep-dive page
   (public/css/claude-code-deep-dive-view.css): charcoal ground, gold accent,
   an opacity scale for ink. Used for the Claude Code chapter and nothing else,
   so that the switch to dark IS the signal that the subject changed. */

.slide--display:has(.tpl--dark),
.slide--phone:has(.tpl--dark) {
  --ccdd-gold: #d4a574;
  /* THE TWO STATE HUES ON CHARCOAL. The site's amber (#A86000) and red (#BE123C)
     are both tuned for white and go muddy here, so each is replaced by a lighter
     member of its own family: 9.17:1 and 6.82:1 on the #13141c mid-stop of the
     ground above, against the 4.5:1 a slide line needs.

     THE AMBER IS THE BRAND RAMP'S OWN ORANGE, which is also the gold accent's
     neighbourhood, so the one real risk here is reading a warning as an accent.
     Measured rather than eyeballed: dE76 48 from --ccdd-gold (L*74 a*25 b*78
     against L*71 a*11 b*32), which is far past the ~10 two colours need to read
     as different at a glance across a room. Do not nudge either towards the gold. */
  --ccdd-amber: #ffa304;
  --ccdd-red: #fb7185;
  --ccdd-ink-100: rgba(255, 255, 255, 0.96);
  --ccdd-ink-80: rgba(255, 255, 255, 0.82);
  --ccdd-ink-60: rgba(255, 255, 255, 0.6);
  --ccdd-ink-45: rgba(255, 255, 255, 0.45);
  --ccdd-line: rgba(255, 255, 255, 0.08);
  background:
    radial-gradient(ellipse 80% 50% at 50% 0%, rgba(212, 165, 116, 0.07) 0%, transparent 60%),
    linear-gradient(180deg, #0b0c12 0%, #13141c 34%, #161722 100%);
  color: var(--ccdd-ink-80);
}

.slide--display:has(.tpl--dark) .slide__title,
.slide--phone:has(.tpl--dark) .slide__title {
  color: var(--ccdd-ink-100);
}

/* The corner brandmarks are GRADIENT-filled, never ink, so they do not vanish
   on charcoal - they only need to sit forward a little, because 0.42 over white
   and 0.42 over #0b0c12 are not the same weight.

   NO `filter: invert(1)`. This rule used to carry one, and it inverted the
   brand ramp itself: #0894FF became #F76B00, #BD32D6 became #42CD29, #FFA304
   became #005CFB, i.e. a green-and-orange mark on the eight Claude Code slides.
   Nobody ever saw it because the brandmark painted nothing at all until the
   sizing bug in slides.css was fixed. Verified by render: at 0.55 with no
   filter both the 5-stop and the 7-stop ramps read correctly on the charcoal. */
.slide--display:has(.tpl--dark) .slide__brandmark { opacity: 0.55; }

.tpl--dark {
  --tpl-ink: var(--ccdd-ink-100);
  --tpl-ink-soft: var(--ccdd-ink-60);
  --tpl-ink-faint: var(--ccdd-ink-45);
  --tpl-line: var(--ccdd-line);
  --tpl-surface: rgba(255, 255, 255, 0.03);
  --tpl-accent: var(--ccdd-gold);
  /* The state inks re-point with everything else. The washes are written out
     rather than pointed at the site's, because those two are films for WHITE:
     rgba(255,163,4,0.10) over charcoal is invisible. 0.14 of the skin's own hue
     is the same weight the card's gold tint carries here. */
  --tpl-warning: var(--ccdd-amber);
  --tpl-error: var(--ccdd-red);
  --tpl-warning-wash: rgba(255, 163, 4, 0.14);
  --tpl-error-wash: rgba(251, 113, 133, 0.14);
}

.tpl--dark .tpl-card {
  background: rgba(255, 255, 255, 0.035);
  border-color: var(--ccdd-line);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  box-shadow: none;
}

.tpl--dark .tpl-card--tint {
  background: rgba(212, 165, 116, 0.08);
  border-color: rgba(212, 165, 116, 0.18);
}

.tpl--dark .tpl-badge {
  background: rgba(212, 165, 116, 0.08);
  border-color: rgba(212, 165, 116, 0.18);
  color: var(--ccdd-gold);
}

.tpl--dark .tpl-code,
.tpl--dark .tpl-kbd {
  background: rgba(255, 255, 255, 0.06);
  border-color: var(--ccdd-line);
  color: var(--ccdd-ink-100);
}

.tpl--dark .tpl-step { border-block-end-color: var(--ccdd-line); }
.tpl--dark .tpl-table th,
.tpl--dark .tpl-table td { border-block-end-color: var(--ccdd-line); }
.tpl--dark .tpl-cmp__col { background: rgba(255, 255, 255, 0.03); border-color: var(--ccdd-line); }
.tpl--dark .tpl-cmp__col--on { background: rgba(212, 165, 116, 0.07); border-color: rgba(212, 165, 116, 0.2); }
.tpl--dark .tpl-anat__part.is-on .tpl-anat__txt { background: rgba(212, 165, 116, 0.1); }
.tpl--dark .tpl-term { border-color: var(--ccdd-line); background: #0f1017; }

/* T12 to T19. Only the parts that own a ground or a border are listed: every
   type colour above is already a `--tpl-ink*` token and re-points itself. */

/* The dot's fill is what hides the track running behind it, so on charcoal it
   has to be the charcoal rather than the 3%-white surface token. #13141c is the
   middle stop of the skin's own ground gradient. */
.tpl--dark .tpl-tl__dot { background: #13141c; }

.tpl--dark .tpl-layer {
  background: rgba(255, 255, 255, 0.035);
  border-color: var(--ccdd-line);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  box-shadow: none;
}

.tpl--dark .tpl-layer.is-on {
  background: rgba(212, 165, 116, 0.08);
  border-color: rgba(212, 165, 116, 0.45);
  box-shadow: none;
}

/* Opt-in, per logo. Most official marks are monochrome dark and disappear on
   charcoal; a blanket invert would wreck the colourful ones beside them. */
.tpl--dark .tpl-logo--invert .tpl-logo__img { filter: invert(1); }

.tpl--dark .tpl-shot {
  background: rgba(255, 255, 255, 0.03);
  border-color: var(--ccdd-line);
  box-shadow: none;
}

.tpl--dark .tpl-trees__head { border-block-end-color: var(--ccdd-line); }
.tpl--dark .tpl-scale__bar { background: rgba(255, 255, 255, 0.07); }
.tpl--dark .tpl-scale__row.is-on { background: rgba(212, 165, 116, 0.08); }
/* All three pip rules, because `.tpl--dark .tpl-stepper__pip` and
   `.tpl-stepper__pip.is-now` are BOTH (0,2,0) and this one is later in the
   file: without the two states restated here the dark skin repaints every pip
   the same grey, and the strip stops saying anything. Measured on the probe
   deck before it was written. */
.tpl--dark .tpl-stepper__pip { background: rgba(255, 255, 255, 0.13); }
.tpl--dark .tpl-stepper__pip.is-done { background: var(--ccdd-gold); opacity: 0.5; }
.tpl--dark .tpl-stepper__pip.is-now { background: var(--tpl-brand); opacity: 1; }

/* The eyebrow's brand dash and the hairline stay full-colour on charcoal: they
   are the only place the brand gradient appears on this skin, and they read. */

/* The one failure mode of opting a skin in from the body: without `:has()` the
   slide is never repainted, and this skin's white ink would land on a white
   ground - a blank projector, in front of a room. Verified working in the
   Chromium the harness drives, and every browser that will be in the venue
   supports it, but the cost of being wrong is the whole chapter. So when the
   selector is unavailable the template paints its OWN box dark instead. The
   slide's title stays in ink on white above it, which is merely a different
   design rather than an unreadable one. */
@supports not selector(:has(*)) {
  .tpl--dark {
    background: linear-gradient(180deg, #0b0c12 0%, #161722 100%);
    padding: calc(3cqw * var(--tpl-scale));
    border-radius: 1.2cqw;
  }
}

/* ── Logo lockup, for the slides that carry the brand explicitly ─────────── */
/* The renderer already swaps the four brand names for the real logo components
   wherever they appear in a body (SlideHtml.vue). These rules only size a
   deliberate lockup.

   THE BRAND NAMES ARE NEVER SPELLED OUT IN THIS FILE, comments included. That
   swap walks into `<style>` elements too, so a literal here becomes a Vue
   component spliced into the middle of a declaration block. */

.tpl-lockup {
  display: flex;
  align-items: center;
  gap: 1.2cqw;
  font-size: calc(clamp(18px, 2.6cqw, 52px) * var(--tpl-scale) * var(--tpl-type-step, 1));
  font-weight: 300;
  color: var(--tpl-ink);
}

.tpl-lockup--lg { font-size: calc(clamp(24px, 4cqw, 80px) * var(--tpl-type-step, 1)); }

/* ── Build-step helpers ──────────────────────────────────────────────────── */
/* slides.css hides `[data-step="N"]` blocks that are still ahead of the live
   step. These are for the cases where the reveal is an emphasis change rather
   than an appearance: a row that lights up, a column that dims. Same one class
   plus one attribute weight as slides.css uses, deliberately - an uneven cascade
   here hides a line with no error and no symptom until the moment it should
   have appeared. */

.tpl-dim { transition: opacity 0.25s ease, color 0.25s ease; }
.slide--step-0 .tpl-dim--from1,
.slide--step-1 .tpl-dim--from2,
.slide--step-2 .tpl-dim--from3 { opacity: 0.32; }

/* ── Text lists ──────────────────────────────────────────────────────────── */
/* BULLETS AND NUMBERS INSIDE A PART, and the lines around them. Three parts may
   hold more than one line - `.tpl-note`, `.tpl-card__body`, and `.tpl-point` as
   a placed box - and one that does is in BLOCK FORM: only `<p>`,
   `<ul class="tpl-bullets">` and `<ol class="tpl-numbers">` children, with no
   whitespace between them. The vocabulary is TEXT_LIST_CLASSES and
   BLOCK_TEXT_PARTS in src/data/slideTemplates.ts, the editor's presets are
   TEXT_LISTS in src/data/slideTextFormat.ts, and tests/slideTextLists.spec.ts
   holds both to this section.

   TWO WEIGHTS, BOTH REACHED BY REPEATING THE CLASS. A list at (0,2,0) beats
   every house rule a bare list would get: `.slide__body ul`/`ol` (0,1,1), which
   makes it a padded flex column, the open-day app's `.slide__body ul` and `li`,
   and the main site's bare `ul` and `li` in global.css. A NESTED list is
   (0,3,0), so the second level beats the first with no second class on the
   element. Each preset restates both levels at the same two weights, later in
   the section, and its class rides on the outer list only. No ancestor is
   named, for the reason the formatting vocabulary below gives: a phone rewrite
   is drawn outside any `.slide`, so every `--tpl-*` read here carries the
   fallback `.slide` gives it.

   THE INDENT IS LOGICAL AND SITS ON THE ITEM. The list has no padding; each
   `li` hangs its marker outside its own start edge and makes room for it with
   `margin-inline-start`. So `<li dir="ltr">` inside a Hebrew list hangs its
   marker on the LEFT and indents from the left, as Docs draws it, with no rule
   of its own. Nothing forces a direction on a marker: "1." reads the way the
   line it numbers reads.

   A CENTRED OR END-ALIGNED PART PUTS THE MARKER ON THE LINE. `text-align`
   moves lines, never an item's start edge, so an outside marker stayed at the
   start edge while its text centred: about 800px from it on a full-width note
   at 1920px. So every class here that centres or end-aligns a block a list can
   sit in publishes two custom properties (the publishers, after the presets):
   `--tpl-list-markers: inside` makes the marker the first thing on its item's
   first line, where the alignment carries it with the text, and
   `--tpl-list-gutter: 0` takes away the margin it no longer needs. Custom
   properties because they inherit the way `text-align` does, so the nearest
   class wins: a start-aligned card body in a centred grid, or a start-aligned
   note on a centred root, keeps its outside markers. A list with no publisher
   above it reads the fallbacks, `outside` and 1, and draws exactly what it drew
   before. Chromium pads an inside disc, circle or arrow, so those stand about
   twice as far from their text as outside ones (half an em becomes one); the
   check, the diamonds and every number keep their spacing.

   MARKERS ARE `::marker`, NOT `::before`. A `::before` would sit on a line of
   its own above the `<p>` the editor's field keeps inside every `<li>`, and a
   second selector path for the field would drift. The cost is Safari, which
   colours and sizes a marker but ignores its font, so a number there is in the
   body face. A browser without `@counter-style` (Safari before 17) draws the
   paren preset as "1." and "a.": its parser drops the unknown name, so the
   plain list's own levels draw and only the ")" is lost (reasoned from
   WebKit's parser, not seen on a pre-17 Safari). Accepted. Numbers wear the
   house mono accent, the look of `.tpl-step__n`: mono face, tabular figures,
   0.9em, accent ink. Bullets take the accent ink only.

   WHY ABOVE THE FORMATTING VOCABULARY. Nothing here styles a property of an
   element that section styles, so the order between the two decides nothing.
   Below it, the marker's `var(--font-mono)` would fall inside the span
   tests/slideTextFormat.spec.ts sweeps for site tokens that must be byte-equal
   in both palettes, and `--font-mono` deliberately is not (the room's stack
   puts Noto Sans Hebrew second, class-app tokens.css). The free-placement
   section has to stay last (tests/editorDeckFreeCss.spec.ts). */

/* The two numbering styles CSS does not name: the paren preset's "1)" and "a)".
   A counter style's name is global to the page, hence the prefix. */
@counter-style tpl-decimal-paren {
  system: extends decimal;
  suffix: ") ";
}

@counter-style tpl-alpha-paren {
  system: extends lower-alpha;
  suffix: ") ";
}

/* The list is its lines and nothing else: no box, no gap, no padding. */
.tpl-bullets.tpl-bullets,
.tpl-numbers.tpl-numbers {
  display: block;
  margin: 0;
  padding: 0;
  list-style-position: var(--tpl-list-markers, outside);
}

.tpl-bullets.tpl-bullets { list-style-type: disc; }
.tpl-numbers.tpl-numbers { list-style-type: decimal; }

/* The second level. It opens one item gap below its parent's line, so the
   rhythm down a nested list is even, as it is in Docs. */
:is(.tpl-bullets, .tpl-numbers) .tpl-bullets.tpl-bullets {
  list-style-type: circle;
  margin-block-start: 0.25em;
}

:is(.tpl-bullets, .tpl-numbers) .tpl-numbers.tpl-numbers {
  list-style-type: lower-alpha;
  margin-block-start: 0.25em;
}

/* Items. The start margin is the marker's room: 1.25em for a bullet, 2.4em for
   a number. The mono face at 0.9em is 0.573em a character, so "12. " is 2.29em
   wide and every number to 99 hangs whole inside the part; at 2em the first
   digit of "10." was cut off by the root's `overflow: hidden` wherever the part
   met the root's edge (measured in Chromium). A roman number is wider: "III. "
   and "VII. " are 2.87em and "VIII. " 3.44em, so a roman list's own items take
   3.6em, and every number to XVII hangs whole. The least height keeps an empty
   item one line tall, as the lines below do. Every gutter is multiplied by
   `--tpl-list-gutter`, which a centred or end-aligned part sets to 0 (the
   publishers, after the presets). */
.tpl-bullets.tpl-bullets > li,
.tpl-numbers.tpl-numbers > li {
  margin: 0;
  margin-inline-start: calc(1.25em * var(--tpl-list-gutter, 1));
  padding: 0;
  min-height: 1lh;
}

.tpl-numbers.tpl-numbers > li { margin-inline-start: calc(2.4em * var(--tpl-list-gutter, 1)); }
.tpl-numbers.tpl-numbers--roman > li { margin-inline-start: calc(3.6em * var(--tpl-list-gutter, 1)); }

.tpl-bullets.tpl-bullets > li + li,
.tpl-numbers.tpl-numbers > li + li { margin-block-start: 0.25em; }

.tpl-bullets.tpl-bullets > li::marker,
.tpl-numbers.tpl-numbers > li::marker { color: var(--tpl-accent, var(--primary-color)); }

.tpl-numbers.tpl-numbers > li::marker {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 0.9em;
}

/* The compare column draws its own dot as `.tpl-cmp__list li::before` (T9) on
   every item below it, so a list written inside a compare item, which the
   vocabulary does not allow but a hand-written body can, would draw two
   markers. `display: none` rather than `content: none`: the same box goes, and
   no `content` value is left for tests/editorFormatSlideHtml.spec.ts to read as
   a painting generated box. */
.tpl-bullets.tpl-bullets > li::before,
.tpl-numbers.tpl-numbers > li::before { display: none; }

/* The presets: level 1, then level 2, each pair later than the default it
   replaces and at its weight. The string markers end in an escaped space
   (`\20`): a space written straight after a hex escape is the escape's own
   terminator and would be swallowed. */
.tpl-bullets.tpl-bullets--arrow                   { list-style-type: disclosure-closed; }
.tpl-bullets--arrow .tpl-bullets.tpl-bullets      { list-style-type: circle; }
.tpl-bullets.tpl-bullets--check                   { list-style-type: "\2713\20"; }
.tpl-bullets--check .tpl-bullets.tpl-bullets      { list-style-type: circle; }
.tpl-bullets.tpl-bullets--diamond                 { list-style-type: "\25C6\20"; }
.tpl-bullets--diamond .tpl-bullets.tpl-bullets    { list-style-type: "\25C7\20"; }

.tpl-numbers.tpl-numbers--paren                   { list-style-type: tpl-decimal-paren; }
.tpl-numbers--paren .tpl-numbers.tpl-numbers      { list-style-type: tpl-alpha-paren; }
.tpl-numbers.tpl-numbers--alpha                   { list-style-type: upper-alpha; }
.tpl-numbers--alpha .tpl-numbers.tpl-numbers      { list-style-type: lower-alpha; }
.tpl-numbers.tpl-numbers--roman                   { list-style-type: upper-roman; }
.tpl-numbers--roman .tpl-numbers.tpl-numbers      { list-style-type: upper-alpha; }
.tpl-numbers.tpl-numbers--hebrew                  { list-style-type: hebrew; }
.tpl-numbers--hebrew .tpl-numbers.tpl-numbers     { list-style-type: decimal; }

/* The publishers: every class in this file that centres or end-aligns a block a
   list can sit in (the two alignment families, and the three roots whose own
   text is centred, which a note or a placed box on them inherits), then the one
   that puts a part back to start. */
.tpl-al--center,
.tpl-al--end,
.tpl-free--center,
.tpl-free--end,
.tpl--center,
.tpl--section,
.tpl--figure {
  --tpl-list-markers: inside;
  --tpl-list-gutter: 0;
}

.tpl-al--start {
  --tpl-list-markers: outside;
  --tpl-list-gutter: 1;
}

/* Lines inside a part are tight, like a paragraph's own lines: (0,2,1) beats
   `.slide__body p + p` (0,1,2). They also take the part's face, size, leading
   and ink rather than a page's bare `p` rule, which on the main site
   (global.css) sets a colour and a leading of its own wherever no `.slide` is
   around to reset it.
   A blank line stays a line: an empty `<p>` has no line box of its own and
   would close up, so every line is at least one line of the part's leading
   tall. A least height rather than a generated zero-width character, which
   would be a painting `::before` that tests/editorFormatSlideHtml.spec.ts
   refuses. */
.tpl-note.tpl-note > p,
.tpl-card__body.tpl-card__body > p,
.tpl-point.tpl-point > p {
  margin: 0;
  font: inherit;
  color: inherit;
  min-height: 1lh;
}

/* THE SPACING TRAP. `.slide__body p + p { margin-block-start: 0.8em }`
   (slides.css, and the main site's deck-slides.css and the editor's port) puts
   space above a one-line `<p class="tpl-note">` that follows a `<p>`, and above
   a `<p>` that follows one. A note that takes block form becomes a `<div>` and
   would lose both, so a part in block form - one with a `<p>`, `<ul>` or `<ol>`
   child - is spaced here as a `<p>` would be. Same (0,1,2) as the rule it
   restores, through `:where()`, so it wins and loses against the same rules;
   a placed box's `margin: 0` at (0,3,0) still wins. A one-line part never has a
   block child, so this matches nothing in a body written before lists. */
.slide__body p + :where(.tpl-note, .tpl-card__body, .tpl-point):has(> :is(p, ul, ol)),
.slide__body :where(.tpl-note, .tpl-card__body, .tpl-point):has(> :is(p, ul, ol)) + p,
.slide__body :where(.tpl-note, .tpl-card__body, .tpl-point):has(> :is(p, ul, ol))
  + :where(.tpl-note, .tpl-card__body, .tpl-point):has(> :is(p, ul, ol)) {
  margin-block-start: 0.8em;
}

/* ── Formatting vocabulary ───────────────────────────────────────────────── */
/* THE CLOSED LIST THE EDITOR WRITES, AND NOTHING ELSE. Every class below is
   declared in src/data/slideTextFormat.ts and registered as a modifier in
   src/data/slideTemplates.ts, and tests/slideTextFormat.spec.ts pins the three
   together. The five type size steps are the one group that is not here: they
   have to LOSE to a part's own size, so they sit above every part ("Type size
   steps"). Everything here has to WIN over a part, so it sits below every part
   and every skin.

   ONE WEIGHT FOR THE WHOLE-BLOCK OVERRIDES: (0,3,0), REACHED BY REPEATING THE
   CLASS. That is the weight of the heaviest part rule that sets anything these
   set - `.tpl-anat__part.is-on .tpl-anat__txt` and `.tpl-tl__stop.is-on
   .tpl-tl__t` for colour, `.slide--phone .tpl--lead .tpl-point` for a phone
   size - and coming later in the file wins the tie. `.tpl-table td:first-child`
   is (0,2,1) and loses outright, which is the point: an ink chosen for a table
   that silently did nothing in its first column is the failure this weight is
   written against. Repeating the class buys the weight WITHOUT naming an
   ancestor, and that matters: a phone rewrite is drawn in `.stu-deck__read` and
   `.cl-student__read`, neither of which is inside a `.slide`, so `.slide .x`
   would match on the projector and nowhere a student reads the rewrite.

   THOSE COLUMNS HAVE NO SKIN TOKENS EITHER, so every `--tpl-*` read in this
   section carries a fallback, and the fallback is the value `.slide` gives that
   token at the top of this file: `var(--tpl-ink, var(--text-color))`,
   `var(--tpl-scale, 1)`. The tokens are defined on `.slide` and nowhere above
   it, and a bare `var()` of an undefined token makes the whole declaration
   invalid at computed-value time, so it falls back to inheritance: an accent
   line painted the body colour and a styled title kept the column's own size,
   silently, while the editor's phone preview (inside a `.slide`) showed both.
   The four site tokens are `:root` tokens on both reading pages (class-app
   tokens.css, the main site's global.css). Inside a slide the token is always
   defined, so a skin still repaints everything here. The placed-block looks
   below need no fallback: a placed box exists only on a slide.
   tests/slideTextFormat.spec.ts pins the fallbacks against the `.slide` rule.

   Order inside the section is the other half. A text style sets a weight and an
   ink of its own, and a weight or ink class on the same element has to beat it
   at the same weight, so the styles come first.

   All of this is typography: nothing here sets opacity, transform, visibility or
   pointer-events, so none of it can reach the build reveal. The one look that
   does touch opacity is at the end of the placed-block looks below, with its
   guard in its own selector. */

/* Text styles: the predefined heading settings. Each is a whole look - face,
   size, weight, leading, tracking and ink - taken from the part it matches, so
   a styled block reads as that part wherever it stands:

     title       the section title (T2): display face, 5.4cqw, 300
     heading     the command name (T16): display face, 3.4cqw, 400 - one step
                 below the slide's own h1 on purpose, as that part is
     subheading  the point's size, 2.3cqw, at 500: a label rather than prose
     body        the lede: 2.05cqw, 400, soft ink
     caption     the note: 1.5cqw, 400, faint ink

   Unlike the section title, every one folds through `--tpl-scale`, so the
   density rungs reach a styled block, and through `--tpl-type-step`, so a size
   step on the same element composes with the style instead of fighting it. */
.tpl-ts--title.tpl-ts--title.tpl-ts--title {
  font-family: var(--font-display);
  font-size: calc(clamp(30px, 5.4cqw, 108px) * var(--tpl-scale, 1) * var(--tpl-type-step, 1));
  font-weight: 300;
  line-height: calc(1.08 * var(--tpl-plh, 1));
  letter-spacing: -0.03em;
  color: var(--tpl-ink, var(--text-color));
}

.tpl-ts--heading.tpl-ts--heading.tpl-ts--heading {
  font-family: var(--font-display);
  font-size: calc(clamp(22px, 3.4cqw, 68px) * var(--tpl-scale, 1) * var(--tpl-type-step, 1));
  font-weight: 400;
  line-height: calc(1.12 * var(--tpl-plh, 1));
  letter-spacing: -0.02em;
  color: var(--tpl-ink, var(--text-color));
}

.tpl-ts--subheading.tpl-ts--subheading.tpl-ts--subheading {
  font-family: var(--font-body);
  font-size: calc(clamp(17px, 2.3cqw, 46px) * var(--tpl-scale, 1) * var(--tpl-type-step, 1));
  font-weight: 500;
  line-height: calc(1.3 * var(--tpl-plh, 1));
  letter-spacing: normal;
  color: var(--tpl-ink, var(--text-color));
}

.tpl-ts--body.tpl-ts--body.tpl-ts--body {
  font-family: var(--font-body);
  font-size: calc(clamp(16px, 2.05cqw, 41px) * var(--tpl-scale, 1) * var(--tpl-type-step, 1));
  font-weight: 400;
  line-height: calc(1.4 * var(--tpl-plh, 1));
  letter-spacing: normal;
  color: var(--tpl-ink-soft, var(--text-secondary));
}

.tpl-ts--caption.tpl-ts--caption.tpl-ts--caption {
  font-family: var(--font-body);
  font-size: calc(clamp(13px, 1.5cqw, 30px) * var(--tpl-scale, 1) * var(--tpl-type-step, 1));
  font-weight: 400;
  line-height: calc(1.4 * var(--tpl-plh, 1));
  letter-spacing: normal;
  color: var(--tpl-ink-faint, var(--text-tertiary));
}

/* The phone's sizes, from the same parts' own phone branch, because a cqw size
   there resolves against no container this deck controls. Same weight as the
   rules above and later, so on a phone these win the size and nothing else. */
.slide--phone .tpl-ts--title.tpl-ts--title      { font-size: calc(26px * var(--tpl-type-step, 1)); }
.slide--phone .tpl-ts--heading.tpl-ts--heading  { font-size: calc(22px * var(--tpl-type-step, 1)); }
.slide--phone .tpl-ts--subheading.tpl-ts--subheading { font-size: calc(16px * var(--tpl-type-step, 1)); }
.slide--phone .tpl-ts--body.tpl-ts--body        { font-size: calc(15px * var(--tpl-type-step, 1)); }
.slide--phone .tpl-ts--caption.tpl-ts--caption  { font-size: calc(13px * var(--tpl-type-step, 1)); }

/* Weights. (0,3,0) beats every part that sets its own (`.tpl-card__head` at
   500, `.tpl--section .tpl-section__title` at 300) and, by a wide margin, the
   site-wide resets the main site lands inside a slide: `* { font-weight: 400 }`
   at (0,0,0) in fonts.css and the `:where(.slide)` block of deck-slides.css at
   (0,0,1). That is the same specificity story that puts `<b>` back to the
   room's weight (tests/deckSlideMarkBasis.spec.ts), so the two surfaces agree in
   whatever order their files are linked. A `<b>` inside a weighted run is still
   `bolder` than the run, on both. No weight here ever meets the brand gradient:
   nothing in this vocabulary paints text with it. */
.tpl-w--300.tpl-w--300.tpl-w--300 { font-weight: 300; }
.tpl-w--400.tpl-w--400.tpl-w--400 { font-weight: 400; }
.tpl-w--500.tpl-w--500.tpl-w--500 { font-weight: 500; }
.tpl-w--700.tpl-w--700.tpl-w--700 { font-weight: 700; }

/* Inks: the skin's own six tokens and nothing else. The Claude Code skin
   re-points every one of them (`.tpl--dark` above: ink at 96% white, soft 60%,
   faint 45%, the gold accent, and its own amber and rose), so an inked line
   stays readable on charcoal. A hex here would survive the skin swap and go
   invisible on it. The fallback in each is the site token `.slide` points that
   ink at, for a phone rewrite drawn outside any slide (the section note above
   says why).

   The first four are degrees of the same voice - how loud a line is. Warning and
   error are the two that say WHAT KIND, and they were added on 2026-09-13
   because without them a caution line came out as Accent, the same blue as an
   emphasis. Their fallbacks are the reason class-app/public/css/tokens.css had
   to gain --warning-color and --alert-color in the same change. */
.tpl-ink--full.tpl-ink--full.tpl-ink--full       { color: var(--tpl-ink, var(--text-color)); }
.tpl-ink--soft.tpl-ink--soft.tpl-ink--soft       { color: var(--tpl-ink-soft, var(--text-secondary)); }
.tpl-ink--faint.tpl-ink--faint.tpl-ink--faint    { color: var(--tpl-ink-faint, var(--text-tertiary)); }
.tpl-ink--accent.tpl-ink--accent.tpl-ink--accent { color: var(--tpl-accent, var(--primary-color)); }
.tpl-ink--warning.tpl-ink--warning.tpl-ink--warning { color: var(--tpl-warning, var(--warning-color)); }
.tpl-ink--error.tpl-ink--error.tpl-ink--error       { color: var(--tpl-error, var(--alert-color)); }

/* Alignment, logical: start is the right edge on a Hebrew slide and the left
   edge inside a Latin block (`.tpl-term`, `.tpl-kv__k`), as the text runs. */
.tpl-al--start.tpl-al--start.tpl-al--start    { text-align: start; }
.tpl-al--center.tpl-al--center.tpl-al--center { text-align: center; }
.tpl-al--end.tpl-al--end.tpl-al--end          { text-align: end; }

/* TEXT-ALIGN MOVES LINES, NOT BOXES, and two kinds of part need the box moved
   too, or "centre" paints something other than what it says:

   - A part with its own measure (`.tpl-point` is 58ch, `.tpl-lede` 62ch) sits at
     the start of a wider column, so lines centred inside it land short of the
     slide's centre - about 90px on a full-width point. Auto inline margins put
     the measure where the lines are. In the `.tpl` flex column they also stop
     the part stretching, which nothing without a background of its own can
     show. A placed box keeps the `margin: 0` the free-placement rule gives it at
     (0,3,0): it is as wide as it was drawn and has no measure to move.
   - A flex row (`.tpl-eyebrow`, `.tpl-cmp__head`, `.tpl-anat__row`,
     `.tpl-lockup`) lays its text out as flex items, which text-align does not
     move at all.

   The pinned first-child eyebrow is neither: it belongs to the title's corner
   and is shrink-wrapped there, so an alignment on it changes nothing. */
.tpl-al--start:is(.tpl-lede, .tpl-point, .tpl-section__title, .tpl-section__sub, .tpl-figure__cap, .tpl-quote__text, .tpl-shot__cap, .tpl-cmd__gloss, .tpl-cmd__note) {
  margin-inline: 0 auto;
}

.tpl-al--center:is(.tpl-lede, .tpl-point, .tpl-section__title, .tpl-section__sub, .tpl-figure__cap, .tpl-quote__text, .tpl-shot__cap, .tpl-cmd__gloss, .tpl-cmd__note) {
  margin-inline: auto;
}

.tpl-al--end:is(.tpl-lede, .tpl-point, .tpl-section__title, .tpl-section__sub, .tpl-figure__cap, .tpl-quote__text, .tpl-shot__cap, .tpl-cmd__gloss, .tpl-cmd__note) {
  margin-inline: auto 0;
}

.tpl-al--start:is(.tpl-eyebrow, .tpl-cmp__head, .tpl-anat__row, .tpl-lockup)  { justify-content: flex-start; }
.tpl-al--center:is(.tpl-eyebrow, .tpl-cmp__head, .tpl-anat__row, .tpl-lockup) { justify-content: center; }
.tpl-al--end:is(.tpl-eyebrow, .tpl-cmp__head, .tpl-anat__row, .tpl-lockup)    { justify-content: flex-end; }

/* The live meeting field (src/lib/deckLiveFields.ts). WORDS IN A SENTENCE, NOT A
   WIDGET: it has to read as the text around it on every surface, because the
   room is reading a date, not operating a control. So this rule declares only
   what a span inherits anyway; it exists to give the class a home in this file
   and to say so. Do not "help" by giving the field a colour, a chip or a hover.
   The one hint that it is a field is the editor's - a dotted underline in
   admin-editor-templates.css, text-decoration only - and it never reaches a
   slide surface. */
.tpl-dyn {
  color: inherit;
  font: inherit;
}

/* ── Placed-block looks ──────────────────────────────────────────────────── */
/* The looks a placed block may carry beyond the free-placement section's own:
   `FREE_LOOK_GROUPS` in src/data/slideTextFormat.ts, declared on the `tpl-free`
   shared part in src/data/slideTemplates.ts. Each is written
   `.tpl-free.tpl-free--x`, (0,2,0), so a look beats the role class the box also
   carries - `.tpl-card`'s border, radius, glass and shadow are (0,1,0) - and the
   dark skin's `.tpl--dark .tpl-card` repaint, which ties and is earlier in the
   file. None of them sets anything the placed-box rule sets, so none needs to
   come after it.

   THEY LIVE HERE, AND NOT IN THE FREE-PLACEMENT SECTION, because two of them
   need exactly what that section refuses. Its guarantee is by construction -
   nothing from its first `@property` to the end of the file declares opacity or
   paints the brand gradient - and carving a hole in that sweep would leave it
   guaranteeing nothing. So the family sits together here, the two that need a
   guard carry it in their own selector, and tests/editorDeckFreeCss.spec.ts
   pins both guards from out here. */

/* Borders, from tokens only: `--tpl-line` is the card hairline, and it, the
   faint ink and the accent all re-point on the Claude Code skin. 1px, like
   every other border in the deck. */
.tpl-free.tpl-free--bd-hair   { border: 1px solid var(--tpl-line); }
.tpl-free.tpl-free--bd-strong { border: 1px solid var(--tpl-ink-faint); }
.tpl-free.tpl-free--bd-accent { border: 1px solid var(--tpl-accent); }

/* The two state edges, from the same two tokens the warning and error inks read,
   so a box outlined amber and a line inked amber are the same colour on every
   skin. They carry the site-token fallback the inks carry, for the same reason:
   a phone rewrite is drawn outside any `.slide`, where no `--tpl-*` is defined.
   1px, like every other border here, and the FULL-strength ink rather than the
   wash - a 10% film reads as no border at all from the back of the room. */
.tpl-free.tpl-free--bd-warning { border: 1px solid var(--tpl-warning, var(--warning-color)); }
.tpl-free.tpl-free--bd-error   { border: 1px solid var(--tpl-error, var(--alert-color)); }

/* THE BRAND LINE: a 2px brand-gradient TOP EDGE - the capped card's look - and
   never a frame and never a fill. It is a 2px-tall BACKGROUND layer standing in
   a 2px transparent top border, and each half of that is doing a job:

   - A `::before` cap, the way `.tpl-card--capped` draws it, needs the box to
     clip it. A card is `overflow: hidden`; a placed box is `overflow: visible`
     at (0,3,0), so on a rounded box the cap's ends would stick out past the
     corners. A background is clipped to the border radius by the box itself,
     so the line follows the curve exactly as the capped card's does.
   - The border slot keeps content off the line: no text ever sits on it.
   - It composes with a fill. The fills below set `background-color` only and
     this sets only the image longhands, so glass or tint under a brand line is
     both at once. `.tpl-free--surface` and `.tpl-free--fill-accent` write the
     shorthand at (0,1,0) and lose these longhands to (0,2,0).

   It takes the place of a role's own top border - a card's 1px - which is the
   capped look: the side and bottom borders stay. */
.tpl-free.tpl-free--bd-brand {
  border-block-start: 2px solid transparent;
  background-image: var(--tpl-brand);
  background-origin: border-box;
  background-position: top;
  background-repeat: no-repeat;
  background-size: 100% 2px;
}

/* Radii, as steps of the deck's own `--tpl-radius` (1.1cqw, the card's), so a
   corner scales with the canvas like every other size here. The pill is the
   badge's `--radius-full`. */
.tpl-free.tpl-free--r-0    { border-radius: 0; }
.tpl-free.tpl-free--r-s    { border-radius: calc(var(--tpl-radius) * 0.5); }
.tpl-free.tpl-free--r-m    { border-radius: var(--tpl-radius); }
.tpl-free.tpl-free--r-l    { border-radius: calc(var(--tpl-radius) * 2); }
.tpl-free.tpl-free--r-pill { border-radius: var(--radius-full); }

/* Fills. Glass is `.tpl-card`'s own pane - translucent white over whatever
   ground the skin paints, blurred - and it goes FLAT on the Claude Code skin
   exactly as the card does, because a bright pane on charcoal reads as a hole
   in the slide. Tint is `.tpl-card--tint`'s wash, and gold on charcoal as the
   card's is. `background-color` only, never the shorthand, so the brand line
   above can stand on either. */
.tpl-free.tpl-free--fill-glass {
  background-color: rgba(255, 255, 255, 0.82);
  backdrop-filter: blur(16px) saturate(160%);
  -webkit-backdrop-filter: blur(16px) saturate(160%);
}

.tpl-free.tpl-free--fill-tint { background-color: var(--primary-light); }

/* The two state fills, and they read the WASH tokens, never the ink ones: a box
   filled with the full amber puts near-black text on a solid orange panel. Each
   wash re-points on `.tpl--dark` (the skin sets its own 14% of its own hue), so
   unlike glass and tint below neither of these needs a dark-skin rule of its
   own - which is the whole reason they are tokens and not two rgba() values. */
.tpl-free.tpl-free--fill-warning { background-color: var(--tpl-warning-wash, var(--warning-light)); }
.tpl-free.tpl-free--fill-error   { background-color: var(--tpl-error-wash, var(--alert-light)); }

.tpl--dark .tpl-free.tpl-free--fill-glass {
  background-color: rgba(255, 255, 255, 0.035);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

.tpl--dark .tpl-free.tpl-free--fill-tint { background-color: rgba(212, 165, 116, 0.08); }

/* Shadows, from the site's own tokens: soft is the card's, raised is what a lit
   layer, a terminal and a screenshot frame carry. The Claude Code skin draws no
   ambient shadow anywhere - its card, layer and shot all set none - so neither
   does a placed box on it. */
.tpl-free.tpl-free--sh-soft   { box-shadow: var(--shadow-md); }
.tpl-free.tpl-free--sh-raised { box-shadow: var(--shadow-lg); }

.tpl--dark .tpl-free.tpl-free--sh-soft,
.tpl--dark .tpl-free.tpl-free--sh-raised { box-shadow: none; }

/* OPACITY, WITH THE GUARD IN THE SELECTOR. The reveal is
   `.slide--step-N [data-step="M"]` at (0,2,0) in slides.css, loaded before this
   file, and it works by opacity: an opacity rule that lands on a `[data-step]`
   element at that weight or more outweighs it and shows a line early on a
   projector, with nothing to say so. So these can never match one.
   `:not([data-step], .tpl-dim)` is in every selector - `.tpl-dim` because the
   build-step helpers just above drive opacity from the step classes too. The
   editor disables the control on a step for the same reason; this is the half
   that does not depend on the editor. An opacity on a box that CONTAINS a step
   is fine: group opacity multiplies, so the step still appears, at the box's
   strength.

   `.slide--display` ONLY. On the projector a faded block composes with what is
   behind it; on a phone nothing is placed and nothing is behind it, and all an
   opacity does in a reading column is make a line harder to read. The class is
   written last in each compound so its name ends the selector, which is what
   the free spec's modifier check reads. */
.slide--display .tpl-free:not([data-step], .tpl-dim).tpl-free--op-80 { opacity: 0.8; }
.slide--display .tpl-free:not([data-step], .tpl-dim).tpl-free--op-60 { opacity: 0.6; }
.slide--display .tpl-free:not([data-step], .tpl-dim).tpl-free--op-40 { opacity: 0.4; }
.slide--display .tpl-free:not([data-step], .tpl-dim).tpl-free--op-20 { opacity: 0.2; }

/* ── Per-part numbers ────────────────────────────────────────────────────── */

/* THREE NUMBERS A PART MAY CARRY, and every one of them is a MULTIPLIER of what
   this file already gives that part. `OVERFLOW_FORBIDDEN` in
   src/data/slideTemplates.ts forbids hand-writing a font size, and it is right
   to: an ABSOLUTE size stops following the density rungs, stops following the
   phone branch, and stops being measurable, which is how 80 of one deck's 130
   slides came to overflow with nothing on the projector to say so. A multiplier
   does none of that.

   The editor's own half is src/components/editor/deck/partNumbers.ts, which can
   produce no unit anywhere by construction, and the vocabulary is registered as
   PART_NUMBER_MODIFIERS so the slot resolver reads a part carrying one as still
   being that part.

   Registered, all three, so an author's `--tpl-pfs: 20px` typed into the raw
   HTML panel fails the `<number>` syntax and falls back to the initial value
   rather than making the whole declaration invalid at computed-value time and
   taking a part's size with it. `inherits: false` on every one, for the reason
   `.tpl-free` gives its eight: a card carrying hand-set leading must not hand it
   down to its own head and number. */
@property --tpl-pfs { syntax: '<number>'; inherits: false; initial-value: 1; }
@property --tpl-plh { syntax: '<number>'; inherits: false; initial-value: 1; }
@property --tpl-piw { syntax: '<number>'; inherits: false; initial-value: 1; }

/* The belt to @property's braces, exactly as `.tpl-free` is. Firefox before 128
   registers nothing at all, so the initial values above do not exist there. One
   rule per class rather than one shared rule, because a part may opt into one
   number and not the others. */
.tpl-p--fs { --tpl-pfs: 1; }
.tpl-p--lh { --tpl-plh: 1; }
.tpl-p--iw { --tpl-piw: 1; }

/* SIZE FEEDS THE MULTIPLIER THIS FILE ALREADY HAS. `--tpl-type-step` is what the
   five `.tpl-t--*` rungs write and what every part's own absolute `font-size`
   here folds in, projector and `.slide--phone` branch alike, beside
   `--tpl-scale`. So this needs no rule of its own on any part: a part at 0.83
   draws at 83% of its template size, shrinks again under `tpl--d2`, and sizes
   itself on a phone. `.slide` and not `.slide--display`, because both surfaces
   read the step. (0,2,0), which beats the rungs at (0,1,0), so a number set by
   hand wins over a word-step the part still carries. */
.slide .tpl-p--fs { --tpl-type-step: var(--tpl-pfs); }

/* WIDTH, `.slide--display` only, for the reason the free section gives below:
   on a phone nothing is placed, the parts are rows in one reading column, and a
   45%-wide column of Hebrew there is unreadable. The max- and min- pair goes
   with it because `.tpl-lede` clamps itself to 62ch and `.tpl-point` to 58ch,
   and without this the drag would stop at the clamp with nothing saying why.
   A DESCENDANT combinator, not a child one: a part inside `tpl-grid`,
   `tpl-split` or `tpl-kv` is the common case, not the exception. */
.slide--display .tpl .tpl-p--iw {
  inline-size: calc(var(--tpl-piw) * 100%);
  max-inline-size: none;
  min-inline-size: 0;
}

/* LINE SPACING IS FOLDED INTO EVERY `line-height` IN THIS FILE rather than given
   a rule here, and that asymmetry is deliberate. A multiplier needs the part's
   own authored value to multiply, and there are 52 of them, each different; a
   single rule here could only impose one ratio for every part, which is an
   absolute leading wearing a multiplier's clothes. Folded, `--tpl-plh` at its
   initial 1 leaves all 52 spelling exactly what they spelled before.

   The consequence, said out loud: for leading alone the CLASS IS BOOKKEEPING.
   `--tpl-plh` on a part takes effect whether or not `tpl-p--lh` is beside it,
   unlike `--tpl-pfs` and `--tpl-piw`, which the two rules above gate on their
   class. The editor always writes the pair, and `shapeTraps.ts` reports a
   property without its class as a named finding, but a body hand-edited in the
   raw HTML panel will draw a lone `--tpl-plh`. */

/* ── Free placement ──────────────────────────────────────────────────────── */
/* NOT A TEMPLATE, and deliberately not a `T<n>` section: `tpl-free` is a SHARED
   PART. Any `.tpl` root may carry free boxes beside its own blocks, and a slide
   made entirely of them is just a root with nothing else in it. The roll call in
   tests/slideTemplatesCssSections.spec.ts counts templates by their `T<n>`
   banner, and this is not one.

   Placed from the body with style="--tpl-fx:7;--tpl-fy:14;--tpl-fw:46", which is
   the same sanction `.tpl-shot__pin` carries at T15: computed values, which is
   the one thing an inline style is still for. The numbers are BARE, and the unit
   is supplied here and only here. That is what makes `--tpl-ff: 24px` and
   `--tpl-fw: 200px` unwritable by construction, which is the only honest answer
   to `overflow-never-hand-write-a-font-size` in src/data/slideTemplates.ts.

   PERCENT OF WHAT IS THE ROOT'S DECISION, and there are two answers. A root
   carrying `tpl--slide-frame` measures its boxes from the WHOLE SLIDE; a root
   without it is a legacy body, measured from its own `.tpl` box under the title,
   and draws exactly as it always did. The rule right after the placed-box rule
   below is the whole difference.

   PHYSICAL left/top, not the logical pair the rest of this file prefers. These
   are picture coordinates on a fixed stage, taken from a pointer, and a slide
   does not mirror when its Hebrew does. The TEXT inside a box still inherits
   direction: rtl and still aligns to start.

   SCOPED TO .slide--display ON PURPOSE. The projector, both presenter-console
   previews, the student player, the student print sheet and the editor canvas
   all mount `slide slide--display`; the room's student handset and the editor's
   phone preview mount `slide slide--phone`. Nothing here matches a phone, so a
   free box there is simply an ordinary block in the phone's flex column, in DOM
   order, with its role class's own `.slide--phone` type. There is no phone
   branch below because there is nothing for one to fix - and the same scoping is
   what makes a deploy skew benign: a room whose CSS has not shipped yet draws an
   ugly readable stack rather than a heap at the origin.

   NOTHING HERE SETS opacity, transform, pointer-events, visibility, transition
   OR animation. Those are what `.slide--step-N [data-step="M"]` writes at
   (0,2,0) in slides.css, and this file loads AFTER slides.css, so a rule here
   would silently outweigh the build reveal and show a line early on a projector
   with nothing to say so. Rotation below is the INDEPENDENT `rotate` property,
   which composes ahead of `transform` rather than replacing it, so a rotated box
   can still be a build step. Never reach for `transform` here.

   NOR container-type, for the reason the whole file is written in cqw: the one
   container on either page is the 1920px canvas, and a second one re-bases every
   size in this vocabulary while looking plausible at every width. */

@property --tpl-fx { syntax: '<number>';  inherits: false; initial-value: 0; }
@property --tpl-fy { syntax: '<number>';  inherits: false; initial-value: 0; }
@property --tpl-fw { syntax: '<number>';  inherits: false; initial-value: 20; }
@property --tpl-fh { syntax: '<number>';  inherits: false; initial-value: 0; }
@property --tpl-fz { syntax: '<integer>'; inherits: false; initial-value: 1; }
@property --tpl-fr { syntax: '<number>';  inherits: false; initial-value: 0; }
@property --tpl-ff { syntax: '<number>';  inherits: false; initial-value: 2; }
@property --tpl-fo { syntax: '<integer>'; inherits: false; initial-value: 0; }

/* THE PREFIX IS SIX CHARACTERS OF INSURANCE, not a naming preference.
   `@property` registration is DOCUMENT-GLOBAL, and /editor loads the whole admin
   bundle beside this file. `.tpl-shot__pin` above already reads bare `--x` and
   `--y`, and nine live bodies write them. Registering a bare `--x` here would
   retype somebody else's property across the entire document, so every name in
   this section starts `--tpl-f`. */

/* The belt to @property's braces. Firefox before 128 registers nothing, and
   without `inherits: false` a free box nested inside another would inherit its
   parent's geometry. An inline value on the child beats this class rule, so the
   two mechanisms agree; do not delete one because the other is present. */
.tpl-free {
  --tpl-fx: 0; --tpl-fy: 0; --tpl-fw: 20; --tpl-fh: 0;
  --tpl-fz: 1; --tpl-fr: 0; --tpl-ff: 2;  --tpl-fo: 0;
}

/* (0,3,0). DEFENCE TWO of the three named on the mascot exclusion above: this
   beats `.tpl > *:not(.tpl-mascot)` at (0,2,0) on weight alone, so a free box
   stays absolutely positioned even if somebody removes the exclusion. Being the
   last section in the file is defence three. */
.slide--display .tpl > .tpl-free {
  position: absolute;
  box-sizing: border-box;
  left: calc(var(--tpl-fx) * 1%);
  top: calc(var(--tpl-fy) * 1%);
  inline-size: calc(var(--tpl-fw) * 1%);
  /* `.tpl-lede` and `.tpl-point` clamp themselves to 62ch / 58ch. A free box is
     as wide as it was drawn. */
  max-inline-size: none;
  min-inline-size: 0;
  /* A MINIMUM, never a height, and overflow stays visible. A fixed height plus a
     clip is a silent line-eater: `--font-mono` orders 'Noto Sans Hebrew' 2nd in
     class-app/public/css/tokens.css and 5th in public/css/global.css, so a
     Hebrew mono run genuinely measures differently in the room than in the
     editor. A box that grew must grow DOWN, where it can be seen.

     AND NO `block-size` AT ALL, not even the `auto` that stood on the next line
     until 2026-09-08. It read as a harmless restatement of the initial value and
     it was not: this rule is (0,3,0), so it outweighed every role class a placed
     box can carry, and `.tpl-rule`'s own `block-size: 2px` at (0,1,0) lost to it.
     An empty box at `auto` is zero pixels tall, so a "Hairline" placed from the
     panel painted NOTHING on any surface - editor canvas, projector, student
     player, print sheet - with no error anywhere and only the overlay's dashed
     outline, which has a minimum height of its own, to suggest a block was even
     there. `auto` is already the initial value for every other role, so leaving
     it out changes nothing for any of them, and the height a role declares for
     itself is the height it gets. Do not re-add it as a tidy-up: the next role
     with an intrinsic height goes the same way, just as silently.
     tests/editorDeckFreeCss.spec.ts pins this per ROLE rather than per class. */
  min-block-size: calc(var(--tpl-fh) * 1%);
  overflow: visible;
  margin: 0;
  z-index: var(--tpl-fz);
}

/* THE SLIDE FRAME: A MARKED ROOT MEASURES ITS PLACED BLOCKS FROM THE WHOLE SLIDE.
   The rule above resolves every percentage against `.tpl`, the nearest
   positioned ancestor, and `.tpl` sits UNDER the title and inside the slide's
   6cqw / 7cqw padding - so no number could put a block above the title, which is
   the first thing the owner asked to do (2026-10-08). A root carrying
   `tpl--slide-frame` (`SLIDE_FRAME_CLASS` in src/lib/slideFrame.ts, appended to
   the root's class list by the editor) means "these numbers are percent of the
   1920x1080 stage": `--tpl-fy:0` is the slide's top edge, above the title.

   `fixed`, NOT A SECOND `absolute`. An absolute box stops at the nearest
   positioned ancestor, and `.tpl` and `.slide__body` cannot stop being
   positioned: the pinned eyebrow, the stepper, the corner mascots and the
   watermark are `absolute` against them, and those must not move. A fixed box
   skips every ancestor that is merely positioned and stops at the nearest one
   that establishes a containing block for fixed descendants, which is the
   `.slide` itself, through `contain: layout` in slides.css on
   `.slide--display:has(> .slide__body > .tpl.tpl--slide-frame)`. So the
   unchanged `left`, `top`, `inline-size` and `min-block-size` above now resolve
   against the slide's padding box, and nothing else in this section needs a
   second spelling. The `contain` lives in slides.css and never here: it belongs
   to the slide box, and this section may not declare it at all
   (tests/editorDeckFreeCss.spec.ts).

   WHAT CHANGES, AND WHAT DOES NOT. `.tpl`'s `overflow: hidden` no longer clips a
   marked block, because an `overflow` never clips a positioned box whose
   containing block is an ancestor of the clipping element; the `.slide`'s own
   `overflow: hidden` clips it instead, at the slide's edge. Paint order is
   unchanged: a fixed box still paints inside `.slide__body`'s stacking context,
   at its own `--tpl-fz`, above the title unless the title is sent to the front
   (slides.css, `slide--title-placed`).

   THE TRAP IT OPENS. Anything between the `.slide` and the block that becomes a
   containing block for fixed descendants captures the block again, measured from
   the wrong box with nothing erroring: `transform`, `translate`, `rotate`,
   `scale`, `perspective`, `filter`, `backdrop-filter`, a `contain` with layout or
   paint in it, `content-visibility`, or a `will-change` naming any of them, on
   `.slide__body` or on the `.tpl` root. (`container-type` is not on the list: a
   container brings size and style containment, never layout, so it is no
   containing block - measured in Chromium, WebKit and Firefox. A second
   container is still banned from this section, for the cqw reason above.) The
   build reveal is the live case: it writes `transform` on a hidden `[data-step]`,
   so a marked root never carries one (`findShapeTraps`). On the block ITSELF any
   of them is harmless, which is why `.tpl-free--rot` below still works.

   ANCHORED, `>` ALL THE WAY DOWN FROM THE SLIDE. The marker counts on the body's
   ROOT only: the element directly under `.slide__body`, which is where every
   16:9 renderer puts it and the only element `isSlideFrame`
   (src/lib/slideFrame.ts) reads. A marked `.tpl` nested inside a wrapper is a
   legacy body to the editor and to every renderer, so it is one here too: its
   blocks stay `absolute` against it. A descendant selector would match it
   anyway and draw a body the editor measures one way from a box the CSS reads
   another way, with nothing erroring. The `:has()` in slides.css is anchored the
   same way, for the same reason.

   (0,5,0), two classes over the rule above, so it wins on weight wherever it
   sits. Nothing else in this file that can reach a placed box writes `position`
   at that weight: the eyebrow and stepper pins carry the same (0,5,0) once their
   `:not(.tpl-free)` is counted, and that `:not()` is exactly why they never
   reach a placed box. A root without the marker never matches, so a legacy body
   renders exactly as it always has. */
.slide--display > .slide__body > .tpl.tpl--slide-frame > .tpl-free {
  position: fixed;
}

/* A PLACED MASCOT KEEPS ITS SHAPE: its height follows its width. The placed-box
   rule already sizes it - its inline-size at (0,3,0) beats `.tpl-mascot`'s
   clamp and the size modifiers at (0,1,0) - and `.tpl-mascot`'s own
   `block-size: auto` gives it the square the 240x240 art is. What would break
   that is the drawn height: a `--tpl-fh` taller than the width is a MINIMUM
   height, and a replaced element stretched past its ratio is a squashed robot.
   (0,4,0), so it outweighs the placed-box rule on this one element and no
   other. The placement classes (`--corner`, `--corner-end`, `--watermark`) are
   a different mechanism for the same figure and do not belong on a placed one:
   they set insets, opacity and pointer-events this section may not undo. */
.slide--display .tpl > .tpl-mascot.tpl-free {
  min-block-size: 0;
}

/* A ROLE THAT CLIPS ITSELF KEEPS CLIPPING WHEN IT IS PLACED. The placed-box rule's
   `overflow: visible` is for a box of text, which must grow down where it can be
   seen. These four are not that: each is `overflow: hidden` by design, because
   what it draws lives inside a rounded frame and only the clip keeps it there -
   the capped card's brand cap, the terminal's title bar, the screenshot's
   picture, the tree's over-long row. Placed, the (0,3,0) `visible` would take
   that away, and the cap and the bar would poke out past the corners of every
   placed or converted card and terminal, with nothing erroring. (0,4,0), so it
   wins for these roles only; `--clip` below still means what it says for every
   other box. It clips at the box's own edge, and the box's height is a minimum,
   so it cannot eat a line the way a fixed height and a clip would. */
.slide--display .tpl > .tpl-free:is(.tpl-card, .tpl-term, .tpl-shot, .tpl-tree) {
  overflow: hidden;
}

/* Opt-in, both of them, so a box that wants neither carries neither side effect:
   a bare font-size here would outweigh every role class at (0,3,0), and `rotate`
   at any value other than `none` establishes a stacking context and a containing
   block. The size is folded through `--tpl-scale` so `tpl--d2` and `tpl--d3`
   still shrink a free slide, which is the rung the overflow ladder relies on. */
.slide--display .tpl > .tpl-free--sized {
  font-size: calc(var(--tpl-ff) * 1cqw * var(--tpl-scale));
}
.slide--display .tpl > .tpl-free--rot {
  rotate: calc(var(--tpl-fr) * 1deg);
}

/* Surface, spacing and ink. Classes, never inline values: a hex in a body
   survives a skin swap and goes invisible on `.tpl--dark`, which redefines
   `--tpl-ink` and `--tpl-surface` above. The brand gradient is a hairline
   (`.tpl-rule`) and is never a fill here. */
.tpl-free--pad          { padding: var(--tpl-pad); }
.tpl-free--boxed        { padding: var(--tpl-pad);
                          border: 1px solid var(--tpl-line);
                          border-radius: var(--tpl-radius); }
.tpl-free--surface      { background: var(--tpl-surface); }
.tpl-free--fill-accent  { background: var(--tpl-accent); }
.tpl-free--ink          { color: var(--tpl-ink); }
.tpl-free--ink-soft     { color: var(--tpl-ink-soft); }
.tpl-free--ink-faint    { color: var(--tpl-ink-faint); }
.tpl-free--accent       { color: var(--tpl-accent); }
.tpl-free--center       { text-align: center; }
.tpl-free--end          { text-align: end; }

/* A PLACED STEP LIST, UNDER ANY ROOT. `.tpl-steps` is styled only as
   `.tpl--steps .tpl-steps`, and that scope is load-bearing where it stands: at
   (0,2,0) it beats `.slide__body ol` (0,1,1), which would otherwise make the
   list a padded flex column. A list placed on another template, or on a bare
   `.tpl`, has no `tpl--steps` root to borrow the weight from, so this restates
   the list's own four declarations at the same weight through `.tpl-free`. The
   rows, numbers and texts are unscoped already. `padding: 0` at (0,2,0)
   outweighs `--pad` and `--boxed` at (0,1,0), so those two are restated on top
   of it rather than lost. */
.tpl-free.tpl-steps {
  list-style: none;
  display: grid;
  gap: calc(0.9cqw * var(--tpl-scale));
  padding: 0;
}

.tpl-free.tpl-steps:is(.tpl-free--pad, .tpl-free--boxed) { padding: var(--tpl-pad); }

/* THE ONE MODIFIER THAT HAS TO BE SCOPED, and the reason is the cascade rather
   than the phone. Every modifier above sets a property the placed-box rule does
   not touch, so a bare class at (0,1,0) is enough. `overflow` is the exception:
   the placed-box rule sets `overflow: visible` at (0,3,0), so a bare
   `.tpl-free--clip` would lose on weight however far down this file it sat, and
   would silently do nothing on every surface that actually places the box. Same
   weight and later in the file is what makes it bite, so KEEP IT BELOW the rule
   it overrides. It also reads correctly on a phone, where nothing places the box
   and clipping a block in a reading column would only lose a line.
   tests/editorDeckFreeCss.spec.ts pins both halves. */
.slide--display .tpl > .tpl-free--clip { overflow: hidden; }
