/* ==========================================================================
   SlideForge
   1. Editor chrome
   2. Slide themes
   3. Slide layouts (shared by editor preview and the live show)
   4. Player / slideshow
   5. Quiz + live
   ========================================================================== */

*, *::before, *::after { box-sizing: border-box; }

:root {
  --ui-bg: #14161a;
  --ui-panel: #1b1e24;
  --ui-panel-2: #22262e;
  --ui-line: #2f343d;
  --ui-text: #e7e9ee;
  --ui-dim: #9aa1ad;
  --ui-accent: #4c8dff;
  --ui-accent-soft: rgba(76, 141, 255, .16);
  --ui-good: #34c98a;
  --ui-bad: #ff5f6d;
  --ui-warn: #ffc75a;
  --ui-radius: 8px;
  --ui-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, system-ui, sans-serif;
}

html, body { height: 100%; margin: 0; }

body {
  font-family: var(--ui-font);
  background: var(--ui-bg);
  color: var(--ui-text);
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
}

button, input, select, textarea { font: inherit; color: inherit; }

/* ------------------------------------------------------------------ 1. chrome */

#app {
  display: grid;
  /* auto, not a fixed 52px: the toolbar wraps to a second row on narrow
     windows rather than clipping Present and Host live off the right edge */
  grid-template-rows: auto 1fr;
  grid-template-columns: 216px 1fr 300px;
  grid-template-areas: 'bar bar bar' 'rail stage inspector';
  height: 100vh;
}

.topbar {
  grid-area: bar;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  row-gap: 6px;
  min-height: 52px;
  padding: 7px 12px;
  background: var(--ui-panel);
  border-bottom: 1px solid var(--ui-line);
}

.brand {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 650;
  letter-spacing: -.01em;
  margin-right: 4px;
  white-space: nowrap;
}
.brand .mark {
  width: 22px; height: 22px;
  border-radius: 5px;
  background: linear-gradient(135deg, #4c8dff, #9b6dff);
  display: grid; place-items: center;
  font-size: 13px; font-weight: 800; color: #fff;
}

#deckTitle {
  background: transparent;
  border: 1px solid transparent;
  border-radius: 6px;
  padding: 6px 8px;
  min-width: 180px;
  font-weight: 550;
}
#deckTitle:hover { border-color: var(--ui-line); }
#deckTitle:focus { outline: none; border-color: var(--ui-accent); background: var(--ui-bg); }

.spacer { flex: 1; }

.btn {
  background: var(--ui-panel-2);
  /* Its own colour, not the container's. A button carries its background with
     it, so it has to carry its text colour too: inheriting put white-on-white
     text on the lobby's buttons, because #lobby sets color:#fff for the PIN
     and the panel colour is near-white under the studio chrome. .primary and
     .live were the only readable ones there, and only because they set a
     colour of their own. */
  color: var(--ui-text);
  border: 1px solid var(--ui-line);
  border-radius: 6px;
  padding: 7px 11px;
  cursor: pointer;
  white-space: nowrap;
  transition: background .12s, border-color .12s, transform .06s;
}
.btn:hover { background: #2b3039; border-color: #3c434e; }
.btn:active { transform: translateY(1px); }
.btn.primary { background: var(--ui-accent); border-color: var(--ui-accent); color: #fff; font-weight: 600; }
.btn.primary:hover { background: #3d7ceb; }
.btn.live { background: #b5273a; border-color: #b5273a; color: #fff; font-weight: 600; }
.btn.live:hover { background: #9c2032; }
.btn.ghost { background: transparent; }
.btn.icon { padding: 6px 9px; }
.btn[disabled] { opacity: .45; cursor: not-allowed; }

select.btn { -webkit-appearance: none; appearance: none; padding-right: 26px;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'><path d='M0 0l5 6 5-6z' fill='%239aa1ad'/></svg>");
  background-repeat: no-repeat; background-position: right 9px center; background-size: 9px; }

/* slide rail ------------------------------------------------------------- */

.rail {
  grid-area: rail;
  background: var(--ui-panel);
  border-right: 1px solid var(--ui-line);
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.rail-head {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 10px 12px;
  font-size: 11px; text-transform: uppercase; letter-spacing: .07em;
  color: var(--ui-dim);
  border-bottom: 1px solid var(--ui-line);
}
/* No `gap` here: the space between two slides is a real element now, because
   that space is what a reorder targets. It is 8px at rest — the gap it
   replaces — and opens up into a click target while a slide is in hand. */
.rail-list { flex: 1; overflow-y: auto; padding: 10px; display: flex; flex-direction: column; }

.rail-slot { flex: none; height: 8px; position: relative; border-radius: 2px; }
.rail-slot.at::after {
  content: ''; position: absolute; left: 2px; right: 2px; top: 50%;
  height: 2px; margin-top: -1px; border-radius: 2px;
  background: var(--ui-accent); outline: 3px solid var(--ui-accent-soft);
}
.rail-list.placing .rail-slot { height: 20px; cursor: copy; }
.rail-list.placing .rail-slot::before {
  content: ''; position: absolute; left: 2px; right: 2px; top: 50%;
  height: 2px; margin-top: -1px; border-radius: 2px;
  background: repeating-linear-gradient(90deg, var(--ui-line) 0 6px, transparent 6px 12px);
}
.rail-list.placing .rail-slot:hover::before,
.rail-list.placing .rail-slot.at::before {
  background: var(--ui-accent); outline: 3px solid var(--ui-accent-soft);
}
.rail-list.placing .rail-slot:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: -2px; }
.rail-list.placing .thumb.carried { opacity: .4; }

/* The band that appears above the rail while a slide is being carried. */
.rail-placing {
  flex: none; display: grid; gap: 3px; padding: 8px 10px;
  border-bottom: 1px solid var(--ui-line);
  background: var(--ui-accent-soft); font-size: 12px;
}
.rail-placing strong { font-size: 12px; }
.rail-placing .rail-placing-what {
  color: var(--ui-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rail-placing .rail-placing-hint { font-size: 11px; color: var(--ui-dim); line-height: 1.5; }
.rail-placing .btn { justify-self: start; margin-top: 2px; padding: 3px 8px; font-size: 11px; }

.thumb {
  position: relative;
  display: flex; gap: 8px; align-items: flex-start;
  cursor: pointer;
  border-radius: 6px;
  padding: 3px;
  border: 2px solid transparent;
}
.thumb:hover { background: var(--ui-panel-2); }
.thumb.sel { border-color: var(--ui-accent); background: var(--ui-accent-soft); }
.thumb-gutter { flex: none; width: 16px; display: flex; flex-direction: column; align-items: center; gap: 3px; padding-top: 4px; }
.thumb .num { font-size: 11px; color: var(--ui-dim); line-height: 1; }
/* Kept out of the way until the row is under the cursor or in play: the rail is
   mostly for reading, and a grip on all forty-nine rows is just noise. */
.thumb-grip {
  width: 16px; height: 16px; padding: 0; border: 0; border-radius: 3px;
  background: none; color: var(--ui-dim);
  font-size: 11px; line-height: 1; cursor: grab; opacity: 0;
}
.thumb:hover .thumb-grip, .thumb.sel .thumb-grip, .thumb-grip:focus-visible { opacity: 1; }
.thumb-grip:hover { color: var(--ui-accent); background: var(--ui-panel-2); }
.thumb-body { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.thumb .frame {
  flex: 1; aspect-ratio: 16/9; border-radius: 4px; overflow: hidden;
  background: #000; position: relative; pointer-events: none;
}
.thumb .frame .slide { transform-origin: 0 0; }
.thumb .badge {
  position: absolute; right: 4px; top: 4px; z-index: 2;
  font-size: 9px; font-weight: 700; padding: 1px 5px; border-radius: 3px;
  background: rgba(0,0,0,.6); color: #fff; letter-spacing: .04em;
}
.thumb .badge.quiz { background: var(--ui-warn); color: #21160a; }
/* An activity names its phase. It sits below the GAME or feedback badge when
   a slide carries both, so neither hides the other, and it takes the tint the
   activities studio uses for that kind — amber for the ones that run in the
   room on a clock, blue for the ones that are a slide. */
.thumb .badge.act {
  top: auto; bottom: 4px; right: 4px;
  background: #e4edf4; color: #2f4d63; font-weight: 800;
}
.thumb .badge.act.timed { background: #fbeedd; color: #8a5416; }
.thumb-tx {
  flex: none;
  align-self: center;
  width: 16px;
  text-align: center;
  font-size: 12px;
  line-height: 1;
  color: var(--ui-dim);
  opacity: .9;
}
.thumb.sel .thumb-tx { color: var(--ui-accent); opacity: 1; }

/* ------------------------------------------------------------ slide sorter

   The whole deck at once, so a move is a short drag with both ends in view. */
body.sorter-on { overflow: hidden; }
.sorter {
  position: fixed; inset: 0; z-index: 210;
  display: flex; flex-direction: column;
  background: var(--ui-bg); color: var(--ui-text);
}
.sorter-head {
  flex: none; display: flex; align-items: center; gap: 12px;
  padding: 12px 18px; border-bottom: 1px solid var(--ui-line);
}
.sorter-head strong { font-size: 14px; }
.sorter-count { font-size: 12px; color: var(--ui-dim); white-space: nowrap; }
.sorter-hint { flex: 1; min-width: 0; font-size: 11px; color: var(--ui-dim); }
.sorter-grid {
  flex: 1; position: relative; overflow-y: auto; align-content: start;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: 16px; padding: 18px;
}
.sorter-tile {
  display: grid; gap: 5px; cursor: pointer;
  padding: 5px; border-radius: 8px; border: 2px solid transparent;
}
.sorter-tile:hover { background: var(--ui-panel-2); }
.sorter-tile.sel { border-color: var(--ui-accent); background: var(--ui-accent-soft); }
.sorter-tile:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: 2px; }
.sorter-tile .frame {
  aspect-ratio: 16/9; border-radius: 5px; overflow: hidden;
  background: #000; position: relative; pointer-events: none;
}
.sorter-tile .frame .slide { transform-origin: 0 0; }
.sorter-foot { display: flex; gap: 6px; align-items: baseline; min-width: 0; }
.sorter-num { flex: none; font-size: 11px; color: var(--ui-dim); font-variant-numeric: tabular-nums; }
.sorter-title {
  flex: 1; min-width: 0; font-size: 11px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* One bar, moved about, rather than a gap element per slide: in a wrapping
   grid an empty gap would still take a cell and shove the layout around. */
.sorter-caret {
  position: absolute; width: 3px; border-radius: 2px; pointer-events: none;
  background: var(--ui-accent); outline: 2px solid var(--ui-accent-soft);
}
/* The quiz studio shares the rail head and has no sorter to open. */
html[data-ws="game"] #btnSorter { display: none; }

.rail-foot { padding: 8px; border-top: 1px solid var(--ui-line); display: flex; flex-direction: column; gap: 8px; flex-shrink: 0; }
.rail-foot .rail-actions { display: flex; flex-wrap: wrap; gap: 6px; }
.rail-foot .rail-actions .btn { flex: 1 1 60px; text-align: center; font-size: 12px; padding: 6px 4px; }
.rail-tx { display: grid; gap: 4px; }
.rail-tx label {
  font-size: 9px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--ui-dim); font-weight: 600;
}
.rail-tx select {
  width: 100%; font-size: 11px; padding: 7px 8px;
  border-radius: 7px; border: 1px solid var(--ui-line);
  background: var(--ui-panel); color: var(--ui-text);
}

/* stage ------------------------------------------------------------------ */

.stage-area {
  grid-area: stage;
  display: flex; flex-direction: column;
  min-width: 0; min-height: 0;
  background:
    radial-gradient(1200px 500px at 50% -10%, #1d2027, var(--ui-bg));
}
.stage-wrap {
  flex: 1; display: grid; padding: 24px; min-height: 0;
  /* Zoomed past Fit the canvas is wider than this area, so it scrolls. The
     `safe` keyword matters: plain `center` on an item bigger than its box
     overflows equally in both directions and puts the slide's top-left
     corner somewhere you cannot scroll back to. */
  align-items: safe center; justify-items: safe center;
  overflow: auto;
  /* A definite column, not the implicit auto one. #previewBox is sized in
     percent, and a percentage against an auto track is circular: the browser
     resolves it once and then keeps that number, so hiding the panel widened
     the stage by 300px and left the canvas exactly where it was. */
  grid-template-columns: minmax(0, 1fr);
}
#previewBox {
  position: relative;
  width: 100%; max-width: 980px;
  aspect-ratio: 16/9;
  border-radius: 10px;
  overflow: hidden;
  outline: 1px solid var(--ui-line);
  background: #000;
}
#previewBox .slide { transform-origin: 0 0; }

/* Controls that sit on the canvas rather than in the inspector. Quiet until
   the pointer is over the preview, so they do not compete with the slide. */
.canvas-btn {
  position: absolute; z-index: 5;
  width: 34px; height: 34px; border-radius: 8px;
  display: grid; place-items: center;
  font-size: 16px; line-height: 1; cursor: pointer;
  color: #fff; background: rgba(12,14,18,.62);
  border: 1px solid rgba(255,255,255,.22);
  opacity: 0; transition: opacity .15s, background .15s;
}
#previewBox:hover .canvas-btn, .canvas-btn:focus-visible { opacity: 1; }
.canvas-btn:hover { background: rgba(12,14,18,.85); }
.canvas-btn.swap-sides { top: 12px; right: 12px; }

/* canvas bar ------------------------------------------------------------- */

.canvas-bar {
  display: flex; align-items: center; gap: 8px;
  padding: 5px 14px;
  border-top: 1px solid var(--ui-line);
  background: var(--ui-panel);
  flex: none;
}
.canvas-zoom { display: flex; align-items: center; gap: 4px; }
.canvas-bar > #btnInspector { margin-left: auto; }
.canvas-bar-btn {
  min-width: 26px; height: 24px; padding: 0 6px;
  display: inline-grid; place-items: center;
  font-size: 13px; line-height: 1; cursor: pointer;
  color: var(--ui-dim); background: transparent;
  border: 1px solid transparent; border-radius: 6px;
}
.canvas-bar-btn.wide { font-size: 12px; gap: 5px; grid-auto-flow: column; }
.canvas-bar-btn:hover { color: var(--ui-text); background: var(--ui-bg); border-color: var(--ui-line); }
.canvas-bar-btn:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: 1px; }
.canvas-bar-btn[aria-pressed="false"] { color: var(--ui-accent); }
.canvas-zoom-level {
  height: 24px; padding: 0 4px;
  font: inherit; font-size: 12px;
  color: var(--ui-dim); background: transparent;
  border: 1px solid transparent; border-radius: 6px;
  cursor: pointer; text-align: center;
}
.canvas-zoom-level:hover { color: var(--ui-text); border-color: var(--ui-line); }
.canvas-zoom-level:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: 1px; }

/* The panel hidden. Specificity (#app + class) beats the plain #app rules in
   the width media queries below, so those do not need their own copy. */
#app.no-inspector { grid-template-columns: 216px 1fr 0; }
#app.no-inspector .inspector { display: none; }

.notes-strip {
  border-top: 1px solid var(--ui-line);
  background: var(--ui-panel);
  padding: 8px 14px 12px;
}
.notes-strip label {
  font-size: 11px; text-transform: uppercase; letter-spacing: .07em;
  color: var(--ui-dim); display: block; margin-bottom: 5px;
}
.notes-strip textarea {
  width: 100%; height: 54px; resize: vertical;
  background: var(--ui-bg); border: 1px solid var(--ui-line);
  border-radius: 6px; padding: 7px 9px; font-size: 13px; line-height: 1.5;
}
.notes-strip textarea:focus { outline: none; border-color: var(--ui-accent); }

/* inspector -------------------------------------------------------------- */

.inspector {
  grid-area: inspector;
  background: var(--ui-panel);
  border-left: 1px solid var(--ui-line);
  overflow-y: auto;
  padding: 14px;
}
.insp-title {
  font-size: 11px; text-transform: uppercase; letter-spacing: .07em;
  color: var(--ui-dim); margin: 0 0 12px;
}
.field { margin-bottom: 13px; }
.field > label {
  display: block; font-size: 12px; color: var(--ui-dim); margin-bottom: 5px;
}
.field input[type=text], .field input[type=number], .field textarea, .field select {
  width: 100%;
  background: var(--ui-bg);
  border: 1px solid var(--ui-line);
  border-radius: 6px;
  padding: 8px 9px;
  font-size: 13px;
}
.field textarea { resize: vertical; min-height: 84px; line-height: 1.55; }
.field input:focus, .field textarea:focus, .field select:focus { outline: none; border-color: var(--ui-accent); }
/* scoped to .hint, not .field .hint: both inspectors append standalone hints
   straight into the panel and they were rendering at body size */
.hint { font-size: 11px; color: var(--ui-dim); margin-top: 5px; line-height: 1.45; }

/* Field help as a rollover — keeps the inspector short. */
.field-tip {
  position: relative;
  display: inline-grid;
  place-items: center;
  width: 15px;
  height: 15px;
  margin-left: 6px;
  border-radius: 50%;
  font-size: 9px;
  font-weight: 700;
  line-height: 1;
  color: #6a7360;
  background: #e8ecdf;
  cursor: help;
  vertical-align: -1px;
  flex: none;
}
.field-tip:focus-visible {
  outline: 2px solid #8a69ae;
  outline-offset: 2px;
}
.field-tip[data-tip]:hover::after,
.field-tip[data-tip]:focus-visible::after {
  content: attr(data-tip);
  position: absolute;
  z-index: 40;
  left: 0;
  top: calc(100% + 6px);
  width: min(240px, 70vw);
  padding: 8px 10px;
  border-radius: 8px;
  background: #2c3a28;
  color: #f4f6f0;
  font-size: 11px;
  font-weight: 400;
  line-height: 1.45;
  white-space: normal;
  text-align: left;
  pointer-events: none;
}

.opt-row { display: flex; align-items: center; gap: 7px; margin-bottom: 7px; }
.opt-row input[type=radio] { flex: none; accent-color: var(--ui-good); width: 16px; height: 16px; }
.opt-row input[type=text] { flex: 1; }
.opt-row .kill {
  flex: none; background: transparent; border: 1px solid var(--ui-line);
  border-radius: 5px; color: var(--ui-dim); cursor: pointer;
  width: 26px; height: 30px; line-height: 1;
}
.opt-row .kill:hover { color: var(--ui-bad); border-color: var(--ui-bad); }

.type-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }
.type-grid-wrap {
  grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
}
.type-grid button {
  background: var(--ui-bg); border: 1px solid var(--ui-line);
  border-radius: 6px; padding: 8px 2px; cursor: pointer;
  font-size: 10px; color: var(--ui-dim);
  display: flex; flex-direction: column; align-items: center; gap: 3px;
}
.type-grid button .g { font-size: 15px; color: var(--ui-text); }
.type-grid button:hover { border-color: var(--ui-accent); }
.type-grid button.on { border-color: var(--ui-accent); background: var(--ui-accent-soft); color: var(--ui-text); }

.switch { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.switch input { width: 16px; height: 16px; accent-color: var(--ui-accent); }

/* ------------------------------------------------------------ 2. themes */

.slide {
  --sf-scale: 1;
  position: absolute; inset: 0;
  width: 1280px; height: var(--slide-h, 720px);
  overflow: hidden;
  font-family: var(--s-font);
  background: var(--s-bg);
  color: var(--s-fg);
  display: flex; flex-direction: column;
}

.theme-midnight {
  --s-bg: linear-gradient(150deg, #16233d 0%, #0d1526 60%, #0a1020 100%);
  --s-fg: #eef2fb;
  --s-dim: #9fb0d0;
  --s-accent: #5b9dff;
  --s-accent-2: #a98bff;
  --s-rule: rgba(255,255,255,.14);
  --s-card: rgba(255,255,255,.06);
  --s-scrim: rgba(0,0,0,.42);
  --s-font: 'Segoe UI', -apple-system, BlinkMacSystemFont, Inter, sans-serif;
}
.theme-paper {
  --s-bg: linear-gradient(160deg, #fbf9f4, #efe9dc);
  --s-fg: #23201b;
  --s-dim: #6c655a;
  --s-accent: #b4531f;
  --s-accent-2: #1f6f5c;
  --s-rule: rgba(0,0,0,.14);
  --s-card: rgba(0,0,0,.045);
  --s-scrim: rgba(255,255,255,.62);
  --s-font: Georgia, 'Iowan Old Style', 'Times New Roman', serif;
}
.theme-ocean {
  --s-bg: linear-gradient(150deg, #0f6a72 0%, #08454c 70%, #05323a 100%);
  --s-fg: #eafcfb;
  --s-dim: #a5d6d6;
  --s-accent: #58e0c8;
  --s-accent-2: #ffd479;
  --s-rule: rgba(255,255,255,.16);
  --s-card: rgba(255,255,255,.08);
  --s-scrim: rgba(0,0,0,.42);
  --s-font: 'Segoe UI', -apple-system, BlinkMacSystemFont, Inter, sans-serif;
}
.theme-ember {
  --s-bg: linear-gradient(150deg, #46202f 0%, #2a1320 65%, #1b0c15 100%);
  --s-fg: #fdeef2;
  --s-dim: #d6a8b6;
  --s-accent: #ff8a5c;
  --s-accent-2: #ffd166;
  --s-rule: rgba(255,255,255,.15);
  --s-card: rgba(255,255,255,.07);
  --s-scrim: rgba(0,0,0,.42);
  --s-font: 'Segoe UI', -apple-system, BlinkMacSystemFont, Inter, sans-serif;
}
.theme-mono {
  --s-bg: #0c0c0c;
  --s-fg: #f2f2f2;
  --s-dim: #a0a0a0;
  --s-accent: #ffffff;
  --s-accent-2: #ffcc00;
  --s-rule: rgba(255,255,255,.2);
  --s-card: rgba(255,255,255,.06);
  --s-scrim: rgba(0,0,0,.5);
  --s-font: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

/* ------------------------------------------------- 3. slide layouts */

.slide .pad { padding: 74px 92px; flex: 1; display: flex; flex-direction: column; min-height: 0; }

.slide h1 { font-size: 76px; line-height: 1.06; margin: 0; font-weight: 700; letter-spacing: -.02em; }
.slide h2 { font-size: 52px; line-height: 1.12; margin: 0 0 34px; font-weight: 650; letter-spacing: -.015em; }
.slide .sub { font-size: 27px; color: var(--s-dim); margin-top: 26px; line-height: 1.35; }

.slide .accent-bar {
  height: 8px; width: 132px; border-radius: 4px; flex: none;
  background: linear-gradient(90deg, var(--s-accent), var(--s-accent-2));
}

/* title */
.layout-title .pad { justify-content: center; }
.layout-title .accent-bar { margin-bottom: 34px; }

/* section */
.layout-section { text-align: center; }
.layout-section .pad { justify-content: center; align-items: center; }
.layout-section h1 { font-size: 90px; }
.layout-section .accent-bar { margin: 34px auto 0; }

/* bullets */
/* ---------------------------------------------------------------- key fact

   One thing set at a size nothing else competes with. Every other bullet
   layout gives its points equal weight, which is right when they are equal and
   wrong for a deadline or a threshold — the slide where the room only has to
   leave with one number. The supporting points are deliberately smaller than
   .layout-content's, because here they are footnotes to the fact above them. */
.layout-keyfact .pad { justify-content: center; }
.layout-keyfact .keyfact {
  border-left: 10px solid var(--s-accent);
  padding: 4px 0 8px 34px;
  margin: 0 0 40px;
}
.layout-keyfact .keyfact-label {
  font-size: 23px; line-height: 1.2; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase;
  color: var(--s-dim); margin-bottom: 16px;
}
.layout-keyfact .keyfact-value {
  font-size: 78px; line-height: 1.05; font-weight: 700; letter-spacing: -.02em;
  white-space: pre-wrap;
}
.layout-keyfact ul.keyfact-notes {
  margin: 0; padding: 0; list-style: none;
  display: flex; flex-direction: column; gap: 18px;
}
.layout-keyfact ul.keyfact-notes li {
  font-size: 26px; line-height: 1.38; color: var(--s-dim);
  padding-left: 34px; position: relative;
}
.layout-keyfact ul.keyfact-notes li::before {
  content: ''; position: absolute; left: 4px; top: .58em;
  width: 9px; height: 9px; border-radius: 50%; background: var(--s-dim);
}
/* A long fact wraps rather than overflowing: better a smaller headline than a
   number sliced off at the edge of the screen. */
.layout-keyfact .keyfact-value:not(:empty) { overflow-wrap: anywhere; }

.layout-content ul { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 24px; }
.layout-content li {
  font-size: 32px; line-height: 1.36;
  padding-left: 42px; position: relative;
}
.layout-content li::before {
  content: ''; position: absolute; left: 4px; top: .5em;
  width: 13px; height: 13px; border-radius: 3px;
  background: var(--s-accent);
}
/* A lead-in runs on from its own point rather than sitting above it — the
   bold part is a signpost inside the sentence, not a heading. */
.layout-content .lead-in { font-weight: 700; }
.layout-content .lead-in + .lead-rest::before { content: ' '; }
.layout-content .lead-rest { opacity: .92; }
.layout-content li.tier-2 { font-size: 26px; margin-left: 46px; color: var(--s-dim); }
.layout-content li.tier-2::before { width: 8px; height: 8px; border-radius: 50%; background: var(--s-dim); top: .55em; }

/* Sparse teaching + live feedback: sit the copy in the middle, not marooned at the top.
   Content and cards already did this; infographic charts stayed pinned to the
   heading so a poll rail made the diagram look like a title card. */
.layout-content.has-feedback .pad,
.layout-cards.has-feedback .pad,
.layout-stats.has-feedback .pad,
.layout-compare.has-feedback .pad,
.layout-funnel.has-feedback .pad,
.layout-timeline.has-feedback .pad,
.layout-chart.has-feedback .pad {
  justify-content: center;
  gap: 28px;
}
.layout-stats.has-feedback .stats-grid,
.layout-compare.has-feedback .compare,
.layout-funnel.has-feedback .funnel,
.layout-timeline.has-feedback .timeline {
  flex: none;
}
.layout-content.has-feedback h2,
.layout-cards.has-feedback h2 {
  text-align: center;
  max-width: 920px;
  margin: 0 auto;
}
.layout-content.has-feedback ul {
  align-items: center;
  gap: 18px;
}
.layout-content.has-feedback li {
  font-size: 36px;
  width: min(720px, 100%);
}

/* Cards: let the column count follow the card count.

   Three columns were hard-coded, so a fourth card started a second row the
   slide has no height for and .slide's overflow:hidden ate it. Silently —
   which is the worst way to lose a card, since the slide still looks finished.
   Four across in one row instead, with the type stepping down to pay for the
   narrower column, and the row centred in the space the shorter block leaves.

   Same shape as the race track's lane-count rules further down: the layout
   reads how much it has been given rather than assuming.

   Five or more cannot be one row at any readable size, so those go back to
   three across and take the second row — affordable because the number tucks
   in beside the text instead of sitting on a line of its own. */
.layout-cards ul:not(.cards-stack):has(> li:nth-child(4)) {
  grid-template-columns: repeat(4, 1fr);
  gap: 16px;
  flex: 1;
  align-content: center;
}
.layout-cards ul:not(.cards-stack):has(> li:nth-child(4)) li {
  font-size: 23px;
  line-height: 1.4;
  padding: 20px;
}

/* Optional picture on each card — same index as the bullet. Four reading
   covers or four discovery plates sit above their copy without fighting the
   number badge. */
.layout-cards ul.has-card-pics > li {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding-top: 16px;
}
.layout-cards ul.has-card-pics > li::before {
  top: 16px;
}
.layout-cards .card-pic {
  flex: none;
  width: 100%;
  aspect-ratio: 3 / 4;
  max-height: 210px;
  border-radius: 10px;
  background: var(--s-card) center / cover no-repeat;
  border: 1px solid color-mix(in srgb, var(--s-fg) 12%, transparent);
}
.layout-cards ul:not(.cards-stack):has(> li:nth-child(4)) .card-pic {
  max-height: 168px;
}
/* Discovery / scientific plates: show the whole figure instead of crop-as-cover. */
.layout-cards ul.has-card-plates .card-pic {
  aspect-ratio: 4 / 3;
  max-height: 148px;
  background-size: contain;
  background-repeat: no-repeat;
}
.layout-cards .card-copy {
  flex: 1;
  min-width: 0;
}
/* Not scoped to .card-copy: that wrapper only exists when the card carries a
   picture, so scoping the block rules to it left every image-less card running
   its label straight into its body — "Plan Define the purpose…" as one line. */
.layout-cards .card-label,
.layout-cards strong.card-label {
  display: block;
  font-weight: 700;
  margin-bottom: 6px;
}
.layout-cards .card-body {
  display: block;
  opacity: .92;
}
.layout-cards ul:not(.cards-stack):has(> li:nth-child(5)) {
  grid-template-columns: repeat(3, 1fr);
}
.layout-cards ul:not(.cards-stack):has(> li:nth-child(5)) li::before {
  display: inline-block;
  margin: 0 9px 0 0;
}

/* Seven and eight go four wide over two rows. Past eight a card slide has a
   content problem that no grid can fix, and shrinking the type far enough to
   fit would only make it unreadable rather than unread — so the ladder stops
   here deliberately. */
.layout-cards ul:not(.cards-stack):has(> li:nth-child(7)) {
  grid-template-columns: repeat(4, 1fr);
  gap: 14px;
}
.layout-cards ul:not(.cards-stack):has(> li:nth-child(7)) li {
  font-size: 19px;
  line-height: 1.32;
  padding: 15px;
  gap: 9px;
}

/* ---- stacked cards ------------------------------------------------------ */

/* One card at a time, in one place, with the pile of ones already made
   showing behind it. The grid puts every card in the same cell; the transform
   fans the finished ones up and back. Needs a build to mean anything, so
   choosing it turns Build on Next on. */
/* Rows: one card per line, full width, as tall as its own words need. The
   choice is about the shape of the content — three labelled definitions want
   width to read along, three short phrases are happier side by side. */
.layout-cards ul:not(.cards-stack).cards-rows {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 16px;
  grid-template-columns: none;
}
.layout-cards ul:not(.cards-stack).cards-rows > li { width: 100%; }

.layout-cards ul.cards-stack {
  display: grid;
  grid-template-columns: minmax(0, 640px);
  place-content: center;
  justify-content: center;
  flex: 1;
  gap: 0;
  perspective: 1400px;
}
.layout-cards ul.cards-stack > li {
  grid-area: 1 / 1;
  width: 640px;
  max-width: 100%;
  font-size: 27px;
  transition: transform .5s cubic-bezier(.2,.75,.2,1), opacity .3s ease;
}
/* Each finished card sits one notch further up, back and over. --depth is
   published by the reveal driver and counts steps behind the current one. */
.layout-cards ul.cards-stack > li.step-past {
  transform:
    translate(calc(var(--depth, 0) * -24px), calc(var(--depth, 0) * -18px))
    scale(calc(1 - var(--depth, 0) * 0.045))
    rotate(calc(var(--depth, 0) * -1.4deg));
}
/* Arrives from below the pile rather than sliding down into it. */
.layout-cards ul.cards-stack > li.step-hidden { transform: translateY(34px) scale(.95); }

/* A stack is read one card at a time, so the type can be bigger than it is in
   a row of four — and the fourth card no longer has to fit beside three
   others, which is what forced 23px in the first place. */
.layout-cards ul.cards-stack > li {
  line-height: 1.45;
  padding: 30px;
}

/* ---- charts ------------------------------------------------------------- */

/* Six categorical slots, derived from Northeastern London's hues and stepped
   until they passed every check in scripts/validate_palette.js: OKLCH
   lightness band, chroma floor, CVD separation under protanopia and
   deuteranopia, a normal-vision floor, and 3:1 against the surface.
   Light: worst adjacent pair ΔE 10.3 (deutan), 22.7 normal, all >= 3:1.
   Dark:  worst adjacent pair ΔE  9.6 (deutan), 22.3 normal, all >= 3:1.

   The order is the safety mechanism, so it is fixed and never cycled. The
   raw brand accents do NOT pass — navy reads gray at C 0.073 and gold sits
   above the lightness band — which is why these are stepped versions of the
   same hues rather than the swatches themselves. Re-run the validator before
   changing any value here. */
.slide {
  --chart-1: #BA0428;  /* red    */
  --chart-2: #049F6C;  /* sage   */
  --chart-3: #DF5904;  /* orange */
  --chart-4: #0C97B6;  /* sky    */
  --chart-5: #8B6404;  /* gold   */
  --chart-6: #006EB8;  /* navy   */
}
/* Dark grounds take their own steps from the same hues, validated against a
   dark surface — not an automatic lightening of the light set. */
/* Every dark ground, from the theme's own `ground: 'dark'` in THEMES rather
   than from a list of names kept by hand. cinematic and brutal are the
   darkest grounds in the app and were missing from the list this replaces, so
   both were drawing from the light-ground steps above — brutal's worst series
   at 2.82:1, under the 3:1 floor for non-text graphics.

   The UKBT carve-out survives without being maintained. Five of the twelve
   combinations of these steps against its two grounds fall under 3:1, so it
   carries the brand's own highlight colours instead — and because
   css/ukbt.css loads after this file, those simply win. A dark theme belongs
   on these steps unless it can beat them, and beating them now means
   declaring better ones rather than being left off a list. */
.slide[data-ground="dark"] {
  --chart-1: #CA042C;
  --chart-2: #049F6C;
  --chart-3: #B84701;
  --chart-4: #1198B8;
  --chart-5: #A77804;
  --chart-6: #008DEB;
}

.layout-chart .pad { padding: 56px 60px 40px; }
.layout-chart h2 { margin-bottom: 18px; }
.chart-wrap { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; }
.chart-svg { width: 100%; height: 100%; overflow: visible; }

/* Recessive: hairline, solid, one step off the surface. Never dashed. */
.ch-grid { stroke: var(--s-rule); stroke-width: 1; }
.ch-axis { stroke: var(--s-rule); stroke-width: 1.5; }
/* Text wears text tokens, never the series colour. */
.ch-tick, .ch-cat { fill: var(--s-dim); font-size: 19px; }
.ch-value { fill: var(--s-fg); font-size: 20px; font-weight: 650; }
/* Pictogram icons are glyphs, and an SVG <text> with no fill is black — which
   is the look on a light ground and invisible on a dark one. They take the
   text colour rather than a series colour: there is only ever one series in
   this form, so a palette step would say nothing, and ISOTYPE draws its
   figures in ink. */
.ch-icon { fill: var(--s-fg); }
.ch-end { fill: var(--s-dim); font-size: 19px; font-weight: 600; }
.ch-slice-label { fill: var(--s-fg); font-size: 19px; font-weight: 600; }
/* The 2px surface ring that keeps a marker legible where two lines cross. */
.ch-dot { stroke: var(--s-bg); stroke-width: 2; }
.ch-wedge { stroke: var(--s-bg); stroke-width: 2; }

/* Legend — always present for two or more series, so identity never depends
   on colour-matching alone. */
.chart-key {
  flex: none; display: flex; flex-wrap: wrap; justify-content: center;
  gap: 10px 28px; margin-top: 14px;
}
.ck-item { display: inline-flex; align-items: center; gap: 9px; font-size: 21px; color: var(--s-dim); }
.ck-dot { width: 15px; height: 15px; border-radius: 3px; flex: none; }

/* The same numbers, for a screen reader and for anyone the hues fail. */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ---- image stack -------------------------------------------------------- */

/* Same idea as the stacked cards, with a picture instead of a paragraph: one
   figure in front, the ones already shown fanned up and back behind it. */
.layout-gallery .pad { padding: 56px 92px 44px; }
.layout-gallery .fig-stack {
  flex: 1;
  display: grid;
  /* Both tracks bounded, so a figure is sized by the space left after the
     heading rather than by its own content. Left to size itself, the picture
     took the full height and shoved its caption off the bottom of the slide. */
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
  justify-items: center;
  align-items: center;
  min-height: 0;
  perspective: 1600px;
}
.layout-gallery .fig {
  grid-area: 1 / 1;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 16px;
  max-width: 100%;
  max-height: 100%;
  min-height: 0;
  transition: transform .5s cubic-bezier(.2,.75,.2,1), opacity .3s ease;
}
.layout-gallery .fig .img {
  /* Shrinks to leave the caption its room; the ratio then sets the width. */
  flex: 1 1 auto;
  min-height: 0;
  height: auto;
  width: auto;
  max-width: 100%;
  aspect-ratio: var(--img-ar, 16 / 9);
  background-position: center;
  background-repeat: no-repeat;
  border-radius: 3px;
  /* The pile only reads as a pile if each sheet has an edge. */
  outline: 1px solid rgba(0,0,0,.18);
}
.layout-gallery .fig .img.cover { background-size: cover; }
.layout-gallery .fig .img.contain { background-size: contain; background-color: var(--s-card); }

.layout-gallery .fig-cap {
  flex: none;
  text-align: center;
  color: var(--s-fg);
  max-width: 100%;
}
.layout-gallery .fig-cap .cap-line { font-size: 30px; font-weight: 650; line-height: 1.2; }
.layout-gallery .fig-cap .cap-credit {
  margin-top: 8px; font-size: 16px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--s-dim);
}
.layout-gallery .fig-cap.cap-bar {
  background: var(--s-accent); color: #fff; padding: 14px 26px; border-radius: 2px;
}
.layout-gallery .fig-cap.cap-bar .cap-credit { color: #fff; opacity: .85; }
.layout-gallery .fig-cap.cap-none { display: none; }
.layout-gallery .fig-cap.cap-top { order: -1; }

/* Behind: each finished figure one notch further up, back and over. Its
   caption goes with it — a caption from three pictures ago floating over the
   current one would be worse than no caption at all. */
.layout-gallery .fig.step-past {
  transform:
    translate(calc(var(--depth, 0) * -26px), calc(var(--depth, 0) * -20px))
    scale(calc(1 - var(--depth, 0) * 0.05))
    rotate(calc(var(--depth, 0) * -1.3deg));
}
/* Only the figure on top of the pile shows its caption — a figure is covered
   when any later one is not still hidden. Stating it that way rather than
   keying on .step-past covers the static render too: a gallery with the build
   switched off, an editor preview, a rail thumbnail and the presenter's
   next-slide pane all used to stack every caption on top of the others,
   because .step-past is only ever applied by a running build. */
.layout-gallery .fig:has(~ .fig:not(.step-hidden)) .fig-cap { opacity: 0; }
.layout-gallery .fig.step-hidden { transform: translateY(36px) scale(.95); }

/* Full bleed gallery — no imageFrame. Picture fills the slide; caption sits
   on it the same way as a full-bleed image slide. The stack still fans past
   layers so build-on-next keeps its pile. */
.layout-gallery .pad.img-bleed {
  padding: 0;
  position: relative;
}
.layout-gallery .pad.img-bleed .fig-stack {
  position: absolute;
  inset: 0;
  display: block;
  perspective: 1600px;
}
.layout-gallery .pad.img-bleed .fig {
  position: absolute;
  inset: 0;
  max-width: none;
  max-height: none;
  gap: 0;
}
.layout-gallery .pad.img-bleed .fig .img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  aspect-ratio: auto;
  border-radius: 0;
  outline: 0;
}
.layout-gallery .pad.img-bleed .fig .img.contain {
  background-color: #0b1220;
}
.layout-gallery .pad.img-bleed .fig-cap {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 2;
  max-width: none;
  text-align: left;
  padding: 100px 92px 56px;
  color: #fff;
  pointer-events: none;
}
.layout-gallery .pad.img-bleed .fig-cap .cap-line {
  font-size: 44px;
  font-weight: 650;
  line-height: 1.14;
}
.layout-gallery .pad.img-bleed .fig-cap .cap-credit {
  margin-top: 12px;
  font-size: 19px;
  font-weight: 500;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: #fff;
  opacity: .82;
}
.layout-gallery .pad.img-bleed .fig-cap.cap-scrim {
  background: linear-gradient(transparent, rgba(0,0,0,.82));
}
.layout-gallery .pad.img-bleed .fig-cap.cap-bar {
  padding: 34px 92px 38px;
  background: var(--s-accent);
  border-radius: 0;
}
.layout-gallery .pad.img-bleed .fig-cap.cap-bar .cap-credit {
  color: #fff;
  opacity: .85;
}
.layout-gallery .pad.img-bleed .fig-cap.cap-plain {
  background: none;
  text-shadow: 0 2px 18px rgba(0,0,0,.75), 0 1px 2px rgba(0,0,0,.9);
}
.layout-gallery .pad.img-bleed .fig-cap.cap-top {
  top: 0;
  bottom: auto;
  order: 0;
}
.layout-gallery .pad.img-bleed .fig-cap.cap-scrim.cap-top {
  background: linear-gradient(rgba(0,0,0,.82), transparent);
  padding: 56px 92px 100px;
}

/* keywords — bold term, lowercase definition */
/* The worked answer, revealed after the room has had its go. Set apart from
   the task above it rather than styled as another row, because it answers the
   slide instead of continuing it. */
.model-answer {
  margin-top: 26px;
  padding: 20px 26px;
  border-left: 8px solid var(--s-accent);
  background: color-mix(in srgb, var(--s-accent) 9%, transparent);
  border-radius: 0 8px 8px 0;
}
.model-answer-label {
  font-size: 17px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  color: var(--s-dim); margin-bottom: 8px;
}
.model-answer-body { font-size: 25px; line-height: 1.38; }

/* A model answer is a second block on a slide that already carries a task, so
   the task tightens to make room for it. Without this the list — which can
   flex-shrink — was squeezed to half its content height and the rows printed
   straight over the answer, which looks like a rendering fault rather than a
   slide with too much on it. `flex: none` is the part that matters: better an
   honest overflow than silent overlap. */
/* The card: task on the front, worked answer on the back. It behaves the same
   in the editor as in the show, because the image-facts card next door does,
   and a control that means two different things in two places is worse than no
   control. Editing the answer is the inspector's job either way. */
.flip { position: relative; perspective: 1800px; min-height: 0; }
.flip-face {
  min-height: 0;
  backface-visibility: hidden;
  transition: transform .5s cubic-bezier(.3, 0, .2, 1), opacity .5s;
}
.flip-back {
  position: absolute; inset: 0;
  transform: rotateY(180deg); opacity: 0;
  display: flex; align-items: center;
}
.flipped .flip-front { transform: rotateY(180deg); opacity: 0; }
.flipped .flip-back { transform: rotateY(360deg); opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .flip-face { transition-duration: 1ms; }
}

/* Same corner, same size, same shape as .image-facts-toggle. One flip control
   in the app, not two that look like different features. */
.flip-toggle {
  position: absolute;
  left: 28px;
  top: 24px;
  z-index: 9;
  width: 44px;
  height: 44px;
  margin: 0;
  padding: 0;
  display: grid;
  place-items: center;
  border-radius: 8px;
  border: 1px solid color-mix(in srgb, var(--s-fg) 22%, transparent);
  background: color-mix(in srgb, var(--s-bg) 82%, transparent);
  color: var(--s-fg);
  font: 600 22px/1 system-ui, sans-serif;
  cursor: pointer;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}
.flip-toggle:hover {
  background: var(--s-accent);
  border-color: var(--s-accent);
  color: #fff;
}
.flip-toggle:focus-visible { outline: 3px solid var(--s-accent); outline-offset: 3px; }
.flip-toggle[aria-expanded="true"] {
  background: var(--s-accent);
  border-color: var(--s-accent);
  color: #fff;
}

/* Turned over, the answer is the only thing on the slide, so it gets the slide.
   No compaction anywhere: the two faces never share the height. */
.flip-back .model-answer {
  width: 100%;
  padding: 34px 44px;
  border-left-width: 12px;
  border-radius: 0 14px 14px 0;
}
/* A draft never reaches the room: it is library copy about another subject
   until the author has made it theirs. Loud in the editor, absent in the show. */
#player .model-answer-draft { display: none; }
.model-answer-draft {
  margin-top: 22px;
  border-left-style: dashed;
  border-left-color: var(--s-dim);
  background: color-mix(in srgb, var(--s-dim) 10%, transparent);
}
.model-answer-draft .model-answer-label { color: var(--s-dim); }
.model-answer-draft .model-answer-body { font-size: 24px; opacity: .75; }

/* An activity that needs things in the room says so before it is chosen. */
.activity-kit {
  display: block;
  margin-top: 6px;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: .06em;
  line-height: 1.35;
  color: #8a5416;
}
.activity-card.needs-kit { border-color: color-mix(in srgb, #8a5416 35%, transparent); }

.model-answer-label { font-size: 22px; margin-bottom: 18px; }
.model-answer-body { font-size: 40px; line-height: 1.36; }


.layout-keywords .kw-list {
  display: flex; flex-direction: column; gap: 22px; margin: 0; min-height: 0;
}
.layout-keywords .kw-row {
  display: grid;
  grid-template-columns: minmax(140px, 0.38fr) minmax(0, 1fr);
  gap: 28px;
  align-items: baseline;
  padding-bottom: 18px;
  border-bottom: 1px solid var(--s-rule);
}
.layout-keywords .kw-row:last-child { border-bottom: 0; padding-bottom: 0; }
.layout-keywords .kw-term {
  font-size: 34px;
  font-weight: 700;
  letter-spacing: -.02em;
  line-height: 1.2;
}
.layout-keywords .kw-def {
  font-size: 26px;
  font-weight: 400;
  line-height: 1.4;
  color: var(--s-dim);
  text-transform: lowercase;
}
.layout-keywords .kw-row.dim .kw-term,
.layout-keywords .kw-row.dim .kw-def { opacity: .55; font-style: italic; font-weight: 500; }

/* italics — emphasised phrase + plain note */
.layout-italics .it-list {
  display: flex; flex-direction: column; gap: 22px; margin: 0; min-height: 0;
}
.layout-italics .it-row {
  display: grid;
  grid-template-columns: minmax(160px, 0.42fr) minmax(0, 1fr);
  gap: 28px;
  align-items: baseline;
  padding-bottom: 18px;
  border-bottom: 1px solid var(--s-rule);
}
.layout-italics .it-row:last-child { border-bottom: 0; padding-bottom: 0; }
.layout-italics .it-phrase {
  font-size: 34px;
  font-weight: 500;
  font-style: italic;
  letter-spacing: -.015em;
  line-height: 1.25;
}
.layout-italics .it-note {
  font-size: 26px;
  font-weight: 400;
  font-style: normal;
  line-height: 1.4;
  color: var(--s-dim);
}
.layout-italics .it-row.dim .it-phrase,
.layout-italics .it-row.dim .it-note { opacity: .55; }

/* links — text on top, clickable URL below */
.layout-links .ln-list {
  display: flex; flex-direction: column; gap: 28px; margin: 0; min-height: 0;
}
.layout-links .ln-row {
  display: flex; flex-direction: column; gap: 10px;
  padding-bottom: 20px;
  border-bottom: 1px solid var(--s-rule);
}
.layout-links .ln-row:last-child { border-bottom: 0; padding-bottom: 0; }
.layout-links .ln-label {
  font-size: 34px;
  font-weight: 650;
  line-height: 1.2;
  letter-spacing: -.015em;
  color: var(--s-fg);
}
.layout-links .ln-link {
  font-size: 22px;
  font-weight: 500;
  line-height: 1.35;
  color: var(--s-accent);
  text-decoration: underline;
  text-underline-offset: 5px;
  word-break: break-all;
  pointer-events: auto;
  cursor: pointer;
}
.layout-links .ln-link:hover { color: var(--s-accent-2, var(--s-accent)); }
.layout-links .ln-url {
  font-size: 22px;
  color: var(--s-dim);
  word-break: break-all;
}
.layout-links .ln-url.bad { color: var(--s-accent); opacity: .85; }
.layout-links .ln-row.dim { opacity: .55; font-style: italic; }

/* Sparse link slides fill the wall; dense lists stay readable. */
.layout-links .ln-list.ln-hero {
  flex: 1; justify-content: center; gap: 36px;
}
.layout-links .ln-list.ln-hero .ln-label { font-size: 52px; line-height: 1.15; }
.layout-links .ln-list.ln-hero .ln-link,
.layout-links .ln-list.ln-hero .ln-url { font-size: 36px; line-height: 1.3; }
.layout-links .ln-list.ln-sparse { gap: 40px; margin-top: 12px; }
.layout-links .ln-list.ln-sparse .ln-label { font-size: 44px; }
.layout-links .ln-list.ln-sparse .ln-link,
.layout-links .ln-list.ln-sparse .ln-url { font-size: 28px; }
.layout-links .ln-list.ln-dense { gap: 16px; }
.layout-links .ln-list.ln-dense .ln-row { padding-bottom: 12px; gap: 6px; }
.layout-links .ln-list.ln-dense .ln-label { font-size: 28px; }
.layout-links .ln-list.ln-dense .ln-link,
.layout-links .ln-list.ln-dense .ln-url { font-size: 18px; }

/* quote */
.layout-quote .pad { justify-content: center; }
.layout-quote .q {
  font-size: 46px; line-height: 1.32; font-weight: 500; font-style: italic;
  position: relative; padding-left: 46px;
  border-left: 6px solid var(--s-accent);
}
.layout-quote .q-build .q-line + .q-line { margin-top: .35em; }
.layout-quote .attrib { margin-top: 32px; padding-left: 52px; font-size: 24px; color: var(--s-dim); }
.layout-quote .attrib::before { content: '— '; }

/* Join QR & PIN — full-bleed arrival screen for phones. */
.layout-join .pad { justify-content: center; align-items: center; }
.layout-join .join-stage {
  width: min(1080px, 100%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 22px;
  text-align: center;
}
.layout-join .join-title {
  font-size: 48px;
  line-height: 1.15;
  letter-spacing: -1px;
  margin: 0;
  max-width: 920px;
  white-space: pre-line;
}
.layout-join .join-sub {
  font-size: 22px;
  color: var(--s-dim);
  max-width: 820px;
  line-height: 1.45;
}
.layout-join .join-bullets {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 10px 18px;
  justify-content: center;
  max-width: 900px;
}
.layout-join .join-bullets li {
  font-size: 20px;
  color: var(--s-fg);
  background: color-mix(in srgb, var(--s-fg) 8%, transparent);
  border: 1px solid color-mix(in srgb, var(--s-fg) 14%, transparent);
  border-radius: 999px;
  padding: 8px 16px;
}
.layout-join .join-board {
  display: flex;
  align-items: center;
  gap: 36px;
  margin-top: 8px;
}
.layout-join .join-qr {
  width: 280px;
  height: 280px;
  background: #fff;
  border-radius: 18px;
  padding: 14px;
  box-sizing: border-box;
  border: 1px solid rgba(0,0,0,.08);
}
.layout-join .join-qr svg { width: 100%; height: 100%; display: block; }
.layout-join .join-side { text-align: left; min-width: 280px; }
.layout-join .join-lead {
  font-size: 14px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--s-dim);
}
.layout-join .join-lead.pin-lead { margin-top: 22px; }
.layout-join .join-url {
  font-size: 28px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  margin-top: 6px;
  color: var(--s-fg);
}
.layout-join .join-pin {
  font-size: 72px;
  font-weight: 700;
  letter-spacing: .12em;
  line-height: 1;
  margin-top: 4px;
  color: var(--s-fg);
}
.deck-viewport.railed .layout-join .join-title { font-size: 36px; }
.deck-viewport.railed .layout-join .join-qr { width: 210px; height: 210px; }
.deck-viewport.railed .layout-join .join-pin { font-size: 56px; }
#previewBox.railed .layout-join .join-title { font-size: 36px; }
#previewBox.railed .layout-join .join-qr { width: 210px; height: 210px; }
#previewBox.railed .layout-join .join-pin { font-size: 56px; }

/* image */
.layout-image .pad { padding: 0; position: relative; overflow: hidden; }
.layout-image .img { position: absolute; inset: 0; background-position: center; background-repeat: no-repeat; }
.layout-image .img.cover { background-size: cover; }
.layout-image .img.contain { background-size: contain; }

/* Slow Ken Burns zoom — projector only, so the editor preview does not keep
   restarting the motion on every redraw. Origin comes from Image focus. */
@keyframes sf-img-zoom {
  from { transform: scale(1); }
  to { transform: scale(1.12); }
}
#player .layout-image .img.img-motion-zoom {
  animation: sf-img-zoom 24s ease-out forwards;
  transform-origin: var(--img-fx, 50%) var(--img-fy, 50%);
  will-change: transform;
}
@media (prefers-reduced-motion: reduce) {
  #player .layout-image .img.img-motion-zoom { animation: none; }
}

/* video — the image layout's frame, with a real player in it.
   The caption rules are shared with .layout-image rather than copied: they
   are the same overlay doing the same job, and the last time a caption was
   styled twice in this file the two drifted apart. */
.layout-video .pad { padding: 0; position: relative; }
.layout-video .vid {
  position: absolute; inset: 0; width: 100%; height: 100%;
  background: #000; border: 0; display: block;
}
/* object-fit means nothing to an iframe; the embedded player letterboxes the
   clip itself, which is what we want either way. */
.layout-video iframe.vid-embed { object-fit: unset; background: #000; }
/* A label on the still, not a line across it: centred at bottom 18% it ran
   straight through the caption, and over a real thumbnail --s-dim is often
   invisible. A pill in the top corner sits clear of both. */
.layout-video .vid-embed-note {
  position: absolute; left: 22px; top: 22px; right: auto; bottom: auto;
  padding: 7px 12px; border-radius: 999px;
  background: rgba(0, 0, 0, .62); color: #fff;
  font-size: 15px; letter-spacing: .08em; text-transform: uppercase;
  font-weight: 650;
}
.layout-video .vid.cover { object-fit: cover; }
.layout-video .vid.contain { object-fit: contain; }
/* Controls are the one thing on a slide a presenter is meant to click. */
.layout-video .vid { pointer-events: auto; }
.layout-video .img { position: absolute; inset: 0; background-position: center; background-repeat: no-repeat; }
.layout-video .img.cover { background-size: cover; }
.layout-video .img.contain { background-size: contain; }
.layout-video .img.vid-blank { background: var(--s-card, rgba(255,255,255,.06)); }
.layout-video .vid-badge {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 84px; height: 84px; border-radius: 50%;
  display: grid; place-items: center;
  font-size: 30px; line-height: 1; padding-left: 6px;
  color: var(--s-fg);
  background: var(--s-scrim, var(--s-card, rgba(0,0,0,.5)));
  border: 3px solid var(--s-rule);
}
.layout-video .cap,
.layout-image .cap {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 100px 92px 56px; font-size: 44px; font-weight: 650;
  background: linear-gradient(transparent, rgba(0,0,0,.82));
  color: #fff;
  /* Click-through. The caption covers the bottom 208px of the slide, which on
     a video is exactly where the play button and the scrubber are: measured
     with elementFromPoint, the caption was the topmost element at both, so a
     presenter could not start or seek the clip at all. Nothing in a caption
     is ever clicked, so it should never be the thing that catches a click. */
  pointer-events: none;
}
/* And it has to clear the control bar as well as let clicks past it, or the
   text sits on top of the scrubber and neither is readable. */
.layout-video .cap { padding-bottom: 118px; }
/* Only a caption at the bottom is in the controls' way — the shared .cap-top
   rules below set the padding for one at the top. */

/* ---- caption treatments ------------------------------------------------- */

/* The caption is a box now rather than a line of text, because a credit sits
   under the caption proper: "says who?" belongs with the claim. */
.layout-video .cap-line,
.layout-image .cap-line { font-size: 44px; font-weight: 650; line-height: 1.14; }
.layout-video .cap-credit,
.layout-image .cap-credit,
.split-media .cap-credit {
  margin-top: 12px;
  font-size: 19px; font-weight: 500; letter-spacing: .06em;
  text-transform: uppercase;
  opacity: .82;
}
.layout-video .cap-line:only-child,
.layout-image .cap-line:only-child { margin: 0; }

/* Scrim: the default. A gradient reads over any photograph, which is why it
   is what an author gets without choosing. */
.layout-video .cap-scrim,
.layout-image .cap-scrim { background: linear-gradient(transparent, rgba(0,0,0,.82)); }

/* A caption that gets out of the way. Scoped to the player because in the
   editor a caption that disappeared while you were writing it would read as a
   bug, and the presenter window keeps its copy so the speaker can still see
   what the room is looking at. `:not(.step-hidden)` makes a build behave: the
   clock starts when the caption is revealed, not when the slide is drawn. */
@keyframes sf-cap-clear { to { opacity: 0; visibility: hidden; } }
#player .layout-image .cap.cap-fade:not(.step-hidden) {
  animation: sf-cap-clear 900ms ease var(--sf-cap-fade, 10s) forwards;
}
@media (prefers-reduced-motion: reduce) {
  #player .layout-image .cap.cap-fade:not(.step-hidden) { animation-duration: 1ms; }
}

/* Bar: a solid band in the deck's own accent. Crisper than a gradient where
   the image runs to a hard edge — charts, screenshots, diagrams. */
.layout-video .cap-bar,
.layout-image .cap-bar {
  padding: 34px 92px 38px;
  background: var(--s-accent);
  color: #fff;
}
.layout-video .cap-bar .cap-line,
.layout-image .cap-bar .cap-line { font-size: 38px; }

/* Plain: no ground at all, for an image with room already in it. The shadow
   is the only thing keeping it legible, so it is heavier than it looks. */
.layout-video .cap-plain,
.layout-image .cap-plain {
  background: none;
  padding: 56px 92px;
  text-shadow: 0 2px 18px rgba(0,0,0,.85), 0 1px 4px rgba(0,0,0,.7);
}

.layout-video .cap-none,
.layout-image .cap-none { display: none; }

/* ---- framed images ------------------------------------------------------ */

/* Out of the absolute full-bleed layout and into ordinary flow: the picture
   takes the shape it was given, the caption sits under it on the slide's own
   ground, and nothing of the image is covered. Everything above still applies
   to a full-bleed image, which is the default. */
.layout-image .pad.img-framed {
  padding: 56px 92px 48px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 26px;
}
.layout-image .pad.img-framed .img {
  position: static;
  inset: auto;
  flex: 0 1 auto;
  height: 100%;
  width: auto;
  max-width: 100%;
  aspect-ratio: var(--img-ar, 16 / 9);
  border-radius: 3px;
}
/* A framed caption is type on the slide, not an overlay on a photograph, so
   it takes the theme's own ink and drops the scrim the overlay needed. */
.layout-image .pad.img-framed .cap {
  position: static;
  flex: none;
  padding: 0;
  max-width: 100%;
  text-align: center;
  color: var(--s-fg);
  background: none;
  text-shadow: none;
}
.layout-image .pad.img-framed .cap-line { font-size: 34px; font-weight: 650; }
.layout-image .pad.img-framed .cap-credit { color: var(--s-dim); opacity: 1; font-size: 17px; }
/* The accent bar survives framing — it just becomes a band under the picture
   the width of the picture, rather than a strip across the slide. */
.layout-image .pad.img-framed .cap-bar {
  padding: 18px 30px;
  background: var(--s-accent);
  color: #fff;
}
.layout-image .pad.img-framed .cap-bar .cap-credit { color: #fff; opacity: .85; }
.layout-image .pad.img-framed .cap-top { order: -1; }

/* A top bar lands exactly where the deck logo sits. Move the logo to the
   opposite corner rather than recolouring it — a deck's logo may be light or
   dark, and inverting it would fix one and ruin the other. */
.layout-image:has(.cap-bar.cap-top) .slide-logo,
.layout-image:has(.cap-plain.cap-top) .slide-logo {
  top: auto;
  bottom: 26px;
  /* Bottom-left, not bottom-right: the slide number already owns that corner
     and the two sat on top of each other on the first attempt. */
  right: auto;
  left: 36px;
}

/* Top: for an image whose subject sits low, or a clip whose motion is in the
   lower half. Flips the gradient with it. */
.layout-video .cap-top,
.layout-image .cap-top {
  top: 0; bottom: auto;
  padding: 56px 92px 100px;
}
.layout-video .cap-scrim.cap-top,
.layout-image .cap-scrim.cap-top { background: linear-gradient(rgba(0,0,0,.82), transparent); }
.layout-video .cap-bar.cap-top,
.layout-video .cap-plain.cap-top,
.layout-image .cap-bar.cap-top,
.layout-image .cap-plain.cap-top { padding: 38px 92px 34px; }

/* A split image credits its source in the corner of the picture, small. The
   heading is already doing the talking on that slide. */
.split-media { position: relative; }
/* Edge to edge: no mount, no inset, no tinted ground. The picture gets the
   whole half it was given. */
.split-media.split-media-full { background: none; }
.split-media.split-media-full .img { inset: 0; border-radius: 0; box-shadow: none; }
.split-media .cap {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 44px 28px 16px;
  color: #fff;
  pointer-events: none;
}
.split-media .cap-scrim { background: linear-gradient(transparent, rgba(0,0,0,.78)); }
.split-media .cap-bar { background: var(--s-accent); padding: 12px 28px; }
.split-media .cap-plain { background: none; text-shadow: 0 2px 14px rgba(0,0,0,.85); }
.split-media .cap-none { display: none; }
.split-media .cap-credit { margin: 0; font-size: 15px; }
.layout-video .empty,
.layout-image .empty {
  position: absolute; inset: 0; display: grid; place-items: center; align-content: center;
  color: var(--s-dim); font-size: 24px; gap: 10px;
  border: 3px dashed var(--s-rule); margin: 60px; border-radius: 16px;
  text-align: center; padding: 30px;
}

/* table */
.layout-table .tbl {
  width: 100%; border-collapse: collapse; table-layout: fixed;
  font-size: 27px; line-height: 1.3;
}
.layout-table .tbl th {
  text-align: left; padding: 0 20px 14px 0;
  font-size: .68em; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
  color: var(--s-dim);
  border-bottom: 3px solid var(--s-accent);
  vertical-align: bottom;
}
.layout-table .tbl td {
  padding: 16px 20px 16px 0;
  color: var(--s-fg);
  border-bottom: 1px solid var(--s-rule);
  vertical-align: top;
}
.layout-table .tbl tbody tr:last-child td { border-bottom: 0; }
.layout-table .tbl td.lead { font-weight: 650; }
.layout-table .tbl th:last-child, .layout-table .tbl td:last-child { padding-right: 0; }
/* Type size steps with the row count. The canvas is fixed and the row cap is
   12, so these are the only cases there are. */
.layout-table .tbl.rows-7, .layout-table .tbl.rows-8 { font-size: 24px; }
.layout-table .tbl.rows-7 td, .layout-table .tbl.rows-8 td { padding-top: 13px; padding-bottom: 13px; }
.layout-table .tbl.rows-9, .layout-table .tbl.rows-10 { font-size: 21px; }
.layout-table .tbl.rows-9 td, .layout-table .tbl.rows-10 td { padding-top: 10px; padding-bottom: 10px; }
.layout-table .tbl.rows-11, .layout-table .tbl.rows-12 { font-size: 19px; }
.layout-table .tbl.rows-11 td, .layout-table .tbl.rows-12 td { padding-top: 8px; padding-bottom: 8px; }
/* Columns cost width, not height, so they step separately. */
.layout-table .tbl.cols-5 { font-size: .88em; }
.layout-table .tbl.cols-6 { font-size: .8em; }
.layout-table .empty {
  flex: 1; display: grid; place-items: center; align-content: center;
  color: var(--s-dim); font-size: 24px; gap: 10px;
  border: 3px dashed var(--s-rule); border-radius: 16px; text-align: center; padding: 30px;
}

/* Code viewer — typed source on the wall, edited only in the inspector */
.layout-code .pad { gap: 18px; }
.layout-code h2 { margin-bottom: 4px; }
.layout-code .code-frame {
  flex: 1; min-height: 0; display: flex; flex-direction: column;
  border: 1px solid var(--s-rule); border-radius: 14px;
  background: color-mix(in srgb, var(--s-card, var(--s-bg)) 88%, #0b1220 12%);
  overflow: hidden;
}
.layout-code .code-chrome {
  display: flex; align-items: center; gap: 12px;
  padding: 10px 14px; border-bottom: 1px solid var(--s-rule);
  font-size: 14px; color: var(--s-dim);
}
.layout-code .code-dots::before {
  content: '';
  display: inline-block; width: 54px; height: 10px;
  background:
    radial-gradient(circle at 5px 5px, #ef6b73 4px, transparent 5px),
    radial-gradient(circle at 23px 5px, #e6c15a 4px, transparent 5px),
    radial-gradient(circle at 41px 5px, #61c279 4px, transparent 5px);
}
.layout-code .code-filename { flex: 1; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.layout-code .code-lang {
  font-size: 12px; letter-spacing: .04em; text-transform: uppercase;
  padding: 3px 8px; border-radius: 999px; border: 1px solid var(--s-rule);
}
.layout-code .code-pane {
  flex: 1; margin: 0; padding: 22px 26px 28px;
  overflow: auto; white-space: pre; tab-size: 2;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 24px; line-height: 1.45; color: var(--s-fg);
  background: transparent;
}
/* Longer snippets step down instead of scrolling out of the frame. The class
   comes from the line count in layoutCode; the sizes are what fits the pane
   at line-height 1.45 with 11, 16 and 24 lines in it. Past that the code is
   too long for one slide and the honest answer is two slides, but it is still
   better to show all of it small than four fifths of it large. */
.layout-code .code-pane.code-lines-md { font-size: 19px; }
.layout-code .code-pane.code-lines-lg { font-size: 16px; }
.layout-code .code-pane.code-lines-xl { font-size: 14px; line-height: 1.38; }
.layout-code .code-pane code { font: inherit; color: inherit; }
/* Line by line: a line not yet reached keeps its space, so the frame does not
   grow under the room as the walk-through proceeds. display:none would make
   every press nudge everything below it. */
.layout-code .code-line { display: block; }
.layout-code .code-line.step-hidden { visibility: hidden; }

/* Syntax colours, chosen by measuring contrast against the two grounds the
   frame actually has rather than by copying an editor theme. The frame is
   color-mix(--s-card 88%, #0b1220 12%), which on a light theme resolves to
   about #dedede and on midnight to about #162137; every colour below clears
   4.5:1 on its own ground, and the first three drafts did not — a mid grey
   comment and a mid green string were at 3.6 and 4.0. */
.layout-code .code-pane .ct-com { color: #525a66; font-style: italic; }
.layout-code .code-pane .ct-str { color: #11663a; }
.layout-code .code-pane .ct-num { color: #8f4108; }
.layout-code .code-pane .ct-kw  { color: #7e2b90; font-weight: 600; }
.layout-code .code-pane .ct-fn  { color: #1746c0; }
/* The dark grounds, named rather than detected — the same list the chart
   palette uses, for the same reason: a theme is dark because we said so. */
.slide[data-ground="dark"] .code-pane .ct-com { color: #95a3bd; }
.slide[data-ground="dark"] .code-pane .ct-str { color: #7fd9a3; }
.slide[data-ground="dark"] .code-pane .ct-num { color: #f0b878; }
.slide[data-ground="dark"] .code-pane .ct-kw { color: #e0a3f0; }
.slide[data-ground="dark"] .code-pane .ct-fn { color: #8fc0ff; }
.layout-code .code-pane.code-typing::after {
  content: '';
  display: inline-block; width: .55ch; height: 1.05em;
  margin-left: 1px; vertical-align: text-bottom;
  background: var(--s-accent, var(--s-fg));
  animation: code-caret 1s steps(1) infinite;
}
@keyframes code-caret {
  0%, 49% { opacity: 1; }
  50%, 100% { opacity: 0; }
}

/* readiness — what will go wrong in the room */
.ready-box { display: flex; flex-direction: column; gap: 4px; }
.ready-ok { font-size: 12px; color: var(--ui-good); line-height: 1.5; padding: 8px 10px;
  background: var(--ui-bg); border-radius: 6px; }
.ready-row {
  display: flex; gap: 8px; align-items: flex-start; text-align: left;
  padding: 7px 9px; border-radius: 6px; cursor: pointer;
  background: var(--ui-bg); border: 1px solid transparent;
  font: inherit; font-size: 12px; color: var(--ui-text); line-height: 1.45;
}
.ready-row:hover:not(:disabled) { border-color: var(--ui-accent); }
.ready-row:disabled { cursor: default; }
.ready-dot {
  flex: none; width: 16px; height: 16px; border-radius: 50%;
  display: grid; place-items: center; margin-top: 1px;
  font-size: 10px; font-weight: 800; color: #0b0f16;
}
/* A shape as well as a colour: ! and ? read the same to everyone. */
.ready-stop .ready-dot { background: var(--ui-bad); }
.ready-check .ready-dot { background: var(--ui-warn); }
.ready-text strong { color: var(--ui-text); }
.ready-text span { color: var(--ui-dim); }

/* AI smoke test — Lesson studio diagnostics panel */
.ai-smoke-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.ai-smoke-card {
  padding: 10px 12px; border-radius: 8px;
  background: var(--ui-bg); border: 1px solid var(--ui-line);
}
.ai-smoke-meta { margin-top: 10px; display: flex; flex-direction: column; gap: 4px; }
.ai-smoke-meta-row {
  display: grid; grid-template-columns: 88px 1fr; gap: 8px;
  font-size: 12px; line-height: 1.4;
}
.ai-smoke-k { color: var(--ui-dim); }
.ai-smoke-v { color: var(--ui-text); word-break: break-word; font-variant-numeric: tabular-nums; }
.ai-smoke-log {
  max-height: 180px; overflow: auto; display: flex; flex-direction: column; gap: 4px;
  padding: 8px 10px; border-radius: 8px;
  background: var(--ui-bg); border: 1px solid var(--ui-line);
  font-size: 11px; line-height: 1.4;
}
.ai-smoke-line { display: grid; grid-template-columns: 58px 1fr; gap: 8px; }
.ai-smoke-time { color: var(--ui-dim); font-variant-numeric: tabular-nums; }
.ai-smoke-ok .ai-smoke-msg { color: var(--ui-good, #3d7a4a); }
.ai-smoke-fail .ai-smoke-msg { color: var(--ui-bad, #b33); }
.ai-smoke-warn .ai-smoke-msg { color: var(--ui-warn, #a67c00); }
.ai-smoke-result {
  margin: 0; max-height: 160px; overflow: auto;
  padding: 10px 12px; border-radius: 8px;
  background: var(--ui-bg); border: 1px solid var(--ui-line);
  font-size: 11px; line-height: 1.45; white-space: pre-wrap; word-break: break-word;
}

/* the teacher's panel — in the app, poppable out */
.teach-modal {
  border: 0; padding: 0; border-radius: 12px;
  width: min(920px, 94vw); max-height: 88vh;
  background: var(--ui-panel); color: var(--ui-text);
  border: 1px solid var(--ui-line);
}
.teach-modal::backdrop { background: rgba(6,8,12,.6); }
.teach-modal header {
  display: flex; align-items: center; gap: 10px;
  padding: 14px 16px; border-bottom: 1px solid var(--ui-line);
}
.teach-modal header h2 { flex: 1; margin: 0; font-size: 17px; }
.teach-tabs { display: flex; gap: 6px; padding: 10px 16px 0; }
.teach-tabs .btn[aria-selected=true] { background: var(--ui-accent-soft); border-color: var(--ui-accent); }
.teach-body { padding: 12px 16px 16px; overflow: auto; max-height: calc(88vh - 120px); }
/* The frame carries manual.html's own stylesheet, so it needs height rather
   than styling. */
.teach-roster { margin: 0 0 12px; padding: 12px; border: 1px solid var(--ui-line); border-radius: 8px; background: var(--ui-panel-2); }
.teach-roster[hidden] { display: none; }
.teach-roster .roster-row { padding: 8px 0; }
#teachFrame { width: 100%; height: 52vh; border: 1px solid var(--ui-line); border-radius: 8px; background: #fff; }
/* Hidden with size, not display:none — some browsers reload an iframe that
   is taken out of layout, which wiped the class list and the draft names. */
#teachFrame.teach-away {
  position: absolute; width: 1px; height: 1px; padding: 0; border: 0;
  overflow: hidden; clip: rect(0,0,0,0);
}

/* session list: a row you can open, and a way to be rid of it */
.session-row { display: flex; align-items: stretch; gap: 4px; margin-bottom: 4px; border-radius: 7px; }
.session-row.selected { background: var(--ui-accent-soft); }
.session-row .session-item { flex: 1; min-width: 0; }
.session-del {
  flex: none; width: 30px; border: 1px solid transparent; border-radius: 6px;
  background: none; color: var(--ui-dim); cursor: pointer; font-size: 15px; line-height: 1;
}
/* Quiet until wanted — a delete control that advertises itself on a list of
   attendance records is an invitation to a mistake. */
.session-row:hover .session-del { color: var(--ui-text); }
.session-del:hover { background: rgba(196,70,80,.14); border-color: var(--ui-bad); color: var(--ui-bad); }
.session-del:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: 1px; }

/* questions overview — the shape of each check */
.qo-wrap { display: flex; flex-direction: column; gap: 10px; }
.qo-card { background: var(--ui-bg); border: 1px solid var(--ui-line); border-radius: 9px; padding: 11px 13px; }
.qo-head { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 9px; }
.qo-n { flex: none; width: 22px; height: 22px; border-radius: 6px; display: grid; place-items: center;
  background: var(--ui-panel-2); color: var(--ui-dim); font-size: 11px; font-weight: 700; }
.qo-title { flex: 1; min-width: 0; font-size: 13px; line-height: 1.35; }
.qo-meta { display: block; margin-top: 3px; font-size: 11px; color: var(--ui-dim); }
.qo-bloom, .qo-open {
  margin-left: 7px; font-size: 9px; letter-spacing: .06em; text-transform: uppercase;
  border: 1px solid var(--ui-line); border-radius: 999px; padding: 1px 6px;
}
.qo-open { color: var(--ui-warn); border-color: var(--ui-warn); }
.qo-pct { flex: none; font-size: 15px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--ui-dim); }
.qo-weak { color: var(--ui-bad); }
.qo-bars { display: flex; flex-direction: column; gap: 4px; }
.qo-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 9px; font-size: 12px; }
.qo-names { flex: 1 1 100%; margin: -2px 0 4px; font-size: 12px; font-weight: 550; color: var(--ui-text); }
.qo-label { flex: 0 0 44%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ui-text); }
.qo-track { flex: 1; height: 9px; background: var(--ui-panel-2); border-radius: 999px; overflow: hidden; }
.qo-fill { display: block; height: 100%; background: var(--ui-dim); border-radius: 999px; }
.qo-count { flex: none; width: 22px; text-align: right; color: var(--ui-dim); font-variant-numeric: tabular-nums; }
/* Green only once it has been revealed — questionOverview never sets this
   class before then, so the colour cannot arrive early. */
.qo-right .qo-fill { background: var(--ui-good); }
.qo-right .qo-label { color: var(--ui-good); font-weight: 650; }
.qo-right .qo-label::before { content: '\2713 '; }
.qo-note { margin: 9px 0 0; font-size: 11px; line-height: 1.5; color: var(--ui-warn); }

.grid-readout { margin: 0 0 12px; }
.grid-headline { margin: 0 0 10px; font-size: 15px; font-weight: 650; line-height: 1.35; color: var(--ui-text); }
.grid-tiles { display: flex; flex-wrap: wrap; gap: 8px; }
.grid-tile { min-width: 88px; padding: 8px 10px; border-radius: 8px; background: var(--ui-panel-2); }
.grid-tile strong { display: block; font-size: 18px; }
.grid-tile span { font-size: 11px; color: var(--ui-dim); }
.grid-toggles { display: flex; flex-wrap: wrap; gap: 12px; margin: 0 0 10px; font-size: 13px; color: var(--ui-dim); }
.grid-toggle { display: flex; align-items: center; gap: 5px; cursor: pointer; }
.g-stem { display: block; font-size: 10px; font-weight: 500; max-width: 88px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ui-text); }

/* every answer — people down, checks across */
.grid-wrap { overflow: auto; max-width: 100%; }
.grid-names { display: flex; align-items: center; gap: 7px; font-size: 12px;
  color: var(--ui-dim); margin: 0 0 10px; cursor: pointer; }
.answer-grid { border-collapse: separate; border-spacing: 2px; font-size: 12px; }
.answer-grid th, .answer-grid td { padding: 5px 8px; border-radius: 5px; text-align: center; }
.answer-grid th { font-weight: 600; color: var(--ui-dim); font-size: 11px; }
.answer-grid .g-name { text-align: left; white-space: nowrap; color: var(--ui-text);
  position: sticky; left: 0; background: var(--ui-panel); }
.g-qn { display: block; font-variant-numeric: tabular-nums; }
.g-unrevealed { display: block; font-size: 9px; letter-spacing: .04em; text-transform: uppercase; opacity: .7; }
.g-src { margin-left: 6px; font-size: 9px; letter-spacing: .06em; text-transform: uppercase;
  color: var(--ui-dim); border: 1px solid var(--ui-line); border-radius: 999px; padding: 1px 5px; }
.g-cell { background: var(--ui-bg); min-width: 42px; max-width: 120px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A mark as well as a colour, so the fill is never the only signal. */
.g-right { background: var(--ui-accent-soft); color: var(--ui-good); font-weight: 650; }
.g-right::before { content: '\2713 '; }
.g-wrong { background: rgba(196,70,80,.12); color: var(--ui-bad); font-weight: 650; }
.g-wrong::before { content: '\2717 '; }
.g-unscored { color: var(--ui-dim); }
.g-none { background: var(--ui-bg); }
.g-na { background: transparent; opacity: .35; }
.answer-grid tfoot td, .g-tot { font-variant-numeric: tabular-nums; color: var(--ui-dim); font-weight: 600; }
.answer-grid tfoot .g-name { color: var(--ui-dim); font-weight: 500; }
.grid-roster { margin: 14px 0 0; padding: 12px 0 0; border-top: 1px solid var(--ui-line); }
.grid-roster-lbl { font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--ui-dim); margin: 0 0 8px; }
.grid-roster-list { display: flex; flex-wrap: wrap; gap: 8px; list-style: none; margin: 0 0 14px; padding: 0; }
.grid-roster-name { padding: 6px 10px; border-radius: 999px; background: var(--ui-panel-2); font-size: 13px; font-weight: 600; }
.grid-name-col { margin: 0 0 12px; padding: 10px 12px; border-radius: 8px; background: var(--ui-panel-2); }
.grid-name-col-h { margin: 0 0 6px; font-size: 13px; font-weight: 650; }
.grid-who-line { margin: 4px 0 0; font-size: 13px; line-height: 1.45; }
.grid-who-k { display: inline-block; min-width: 5.5em; margin-right: 8px; font-size: 11px; letter-spacing: .04em; text-transform: uppercase; color: var(--ui-dim); font-weight: 650; }
.grid-who-right .grid-who-k { color: var(--ui-good); }
.grid-who-wrong .grid-who-k { color: var(--ui-bad); }
.roster-manage { margin: 16px 0 0; padding: 12px 0 0; border-top: 1px solid var(--ui-line); }
.roster-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 8px 0; border-bottom: 1px solid var(--ui-line); }
.roster-row strong { min-width: 8em; }
.roster-act { font-size: 12px; padding: 5px 10px; }
.roster-act.danger { color: var(--ui-bad); }
/* The number worth stopping for. */
.g-weak { color: var(--ui-bad); background: rgba(196,70,80,.12); }

/* layout variations — the same content, rendered three other ways */
.variant-row { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 7px; }
.variant-card {
  display: flex; flex-direction: column; gap: 5px;
  padding: 5px; border-radius: 7px; cursor: pointer;
  background: var(--ui-panel-2); border: 1px solid transparent;
  font: inherit; text-align: center;
}
.variant-card:hover { border-color: var(--ui-accent); }
.variant-frame {
  position: relative; width: 100%; aspect-ratio: 16/9;
  border-radius: 4px; overflow: hidden; background: #000;
}
/* SF.fit writes left/top with a top-left origin — same rule as everywhere
   else that scales a slide down. */
.variant-frame .slide { transform-origin: 0 0; }
.variant-name { font-size: 10px; color: var(--ui-dim); line-height: 1.2; }
/* A starter leads with the shape, so the frame takes the space the icon used
   to and the words sit beneath it. */
.starter-card .starter-frame { margin-bottom: 10px; outline: 1px solid rgba(0,0,0,.12); }
.starter-card .activity-icon { display: none; }
.variant-card:hover .variant-name { color: var(--ui-text); }

/* theme picker — real slides, scaled down, so a theme is chosen by looking */
.theme-grid {
  /* minmax(0,...) rather than 1fr: a bare 1fr floors at min-content, so
     "Studio · Sage & ink" made its column wider than the other two and the
     previews came out three different sizes. */
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px;
}
.theme-card {
  display: flex; flex-direction: column; gap: 7px;
  padding: 7px; border-radius: 8px; cursor: pointer;
  background: var(--ui-panel-2); border: 2px solid transparent;
  font: inherit; text-align: center;
}
.theme-card:hover { border-color: var(--ui-line); }
.theme-card.on { border-color: var(--ui-accent); }
.theme-frame {
  position: relative; width: 100%; aspect-ratio: 16/9;
  border-radius: 5px; overflow: hidden;
  /* The card is the control; the slide inside it is a picture of one. */
  pointer-events: none;
}
/* SF.fit writes a transform and a left/top that assume the box shrinks
   towards its top-left corner. Without this the default 50% 50% origin
   shrinks the 1280px slide towards its own centre, which puts the visible
   155px some 562px to the right of the frame — clipped away entirely, so the
   card shows nothing but its placeholder. The rail thumbnails set the same
   rule for the same reason. */
.theme-frame .slide { transform-origin: 0 0; }
/* The logo-in-place shot in the settings sheet. Same fitter, so the same
   origin rule applies. */
.logo-shot {
  position: relative; width: 100%; aspect-ratio: 16/9;
  border-radius: 6px; overflow: hidden; background: #000;
  border: 1px solid var(--ui-line);
}
.logo-shot .slide { transform-origin: 0 0; }
.theme-name { font-size: 11px; color: var(--ui-dim); line-height: 1.2; }
.theme-card.on .theme-name { color: var(--ui-text); font-weight: 600; }
/* The preview is a section slide at 1/9 scale, where the real type sizes are
   unreadable, so they are overridden for the preview only. */
.theme-frame .slide h1 { font-size: 190px; }
.theme-frame .slide .accent-bar { height: 26px; width: 300px; margin-top: 40px; }

/* dual coding — half copy, half image */
.layout-split .pad.split-pad {
  padding: 0;
  flex-direction: row;
  align-items: stretch;
}
.layout-split .split-copy {
  flex: 1 1 50%;
  min-width: 0;
  padding: 64px 56px 56px;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.layout-split .split-copy h2 {
  font-size: 42px;
  margin: 0 0 28px;
  line-height: 1.15;
}
.layout-split .split-copy ul {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 18px;
}
.layout-split .split-copy li {
  font-size: 26px;
  line-height: 1.35;
  padding-left: 34px;
  position: relative;
}
.layout-split .split-copy li::before {
  content: '';
  position: absolute;
  left: 2px;
  top: .55em;
  width: 11px;
  height: 11px;
  border-radius: 3px;
  background: var(--s-accent);
}
.layout-split .split-copy li.tier-2 {
  font-size: 22px;
  margin-left: 28px;
  color: var(--s-dim);
}
.layout-split .split-copy li.dim {
  color: var(--s-dim);
  font-style: italic;
}
.layout-split .split-copy li.dim::before { background: var(--s-dim); opacity: .45; }
.layout-split .split-media {
  flex: 1 1 50%;
  min-width: 0;
  position: relative;
  background: var(--s-rule);
}
.layout-split .split-media .img {
  position: absolute;
  inset: 0;
  background-position: center;
  background-repeat: no-repeat;
}
.layout-split .split-media .img.cover { background-size: cover; }
.layout-split .split-media .img.contain { background-size: contain; }
.layout-split .split-empty {
  position: absolute;
  inset: 28px;
  border: 2px dashed var(--s-rule);
  border-radius: 14px;
  display: grid;
  place-items: center;
  align-content: center;
  gap: 8px;
  color: var(--s-dim);
  font-size: 22px;
  text-align: center;
  background: color-mix(in srgb, var(--s-bg) 70%, transparent);
}

/* slide number + progress */
/* THE GENERIC CHROME REGION.
 *
 * One declaration for where a slide's furniture lives, so the layouts stop
 * each deciding it. Measured before this existed: across six generic slides
 * the mark's top was 24, 26 or 28px and its right edge 36 or 40, because the
 * base rule said 28/36 and a [data-composition] override said 26/40 — and
 * separately the mark sat at right:36 while the page number sat at right:40,
 * so the two had never lined up with each other at all.
 *
 * The campaign does not use these: it has a real .cp-header and .cp-footer
 * and its own 52px content edge, and it keeps them. These tokens are the
 * generic layouts' equivalent — the region the mark and the number sit in,
 * and the thing a body region can later be measured between.
 */
.slide {
  --sf-chrome-top: 28px;
  --sf-chrome-side: 40px;
  --sf-header-h: 52px;
  --sf-footer-h: 30px;
}
.slide .pagenum {
  position: absolute; right: var(--sf-chrome-side); bottom: 26px;
  font-size: 17px; color: var(--s-dim); letter-spacing: .04em;
}
.slide .track { position: absolute; left: 0; right: 0; bottom: 0; height: 5px; background: var(--s-rule); }
.slide .track > i { display: block; height: 100%; background: linear-gradient(90deg, var(--s-accent), var(--s-accent-2)); }

.slide-logo {
  position: absolute;
  top: var(--sf-chrome-top);
  right: var(--sf-chrome-side);
  z-index: 4;
  height: var(--sf-header-h);
  max-width: 180px;
  pointer-events: none;
}
.slide-logo img {
  display: block;
  height: 100%;
  width: auto;
  max-width: 180px;
  object-fit: contain;
}

/* The reverse lockup, made rather than supplied. brightness(0) crushes every
   colour to black and invert(1) lifts it to white, so the result is a clean
   white silhouette with the original anti-aliasing intact — which inverting
   the pixels would not give, and which works for any deck's logo rather than
   only the one this repo happens to ship.

   The corner-swap further up was the earlier answer to this ("a deck's logo
   may be light or dark, and inverting it would fix one and ruin the other").
   That holds for a blanket rule; it does not hold when the theme is the thing
   deciding, because a theme knows which of its own grounds are dark. */
.slide.logo-reverse .slide-logo img { filter: brightness(0) invert(1); }
.slide.logo-reverse.layout-image .slide-logo img {
  filter: brightness(0) invert(1) drop-shadow(0 1px 8px rgba(0,0,0,.55));
}

/* Any dark ground. `:not(.logo-normal)` leaves a way out for a deck whose own
   logo is already white — which is how the UKBT decks are unaffected by
   joining this rule: every one of them sets logoReverse: 'never', because the
   brand ships its wordmark already reversed. */
[data-ground="dark"] .slide:not(.logo-normal) .slide-logo img,
.slide[data-ground="dark"]:not(.logo-normal) .slide-logo img { filter: brightness(0) invert(1); }
/* These move the REGION, not just the mark in it. Setting top/right on
   .slide-logo alone left the page number behind at the default inset, so on a
   title or section the mark sat 48px in and the number 40px in — the two
   never lined up. Shifting the tokens moves both, which is the point of
   having them. Size stays on the mark: how big it is, is its own business. */
.layout-title,
.layout-section { --sf-chrome-top: 36px; --sf-chrome-side: 48px; }
.layout-title .slide-logo,
.layout-section .slide-logo {
  height: 60px;
  max-width: 200px;
}
/* Tighter into the corner, because the photograph goes under it. */
.layout-image { --sf-chrome-top: 24px; --sf-chrome-side: 28px; }
.layout-image .slide-logo {
  height: 44px;
  filter: drop-shadow(0 1px 8px rgba(0,0,0,.35));
}

/* --------------------------------------------------- 4. player */

#player {
  position: fixed; inset: 0; z-index: 100;
  background: #000;
  display: none;
  place-items: center;
}
#player.on { display: grid; }
#player.blank .deck-viewport { visibility: hidden; }

.deck-viewport { position: relative; overflow: hidden; background: #000; }
.deck-viewport .slide { transform-origin: 0 0; }

/* transitions: outgoing/incoming pair */
.deck-viewport .slide.leaving { position: absolute; z-index: 1; }
.deck-viewport .slide.entering { position: absolute; z-index: 2; }

/* Slides are letterboxed with transform: scale(var(--sf-scale)), so every
   keyframe that touches transform has to carry that scale through or the
   slide snaps to full 1280x720 mid-transition. Scale comes first so the
   translate percentages resolve against the on-screen size. */
@keyframes sf-fade-in   { from { opacity: 0 } to { opacity: 1 } }
@keyframes sf-fade-out  { from { opacity: 1 } to { opacity: 0 } }
@keyframes sf-push-in    { from { transform: scale(var(--sf-scale)) translateX(100%) }  to { transform: scale(var(--sf-scale)) translateX(0) } }
@keyframes sf-push-out   { from { transform: scale(var(--sf-scale)) translateX(0) }     to { transform: scale(var(--sf-scale)) translateX(-100%) } }
@keyframes sf-push-in-r  { from { transform: scale(var(--sf-scale)) translateX(-100%) } to { transform: scale(var(--sf-scale)) translateX(0) } }
@keyframes sf-push-out-r { from { transform: scale(var(--sf-scale)) translateX(0) }     to { transform: scale(var(--sf-scale)) translateX(100%) } }
@keyframes sf-zoom-in   { from { transform: scale(calc(var(--sf-scale) * 1.12)); opacity: 0 } to { transform: scale(var(--sf-scale)); opacity: 1 } }
@keyframes sf-zoom-out  { from { opacity: 1 } to { opacity: 0 } }
@keyframes sf-wipe-in   { from { clip-path: inset(0 0 0 100%) } to { clip-path: inset(0 0 0 0) } }

.tr-fade.entering  { animation: sf-fade-in .34s ease both; }
.tr-fade.leaving   { animation: sf-fade-out .34s ease both; }
.tr-push.entering  { animation: sf-push-in .38s cubic-bezier(.4,0,.2,1) both; }
.tr-push.leaving   { animation: sf-push-out .38s cubic-bezier(.4,0,.2,1) both; }
.back .tr-push.entering { animation-name: sf-push-in-r; }
.back .tr-push.leaving  { animation-name: sf-push-out-r; }
.tr-zoom.entering  { animation: sf-zoom-in .4s cubic-bezier(.3,0,.2,1) both; }
.tr-zoom.leaving   { animation: sf-zoom-out .3s ease both; }
.tr-wipe.entering  { animation: sf-wipe-in .42s cubic-bezier(.4,0,.2,1) both; }
.tr-wipe.leaving   { animation: sf-fade-out .42s ease both; }

/* player HUD */
.hud {
  position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%);
  display: flex; align-items: center; gap: 4px;
  background: rgba(18,20,24,.9);
  border: 1px solid rgba(255,255,255,.12);
  border-radius: 999px; padding: 5px 8px;
  backdrop-filter: blur(12px);
  opacity: 0; transition: opacity .25s; pointer-events: none;
  z-index: 110; color: #fff;
}
.hud.show { opacity: 1; pointer-events: auto; }
.hud button {
  background: transparent; border: 0; color: #fff; cursor: pointer;
  width: 34px; height: 30px; border-radius: 999px; font-size: 14px;
}
.hud button:hover { background: rgba(255,255,255,.14); }

/* Next is about to do something other than move on. A ring rather than a
   colour swap, so it reads as "this one is different" without competing with
   the slide behind it. */
.hud button.will-reveal { outline: 2px solid #34c98a; outline-offset: -2px; }
.hud button.will-hold { outline: 2px solid #ffc75a; outline-offset: -2px; }
.hud button.on { background: rgba(255,255,255,.2); }
/* Three stops on one button, so the button has to show which one it is on:
   an outline for beside the slide, filled for the whole screen. */
.hud button[data-state="beside"] { outline: 1.5px solid rgba(255,255,255,.55); outline-offset: -1.5px; }
.hud button[data-state="full"] { background: #fff; color: #14171d; }
.hud .pos { font-size: 13px; padding: 0 10px; color: #cfd4de; font-variant-numeric: tabular-nums; }
.hud .sep { width: 1px; height: 18px; background: rgba(255,255,255,.16); margin: 0 4px; }

/* keyboard cheat sheet */
#cheats {
  position: fixed; inset: 0; z-index: 120;
  background: rgba(6,7,9,.9); backdrop-filter: blur(6px);
  display: none; place-items: center; color: #fff;
}
#cheats.on { display: grid; }
#cheats .card {
  background: #1a1d23; border: 1px solid #2f343d; border-radius: 12px;
  padding: 26px 30px; min-width: 420px;
}
#cheats h3 { margin: 0 0 16px; font-size: 15px; text-transform: uppercase; letter-spacing: .08em; color: #9aa1ad; }
#cheats table { border-collapse: collapse; font-size: 14px; }
#cheats td { padding: 6px 0; }
#cheats td:first-child { padding-right: 26px; }
#cheats kbd {
  background: #262b33; border: 1px solid #3a414c; border-bottom-width: 2px;
  border-radius: 5px; padding: 2px 7px; font-family: ui-monospace, monospace; font-size: 12px;
}

/* ------------------------------------------------------- 5. quiz */

.layout-quiz .pad { padding: 56px 78px 64px; }
.layout-quiz .qhead { display: flex; align-items: flex-start; gap: 20px; margin-bottom: 8px; }
/* reserve the countdown's corner so a two-line question doesn't run under it */
.layout-quiz.has-clock .qhead { padding-right: 76px; }
.layout-quiz .qnum {
  flex: none; font-size: 18px; font-weight: 700; letter-spacing: .1em;
  color: var(--s-accent); padding-top: 12px;
}
.layout-quiz .q {
  --q-f: 46px;
  font-size: var(--q-f); line-height: 1.18; font-weight: 650; margin: 0; flex: 1;
}

/* Rows hug their content and the block centres in whatever space is left.
   Stretching the rows to fill the slide is what produced boxes that were
   mostly padding — the answer text cannot sensibly grow to 80px just to fill
   a box, because then it out-shouts the question. */
.layout-quiz .opts {
  /* declared here, not on .opt: a declaration on the item itself would shadow
     the value fitQuizSlide() writes to this container */
  --opt-f: 33px;
  margin-top: 30px; flex: 1; min-height: 0;
  display: grid; grid-template-columns: 1fr 1fr; gap: 18px;
  grid-auto-rows: min-content;
  /* `safe` matters: plain `center` splits any overflow evenly above and below,
     so answers that do not fit ride up over the question and down over the
     vote bars — and fitQuizSlide's scrollHeight only sees the bottom half of
     the problem, so it stops shrinking too early. */
  align-content: safe center;
}
.layout-quiz .opts.stack { grid-template-columns: 1fr; }

/* --opt-f is set by fitQuizSlide() in player.js: the answer type grows to fill
   its box and shrinks when the wording is long, so a box is never mostly
   padding. Padding and the key badge scale with it so the proportions hold at
   any size. */
.opt {
  display: flex; flex-direction: column; justify-content: center;
  background: var(--s-card);
  border: 3px solid var(--s-rule);
  border-radius: 14px;
  padding: calc(var(--opt-f) * .42) calc(var(--opt-f) * .66);
  font-size: var(--opt-f); line-height: 1.2;
  cursor: pointer;
  text-align: left;
  color: inherit; font-family: inherit;
  transition: transform .12s, border-color .16s, background .16s;
  min-height: 0;
  overflow: hidden;
}
/* the answer itself stays a single row inside the box */
.opt .opt-line { display: flex; align-items: center; gap: calc(var(--opt-f) * .5); }
.opt:hover:not(.locked) { transform: translateY(-3px); border-color: var(--s-accent); }
.opt .key {
  flex: none;
  width: calc(var(--opt-f) * 1.34); height: calc(var(--opt-f) * 1.34);
  border-radius: calc(var(--opt-f) * .26);
  display: grid; place-items: center;
  background: var(--s-accent); color: #08101f;
  font-weight: 800; font-size: calc(var(--opt-f) * .74);
}
/* A question slide already owns its top-right corner: the countdown ring is
   at right:46px and is 84px across, so a mark anchored to the same corner
   lands on top of it. Pushed clear of the ring rather than moved back to the
   left, so the logo sits in the same place on every slide in the deck. */
.layout-quiz.has-clock .slide-logo { right: 148px; }

.theme-mono .opt .key { color: #0c0c0c; }
.opt .opt-line .txt { flex: 1; }

/* Set by fitQuizSlide() only when no answer wraps. The badge stays left so the
   A/B/C/D column still lines up across the grid; it is the wording that
   centres. Once anything wraps this comes off, because centred multi-line text
   gives every line a different left edge and is slower to read. */
.layout-quiz .opts.centred .opt .opt-line .txt { text-align: center; }
.opt .tick { flex: none; font-size: calc(var(--opt-f) * .9); opacity: 0; }

.opt.correct { border-color: var(--s-good, #34c98a); background: rgba(52,201,138,.2); }
.opt.correct .key { background: #34c98a; color: #06281a; }
.opt.correct .tick { opacity: 1; }
.opt.wrong { border-color: #ff5f6d; background: rgba(255,95,109,.16); }
.opt.wrong .key { background: #ff5f6d; color: #2d0409; }
.opt.wrong .tick { opacity: 1; }
.opt.muted { opacity: .38; }
.opt.locked { cursor: default; }

/* countdown */
.layout-quiz .clock,
.slide > .clock.slide-clock {
  position: absolute; right: 46px; top: 40px;
  width: 84px; height: 84px;
}
.slide > .clock.slide-clock svg { transform: rotate(-90deg); }
.slide > .clock.slide-clock .ring-bg { stroke: var(--s-rule); }
.slide > .clock.slide-clock .ring {
  stroke: var(--s-accent);
  transition: stroke-dashoffset .3s linear, stroke .3s;
}
/* A minute's warning, not five seconds': an activity needs long enough to start
   wrapping up, which is a different urgency from a quiz question. */
.slide > .clock.slide-clock.hurry .ring,
.slide > .clock.slide-clock.done .ring { stroke: #ff5f6d; }
.slide > .clock.slide-clock.done .n { color: #ff5f6d; }
.slide > .clock.slide-clock .n {
  position: absolute; inset: 0; display: grid; place-items: center;
  font-size: 21px; font-weight: 700; font-variant-numeric: tabular-nums;
}
/* The logo sits in that corner too, so a timed slide moves it clear. */
.slide.has-clock > .slide-logo { right: 150px; }
.layout-quiz .clock svg { transform: rotate(-90deg); }
.layout-quiz .clock .ring-bg { stroke: var(--s-rule); }
.layout-quiz .clock .ring { stroke: var(--s-accent); transition: stroke-dashoffset .95s linear, stroke .3s; }
.layout-quiz .clock.hurry .ring { stroke: #ff5f6d; }
.layout-quiz .clock .n {
  position: absolute; inset: 0; display: grid; place-items: center;
  font-size: 28px; font-weight: 700; font-variant-numeric: tabular-nums;
}

/* answer tally bars (live mode) */
.layout-quiz .tally { display: flex; gap: 14px; align-items: flex-end; height: 0; overflow: hidden; transition: height .3s; margin-top: 18px; }
.layout-quiz .tally.on { height: 74px; }
.layout-quiz .tally .col { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 5px; }
.layout-quiz .tally .bar { width: 100%; background: var(--s-accent); border-radius: 5px 5px 0 0; min-height: 3px; transition: height .5s cubic-bezier(.3,0,.2,1); }
/* Green only once the answer is out.

   The correct column used to be green from the moment the tally appeared,
   which meant a live vote announced its own answer before anybody discussed
   anything — fatal to peer instruction, where the whole method is to show the
   split and withhold the answer. Keyed off .opt.correct, which both reveal
   paths add to the slide, so the colour cannot arrive early again. */
.layout-quiz:has(.opt.correct) .tally .col.right .bar { background: #34c98a; }
.layout-quiz .tally .cnt { font-size: 15px; color: var(--s-dim); }

/* results slide */
.layout-results .pad { justify-content: center; }
.layout-results .score-hero { display: flex; align-items: center; gap: 56px; }
.layout-results .donut { position: relative; width: 260px; height: 260px; flex: none; }
.layout-results .donut svg { transform: rotate(-90deg); }
.layout-results .donut .ring-bg { stroke: var(--s-rule); }
.layout-results .donut .ring { stroke: var(--s-accent); transition: stroke-dashoffset 1.1s cubic-bezier(.3,0,.2,1); }
.layout-results .donut .mid {
  position: absolute; inset: 0; display: grid; place-items: center; text-align: center;
}
.layout-results .donut .pct { font-size: 62px; font-weight: 750; line-height: 1; }
.layout-results .donut .of { font-size: 18px; color: var(--s-dim); margin-top: 8px; }
.layout-results .breakdown { flex: 1; display: flex; flex-direction: column; gap: 13px; min-width: 0; }
.layout-results .row {
  display: flex; align-items: center; gap: 15px;
  font-size: 21px; padding: 11px 16px;
  background: var(--s-card); border-radius: 9px;
  border-left: 5px solid var(--s-rule);
}
.layout-results .row.ok { border-left-color: #34c98a; }
.layout-results .row.no { border-left-color: #ff5f6d; }
.layout-results .row .qi { flex: none; color: var(--s-dim); font-size: 15px; width: 30px; }
.layout-results .row .qt { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.layout-results .row .mk { flex: none; font-size: 22px; }
.layout-results .none { font-size: 26px; color: var(--s-dim); text-align: center; }

/* live leaderboard slide inside the player */
.leaderboard { padding: 60px 90px; }
.leaderboard h2 { margin-bottom: 26px; }
.lb-row {
  display: flex; align-items: center; gap: 22px;
  font-size: 38px; padding: 19px 26px; margin-bottom: 13px;
  background: var(--s-card); border-radius: 13px;
}
.lb-row .rank { width: 56px; font-weight: 750; color: var(--s-accent); flex: none; font-variant-numeric: tabular-nums; }
.lb-row .who { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lb-row .pts { font-variant-numeric: tabular-nums; font-weight: 750; flex: none; }
.lb-row.top1 { background: linear-gradient(90deg, rgba(255,199,90,.28), var(--s-card)); }

/* live lobby panel (host side, shown over the deck) */
#lobby {
  position: fixed; inset: 0; z-index: 1200;
  background: rgba(6,8,12,.94); backdrop-filter: blur(10px);
  display: none; place-items: center; color: #fff; text-align: center;
}
#lobby.on { display: grid; }
#lobby .pin {
  font-size: 92px; font-weight: 800; letter-spacing: .18em;
  font-variant-numeric: tabular-nums;
  background: linear-gradient(135deg, #6ba7ff, #b58bff);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
#lobby .url { font-size: 27px; color: #cfd6e4; margin-top: 6px; font-family: ui-monospace, monospace; }
#lobby .lead { font-size: 17px; text-transform: uppercase; letter-spacing: .12em; color: #8f97a6; }
#lobby .who-list {
  display: flex; flex-wrap: wrap; gap: 10px; justify-content: center;
  max-width: 860px; margin: 34px auto 0; max-height: 200px; overflow-y: auto;
}
#lobby .chip {
  background: rgba(255,255,255,.1); border: 1px solid rgba(255,255,255,.16);
  border-radius: 999px; padding: 8px 16px; font-size: 17px;
  animation: sf-fade-in .3s ease both;
}
#lobby .count { font-size: 15px; color: #8f97a6; margin-top: 22px; }
#lobby .actions { margin-top: 30px; display: flex; gap: 12px; justify-content: center; }
/* The lobby is a projected screen with 96px digits on it, so the controls
   under them cannot be the 13px buttons from the editor toolbar. Same styles,
   sized for the back of a room. */
#lobby .actions .btn { font-size: 17px; padding: 13px 22px; border-radius: 9px; }
#lobby .warn { color: var(--ui-warn); font-size: 14px; margin-top: 20px; max-width: 520px; line-height: 1.55; }

/* toast */
#toast {
  position: fixed; left: 50%; bottom: 26px; transform: translate(-50%, 20px);
  background: #22262e; border: 1px solid var(--ui-line); color: var(--ui-text);
  padding: 10px 16px; border-radius: 8px; font-size: 13px;
  opacity: 0; transition: opacity .2s, transform .2s;
  z-index: 200; pointer-events: none; max-width: 70vw;
}
#toast.on { opacity: 1; transform: translate(-50%, 0); }

/* deck browser modal */
.modal {
  position: fixed; inset: 0; z-index: 150;
  background: rgba(6,8,12,.72);
  display: none; place-items: center;
}
.modal.on { display: grid; }
.modal .sheet {
  background: var(--ui-panel); border: 1px solid var(--ui-line);
  border-radius: 12px; width: min(560px, 92vw); max-height: 76vh;
  display: flex; flex-direction: column; overflow: hidden;
}
.modal .sheet header {
  padding: 15px 18px; border-bottom: 1px solid var(--ui-line);
  display: flex; align-items: center; gap: 10px;
}
.modal .sheet h3 { margin: 0; font-size: 15px; flex: 1; min-width: 0; }
.modal .sheet .sheet-tools { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; justify-content: flex-end; }
.modal .sheet .sheet-tools .btn { font-size: 12px; padding: 5px 9px; }
.modal .sheet .body { overflow-y: auto; padding: 10px; }
.deck-item {
  display: flex; align-items: center; gap: 12px;
  padding: 11px 12px; border-radius: 8px; cursor: pointer;
}
.deck-item:hover { background: var(--ui-panel-2); }
.deck-item .info { flex: 1; min-width: 0; }
.deck-item .nm { font-weight: 550; }
.deck-item .mt { font-size: 12px; color: var(--ui-dim); margin-top: 2px; }
.deck-item .kill { background: transparent; border: 0; color: var(--ui-dim); cursor: pointer; padding: 6px; }
.deck-item .kill:hover { color: var(--ui-bad); }
.deck-item .pick-tick { flex: 0 0 auto; width: 16px; height: 16px; accent-color: var(--ui-accent); cursor: pointer; }
.empty-note { color: var(--ui-dim); font-size: 13px; padding: 20px; text-align: center; }

@media (max-width: 1240px) {
  #app { grid-template-columns: 180px 1fr 270px; }
  .topbar .btn { padding: 6px 9px; font-size: 13px; }
  #deckTitle { min-width: 130px; }
}

@media (max-width: 1000px) {
  #app { grid-template-columns: 150px 1fr 240px; }
  .stage-wrap { padding: 14px; }
}

/* ==========================================================================
   6. Persistent score rail

   A sibling of the slides inside .deck-viewport, so it survives slide changes
   and repaints on its own. Authored in slide units and scaled with the slide.

   Everything here is sized for a projector seen from the back of a room, so
   the type is deliberately large and the row size is driven by how many
   entries there are: two teams get huge rows, twelve players get compact ones.
   The width lives in --rail-w so the slide offsets below can never drift out
   of step with it.
   ========================================================================== */

.deck-viewport.railed { --rail-w: 384px; }

.scorerail {
  position: absolute;
  top: 0; right: 0;
  width: var(--rail-w, 384px); height: 720px;
  transform-origin: 100% 0;
  z-index: 3;
  display: flex; flex-direction: column;
  padding: 28px 20px 22px;
  color: var(--s-fg);
  /* Same token as the slide — a hardcoded dark wash used to win on midnight
     and get overridden by light themes (studio/paper), so the two looked like
     different products. */
  background: var(--s-bg);
  border-left: 3px solid var(--s-rule);
  font-family: var(--s-font);
  pointer-events: none;

  /* defaults; each density below overrides them */
  --nm-f: 28px;
  --sc-f: 30px;
  --rk-f: 18px;
  --mem-f: 15px;
  --row-py: 14px;
  --row-px: 18px;
  --row-gap: 11px;
  --row-r: 12px;
  --row-max: 116px;
}

.scorerail[data-density="xl"] { --nm-f: 42px; --sc-f: 40px; --rk-f: 26px; --mem-f: 19px; --row-py: 24px; --row-px: 22px; --row-gap: 18px; --row-r: 16px; --row-max: 210px; }
.scorerail[data-density="lg"] { --nm-f: 34px; --sc-f: 33px; --rk-f: 23px; --mem-f: 17px; --row-py: 19px; --row-px: 20px; --row-gap: 15px; --row-r: 14px; --row-max: 150px; }
.scorerail[data-density="md"] { --nm-f: 29px; --sc-f: 28px; --rk-f: 19px; --mem-f: 16px; --row-py: 15px; --row-px: 18px; --row-gap: 12px; --row-max: 116px; }
.scorerail[data-density="sm"] { --nm-f: 24px; --sc-f: 24px; --rk-f: 17px; --mem-f: 14px; --row-py: 11px; --row-px: 15px; --row-gap: 9px; --row-max: 88px; }
.scorerail[data-density="xs"] { --nm-f: 21px; --sc-f: 22px; --rk-f: 15px; --mem-f: 13px; --row-py: 7px;  --row-px: 12px; --row-gap: 5px; --row-r: 9px; --row-max: 62px; }

/* A note on the rail is 40-odd px the board does not have. It steps down one
   density while the note is up, rather than losing the last row to the clip —
   a board with a row sliced in half reads as broken, and during a boss battle
   there is a note after every question. */
.scorerail.has-news[data-density="xl"] { --nm-f: 34px; --sc-f: 33px; --rk-f: 23px; --mem-f: 17px; --row-py: 19px; --row-px: 20px; --row-gap: 15px; --row-r: 14px; --row-max: 150px; }
.scorerail.has-news[data-density="lg"] { --nm-f: 29px; --sc-f: 28px; --rk-f: 19px; --mem-f: 16px; --row-py: 15px; --row-px: 18px; --row-gap: 12px; --row-max: 116px; }
.scorerail.has-news[data-density="md"] { --nm-f: 24px; --sc-f: 24px; --rk-f: 17px; --mem-f: 14px; --row-py: 11px; --row-px: 15px; --row-gap: 9px;  --row-max: 88px; }
.scorerail.has-news[data-density="sm"] { --nm-f: 21px; --sc-f: 22px; --rk-f: 15px; --mem-f: 13px; --row-py: 7px;  --row-px: 12px; --row-gap: 5px; --row-r: 9px; --row-max: 62px; }
.scorerail.has-news[data-density="xs"] { --nm-f: 19px; --sc-f: 20px; --rk-f: 14px; --mem-f: 12px; --row-py: 5px;  --row-px: 10px; --row-gap: 4px; --row-r: 8px; --row-max: 54px; }

.scorerail .rail-title {
  font-size: 19px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--s-dim); flex: none; margin-bottom: 3px;
}
.scorerail .rail-sub {
  font-size: 17px; color: var(--s-dim); flex: none; margin-bottom: 18px;
}

/* rows are content-sized and centred, so a two-team board reads big without
   stretching into two absurd slabs */
.scorerail .rows {
  flex: 1; min-height: 0;
  display: flex; flex-direction: column; justify-content: safe center;
  gap: var(--row-gap);
  overflow: hidden;
}

.scorerail .srow {
  flex: 1 1 auto;
  max-height: var(--row-max);
  display: flex; align-items: center; gap: 11px;
  padding: var(--row-py) var(--row-px);
  border-radius: var(--row-r);
  background: var(--s-card);
  border-left: 9px solid var(--s-rule);
  transition: background .3s;
}

.scorerail .srow .rk {
  flex: none; min-width: 1.1em;
  font-size: var(--rk-f); font-weight: 700; color: var(--s-dim);
  font-variant-numeric: tabular-nums;
}

/* the name column truncates; the score column never gets squeezed */
.scorerail .srow .who { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
/* Two lines beats an ellipsis on a board read from across a room: "The
   Quizzinators" should stay readable rather than becoming "The …". */
.scorerail .srow .nm {
  font-size: var(--nm-f); font-weight: 650; line-height: 1.1;
  letter-spacing: -.015em;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
  word-break: normal; overflow-wrap: break-word;
}
.scorerail .srow .mem {
  font-size: var(--mem-f); color: var(--s-dim); line-height: 1.2;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.scorerail .srow .sc {
  flex: none;
  font-size: var(--sc-f); font-weight: 750;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.02em;
}

.scorerail .srow.lead { background: var(--s-card); outline: 1px solid var(--s-accent); outline-offset: -1px; }
/* On a dark ground the leader is lifted rather than outlined: every dark
   theme's --s-card is translucent white at 6-8%, and a 1px accent outline on
   top of that reads as noise next to the glow of the accent itself. The rail
   takes its ground from the slide beside it, so this matches whichever side
   of a per-layout flip is on screen. */
[data-ground="dark"] .scorerail .srow.lead,
.scorerail[data-ground="dark"] .srow.lead { background: rgba(255,255,255,.17); outline: none; }
.scorerail .srow.gain { animation: sf-score-pop .85s ease; }
@keyframes sf-score-pop {
  0%   { transform: scale(1); }
  28%  { transform: scale(1.035); background: rgba(52,201,138,.36); }
  100% { transform: scale(1); }
}

.scorerail .more {
  flex: none; text-align: center;
  font-size: var(--mem-f); color: var(--s-dim); padding-top: 4px;
}

.scorerail .empty-rail {
  font-size: 21px; color: var(--s-dim); line-height: 1.45; text-align: center;
  padding: 0 6px;
}
.scorerail .foot {
  flex: none; padding-top: 12px;
  border-top: 1px solid var(--s-rule);
}
.scorerail .foot .notes { font-size: 15px; color: var(--s-dim); line-height: 1.4; }

/* the standing join prompt: quiet, but the PIN itself has to be readable */
.scorerail .joinline {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  margin-bottom: 6px;
}
.scorerail .joinline .jl-lbl {
  font-size: 12px; letter-spacing: .14em; color: var(--s-dim);
}
/* Same reasoning as .rj-lbl.shut: an accent as text is not guaranteed to
   contrast with the slide it sits on. */
.scorerail .joinline.shut .jl-lbl { color: var(--s-fg); }
.scorerail .joinline .jl-url {
  font-size: 15px; color: var(--s-dim);
  font-family: ui-monospace, monospace;
}
.scorerail .joinline .jl-pin {
  font-size: 25px; font-weight: 800; letter-spacing: .07em;
  color: var(--s-fg); font-variant-numeric: tabular-nums;
  margin-left: auto;
}

/* ---- full-screen join card (J during a live game) ---- */

#joincard {
  position: fixed; inset: 0; z-index: 118;
  background: rgba(5,7,11,.95);
  display: none; place-items: center; text-align: center;
  color: #fff;
}
#joincard.on { display: grid; }
#joincard .lead {
  font-size: 20px; text-transform: uppercase; letter-spacing: .16em; color: #8f97a6;
}
#joincard .url {
  font-size: 46px; margin-top: 10px; color: #e8ecf4;
  font-family: ui-monospace, monospace; word-break: break-all;
  padding: 0 24px;
}
#joincard .pin {
  font-size: 152px; font-weight: 800; letter-spacing: .16em; line-height: 1.05;
  font-variant-numeric: tabular-nums;
  background: linear-gradient(135deg, #6ba7ff, #b58bff);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
#joincard .teams { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; margin-top: 24px; }
#joincard .tchip {
  padding: 10px 22px; border-radius: 999px;
  font-size: 22px; font-weight: 700; color: #fff;
}
#joincard .state {
  margin-top: 26px; font-size: 22px; font-weight: 650;
  display: inline-block; padding: 8px 22px; border-radius: 999px;
}
#joincard .state.open { color: #45d69b; background: rgba(69,214,155,.14); }
#joincard .state.shut { color: #ffc75a; background: rgba(255,199,90,.14); }
#joincard .count { margin-top: 18px; font-size: 20px; color: #b9c0cd; }
#joincard .dismiss { margin-top: 30px; font-size: 15px; color: #6c7482; }

/* Room sidebar (◧), join QR (◉), and expand (⛶) stay on the HUD for every
   show. Host live fills them; Present alone still offers the controls. */
.hud button[data-act="join"],
.hud button[data-act="focus"],
.hud button[data-act="rail"] { display: inline-block; }
.hud button[data-act="rail"].on,
.hud button[data-act="join"].on,
.hud button[data-act="focus"].on { background: rgba(255,255,255,.22); }


/* ---- make room for the rail; all offsets derive from --rail-w ---- */

.deck-viewport.railed .slide .pad { padding-right: calc(var(--rail-w) + 54px); }
.deck-viewport.railed .layout-image .pad,
.deck-viewport.railed .layout-video .pad { padding-right: 0; }
.deck-viewport.railed .layout-image .img,
.deck-viewport.railed .layout-video .img { right: var(--rail-w); }
.deck-viewport.railed .layout-video .vid { right: var(--rail-w); width: auto; }
.deck-viewport.railed .layout-image .cap,
.deck-viewport.railed .layout-video .cap { right: var(--rail-w); padding-right: 46px; }
.deck-viewport.railed .layout-table .tbl { font-size: .84em; }
.deck-viewport.railed .layout-split .pad.split-pad,
#previewBox.railed .layout-split .pad.split-pad {
  padding-right: 0;
  width: calc(100% - var(--rail-w));
  box-sizing: border-box;
}
.deck-viewport.railed .layout-split .split-copy h2,
#previewBox.railed .layout-split .split-copy h2 { font-size: 34px; }
.deck-viewport.railed .layout-split .split-copy li,
#previewBox.railed .layout-split .split-copy li { font-size: 22px; }
.deck-viewport.railed .slide .pagenum { right: calc(var(--rail-w) + 34px); }
.deck-viewport.railed .slide-logo { right: calc(var(--rail-w) + 34px); }
.deck-viewport.railed .layout-title .slide-logo,
.deck-viewport.railed .layout-section .slide-logo { right: calc(var(--rail-w) + 44px); }
.deck-viewport.railed .layout-image .slide-logo { right: calc(var(--rail-w) + 26px); }
.deck-viewport.railed .layout-quiz.has-clock .slide-logo { right: calc(var(--rail-w) + 140px); }
.deck-viewport.railed .layout-quiz .clock { right: calc(var(--rail-w) + 38px); }
.deck-viewport.railed .leaderboard { padding-right: calc(var(--rail-w) + 54px); }

/* ---- railed question layout ----
   With ~860px of usable width, one full-width column per answer reads far
   better from a distance than two narrow ones, and it uses the height. */

/* Shorthand `padding` here would reset the right padding that clears the
   rail, so the right value is restated explicitly. */
.deck-viewport.railed .layout-quiz .pad {
  padding: 50px calc(var(--rail-w) + 54px) 54px 56px;
}
.deck-viewport.railed .layout-quiz .opts { gap: 14px; margin-top: 26px; }
.deck-viewport.railed .layout-quiz .opts.stack { grid-template-columns: 1fr; }
.deck-viewport.railed .layout-quiz.has-clock .qhead { padding-right: 104px; }

.deck-viewport.railed .layout-results .donut { width: 220px; height: 220px; }
.deck-viewport.railed .layout-results .score-hero { gap: 36px; }
.deck-viewport.railed .layout-results .row { font-size: 21px; padding: 11px 15px; }
.deck-viewport.railed .layout-title h1,
.deck-viewport.railed .layout-section h1 { font-size: 66px; }
.deck-viewport.railed .layout-content li { font-size: 29px; }
.deck-viewport.railed .layout-quote .q { font-size: 40px; }


/* solo (no audience) running tally, shown on quiz and score slides */
.soloscore {
  position: absolute; right: 0; bottom: 0;
  transform-origin: 100% 100%;
  z-index: 3;
  margin: 0 34px 30px 0;
  display: flex; align-items: center; gap: 10px;
  padding: 10px 18px;
  border-radius: 999px;
  background: rgba(0,0,0,.42);
  border: 2px solid var(--s-rule);
  color: var(--s-fg);
  font-family: var(--s-font);
  font-size: 25px;
}
.soloscore .lbl { color: var(--s-dim); font-size: 17px; letter-spacing: .1em; text-transform: uppercase; }
.soloscore .val { font-weight: 750; font-variant-numeric: tabular-nums; }

/* team picker in the live lobby */
#lobby .teams {
  display: flex; flex-wrap: wrap; gap: 10px; justify-content: center;
  margin: 26px auto 0; max-width: 780px;
}
#lobby .tchip {
  display: flex; align-items: center; gap: 9px;
  border-radius: 12px; padding: 10px 18px;
  font-size: 17px; font-weight: 600;
  background: rgba(255,255,255,.08);
  border: 2px solid transparent;
}
#lobby .tchip .swatch { width: 14px; height: 14px; border-radius: 4px; flex: none; }
#lobby .tchip .n { color: #b9c0cd; font-weight: 500; font-size: 15px; }



/* ==========================================================================
   7. Game embed slide (editor only — replaced by real questions at showtime)
   ========================================================================== */

.layout-game .pad { justify-content: center; align-items: center; }

.gamecard {
  width: 100%; max-width: 900px;
  border: 4px dashed var(--s-rule);
  border-radius: 22px;
  padding: 48px 52px;
  background: var(--s-card);
  display: flex; flex-direction: column; gap: 22px;
}
.gamecard .gc-top { display: flex; align-items: center; gap: 16px; }
.gamecard .gc-badge {
  font-size: 17px; font-weight: 800; letter-spacing: .16em;
  padding: 7px 15px; border-radius: 8px;
  background: linear-gradient(120deg, var(--s-accent), var(--s-accent-2));
  color: #0b1020;
}
.theme-mono .gamecard .gc-badge { color: #0c0c0c; }
.gamecard .gc-note { font-size: 20px; color: var(--s-dim); }
.gamecard .gc-title { font-size: 58px; font-weight: 700; line-height: 1.1; letter-spacing: -.02em; }
.gamecard .gc-facts { display: flex; flex-wrap: wrap; gap: 14px; font-size: 22px; color: var(--s-dim); }
.gamecard .gc-fact {
  padding: 7px 16px; border-radius: 999px;
  border: 2px solid var(--s-rule);
}
.gamecard .gc-teams { display: flex; flex-wrap: wrap; gap: 11px; }
.gamecard .gc-team {
  padding: 8px 18px; border-radius: 999px;
  font-size: 21px; font-weight: 700; color: #fff;
}

/* ==========================================================================
   8. Workspace switch — the presentation engine and the game engine share
   one shell; [data-ws] marks controls that belong to only one of them.
   ========================================================================== */

.wsswitch {
  display: flex; gap: 2px;
  background: var(--ui-bg);
  border: 1px solid var(--ui-line);
  border-radius: 8px;
  padding: 2px;
  flex: none;
}
.wsswitch button {
  background: transparent; border: 0; cursor: pointer;
  color: var(--ui-dim);
  padding: 5px 12px; border-radius: 6px;
  font-size: 13px; font-weight: 550; white-space: nowrap;
}
.wsswitch button:hover { color: var(--ui-text); }
.wsswitch button.on { background: var(--ui-panel-2); color: var(--ui-text); }

.vsep { width: 1px; height: 22px; background: var(--ui-line); margin: 0 2px; flex: none; }

/* Prefer html[data-ws] (set in <head> before CSS) so a refresh never shows
   both studios. body.ws-* stays for anything that still keys off the body. */
/* Hide every control that belongs to a studio other than the active one.

   ~= lets one control name several studios (e.g. Host live on deck + game)
   without a third studio having to invent a special case. Exact-match
   [data-ws="deck"] still works for single-studio controls. */
html[data-ws="deck"] [data-ws]:not([data-ws~="deck"]),
html[data-ws="game"] [data-ws]:not([data-ws~="game"]),
html[data-ws="plan"] [data-ws]:not([data-ws~="plan"]),
html:not([data-ws]) [data-ws],
body.ws-deck [data-ws]:not([data-ws~="deck"]),
body.ws-game [data-ws]:not([data-ws~="game"]),
body.ws-plan [data-ws]:not([data-ws~="plan"]) { display: none !important; }

.rail-count {
  font-size: 10px; color: var(--ui-dim); flex: none;
  background: var(--ui-bg); border-radius: 999px; padding: 1px 7px;
  font-variant-numeric: tabular-nums;
}
/* The label takes the slack so the count and the cog stay together on the
   right, rather than space-between spreading all three across the head. */
.rail-head #railLabel { flex: 1 1 auto; min-width: 0; }
.rail-tools { display: flex; align-items: center; gap: 6px; flex: none; }
html[data-ws="deck"] #railCount { display: none; }
.rail-go {
  display: flex; align-items: center; gap: 2px;
  background: var(--ui-bg); border-radius: 999px; padding: 1px 7px 1px 4px;
  font-size: 10px; color: var(--ui-dim); font-variant-numeric: tabular-nums;
  letter-spacing: 0; text-transform: none;
}
.rail-go input {
  width: 2.2em; border: 0; background: transparent; padding: 0;
  font: inherit; color: var(--ui-text); text-align: right;
  -moz-appearance: textfield;
}
.rail-go input:focus { outline: none; color: var(--ui-text); }
.rail-go input::-webkit-outer-spin-button,
.rail-go input::-webkit-inner-spin-button { appearance: none; margin: 0; }
.rail-cog {
  flex: none; width: 22px; height: 22px; padding: 0;
  display: grid; place-items: center;
  font-size: 13px; line-height: 1; cursor: pointer;
  color: var(--ui-dim); background: none;
  border: 1px solid transparent; border-radius: 5px;
}
.rail-cog:hover { color: var(--ui-text); background: var(--ui-bg); border-color: var(--ui-line); }
.rail-cog:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: 1px; }

/* question rows in the game workspace rail */
.qthumb {
  display: flex; gap: 9px; align-items: flex-start;
  cursor: pointer; border-radius: 7px; padding: 8px 9px;
  border: 2px solid transparent;
  background: var(--ui-bg);
  flex-shrink: 0;
}
.qthumb:hover { background: var(--ui-panel-2); }
.qthumb.sel { border-color: var(--ui-accent); background: var(--ui-accent-soft); }
.qthumb .qn {
  flex: none; width: 22px; height: 22px; border-radius: 6px;
  background: var(--ui-panel-2); color: var(--ui-dim);
  display: grid; place-items: center; font-size: 11px; font-weight: 700;
}
.qthumb.sel .qn { background: var(--ui-accent); color: #fff; }
.qthumb .qbody { flex: 1; min-width: 0; }
.qthumb .qtext {
  font-size: 12px; line-height: 1.35; color: var(--ui-text);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.qthumb .qmeta { font-size: 10px; color: var(--ui-dim); margin-top: 4px; display: flex; gap: 7px; }
.qthumb .qmeta .warn { color: var(--ui-warn); }
.qthumb .qmeta .why { color: var(--ui-good); }
.qthumb .qmeta .pic { color: var(--ui-accent); }

/* game settings sheet bits */
.setrow { display: flex; gap: 10px; }
.setrow > * { flex: 1; }

/* the running tally owns the bottom-right corner while it is on screen */
.deck-viewport.soloed .slide .pagenum { display: none; }

/* ==========================================================================
   9. Explanation slide
   Generated after a question that has an explanation. Sized so the reasoning
   is readable from the back of a room, and the type steps down as the text
   gets longer rather than spilling off the slide.
   ========================================================================== */

.layout-explain .pad { padding: 60px 84px 56px; justify-content: flex-start; }

.layout-explain .ex-head {
  display: flex; align-items: baseline; gap: 16px; flex: none;
  margin-bottom: 26px;
}
.layout-explain .ex-qn {
  flex: none; font-size: 18px; font-weight: 700; letter-spacing: .1em;
  color: var(--s-accent);
}
.layout-explain .ex-q {
  font-size: 27px; line-height: 1.25; color: var(--s-dim);
}

.layout-explain .ex-answer {
  display: flex; align-items: center; gap: 22px; flex: none;
  background: rgba(52,201,138,.16);
  border: 3px solid #34c98a;
  border-radius: 15px;
  padding: 18px 26px;
  font-size: 38px; font-weight: 650;
  margin-bottom: 34px;
}
.layout-explain .ex-answer .key {
  flex: none; width: 62px; height: 62px; border-radius: 12px;
  display: grid; place-items: center;
  background: #34c98a; color: #06281a;
  font-weight: 800; font-size: 30px;
}
.layout-explain .ex-answer .txt { flex: 1; }
.layout-explain .ex-answer .tick { flex: none; font-size: 36px; color: #34c98a; }

.layout-explain .ex-body {
  flex: 1; min-height: 0; overflow: hidden;
  display: flex; flex-direction: column; gap: 18px;
}
.layout-explain .ex-body p { margin: 0; }
.layout-explain .ex-body[data-len="md"] p { font-size: 34px; line-height: 1.42; }
.layout-explain .ex-body[data-len="lg"] p { font-size: 29px; line-height: 1.45; }
.layout-explain .ex-body[data-len="xl"] p { font-size: 24px; line-height: 1.48; }

.layout-explain .ex-source {
  flex: none; margin-top: 18px; padding-top: 16px;
  border-top: 1px solid var(--s-rule);
  font-size: 20px; color: var(--s-dim);
}

/* railed: the rail takes ~30%, so ease the answer and body down a step */
.deck-viewport.railed .layout-explain .pad {
  padding: 50px calc(var(--rail-w) + 54px) 50px 56px;
}
.deck-viewport.railed .layout-explain .ex-q { font-size: 24px; }
.deck-viewport.railed .layout-explain .ex-answer { font-size: 32px; padding: 15px 22px; margin-bottom: 26px; }
.deck-viewport.railed .layout-explain .ex-answer .key { width: 52px; height: 52px; font-size: 26px; }
.deck-viewport.railed .layout-explain .ex-body[data-len="md"] p { font-size: 29px; }
.deck-viewport.railed .layout-explain .ex-body[data-len="lg"] p { font-size: 25px; }
.deck-viewport.railed .layout-explain .ex-body[data-len="xl"] p { font-size: 21px; }

/* ==========================================================================
   10. Inline explanation
   On reveal the correct answer's box grows to hold the reasoning, and the
   wrong answers compact to make room. The question and the other options stay
   on screen, which is the point: the room can see what they got wrong while
   they read why.
   ========================================================================== */

.opt .why { display: none; }

.layout-quiz.why-open .opt.correct .why {
  display: flex; flex-direction: column; gap: 12px;
  margin-top: 16px; padding-top: 16px;
  border-top: 2px solid rgba(52, 201, 138, .45);
  animation: sf-why-in .34s ease both;
}
.layout-quiz.why-open .opt.correct .why .p {
  font-weight: 400; letter-spacing: 0;
  color: var(--s-fg);
}
/* --why-f is a starting point only; fitInlineWhy() in player.js measures the
   rendered result and steps it down until it genuinely fits the slide. Text on
   a fixed 1280x720 canvas cannot be trusted to fit from a character count. */
.layout-quiz.why-open .opt.correct .why { --why-f: 27px; }
.layout-quiz.why-open .opt.correct .why[data-len="lg"] { --why-f: 24px; }
.layout-quiz.why-open .opt.correct .why[data-len="xl"] { --why-f: 21px; }
.layout-quiz.why-open .opt.correct .why[data-len="xs"] { --why-f: 18px; }
.layout-quiz.why-open .opt.correct .why .p {
  font-size: var(--why-f);
  line-height: 1.42;
}
.layout-quiz.why-open .opt.correct .why .src {
  font-size: 17px; color: var(--s-dim); font-weight: 400;
}

@keyframes sf-why-in {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* the grid stops sharing height equally so the correct box can take what it
   needs and the rest shrink to a compact line */
.layout-quiz.why-open .opts,
.layout-quiz.why-open .opts.stack {
  grid-template-columns: 1fr;
  grid-auto-rows: auto;
  align-content: start;
  gap: 11px;
}
.layout-quiz.why-open .opt { transition: none; }
.layout-quiz.why-open .opt { --opt-f: 33px; }
.layout-quiz.why-open .opt.muted,
.layout-quiz.why-open .opt.wrong {
  --opt-f: 24px;
  padding: 9px 22px;
}
/* the key badge and tick already scale from --opt-f */

/* with the rail up there is less width, so ease the body down one more step */
.deck-viewport.railed .layout-quiz.why-open .opt.correct .why { --why-f: 24px; }
.deck-viewport.railed .layout-quiz.why-open .opt.correct .why[data-len="lg"] { --why-f: 21px; }
.deck-viewport.railed .layout-quiz.why-open .opt.correct .why[data-len="xl"] { --why-f: 19px; }
.deck-viewport.railed .layout-quiz.why-open .opt.correct .why[data-len="xs"] { --why-f: 17px; }
.deck-viewport.railed .layout-quiz.why-open .opt { --opt-f: 29px; }
.deck-viewport.railed .layout-quiz.why-open .opt.muted,
.deck-viewport.railed .layout-quiz.why-open .opt.wrong { --opt-f: 21px; padding: 8px 18px; }

/* Last resort for a very long explanation: drop the wrong answers to give the
   reasoning the whole panel. Only used when shrinking the type is not enough. */
.layout-quiz.why-only .opt.muted,
.layout-quiz.why-only .opt.wrong { display: none; }

/* Safety net so a mis-fit can never paint over the neighbouring answers the
   way an unclamped box does. */
.layout-quiz.why-open .opt { overflow: hidden; }

/* ==========================================================================
   11. Question image
   A fixed band between the question and the answers. Fixed rather than
   flexible so the answer grid stays constrained and fitQuizSlide() can still
   measure whether the answers fit — a flexible image would absorb the slack
   and the fitter would always think there was room.
   ========================================================================== */

.layout-quiz .qmedia {
  flex: 0 0 auto;
  height: 220px;
  margin: 14px 0 6px;
  display: flex; align-items: center; justify-content: center;
  min-height: 0;
}
.layout-quiz .qmedia img {
  max-width: 100%; max-height: 100%;
  object-fit: contain;
  border-radius: 12px;
  background: var(--s-card);
}
/* a URL that fails to load should collapse, not leave a hole */
.layout-quiz .qmedia.broken { display: none; }

.deck-viewport.railed .layout-quiz .qmedia { height: 180px; margin: 10px 0 4px; }

/* Once the explanation is open the reasoning matters more than the picture,
   but the picture is often what the reasoning refers to — so shrink it rather
   than dropping it. */
.layout-quiz.why-open .qmedia { height: 96px; margin: 8px 0 4px; }
/* Dropped only as the last resort, after the wrong answers have already gone:
   when a question is about a picture, losing the picture costs more than
   losing the options. */
.layout-quiz.why-nomedia .qmedia { display: none; }

/* ==========================================================================
   12. Image layouts
   'band'    question, then the image, then the answers  (the default)
   'first'   the image leads and the question sits under it
   'overlay' the image leads and the question sits on it behind a gradient
   ========================================================================== */

/* ---- image first: the picture leads, question in its own space below ---- */
.layout-quiz.media-first .qmedia { height: 210px; margin: 0 0 14px; }
.layout-quiz.media-first .qhead { margin-bottom: 4px; }
.deck-viewport.railed .layout-quiz.media-first .qmedia { height: 168px; }

/* ---- overlay: the image is the slide's top third and carries the question --
   `cover` rather than `contain` here on purpose: the picture has to reach the
   edges for the gradient to read as part of it. That crops, which suits photos
   and not diagrams — which is why it is a per-question choice. */
.layout-quiz.media-overlay .qmedia {
  height: 340px;
  margin: 0 0 8px;
  position: relative;
  border-radius: 16px;
  overflow: hidden;
  display: block;
}
.layout-quiz.media-overlay .qmedia img {
  width: 100%; height: 100%;
  max-width: none; max-height: none;
  object-fit: cover;
  border-radius: 0;
  display: block;
}
.layout-quiz.media-overlay .qmedia .qhead {
  position: absolute; left: 0; right: 0; bottom: 0;
  margin: 0;
  padding: 90px 34px 24px;
  /* three stops rather than two: a straight fade leaves the middle of a
     descender sitting on mid-tone image and it goes muddy */
  background: linear-gradient(
    to bottom,
    rgba(0,0,0,0) 0%,
    rgba(0,0,0,.45) 42%,
    rgba(0,0,0,.82) 78%,
    rgba(0,0,0,.92) 100%
  );
}
.layout-quiz.media-overlay .qmedia .q { color: #fff; text-shadow: 0 2px 14px rgba(0,0,0,.5); }
.layout-quiz.media-overlay .qmedia .qnum { color: #cfe0ff; }
.layout-quiz.media-overlay.has-clock .qhead { padding-right: 34px; }

.deck-viewport.railed .layout-quiz.media-overlay .qmedia { height: 300px; }

/* the countdown sits over the picture in overlay mode, so give it a disc to
   stand on or it disappears against a light image. A thick outline rather
   than a pseudo-element: no extra stacking context to get wrong, and no
   blur — the app draws no shadows. */
.layout-quiz.media-overlay .clock {
  z-index: 2;
  background: rgba(6,10,18,.58);
  border-radius: 50%;
  outline: 10px solid rgba(6,10,18,.58);
}

/* reveal: the image shrinks to a strip. An overlaid question is moved out of
   the image by the player first (flattenOverlay), because hiding it would take
   away what was actually asked just as the explanation appears. */
.layout-quiz.why-open.media-first .qmedia { height: 96px; }

/* ==========================================================================
   13. Horse race track
   Shown as an overlay after each reveal. Laid out as a grid so step N sits at
   the same x across every lane — comparing lanes is the entire point of a
   race, and per-lane flex layout would let them drift.
   ========================================================================== */

.layout-race .pad { justify-content: center; padding: 54px 72px; }

.layout-race .race-title {
  font-size: 52px; font-weight: 700; letter-spacing: -.02em;
  margin-bottom: 6px; flex: none;
}
.layout-race .race-note {
  font-size: 23px; color: var(--s-dim); margin-bottom: 30px; flex: none;
}

.layout-race .racetrack { display: flex; flex-direction: column; gap: 16px; }

.layout-race .lane {
  display: grid;
  grid-template-columns: 300px 1fr 130px;
  align-items: center;
  gap: 22px;
}

.layout-race .lane-name { display: flex; align-items: center; gap: 14px; min-width: 0; }
.layout-race .lane-dot { width: 20px; height: 20px; border-radius: 5px; flex: none; }
.layout-race .lane-text {
  font-size: 31px; font-weight: 650;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.layout-race .lane-rail {
  position: relative;
  display: grid;
  grid-template-columns: repeat(var(--steps), 1fr);
  gap: 7px;
  height: 54px;
}
.layout-race .step {
  border-radius: 7px;
  background: var(--s-card);
  border: 2px solid var(--s-rule);
}
/* completed steps take the lane's own colour, so you can read a lane's
   progress at a glance without counting */
.layout-race .step.done {
  background: var(--lane-tint, rgba(255,255,255,.16));
  border-color: var(--lane-color, rgba(255,255,255,.3));
}
.layout-race .step.finish {
  border-color: var(--s-accent-2);
  background:
    repeating-linear-gradient(45deg,
      rgba(255,255,255,.22) 0 8px, transparent 8px 16px);
}

/* the runner rides above the cells so it can transition between them */
.layout-race .runner {
  position: absolute; top: 50%;
  width: 58px; height: 58px;
  margin-left: -29px;
  transform: translateY(-50%);
  display: grid; place-items: center;
  font-size: 34px;
  border-radius: 50%;
  background: var(--lane-color, var(--s-accent));
  transition: left .9s cubic-bezier(.34, 1.2, .5, 1);
}
.layout-race .lane.moved .runner { animation: sf-gallop .9s ease; }
@keyframes sf-gallop {
  0%   { transform: translateY(-50%) scale(1); }
  35%  { transform: translateY(-70%) scale(1.12); }
  100% { transform: translateY(-50%) scale(1); }
}

.layout-race .lane-pos {
  font-size: 27px; font-weight: 700; color: var(--s-dim);
  font-variant-numeric: tabular-nums; text-align: right;
}
.layout-race .lane.won .lane-text { color: var(--s-accent-2); }
.layout-race .lane.won .lane-pos { color: var(--s-accent-2); }

.layout-boss .pad { justify-content: center; padding: 72px 80px; gap: 22px; }
.layout-boss .boss-title { font-size: 48px; font-weight: 750; letter-spacing: -.02em; }
.layout-boss .boss-note { font-size: 22px; color: var(--s-muted, #9aa3b5); }
.layout-boss .boss-meter {
  width: min(720px, 100%); height: 28px; border-radius: 999px;
  background: rgba(255,255,255,.12); overflow: hidden; border: 1px solid rgba(255,255,255,.18);
}
.layout-boss .boss-fill {
  height: 100%; width: 0; border-radius: inherit;
  background: linear-gradient(90deg, #c43c3c, #e08a3a);
  transition: width .45s ease;
}
.layout-boss.boss-down .boss-fill { background: linear-gradient(90deg, #3d8b5a, #6bc48a); }
.layout-boss.boss-hit .boss-meter { animation: sf-boss-hit .35s ease; }
.layout-boss .boss-hp { font-size: 28px; font-variant-numeric: tabular-nums; font-weight: 650; }

.layout-wordreveal .pad { justify-content: center; padding: 64px 72px; gap: 20px; text-align: center; }
.layout-wordreveal .wr-title { font-size: 28px; font-weight: 650; letter-spacing: -.01em; color: var(--s-muted, #9aa3b5); }
.layout-wordreveal .wr-hint { font-size: 24px; color: var(--s-muted, #9aa3b5); }
.layout-wordreveal .wr-mask {
  font-size: 56px; font-weight: 750; letter-spacing: .18em;
  font-variant-numeric: tabular-nums; word-break: break-word;
}
.layout-wordreveal .wr-meta { font-size: 20px; color: var(--s-muted, #9aa3b5); }

.layout-study .pad { justify-content: center; padding: 72px 80px; gap: 18px; text-align: center; }
.layout-study .study-term { font-size: 52px; font-weight: 750; letter-spacing: -.02em; }
.layout-study .study-def { font-size: 28px; color: var(--s-muted, #9aa3b5); max-width: 36em; margin: 0 auto; }
.layout-study .study-note { font-size: 18px; color: var(--s-muted, #9aa3b5); }

/* ---- catalogue presentations: stage shape on the house theme ------------ */

/* Keep the deck theme. Stages differ by layout, not by painting a new room. */
.layout-quiz[class*="present-"]::before {
  content: none;
}
.layout-quiz[class*="present-"] > .pad,
.layout-quiz[class*="present-"] > .pagenum,
.layout-quiz[class*="present-"] > .track,
.layout-quiz[class*="present-"] > .slide-logo {
  position: relative; z-index: 1;
}
/* Clock stays absolute — the house top-right countdown, never in the flow. */
.layout-quiz[class*="present-"] > .clock {
  z-index: 2;
}

.layout-quiz.present-truefalse .pad,
.layout-quiz.present-headsup .pad,
.layout-quiz.present-spin .pad,
.layout-quiz.present-wordreveal .pad,
.layout-quiz.present-claim .pad,
.layout-quiz.present-memorymatch .pad,
.layout-quiz.present-knowledge .pad,
.layout-quiz.present-connection .pad,
.layout-quiz.present-challenge .pad,
.layout-quiz.present-emoji .pad,
.layout-quiz.present-blanks .pad {
  justify-content: center;
}

.layout-quiz .stage-hero {
  position: relative;
  flex: none;
  z-index: 1;
  display: flex; flex-direction: column; align-items: center; gap: 16px;
  text-align: center; margin-bottom: 12px; width: 100%;
}
.layout-quiz .stage-atmosphere {
  pointer-events: none;
  position: absolute; inset: -80px -100px auto;
  height: 280px; z-index: 0;
  opacity: .4;
  background:
    radial-gradient(ellipse 70% 80% at 50% 0%,
      color-mix(in srgb, var(--s-accent) 22%, transparent), transparent 70%);
}
.layout-quiz .stage-kicker {
  position: relative; z-index: 1;
  font-size: 15px; letter-spacing: .22em; text-transform: uppercase;
  color: var(--s-accent); font-weight: 700;
}
.layout-quiz .stage-note {
  position: relative; z-index: 1;
  font-size: 18px; color: var(--s-dim); max-width: 28em;
}

/* True / False — statement owns the room; two tall doors (theme boxes) */
.layout-quiz.present-truefalse .q {
  --q-f: 52px; text-align: center; max-width: 18em; margin: 0 auto;
}
.layout-quiz.present-truefalse .qhead { justify-content: center; margin-bottom: 8px; }
.layout-quiz.present-truefalse .opts.tf-duo {
  grid-template-columns: 1fr 1fr; gap: 28px; margin-top: 36px;
  max-width: 920px; margin-left: auto; margin-right: auto;
}
.layout-quiz.present-truefalse .opts.tf-duo .opt {
  min-height: 168px; justify-content: center; --opt-f: 44px;
  border-radius: 14px; border-width: 3px;
  background: var(--s-card);
  border-color: var(--s-rule);
}
.layout-quiz.present-truefalse .opts.tf-duo .opt:nth-child(1) {
  border-color: color-mix(in srgb, var(--s-good, #34c98a) 55%, var(--s-rule));
}
.layout-quiz.present-truefalse .opts.tf-duo .opt:nth-child(2) {
  border-color: color-mix(in srgb, #ff5f6d 55%, var(--s-rule));
}
.layout-quiz.present-truefalse .opts.tf-duo .opt .key {
  width: calc(var(--opt-f) * 1.4); height: calc(var(--opt-f) * 1.4);
  font-size: calc(var(--opt-f) * .9); border-radius: 50%;
}
.layout-quiz.present-truefalse .tf-atmosphere {
  position: absolute; inset: 0; pointer-events: none; z-index: 0;
  background:
    linear-gradient(105deg,
      color-mix(in srgb, var(--s-good, #34c98a) 8%, transparent) 0%,
      transparent 42%,
      transparent 58%,
      color-mix(in srgb, #ff5f6d 8%, transparent) 100%);
}

/* Beat the Clock — taut options; clock stays in the house position */
.layout-quiz.present-speed .q { --q-f: 42px; }
.layout-quiz.present-speed .opts.speed-opts {
  gap: 14px; margin-top: 22px;
}
.layout-quiz.present-speed .opts.speed-opts .opt {
  border-width: 3px; border-radius: 14px;
  background: var(--s-card);
}

/* Boss — one band: what you are hitting, what a hit is worth, what it has left.
   Stacked down the middle it was a crest, a badge, a bar, a turn line and two
   buttons: about 240px of furniture above the answers, which then had to
   overflow something. In a row it reads left to right in one glance and gives
   the question back its slide. */
.layout-quiz.present-boss .boss-stage {
  display: flex; flex-direction: row; flex-wrap: wrap;
  align-items: center; justify-content: center; gap: 4px 20px;
  margin-bottom: 6px;
}
.layout-quiz.present-boss .boss-crest {
  position: relative; z-index: 1;
  display: flex; flex-direction: row; align-items: center; gap: 12px;
}
.layout-quiz.present-boss .boss-glyph {
  font-size: 34px; line-height: 1; color: var(--s-accent);
  animation: sf-boss-bob 2.4s ease-in-out infinite;
}
.layout-quiz.present-boss .boss-hit-badge {
  font-size: 16px; font-weight: 750; letter-spacing: .06em; text-transform: uppercase;
  padding: 7px 14px; border-radius: 12px;
  background: var(--s-card);
  border: 2px solid var(--s-accent);
  color: var(--s-fg);
}
/* Centred on the band now that the band is one line — anchored to the top,
   the gradient's strong edge cut a pink stripe across the question text. */
.layout-quiz.present-boss .stage-atmosphere {
  inset: -22px -100px auto; height: 150px; opacity: .3;
  background:
    radial-gradient(ellipse 80% 100% at 50% 50%,
      color-mix(in srgb, var(--s-accent) 16%, transparent), transparent 72%);
}
@keyframes sf-boss-bob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-6px); }
}

/* Word Reveal — physical letter tiles on theme surfaces */
.layout-quiz.present-wordreveal .wr-stage { margin-top: 8px; min-height: 280px; justify-content: center; }
.layout-quiz.present-wordreveal .wr-board {
  position: relative; z-index: 1;
  display: flex; flex-wrap: wrap; gap: 10px; justify-content: center;
  max-width: 1000px;
}
.layout-quiz.present-wordreveal .wr-gap { width: 28px; }
.layout-quiz.present-wordreveal .wr-tile {
  width: 64px; height: 78px;
  display: grid; place-items: center;
  font-size: 36px; font-weight: 800; letter-spacing: 0;
  border-radius: 12px;
  border: 3px solid var(--s-rule);
  background: var(--s-card);
  color: var(--s-fg);
  animation: sf-tile-in .45s cubic-bezier(.2,.8,.2,1) both;
}
.layout-quiz.present-wordreveal .wr-tile.lit {
  border-color: var(--s-accent);
  background: color-mix(in srgb, var(--s-accent) 18%, var(--s-card));
}
.layout-quiz.present-wordreveal .wr-tile.blank {
  background: transparent;
  border-style: dashed;
  color: var(--s-dim);
}
@keyframes sf-tile-in {
  from { opacity: 0; transform: translateY(12px) scale(.92); }
  to { opacity: 1; transform: none; }
}
.layout-quiz.present-wordreveal .opts.typed { margin-top: 28px; max-width: 720px; width: 100%; }

/* Memory / Knowledge — same theme boxes as answer pads (--s-card / --s-accent) */
.layout-quiz.present-claim .claim-stage,
.layout-quiz.present-memorymatch .match-stage,
.layout-quiz.present-knowledge .knowledge-stage {
  min-height: 340px; justify-content: center; gap: 18px;
}
.layout-quiz.present-claim .claim-term,
.layout-quiz.present-knowledge .claim-term {
  font-size: 52px; font-weight: 780; letter-spacing: -.03em; line-height: 1.08;
  color: var(--s-fg);
}
.layout-quiz.present-claim .claim-def,
.layout-quiz.present-knowledge .claim-def {
  margin-top: 10px; font-size: 22px; line-height: 1.4;
  color: var(--s-dim); max-width: 22em; margin-left: auto; margin-right: auto;
}
.layout-quiz.present-knowledge .claim-def.soft { opacity: .85; font-style: italic; }
.layout-quiz.present-knowledge .knowledge-chip {
  position: relative; z-index: 1;
  padding: 36px 48px; border-radius: 14px;
  background: var(--s-card);
  border: 3px solid var(--s-rule);
  box-shadow: none;
}
.layout-quiz.present-knowledge .knowledge-chip:hover {
  border-color: var(--s-accent);
}

/* Memory Flip — single turning card, theme surfaces */
.layout-quiz.present-claim .flip-card {
  position: relative; z-index: 1;
  width: min(520px, 86%);
  min-height: 280px;
  transform-style: preserve-3d;
  transition: transform .55s cubic-bezier(.2,.8,.2,1);
}
.layout-quiz.present-claim .flip-card.open { transform: rotateY(180deg); }
.layout-quiz.present-claim .flip-face {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 12px; padding: 40px 36px; border-radius: 14px;
  backface-visibility: hidden;
  background: var(--s-card);
  border: 3px solid var(--s-rule);
  color: var(--s-fg);
  box-shadow: none;
}
.layout-quiz.present-claim .flip-face.back {
  transform: rotateY(180deg);
  border-color: var(--s-accent);
}

/* Memory Match — board + active pair on theme tokens */
.layout-quiz.present-memorymatch .match-board {
  position: relative; z-index: 1;
  display: grid;
  grid-template-columns: repeat(6, minmax(52px, 72px));
  gap: 10px; justify-content: center;
  margin-bottom: 6px;
}
.layout-quiz.present-memorymatch .match-tile {
  aspect-ratio: 3 / 4;
  border-radius: 12px;
  border: 3px solid var(--s-rule);
  background: var(--s-card);
  box-shadow: none;
  display: flex; align-items: center; justify-content: center;
  color: var(--s-accent);
  font-size: 18px; padding: 0; cursor: default;
  animation: sf-tile-in .4s cubic-bezier(.2,.8,.2,1) both;
}
.layout-quiz.present-memorymatch .match-tile.ghost {
  opacity: .4;
  background: transparent;
  border-style: dashed;
  color: var(--s-dim);
}
.layout-quiz.present-memorymatch .match-tile.active {
  border-color: var(--s-accent);
  background: color-mix(in srgb, var(--s-accent) 14%, var(--s-card));
  transform: translateY(-4px);
}
.layout-quiz.present-memorymatch .match-tile-front {
  display: none;
}
.layout-quiz.present-memorymatch .match-duo {
  position: relative; z-index: 1;
  display: flex; align-items: stretch; justify-content: center;
  gap: 18px; width: min(920px, 100%);
}
.layout-quiz.present-memorymatch .match-card {
  flex: 1 1 0; min-width: 0; max-width: 380px;
  min-height: 220px; padding: 28px 26px;
  border-radius: 14px;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 12px; text-align: center;
  background: var(--s-card);
  border: 3px solid var(--s-rule);
  color: var(--s-fg);
  box-shadow: none;
  animation: sf-term-rise .5s cubic-bezier(.2,.8,.2,1) both;
}
.layout-quiz.present-memorymatch .match-card.term {
  border-color: var(--s-accent);
}
.layout-quiz.present-memorymatch .match-card.def.shut {
  background: var(--s-card);
  border-style: dashed;
  border-color: var(--s-rule);
}
.layout-quiz.present-memorymatch .match-card.def.open {
  border-color: var(--s-accent-2, var(--s-accent));
}
.layout-quiz.present-memorymatch .match-face-label,
.layout-quiz.present-claim .match-face-label {
  font-size: 13px; letter-spacing: .2em; text-transform: uppercase;
  color: var(--s-accent); font-weight: 700;
}
.layout-quiz.present-memorymatch .match-face-text {
  font-size: 36px; font-weight: 760; letter-spacing: -.02em; line-height: 1.15;
  text-wrap: balance;
  color: var(--s-fg);
}
.layout-quiz.present-memorymatch .match-face-hidden {
  font-size: 72px; font-weight: 700; line-height: 1;
  color: var(--s-dim); opacity: .7;
}
.layout-quiz.present-memorymatch .match-face-hint {
  font-size: 15px; color: var(--s-dim); max-width: 14em;
}
.layout-quiz.present-memorymatch .match-link {
  flex: none; align-self: center;
  width: 48px; height: 48px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 22px; color: var(--s-accent);
  background: var(--s-card);
  border: 2px solid var(--s-rule);
}

.layout-quiz.present-headsup .heads-stage {
  min-height: 380px; justify-content: center; padding: 24px 12px;
}
.layout-quiz.present-headsup .stage-atmosphere {
  inset: -40px -80px; height: 520px;
  background:
    radial-gradient(circle at 50% 45%,
      color-mix(in srgb, var(--s-accent) 22%, transparent), transparent 62%);
}
.layout-quiz.present-headsup .oracy-term {
  position: relative; z-index: 1;
  font-size: 88px; font-weight: 800; letter-spacing: -.04em; line-height: 1.05;
  max-width: 11em;
  text-wrap: balance;
  animation: sf-term-rise .55s cubic-bezier(.2,.8,.2,1) both;
}
@keyframes sf-term-rise {
  from { opacity: 0; transform: scale(.94); filter: blur(4px); }
  to { opacity: 1; transform: none; filter: none; }
}

/* Spin — wheel under the term */
.layout-quiz.present-spin .spin-stage {
  position: relative;
  width: min(460px, 78%); aspect-ratio: 1;
  justify-content: center; margin: 4px auto 16px;
  border-radius: 50%;
  border: 3px solid color-mix(in srgb, var(--s-accent) 50%, transparent);
  background: color-mix(in srgb, var(--s-card) 40%, transparent);
  overflow: hidden;
}
.layout-quiz.present-spin .spin-wheel {
  position: absolute; inset: 8%;
  animation: sf-spin-slow 18s linear infinite;
}
.layout-quiz.present-spin .spin-ray {
  position: absolute; left: 50%; top: 50%;
  width: 2px; height: 48%;
  margin-left: -1px; transform-origin: bottom center;
  background: linear-gradient(to top,
    transparent, color-mix(in srgb, var(--s-accent) 55%, transparent));
  opacity: .55;
}
.layout-quiz.present-spin .oracy-term {
  position: relative; z-index: 2;
  font-size: 48px; font-weight: 780; letter-spacing: -.03em;
  max-width: 8em; padding: 0 24px;
}
@keyframes sf-spin-slow { to { transform: rotate(360deg); } }

/* Connection — skewed pair */
.layout-quiz.present-connection .connection-stage { padding-top: 8px; }
.layout-quiz.present-connection .conn-pair {
  position: relative; z-index: 1;
  display: grid; grid-template-columns: 1fr auto 1fr; gap: 20px;
  align-items: stretch; width: 100%; max-width: 1040px;
}
.layout-quiz.present-connection .conn-tile {
  position: relative;
  border-radius: 14px; padding: 32px 28px 36px;
  min-height: 180px;
  display: flex; flex-direction: column; justify-content: center; gap: 12px;
  border: 3px solid var(--s-rule);
  background: var(--s-card);
}
.layout-quiz.present-connection .conn-tile.a {
  border-color: var(--s-accent);
}
.layout-quiz.present-connection .conn-tile.b {
  border-color: var(--s-accent-2, var(--s-accent));
}
.layout-quiz.present-connection .conn-label {
  font-size: 14px; letter-spacing: .2em; text-transform: uppercase;
  color: var(--s-accent); font-weight: 750;
}
.layout-quiz.present-connection .conn-text {
  font-size: 34px; font-weight: 720; line-height: 1.2;
}
.layout-quiz.present-connection .conn-bridge {
  font-size: 40px; align-self: center; color: var(--s-accent);
  animation: sf-bridge-pulse 2s ease-in-out infinite;
}
@keyframes sf-bridge-pulse {
  0%, 100% { transform: scale(1); opacity: .85; }
  50% { transform: scale(1.12); opacity: 1; }
}

/* Concept chain — stepping stones */
.layout-quiz.present-chain .chain-steps {
  position: relative; z-index: 1;
  display: flex; align-items: center; justify-content: center; gap: 14px;
  flex-wrap: wrap; width: 100%; max-width: 1100px; margin-top: 12px;
}
.layout-quiz.present-chain .chain-node {
  padding: 22px 28px; border-radius: 14px;
  font-size: 28px; font-weight: 720;
  border: 3px solid var(--s-rule);
  background: var(--s-card);
  color: var(--s-fg);
}
.layout-quiz.present-chain .chain-node.seed {
  font-size: 36px; border-color: var(--s-accent);
  background: color-mix(in srgb, var(--s-accent) 18%, var(--s-card));
}
.layout-quiz.present-chain .chain-node.done {
  font-size: 22px; font-weight: 700;
  border-color: color-mix(in srgb, var(--s-accent) 45%, var(--s-rule));
}
.layout-quiz.present-chain .chain-node.link {
  font-size: 18px; font-weight: 600; max-width: 220px;
  text-align: center; line-height: 1.3;
}
.layout-quiz.present-chain .chain-node.ghost {
  opacity: .45; border-style: dashed; font-weight: 550; font-size: 22px;
}
.layout-quiz.present-chain .chain-arrow {
  width: 36px; height: 3px; border-radius: 2px;
  background: linear-gradient(90deg, var(--s-accent), transparent);
}
.layout-quiz.present-chain .chain-prompt {
  margin-top: 14px; max-width: 720px; text-align: center;
}
.layout-quiz.present-chain .chain-capture {
  position: relative; z-index: 1;
  margin-top: 16px; width: min(640px, 92%);
  display: flex; flex-direction: column; gap: 8px;
}
.layout-quiz.present-chain .chain-link-input {
  width: 100%;
  padding: 14px 16px;
  border-radius: 12px;
  border: 2px solid var(--s-rule);
  background: var(--s-card);
  color: var(--s-fg);
  font: inherit;
  font-size: 18px;
}
.layout-quiz.present-chain .chain-capture-hint {
  margin: 0;
  font-size: 14px;
  color: var(--s-dim);
  text-align: center;
}

/* Challenge — poster card */
.layout-quiz.present-challenge .challenge-poster {
  position: relative; z-index: 1;
  width: min(820px, 94%);
  padding: 56px 52px;
  border-radius: 14px;
  text-align: left;
  background: var(--s-card);
  border: 3px solid var(--s-rule);
  color: var(--s-fg);
  box-shadow: none;
}
.layout-quiz.present-challenge .challenge-body {
  margin-top: 18px;
  font-size: 42px; font-weight: 680; line-height: 1.28;
  text-wrap: balance;
}

/* Bingo — board game grid */
/* Quiz Bowl — Jeopardy cell */
.layout-quiz.present-bowl .bowl-stage { margin-bottom: 10px; }
.layout-quiz.present-bowl .bowl-cell {
  position: relative; z-index: 1;
  min-width: 280px; padding: 28px 40px;
  border-radius: 14px;
  background: var(--s-card);
  border: 3px solid var(--s-accent);
  box-shadow: none;
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  color: var(--s-fg);
}
.layout-quiz.present-bowl .bowl-cat {
  font-size: 16px; letter-spacing: .18em; text-transform: uppercase;
  color: var(--s-dim); font-weight: 700;
}
.layout-quiz.present-bowl .bowl-val {
  font-size: 56px; font-weight: 850; letter-spacing: -.02em;
  color: var(--s-accent);
  font-variant-numeric: tabular-nums;
}
.layout-quiz.present-bowl .q { --q-f: 40px; }

/* Ranking — spine list */
.layout-quiz.present-ranking .ranking-stage {
  align-items: flex-start; margin-bottom: 4px;
}
.layout-quiz.present-ranking .opts.ordered {
  gap: 12px; max-width: 900px;
}
.layout-quiz.present-ranking .opts.ordered .opt {
  border-radius: 12px;
  border-left-width: 6px;
  border-left-color: var(--s-accent);
}
.layout-quiz.present-ranking .opts.ordered .opt .key {
  background: color-mix(in srgb, var(--s-accent) 30%, transparent);
  border-radius: 50%;
}

/* Host verdict strip — action bar, not quiz grid */
.layout-quiz .opts.judge-strip {
  display: flex; flex-wrap: wrap; gap: 12px; justify-content: center;
  align-items: center;
  margin-top: auto; padding-top: 14px;
  grid-template-columns: none;
}
.layout-quiz .opts.judge-strip .judge-caption {
  flex: 1 0 100%;
  text-align: center;
  font-size: 13px; letter-spacing: .18em; text-transform: uppercase;
  color: var(--s-dim); font-weight: 650; margin-bottom: 2px;
}
.layout-quiz .opts.judge-strip .opt.judge {
  flex: 0 1 200px; max-width: 240px; min-height: 64px;
  --opt-f: 22px; justify-content: center;
  border-radius: 14px;
  background: var(--s-card);
  border: 3px solid var(--s-rule);
}
.layout-quiz .opts.judge-strip .opt.judge .key { display: none; }
.layout-quiz .opts.judge-strip .opt.judge .judge-mark {
  width: 28px; height: 28px; border-radius: 8px;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 16px; font-weight: 700; flex: none;
  background: var(--s-accent); color: #08101f;
}
.theme-mono .layout-quiz .opts.judge-strip .opt.judge .judge-mark { color: #0c0c0c; }
.layout-quiz .opts.judge-strip .opt.judge.yes {
  border-color: var(--s-accent);
  background: color-mix(in srgb, var(--s-accent) 14%, var(--s-card));
}
.layout-quiz .opts.judge-strip .opt.judge.yes .judge-mark {
  background: var(--s-accent);
}
.layout-quiz .opts.judge-strip .opt.judge.skip {
  border-color: var(--s-rule);
  opacity: .95;
}
.layout-quiz .opts.judge-strip .opt.judge.skip .judge-mark {
  background: transparent;
  border: 2px solid var(--s-rule);
  color: var(--s-dim);
}


/* Emoji Guess — giant rebus on theme card */
/* The clues and the help under them are one stack of the same width. Sized to
   their own content they came out three different widths — and a ragged column
   reads as three unrelated boxes rather than a prompt and the help for it. */
.layout-quiz.present-emoji .emoji-stage {
  min-height: 300px; justify-content: center;
  align-items: stretch; width: min(860px, 100%);
  /* Clear of the title row. The clue panel sat hard under the heading, which
     read as one block of chrome rather than a question and its evidence. */
  margin: 26px auto 0;
}
.layout-quiz.present-emoji .emoji-hero {
  position: relative; z-index: 1;
  text-align: center;
  font-size: 96px; line-height: 1.15; letter-spacing: .06em;
  padding: 36px 44px; border-radius: 14px;
  background: var(--s-card);
  border: 3px solid var(--s-accent);
  color: var(--s-fg);
  animation: sf-term-rise .5s cubic-bezier(.2,.8,.2,1) both;
}
/* The answer box is the last row of the same stack as the clues and the
   letters, not a form field parked under them: same width, same panel, and the
   value centred in it rather than pushed left behind a pencil. */
.layout-quiz.present-emoji .opts.typed {
  width: min(860px, 100%); max-width: none; margin: 14px auto 0;
}
.layout-quiz.present-emoji .opt.answer {
  padding: 18px 24px; border-radius: 12px; border-width: 3px;
  border-color: var(--s-accent); background: var(--s-card);
}
.layout-quiz.present-emoji .opt.answer.held {
  border-style: dashed; border-color: var(--s-rule); background: transparent;
}
.layout-quiz.present-emoji .opt.answer .opt-line {
  display: flex; align-items: baseline; gap: 16px;
}
.layout-quiz.present-emoji .opt.answer .key { flex: none; align-self: center; }
.layout-quiz.present-emoji .opt.answer .txt { flex: 1; text-align: center; }

/* Fill in the Blanks — accent underscore pills */
.layout-quiz.present-blanks .blanks-line {
  position: relative; z-index: 1;
  font-size: 36px; font-weight: 650; line-height: 1.55;
  max-width: 28em; text-wrap: balance;
  color: var(--s-fg);
}
.layout-quiz.present-blanks .blank-pill {
  display: inline-block; margin: 0 6px; padding: 4px 18px;
  border-radius: 10px; font-weight: 800; letter-spacing: .12em;
  background: color-mix(in srgb, var(--s-accent) 18%, var(--s-card));
  color: var(--s-fg);
  border: 2px solid var(--s-accent);
  vertical-align: baseline;
}

/* Odd One Out — four equal theme tiles; discuss then reveal */
.layout-quiz.present-oddone .opts.odd-grid {
  grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 28px;
  max-width: 960px; margin-left: auto; margin-right: auto;
  display: grid;
}
.layout-quiz.present-oddone .opts.odd-grid .opt {
  min-height: 120px; --opt-f: 32px; justify-content: center;
  border-radius: 14px;
  background: var(--s-card);
  border: 3px solid var(--s-rule);
  cursor: default;
}
.layout-quiz.present-oddone .opts.odd-grid .opt .opt-line {
  justify-content: center;
  text-align: center;
}
.layout-quiz.present-oddone .opts.odd-grid .opt .key { display: none; }
.layout-quiz.present-oddone .opts.odd-grid .opt .tick:empty { display: none; }
.layout-quiz.present-oddone .opts.odd-grid .opt.odd-marked {
  border-color: var(--s-accent);
  background: color-mix(in srgb, var(--s-accent) 14%, var(--s-card));
}
.layout-quiz.present-oddone .opts.odd-grid .opt.odd-marked .tick {
  display: block;
  font-size: 12px;
  font-weight: 800;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--s-accent);
  margin-top: 8px;
}
.layout-quiz.present-oddone .oddone-discuss {
  margin: 22px auto 0;
  max-width: 720px;
  text-align: center;
  font-size: 16px;
  color: var(--s-dim);
}
.layout-quiz.present-oddone .q { text-align: center; --q-f: 36px; }
.layout-quiz.present-oddone .tally,
.layout-quiz.present-oddone .answered-count { display: none; }

/* Compare & Contrast — two equal tiles; discuss then reveal alike / differ */
.layout-quiz.present-compare .compare-stage {
  position: relative; z-index: 1;
  display: flex; flex-direction: column; align-items: center;
  gap: 18px; width: 100%; max-width: 1040px; margin-top: 8px;
}
.layout-quiz.present-compare .compare-pair {
  display: grid; grid-template-columns: 1fr 1fr; gap: 20px;
  width: 100%;
}
.layout-quiz.present-compare .compare-tile {
  border-radius: 14px; padding: 28px 24px 32px;
  min-height: 140px;
  display: flex; flex-direction: column; justify-content: center; gap: 10px;
  border: 3px solid var(--s-rule);
  background: var(--s-card);
  text-align: center;
}
.layout-quiz.present-compare .compare-label {
  font-size: 13px; font-weight: 800; letter-spacing: .12em;
  text-transform: uppercase; color: var(--s-dim);
}
.layout-quiz.present-compare .compare-text {
  font-size: 34px; font-weight: 760; letter-spacing: -.02em;
  color: var(--s-fg); line-height: 1.2;
}
.layout-quiz.present-compare .compare-discuss {
  margin: 4px 0 0;
  max-width: 720px;
  text-align: center;
  font-size: 16px;
  color: var(--s-dim);
}
.layout-quiz.present-compare .compare-panels {
  display: none;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
  width: 100%;
}
.layout-quiz.present-compare .compare-panels.on {
  display: grid;
}
.layout-quiz.present-compare .compare-panel {
  border-radius: 14px;
  padding: 20px 22px;
  border: 3px solid var(--s-rule);
  background: var(--s-card);
  text-align: left;
}
.layout-quiz.present-compare .compare-panel.alike {
  border-color: color-mix(in srgb, var(--s-accent) 55%, var(--s-rule));
  background: color-mix(in srgb, var(--s-accent) 10%, var(--s-card));
}
.layout-quiz.present-compare .compare-panel.differ {
  border-color: color-mix(in srgb, var(--s-accent-2, var(--s-accent)) 45%, var(--s-rule));
}
.layout-quiz.present-compare .compare-panel-label {
  font-size: 13px; font-weight: 800; letter-spacing: .1em;
  text-transform: uppercase; color: var(--s-accent);
  margin-bottom: 10px;
}
.layout-quiz.present-compare .compare-panel-body {
  font-size: 18px; line-height: 1.45; font-weight: 550; color: var(--s-fg);
}
.layout-quiz.present-compare .q { text-align: center; --q-f: 28px; }
.layout-quiz.present-compare .tally,
.layout-quiz.present-compare .answered-count { display: none; }

/* Spot / Predict / Low-stakes / Definition / Time — layout only, house theme */
.layout-quiz.present-spoterror .qhead {
  border-left: 6px solid color-mix(in srgb, #ff5f6d 70%, var(--s-accent));
  padding-left: 18px;
}
.layout-quiz.present-definition .qhead .q { white-space: pre-wrap; }
.layout-quiz.present-definition .definition-stage {
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin-bottom: 8px;
  min-height: 0;
}
.layout-quiz.present-definition .definition-eyebrow {
  font-size: 13px;
  font-weight: 800;
  letter-spacing: .14em;
  color: var(--s-accent);
}
.layout-quiz.present-definition .definition-passage {
  margin: 0;
  padding: 22px 26px;
  border: 3px solid var(--s-rule);
  border-radius: 14px;
  background: var(--s-card);
  font-size: 26px;
  line-height: 1.4;
  font-weight: 550;
  color: var(--s-fg);
}
.layout-quiz.present-definition .phase-asking .definition-caption {
  font-size: 16px;
  color: var(--s-dim);
  margin: 0;
}
.layout-quiz.present-definition .definition-caption,
.layout-quiz.present-definition .definition-gap {
  margin: 0;
  font-size: 15px;
  color: var(--s-dim);
}
.layout-quiz.present-definition .definition-gap {
  color: var(--s-accent);
  font-weight: 650;
}
.layout-quiz.present-definition .definition-ask {
  align-self: flex-start;
  font: inherit;
  font-size: 16px;
  font-weight: 750;
  border: 3px solid var(--s-accent);
  border-radius: 14px;
  padding: 12px 18px;
  background: var(--s-accent);
  color: #08101f;
  cursor: pointer;
}
.theme-mono .layout-quiz.present-definition .definition-ask { color: #0c0c0c; }
.layout-quiz.present-definition .definition-ask:focus-visible {
  outline: 4px solid var(--s-accent);
  outline-offset: 4px;
}
.layout-quiz.present-definition .definition-wait {
  margin-top: 8px;
  font-size: 15px;
  color: var(--s-dim);
}
.layout-quiz.present-timetravel .qhead {
  border-left: 6px solid var(--s-accent);
  padding-left: 18px;
}

@keyframes sf-boss-hit {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-6px); }
  75% { transform: translateX(6px); }
}

/* five or more lanes: tighten so they all fit the slide */
.layout-race .racetrack:has(.lane:nth-child(5)) { gap: 11px; }
.layout-race .racetrack:has(.lane:nth-child(5)) .lane-rail { height: 44px; }
.layout-race .racetrack:has(.lane:nth-child(5)) .runner { width: 46px; height: 46px; margin-left: -23px; font-size: 26px; }
.layout-race .racetrack:has(.lane:nth-child(5)) .lane-text { font-size: 26px; }
.layout-race .racetrack:has(.lane:nth-child(5)) .lane { grid-template-columns: 250px 1fr 110px; }

.deck-viewport.railed .layout-race .pad { padding: 48px calc(var(--rail-w) + 48px) 48px 52px; }
.deck-viewport.railed .layout-race .lane { grid-template-columns: 210px 1fr 100px; gap: 16px; }
.deck-viewport.railed .layout-race .lane-text { font-size: 26px; }
.deck-viewport.railed .layout-race .race-title { font-size: 42px; }

/* ---- race standings strip, on the question slide ----------------------- */

.layout-quiz .race-strip {
  flex: none;
  display: flex; flex-direction: column; gap: 7px;
  margin-bottom: 20px;
  padding-bottom: 18px;
  border-bottom: 2px solid var(--s-rule);
}
/* The strip now leads the slide, so it is the strip — not the question — that
   has to keep clear of the countdown in its corner. */
.layout-quiz.has-clock .race-strip { padding-right: 112px; }
.layout-quiz.is-race.has-clock .qhead { padding-right: 0; }
.deck-viewport.railed .layout-quiz.has-clock .race-strip { padding-right: 104px; }
.deck-viewport.railed .layout-quiz.is-race.has-clock .qhead { padding-right: 0; }
.layout-quiz .race-strip .rlane {
  display: grid; grid-template-columns: 176px 1fr 62px;
  align-items: center; gap: 14px;
  min-height: 32px;          /* the runner is taller than the rail */
}
.layout-quiz .race-strip .rname {
  font-size: 19px; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--lane-color);
}
.layout-quiz .race-strip .rrail {
  position: relative; height: 15px;
  border-radius: 999px;
  background: var(--s-card);
  border: 1px solid var(--s-rule);
}
.layout-quiz .race-strip .rfill {
  position: absolute; left: 0; top: 0; bottom: 0;
  border-radius: 999px;
  background: var(--lane-tint);
}
/* The runner overhangs the rail deliberately — a horse riding the track reads
   better than a dot sitting inside it, and it matches the full track overlay. */
.layout-quiz .race-strip .rmark {
  position: absolute; top: 50%;
  width: 30px; height: 30px; margin-left: -15px;
  transform: translateY(-50%);
  border-radius: 50%;
  background: var(--lane-color);
  display: grid; place-items: center;
  font-size: 17px; line-height: 1;
}
.layout-quiz .race-strip .rpos {
  font-size: 16px; text-align: right; color: var(--s-dim);
  font-variant-numeric: tabular-nums;
}

/* five or more lanes: thin them so the question keeps the room it needs */
.layout-quiz .race-strip:has(.rlane:nth-child(5)) { gap: 4px; margin-bottom: 14px; padding-bottom: 12px; }
.layout-quiz .race-strip:has(.rlane:nth-child(5)) .rrail { height: 11px; }
.layout-quiz .race-strip:has(.rlane:nth-child(5)) .rmark {
  width: 24px; height: 24px; margin-left: -12px; font-size: 13px;
}
.layout-quiz .race-strip:has(.rlane:nth-child(5)) .rname { font-size: 16px; }
.layout-quiz .race-strip:has(.rlane:nth-child(5)) .rlane { grid-template-columns: 150px 1fr 54px; }

/* the strip disappears once the explanation takes over the slide */
.layout-quiz.why-open .race-strip { display: none; }

.deck-viewport.railed .layout-quiz .race-strip .rlane { grid-template-columns: 140px 1fr 56px; gap: 11px; }
.deck-viewport.railed .layout-quiz .race-strip .rname { font-size: 17px; }

/* feedback badges in the slide rail */
.thumb .badge.fb { background: var(--ui-accent); color: #fff; }
.thumb .badge.fb.warn { background: var(--ui-warn); color: #21160a; }
.opt-row .poll-i {
  flex: none; width: 20px; text-align: center;
  font-size: 12px; color: var(--ui-dim); font-variant-numeric: tabular-nums;
}

/* ==========================================================================
   14. Feedback rail
   Shares .scorerail's geometry and scaling; only the body differs.
   ========================================================================== */

.fbrail { --cloud-f: 34px; }
.fbrail .fb-body {
  flex: 1; min-height: 0;
  display: flex; flex-direction: column; gap: 12px;
  overflow: hidden;
  padding-top: 4px;
  /* Poll bars and the scale used to stack under the title. The cloud already
     fills this slot and sits in the middle — same treatment for the rest. */
  justify-content: center;
}
.fbrail .fb-roster-lbl {
  font-size: 13px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--s-dim); margin-bottom: 2px;
}
.fbrail .fb-who-in {
  font-size: 22px; font-weight: 600; line-height: 1.25;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.fbrail .fb-who-more { font-size: 15px; color: var(--s-dim); }

/* ---- poll ---- */
.fbrail .pollrow { display: flex; flex-direction: column; gap: 6px; }
.fbrail .pollhead { display: flex; align-items: baseline; gap: 10px; }
.fbrail .plabel {
  flex: 1; min-width: 0; font-size: 22px; font-weight: 600; line-height: 1.15;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.fbrail .pn {
  flex: none; font-size: 24px; font-weight: 750;
  font-variant-numeric: tabular-nums;
}
.fbrail .pbar {
  height: 14px; border-radius: 999px;
  background: var(--s-card); border: 1px solid var(--s-rule);
  overflow: hidden;
}
.fbrail .pbar > i {
  display: block; height: 100%;
  background: linear-gradient(90deg, var(--s-accent), var(--s-accent-2));
  transition: width .5s cubic-bezier(.3,0,.2,1);
}
.fbrail .ppct { font-size: 15px; color: var(--s-dim); font-variant-numeric: tabular-nums; }

/* five or more options: tighten */
.fbrail .fb-body:has(.pollrow:nth-child(5)) { gap: 7px; }
.fbrail .fb-body:has(.pollrow:nth-child(5)) .plabel { font-size: 18px; }
.fbrail .fb-body:has(.pollrow:nth-child(5)) .pn { font-size: 19px; }
.fbrail .fb-body:has(.pollrow:nth-child(5)) .pbar { height: 11px; }

/* ---- word cloud ---- */
.fbrail .cloud {
  display: flex; flex-wrap: wrap; align-content: center;
  gap: 6px 14px; line-height: 1.1;
  flex: 1; min-height: 0; overflow: hidden;
}
.fbrail .cloud .word {
  font-weight: 650; letter-spacing: -.01em;
  color: var(--s-fg);
}
.fbrail .cloud .word:nth-child(3n+2) { color: var(--s-accent); }
.fbrail .cloud .word:nth-child(3n+3) { color: var(--s-accent-2); }
.fbrail .cloud .word sup {
  font-size: .5em; font-weight: 500; color: var(--s-dim);
  margin-left: 2px; vertical-align: super;
}

/* ---- brainstorm ---- */
/* No entry animation: the list is rebuilt on every response, so animating it
   would re-fade every existing card each time somebody new replies. */
.fbrail .fbcard {
  background: var(--s-card);
  border-left: 5px solid var(--s-accent);
  border-radius: 9px;
  padding: 11px 13px;
}
.fbrail .fbtext {
  font-size: 19px; line-height: 1.35;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.fbrail .fbwho { font-size: 14px; color: var(--s-dim); margin-top: 5px; }

/* ==========================================================================
   15. Feedback, full screen
   The rail is a summary; this is the version worth discussing. Sizes are set
   for a projector read from the back of a room.
   ========================================================================== */

.layout-feedback { --fk-cloud: 96px; }
.layout-feedback .pad {
  padding: 54px 78px 44px;
  justify-content: center;
  position: relative;
}

.layout-feedback .fk-head { flex: none; margin-bottom: 30px; text-align: center; }
.layout-feedback .fk-kind {
  font-size: 19px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--s-dim); margin-bottom: 8px;
}
.layout-feedback .fk-prompt {
  font-size: 46px; font-weight: 650; line-height: 1.15; letter-spacing: -.02em;
  max-width: 980px; margin: 0 auto;
}

.layout-feedback .fk-body {
  flex: none;
  width: min(980px, 100%);
  margin: 0 auto;
  display: flex; flex-direction: column; justify-content: center; gap: 20px;
}
.layout-feedback .fk-empty {
  font-size: 30px; color: var(--s-dim); text-align: center;
}

/* ---- poll ---- */
.layout-feedback .fk-poll {
  display: grid; grid-template-columns: 1fr 150px;
  align-items: center; gap: 10px 28px;
}
.layout-feedback .fk-plabel {
  grid-column: 1; font-size: 34px; font-weight: 600; line-height: 1.15;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.layout-feedback .fk-pbar {
  grid-column: 1; height: 30px; border-radius: 999px;
  background: var(--s-card); border: 2px solid var(--s-rule); overflow: hidden;
}
.layout-feedback .fk-pbar > i {
  display: block; height: 100%;
  background: linear-gradient(90deg, var(--s-accent), var(--s-accent-2));
  transition: width .55s cubic-bezier(.3,0,.2,1);
}
.layout-feedback .fk-pnum {
  grid-column: 2; grid-row: 1 / span 2;
  display: flex; flex-direction: column; align-items: flex-end; justify-content: center;
}
.layout-feedback .fk-pn { font-size: 46px; font-weight: 750; font-variant-numeric: tabular-nums; }
.layout-feedback .fk-ppct { font-size: 21px; color: var(--s-dim); font-variant-numeric: tabular-nums; }
.layout-feedback .fk-poll.lead .fk-plabel { color: var(--s-accent); }

/* five or more options: step down so they all fit */
.layout-feedback .fk-body:has(.fk-poll:nth-child(5)) { gap: 11px; }
.layout-feedback .fk-body:has(.fk-poll:nth-child(5)) .fk-plabel { font-size: 27px; }
.layout-feedback .fk-body:has(.fk-poll:nth-child(5)) .fk-pbar { height: 22px; }
.layout-feedback .fk-body:has(.fk-poll:nth-child(5)) .fk-pn { font-size: 34px; }

/* ---- word cloud ---- */
.layout-feedback .fk-cloud {
  display: flex; flex-wrap: wrap; align-items: center; align-content: center;
  justify-content: center; gap: 8px 34px;
  line-height: 1.02; text-align: center;
}
.layout-feedback .fk-word { font-weight: 700; letter-spacing: -.02em; color: var(--s-fg); }
.layout-feedback .fk-word:nth-child(4n+2) { color: var(--s-accent); }
.layout-feedback .fk-word:nth-child(4n+3) { color: var(--s-accent-2); }
.layout-feedback .fk-word:nth-child(4n+4) { color: var(--s-dim); }
.layout-feedback .fk-word sup {
  font-size: .32em; font-weight: 500; color: var(--s-dim);
  margin-left: 4px; vertical-align: super;
}

/* ---- brainstorm ---- */
.layout-feedback .fk-cards { display: grid; gap: 16px; align-content: center; }
.layout-feedback .fk-cards[data-cols="2"] { grid-template-columns: 1fr 1fr; }
.layout-feedback .fk-cards[data-cols="3"] { grid-template-columns: repeat(3, 1fr); }
.layout-feedback .fk-card {
  background: var(--s-card);
  border-left: 6px solid var(--s-accent);
  border-radius: 12px; padding: 18px 20px;
}
.layout-feedback .fk-ctext { font-size: 26px; line-height: 1.32; }
.layout-feedback .fk-cards[data-cols="3"] .fk-ctext { font-size: 22px; }
.layout-feedback .fk-cwho { font-size: 17px; color: var(--s-dim); margin-top: 9px; }
.layout-feedback .fk-more { font-size: 19px; color: var(--s-dim); text-align: center; }

/* ---- footer ---- */
.layout-feedback .fk-foot {
  flex: none;
  position: absolute;
  left: 78px;
  right: 78px;
  bottom: 36px;
  display: flex; align-items: center; justify-content: space-between;
  gap: 16px; padding-top: 0; margin-top: 0;
  border-top: 0;
  font-size: 19px; color: var(--s-dim);
}
.layout-feedback .fk-hint { font-size: 15px; opacity: .7; }
.layout-feedback .fk-tag {
  font-size: 14px; letter-spacing: .14em; font-weight: 700;
  padding: 4px 12px; border-radius: 999px;
  background: var(--s-accent-2); color: #21160a;
}

/* The focus view owns the screen: the rail is hidden and the overlay reclaims
   the width the rail was holding. Applies to every focusable feed, or a
   focused leaderboard/track would sit in the left two-thirds with a gap. */
.deck-viewport.fb-focus .scorerail { display: none; }
.deck-viewport.railed .layout-feedback .pad { padding: 54px 78px 44px; }
.deck-viewport.fb-focus .leaderboard { padding-right: 90px; }
.deck-viewport.fb-focus .layout-race .pad { padding: 54px 72px; }
.deck-viewport.fb-focus .layout-race .lane { grid-template-columns: 300px 1fr 130px; }
.deck-viewport.fb-focus .layout-race .lane-text { font-size: 31px; }
.deck-viewport.fb-focus .layout-race .race-title { font-size: 52px; }

/* the expand button belongs to any live session — the rail always has
   something in it worth putting on the whole screen */
/* Focus / join / rail stay on the HUD for every show — Host live fills them
   with a live room; Present alone still offers the control and explains. */

/* the editor preview can host the feedback rail, so it needs the same width
   variable and the same right-edge anchoring the player viewport has */
#previewBox.railed { --rail-w: 384px; }
#previewBox .scorerail { transform-origin: 100% 0; }

/* ==========================================================================
   16. A question from the room, on the wall
   ========================================================================== */

.layout-question .pad { justify-content: center; padding: 70px 96px; }
.layout-question .qc-label {
  flex: none; font-size: 19px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--s-dim); margin-bottom: 26px;
}
.layout-question .qc-text {
  flex: none; font-size: 54px; line-height: 1.24; font-weight: 600;
  letter-spacing: -.02em;
}
.layout-question .qc-foot {
  flex: none; display: flex; align-items: baseline; gap: 22px;
  margin-top: 34px; padding-top: 22px;
  border-top: 1px solid var(--s-rule);
  font-size: 24px; color: var(--s-dim);
}
.layout-question .qc-votes { color: var(--s-accent); }

/* long questions step down rather than overflowing */
.layout-question .qc-text[data-len="lg"] { font-size: 44px; }
.layout-question .qc-text[data-len="xl"] { font-size: 36px; }

/* One strip for the standing cues, bottom-left because the slide number owns
   bottom-right. Side by side rather than stacked: a question slide has one
   band of space under the answers, and a second row of pills climbs straight
   into them. */
.cuebar {
  position: absolute; left: 0; bottom: 0;
  transform-origin: 0 100%;
  z-index: 3;
  margin: 0 0 28px 32px;
  display: flex; align-items: center; gap: 10px;
}
/* Pace first: being asked to slow down is the more urgent of the two. */
.cuebar .pacecue { order: 0; }
.cuebar .qacue { order: 1; }
/* Anything floating over a slide takes its wash from the theme rather than
   assuming the slide is dark. A hardcoded rgba(0,0,0,.42) read at 17:1 on
   midnight and 1.16:1 on paper — the same pill, invisible. --s-card is the
   fallback because its polarity is already per-theme: a light wash on dark
   themes, a dark one on light. */
.qacue, .pacecue {
  background: var(--s-scrim, var(--s-card));
  border: 2px solid var(--s-rule);
  color: var(--s-fg);
  backdrop-filter: blur(8px);
}

/* the standing cue: how many are waiting, unobtrusive but always there */
.qacue {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 17px;
  border-radius: 999px;
  font-family: var(--s-font);
  font-size: 18px;
}
.qacue .qa-dot {
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--s-accent);
}
.qacue.waiting .qa-dot { background: var(--s-accent-2); }
.qacue .qa-n { font-weight: 700; font-variant-numeric: tabular-nums; }

/* Clear whatever occupies the foot of a question slide. The clearances are
   written in slide pixels and multiplied by the letterbox scale, because what
   is being cleared is inside the scaled slide while the strip is not — a fixed
   pixel gap only ever looks right at one window size. */
/* Out of the way while the room is on the whole screen: the focus view has
   its own footer in that corner, and two things in one corner is one thing
   nobody can read. The cues come back when it closes. */
.deck-viewport.fb-focus .cuebar { display: none; }
.deck-viewport.quiz-slide .cuebar { margin-bottom: calc(28px + var(--sf-scale, 1) * 150px); }
.deck-viewport.quiz-line .cuebar { margin-bottom: calc(28px + var(--sf-scale, 1) * 225px); }

/* ----------------------------------------------------------------------
   Typed answers

   A typed question has one answer box rather than a grid of them, so it
   reuses .opt and every measure-and-fit rule that comes with it. What is
   different is that the box holds its content back: in a live room the
   answer must not be on the wall while the room is still typing it.
   ---------------------------------------------------------------------- */
/* One box holding the answer itself, so it leads rather than sitting a step
   below the question the way a list of candidates does. fitQuizSlide() sizes
   it in the show; this is the standing value the editor preview uses. */
.layout-quiz .opts.typed { --opt-f: 60px; align-content: center; grid-template-columns: 1fr; }
/* Held and revealed are the same height, so the answer arriving does not make
   the slide jump under the room's eyes. */
.layout-quiz .opts.typed .opt { cursor: default; min-height: calc(var(--opt-f) * 2.1); }
.layout-quiz .opts.typed .opt .opt-line { flex: 1; }
.layout-quiz .opts.typed .opt .opt-line .txt { text-align: center; }
.layout-quiz .opts.typed .opt.held {
  border-style: dashed;
  background: transparent;
}
/* Quieter than the answer it stands in for: this is a state, not a result. */
.opts.typed .opt.held .txt {
  color: var(--s-dim); font-style: italic;
  font-size: calc(var(--opt-f) * .46);
}
.opts.typed .opt.held .key { background: transparent; border: 2px dashed var(--s-rule); color: var(--s-dim); }
.opts.typed .opt.held .tick { opacity: 0; }

/* Where a choice question puts its bars: the live count while they type,
   then what the room actually typed once the answer is out. */
.layout-quiz .typedlist { margin-top: 16px; min-height: 30px; }
.layout-quiz .typedcount { font-size: 17px; color: var(--s-dim); letter-spacing: .04em; }
.layout-quiz .typedgroups { display: flex; flex-wrap: wrap; gap: 9px; margin-top: 10px; }
.layout-quiz .tchip {
  display: inline-flex; align-items: baseline; gap: 7px;
  padding: 5px 13px; border-radius: 999px;
  background: var(--s-card); border: 2px solid var(--s-rule);
  font-size: 19px; color: var(--s-dim);
}
.layout-quiz .tchip.right { border-color: #34c98a; color: var(--s-fg); }
.layout-quiz .tchip .n { font-size: 15px; opacity: .7; font-variant-numeric: tabular-nums; }

/* The accepted-answer rows in the game editor. No radio button, because
   there is no single correct one to mark — every row is accepted. */
.accept-n {
  flex: none; width: 74px; font-size: 10px; font-weight: 700;
  letter-spacing: .08em; color: var(--ui-dim);
}

/* ----------------------------------------------------------------------
   The estimate line

   A slider question's answers are positions, not words, so they are shown
   as positions. Nothing that says where the answer is — the band or the
   target — is placed until the reveal; see Player.showPlacedValues().
   ---------------------------------------------------------------------- */
/* Room above the line for the stacks of placings to grow into. */
.layout-quiz .numberline { margin-top: 14px; padding-top: 84px; }
.layout-quiz .nl-line {
  position: relative; height: 30px;
  border-radius: 999px;
  background: var(--s-card);
  border: 2px solid var(--s-rule);
}
.layout-quiz .nl-band {
  position: absolute; top: -2px; bottom: -2px; left: 0; width: 0;
  border-radius: 999px;
  background: rgba(52, 201, 138, .22);
  border: 2px solid #34c98a;
  opacity: 0; transition: opacity .4s;
}
.layout-quiz .nl-target {
  position: absolute; top: -7px; bottom: -7px; left: 0; width: 3px;
  margin-left: -1px; background: #34c98a;
  opacity: 0; transition: opacity .4s;
}
.layout-quiz .nl-line.on .nl-band, .layout-quiz .nl-line.on .nl-target { opacity: 1; }
/* Overflow is visible on purpose: a stack of dots for a popular value grows
   up out of the line rather than being clipped by it. */
.layout-quiz .nl-marks { position: absolute; inset: 0; overflow: visible; }
/* Above the line, not on it: the band inside the line is the answer, and a
   cluster of placings drawn over it hides the very thing they are being
   compared against. */
.layout-quiz .nl-dot {
  /* --stack is how many placings already sit on this value; the geometry of
     the pile lives here rather than being computed in two places. */
  position: absolute; bottom: calc(36px + var(--stack, 0) * 15px);
  width: 14px; height: 14px;
  margin-left: -7px; border-radius: 50%;
  background: var(--s-dim);
  border: 2px solid var(--s-bg);
}
.layout-quiz .nl-dot.right { background: #34c98a; }
.layout-quiz .nl-ends {
  display: flex; justify-content: space-between;
  margin-top: 7px; font-size: 16px; color: var(--s-dim);
  font-variant-numeric: tabular-nums;
}

/* ----------------------------------------------------------------------
   Scale results

   Columns, not rows. A scale runs from one end to the other, and drawing
   it as independent bars throws away the only thing that makes it a
   scale. `sc-` is the rail; `fksc-` is the same chart full screen.
   ---------------------------------------------------------------------- */
.sc, .fksc { display: flex; flex-direction: column; gap: 8px; }
.fksc { gap: 18px; width: 100%; }
.fbrail .sc { flex: 1; min-height: 0; justify-content: center; }
.sc-cols, .fksc-cols { display: flex; align-items: flex-end; gap: 6px; }
.fbrail .sc-cols { flex: 1; min-height: 140px; align-items: stretch; }
.fksc-cols { gap: 14px; min-height: 300px; }
.sc-col, .fksc-col { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4px; }
.fbrail .sc-col { height: 100%; justify-content: flex-end; }
.fksc-col { gap: 10px; }
/* --s-rule, not a 5%-white wash: that measured 1.01:1 against the slide on
   studio and 1.14:1 on midnight, so the unfilled part of a bar was invisible
   on every theme rather than only the light ones. */
.sc-bar, .fksc-bar {
  width: 100%; display: flex; align-items: flex-end;
  background: var(--s-rule); border-radius: 5px 5px 0 0;
  /* An outline as well as a fill: --s-rule is deliberately faint, and the
     extent of the track is what says how big the bar could have been. */
  outline: 1px solid var(--s-rule); outline-offset: -1px;
}
.sc-bar { height: 74px; }
.fbrail .sc-bar { flex: 1; height: auto; min-height: 90px; }
/* Full-screen scale: tall enough to own the middle of the wall. */
.fksc-bar { height: 280px; border-radius: 8px 8px 0 0; }
.sc-bar i, .fksc-bar i {
  width: 100%; background: var(--s-accent);
  border-radius: 5px 5px 0 0; min-height: 3px;
  transition: height .45s cubic-bezier(.3,0,.2,1);
}
.sc-n, .fksc-n { font-variant-numeric: tabular-nums; color: var(--s-fg); }
.sc-n { font-size: 13px; min-height: 15px; }
.fksc-n { font-size: 26px; font-weight: 700; min-height: 32px; }
.sc-p, .fksc-p { color: var(--s-dim); font-variant-numeric: tabular-nums; }
.sc-p { font-size: 12px; }
.fksc-p { font-size: 22px; }
.sc-ends, .fksc-ends { display: flex; justify-content: space-between; gap: 10px; color: var(--s-dim); }
.sc-ends { font-size: 12px; }
.fksc-ends { font-size: 22px; margin-top: 4px; }
.sc-ends span:last-child, .fksc-ends span:last-child { text-align: right; }
.sc-read, .fksc-read { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.sc-read { font-size: 13px; color: var(--s-dim); }
.sc-read strong { font-size: 21px; color: var(--s-fg); }
.fksc-read { font-size: 24px; color: var(--s-dim); margin-top: 14px; }
.fksc-read strong { font-size: 46px; color: var(--s-fg); }
/* Named, not left for the reader to infer from the bars: a mean of 3 from a
   room at 1 and 5 is the opposite situation to a room all sitting at 3. */
/* Outlined rather than filled. Filling it with --s-accent-2 and writing dark
   text on top assumes every theme's second accent is light — paper's is a
   dark green, which put dark on dark. An outline needs no such assumption. */
/* Text is --s-fg or --s-dim; accents are for borders, fills and dots.
   
   That rule is the whole lesson from this pass. Filling this with
   --s-accent-2 and writing dark text on it assumed the second accent is
   light, which put dark on dark on paper. Writing the text *in* accent-2
   assumed it contrasts with the slide, which put pale lilac on sage at 1.97:1
   on studio. Only --s-fg is guaranteed to read against --s-bg, because that
   is what the pair is for. */
.sc-split, .fksc-split {
  background: transparent; color: var(--s-fg);
  border: 2px solid var(--s-accent-2);
  border-radius: 999px; font-weight: 700; letter-spacing: .06em;
}
.sc-split { font-size: 10px; padding: 2px 8px; }
.fksc-split { font-size: 18px; padding: 4px 14px; }

/* ----------------------------------------------------------------------
   The pace cue

   Sits opposite the Q&A cue. It appears only when enough of the room is
   asking for something — showing them the signal landed is the point,
   since one that visibly changes nothing gets sent once and never again.
   Counts only, never names: see Player.setPaceCue().
   ---------------------------------------------------------------------- */
.pacecue {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 17px;
  border-radius: 999px;
  font-family: var(--s-font);
  font-size: 18px;
}
/* The pace cue keeps the accent border that marks it apart from the Q&A one. */
.pacecue { border-color: var(--s-accent-2); }
.pacecue .pace-dot {
  width: 9px; height: 9px; border-radius: 50%;
  background: var(--s-accent-2);
  animation: sf-pace-pulse 1.8s ease-in-out infinite;
}
@keyframes sf-pace-pulse { 0%,100% { opacity: 1 } 50% { opacity: .35 } }
.pacecue .pace-n { font-weight: 700; font-variant-numeric: tabular-nums; }

/* ----------------------------------------------------------------------
   The Adapt report

   Lives in the reports modal, which is the light studio skin, so it uses
   the --ui-* tokens rather than the slide theme's. Deliberately sparse:
   the value is in the sentences, and a page of chrome around them reads
   as a dashboard, which is the thing this is not.
   ---------------------------------------------------------------------- */
.adapt-headline {
  font-size: 20px; font-weight: 500; letter-spacing: -.3px;
  margin: 18px 0 10px;
}
/* The band, not a badge per card: the heading carries the urgency once and
   the findings under it stay quiet. */
.adapt-band {
  margin: 28px 0 4px; padding: 8px 12px; border-radius: 8px;
  font-size: 10px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
}
.adapt-act { background: #fbe6e6; color: #8d3535; }
.adapt-watch { background: #fff0d8; color: #875e24; }
.adapt-note { background: #edf2e6; color: #5a6b4c; }

.adapt-finding {
  padding: 18px 0 16px;
  border-bottom: 1px solid var(--ui-line);
}
.adapt-finding h4 {
  font-size: 15px; font-weight: 600; margin: 0 0 8px;
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
}
/* Said on the finding itself, not only in the summary at the top: a reader
   who scrolls straight to a heading should still see what it rests on. */
.adapt-thin {
  font-size: 9px; font-weight: 700; letter-spacing: .09em;
  background: #eceff3; color: #6b7280;
  border-radius: 999px; padding: 3px 8px;
}
.adapt-evidence {
  font-size: 12px; color: var(--ui-dim); margin: 0 0 8px;
  font-variant-numeric: tabular-nums;
}
.adapt-action { font-size: 13px; line-height: 1.75; margin: 0; }
/* The teacher's own words, so they look like a quotation rather than more
   advice from the app. */
.adapt-plan {
  margin: 12px 0 0; padding: 13px 15px;
  background: #f0ecf6; border-radius: 9px;
  font-size: 13px; line-height: 1.7; color: #594270;
}
.adapt-plan-label {
  display: block; font-size: 9px; font-weight: 700; letter-spacing: .1em;
  color: #9376aa; margin-bottom: 6px;
}
.adapt-where { font-size: 11px; color: var(--ui-dim); margin: 10px 0 0; }

/* ----------------------------------------------------------------------
   The join QR

   Sits above the address and the PIN, not instead of them. A camera is
   fewer steps for most of the room, and no use at all to an older phone,
   a locked-down device, or someone already looking at the page — so both
   routes stay on screen.
   ---------------------------------------------------------------------- */
.joinqr {
  display: none;
  width: 260px; margin: 0 auto 26px;
  padding: 14px; border-radius: 18px;
  /* White behind it whatever the theme is doing: the quiet zone only works
     against a light background, and a dark one stops it scanning. */
  background: #fff;
  border: 1px solid rgba(0,0,0,.12);
  line-height: 0;
}
.joinqr.on { display: block; }
.joinqr svg { width: 100%; height: auto; display: block; }
#joincard .joinqr { width: 210px; margin-bottom: 20px; }

/* On a short window the code is the first thing that can afford to shrink —
   the PIN and the address have to stay legible from the back of a room. */
@media (max-height: 760px) {
  .joinqr { width: 190px; padding: 11px; margin-bottom: 18px; }
  #joincard .joinqr { width: 160px; }
}
@media (max-height: 620px) {
  .joinqr { display: none !important; }
}

/* ----------------------------------------------------------------------
   Reactions

   They rise once and are gone. Nothing accumulates — no list, no count,
   no history, and nothing in the session journal. That is the design
   rather than an omission: the moment a gesture is collected into
   something it becomes a feed, which is what this app is deliberately
   not. See Player.showReaction().
   ---------------------------------------------------------------------- */
/* A strip along the foot of the slide itself, in slide coordinates, so it is
   scaled by the same transform as everything it is reacting to. */
.reactbay {
  position: absolute; left: 0; right: 0; bottom: 0;
  height: 300px;
  pointer-events: none;
  overflow: hidden;
  z-index: 2;
}
/* Starts inside the bay, not below it. The bay clips its overflow so nothing
   escapes into the rail or off the slide, which also means a glyph placed
   below the bottom edge is clipped for the whole first half of its rise —
   it renders as nothing at all. */
.reaction {
  position: absolute; bottom: 0;
  font-size: 52px; line-height: 1;
  opacity: 0;
  animation: sf-react 2.6s cubic-bezier(.25,.6,.3,1) forwards;
}
@keyframes sf-react {
  0%   { opacity: 0;   transform: translate(0, 0) scale(.6) rotate(0deg); }
  14%  { opacity: .95; transform: translate(0, -46px) scale(1) rotate(0deg); }
  70%  { opacity: .8; }
  100% { opacity: 0;   transform: translate(var(--drift, 0px), -250px) scale(.9) rotate(var(--spin, 0deg)); }
}
/* Nothing floats over a question — the foot of the slide is carrying the
   answer tally, and the relay drops reactions during one anyway. This is the
   belt to that braces. */
.deck-viewport.quiz-slide .reactbay { display: none; }

/* ----------------------------------------------------------------------
   The rail's join panel and arrival notices

   The rail is the one thing on screen the whole lesson, so it carries the
   way in for as long as the way in is open: a code to scan and the PIN to
   type. Big while the board is empty — an empty rail saying "waiting for
   players" is the one moment the screen has nothing better to do than
   show people how to arrive — and compact once anyone is in, so the
   standings keep the space. See paintRailJoin().
   ---------------------------------------------------------------------- */
.scorerail .rail-join { display: none; flex: none; }
.scorerail .rail-join.on {
  display: flex; align-items: center; gap: 14px;
  margin-top: 16px; padding-top: 16px;
  border-top: 1px solid var(--s-rule);
}
/* flex: none, not flex: 1 — the panel sizes to its content. Sharing the
   column with a body that is also flex: 1 gave it half the rail and it
   overflowed into the footer. */
.scorerail .rail-join.big {
  flex-direction: column; gap: 12px;
  margin: auto 0; padding-top: 0; border-top: 0;
  justify-content: center;
  /* Grow into the free middle so an empty poll is the QR, not a hollow gap. */
  flex: 1 1 auto; min-height: 0;
}
/* The panel is already saying it, larger. */
.scorerail.joining-big .joinline { display: none; }
.scorerail.joining-big .rows,
.scorerail.joining-big .fb-body { flex: none; }
/* White behind it whatever the theme: the quiet zone only works against a
   light background, and a dark one stops it scanning. */
/* The SVG carries its own quiet zone, so this padding is only there to round
   the corner off against the rail. */
.scorerail .rj-qr {
  flex: none; background: #fff; border-radius: 10px; padding: 3px;
  line-height: 0; width: 104px;
}
.scorerail .rail-join.big .rj-qr { width: 208px; border-radius: 16px; padding: 4px; }
.scorerail .rj-qr svg { width: 100%; height: auto; display: block; }
.scorerail .rj-side { min-width: 0; }
.scorerail .rail-join.big .rj-side { text-align: center; }
.scorerail .rj-lbl {
  font-size: 13px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--s-dim);
}
.scorerail .rail-join.big .rj-lbl { font-size: 17px; }
/* Shut, not broken: the code still works, it lands you in the next round.
   Said with weight rather than hue — the open label is --s-dim, so full
   --s-fg reads as the more important state on any theme, where an accent
   would have to be checked against every background. */
.scorerail .rj-lbl.shut { color: var(--s-fg); }
.scorerail .rj-wait { font-size: 15px; color: var(--s-dim); margin-top: 6px; }
.scorerail .rail-join.big .rj-wait { font-size: 18px; margin-top: 10px; }
.scorerail .rj-pin {
  font-size: 30px; font-weight: 800; letter-spacing: .1em;
  font-variant-numeric: tabular-nums; line-height: 1.15;
}
.scorerail .rail-join.big .rj-pin { font-size: 46px; letter-spacing: .12em; }
.scorerail .rj-url {
  font-size: 16px; color: var(--s-dim); margin-top: 4px;
  font-family: ui-monospace, monospace; overflow-wrap: anywhere;
}

/* Arrivals. One line each, briefly — the roster underneath already carries
   who is in, so this only has to catch the eye once. */
.scorerail .rail-news { flex: none; display: flex; flex-direction: column; gap: 6px; }
.scorerail .rail-news:not(:empty) { margin: 8px 0 2px; }
/* The resting state is the visible one and the animation only slides it in.
   With `both` and a transparent first frame, a note is invisible for as long
   as animations do not run — a hidden tab composites no frames, and reduced
   motion may cancel them outright — which is a fine failure for a floating
   reaction and a bad one for a line telling you who arrived. */
.scorerail .rnote {
  font-size: 17px; color: var(--s-fg);
  background: var(--s-card);
  border-left: 3px solid var(--s-accent);
  border-radius: 8px; padding: 7px 11px;
  animation: sf-rnote .28s ease;
}
@keyframes sf-rnote {
  from { opacity: 0; transform: translateX(14px); }
  to   { opacity: 1; transform: translateX(0); }
}

/* Teacher workspace: room to manage a full class, including on small screens. */
.teach-tabs{flex-wrap:wrap}
#teachFrame{height:62vh}
@media(max-width:640px){.teach-modal header{flex-wrap:wrap}.teach-tabs .btn{font-size:12px;padding:7px 9px}.teach-body{padding:8px;max-height:calc(88vh - 160px)}}
/* Content-first layout selection and accessible structured reordering. */
.layout-library,.saved-content,.bulk-content{border:1px solid var(--ui-line);border-radius:9px;padding:12px;margin:12px 0}.unused-on-layout{margin:8px 0 12px;padding:10px;background:#f5f7f1;border-radius:8px}.unused-on-layout .btn{display:block;width:100%;margin-top:6px;text-align:left}.layout-library summary,.saved-content summary,.bulk-content summary{cursor:pointer;font-size:13px;font-weight:650}.layout-library-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.layout-choice{min-width:0;padding:6px;background:var(--ui-panel);color:var(--ui-text);border:1px solid var(--ui-line);border-radius:8px;cursor:pointer}.layout-choice.on{border-color:var(--ui-accent);background:var(--ui-accent-soft)}.layout-choice>span{display:block;font-size:11px;padding:5px}.layout-fit{display:block;font-size:10px;letter-spacing:.03em;text-transform:uppercase;padding:0 5px 4px}.layout-choice[data-fit=tight]{border-color:#c8102e}.layout-choice[data-fit=tight] .layout-fit{color:#a3152a}.layout-choice[data-fit=small]{border-color:#d7943a}.layout-choice[data-fit=small] .layout-fit{color:#8a5a12}.layout-choice .variant-frame{width:100%;aspect-ratio:16/9;overflow:hidden}.content-order{display:flex;flex-direction:column;gap:3px;flex:none}.content-order button{padding:2px 5px;font-size:12px;background:var(--ui-panel);color:var(--ui-text);border:1px solid var(--ui-line);border-radius:4px}.content-order .content-grip{cursor:grab;touch-action:pan-y}.content-order button:disabled{opacity:.3}.pit-row.content-drop{outline:2px solid var(--ui-accent);background:var(--ui-accent-soft)}.bulk-content textarea{box-sizing:border-box;width:100%;margin-top:10px;padding:8px;font:inherit;border:1px solid var(--ui-line);border-radius:6px;background:var(--ui-panel);color:var(--ui-text)}.saved-content .btn{display:block;margin-top:6px}button:focus-visible{outline:2px solid var(--ui-accent);outline-offset:2px}
#inspector .pit-row{display:grid;grid-template-columns:26px 20px minmax(0,1fr) 24px;align-items:center;gap:5px;padding:8px 0;border-bottom:1px solid var(--ui-line)}#inspector .pit-row>.content-order{grid-column:1;grid-row:1 / span 2}#inspector .pit-row>.pit-i{grid-column:2;grid-row:1;font-size:11px}#inspector .pit-row>input,#inspector .pit-row>.kw-pit-fields{grid-column:3;grid-row:1;min-width:0;width:100%;box-sizing:border-box}#inspector .pit-row>.kill{grid-column:4;grid-row:1;padding:4px}#inspector .pit-row>.format-tools{grid-column:2 / -1;grid-row:2;flex-wrap:wrap}#inspector .kw-pit-fields{grid-template-columns:1fr}#previewBox{position:relative}.canvas-editable:hover{outline:2px dashed #729458;outline-offset:4px;cursor:text}.canvas-edit-form{position:absolute;z-index:15;left:5%;right:5%;bottom:12px;background:var(--ui-panel);color:var(--ui-text);padding:16px;border:1px solid var(--ui-accent);border-radius:10px;font-size:14px}.canvas-edit-form textarea{display:block;box-sizing:border-box;width:100%;margin:8px 0;font:inherit;padding:10px}.canvas-edit-form .format-tools{margin:0 0 8px;padding:6px}.canvas-edit-form button{margin-right:8px}
.canvas-edit-drag{display:flex;align-items:center;gap:8px;margin:-16px -16px 12px;padding:12px 16px;border-bottom:1px solid var(--ui-line);background:var(--ui-panel);border-radius:10px 10px 0 0;cursor:grab;user-select:none;font-weight:650}
.canvas-edit-drag:active{cursor:grabbing}
.canvas-edit-drag-hint{margin-left:auto;font-size:12px;font-weight:500;opacity:.7}
.canvas-edit-lattice{display:flex;flex-direction:column;gap:8px;margin:10px 0;padding:10px;border:1px solid var(--ui-line);border-radius:8px;background:var(--ui-panel)}
.canvas-edit-lattice-row{display:flex;flex-wrap:wrap;align-items:center;gap:6px}
.canvas-edit-lattice-label{font-size:12px;font-weight:650;min-width:3.2em;color:var(--ui-muted, #53604f)}
.canvas-edit-lattice-val{font:650 13px/1 system-ui;min-width:2.2em;text-align:center}
.canvas-edit-lattice-row .btn{margin:0;padding:4px 8px;font-size:12px}

/* Emoji guess: help released a step at a time under the clues. */
.layout-quiz.present-emoji .emoji-help {
  position: relative; z-index: 1;
  display: flex; align-items: baseline; gap: 16px;
  padding: 12px 24px; border-radius: 12px;
  /* Released help is something the room is meant to read from the back. It
     carried the faint rule border, which at a projector's contrast left a row
     of thin dashes floating on a pale panel. */
  border: 3px solid var(--s-accent);
  background: var(--s-card);
}
.layout-quiz.present-emoji .emoji-help-label {
  font-size: 12px; letter-spacing: .14em; color: var(--s-dim); flex: none;
}
.layout-quiz.present-emoji .emoji-help strong {
  font-size: 30px; line-height: 1.2; overflow-wrap: anywhere;
}
.layout-quiz.present-emoji .emoji-help-blanks { padding: 18px 24px; }
/* strong.emoji-blanks, not .emoji-blanks: the rule above it is one element
   deeper, so it outranked a plain class selector and the size set here never
   applied. Same trap as the bingo card's border. */
.layout-quiz.present-emoji .emoji-help strong.emoji-blanks {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  /* Fills the row it is in, at the heading's scale. A run of underscores has
     almost no ink, so at the hint's size and hugging the left edge it read as
     an empty box. */
  flex: 1; text-align: center;
  font-size: 56px; font-weight: 700; line-height: 1;
  letter-spacing: .28em; color: var(--s-accent);
}

/* The hint IS the title row — one heading per puzzle, never two competing on
   the same line and never an empty one. It carries .q, so its size, weight and
   line height come from the heading rule, and fitQuizSlide shrinks a long one
   exactly as it would any other question. */
.layout-quiz.present-emoji .qhint {
  display: flex; align-items: baseline; gap: 14px;
}
.layout-quiz.present-emoji .qhint-label {
  font-size: 12px; letter-spacing: .14em; color: var(--s-dim); flex: none;
  align-self: center;
}
.layout-quiz.present-emoji .qhint strong {
  font: inherit; color: var(--s-accent); overflow-wrap: anywhere;
}

/* How many of the room are in, on the wall rather than as notes in the rail. */
.layout-quiz .answered-count {
  margin-top: 10px; text-align: center;
  font-size: 17px; color: var(--s-dim);
  font-variant-numeric: tabular-nums;
  min-height: 22px;
}

/* Are-you-sure, in the app rather than the browser's own dialog. */
.ask-modal {
  border: none; border-radius: 14px; padding: 22px 24px 18px;
  max-width: 430px; width: calc(100% - 40px);
  background: var(--ui-panel); color: var(--ui-text);
  border: 1px solid var(--ui-line);
}
.ask-modal::backdrop { background: rgba(12,16,14,.45); }
.ask-modal h3 { margin: 0 0 8px; font-size: 18px; line-height: 1.3; }
.ask-modal p {
  margin: 0 0 18px; font-size: 13px; line-height: 1.55; color: var(--ui-dim);
  white-space: pre-line;
}
.ask-modal p[hidden] { display: none; }
.ask-actions { display: flex; justify-content: flex-end; gap: 8px; }
.ask-modal .btn.danger { background: var(--ui-bad); border-color: var(--ui-bad); color: #fff; }
.ask-modal #askInput {
  width: 100%; margin: 0 0 18px; padding: 9px 11px;
  font: inherit; font-size: 14px;
  border: 1px solid var(--ui-line); border-radius: 8px;
  background: var(--ui-bg); color: var(--ui-text);
}
.ask-modal #askInput[hidden] { display: none; }
.ask-modal.has-qr { max-width: 460px; }
.ask-qr {
  width: 180px; margin: 0 auto 16px; padding: 10px;
  background: #fff; border: 1px solid var(--ui-line); border-radius: 12px;
  line-height: 0;
}
.ask-qr[hidden] { display: none; }
.ask-qr svg { width: 100%; height: auto; display: block; }

/* A race the teacher runs: the lanes are the control. */
.race-strip.runnable .rlane {
  appearance: none; width: 100%; text-align: left;
  font: inherit; color: inherit;
  border: 2px solid transparent; border-radius: 10px;
  background: transparent; cursor: pointer;
}
.race-strip.runnable .rlane:not(:disabled):hover {
  border-color: var(--lane-color, var(--s-accent));
  background: color-mix(in srgb, var(--s-fg) 5%, transparent);
}
.race-strip.runnable .rlane:focus-visible {
  outline: 3px solid var(--s-accent); outline-offset: 2px;
}
.race-strip.runnable .rlane:disabled { cursor: default; opacity: .85; }
.race-strip .rlane.won .rpos { color: var(--s-accent); font-weight: 750; }

/* Boss battle: the health of the thing they are hitting, in stage tokens. */
.layout-quiz.present-boss .boss-hp {
  position: relative; z-index: 1;
  display: flex; align-items: center; gap: 14px;
  /* Takes the rest of the band, and there is no band without it: an author
     preview has no fight to draw, so the crest centres on its own. */
  flex: 1 1 280px; min-width: 200px;
  width: auto; margin: 0;
}
.layout-quiz.present-boss .boss-hp-rail {
  flex: 1; height: 18px; border-radius: 999px;
  border: 3px solid var(--s-rule);
  background: color-mix(in srgb, var(--s-fg) 8%, transparent);
  overflow: hidden;
}
.layout-quiz.present-boss .boss-hp-fill {
  height: 100%; background: var(--s-accent);
  transition: width .45s cubic-bezier(.2,.8,.2,1);
}
.layout-quiz.present-boss .boss-hp-n {
  font-size: 17px; font-weight: 700; font-variant-numeric: tabular-nums;
  color: var(--s-fg); min-width: 108px; text-align: right;
}
.layout-quiz.present-boss .boss-turn {
  position: relative; z-index: 1;
  flex: 0 0 100%;
  margin-top: 0; font-size: 16px; color: var(--s-dim); text-align: center;
}
/* Three looks, from the theme rather than from a palette of its own. */
.layout-quiz.present-boss.is-hurt .boss-hp-fill { background: var(--s-accent-2, var(--s-accent)); }
.layout-quiz.present-boss.is-weak .boss-hp-fill { background: var(--s-accent); opacity: .7; }
.layout-quiz.present-boss.is-defeated .boss-hp-fill { background: var(--s-dim); }
.layout-quiz.present-boss .boss-actions {
  position: relative; z-index: 1;
  display: flex; justify-content: center; gap: 10px; margin-top: 8px;
}
.layout-quiz.present-boss .boss-button {
  font: inherit; font-size: 16px; font-weight: 600;
  border: 3px solid var(--s-rule); border-radius: 10px;
  background: transparent; color: var(--s-fg);
  padding: 10px 18px; cursor: pointer;
}
.layout-quiz.present-boss .boss-button.primary {
  background: var(--s-accent); border-color: var(--s-accent); color: var(--s-bg);
}
.layout-quiz.present-boss .boss-button:focus-visible {
  outline: 4px solid var(--s-accent); outline-offset: 3px;
}
.layout-quiz.present-boss .boss-gap { color: var(--s-accent); font-weight: 600; }

/* The rail says two things and labels both: how they are doing, and where
   they are in the game. They are not the same number and never were. */
.scorerail .rail-legend {
  display: flex; justify-content: space-between; gap: 8px;
  padding: 0 10px 4px; font-size: 9px; letter-spacing: .1em;
  color: var(--s-dim); opacity: .8;
}
.scorerail .rail-legend[hidden] { display: none; }
.scorerail .srow .learn {
  font-size: 11px; color: var(--s-dim);
  font-variant-numeric: tabular-nums; line-height: 1.3;
}
.scorerail .srow .learn.needs { color: var(--s-accent); font-weight: 650; }
.scorerail[data-density="sm"] .srow .learn,
.scorerail[data-density="xs"] .srow .learn { display: none; }

/* The rail says two things and labels both: how they are doing, and where
   they are in the game. They are not the same number and never were. */
.scorerail .rail-legend {
  display: flex; justify-content: space-between; gap: 8px;
  padding: 0 10px 4px; font-size: 9px; letter-spacing: .1em;
  color: var(--s-dim); opacity: .8;
}
.scorerail .rail-legend[hidden] { display: none; }
.scorerail .srow .learn {
  font-size: 11px; color: var(--s-dim);
  font-variant-numeric: tabular-nums; line-height: 1.3;
}
.scorerail .srow .learn.needs { color: var(--s-accent); font-weight: 650; }
.scorerail[data-density="sm"] .srow .learn,
.scorerail[data-density="xs"] .srow .learn { display: none; }

/* Activity briefs have named stages, not vocabulary definitions. Preserve
   sentence case and give six short rows room within the 1280 × 720 canvas. */
.slide.activity-slide.layout-keywords .pad { justify-content: center; }
.slide.activity-slide.layout-keywords h2 { font-size: 42px; line-height: 1.12; margin-bottom: 24px; }
.slide.activity-slide.layout-keywords .kw-list { gap: 12px; }
.slide.activity-slide.layout-keywords .kw-row { gap: 24px; padding-bottom: 12px; grid-template-columns: minmax(170px, .38fr) minmax(0, 1fr); }
.slide.activity-slide.layout-keywords .kw-term { font-size: 25px; line-height: 1.2; }
.slide.activity-slide.layout-keywords .kw-def { font-size: 24px; line-height: 1.3; text-transform: none; color: var(--s-fg); }
.slide.activity-slide.layout-table h2 { font-size: 42px; }

.slide.activity-slide.layout-cards h2 { font-size: 44px; margin-bottom: 32px; }
.slide.activity-slide.layout-cards .activity-card-label { display: block; font-size: 29px; line-height: 1.2; margin-bottom: 20px; }
.slide.activity-slide.layout-cards .activity-card-copy { display: block; font-size: 26px; line-height: 1.4; }
.slide.activity-slide.layout-table .tbl { table-layout: fixed; border-collapse: separate; border-spacing: 10px; }
.slide.activity-slide.layout-table .tbl td { width: 25%; height: 92px; padding: 16px; border: 1px solid var(--s-rule); border-radius: 12px; background: var(--s-card); font-size: 23px; font-weight: 500; vertical-align: middle; }

/* Distinct activity structures preserve the label/value editing contract. */
.slide.activity-slide.layout-keywords.activity-steps .kw-list { counter-reset: activity-step; }
.slide.activity-slide.layout-keywords.activity-steps .kw-row { counter-increment: activity-step; position: relative; margin-left: 16px; padding-left: 52px; border-left: 2px solid var(--s-rule); border-bottom: 0; }
.slide.activity-slide.layout-keywords.activity-steps .kw-row::before { content: counter(activity-step, decimal-leading-zero); position: absolute; left: -18px; top: 0; width: 34px; height: 34px; display: grid; place-items: center; border-radius: 50%; background: var(--s-fg); color: var(--s-bg); font-size: 16px; font-weight: 700; }
.slide.activity-slide.layout-keywords.activity-panels .kw-list { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.slide.activity-slide.layout-keywords.activity-panels .kw-row { display: flex; flex-direction: column; gap: 14px; align-items: stretch; padding: 24px; border: 1px solid var(--s-rule); border-radius: 16px; background: var(--s-card); }
.slide.activity-slide.layout-keywords.activity-panels .kw-row:last-child { padding-bottom: 24px; border-bottom: 1px solid var(--s-rule); }
.slide.activity-slide.layout-keywords.activity-panels.theme-studio .kw-row:nth-child(1) { background: #eee9db; }
.slide.activity-slide.layout-keywords.activity-panels.theme-studio .kw-row:nth-child(2) { background: #dac9ea; }
.slide.activity-slide.layout-keywords.activity-panels.theme-studio .kw-row:nth-child(3) { background: #cddcbf; }
.slide.activity-slide.layout-keywords.activity-panels.theme-studio .kw-row:nth-child(4) { background: #e2e7ed; }
.slide.activity-slide.layout-keywords.activity-brief .kw-list { display: grid; grid-template-columns: 1fr 1fr; gap: 18px 28px; }
.slide.activity-slide.layout-keywords.activity-brief .kw-row { display: flex; flex-direction: column; align-items: stretch; gap: 8px; padding-bottom: 14px; }
.slide.activity-slide.layout-keywords.activity-brief .kw-row:first-child { grid-column: 1 / -1; padding: 24px; background: var(--s-card); border: 1px solid var(--s-rule); border-left: 6px solid var(--s-fg); border-radius: 0 14px 14px 0; }
.slide.activity-slide.layout-keywords.activity-brief .kw-row:first-child .kw-def { font-size: 28px; }
.slide.activity-slide.layout-keywords.activity-brief .kw-row:last-child:nth-child(even) { grid-column: 1 / -1; }

/* Join line on the full-screen feedback overlay. The rail has carried one for
   a while; full screen covers the wall, so without it a phone arriving mid-poll
   has nothing to read. Sits above the question rather than beside it. */
.slide.layout-feedback .pad .fk-join {
  display: flex; align-items: center; gap: 14px;
  margin: 0 0 18px; padding: 10px 22px;
  border: 1px solid var(--s-rule); border-radius: 999px;
  background: var(--s-card); align-self: center;
  transition: transform 0.15s ease, border-color 0.15s ease;
}
.slide.layout-feedback .pad .fk-join:hover {
  transform: translateY(-1px);
  border-color: var(--s-fg);
}
.slide.layout-feedback .pad .fk-join .jl-lbl {
  font-size: 13px; font-weight: 800; letter-spacing: .12em;
  color: var(--s-accent, #4f7df9); text-transform: uppercase;
}
.slide.layout-feedback .pad .fk-join .jl-url {
  font-size: 17px; font-weight: 600; color: var(--s-dim);
  font-family: ui-monospace, monospace;
}
.slide.layout-feedback .pad .fk-join .jl-pin {
  font-size: 22px; font-weight: 800; letter-spacing: .08em;
  color: var(--s-fg); font-variant-numeric: tabular-nums;
}
.slide.layout-feedback .pad .fk-join .jl-qr-hint {
  font-size: 12px; font-weight: 700; letter-spacing: .05em;
  padding: 3px 8px; border-radius: 6px;
  background: rgba(125, 125, 125, 0.15); color: var(--s-dim);
  margin-left: 6px;
}

/* Image-stack inspector: one bordered block per layer, numbered to match the
   order they are shown in. */
.layer-row { border: 1px solid var(--ui-line); border-radius: 9px; padding: 12px; margin-bottom: 10px; }
.layer-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.layer-num {
  font-size: 11px; font-weight: 700; color: var(--ui-dim);
  background: var(--ui-panel-2); border-radius: 4px; padding: 3px 8px;
}

.field-warn { color: #8a4b2a; font-weight: 600; }

/* Share in the top bar. Marked rather than loud: it is the action somebody
   else is waiting on, so it should be findable at a glance, but it is not the
   thing the button cluster to its right is for — that is starting the room. */
/* A choice dialog: one button per option, stacked, each with its own reason.
   Wide enough that a sentence of explanation does not wrap to four lines. */
.ask-choice { max-width: 34rem; }
.ask-choice .choice-list { display: flex; flex-direction: column; gap: 10px; margin: 14px 0 4px; }
.ask-choice .choice {
  display: block; width: 100%; text-align: left;
  padding: 14px 16px; line-height: 1.35;
}
.ask-choice .choice strong { display: block; font-size: 15px; }
.ask-choice .choice span { display: block; margin-top: 4px; font-size: 13px; opacity: .72; }
.ask-choice .choice.is-off { opacity: .55; cursor: default; }

.share-btn { font-weight: 600; }
.share-btn:hover { border-color: var(--ink, currentColor); }

/* Teaching exploration: movement has a destination and then settles. */
.exploration-slide .pad{gap:14px;display:flex;flex-direction:column;padding:40px 60px}
.exploration-slide h2{margin:0;font-size:40px}.exploration-slide [hidden]{display:none!important}
.explore-compare,.explore-scene{position:relative;flex:1;min-height:0;overflow:hidden;border-radius:18px;background:rgba(128,128,128,.12)}
.explore-image{width:100%;height:100%;object-fit:contain;display:block}
.explore-after,.explore-moving{position:absolute;inset:0}.explore-moving{transition:transform .45s ease,transform-origin .45s ease}
.explore-divider{position:absolute;top:0;bottom:0;width:3px;background:currentColor;pointer-events:none}
.explore-before-label,.explore-after-label{position:absolute;top:16px;border-radius:10px;padding:8px 14px;background:#17251fee;color:white;font-size:21px}.explore-before-label{left:16px}.explore-after-label{right:16px}
.explore-range{display:flex;align-items:center;gap:24px;font-size:22px}.explore-range input{flex:1;min-width:100px;accent-color:var(--accent,#1d6b45);height:32px;cursor:ew-resize}
.explore-actions{display:flex;gap:12px}.explore-button{font:inherit;font-size:20px;border:1px solid currentColor;color:inherit;background:transparent;border-radius:10px;padding:10px 18px;cursor:pointer;align-self:flex-start}
.explore-button:disabled{cursor:default;opacity:.65}.explore-button:focus-visible,.explore-hotspot:focus-visible,.explore-range input:focus-visible{outline:4px solid #d88716;outline-offset:4px}
.explore-hotspot{position:absolute;transform:translate(-50%,-50%);width:44px;height:44px;border-radius:50%;background:#fff;color:#17251f;border:3px solid #1d6b45;font-size:23px;font-weight:700;cursor:pointer}
.explore-hotspot[aria-pressed=true]{background:#1d6b45;color:white}.explore-caption{font-size:22px;min-height:90px}.explore-caption p{margin:7px 0;line-height:1.4}
.explore-graph{flex:1;min-height:0;width:100%;color:inherit}.explore-graph circle{transition:cx .12s linear,cy .12s linear}
.explore-reading{font-size:30px;font-weight:700}.explore-formula{font-size:21px;margin:0;opacity:.8}
.explore-predict{flex:1;display:flex;flex-direction:column;justify-content:center;align-items:flex-start;gap:20px}.explore-predict h3{font-size:45px;margin:0}.explore-predict p{font-size:24px;line-height:1.5;max-width:850px}.explore-eyebrow{font-size:18px;letter-spacing:3px}
.explore-chart-result{display:flex;flex-direction:column;flex:1;min-height:0}.explore-revealed{animation:sf-fade-in .35s ease both}.explore-empty{padding:30px;font-size:24px}
@media(prefers-reduced-motion:reduce){.explore-moving,.explore-graph circle{transition:none}.explore-revealed{animation:none}}

/* Visual thinking layouts share the slide's theme and ordinary reveal steps. */
.slide .slide-date { display: block; margin-top: 24px; color: var(--s-dim); font-size: 25px; }
.layout-mindmap .pad { padding: 74px 56px 62px; }
.mindmap { position: relative; flex: 1; min-height: 0; }
.mindmap-branch { position: absolute; inset: 0; }
.mindmap-branch svg { position: absolute; width: 100%; height: 100%; overflow: visible; }
.mindmap-branch path { fill: none; stroke: var(--s-accent); stroke-width: 3px; opacity: .55; }
.mindmap-node { position: absolute; width: 29%; transform: translate(-50%, -50%); padding: 18px 20px; border: 2px solid var(--s-accent); border-radius: 22px; background: var(--s-bg); color: var(--s-fg); box-sizing: border-box; overflow-wrap: anywhere; }
.mindmap-node strong { display: block; font-size: 26px; line-height: 1.15; }
.mindmap-node span { display: block; margin-top: 8px; font-size: 21px; line-height: 1.3; color: var(--s-dim); }
.mindmap-centre { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); width: 27%; min-height: 170px; padding: 22px; box-sizing: border-box; display: grid; place-items: center; border-radius: 50%; border: 4px solid var(--s-accent); background: var(--s-bg); color: var(--s-fg); text-align: center; }
.slide .mindmap-centre h2 { margin: 0; color: inherit; font-size: 36px; line-height: 1.15; overflow-wrap: anywhere; }
.layout-introduction .pad { flex-direction: row; align-items: center; gap: 64px; }
.lecturer-portrait { width: 33%; height: 390px; flex-shrink: 0; border-radius: 28px; overflow: hidden; background: var(--s-accent); }
.lecturer-portrait img { display: block; width: 100%; height: 100%; }
.lecturer-portrait.empty { display: grid; place-items: center; border: 2px dashed var(--s-dim); background: transparent; color: var(--s-dim); font-size: 28px; }
.lecturer-copy { flex: 1; min-width: 0; }
.lecturer-kicker { font-size: 19px; letter-spacing: .12em; text-transform: uppercase; color: var(--s-dim); margin-bottom: 22px; }
.slide .lecturer-copy h1 { font-size: 62px; line-height: 1.1; margin: 0 0 20px; overflow-wrap: anywhere; }
.lecturer-role { font-size: 29px; color: var(--s-accent); line-height: 1.3; }
.lecturer-bio { font-size: 25px; line-height: 1.45; white-space: pre-line; }

/* A route keeps the sequence visible; a handover places connected stages beside each other. */
.layout-journey .pad { padding: 58px 80px 48px; }
.slide.layout-journey h2 { font-size: 40px; margin: 0 0 10px; }
.journey-context { font-size: 21px; line-height: 1.35; color: var(--s-dim); margin-bottom: 22px; }
.slide .journey-route { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; flex: 1; min-height: 0; gap: 10px; }
.journey-stop { position: relative; display: flex; align-items: flex-start; gap: 23px; flex: 1; min-height: 0; }
.journey-stop:not(:last-child)::after { content: ''; position: absolute; top: 44px; left: 21px; height: calc(100% - 34px); border-left: 3px solid var(--s-accent); opacity: .45; }
.journey-marker { width: 44px; height: 44px; flex-shrink: 0; border: 2px solid var(--s-accent); border-radius: 50%; display: grid; place-items: center; font-size: 20px; font-weight: 700; color: var(--s-fg); background: var(--s-bg); }
.journey-copy { min-width: 0; }
.slide .journey-copy h3 { font-size: 25px; line-height: 1.15; margin: 0 0 5px; }
.slide .journey-copy p { font-size: 20px; line-height: 1.3; margin: 0; color: var(--s-dim); white-space: pre-line; }
.journey-takeaway { margin-top: 18px; padding-top: 14px; border-top: 2px solid var(--s-accent); font-size: 20px; line-height: 1.35; }
.slide .journey-handover { flex-direction: row; gap: 60px; align-items: stretch; }
.journey-handover .journey-stop { flex-direction: column; gap: 20px; padding: 24px 0; border-top: 3px solid var(--s-accent); }
.journey-handover .journey-stop:not(:last-child)::after { content: '→'; top: 42%; left: auto; right: -45px; border: 0; height: auto; font-size: 36px; color: var(--s-accent); opacity: 1; }
.slide .journey-handover .journey-copy h3 { font-size: 44px; margin-bottom: 20px; }
.slide .journey-handover .journey-copy p { font-size: 24px; line-height: 1.6; }
/* Keep the teaching route readable beside the live room panel. */
.deck-viewport.railed .layout-journey h2 { font-size: 32px; }
.deck-viewport.railed .journey-context { font-size: 18px; margin-bottom: 14px; }
.deck-viewport.railed .journey-copy h3 { font-size: 22px; }
.deck-viewport.railed .journey-copy p { font-size: 18px; line-height: 1.25; }
.deck-viewport.railed .journey-takeaway { font-size: 17px; margin-top: 12px; padding-top: 10px; }
.deck-viewport.railed .journey-handover { gap: 40px; }
.deck-viewport.railed .journey-handover .journey-copy h3 { font-size: 34px; }
.deck-viewport.railed .journey-handover .journey-copy p { font-size: 20px; line-height: 1.4; }
.deck-viewport.railed .journey-handover .journey-stop:not(:last-child)::after { right: -32px; font-size: 28px; }
.deck-viewport.railed .mindmap-node { padding: 12px; }
.deck-viewport.railed .mindmap-node strong { font-size: 21px; }
.deck-viewport.railed .mindmap-node span { font-size: 17px; }
.deck-viewport.railed .mindmap-centre { min-height: 140px; padding: 12px; }
.deck-viewport.railed .mindmap-centre h2 { font-size: 28px; }
.image-facts-back { position:absolute; inset:0; z-index:8; padding:90px 100px 110px; background:var(--s-bg); color:var(--s-fg); overflow:auto; }
.image-facts-back[hidden] { display:none; }
.slide .image-facts-back h2 { font-size:44px; margin-bottom:36px; }
.image-facts-back p { font-size:28px; line-height:1.45; max-width:900px; }
/* Same flip glyph as the editor’s text/image swap (⇄), quiet chrome on the plate. */
.image-facts-toggle {
  position: absolute;
  left: 28px;
  top: 24px;
  z-index: 9;
  width: 44px;
  height: 44px;
  margin: 0;
  padding: 0;
  display: grid;
  place-items: center;
  border-radius: 8px;
  border: 1px solid color-mix(in srgb, var(--s-fg) 22%, transparent);
  background: color-mix(in srgb, var(--s-bg) 82%, transparent);
  color: var(--s-fg);
  font: 600 22px/1 system-ui, sans-serif;
  cursor: pointer;
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
}
.image-facts-toggle:hover {
  background: var(--s-accent);
  border-color: var(--s-accent);
  color: #fff;
}
.image-facts-toggle:focus-visible {
  outline: 3px solid var(--s-accent);
  outline-offset: 3px;
}
.image-facts-toggle[aria-expanded="true"] {
  background: var(--s-accent);
  border-color: var(--s-accent);
  color: #fff;
}

/* Where the work is. Quiet by default — it is a statement of fact, not an
   alert — and only leans on the eye while a write is in flight. The wording
   carries "this browser" at all times on purpose: "Saved" alone is the word
   that makes an invisible, per-origin, easily-lost copy feel filed. */
.store-state {
  font-size: 12px;
  color: var(--ui-dim);
  padding: 4px 9px;
  border: 0;
  background: transparent;
  white-space: nowrap;
  cursor: default;
  pointer-events: none;
}
.store-state::before {
  content: '';
  display: inline-block;
  width: 6px; height: 6px;
  margin-right: 6px;
  border-radius: 50%;
  background: #729458;
  vertical-align: 1px;
}
.store-state:hover { color: var(--ui-dim); }
.store-state.is-saving { color: var(--ui-text); }
.store-state.is-saving::before { background: var(--ui-dim); animation: store-pulse 1s ease-in-out infinite; }
@keyframes store-pulse { 0%, 100% { opacity: .3 } 50% { opacity: 1 } }
@media (prefers-reduced-motion: reduce) { .store-state.is-saving::before { animation: none } }
@media (max-width: 900px) { .store-state { display: none } }

/* A slide kept in the deck and out of the show. Dimmed rather than hidden:
   the rail is where its author goes looking for it, so it has to stay
   findable — and the strike through the number says "not in the running
   order" more plainly than opacity alone, which just reads as unselected. */
.rail-list .thumb.hidden-slide .frame { opacity: .38; filter: grayscale(1); }
.rail-list .thumb.hidden-slide .num { text-decoration: line-through; opacity: .6; }
.rail-list .thumb-hide {
  display: block;
  margin-top: 4px;
  padding: 2px 3px;
  font-size: 11px;
  line-height: 1;
  opacity: 0;
  background: none;
  border: 0;
  cursor: pointer;
}
.rail-list .thumb:hover .thumb-hide,
.rail-list .thumb:focus-within .thumb-hide,
.rail-list .thumb.hidden-slide .thumb-hide { opacity: .75; }
.rail-list .thumb-hide:hover { opacity: 1; }

/* A jump inside the lesson. Marked as a link because it is one, but dotted
   rather than solid: it does not leave the deck, and a solid underline on a
   projected slide promises a web page. */
#player .slide-jump, #previewBox .slide-jump {
  color: inherit;
  text-decoration: underline dotted;
  text-underline-offset: 3px;
  cursor: pointer;
}
#player .slide-jump:hover { text-decoration-style: solid; }

/* Stacked-bar values sit on the fill, so they take the surface colour rather
   than the text colour — a dark number on a dark segment is invisible. */
#player .ch-on-fill, #previewBox .ch-on-fill { fill: #fff; font-weight: 700; }
/* The donut's hole is the slide, not white: these charts are drawn on themed
   backgrounds and a white disc would be a hole in the wrong colour. */
#player .ch-donut-hole, #previewBox .ch-donut-hole { fill: var(--slide-bg, #fff); }
#player .ch-donut-total, #previewBox .ch-donut-total { font-size: 40px; font-weight: 800; fill: currentColor; }
#player .ch-donut-cap, #previewBox .ch-donut-cap { font-size: 16px; letter-spacing: .1em; text-transform: uppercase; opacity: .6; fill: currentColor; }

/* ---------------------------------------------------- chart motion & focus

   Two behaviours, both read off slide.design the way image motion is, and
   both confined to #player: an author changing a number should not have to
   watch the chart perform every time.

   The generic build animation is a fade and a small lift, which is right for
   a line of text and wrong for a bar — a bar that fades in has no relation
   to its own axis, where one that rises out of it is the quantity arriving.
   So each family grows the way its idiom reads: bars from the baseline,
   lines along their own stroke, wedges around the centre. */
@keyframes sf-ch-rise { from { transform: scaleY(0); } to { transform: scaleY(1); } }
@keyframes sf-ch-widen { from { transform: scaleX(0); } to { transform: scaleX(1); } }
@keyframes sf-ch-draw { from { stroke-dashoffset: var(--ch-len, 2000); } to { stroke-dashoffset: 0; } }
@keyframes sf-ch-sweep { from { transform: rotate(-90deg) scale(.82); opacity: 0; }
                         to { transform: rotate(0deg) scale(1); opacity: 1; } }
@keyframes sf-ch-fade { from { opacity: 0; } to { opacity: 1; } }

/* Vertical bars stand on the axis, so that is what they grow out of. The
   value label is not scaled with them — text stretched from nothing and
   snapping to size reads as a glitch, so it simply arrives. */
#player .ch-motion.chart-bar .ch-bar,
#player .ch-motion.chart-stack .ch-bar {
  animation: sf-ch-rise .62s cubic-bezier(.2,.75,.2,1) backwards;
  transform-origin: 50% 100%;
  transform-box: fill-box;
}
#player .ch-motion.chart-hbar .ch-bar {
  animation: sf-ch-widen .62s cubic-bezier(.2,.75,.2,1) backwards;
  transform-origin: 0% 50%;
  transform-box: fill-box;
}
#player .ch-motion .ch-value { animation: sf-ch-fade .5s ease .34s backwards; }
/* Each category a beat behind the last, so the eye reads left to right
   rather than being handed the whole chart at once. Capped at nine so a
   wide chart does not spend three seconds assembling itself. */
#player .ch-motion .ch-bar:nth-child(1) { animation-delay: 0s; }
#player .ch-motion .ch-bar:nth-child(2) { animation-delay: .06s; }
#player .ch-motion .ch-bar:nth-child(3) { animation-delay: .12s; }
#player .ch-motion .ch-bar:nth-child(4) { animation-delay: .18s; }
#player .ch-motion .ch-bar:nth-child(5) { animation-delay: .24s; }
#player .ch-motion .ch-bar:nth-child(6) { animation-delay: .30s; }
#player .ch-motion .ch-bar:nth-child(n+7) { animation-delay: .36s; }

#player .ch-motion.chart-line .ch-line path,
#player .ch-motion.chart-area .ch-line path[fill="none"] {
  stroke-dasharray: var(--ch-len, 2000);
  animation: sf-ch-draw 1.05s ease-out backwards;
}
#player .ch-motion .ch-dot { animation: sf-ch-fade .4s ease .7s backwards; }
#player .ch-motion.chart-area .ch-line path:not([fill="none"]) { animation: sf-ch-fade .7s ease .45s backwards; }

#player .ch-motion.chart-pie .ch-slice,
#player .ch-motion.chart-donut .ch-slice {
  animation: sf-ch-sweep .7s cubic-bezier(.2,.75,.2,1) backwards;
  transform-origin: 50% 50%;
  transform-box: view-box;
}
#player .ch-motion.chart-donut .ch-donut-hole,
#player .ch-motion.chart-donut .ch-donut-total,
#player .ch-motion.chart-donut .ch-donut-cap { animation: sf-ch-fade .5s ease .5s backwards; }

/* Isolating a series. Held back, not hidden: the comparison the chart was
   drawn for stays on the slide, so the room can be brought to one series and
   returned to all of them without the picture changing shape underneath
   them.

   Further back than the .38 a build uses for a point already made, and for
   a reason: that dims grey text against a light slide, where this dims a
   saturated fill. At .38 a red bar still pulls the eye harder than the
   green one it is meant to be standing behind, so the number that reads as
   "retreated" is lower here than it is for type.

   Which marks to dim is decided in the renderer, not here: the focused index
   is a number on the slide, and CSS cannot compare an attribute against a
   variable. Shown in the editor as well as the show, because focus is a
   property of the slide rather than a thing that happens on a projector. */
.ch-dim { opacity: .18; }
.chart-wrap [data-series], .chart-wrap .ck-item { transition: opacity .45s ease; }

#player .ch-motion.chart-bullet .ch-bar {
  animation: sf-ch-widen .62s cubic-bezier(.2,.75,.2,1) backwards;
  transform-origin: 0% 50%;
  transform-box: fill-box;
}
#player .ch-motion.chart-combo .ch-bar {
  animation: sf-ch-rise .62s cubic-bezier(.2,.75,.2,1) backwards;
  transform-origin: 50% 100%;
  transform-box: fill-box;
}
#player .ch-motion.chart-combo .ch-marker,
#player .ch-motion.chart-combo .ch-marker-line {
  animation: sf-ch-fade .5s ease .4s backwards;
}
#player .ch-motion.chart-treemap .ch-tree-rect,
#player .ch-motion.chart-waffle .ch-waffle-sq {
  animation: sf-ch-fade .45s ease backwards;
}
#player .ch-motion.chart-treemap .ch-cell:nth-child(1) .ch-tree-rect,
#player .ch-motion.chart-waffle .ch-waffle-cell:nth-child(1) .ch-waffle-sq { animation-delay: 0s; }
#player .ch-motion.chart-treemap .ch-cell:nth-child(2) .ch-tree-rect,
#player .ch-motion.chart-waffle .ch-waffle-cell:nth-child(2) .ch-waffle-sq { animation-delay: .04s; }
#player .ch-motion.chart-treemap .ch-cell:nth-child(3) .ch-tree-rect,
#player .ch-motion.chart-waffle .ch-waffle-cell:nth-child(3) .ch-waffle-sq { animation-delay: .08s; }
#player .ch-motion.chart-treemap .ch-cell:nth-child(4) .ch-tree-rect,
#player .ch-motion.chart-waffle .ch-waffle-cell:nth-child(4) .ch-waffle-sq { animation-delay: .12s; }
#player .ch-motion.chart-treemap .ch-cell:nth-child(5) .ch-tree-rect,
#player .ch-motion.chart-waffle .ch-waffle-cell:nth-child(n+5) .ch-waffle-sq { animation-delay: .16s; }
#player .ch-motion.chart-treemap .ch-cell:nth-child(n+6) .ch-tree-rect { animation-delay: .2s; }

#player .ch-tree-label, #previewBox .ch-tree-label {
  fill: var(--s-fg); font-size: 18px; font-weight: 650;
  paint-order: stroke; stroke: var(--s-bg, #fff); stroke-width: 3px;
}
#player .ch-tree-value, #previewBox .ch-tree-value {
  fill: var(--s-fg); font-size: 15px; opacity: .85;
  paint-order: stroke; stroke: var(--s-bg, #fff); stroke-width: 3px;
}
#player .ch-bullet-track, #previewBox .ch-bullet-track {
  fill: var(--s-muted, #ccc); opacity: .28;
}
#player .ch-bullet-tick, #previewBox .ch-bullet-tick {
  stroke: var(--s-fg); stroke-width: 3; stroke-linecap: round;
}

@media (prefers-reduced-motion: reduce) {
  #player .ch-motion .ch-bar,
  #player .ch-motion .ch-value,
  #player .ch-motion .ch-line path,
  #player .ch-motion .ch-dot,
  #player .ch-motion .ch-slice,
  #player .ch-motion .ch-donut-hole,
  #player .ch-motion .ch-donut-total,
  #player .ch-motion .ch-donut-cap,
  #player .ch-motion .ch-tree-rect,
  #player .ch-motion .ch-waffle-sq,
  #player .ch-motion .ch-marker,
  #player .ch-motion .ch-marker-line { animation: none; }
}

/* The position-based idioms and the pictogram. */
#player .ch-point, #previewBox .ch-point { stroke: var(--s-bg, #fff); stroke-width: 2; }
#player .ch-axis-label, #previewBox .ch-axis-label { fill: var(--s-fg); font-size: 17px; opacity: .7; }
#player .ch-bin, #previewBox .ch-bin { stroke: var(--s-bg, #fff); stroke-width: 1; }
#player .ch-whisker, #previewBox .ch-whisker { stroke-width: 2; }
/* The median is the line the eye should land on first, so it is the heaviest
   mark in the box — a 2px median inside a 2px outline reads as decoration. */
#player .ch-median, #previewBox .ch-median { stroke-width: 5; stroke-linecap: round; }
/* Hollow, so an outlier reads as a point standing apart rather than another
   filled mark competing with the box. */
#player .ch-outlier, #previewBox .ch-outlier { fill: none; stroke-width: 2; }
#player .ch-icon, #previewBox .ch-icon { dominant-baseline: alphabetic; }

/* Radar and Sankey. */
#player .ch-web, #previewBox .ch-web { stroke-dasharray: 3 5; }
/* Translucent so a band crossing another still reads as two bands; no
   colour scale, because width is already carrying the quantity and a second
   channel saying the same thing adds nothing but noise. */
#player .ch-flow, #previewBox .ch-flow { opacity: .44; }
#player .ch-flow:hover, #previewBox .ch-flow:hover { opacity: .72; }
#player .ch-node, #previewBox .ch-node { rx: 2; }
#player .ch-node-label, #previewBox .ch-node-label { font-size: 17px; }

/* ---------------------------------------------------- people & structure

   Flex columns rather than a grid of fixed tracks: a chart is two levels
   deep and four wide as often as it is four deep and two wide, and the
   layout has to hold both without the author choosing a shape. Connectors
   are borders on the wrappers, so they follow the boxes at whatever size
   the names force rather than being positioned against a guess. */
.layout-orgchart .pad { display: flex; flex-direction: column; }
.org-chart {
  display: flex; justify-content: center; align-items: flex-start;
  gap: 26px; flex: 1; min-height: 0; padding-top: 6px;
  /* Wide teams scroll rather than clipping mid-name. */
  overflow-x: auto; overflow-y: hidden;
}
.org-node { display: flex; flex-direction: column; align-items: center; position: relative; }
.org-card {
  display: flex; align-items: center; gap: 12px;
  background: var(--s-card, #ffffff14); border: 2px solid currentColor;
  border-radius: 14px; padding: 10px 16px 10px 10px; max-width: 290px;
}
.org-photo {
  width: 54px; height: 54px; flex: 0 0 54px; border-radius: 50%;
  background-size: cover; background-position: 50% 30%;
  border: 3px solid currentColor;
}
.org-initials {
  display: grid; place-content: center;
  font-size: 19px; font-weight: 800; letter-spacing: .04em;
  /* Disc is the text colour; letters are the slide background so they stay
     readable whatever the theme made currentColor. */
  background: currentColor;
  color: var(--s-bg, #fff);
}
.org-who { display: flex; flex-direction: column; min-width: 0; }
.org-who strong { font-size: 21px; line-height: 1.2; }
.org-who span { font-size: 16px; opacity: .72; line-height: 1.25; }

.org-kids { display: flex; gap: 22px; margin-top: 30px; position: relative; }
/* The stem down from a parent, and the rail across its children. Drawn from
   the children's wrapper so it spans exactly what is under it. */
.org-kids::before {
  content: ''; position: absolute; left: 0; right: 0; top: -16px;
  border-top: 2px solid currentColor; opacity: .38;
}
.org-kids.one::before { left: 50%; right: 50%; }
.org-node > .org-kids::after {
  content: ''; position: absolute; left: 50%; top: -30px; height: 14px;
  border-left: 2px solid currentColor; opacity: .38;
}
.org-kids > .org-node::before {
  content: ''; position: absolute; left: 50%; top: -16px; height: 16px;
  border-left: 2px solid currentColor; opacity: .38;
}
/* A flat team is a row of equals, so it loses the connectors entirely —
   lines between people who do not report to each other say something untrue. */
.org-flat .org-card { padding: 12px 18px 12px 12px; }
.org-chart.org-dense .org-card { padding: 7px 12px 7px 7px; max-width: 232px; }
.org-chart.org-dense .org-photo { width: 42px; height: 42px; flex-basis: 42px; }
.org-chart.org-dense .org-who strong { font-size: 18px; }
.org-chart.org-dense .org-who span { font-size: 14px; }
.org-chart.lvl-4 .org-card { padding: 6px 11px 6px 6px; }
.org-chart.lvl-4 .org-photo { width: 36px; height: 36px; flex-basis: 36px; }
.org-chart.lvl-4 .org-who strong { font-size: 16px; }
.org-chart.lvl-4 .org-who span { font-size: 13px; }
.org-chart.lvl-5 .org-card,
.org-chart.lvl-6 .org-card { padding: 5px 10px 5px 5px; max-width: 200px; }
.org-chart.lvl-5 .org-photo,
.org-chart.lvl-6 .org-photo { width: 32px; height: 32px; flex-basis: 32px; }
.org-chart.lvl-5 .org-who strong,
.org-chart.lvl-6 .org-who strong { font-size: 14px; }
.org-chart.lvl-5 .org-who span,
.org-chart.lvl-6 .org-who span { font-size: 12px; }
.org-chart.lvl-5 .org-kids,
.org-chart.lvl-6 .org-kids { gap: 14px; margin-top: 22px; }

/* The line under a chart saying where the numbers came from and what they
   are not. Quiet, but on the slide rather than in the speaker notes: a
   caveat only the lecturer can see is not a caveat. */
.chart-source {
  margin-top: 6px;
  font-size: 15px;
  line-height: 1.35;
  opacity: .62;
  max-width: 90%;
}
@media print { .chart-source { opacity: .8; font-size: 12px; } }

/* Dumbbell and evidence matrix. */
#player .ch-bell-bar, #previewBox .ch-bell-bar { stroke: currentColor; stroke-width: 4; opacity: .28; stroke-linecap: round; }
#player .ch-bell-dot, #previewBox .ch-bell-dot { stroke: var(--s-bg, #fff); stroke-width: 2; }
#player .ch-cell-label, #previewBox .ch-cell-label { font-size: 15px; font-weight: 600; fill: var(--s-fg); }
/* Flipped on the darker cells, where the slide's own text colour disappears. */
#player .ch-cell-label.on-dark, #previewBox .ch-cell-label.on-dark { fill: var(--s-bg, #fff); }

/* Named points on a scatter. The leader is lighter than the label it serves:
   it is there to attach, not to be read. */
#player .ch-point-label, #previewBox .ch-point-label { font-size: 15px; font-weight: 600; fill: var(--s-fg); }
#player .ch-leader, #previewBox .ch-leader { stroke: currentColor; stroke-width: 1; opacity: .38; }

/* Small multiples. Colour says direction, not identity: every panel is the
   same measure, so what differs between them is whether it rose or fell. */
#player .ch-multiple-title, #previewBox .ch-multiple-title { font-size: 15px; font-weight: 700; }
#player .ch-mult-value, #previewBox .ch-mult-value { font-size: 12px; fill: var(--s-fg); opacity: .8; }
#player .ch-mult-axis, #previewBox .ch-mult-axis { font-size: 11px; fill: var(--s-fg); opacity: .5; }
.ch-mult-line.dir-up, .ch-mult-dot.dir-up { stroke: #C83D77; fill: #C83D77; }
.ch-mult-line.dir-down, .ch-mult-dot.dir-down { stroke: #477F54; fill: #477F54; }
.ch-mult-line.dir-flat, .ch-mult-dot.dir-flat { stroke: #8A7A6D; fill: #8A7A6D; }
.ch-mult-line { fill: none; }

/* ============================================================ infographic
   Five shapes that read as a diagram rather than a list: stat tiles, versus
   columns, a funnel, a timeline and an iceberg. All are theme-aware through --s-* so
   every skin recolours them; the variant themes tune weights below. Type
   sizes match the bullet layouts they sit beside, so switching a slide
   between Cards and Stat tiles does not jump the heading. */
.layout-stats .pad, .layout-compare .pad, .layout-funnel .pad, .layout-timeline .pad,
.layout-iceberg .pad, .layout-spectrum .pad, .layout-sourcecheck .pad,
.layout-shift .pad, .layout-spotfake .pad { padding: 58px 80px 48px; }
.slide.layout-stats h2, .slide.layout-compare h2, .slide.layout-funnel h2, .slide.layout-timeline h2,
.slide.layout-iceberg h2, .slide.layout-spectrum h2, .slide.layout-shift h2,
.slide.layout-spotfake h2 { font-size: 40px; margin: 0 0 10px; }
.info-context { font-size: 21px; line-height: 1.35; color: var(--s-dim); margin-bottom: 26px; }
.info-empty { font-size: 26px; margin: auto 0; }
.info-takeaway { margin-top: 20px; padding-top: 14px; border-top: 2px solid var(--s-accent); font-size: 21px; line-height: 1.35; }

/* --- stat tiles */
.stats-grid { display: grid; gap: 22px; flex: 1; min-height: 0; align-content: center; grid-template-columns: repeat(3, 1fr); }
.stats-grid.stats-n1 { grid-template-columns: 1fr; max-width: 60%; margin: 0 auto; }
.stats-grid.stats-n2 { grid-template-columns: repeat(2, 1fr); }
.stats-grid.stats-n4 { grid-template-columns: repeat(4, 1fr); gap: 18px; }
.stats-grid.stats-n5, .stats-grid.stats-n6 { grid-template-columns: repeat(3, 1fr); }
/* Flat. A tile is a fill and a hairline, never a drop shadow — shadows on a
   projector read as clip-art, and the number is meant to do the work. */
.stat {
  display: flex; flex-direction: column; justify-content: center; gap: 8px;
  padding: 28px 26px; border-radius: 18px;
  background: var(--s-card);
  border: 1px solid color-mix(in srgb, var(--s-fg) 10%, transparent);
  min-width: 0;
}
.stat-value { font-size: 68px; line-height: 1; font-weight: 800; letter-spacing: -.03em; color: var(--s-accent); overflow-wrap: anywhere; }
.stats-n4 .stat-value, .stats-n5 .stat-value, .stats-n6 .stat-value { font-size: 52px; }
.stat-label { font-size: 22px; line-height: 1.25; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--s-fg); }
.stat-note { font-size: 19px; line-height: 1.35; color: var(--s-dim); }
/* Ring: a conic fill to --share of the circle drawn on ::before and masked
   to an annulus, so the value can sit unmasked in the hole. */
.stats-ring .stat { align-items: center; text-align: center; }
.stat-ring {
  --ring: 14px;
  width: 168px; height: 168px; border-radius: 50%; display: grid; place-items: center;
  position: relative;
}
.stat-ring .stat-value { font-size: 40px; color: var(--s-fg); text-align: center; padding: 0 12px; position: relative; }
.stat-ring::before {
  content: ''; position: absolute; inset: 0; border-radius: 50%;
  background: conic-gradient(var(--s-accent) calc(var(--share) * 360deg), color-mix(in srgb, var(--s-fg) 12%, transparent) 0);
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - var(--ring)), #000 calc(100% - var(--ring) + 1px));
          mask: radial-gradient(farthest-side, transparent calc(100% - var(--ring)), #000 calc(100% - var(--ring) + 1px));
}
.stats-n4 .stat-ring, .stats-n5 .stat-ring, .stats-n6 .stat-ring { width: 128px; height: 128px; --ring: 11px; }
.stats-n4 .stat-ring .stat-value, .stats-n5 .stat-ring .stat-value, .stats-n6 .stat-ring .stat-value { font-size: 30px; }
/* Bar: a track under the figure filled to --share. */
.stat-track { height: 10px; border-radius: 999px; background: color-mix(in srgb, var(--s-fg) 12%, transparent); overflow: hidden; }
.stat-fill { height: 100%; width: calc(var(--share) * 100%); background: var(--s-accent); border-radius: inherit; transition: width .5s ease; }

/* --- versus */
.compare { display: flex; flex-direction: column; gap: 10px; flex: 1; min-height: 0; justify-content: center; }
.compare-row { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.compare-labelled .compare-row { grid-template-columns: .6fr 1fr 1fr; }
.compare-cell {
  font-size: 25px; line-height: 1.35; padding: 18px 22px; border-radius: 12px;
  background: var(--s-card); border: 1px solid color-mix(in srgb, var(--s-fg) 10%, transparent);
  min-width: 0; overflow-wrap: anywhere;
}
.compare-aspect { font-size: 21px; line-height: 1.3; font-weight: 700; color: var(--s-dim); display: flex; align-items: center; padding: 0 6px; }
.compare-head .compare-cell {
  font-size: 22px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  background: none; border: 0; border-radius: 0; padding: 6px 22px 14px; border-bottom: 3px solid var(--s-accent);
}
.compare-head .compare-right { border-bottom-color: var(--s-fg); opacity: .8; }
.compare-row:not(.compare-head) .compare-left { border-left: 5px solid var(--s-accent); }
.compare-row:not(.compare-head) .compare-right { border-left: 5px solid color-mix(in srgb, var(--s-fg) 45%, transparent); }

/* --- iceberg: what you see, and the mass under it

   The waterline is the whole idea, so it is the one hard edge on the slide:
   a full-bleed rule with the tip sitting on it and the mass hanging below.
   Bands widen with depth (--w, set per band by layoutIceberg) and fade as
   they go (--depth), so the bottom of the slide reads as "this keeps going"
   rather than as a list that happens to end.

   Centred rather than left-aligned, which is the one place this file breaks
   its own habit: an iceberg that is not symmetrical about its own tip is a
   cliff, and a cliff does not say "most of this is out of sight". */
.berg { flex: 1; min-height: 0; display: flex; flex-direction: column; align-items: center; }

.berg-tip {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding-bottom: 14px;
  min-height: 74px;
}
.berg-seen {
  font-size: 30px;
  font-weight: 700;
  line-height: 1.2;
  text-align: center;
  max-width: 60%;
  color: var(--s-fg);
}

/* The waterline. Bleeds the full width of the pad and a little past it, so it
   reads as a surface the slide is cut by rather than a border on a box. */
.berg-line {
  width: calc(100% + 60px);
  margin: 0 -30px 0;
  height: 3px;
  background: var(--s-accent);
  opacity: .9;
  flex: none;
}

.berg-mass {
  list-style: none;
  margin: 0;
  padding: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding-top: 10px;
  min-height: 0;
}

/* Each band: as wide as layoutIceberg says, dimming with depth. The clip-path
   takes the bottom corners in a little so the stack tapers to a point at the
   bottom instead of ending square. */
.berg-band {
  width: var(--w, 80%);
  display: flex;
  align-items: baseline;
  gap: 16px;
  padding: 12px 26px;
  background: color-mix(in srgb, var(--s-accent) 14%, var(--s-card));
  border-top: 1px solid color-mix(in srgb, var(--s-accent) 26%, transparent);
  opacity: calc(1 - var(--depth, 0) * .1);
}
/* No clip-path taper on the last band. An earlier version cut the bottom
   corners in by 12% to point the stack, which is about 150px on a full-width
   band — far more than the 26px of padding — so it sliced the first letters
   off "Water" and the last off its value. The widening bands and the
   waterline already read as an iceberg without it; a shape that eats the
   content it exists to frame is not worth the metaphor. */
.berg-band:last-child { padding-bottom: 16px; }

.berg-copy { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; text-align: left; }
.berg-label { font-size: 24px; font-weight: 700; line-height: 1.2; color: var(--s-fg); }
.berg-note { font-size: 18px; line-height: 1.3; color: var(--s-dim); }
.berg-value {
  font-size: 30px;
  font-weight: 800;
  letter-spacing: -.02em;
  color: var(--s-accent);
  flex: none;
  font-variant-numeric: tabular-nums;
}

/* Six bands is the pit limit and the point at which 12px of padding each is
   the difference between fitting and being cut off by .slide's overflow. */
.berg-mass:has(> li:nth-child(5)) .berg-band { padding-top: 9px; padding-bottom: 9px; }
.berg-mass:has(> li:nth-child(5)) .berg-label { font-size: 21px; }
.berg-mass:has(> li:nth-child(5)) .berg-value { font-size: 26px; }

/* --- spectrum: a continuum, with things placed on it

   The dot stays on the line and only the label moves. An earlier version
   offset the whole mark to alternate labels above and below, which took the
   dot with it — so half the things being placed were sitting a centimetre off
   the axis that gives them their meaning. The mark is now a zero-size anchor
   pinned to its position, with the dot centred on it and the label absolutely
   placed above or below.

   The axis is inset by 120px each side. A label is 200px wide and centred on
   its dot, so anything at 0% or 100% would otherwise hang off the slide —
   "Climate modelling" did exactly that. */
.spectrum {
  flex: 1; min-height: 0;
  display: flex; flex-direction: column; justify-content: center;
  position: relative;
  padding: 0 120px;
}
.spec-axis {
  position: relative;
  height: 6px;
  border-radius: 3px;
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--s-accent) 22%, transparent),
    var(--s-accent));
}
/* The end labels sit clear of the label bands, hard against the slide edges
   rather than the axis ends, so they never land under a mark. */
.spec-ends {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  display: flex; justify-content: space-between;
}
.spec-end { font-size: 19px; font-weight: 800; color: var(--s-dim); text-transform: uppercase; letter-spacing: .05em; max-width: 30%; }
.spec-end-hi { text-align: right; }

.spec-marks { list-style: none; margin: 0; padding: 0; position: absolute; left: 120px; right: 120px; top: 0; bottom: 0; }
.spec-mark {
  position: absolute;
  left: var(--at, 50%);
  top: 50%;
  width: 0; height: 0;
  display: block;
}
.spec-dot {
  position: absolute;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--s-accent);
  box-shadow: 0 0 0 5px var(--s-bg);
}
.spec-copy {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  width: 200px;
  display: flex; flex-direction: column; gap: 2px;
  text-align: center;
}
/* FOUR bands, not two. Alternating above/below separates a mark from its
   immediate neighbour and that is all — with five marks, items 1, 3 and 5 all
   sit above, so two of them 18% apart on the axis put two 200px labels in the
   same place. "Junior software development" landed on top of "Routine customer
   service". Cycling four positions doubles the clearance between same-side
   neighbours without shrinking the labels. */
.spec-mark:nth-child(4n+1) .spec-copy { bottom: 94px; }
.spec-mark:nth-child(4n+3) .spec-copy { bottom: 26px; }
.spec-mark:nth-child(4n+2) .spec-copy { top: 26px; }
.spec-mark:nth-child(4n+4) .spec-copy { top: 94px; }
/* A stem from dot to label, now that some labels sit a band further out.
   Without it a far label is loose text near a line rather than a reading. */
.spec-mark::before {
  content: '';
  position: absolute;
  left: 50%;
  width: 1px;
  background: var(--s-rule);
}
.spec-mark:nth-child(4n+1)::before { bottom: 12px; height: 80px; }
.spec-mark:nth-child(4n+2)::before { top: 12px; height: 12px; }
.spec-mark:nth-child(4n+3)::before { bottom: 12px; height: 12px; }
.spec-mark:nth-child(4n+4)::before { top: 12px; height: 80px; }
.spec-label { font-size: 20px; font-weight: 700; line-height: 1.2; color: var(--s-fg); }
.spec-note { font-size: 16px; line-height: 1.3; color: var(--s-dim); }
/* Anything the author gave no number sits at the start with its dot hollow,
   so it reads as "not placed yet" rather than as "placed at zero". */
.spec-unplaced .spec-dot { background: var(--s-bg); box-shadow: 0 0 0 3px var(--s-dim) inset, 0 0 0 5px var(--s-bg); }

/* --- claim & source */
.claim-quote { margin: 0 0 18px; padding: 0 0 0 26px; border-left: 6px solid var(--s-accent); }
.claim-quote p { margin: 0; font-size: 34px; line-height: 1.24; font-weight: 700; color: var(--s-fg); }
.claim-rows { margin: 0; display: grid; gap: 10px; flex: 1; min-height: 0; align-content: center; }
.claim-row {
  display: grid;
  grid-template-columns: 150px 1fr;
  gap: 20px;
  align-items: baseline;
  padding: 12px 0;
  border-top: 1px solid var(--s-rule);
}
.claim-key {
  margin: 0;
  font-size: 17px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase;
  color: var(--s-accent);
}
.claim-val { margin: 0; font-size: 23px; line-height: 1.3; color: var(--s-fg); }
.claim-note { display: block; font-size: 17px; line-height: 1.35; color: var(--s-dim); margin-top: 3px; }

/* Five rows or more and the stack runs into the source line under it:
   .claim-rows centres its content in a flex:1 box, so an overflowing set grows
   through whatever follows rather than being clipped by it. Same treatment the
   iceberg uses at five bands. */
.claim-rows:has(> div:nth-child(5)) .claim-row { padding: 8px 0; }
.claim-rows:has(> div:nth-child(5)) .claim-val { font-size: 21px; }
.claim-rows:has(> div:nth-child(5)) .claim-note { font-size: 15px; }
.claim-rows:has(> div:nth-child(6)) .claim-row { padding: 6px 0; }

/* --- then / now / next

   The bar row is a 1fr track so a percentage height has something real to
   resolve against. Left to a content-sized flex column, --h computed against
   an auto height and every bar collapsed to a few pixels — the shape carried
   none of the information the numbers did.

   Columns share one baseline, and the multiple between each pair sits in the
   gutter at bar height, beside the jump it describes. */
.shift-track {
  list-style: none; margin: 0; padding: 0;
  flex: 1; min-height: 0;
  display: flex; align-items: stretch; gap: 0;
}
.shift-step {
  flex: 1; min-width: 0;
  display: grid;
  grid-template-rows: 1fr auto auto auto;
  align-items: end;
  text-align: center;
  padding: 0 14px;
}
.shift-bar {
  height: var(--h, 40%);
  min-height: 8px;
  align-self: end;
  border-radius: 6px 6px 0 0;
  background: linear-gradient(180deg, var(--s-accent), color-mix(in srgb, var(--s-accent) 55%, transparent));
  margin-bottom: 14px;
}
.shift-bar-unknown {
  height: 26%;
  background: none;
  border: 3px dashed color-mix(in srgb, var(--s-accent) 45%, transparent);
  border-bottom: 0;
}
.shift-value { font-size: 34px; font-weight: 800; letter-spacing: -.02em; color: var(--s-fg); font-variant-numeric: tabular-nums; line-height: 1.1; }
.shift-label { font-size: 19px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--s-accent); margin-top: 4px; }
.shift-note { font-size: 16px; line-height: 1.3; color: var(--s-dim); margin-top: 4px; }
.shift-gap {
  flex: none; width: 92px;
  display: grid;
  grid-template-rows: 1fr auto auto auto;
  align-items: end;
  justify-items: center;
  font-size: 24px; font-weight: 800;
  color: var(--s-accent);
  font-variant-numeric: tabular-nums;
}
/* Sits in the bar band, halfway up it, rather than on the baseline with the
   labels — it describes the step between two heights, so it belongs at that
   height. */
.shift-gap > span { align-self: center; padding-bottom: 14px; }
.shift-gap.shift-down { color: var(--s-dim); }

/* --- spot the fake */
.fake-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 28px; flex: 1; min-height: 0; }
.fake-side { margin: 0; display: flex; flex-direction: column; min-height: 0; position: relative; }
.fake-shot {
  flex: 1; min-height: 0;
  background: var(--s-card) center / cover no-repeat;
  border: 1px solid var(--s-rule);
  border-radius: 10px;
}
.fake-shot-empty {
  display: grid; place-items: center;
  border-style: dashed; border-width: 3px;
  color: var(--s-dim); font-size: 20px;
}
.fake-name {
  margin-top: 10px;
  font-size: 22px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
  color: var(--s-dim); text-align: center;
}
/* The verdict sits on the picture, because at the back of a hall the answer
   has to be a colour on an image rather than a word in a caption. */
.fake-verdict {
  position: absolute;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  padding: 12px 26px;
  border-radius: 999px;
  font-size: 24px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase;
  color: #fff;
}
.fake-verdict.is-real { background: #1f8a4c; }
.fake-verdict.is-fake { background: #c0392b; }
.fake-tells { margin: 18px 0 0; padding-left: 26px; display: grid; gap: 6px; }
.fake-tells li { font-size: 21px; line-height: 1.35; color: var(--s-fg); }

/* --- funnel */
.slide .funnel { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; flex: 1; min-height: 0; justify-content: center; align-items: center; }
.slide .funnel-up { flex-direction: column-reverse; }
.funnel-band {
  width: var(--w, 100%); min-width: 40%;
  display: flex; align-items: center; justify-content: space-between; gap: 24px;
  padding: 16px 30px; border-radius: 12px;
  background: color-mix(in srgb, var(--s-accent) calc(100% - var(--fade, 0%)), var(--s-card));
  color: var(--s-bg);
  box-sizing: border-box;
}
/* Later bands lighten so the eye reads the narrowing as a gradient too. */
.funnel-band:nth-child(1) { --fade: 0%; } .funnel-band:nth-child(2) { --fade: 14%; } .funnel-band:nth-child(3) { --fade: 28%; }
.funnel-band:nth-child(4) { --fade: 42%; } .funnel-band:nth-child(5) { --fade: 56%; } .funnel-band:nth-child(6) { --fade: 66%; }
.funnel-band:nth-child(n+3) { color: var(--s-fg); }
.funnel-copy { display: flex; flex-direction: column; flex: 1; min-width: 0; }
.funnel-value { flex: none; }
.slide .funnel-label { font-size: 25px; line-height: 1.2; font-weight: 700; }
.funnel-note { font-size: 18px; line-height: 1.3; opacity: .85; }
/* Numbers stay on one line; a worded value ("Make something") wraps rather
   than starving the label beside it in the narrowest band. */
.funnel-value { font-size: 34px; line-height: 1.1; font-weight: 800; letter-spacing: -.02em; max-width: 50%; text-align: right; }

/* The drop between two bands, as a figure rather than a taper. Sits in the
   flow between them so it moves with a reordered stage and needs no
   positioning; aria-hidden because the values either side already say it. */
.funnel-drop {
  display: flex; justify-content: center; align-items: center;
  height: 0; margin: -2px 0; pointer-events: none;
}
.funnel-drop span {
  font-size: 17px; font-weight: 700; letter-spacing: .04em;
  color: var(--s-dim);
  background: var(--s-bg);
  padding: 0 10px; border-radius: 999px;
  position: relative; z-index: 1;
}

/* --- timeline */
/* Neither shape stretches to fill the pad: auto margins float the track to
   the vertical middle, so six events sit centred rather than pinned under
   the heading with empty slide beneath. */
.slide .timeline { list-style: none; margin: auto 0; padding: 0; flex: none; min-height: 0; }
.slide .timeline-horizontal {
  display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; align-items: start;
  position: relative; padding-top: 30px;
}
.timeline-horizontal::before {
  content: ''; position: absolute; left: 0; right: 0; top: 30px; height: 4px; border-radius: 2px;
  background: color-mix(in srgb, var(--s-accent) 55%, transparent);
}
.timeline-horizontal .timeline-event { position: relative; padding: 32px 14px 0; min-width: 0; }
.timeline-horizontal .timeline-dot {
  position: absolute; top: -9px; left: 50%; transform: translateX(-50%);
  width: 22px; height: 22px; border-radius: 50%; background: var(--s-accent); border: 4px solid var(--s-bg);
  outline: 2px solid var(--s-accent);
}
.timeline-copy { display: flex; flex-direction: column; gap: 6px; text-align: center; }
.timeline-date { font-size: 18px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; color: var(--s-accent); }
.slide .timeline-title { font-size: 23px; line-height: 1.25; font-weight: 700; }
.timeline-note { font-size: 18px; line-height: 1.35; color: var(--s-dim); }
.timeline-n7 .timeline-title, .timeline-n8 .timeline-title { font-size: 19px; }
.timeline-n7 .timeline-note, .timeline-n8 .timeline-note { font-size: 15px; }
/* Long-form: spine down the left, one paragraph per event. */
.slide .timeline-vertical { display: flex; flex-direction: column; gap: 18px; position: relative; padding-left: 46px; }
.timeline-vertical::before { content: ''; position: absolute; left: 12px; top: 8px; bottom: 8px; border-left: 4px solid color-mix(in srgb, var(--s-accent) 55%, transparent); border-radius: 2px; }
.timeline-vertical .timeline-event { position: relative; }
.timeline-vertical .timeline-dot { position: absolute; left: -46px; top: 6px; width: 28px; height: 28px; border-radius: 50%; background: var(--s-accent); border: 5px solid var(--s-bg); outline: 2px solid var(--s-accent); }
.timeline-vertical .timeline-copy { text-align: left; display: grid; grid-template-columns: 150px 1fr; column-gap: 20px; row-gap: 2px; align-items: baseline; }
.timeline-vertical .timeline-date { grid-row: 1 / span 2; font-size: 20px; }
.timeline-vertical .timeline-title { font-size: 26px; }
.timeline-vertical .timeline-note { font-size: 20px; }

/* --- journey · stepper: one rail, numbered discs, copy beneath */
.slide .journey-stepper { flex: none; margin: auto 0; flex-direction: row; gap: 18px; align-items: stretch; position: relative; padding-top: 12px; }
.slide .journey-stepper::before { content: ''; position: absolute; left: 6%; right: 6%; top: 34px; height: 3px; background: var(--s-accent); opacity: .5; }
.journey-stepper .journey-stop { flex-direction: column; align-items: center; text-align: center; gap: 16px; }
.journey-stepper .journey-stop::after { display: none; }
.journey-stepper .journey-marker { width: 48px; height: 48px; font-size: 21px; background: var(--s-accent); color: var(--s-bg); border-color: var(--s-accent); position: relative; z-index: 1; }
.slide .journey-stepper .journey-copy h3 { font-size: 24px; }
.slide .journey-stepper .journey-copy p { font-size: 19px; }

/* --- cards · picture cards: an empty slot says where the photograph goes */
.layout-cards .card-pic.card-pic-empty {
  display: grid; place-items: center;
  border: 2px dashed var(--s-rule);
  background: color-mix(in srgb, var(--s-fg) 4%, transparent);
  color: var(--s-dim); font-size: 17px; text-align: center;
}

/* Beside the live room panel everything steps down a notch. */
.deck-viewport.railed .stat-value { font-size: 48px; }
.deck-viewport.railed .compare-cell { font-size: 20px; padding: 12px 16px; }
.deck-viewport.railed .funnel-label { font-size: 20px; }
.deck-viewport.railed .timeline-title { font-size: 19px; }

/* ---- generated cover motion --------------------------------------------- */

/* Motion for a cover, made out of the slide's own colours.

   The alternative was a video file per palette: megabytes, wrong the moment
   the theme changes, and another asset to keep in step. Every colour here is
   the theme's own accent at low alpha, so a blob is a wash of the theme over
   its own ground — pale on paper, a glow on midnight — and a theme added next
   year is handled without touching this file.

   Alpha rather than a mix towards --s-bg, which was the first attempt: half
   the themes set that token to a linear-gradient, and color-mix() with a
   gradient in it is invalid, so the blobs computed to transparent on exactly
   the themes with the most interesting grounds. Translucency does not care
   what is behind it.

   Behind everything: the theme art paints over it, the pad sits above both. */
.slide .slide-motion {
  position: absolute; inset: 0; z-index: 0;
  overflow: hidden; pointer-events: none;
  border-radius: inherit;
}
.slide .slide-motion .mo {
  position: absolute;
  width: 58%; aspect-ratio: 1 / 1;
  border-radius: 50%;
  /* Blurred this hard, the shapes stop being shapes and become weather. */
  filter: blur(64px);
  will-change: transform;
}
.slide .slide-motion .mo-1 {
  left: -8%; top: -14%;
  background: color-mix(in srgb, var(--s-accent) 40%, transparent);
}
.slide .slide-motion .mo-2 {
  right: -12%; top: 12%;
  background: color-mix(in srgb, var(--s-accent-2, var(--s-accent)) 34%, transparent);
}
.slide .slide-motion .mo-3 {
  left: 26%; bottom: -26%;
  width: 66%;
  background: color-mix(in srgb, var(--s-fg) 14%, transparent);
}

/* Drift: three closed paths at three speeds. Each one returns to where it
   started, so there is no jump to hide, and the periods are deliberately not
   multiples of each other — the composite pattern takes minutes to repeat,
   which is longer than any cover is on screen. */
@keyframes sf-mo-a {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  50%      { transform: translate3d(14%, 10%, 0) scale(1.08); }
}
@keyframes sf-mo-b {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1.04); }
  50%      { transform: translate3d(-12%, 14%, 0) scale(1); }
}
@keyframes sf-mo-c {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  50%      { transform: translate3d(8%, -12%, 0) scale(1.1); }
}
.slide .motion-drift .mo-1 { animation: sf-mo-a 31s ease-in-out infinite; }
.slide .motion-drift .mo-2 { animation: sf-mo-b 43s ease-in-out infinite; }
.slide .motion-drift .mo-3 { animation: sf-mo-c 37s ease-in-out infinite; }

/* Grid: the same tinted ground with a ruled plane travelling over it, one
   cell per cycle — which is the same picture again, so it never cuts. For a
   data course this reads as squared paper rather than as decoration. */
.slide .motion-grid .mo-3 { display: none; }
.slide .motion-grid::after {
  content: ''; position: absolute; inset: -80px;
  background-image:
    linear-gradient(to right, color-mix(in srgb, var(--s-fg) 9%, transparent) 1px, transparent 1px),
    linear-gradient(to bottom, color-mix(in srgb, var(--s-fg) 9%, transparent) 1px, transparent 1px);
  background-size: 80px 80px;
  animation: sf-mo-grid 24s linear infinite;
}
@keyframes sf-mo-grid {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(80px, 80px, 0); }
}

/* Glow: one slow breath behind the words, for a cover whose type is doing the
   work and only wants the slide to look switched on. */
.slide .motion-glow .mo-1,
.slide .motion-glow .mo-2 { display: none; }
.slide .motion-glow .mo-3 {
  left: 50%; bottom: auto; top: 50%;
  width: 92%;
  transform: translate(-50%, -50%);
  background: radial-gradient(closest-side,
    color-mix(in srgb, var(--s-accent) 34%, transparent), transparent);
  filter: blur(40px);
  animation: sf-mo-breathe 19s ease-in-out infinite;
}
@keyframes sf-mo-breathe {
  0%, 100% { opacity: .55; transform: translate(-50%, -50%) scale(1); }
  50%      { opacity: .9;  transform: translate(-50%, -50%) scale(1.12); }
}

/* Still for anyone who asked for still, and in a handout, where a page that
   animates is a page that prints differently every time. The ground stays —
   it is the motion that is optional. */
@media (prefers-reduced-motion: reduce) {
  .slide .slide-motion .mo,
  .slide .slide-motion::after { animation: none; }
}
.pdf-page .slide-motion .mo,
.pdf-page .slide-motion::after { animation: none; }

/* Centred type takes its furniture with it. The accent bar is a box, so
   text-align does nothing to it: pinned left under a centred title it reads as
   a mistake rather than a choice. */
.pad.pad-align-center > .accent-bar { margin-left: auto; margin-right: auto; }
.pad.pad-align-right > .accent-bar { margin-left: auto; margin-right: 0; }

/* ---- statement ---------------------------------------------------------- */

/* One line, bold, in the middle. Title is a front door and Section is a
   divider; this is for a thought — six words with nothing else on the slide.

   Sized by band rather than by measurement so the editor, the wall, a rail
   thumbnail and a printed page all agree. */
.layout-statement .pad {
  justify-content: center; align-items: center;
  text-align: center;
  padding: 72px 96px;
}
.layout-statement .statement {
  font-weight: 800;
  line-height: 1.04;
  letter-spacing: -0.02em;
  /* Breaks the line where the sense is rather than at the right margin. */
  text-wrap: balance;
  max-width: 22ch;
}
.layout-statement .statement[data-len="xs"] { font-size: 140px; }
.layout-statement .statement[data-len="sm"] { font-size: 104px; max-width: 26ch; }
.layout-statement .statement[data-len="md"] { font-size: 78px;  max-width: 30ch; }
.layout-statement .statement[data-len="lg"] { font-size: 58px;  max-width: 34ch; }
.layout-statement .statement[data-len="xl"] { font-size: 44px;  max-width: 38ch; }
.layout-statement .statement.dim { opacity: .45; }
.layout-statement .statement-credit {
  margin-top: 34px;
  font-size: 22px; font-weight: 500; letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--s-dim);
}
/* A thumbnail is 1/7 of a slide, and 140px of type in it is one letter. The
   rail shows the shape of the slide, so the band scales with the frame. */
.variant-frame .layout-statement .statement,
.rail .layout-statement .statement { letter-spacing: 0; }

/* One word takes the whole line: the character cap that keeps a sentence to a
   readable measure would otherwise wrap a long word mid-air. */
.layout-statement .statement.statement-word {
  max-width: none;
  letter-spacing: -0.035em;
}

/* ---- words arriving ----------------------------------------------------- */

/* A statement can bring its words in one at a time.

   The easing is After Effects' Easy Ease — 33% influence each side, which is
   cubic-bezier(.33, 0, .67, 1). The stagger is eased too (computed per word in
   render.js) rather than a fixed step, so the line arrives as one movement
   that slows as it finishes instead of a metronome.

   The blur is the cheap honest version of motion blur: a few pixels while the
   word is travelling, resolving to nothing as it lands. It costs one composited
   filter per word for under a second, and it is what stops a fast transform
   looking like a jump cut. */
.slide .statement.words .w { display: inline-block; }

/* Only in the show. In the editor the line is rebuilt on every keystroke, so
   an entrance would replay while you typed — the same reason the caption fade
   is scoped to the player. */
#player .slide .statement.words .w {
  /* Set per slide from the Speed control; the fallbacks are Medium. */
  animation-duration: var(--w-dur, 620ms);
  animation-delay: var(--d, 0ms);
  animation-timing-function: cubic-bezier(.33, 0, .67, 1);   /* Easy Ease */
  animation-fill-mode: both;
}

/* Rise: up from below, blur clearing as it settles. */
@keyframes sf-word-rise {
  from { opacity: 0; transform: translate3d(0, var(--w-lift, .55em), 0); filter: blur(7px); }
  60%  { opacity: 1; }
  to   { opacity: 1; transform: translate3d(0, 0, 0); filter: blur(0); }
}
#player .slide .statement.words-rise .w { animation-name: sf-word-rise; }

/* Fade: no movement, for a statement that should not look like a title card. */
@keyframes sf-word-fade {
  from { opacity: 0; filter: blur(4px); }
  to   { opacity: 1; filter: blur(0); }
}
#player .slide .statement.words-fade .w { animation-name: sf-word-fade; }

/* Reveal: each word wiped up from behind its own line, the kinetic-type look.
   clip-path rather than a mask element, so nothing is added to the DOM. */
@keyframes sf-word-reveal {
  from { clip-path: inset(0 0 100% 0); transform: translate3d(0, .22em, 0); }
  to   { clip-path: inset(0 0 -18% 0); transform: translate3d(0, 0, 0); }
}
#player .slide .statement.words-reveal .w {
  animation-name: sf-word-reveal;
  /* A wipe needs a little longer than a fade to read as a wipe. */
  animation-duration: calc(var(--w-dur, 620ms) * 1.13);
}

/* Asked for less motion: the words are simply there. Not a slower version —
   a still one. */
@media (prefers-reduced-motion: reduce) {
  #player .slide .statement.words .w {
    animation: none !important;
    opacity: 1; filter: none; clip-path: none; transform: none;
  }
}
/* And a printed page is a still by definition. */
.pdf-page .statement.words .w { animation: none !important; }

/* ---- words arriving, and leaving again ---------------------------------- */

/* In, hold, out, round again — for a cover on screen while a room fills.

   One keyframe set per effect rather than an entrance animation plus an exit
   animation: two animations on one element need their own delays kept in step,
   and the first time the hold changed they would drift apart. Here the hold is
   a percentage of the cycle, so there is one number to change.

   The percentages: the words are in by 12%, hold to 74% — about four seconds
   of a six-second cycle, which is long enough to read a six-word line twice —
   then the same eased wave takes them out by 88%, and the last stretch is the
   pause before it comes round. Easy Ease both ways, which is what makes it
   read as one breath rather than two events. */
#player .slide .statement.words-loop .w {
  animation-duration: var(--w-cycle, 6200ms);
  animation-iteration-count: infinite;
  animation-fill-mode: both;
}

/* The percentages, and why they moved: the exit used to be 74%→88%, which on
   a 6.2s cycle is 870ms for the whole wave to leave — quick enough that a
   room looking at the words saw them gone rather than going. It is 58%→86%
   now, nearly two seconds on the default cycle, and the hold is still the
   longest phase at half of it. */
@keyframes sf-cycle-rise {
  0%          { opacity: 0; transform: translate3d(0, var(--w-lift, .55em), 0); filter: blur(7px); }
  10%, 58%    { opacity: 1; transform: translate3d(0, 0, 0); filter: blur(0); }
  86%, 100%   { opacity: 0; transform: translate3d(0, calc(var(--w-lift, .55em) * -0.8), 0); filter: blur(8px); }
}
#player .slide .statement.words-loop.words-rise .w { animation-name: sf-cycle-rise; }

@keyframes sf-cycle-fade {
  0%          { opacity: 0; filter: blur(4px); }
  10%, 58%    { opacity: 1; filter: blur(0); }
  86%, 100%   { opacity: 0; filter: blur(5px); }
}
#player .slide .statement.words-loop.words-fade .w { animation-name: sf-cycle-fade; }

@keyframes sf-cycle-reveal {
  0%          { clip-path: inset(0 0 100% 0); transform: translate3d(0, .22em, 0); }
  10%, 58%    { clip-path: inset(0 0 -18% 0); transform: translate3d(0, 0, 0); }
  86%, 100%   { clip-path: inset(100% 0 0 0); transform: translate3d(0, -.18em, 0); }
}
#player .slide .statement.words-loop.words-reveal .w { animation-name: sf-cycle-reveal; }

/* A loop is the one thing reduced motion must not leave running. The rule
   above it already stops the animation; this makes sure what is left on screen
   is the readable state rather than whichever frame it stopped on. */
@media (prefers-reduced-motion: reduce) {
  #player .slide .statement.words-loop .w {
    animation: none !important;
    opacity: 1; filter: none; clip-path: none; transform: none;
  }
}

/* Ken Burns with a destination: from one focus point to another.

   The two frames are computed per slide (see imageTravel in render.js) and
   handed to the keyframes as custom properties — the browser interpolates
   between two transforms it was given, which is stable, composited, and does
   not need a JS timer ticking through a lecture.

   Easy Ease again: a linear pan across a photograph looks like a machine. */
#player .layout-image .img.img-motion-travel {
  transform-origin: 0 0;
  animation: sf-img-travel var(--kb-dur, 20s) cubic-bezier(.33, 0, .67, 1) both;
  will-change: transform;
}
@keyframes sf-img-travel {
  from { transform: var(--kb-from, scale(1.2)); }
  to   { transform: var(--kb-to, scale(1.2)); }
}
/* Still, on the frame the author chose to start on — which is the
   establishing shot, not the destination. */
@media (prefers-reduced-motion: reduce) {
  #player .layout-image .img.img-motion-travel {
    animation: none;
    transform: var(--kb-from, scale(1.2));
  }
}
.pdf-page .img.img-motion-travel { animation: none; transform: var(--kb-from, scale(1.2)); }

/* ---- chart callouts ----------------------------------------------------- */

/* "Look at this bit now", as steps inside one chart slide.

   The zoom is a transform on the SVG rather than a change of viewBox: the
   transform is composited and animatable, and SVG re-rasterises at the scaled
   size, so a zoomed axis label is still sharp rather than a blown-up bitmap.
   The wrapper clips, which is what makes the rest of the chart leave the frame
   instead of overflowing the slide. */
.layout-chart .chart-wrap { overflow: hidden; }
.layout-chart .chart-svg {
  transition: transform .62s cubic-bezier(.33, 0, .67, 1);   /* Easy Ease */
  will-change: transform;
}
/* The note for the band being looked at. It occupies its line from the start
   — see the empty element in layoutChart — so beginning the walk does not
   reflow the chart it is describing. */
.layout-chart .ch-callout {
  min-height: 34px;
  margin-top: 10px;
  font-size: 23px; font-weight: 600; line-height: 1.25;
  color: var(--s-fg);
  opacity: 0;
  transition: opacity .3s ease;
}
.layout-chart .ch-callout.on { opacity: 1; }
/* A hairline under the zoomed chart, so it reads as a detail of something
   rather than as a different chart. */
.layout-chart .chart-svg.ch-zoomed { outline: 1px solid var(--s-rule); outline-offset: -1px; }

@media (prefers-reduced-motion: reduce) {
  .layout-chart .chart-svg { transition: none; }
}

/* ---- spotlight builds --------------------------------------------------- */

/* The room's eye on the point being made.

   `dim` already holds the past back; this adds the other half of what a
   presenter does with their hand — it takes the light off everything else.
   The past steps fall further back than in dim mode, the live one is left at
   full strength, and a soft vignette closes in from the edges of the slide so
   the middle reads as lit rather than the rest as broken.

   Everything here is opacity and a gradient: no layout changes, so a build
   cannot reflow the slide halfway through being taught. */
#player .slide.build-spot .step.step-past {
  opacity: .32;
  filter: saturate(.75);
}
#player .slide.build-spot .step.step-live {
  /* Not a glow. The live point is simply the only thing at full strength;
     lighting it as well as dimming the others reads as a highlighter pen. */
  opacity: 1;
}
#player .slide.build-spot::after {
  content: '';
  position: absolute; inset: 0;
  pointer-events: none;
  z-index: 2;
  background: radial-gradient(ellipse 128% 92% at 50% 50%,
    transparent 38%, color-mix(in srgb, #000 34%, transparent) 100%);
  opacity: 0;
  transition: opacity .5s ease;
}
/* Only once the build has started: a vignette on an untouched slide is a
   vignette nobody asked for. */
#player .slide.build-spot:has(.step-live)::after { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  #player .slide.build-spot::after { transition: none; }
}
/* A handout is read, not presented: every point at full strength. */
.pdf-page .slide.build-spot::after { display: none; }
.pdf-page .slide.build-spot .step.step-past { opacity: 1; filter: none; }

/* A per-word choreography, written rather than chosen.

   Each word starts wherever the plan put it — offset, turned, scaled, blurred
   — and lands at rest. One keyframe set drives all of them because the
   difference between the words is in the custom properties, not in the
   animation: eighteen words is eighteen sets of variables and one rule. */
#player .slide .statement.words-plan .w {
  /* The arc, per word — settle, bounce or mist. Each keyframe set reads the
     same six variables, so one word can land differently from its neighbour
     without a second rule. */
  animation-name: var(--wk, sf-word-plan) !important;
  transform-origin: 50% 60%;
}
/* Letter by letter. The word stays one box so the line can only break
   between words; the letters inside it are what move.

   A letter is a narrower box than a word, so the transform origin comes back
   to the middle — a letter rotating about a point 60% down its own height
   swings, where a word pivots. */
#player .slide .statement .wword { display: inline-block; white-space: pre; }
#player .slide .statement.words-letters .w { transform-origin: 50% 50%; }
@keyframes sf-word-plan {
  from {
    opacity: 0;
    transform: translate3d(var(--wx, 0), var(--wy, .5em), 0)
               rotate(var(--wr, 0deg)) scale(var(--ws, 1));
    filter: blur(var(--wb, 6px));
  }
  70% { opacity: 1; }
  to  { opacity: 1; transform: none; filter: blur(0); }
}
/* The loop version: the same start, held, then back out the way it came in —
   so a planned line can also be left running on a cover. */
@keyframes sf-cycle-plan {
  0% {
    opacity: 0;
    transform: translate3d(var(--wx, 0), var(--wy, .5em), 0)
               rotate(var(--wr, 0deg)) scale(var(--ws, 1));
    filter: blur(var(--wb, 6px));
  }
  10%, 58% { opacity: 1; transform: none; filter: blur(0); }
  86%, 100% {
    opacity: 0;
    transform: translate3d(var(--wx, 0), var(--wy, .5em), 0)
               rotate(var(--wr, 0deg)) scale(var(--ws, 1));
    filter: blur(var(--wb, 6px));
  }
}
/* Past the resting place and back, twice, smaller each time.

   The overshoot is expressed as a fraction of the word's OWN start offset, so
   a word that came from the left overshoots to the right by a proportional
   amount and one that barely moved barely overshoots — the bounce stays in
   proportion to the journey instead of being a fixed jolt bolted on the end.
   Scale reads the same way: 1 + (1 - --ws) continues past rest in whichever
   direction the word was growing. */
@keyframes sf-word-plan-bounce {
  0% {
    opacity: 0;
    transform: translate3d(var(--wx, 0px), var(--wy, .5em), 0)
               rotate(var(--wr, 0deg)) scale(var(--ws, 1));
    filter: blur(var(--wb, 6px));
  }
  42% { opacity: 1; filter: blur(0); }
  60% {
    transform: translate3d(calc(var(--wx, 0px) * -0.16), calc(var(--wy, .5em) * -0.16), 0)
               rotate(calc(var(--wr, 0deg) * -0.16))
               scale(calc(1 + (1 - var(--ws, 1)) * 0.16));
  }
  78% {
    transform: translate3d(calc(var(--wx, 0px) * 0.07), calc(var(--wy, .5em) * 0.07), 0)
               rotate(calc(var(--wr, 0deg) * 0.07))
               scale(calc(1 - (1 - var(--ws, 1)) * 0.07));
  }
  90% {
    transform: translate3d(calc(var(--wx, 0px) * -0.03), calc(var(--wy, .5em) * -0.03), 0)
               rotate(calc(var(--wr, 0deg) * -0.03))
               scale(calc(1 + (1 - var(--ws, 1)) * 0.03));
  }
  100% { opacity: 1; transform: none; filter: blur(0); }
}
/* Out of the mist: in place long before it is in focus.

   The difference from a settle is which property finishes last. A settle
   clears its blur at 70% and spends the rest arriving; this arrives by 55%
   and spends the rest condensing, which is what reads as a word resolving out
   of fog rather than a word flying in slightly soft. The blur floor means a
   plan with blur 0 still mists — the arc supplies the fog if the numbers
   did not. */
@keyframes sf-word-plan-mist {
  0% {
    opacity: 0;
    transform: translate3d(var(--wx, 0px), var(--wy, .5em), 0)
               rotate(var(--wr, 0deg)) scale(var(--ws, 1));
    filter: blur(max(var(--wb, 6px), 5px));
  }
  22% { opacity: .3; }
  55% {
    opacity: .72;
    /* 92% of the way to rest: close enough to read as placed, not yet still. */
    transform: translate3d(calc(var(--wx, 0px) * 0.08), calc(var(--wy, .5em) * 0.08), 0)
               rotate(calc(var(--wr, 0deg) * 0.08))
               scale(calc(var(--ws, 1) + (1 - var(--ws, 1)) * 0.92));
    filter: blur(calc(max(var(--wb, 6px), 5px) * 0.5));
  }
  100% { opacity: 1; transform: none; filter: blur(0); }
}
/* The mist loop: in soft, held sharp, back out soft. */
@keyframes sf-cycle-mist {
  0% {
    opacity: 0;
    transform: translate3d(var(--wx, 0px), var(--wy, .5em), 0)
               rotate(var(--wr, 0deg)) scale(var(--ws, 1));
    filter: blur(max(var(--wb, 6px), 5px));
  }
  14%, 56% { opacity: 1; transform: none; filter: blur(0); }
  82%, 100% {
    opacity: 0;
    transform: translate3d(calc(var(--wx, 0px) * 0.5), calc(var(--wy, .5em) * 0.5), 0)
               rotate(calc(var(--wr, 0deg) * 0.5)) scale(var(--ws, 1));
    filter: blur(max(var(--wb, 6px), 5px));
  }
}
#player .slide .statement.words-plan.words-loop .w { animation-name: var(--wk, sf-cycle-plan) !important; }

@media (prefers-reduced-motion: reduce) {
  #player .slide .statement.words-plan .w {
    animation: none !important;
    opacity: 1; transform: none; filter: none;
  }
}

/* The choreography control: a brief, a button, and what came back. */
#inspector .word-plan { display: flex; flex-direction: column; gap: 8px; }
#inspector .word-plan .hint { margin: 2px 0 0; }

/* Canvas arrangement tools are editor-only, never part of an exported slide. */
#previewBox .chrome-slot { position:relative; }
.canvas-region-handle {
  position:absolute; top:-18px; right:-8px; z-index:8; width:40px; height:40px;
  padding:0; border:2px solid #fff; border-radius:50%; background:#213d31;
  color:#fff; font:26px/1 sans-serif; cursor:grab; touch-action:none;
  opacity:0; pointer-events:auto; box-shadow:0 2px 7px #0005;
}
.chrome-slot:hover .canvas-region-handle,.canvas-region-handle:focus-visible,
.canvas-region-handle[aria-expanded=true] { opacity:1; }
.canvas-region-handle:active { cursor:grabbing; }
.canvas-region-targets {
  position:absolute; inset:24px 44px; z-index:50; display:grid;
  grid-template-columns:repeat(3,minmax(0,1fr)); grid-template-rows:88px 1fr 88px;
  gap:16px; pointer-events:none;
}
.canvas-region-targets button {
  pointer-events:auto; padding:12px; border:3px dashed #546c58; border-radius:12px;
  background:#fffffff2; color:#172d22; font:600 20px/1.3 system-ui; cursor:pointer;
}
.canvas-region-targets button:is(:hover,:focus-visible,.snap-active) { border-style:solid; border-color:#172d22; outline:3px solid #e4f178; background:#e4f178; }
.canvas-region-hint { grid-column:1/-1; grid-row:2; align-self:start; justify-self:center; margin-top:8px; padding:10px 16px; background:#172d22; color:#fff; border-radius:8px; font:18px/1.3 system-ui; }
@media (hover:none) { .canvas-region-handle { opacity:1; } }
