/* Pull in bundled @font-face declarations. @import doesn't trigger
   font downloads by itself — each face only loads when it's actually
   requested by some element's computed font-family. */
@import url("/fonts/fonts.css");

/* Base styles. Kept intentionally small. View-specific tweaks live in
   each view's HTML <style> block.

   ─── Theming model ──────────────────────────────────────────────────
   Two token families, independent of each other.

   1. CONTENT tokens — how the slides themselves render.
      Names: --bg, --fg, --muted, --accent, --rule, --border, --card,
             --callout-*, --code-*, --table-stripe, --focus-bg, …
      Scope: :root by default. Overridable via [data-slide-theme] on
             <html> or on any slide container.
      Owner: the deck. Phase 2 will hydrate these from deck.theme.

   2. CHROME tokens — the editor / presenter SHELL around the slides.
      Names: --chrome-bg, --chrome-fg, --chrome-muted, --chrome-accent,
             --chrome-border, --chrome-card, --chrome-notes-bg,
             --chrome-input-bg, --chrome-shadow, --chrome-hover-shadow,
             --chrome-danger
      Scope: :root by default. Overridable via [data-theme=dark|light]
             on <html>.
      Owner: the user's local preference (slideshow:theme in
             localStorage).

   The two are intentionally independent. Toggling the editor's chrome
   to dark mode (working at night) does NOT darken slide previews —
   the author still sees what the audience will see. Conversely, the
   audience's "d" toggle flips CONTENT theme only; it does not affect
   the presenter's chrome on a different screen.
   ───────────────────────────────────────────────────────────────────── */

:root {
  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --serif: "Charter", "Iowan Old Style", "Georgia", serif;
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Inter", system-ui, sans-serif;

  /* ──────────────── Phase 7 — engine chrome primitives ────────────────
     Radius, motion, and block-type hues. These are chrome-only — slide
     rendering contracts are untouched. `--radius-*` and `--dur-*` are
     consumed by the shared .btn / .chip / .tabs primitives below.
     `--bt-*` is the per-block-type accent palette (scan by color rather
     than reading every label on the add-block bar).
  */
  --radius-sm:   6px;
  --radius-md:   8px;
  --radius-lg:  12px;
  --radius-pill: 999px;

  --ease:     cubic-bezier(.2,.6,.2,1);
  --dur-fast: 120ms;
  --dur-med:  180ms;

  /* Shared navbar height — promoted to :root so sticky descendants
     (editor palette strip, side panes, presenter body padding) track
     the same value. Update here when the nav padding/type changes. */
  --nav-height: 56px;

  /* Per-block-type hues — chrome only, DO NOT use for rendered slide content. */
  --bt-heading:     #7dd3fc; /* sky */
  --bt-paragraph:   #cbd5e1; /* slate */
  --bt-bullets:     #5eead4; /* teal */
  --bt-quote:       #f5b342; /* amber */
  --bt-code:        #c4b5fd; /* violet */
  --bt-image:       #fda4af; /* rose */
  --bt-table:       #93c5fd; /* blue */
  --bt-callout:     #86efac; /* emerald */
  --bt-poll:        #67e8f9; /* cyan */
  --bt-reaction:    #f0abfc; /* fuchsia */
  --bt-divider:     #9ca3af; /* gray */
  --bt-text-submit: #bef264; /* lime */
  --bt-svg:         #fdba74; /* orange */
  --bt-citations:   #d6c28b; /* bronze */

  /* ──────────────── Phase 6 Track A — design tokens ────────────────
     Tokens are the authoritative vocabulary for color / type / space.
     The linter (Track C) reads these to decide whether an inline value
     in a deck is on-scale or off-scale. Downstream tracks (B patterns,
     D variant preview) also consume them. Do not add ad-hoc hex values
     or em scalars in new block CSS — extend the scale or pick the
     nearest token.

     Customization path: theme.json at the deck level overrides --bg /
     --fg / --accent (see src/deck.ts). Per-slide overrides are still
     handled by the slide.overrides path. Dark default is a seed, not
     a cage. Existing decks that want the light palette should set
     "slideTheme": "light" in their theme.json.
  */

  /* Semantic content colors — reserved for medical-content discipline.
     Do NOT use these for generic accent coloring; the linter (Track C)
     will flag misuse as drift. */
  --sem-pathologic: #F87171;   /* abnormal / urgent */
  --sem-normal:     #4ADE80;   /* at-target / normal */
  --sem-caution:    #FCD34D;   /* watch / caution */
  --sem-info:       #60A5FA;   /* informational emphasis */
  --sem-reference:  #A78BFA;   /* citations / footnotes */
  --accent-2: #2DD4BF;         /* secondary content accent */
  --accent-3: #A78BFA;         /* tertiary content accent */
  --accent-soft: color-mix(in srgb, var(--accent) 14%, transparent);
  --accent-2-soft: color-mix(in srgb, var(--accent-2) 12%, transparent);
  --accent-3-soft: color-mix(in srgb, var(--accent-3) 12%, transparent);
  --panel-bg: color-mix(in srgb, var(--fg) 4%, var(--bg));
  --panel-border: color-mix(in srgb, var(--fg) 10%, transparent);
  --panel-border-strong: color-mix(in srgb, var(--fg) 22%, transparent);

  /* Type scale — discrete px values on a 1920×1080 canvas. Line-height
     1.25 for display sizes, 1.5 for body. Use these in block CSS instead
     of em scalars. */
  --fs-2xs:    10px;
  --fs-xs:     12px;
  --fs-sm:     14px;
  --fs-base:   18px;
  --fs-lg:     24px;
  --fs-xl:     36px;
  --fs-display: 56px;
  --fs-hero:   88px;
  --lh-display: 1.25;
  --lh-body:    1.5;

  /* Spacing — 8px grid. Prefer these over magic numbers. */
  --space-1:  8px;
  --space-2: 16px;
  --space-3: 24px;
  --space-4: 32px;
  --space-5: 48px;
  --space-6: 64px;
  --space-7: 96px;
}

/* ──────────────── CHROME tokens ──────────────── */

:root,
html[data-theme="light"] {
  /* Warm paper — warmer than the flat #fafaf7 it used to be, so slide
     cards feel like they're floating on a stock rather than glued flat. */
  --chrome-bg: #f4f1ea;
  --chrome-fg: #1a1816;
  --chrome-muted: #595649;
  --chrome-accent: #1f4b78;
  --chrome-accent-hi: #2a5f92;
  --chrome-accent-ink: #ffffff;
  --chrome-border: #e4dfd3;
  --chrome-border-strong: #c7c0b0;
  --chrome-card: #ffffff;
  --chrome-card-strong: #ffffff;      /* floating islands, more opaque */
  --chrome-notes-bg: #fafaf3;
  --chrome-input-bg: #ffffff;
  --chrome-btn-bg: #f2f2ed;
  --chrome-btn-bg-hover: #e8e8e0;
  --chrome-btn-border: #cfcfc8;
  /* Phase 7 additions — surface layering for chrome. */
  --chrome-bg-sunken: #ede9df;
  --chrome-bg-hover:  #efece3;
  --chrome-panel:     #ffffff;
  --chrome-panel-2:   #ffffff;
  --chrome-warm:      #a47424;         /* pearl accent */
  --chrome-ok:        #2f7a43;
  --chrome-info:      #1f5f8a;
  --chrome-shadow:    0 1px 2px rgba(30,20,10,.06);
  --chrome-hover-shadow: 0 6px 18px rgba(30,20,10,.08), 0 1px 2px rgba(30,20,10,.05);
  --chrome-shadow-lg: 0 20px 40px rgba(30,20,10,.12), 0 2px 6px rgba(30,20,10,.08);
  --chrome-ring:      0 0 0 3px rgba(31,75,120,.22);
  --chrome-danger:    #a63d3d;
}

html[data-theme="dark"] {
  /* Warmer, more saturated dark than the prior near-black. Keeps the
     blue cast but pulls toward sepia-tinged neutral so the serif
     headings feel at home. */
  --chrome-bg: #0d1117;
  --chrome-fg: #e8e6e0;
  --chrome-muted: #a4a9b4;
  --chrome-accent: #7db3e0;
  --chrome-accent-hi: #9dc8ed;
  --chrome-accent-ink: #0b1420;
  --chrome-border: #262c38;
  --chrome-border-strong: #3a4152;
  --chrome-card: #141a22;
  --chrome-card-strong: #1a2130;
  --chrome-notes-bg: #141720;
  --chrome-input-bg: #1c2029;
  --chrome-btn-bg: #232731;
  --chrome-btn-bg-hover: #2c3039;
  --chrome-btn-border: #3a3f48;
  /* Phase 7 additions — surface layering for chrome. */
  --chrome-bg-sunken: #0a0e14;
  --chrome-bg-hover:  #1f2633;
  --chrome-panel:     #141a22;
  --chrome-panel-2:   #1a2130;
  --chrome-warm:      #e5b463;          /* pearl accent */
  --chrome-ok:        #6bce8f;
  --chrome-info:      #74b5f5;
  --chrome-shadow:    0 1px 2px rgba(0,0,0,.4);
  --chrome-hover-shadow: 0 4px 14px rgba(0,0,0,.35), 0 1px 2px rgba(0,0,0,.4);
  --chrome-shadow-lg: 0 20px 40px rgba(0,0,0,.5), 0 2px 6px rgba(0,0,0,.35);
  --chrome-ring:      0 0 0 3px rgba(125,179,224,.28);
  --chrome-danger:    #e87a7a;
}

/* ──────────────── CONTENT tokens ──────────────── */
/* Phase 6 Track A flipped the unscoped default from light → dark. New
   decks render dark out of the box. Existing decks that want the old
   palette set "slideTheme": "light" in theme.json — see deck.theme
   hydration in src/deck.ts. The light and dark rules themselves are
   unchanged so `slideTheme` acts as the opt-in/opt-out knob. */

:root,
html[data-slide-theme="dark"],
[data-slide-theme="dark"] {
  --bg: #0B0F14;               /* near-black with blue cast */
  --fg: #E8E6E0;               /* warm off-white body */
  --muted: #9BA3AE;            /* captions, metadata */
  --accent: #7db3e0;           /* deck-chosen; overridden by theme.json */
  --surface: #141A22;          /* callout / table backgrounds (Phase 6) */
  --rule: #3a3f47;
  --border: #2a2e35;
  --card: #171a20;
  --focus-bg: #2a2410;
  --table-stripe: #15181e;
  --code-bg: #0a0c10;
  --code-fg: #E8E6E0;
  --inline-code-bg: rgba(255,255,255,0.08);
  --inline-code-fg: #E8E6E0;
  --callout-info-bg: #1a2330;
  --callout-info-fg: #E8E6E0;
  --callout-answer-bg: #183020;
  --callout-answer-fg: #dff0df;
  --callout-pearl-bg: #2c2410;
  --callout-pearl-fg: #f5e8c2;
  --callout-warn-bg: #30181a;
  --callout-warn-fg: #f5d2cf;
  /* Alias --fg-muted to --muted so Track C / pattern CSS can use the
     semantic name without a second source of truth. */
  --fg-muted: var(--muted);
}

html[data-slide-theme="light"],
[data-slide-theme="light"] {
  --bg: #fafaf7;
  --fg: #1a1a1a;
  --muted: #6b6b6b;
  --accent: #2c5f8d;
  --surface: #ffffff;
  --rule: #cfcfc8;
  --border: #e4e4df;
  --card: #ffffff;
  --focus-bg: #fff9e6;
  --table-stripe: #f5f5f0;
  --code-bg: #111;
  --code-fg: #f5f5f0;
  --inline-code-bg: rgba(0,0,0,0.06);
  --inline-code-fg: #1a1a1a;
  --callout-info-bg: #eef4fa;
  --callout-info-fg: #1a1a1a;
  --callout-answer-bg: #eaf5ea;
  --callout-answer-fg: #1a1a1a;
  --callout-pearl-bg: #fff4d6;
  --callout-pearl-fg: #2a2210;
  --callout-warn-bg: #fbe7e5;
  --callout-warn-fg: #4a1818;
  --fg-muted: var(--muted);
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
}

a { color: var(--accent); }

/* ----- Slide rendering -----
   The slide is ALWAYS a fixed 1920×1080 logical canvas. Every view
   wraps it in a .slide-viewport that handles scaling to fit the
   available space via CSS transform (see below). This gives us a
   single authoritative coordinate system — what looks right in the
   editor is exactly what the audience sees, regardless of screen size.
*/

.slide {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: flex-start;
  gap: 0.75em;
  padding: 64px 96px;
  width: 1920px;
  height: 1080px;
  flex-shrink: 0;          /* don't let flex parents squeeze us */
  overflow: hidden;
  /* Slide carries its own content-theme colors so it renders
     correctly regardless of whatever chrome surrounds it. */
  background: var(--bg);
  color: var(--fg);
}

.slide[data-layout="title"] {
  justify-content: center;
  align-items: center;
  text-align: center;
}

/* Two-column: blocks flow left-to-right, wrapping after the column
   width is exhausted. Odd blocks in the left column, even in the right
   — CSS :nth-child isn't used because it handles block-count changes
   cleanly without renderer assistance. Instead we just split the flex
   row in half with a consistent block width. */
.slide[data-layout="two-column"] {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: flex-start;
  align-content: flex-start;
  column-gap: 64px;
  row-gap: 0.75em;
}
.slide[data-layout="two-column"] > .block {
  flex: 0 0 calc(50% - 32px);
  max-width: calc(50% - 32px);
}
.slide[data-layout="two-column"] > .block.heading {
  flex-basis: 100%;
  max-width: 100%;
}

/* Image-left / image-right: first image block pinned to one side,
   everything else in a column on the other side. Implemented with
   flex: the first image gets a fixed share of the width and is
   order-1/order-2 controlled. */
.slide[data-layout="image-left"],
.slide[data-layout="image-right"] {
  flex-direction: row;
  align-items: center;
  gap: 64px;
}
.slide[data-layout="image-left"] > .block.image,
.slide[data-layout="image-right"] > .block.image {
  flex: 0 0 45%;
  max-width: 45%;
  max-height: 100%;
}
.slide[data-layout="image-left"] > .block.image img,
.slide[data-layout="image-right"] > .block.image img {
  max-width: 100%;
  max-height: 900px;
  object-fit: contain;
}
.slide[data-layout="image-left"] > .block:not(.image),
.slide[data-layout="image-right"] > .block:not(.image) {
  flex: 1 1 auto;
}
.slide[data-layout="image-right"] > .block.image {
  order: 2;
}

/* Centered-quote: one pull-quote filling the canvas. */
.slide[data-layout="centered-quote"] {
  justify-content: center;
  align-items: center;
  text-align: center;
}
.slide[data-layout="centered-quote"] .quote {
  font-size: 1.8em;
  max-width: 1400px;
  border-left: none;
  padding: 0;
}

/* Flow: no flex centering — blocks stack naturally from the top.
   Useful for long prose slides. */
.slide[data-layout="flow"] {
  justify-content: flex-start;
}

/* Free-canvas: switches the slide root to position:relative so blocks
   with position.mode="absolute" land relative to the slide canvas
   (1920×1080 logical pixels). Flex centering is dropped — blocks
   place themselves. */
.slide[data-layout="free-canvas"] {
  display: block;
  position: relative;
  padding: 0;
  gap: 0;
  justify-content: stretch;
  align-items: stretch;
}
.slide[data-layout="free-canvas"] > .block.svg,
.slide[data-layout="free-canvas"] .svg-figure {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  flex: 1;
}
.slide[data-layout="free-canvas"] .svg-content {
  height: 100%;
}
.slide[data-layout="free-canvas"] > .block[data-position-mode="absolute"] {
  position: absolute;
}
.slide[data-layout="free-canvas"] > .block[data-anchor="top-right"] {
  transform: translateX(-100%);
}
.slide[data-layout="free-canvas"] > .block[data-anchor="bottom-left"] {
  transform: translateY(-100%);
}
.slide[data-layout="free-canvas"] > .block[data-anchor="bottom-right"] {
  transform: translate(-100%, -100%);
}
.slide[data-layout="free-canvas"] > .block[data-anchor="center"] {
  transform: translate(-50%, -50%);
}

/* ───────── 12×12 grid layout ─────────
   When data-layout="grid", the slide becomes a CSS grid container
   with 12 equal columns and 12 equal rows. Blocks place themselves
   via:

     - position.mode="slot" → matched to a class selector below that
                              sets grid-column and grid-row for the
                              named region.
     - position.mode="grid" → renderer writes inline grid-column /
                              grid-row from x/y/w/h.
     - position.mode="flow" → block falls into the implicit auto rows
                              after the explicit grid. Should be rare;
                              prefer slot="footer".

   The grid lives inside the slide's normal padding (64×96), so slot
   coordinates are in a 1728×952 effective area split into 12×12 cells.
   Each cell is ~144×79.3 — small enough that a slot like "corner-tr"
   (cols 11-12) is a comfortable badge size, and large enough that a
   6-cell-wide slot is roughly half the canvas. */
.slide[data-layout="grid"] {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  grid-template-rows: repeat(12, 1fr);
  /* 16px gutter between grid cells. Tweak per deck via per-slide CSS
     overrides if a denser grid is wanted. */
  gap: 16px;
  /* Override the flex defaults inherited from `.slide` — alignment is
     up to the placed blocks. */
  justify-content: stretch;
  align-items: stretch;
}

/* Named slots. Each slot sets grid-column + grid-row based on the
   registry in src/registry.ts. Keep this block in sync — registry is
   the source of truth, CSS is the implementation. */
.slide[data-layout="grid"] > .block[data-slot="left-rail"]           { grid-column: 1 / 5;   grid-row: 2 / 13; }
.slide[data-layout="grid"] > .block[data-slot="right-rail"]          { grid-column: 9 / 13;  grid-row: 2 / 13; }
.slide[data-layout="grid"] > .block[data-slot="main"]                { grid-column: 1 / 9;   grid-row: 2 / 12; }
.slide[data-layout="grid"] > .block[data-slot="main-with-left-rail"] { grid-column: 5 / 13;  grid-row: 2 / 12; }
.slide[data-layout="grid"] > .block[data-slot="header"]              { grid-column: 1 / 13;  grid-row: 1 / 2; }
.slide[data-layout="grid"] > .block[data-slot="footer"]              { grid-column: 1 / 13;  grid-row: 12 / 13; }
.slide[data-layout="grid"] > .block[data-slot="corner-tl"]           { grid-column: 1 / 3;   grid-row: 1 / 3; }
.slide[data-layout="grid"] > .block[data-slot="corner-tr"]           { grid-column: 11 / 13; grid-row: 1 / 3; }
.slide[data-layout="grid"] > .block[data-slot="corner-bl"]           { grid-column: 1 / 3;   grid-row: 11 / 13; }
.slide[data-layout="grid"] > .block[data-slot="corner-br"]           { grid-column: 11 / 13; grid-row: 11 / 13; }
.slide[data-layout="grid"] > .block[data-slot="centered"]            { grid-column: 3 / 11;  grid-row: 4 / 10; }
.slide[data-layout="grid"] > .block[data-slot="full-bleed"]          { grid-column: 1 / 13;  grid-row: 1 / 13; }
.slide[data-layout="grid"] > .block[data-slot="col-left"]            { grid-column: 1 / 7;   grid-row: 2 / 12; }
.slide[data-layout="grid"] > .block[data-slot="col-right"]           { grid-column: 7 / 13;  grid-row: 2 / 12; }

/* Grid-placed blocks need minmax safeguards so content can't expand
   their cell beyond the grid track. min-width/height:0 on a grid item
   is the standard fix for long bullet text overflowing a column. */
.slide[data-layout="grid"] > .block {
  min-width: 0;
  min-height: 0;
}

/* Full-bleed blocks go edge-to-edge — neutralize the slide padding
   with negative margins. Simpler than restructuring the grid. */
.slide[data-layout="grid"] > .block[data-slot="full-bleed"] {
  margin: -64px -96px;
}

/* ----- Slide viewport -----
   Wraps a fixed-size .slide and scales it to fit. The wrapper sizes
   itself to available space (each view applies its own rules); the
   inner .slide transforms from center-origin, so flex centering +
   overflow:hidden cleanly crops the oversized layout box.
*/

.slide-viewport {
  position: relative;
  overflow: hidden;
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: center;
}

.slide-viewport > .slide {
  transform-origin: center center;
  transform: scale(var(--slide-scale, 1));
}

/* Overflow flag — editor uses this to warn authors that their content
   has spilled past the 1920×1080 canvas. Audience/presenter views can
   style the indicator differently (or hide it) with more specific rules. */
.slide-viewport[data-overflow="true"] {
  outline: 2px solid var(--chrome-danger, #d93a2f);
  outline-offset: -2px;
}
.slide-viewport[data-overflow="true"]::after {
  content: "⚠ content overflow";
  position: absolute; top: 4px; right: 6px;
  background: var(--chrome-danger, #d93a2f);
  color: #fff;
  font-family: var(--mono); font-size: 11px;
  padding: 2px 6px; border-radius: 3px;
  pointer-events: none;
  z-index: 2;
}

/* CSS floor — Phase 6 Track E.
   Every block gets safe wrapping by default: long words break cleanly,
   hyphens assist when the language allows. Combined with the slide
   viewport's overflow:hidden, this keeps a pathologically long URL or
   cardiology-term compound from shoving a column off-canvas. Blocks can
   opt into stricter visual discipline with a `maxLines` prop (see
   [data-max-lines] rule below). */
.block {
  margin: 0;
  max-width: 100%;
  overflow-wrap: anywhere;
  word-break: normal;
  hyphens: auto;
}

/* Opt-in line clamp with visible ellipsis. Renderer stamps
   `data-max-lines="N"` and sets `--block-max-lines: N`; this rule
   hooks it to the CSS line-clamp. Applies to heading and paragraph
   blocks (see types.ts). */
.block[data-max-lines] {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: var(--block-max-lines, 3);
  line-clamp: var(--block-max-lines, 3);
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Per-block overflow badges (editor-only).
   public/client/viewport.js walks each block's bounding rect against
   the slide's 1920×1080 canvas and stamps `data-overflow="block-bleed"`
   (block extends past the canvas) or `data-overflow="text-clip"` (the
   block's content is clipped by its own overflow:hidden). Only
   .slide-viewport[data-mode="card"] — the editor slide card — shows
   the badges so the audience view stays clean. */
.slide-viewport[data-mode="card"] .block[data-overflow="block-bleed"] {
  outline: 2px dashed var(--chrome-danger, #d93a2f);
  outline-offset: 2px;
  position: relative;
}
.slide-viewport[data-mode="card"] .block[data-overflow="block-bleed"]::before {
  content: "⚠ bleeds off canvas";
  position: absolute;
  top: -18px;
  right: 0;
  background: var(--chrome-danger, #d93a2f);
  color: #fff;
  font: 600 10px/1.2 var(--mono);
  padding: 2px 6px;
  border-radius: 3px;
  pointer-events: none;
  z-index: 3;
}
.slide-viewport[data-mode="card"] .block[data-overflow="text-clip"] {
  outline: 2px dashed var(--chrome-warn, #f59e0b);
  outline-offset: 2px;
  position: relative;
}
.slide-viewport[data-mode="card"] .block[data-overflow="text-clip"]::before {
  content: "⚠ text clipped";
  position: absolute;
  top: -18px;
  right: 0;
  background: var(--chrome-warn, #f59e0b);
  color: #111;
  font: 600 10px/1.2 var(--mono);
  padding: 2px 6px;
  border-radius: 3px;
  pointer-events: none;
  z-index: 3;
}

/* Type-scale migration (Phase 6 Track A).
   Block CSS reads from --fs-* / --lh-* tokens rather than em scalars
   so sizes are consistent across slides and auditable by the linter.
   The quote/code sizes are slightly larger than the old em ratios
   because the token set is coarser — that's intentional: 8 steps to
   pick from keeps new decks from drifting into 57px / 62px one-offs. */

.heading {
  font-family: var(--heading-font, var(--serif));
  line-height: var(--lh-display);
  letter-spacing: -0.01em;
  margin: 0;
}
.heading.h-1 { font-size: var(--fs-display); font-weight: 700; }
.heading.h-2 { font-size: var(--fs-xl); font-weight: 600; color: var(--fg-muted); }

.paragraph {
  font-size: var(--fs-base);
  line-height: var(--lh-body);
  max-width: 36ch;
}

.bullets {
  font-size: var(--fs-base);
  line-height: var(--lh-body);
  padding-left: 1.2em;
}
.bullets .bullet { margin-bottom: var(--space-1); }

.quote {
  font-family: var(--serif);
  font-size: var(--fs-xl);
  font-style: italic;
  line-height: var(--lh-display);
  max-width: 24ch;
  border-left: 4px solid var(--accent);
  padding-left: 1rem;
  margin: 0;
}
.quote-attribution {
  font-style: normal;
  font-size: var(--fs-sm);
  color: var(--fg-muted);
  margin-top: var(--space-1);
}

.code {
  font-family: var(--mono);
  font-size: var(--fs-base);
  background: var(--code-bg);
  color: var(--code-fg);
  padding: 1rem 1.5rem;
  border-radius: 8px;
  overflow-x: auto;
  max-width: 100%;
}
.inline-code {
  font-family: var(--mono);
  font-size: 0.9em;
  background: var(--inline-code-bg);
  color: var(--inline-code-fg);
  padding: 0.1em 0.4em;
  border-radius: 3px;
}

/* ----- Inline typography marks -----
   Produced by renderer.js inlineMd(). The content-theme decides colors
   so light/dark themes render these cleanly. */

.block mark,
.slide mark {
  background: #fff3a0;
  color: inherit;
  padding: 0 0.12em;
  border-radius: 2px;
}
html[data-slide-theme="dark"] .block mark,
html[data-slide-theme="dark"] .slide mark,
[data-slide-theme="dark"] .block mark,
[data-slide-theme="dark"] .slide mark {
  background: #6a5a1a;
  color: #fff3a0;
}

.block sup, .block sub,
.slide sup, .slide sub {
  font-size: 0.7em;
  line-height: 0;
  position: relative;
  vertical-align: baseline;
}
.block sup, .slide sup { top: -0.5em; }
.block sub, .slide sub { bottom: -0.2em; }

.colored-text[data-color="red"]    { color: #c0392b; }
.colored-text[data-color="green"]  { color: #2f7a43; }
.colored-text[data-color="blue"]   { color: #2c5f8d; }
.colored-text[data-color="amber"]  { color: #a6801f; }
.colored-text[data-color="accent"] { color: var(--accent); }
.colored-text[data-color="muted"]  { color: var(--muted); }
html[data-slide-theme="dark"] .colored-text[data-color="red"],
[data-slide-theme="dark"] .colored-text[data-color="red"]    { color: #ee8878; }
html[data-slide-theme="dark"] .colored-text[data-color="green"],
[data-slide-theme="dark"] .colored-text[data-color="green"]  { color: #6ecb7e; }
html[data-slide-theme="dark"] .colored-text[data-color="blue"],
[data-slide-theme="dark"] .colored-text[data-color="blue"]   { color: #7db3e0; }
html[data-slide-theme="dark"] .colored-text[data-color="amber"],
[data-slide-theme="dark"] .colored-text[data-color="amber"]  { color: #e5c56a; }

/* ----- Selection toolbar -----
   Floating bubble that appears when text is selected inside an
   editable slide block. Inserts the markdown markers that inlineMd()
   recognises. CHROME tokens — it's editor furniture, not slide content. */

#selection-toolbar {
  position: fixed;
  display: none;
  z-index: 60;
  gap: 2px;
  align-items: center;
  padding: 3px;
  background: var(--chrome-card-strong, var(--chrome-card));
  color: var(--chrome-fg);
  border: 1px solid var(--chrome-border);
  border-radius: 6px;
  box-shadow: var(--chrome-hover-shadow);
  font-family: var(--sans);
  font-size: 13px;
  -webkit-user-select: none;
  user-select: none;
}
#selection-toolbar.visible { display: inline-flex; }

#selection-toolbar button,
#selection-toolbar select {
  font: inherit;
  padding: 3px 7px;
  min-width: 26px;
  height: 24px;
  background: transparent;
  color: var(--chrome-fg);
  border: 1px solid transparent;
  border-radius: 4px;
  cursor: pointer;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
#selection-toolbar button:hover,
#selection-toolbar select:hover {
  background: var(--chrome-btn-bg-hover);
  border-color: var(--chrome-btn-border);
}
#selection-toolbar button b { font-weight: 700; }
#selection-toolbar button i { font-style: italic; }
#selection-toolbar .tb-hl {
  background: #fff3a0;
  color: #2a2210;
  padding: 0 4px;
  border-radius: 2px;
}
#selection-toolbar .tb-code {
  font-family: var(--mono);
  font-size: 0.9em;
}
#selection-toolbar .tb-sep {
  width: 1px;
  height: 16px;
  background: var(--chrome-border);
  margin: 0 2px;
}
#selection-toolbar select {
  padding: 3px 6px;
  background: var(--chrome-btn-bg);
  border-color: var(--chrome-btn-border);
}

.image { margin: 0; max-width: 100%; }
.image img { max-width: 100%; max-height: 60vh; display: block; }
.image-caption { font-size: 0.85em; color: var(--muted); margin-top: 0.25rem; }

.divider {
  width: 40%;
  border: 0;
  border-top: 1px solid var(--rule);
  margin: 1rem 0;
}

/* ----- Table ----- */
.table-wrap {
  width: 100%;
  max-width: 100%;
  overflow-x: auto;
  margin: 0;
}
.table {
  border-collapse: collapse;
  width: 100%;
  font-size: 1em;
  line-height: 1.35;
}
.table th, .table td {
  text-align: left;
  vertical-align: top;
  padding: 0.45em 0.7em;
  border-bottom: 1px solid var(--rule);
}
.table th {
  font-weight: 600;
  color: var(--muted);
  font-size: 0.85em;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  border-bottom: 1.5px solid var(--rule);
}
.table tbody tr:nth-child(even) { background: var(--table-stripe); }
.table-caption {
  font-size: 0.85em;
  color: var(--muted);
  margin-top: 0.35em;
  font-style: italic;
}

/* ----- Citations ----- */
.citations {
  font-size: 0.75em;
  color: var(--muted);
  padding-left: 1.2em;
  line-height: 1.35;
  margin: 0;
}
.citations .citation { margin-bottom: 0.15em; }

/* ----- Callouts ----- */
.callout {
  padding: 0.75em 1em;
  border-radius: 6px;
  font-size: 1.15em;
  line-height: 1.45;
  border-left: 4px solid var(--accent);
  max-width: 100%;
}
.callout-info    { background: var(--callout-info-bg);    color: var(--callout-info-fg);    border-left-color: var(--accent); }
.callout-answer  { background: var(--callout-answer-bg);  color: var(--callout-answer-fg);  border-left-color: #3a8a4f; }
.callout-pearl   { background: var(--callout-pearl-bg);   color: var(--callout-pearl-fg);   border-left-color: #c4a03a; }
.callout-warn    { background: var(--callout-warn-bg);    color: var(--callout-warn-fg);    border-left-color: #b33a30; }

/* Composed callouts — callouts that carry a `title` and/or nested
   children instead of a single text body. The title sits at the top
   like a small caption; children stack inside a body container with
   the callout's usual padding.

   Children are regular blocks (.heading/.paragraph/.bullets/...) so
   they inherit slide typography. We strip their outer margins here
   so the stack is visually tight inside the box, and we shrink the
   heading sizes a touch — a nested h1 at 60px inside a callout is
   absurd. */
.callout-title {
  font-family: var(--sans);
  font-weight: 700;
  font-size: 0.85em;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  opacity: 0.75;
  margin-bottom: 0.35em;
}
.callout-body {
  display: flex;
  flex-direction: column;
  gap: 0.4em;
}
.callout-body > .block { margin: 0; max-width: 100%; }
.callout-body > .block.heading.h-1 { font-size: 1.4em; line-height: 1.2; }
.callout-body > .block.heading.h-2 { font-size: 1.15em; line-height: 1.25; }
.callout-body > .block.paragraph   { font-size: 1em; line-height: 1.4; }
.callout-body > .block.bullets     { padding-left: 1.2em; font-size: 1em; }
.callout-body > .block.citations   { font-size: 0.85em; opacity: 0.85; }

.callout-body-legacy { /* no reset — inherits the .callout box styling */ }

/* Editor-only affordances for composed callouts. The "+ child" bar
   sits below the body; the × delete button per child floats top-right
   inside the child. Both only exist in the editor (the renderer in
   audience/presenter mode doesn't run decorateCallout). */
.callout-add-child {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin-top: 0.6em;
  padding-top: 0.5em;
  border-top: 1px dashed currentColor;
  opacity: 0.55;
  transition: opacity 120ms;
}
.callout:hover .callout-add-child { opacity: 0.9; }
.callout-add-child button {
  font: inherit;
  font-family: var(--mono);
  font-size: 11px;
  padding: 2px 8px;
  background: transparent;
  color: inherit;
  border: 1px solid currentColor;
  border-radius: 3px;
  cursor: pointer;
  opacity: 0.8;
}
.callout-add-child button:hover { opacity: 1; }

.callout-body > .block {
  position: relative;
}
.callout-child-controls {
  position: absolute;
  top: 2px;
  right: 2px;
  display: flex;
  gap: 4px;
  opacity: 0;
  transition: opacity 120ms;
}
.callout-body > .block:hover .callout-child-controls { opacity: 1; }
.callout-child-controls button {
  font: inherit;
  font-family: var(--mono);
  font-size: 14px;
  line-height: 1;
  width: 20px;
  height: 20px;
  padding: 0;
  background: var(--bg);
  color: var(--fg);
  border: 1px solid currentColor;
  border-radius: 3px;
  cursor: pointer;
  opacity: 0.7;
}
.callout-child-controls button:hover {
  opacity: 1;
  background: #d33;
  color: #fff;
  border-color: #d33;
}

/* ----- Poll block ----------------------------------------------
   Live audience polls. The block paints a question, a stack of option
   buttons, and a status line. Each option can render as a flat button
   (results hidden) or as a horizontal bar chart (results visible). The
   bar lives BEHIND the label via absolute positioning, so the fill
   can sweep across without cutting the text in half. */

.poll {
  border: 1px solid var(--rule, currentColor);
  border-radius: 8px;
  padding: 0.9em 1em 0.8em;
  background: color-mix(in srgb, var(--accent, currentColor) 4%, transparent);
}

.poll-question {
  font-weight: 600;
  font-size: 1.05em;
  margin-bottom: 0.7em;
  line-height: 1.35;
}

.poll-options {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.4em;
}

.poll-option { margin: 0; padding: 0; }

.poll-option-btn {
  position: relative;
  width: 100%;
  min-height: 2.4em;
  padding: 0.55em 0.9em;
  background: var(--bg);
  color: inherit;
  border: 1px solid var(--rule, currentColor);
  border-radius: 6px;
  font: inherit;
  font-size: 0.95em;
  text-align: left;
  cursor: pointer;
  overflow: hidden;
  display: flex;
  align-items: center;
  gap: 0.6em;
  transition: border-color 120ms, background 120ms;
}
.poll-option-btn:hover:not(:disabled) {
  border-color: var(--accent, currentColor);
}
.poll-option-btn:disabled {
  cursor: default;
}

/* The bar sits absolutely behind the label. Using a color-mix keeps
   the fill tinted by the slide's accent without hardcoding a value. */
.poll-option-bar {
  position: absolute;
  left: 0; top: 0; bottom: 0;
  background: color-mix(in srgb, var(--accent, currentColor) 22%, transparent);
  border-right: 1px solid color-mix(in srgb, var(--accent, currentColor) 55%, transparent);
  transition: width 220ms ease;
  pointer-events: none;
  z-index: 0;
}

.poll-option-label,
.poll-option-tally,
.poll-option-pick-mark {
  position: relative;
  z-index: 1;
}

.poll-option-label { flex: 1; min-width: 0; }
.poll-option-tally {
  font-family: var(--mono);
  font-size: 0.82em;
  opacity: 0.75;
  white-space: nowrap;
}
.poll-option-pick-mark {
  display: inline-block;
  width: 1.2em;
  text-align: center;
  color: var(--accent, currentColor);
  font-weight: 700;
}

.poll-option[data-picked="true"] .poll-option-btn {
  border-color: var(--accent, currentColor);
  background: color-mix(in srgb, var(--accent, currentColor) 7%, var(--bg));
}

.poll-status {
  margin-top: 0.7em;
  font-family: var(--mono);
  font-size: 0.78em;
  opacity: 0.7;
}

/* State-specific tweaks */
.poll-idle .poll-option-btn   { opacity: 0.75; }
.poll-closed .poll-option-btn { cursor: default; }

/* Phase 7 Phase B — quiz-style reveal. Quiz mode kicks in when the
   server hands us a closed poll with a correctOptionId. Two axes:
   whether the option IS the correct one, and whether the voter picked
   it (separately: picked + wrong, or picked + correct). */
.poll-option-correct-mark,
.poll-option-wrong-mark {
  position: relative;
  z-index: 1;
  display: inline-block;
  width: 1.2em;
  text-align: center;
  font-weight: 800;
  margin-left: 0.35em;
}
.poll-option-correct-mark { color: #2aa56b; }
.poll-option-wrong-mark   { color: #d64c4c; }

.poll-option[data-correct="true"] .poll-option-btn {
  border-color: #2aa56b;
  background: color-mix(in srgb, #2aa56b 10%, var(--bg));
}
.poll-option[data-wrong="true"] .poll-option-btn {
  border-color: #d64c4c;
  background: color-mix(in srgb, #d64c4c 10%, var(--bg));
}
.poll-option[data-correct="true"] .poll-option-bar {
  background: color-mix(in srgb, #2aa56b 22%, transparent);
  border-right-color: color-mix(in srgb, #2aa56b 55%, transparent);
}

.poll[data-voter-result="correct"] .poll-status { color: #2aa56b; opacity: 1; }
.poll[data-voter-result="wrong"]   .poll-status { color: #d64c4c; opacity: 1; }

/* Editor-only: per-poll controls (state, show-results, options).
   Hidden in audience/presenter; the editor injects .poll-controls
   via decoratePoll and styles them inline. */
.poll-controls {
  margin-top: 0.8em;
  padding-top: 0.6em;
  border-top: 1px dashed currentColor;
  display: flex;
  flex-direction: column;
  gap: 0.5em;
  opacity: 0.6;
  transition: opacity 120ms;
  font-family: var(--mono);
  font-size: 11px;
}
.poll:hover .poll-controls { opacity: 1; }
.poll-controls-row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
}
.poll-controls label { font-size: 11px; opacity: 0.8; margin-right: 2px; }
.poll-controls button,
.poll-controls select {
  font: inherit;
  font-family: var(--mono);
  font-size: 11px;
  padding: 2px 8px;
  background: transparent;
  color: inherit;
  border: 1px solid currentColor;
  border-radius: 3px;
  cursor: pointer;
}
.poll-controls button:hover { background: color-mix(in srgb, currentColor 10%, transparent); }
.poll-controls .poll-option-editor {
  display: flex;
  gap: 6px;
  align-items: center;
}
.poll-controls .poll-option-editor input[type="text"] {
  flex: 1;
  min-width: 0;
  font: inherit;
  font-family: inherit;
  font-size: 12px;
  padding: 3px 6px;
  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--rule, currentColor);
  border-radius: 3px;
}

/* Phase 7 Phase B — editor-only "correct answer" toggle. Sits as the
   first child in each option editor row (before the label input). Hollow
   star is "not correct"; filled-gold star is "correct". Mutually
   exclusive across options — only one correctOptionId per poll. */
.poll-controls .poll-correct-toggle {
  width: 22px;
  min-width: 22px;
  padding: 0;
  font-size: 14px;
  line-height: 1;
  border: 1px solid transparent;
  background: transparent;
  color: color-mix(in srgb, currentColor 55%, transparent);
  cursor: pointer;
}
.poll-controls .poll-correct-toggle:hover {
  color: currentColor;
}
.poll-controls .poll-correct-toggle[data-on="true"] {
  color: #d4a84a;
  background: color-mix(in srgb, #d4a84a 12%, transparent);
  border-color: color-mix(in srgb, #d4a84a 55%, transparent);
}

/* ----- Text-submit block -----
   Mirrors .poll's structural choices: accent-mixed background tint,
   prompt on top, input row in the middle, submissions list below,
   status footer. Curation UI is opt-in via the editor's decoration. */

.text-submit {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 14px 16px;
  background: color-mix(in srgb, var(--accent) 6%, transparent);
  border-radius: 6px;
  border: 1px solid color-mix(in srgb, var(--accent) 20%, var(--rule, transparent));
}
.text-submit-prompt {
  font-weight: 600;
  font-size: 1.05em;
  margin: 0;
}
.text-submit-input-row {
  display: flex;
  gap: 8px;
  align-items: flex-end;
}
.text-submit-input {
  flex: 1;
  min-width: 0;
  font: inherit;
  font-family: inherit;
  font-size: 0.95em;
  padding: 6px 8px;
  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--rule, currentColor);
  border-radius: 4px;
  resize: vertical;
  min-height: 2.4em;
}
.text-submit-input:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}
.text-submit-counter {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--muted);
  min-width: 64px;
  text-align: right;
}
.text-submit-counter.over {
  color: #b00;
}
.text-submit-btn {
  font: inherit;
  font-size: 0.85em;
  padding: 6px 12px;
  background: var(--accent);
  color: var(--bg);
  border: 1px solid var(--accent);
  border-radius: 4px;
  cursor: pointer;
}
.text-submit-btn:hover:not(:disabled) {
  filter: brightness(1.08);
}
.text-submit-btn:disabled {
  opacity: 0.55;
  cursor: default;
}
.text-submit-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.text-submit-entry {
  position: relative;
  padding: 8px 10px;
  background: var(--bg);
  border-left: 3px solid color-mix(in srgb, var(--accent) 40%, transparent);
  border-radius: 0 4px 4px 0;
  font-size: 0.95em;
  line-height: 1.4;
}
.text-submit-entry[data-mine="true"] {
  border-left-color: var(--accent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 30%, transparent);
}
.text-submit-entry[data-highlighted="true"] {
  background: color-mix(in srgb, var(--accent) 14%, var(--bg));
  border-left-color: var(--accent);
  border-left-width: 4px;
  font-weight: 500;
}
.text-submit-entry[data-hidden="true"] {
  opacity: 0.45;
  text-decoration: line-through;
}
.text-submit-entry-body { white-space: pre-wrap; }
.text-submit-status {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--muted);
  margin-top: 2px;
}
.text-submit-idle .text-submit-input { opacity: 0.6; }

/* Editor-only curation UI. Injected by decorateTextSubmit in
   public/editor.html. Dashed top border marks it as editor chrome. */
.text-submit-controls {
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px dashed var(--rule, currentColor);
  display: flex;
  flex-direction: column;
  gap: 6px;
  opacity: 0.7;
  transition: opacity 120ms;
}
.text-submit:hover .text-submit-controls { opacity: 1; }
.text-submit-controls-row {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
}
.text-submit-controls label { font-size: 11px; opacity: 0.8; margin-right: 2px; }
.text-submit-controls button,
.text-submit-controls select,
.text-submit-controls input[type="number"] {
  font: inherit;
  font-family: inherit;
  font-size: 12px;
  padding: 3px 6px;
  background: var(--bg);
  color: var(--fg);
  border: 1px solid var(--rule, currentColor);
  border-radius: 3px;
  cursor: pointer;
}
.text-submit-controls button:hover {
  background: color-mix(in srgb, currentColor 10%, transparent);
}
.text-submit-entry-controls {
  display: inline-flex;
  gap: 4px;
  margin-left: 8px;
  vertical-align: middle;
}
.text-submit-entry-controls button {
  font: inherit;
  font-size: 11px;
  padding: 1px 6px;
  background: transparent;
  color: var(--muted);
  border: 1px solid var(--rule, currentColor);
  border-radius: 3px;
  cursor: pointer;
}
.text-submit-entry-controls button:hover {
  color: var(--fg);
  background: color-mix(in srgb, currentColor 8%, transparent);
}
.text-submit-entry-controls button.active {
  background: color-mix(in srgb, var(--accent) 20%, transparent);
  border-color: var(--accent);
  color: var(--accent);
}

/* Editable state */
[contenteditable="plaintext-only"] {
  outline: 1px dashed transparent;
  border-radius: 3px;
  transition: outline-color 120ms;
}
[contenteditable="plaintext-only"]:hover { outline-color: var(--rule); }
[contenteditable="plaintext-only"]:focus {
  outline: 1px solid var(--accent);
  background: var(--focus-bg);
}

/* ----- Shared nav bar (editor/presenter/audience) -----
   Rendered by public/client/navbar.js. Uses CHROME tokens so it
   responds to the user's chrome theme, not the slide theme. */

/* Phase 7 G.3 — three-zone layout.
   The navbar distributes across full width: left (identity), center
   (view switcher), right (secondary actions + status). Before G.3 every
   piece was bunched left with a trailing .spacer — nav read as an
   afterthought strip. Now it reads as chrome with intent. */
.navbar {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 16px;
  padding: 0 20px;
  min-height: var(--nav-height);
  /* The bar floats: sticky at the top, translucent panel color with a
     blur behind it. Readers see the slide cards scroll under a chrome
     layer rather than a sharp-edged strip. */
  position: sticky; top: 0;
  background: color-mix(in srgb, var(--chrome-panel, var(--chrome-card)) 88%, transparent);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  color: var(--chrome-fg);
  border-bottom: 1px solid var(--chrome-border);
  font-family: var(--sans); font-size: 13px;
  box-shadow: var(--chrome-shadow);
  z-index: 50;
}

/* ── Zone layout ── */
.navbar-left   { display: flex; align-items: center; gap: 12px; justify-self: start; min-width: 0; }
.navbar-center { display: flex; align-items: center; justify-self: center; }
.navbar-right  { display: flex; align-items: center; gap: 8px; justify-self: end; }

/* ── Left zone — brand/title ── */
.navbar .brand {
  display: flex; align-items: center; gap: 10px;
  font-family: var(--serif); font-weight: 700;
  color: var(--chrome-fg);
  min-width: 0;
}
.navbar .brand-title {
  font-size: 18px;
  letter-spacing: -0.015em;
  line-height: 1.2;
  /* Truncate gracefully on narrow widths so the center tabs never get
     squeezed out of the center column. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 36ch;
}

/* ── Center zone — view tabs ──
   Use the F.1 .tabs primitive for the pill-group. No navbar-specific
   overrides needed: the primitive already tracks chrome tokens and
   the active/hover states read correctly. */

/* ── Right zone — legacy support for extras that pre-date the .btn
   primitive. They still render inline, just with chrome-token colors
   so nothing clashes with the new zone backgrounds. */
.navbar-right > *:not(.btn):not(.chip) {
  font-family: var(--sans); font-size: 12px;
  color: var(--chrome-muted);
}

/* Floating navbar variant — audience/presenter. Hidden until hover,
   mirrors the pattern the audience cursor already follows. */
.navbar.floating {
  position: fixed; top: 0; left: 0; right: 0;
  opacity: 0;
  transition: opacity 200ms;
  background: rgba(20,22,28,0.92);
  border-bottom-color: rgba(255,255,255,0.08);
  color: #e4e4e4;
}
.navbar.floating .brand,
.navbar.floating .brand-title { color: #e4e4e4; }
.navbar.floating a,
.navbar.floating button { color: #e4e4e4; }
.navbar.floating .tabs {
  background: rgba(255,255,255,0.05);
  border-color: rgba(255,255,255,0.12);
}
.navbar.floating .tabs a { color: #a0a4ac; }
.navbar.floating .tabs a.active {
  background: rgba(255,255,255,0.12);
  color: #ffffff;
  border-color: rgba(255,255,255,0.2);
}
.navbar.floating a:hover,
.navbar.floating button:hover {
  background: rgba(255,255,255,0.08);
  border-color: rgba(255,255,255,0.15);
}
body:hover .navbar.floating,
.navbar.floating:hover,
.navbar.floating:focus-within { opacity: 1; }

/* ----- Share modal ----- */
.share-modal-backdrop {
  position: fixed; inset: 0;
  background: rgba(0,0,0,0.5);
  display: flex; align-items: center; justify-content: center;
  z-index: 100;
}
.share-modal {
  background: var(--chrome-card);
  color: var(--chrome-fg);
  border: 1px solid var(--chrome-border);
  border-radius: 10px;
  padding: 24px;
  min-width: 360px; max-width: 520px;
  box-shadow: 0 10px 40px rgba(0,0,0,0.3);
  font-family: var(--sans);
}
.share-modal h3 {
  margin: 0 0 6px 0;
  font-family: var(--sans);
  font-size: 1.2em;
  font-weight: 600;
}
.share-modal .hint {
  color: var(--chrome-muted);
  font-size: 0.85em;
  margin-bottom: 18px;
}
.share-modal .url-row {
  display: flex; gap: 8px; margin-bottom: 10px;
}
.share-modal .url-row input {
  flex: 1;
  font-family: var(--mono); font-size: 12px;
  padding: 8px 10px;
  background: var(--chrome-input-bg);
  color: var(--chrome-fg);
  border: 1px solid var(--chrome-border);
  border-radius: 5px;
}
.share-modal .url-row button {
  font: inherit;
  font-family: var(--mono); font-size: 12px;
  padding: 8px 14px;
  background: var(--chrome-accent);
  color: #fff;
  border: none;
  border-radius: 5px;
  cursor: pointer;
}
.share-modal .url-row button:hover { opacity: 0.9; }
.share-modal .url-label {
  font-family: var(--mono); font-size: 10px;
  color: var(--chrome-muted);
  text-transform: uppercase; letter-spacing: 0.08em;
  margin-bottom: 4px;
  margin-top: 14px;
}
.share-modal .url-label:first-child { margin-top: 0; }
.share-modal .qr-wrap {
  margin: 10px 0 4px 0;
  display: flex; flex-direction: column; align-items: center;
  gap: 4px;
}
.share-modal .qr-wrap img {
  width: 180px; height: 180px;
  background: #fff;
  border: 1px solid var(--chrome-border);
  border-radius: 6px;
  padding: 6px;
}
.share-modal .qr-hint {
  font-family: var(--mono); font-size: 10px;
  color: var(--chrome-muted);
  text-transform: uppercase; letter-spacing: 0.08em;
}
.share-modal .close-row {
  display: flex; justify-content: flex-end;
  margin-top: 20px;
}
.share-modal .close-row button {
  font: inherit;
  padding: 6px 14px;
  background: transparent;
  color: var(--chrome-fg);
  border: 1px solid var(--chrome-border);
  border-radius: 5px;
  cursor: pointer;
}
.share-modal .close-row button:hover { background: var(--chrome-bg); }

/* ═══════════════════ BLOCK VARIANTS ═══════════════════
   Each block type has a per-type variant vocabulary published via
   /api/registry (see src/registry.ts). The renderer writes the chosen
   value to `data-variant` on the block. These selectors style each
   named variant. Changing how a variant looks is a CSS edit — no
   content migration. Adding a new variant: add it to registry.ts AND
   add a rule here.

   Variants are additive: a block without a variant gets the block-
   type default from the sections above; the variant rules below layer
   on top via `.block[data-variant="name"]` selectors. */

/* ─── heading variants ─── */
.heading[data-variant="eyebrow"] {
  font-family: var(--sans);
  font-size: 0.9em;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent);
}
.heading[data-variant="display"] {
  font-size: 5em;
  line-height: 1.05;
  letter-spacing: -0.02em;
}

/* ─── paragraph variants ─── */
.paragraph[data-variant="lead"] {
  font-size: 1.6em;
  font-weight: 500;
  line-height: 1.45;
  max-width: 40ch;
}
.paragraph[data-variant="caption"] {
  font-size: 0.95em;
  color: var(--muted);
  max-width: 50ch;
  line-height: 1.4;
}

/* ─── bullets variants ─── */
.bullets[data-variant="compact"] {
  font-size: 1em;
  line-height: 1.35;
}
.bullets[data-variant="compact"] .bullet { margin-bottom: 0.15em; }
.bullets[data-variant="checks"] {
  list-style: none;
  padding-left: 0;
}
.bullets[data-variant="checks"] .bullet {
  position: relative;
  padding-left: 1.6em;
}
.bullets[data-variant="checks"] .bullet::before {
  content: "✓";
  position: absolute;
  left: 0;
  color: var(--accent);
  font-weight: 700;
}
.bullets[data-variant="none"] {
  list-style: none;
  padding-left: 0;
}

/* ─── quote variants ─── */
.quote[data-variant="pull"] {
  font-size: 2.6em;
  text-align: center;
  border-left: none;
  padding: 0;
  max-width: 28ch;
  margin: 0 auto;
}

/* ─── code variants ─── */
.code[data-variant="terminal"] {
  padding-left: 2.2rem;
  position: relative;
}
.code[data-variant="terminal"]::before {
  content: "❯";
  position: absolute;
  left: 0.9rem;
  top: 1rem;
  color: var(--accent);
  font-family: var(--mono);
}

/* ─── image variants ─── */
.image[data-variant="bordered"] img {
  border: 1px solid var(--border);
  border-radius: 4px;
}
.image[data-variant="polaroid"] {
  background: #fff;
  padding: 12px 12px 36px;
  box-shadow: 0 6px 22px rgba(0,0,0,0.18);
  transform: rotate(-1.2deg);
}
.image[data-variant="polaroid"] img { display: block; max-width: 100%; }
.image[data-variant="polaroid"] .image-caption {
  color: #333;
  font-size: 0.9em;
  text-align: center;
  margin-top: 8px;
}
.image[data-variant="full-bleed"] {
  width: 100%;
  height: 100%;
  margin: 0;
}
.image[data-variant="full-bleed"] img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* ─── divider variants ─── */
.block[data-block-type="divider"][data-variant="thick"] {
  border-top: 6px solid var(--accent);
}
.block[data-block-type="divider"][data-variant="dotted"] {
  border-top: 2px dotted var(--rule);
}

/* ─── table variants ─── */
.table-wrap[data-variant="compact"] .table th,
.table-wrap[data-variant="compact"] .table td {
  padding: 4px 8px;
  font-size: 0.9em;
}
.table-wrap[data-variant="striped"] .table tbody tr:nth-child(odd) {
  background: var(--table-stripe);
}

/* ─── citations variants ─── */
.citations[data-variant="footnote"] {
  font-size: 0.75em;
  color: var(--muted);
  border-top: 1px solid var(--rule);
  padding-top: 0.6em;
}

/* ─── poll variants ─── */
.poll[data-variant="compact"] {
  font-size: 0.85em;
}
.poll[data-variant="compact"] .poll-option-btn {
  padding: 6px 10px;
}

/* ─── text-submit variants ─── */
.text-submit[data-variant="wall"] .ts-submissions-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 12px;
}

/* ═══════════════════ THEME VARIANTS ═══════════════════
   Named "looks" for a single slide. Turned on by
   `slide.overrides.themeVariant = "name"` which adds a
   `.theme-variant--<name>` class to the slide root. Each variant
   recolors the CONTENT tokens for that slide only — bg/fg/accent
   etc. The per-slide overrides.bg/fg/accent still win individually,
   so a variant sets a coordinated default that the author can tweak. */

/* — case-presentation: clinical case styling —
   Serif body, left-accent rule on the title, soft paper background. */
.slide.theme-variant--case-presentation {
  --bg: #fbfaf5;
  --fg: #1a1816;
  --muted: #6e665e;
  --accent: #8a4a2c;
  --rule: #cfc8bb;
}
.slide.theme-variant--case-presentation .heading.h-1 {
  border-left: 6px solid var(--accent);
  padding-left: 0.6rem;
  font-family: var(--serif);
}

/* — board-question: ABIM-style vignette layout —
   Generous whitespace, subdued palette, bold stem, lettered options. */
.slide.theme-variant--board-question {
  --bg: #ffffff;
  --fg: #111111;
  --accent: #1a4b7a;
  justify-content: flex-start;
  padding-top: 96px;
}
.slide.theme-variant--board-question .heading.h-1 {
  font-size: 2.4em;
  font-weight: 600;
  line-height: 1.25;
  max-width: 1440px;
}
.slide.theme-variant--board-question .paragraph {
  font-size: 1.25em;
  max-width: 50ch;
  line-height: 1.55;
}
.slide.theme-variant--board-question .bullets {
  font-size: 1.25em;
  list-style-type: upper-alpha;
  padding-left: 1.8em;
}

/* — section-divider: big display title on solid color —
   Flips to reverse: accent background, off-white foreground. */
.slide.theme-variant--section-divider {
  --bg: #1e3a5f;
  --fg: #f5f5f0;
  --muted: #c5cedb;
  --accent: #f5b342;
  justify-content: center;
  align-items: center;
  text-align: center;
}
.slide.theme-variant--section-divider .heading.h-1 {
  font-size: 5.5em;
  letter-spacing: -0.02em;
}
.slide.theme-variant--section-divider .heading.h-2 {
  color: var(--muted);
  font-weight: 400;
}

/* — pearl-page: teaching pearls aggregated on one slide —
   Soft amber background echoing the callout-pearl color. */
.slide.theme-variant--pearl-page {
  --bg: #fff7e0;
  --fg: #2a2210;
  --muted: #7a6a40;
  --accent: #a87a10;
  --rule: #e8d490;
}

/* — lab-data: high-contrast for tabular slides —
   White background, strong type, accent-blue headers. */
.slide.theme-variant--lab-data {
  --bg: #ffffff;
  --fg: #101418;
  --muted: #596773;
  --accent: #0b5394;
  --rule: #d6dbe0;
  --table-stripe: #f1f4f7;
}
.slide.theme-variant--lab-data .table th {
  background: var(--accent);
  color: #ffffff;
}

/* ── REACTION ──────────────────────────────────────────────────────
   Horizontal emoji bar, cheap cousin of the poll. Same card chrome,
   same idle/open/closed state classes (.reaction-idle etc.) so it
   picks up the dimmed-when-idle pattern with one selector.
   Each cell is button-shaped: emoji glyph on top, running tally
   underneath. When results are visible (always, for reactions) the
   tally stays live. Picked state is signaled by a thicker accent
   border on the cell, borrowing the poll pattern. */
.reaction {
  border: 1px solid var(--rule, currentColor);
  border-radius: 8px;
  padding: 0.9em 1em 0.8em;
  background: color-mix(in srgb, var(--accent, currentColor) 4%, transparent);
}
.reaction-prompt {
  font-weight: 600;
  font-size: 1.05em;
  margin-bottom: 0.7em;
  line-height: 1.35;
}
.reaction-bar {
  list-style: none;
  margin: 0 0 0.5em 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5em;
}
.reaction-cell { margin: 0; padding: 0; }
.reaction-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.15em;
  min-width: 3.6em;
  padding: 0.55em 0.9em;
  background: var(--bg);
  color: inherit;
  border: 1px solid var(--rule, currentColor);
  border-radius: 8px;
  font: inherit;
  cursor: pointer;
  transition: border-color 120ms, transform 80ms;
}
.reaction-btn:hover:not(:disabled) {
  border-color: var(--accent, currentColor);
  transform: translateY(-1px);
}
.reaction-btn:disabled {
  cursor: default;
  opacity: 0.75;
}
.reaction-glyph {
  font-size: 1.6em;
  line-height: 1;
}
.reaction-tally {
  font-size: 0.75em;
  opacity: 0.75;
  font-variant-numeric: tabular-nums;
}
.reaction-cell[data-picked="true"] .reaction-btn {
  border-color: var(--accent, currentColor);
  border-width: 2px;
  padding: calc(0.55em - 1px) calc(0.9em - 1px);
}
.reaction-status {
  font-size: 0.85em;
  opacity: 0.7;
  margin-top: 0.3em;
}
.reaction-idle .reaction-btn   { opacity: 0.7; }
.reaction-closed .reaction-btn { cursor: default; }

/* Compact variant: inline row, no tally until closed. */
.reaction[data-variant="compact"] .reaction-bar { gap: 0.3em; }
.reaction[data-variant="compact"] .reaction-btn { min-width: 2.8em; padding: 0.3em 0.5em; }
.reaction[data-variant="compact"] .reaction-glyph { font-size: 1.3em; }
.reaction[data-variant="compact"].reaction-open .reaction-tally { display: none; }

/* =================================================================
   Staged reveals (Phase 4)
   -----------------------------------------------------------------
   Blocks carry an optional `reveal` stage. The renderer sets
   data-revealed="true|false" on each block when a revealStage is in
   scope (audience + presenter). When it's absent (editor, pinned
   screenshot), nothing here applies and every block is visible.

   Design contract:
     audience  -> body[data-view="audience"]   -> hide unrevealed
     presenter -> body[data-view="presenter"]  -> ghost unrevealed so
                                                  the presenter can
                                                  see what's coming
     editor    -> body[data-view="editor"]     -> always show; no
                                                  revealStage passed

   Audience is the default (no body[data-view] selector needed) so
   that the static HTML export (which also has no data-view) behaves
   like the audience.
   ================================================================= */

/* Default: a block with revealed="false" is hidden. Audience + export. */
.block[data-revealed="false"] { display: none; }

/* Presenter: show the block ghosted so the presenter can preview the
   upcoming reveal. Low opacity + a dashed hint keeps the preview from
   being mistaken for live audience content. */
body[data-view="presenter"] .block[data-revealed="false"] {
  display: revert;
  opacity: 0.28;
  outline: 1px dashed var(--accent, currentColor);
  outline-offset: 2px;
  filter: grayscale(0.4);
  transition: opacity 0.18s ease-out;
}
/* When a pending block is the *next* reveal (reveal === stage + 1),
   bump opacity so "what's coming next" stands out vs "way-in-the-
   future" reveals. Purely a presenter nicety. */
body[data-view="presenter"] .slide[data-stage="0"] .block[data-reveal="1"][data-revealed="false"],
body[data-view="presenter"] .slide[data-stage="1"] .block[data-reveal="2"][data-revealed="false"],
body[data-view="presenter"] .slide[data-stage="2"] .block[data-reveal="3"][data-revealed="false"],
body[data-view="presenter"] .slide[data-stage="3"] .block[data-reveal="4"][data-revealed="false"],
body[data-view="presenter"] .slide[data-stage="4"] .block[data-reveal="5"][data-revealed="false"] {
  opacity: 0.55;
  filter: none;
}

/* Editor / pinned view / static export: belt-and-suspenders — make
   sure the reveal attributes never cause hidden content when the
   caller has explicitly set data-view="editor" (even if a stray
   data-revealed="false" leaks in from a bug). */
body[data-view="editor"] .block[data-revealed="false"] {
  display: revert;
  opacity: 1;
  outline: none;
  filter: none;
}

/* =================================================================
   SVG block (Phase 4)
   -----------------------------------------------------------------
   Inline figures for schematics, ECG strips, PV loops, etc. The
   author owns the markup; we just give the container a predictable
   sizing surface and wire inner reveal hooks.

   Sizing model:
     - .svg-figure           — the block container; carries an optional
                               `aspect-ratio` style from block.aspectRatio
     - .svg-content          — the SVG host; spans the figure's width
     - .svg-content > svg    — fills the host, preserveAspectRatio
                               handled by the SVG's own viewBox

   Inner reveals piggyback on the same data-revealed attribute used
   by top-level blocks. We use a descendant selector so this rule
   ONLY targets nodes inside an SVG block — no chance of double-
   matching with .block[data-revealed=...].
   ================================================================= */
.svg-figure {
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  width: 100%;
}
.svg-content {
  width: 100%;
  /* Without an aspectRatio override, the SVG content collapses to
     intrinsic size. With one set on the figure, the host stretches
     to fill so the SVG can size to 100% of its container. */
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 1;
}
.svg-content svg {
  width: 100%;
  height: 100%;
  max-width: 100%;
  display: block;
}
.svg-caption {
  margin-top: .5em;
  color: var(--muted, #888);
  font-size: .85em;
  text-align: center;
  font-style: italic;
}

/* Inner reveals on SVG descendants. Audience hides them, presenter
   ghosts them, editor always shows them. Mirrors the top-level
   `.block[data-revealed=...]` rules above so an SVG annotation
   building up a figure feels identical to a paragraph building up
   a list. */
.svg-content [data-revealed="false"] { display: none; }
body[data-view="presenter"] .svg-content [data-revealed="false"] {
  display: revert;
  opacity: 0.28;
  filter: grayscale(0.4);
  transition: opacity 0.18s ease-out;
}
body[data-view="presenter"]
  .slide[data-stage="0"] .svg-content [data-reveal="1"][data-revealed="false"],
body[data-view="presenter"]
  .slide[data-stage="1"] .svg-content [data-reveal="2"][data-revealed="false"],
body[data-view="presenter"]
  .slide[data-stage="2"] .svg-content [data-reveal="3"][data-revealed="false"],
body[data-view="presenter"]
  .slide[data-stage="3"] .svg-content [data-reveal="4"][data-revealed="false"],
body[data-view="presenter"]
  .slide[data-stage="4"] .svg-content [data-reveal="5"][data-revealed="false"] {
  opacity: 0.55;
  filter: none;
}
body[data-view="editor"] .svg-content [data-revealed="false"] {
  display: revert;
  opacity: 1;
  filter: none;
}

/* SVG variants — bordered chrome, full-bleed for hero figures. */
.svg-figure[data-variant="bordered"] {
  border: 1px solid var(--border, rgba(0,0,0,.15));
  border-radius: 6px;
  padding: 16px;
  background: color-mix(in srgb, var(--bg) 96%, var(--fg) 4%);
}
.svg-figure[data-variant="full-bleed"] {
  margin: 0;
  padding: 0;
}
.svg-figure[data-variant="full-bleed"] .svg-caption { display: none; }

/* ── Phase 7 Phase C — Rankings block ────────────────────────────────
   The rankings block renders the live scoreboard. It's surface-neutral:
   on /join it appears inside the viewer-box using chrome-adjacent
   colors; on /audience and /presenter it uses deck tokens (--fg, --bg,
   --accent) so it reads like any other slide block.

   Design decisions:
   - Podium numerals (1/2/3) in colored blocks, not emoji medals. Reads
     "academic seminar" rather than "game show" — gold/silver/bronze
     gradients still do the emotional work.
   - Layout order on podium: 2 · 1 · 3 via CSS order, so gold sits
     center-tallest.
   - Me-row gets an accent tint + "You" pill above it. The pill is
     absolutely positioned so it doesn't add height to the row.
   - Tokens fall back to conservative neutrals when --slide-fg / --slide-
     line aren't set (e.g., when rendering on /presenter where tokens
     come from styles.css :root instead of join.html).
 */
.rankings {
  display: block;
  width: 100%;
  /* Scope local tokens so the podium/trending blocks read the same
     metal-gradient vars regardless of surface. Join.html sets them on
     :root already; this fallback is only used by /presenter + /editor. */
  --gold-1:   #fbbf24;
  --gold-2:   #b45309;
  --silver-1: #cbd5e1;
  --silver-2: #64748b;
  --bronze-1: #d97706;
  --bronze-2: #7c2d12;
  --rank-surface: var(--bg, #fff);
  --rank-line:    var(--border, rgba(0, 0, 0, 0.08));
  --rank-ink:     var(--fg, #15161a);
  --rank-ink-2:   var(--muted, #52545c);
  --rank-ink-3:   var(--muted, #8a8c94);
  --rank-green:   #059669;
  --rank-green-bg: rgba(5, 150, 105, 0.12);
  --rank-red:     #dc2626;
}
.rankings .rank-head {
  display: flex; align-items: baseline; gap: 10px;
  margin-bottom: 14px;
}
.rankings .rank-title {
  font-size: 20px; font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--rank-ink);
}
.rankings .rank-sub {
  font-size: 12.5px;
  color: var(--rank-ink-3);
  margin-left: auto;
  font-variant-numeric: tabular-nums;
}
.rankings .rank-empty {
  text-align: center;
  color: var(--rank-ink-3);
  font-size: 14px;
  padding: 24px 12px;
  border: 1px dashed var(--rank-line);
  border-radius: 12px;
}

/* Trending banner — biggest positive mover. Positive-only so a bad
   round doesn't publicly shame anyone. */
.rankings .trending {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 14px;
  border-radius: 12px;
  background: var(--rank-green-bg);
  border: 1px solid var(--rank-green);
  margin-bottom: 14px;
}
.rankings .trend-icon {
  width: 30px; height: 30px; flex: 0 0 30px;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--rank-green); color: #fff;
  font-weight: 800; font-size: 15px;
}
.rankings .trend-body {
  font-size: 13.5px; color: var(--rank-ink); line-height: 1.3;
}
.rankings .trend-body strong { font-weight: 700; }
.rankings .trend-body .count {
  color: var(--rank-green);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* Podium — top-3 as gold/silver/bronze columns. Layout: 2 · 1 · 3. */
.rankings .rank-podium {
  margin-bottom: 16px;
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 10px;
  align-items: end;
  min-height: 160px;
}
.rankings .podium-col {
  display: flex; flex-direction: column; align-items: center;
  gap: 8px;
  padding-bottom: 0;
}
.rankings .podium-col .pod-avatar { width: 48px; height: 48px; color: var(--accent, #2563eb); }
.rankings .podium-col .pod-avatar svg { width: 100%; height: 100%; display: block; }
.rankings .podium-col .pod-name {
  font-size: 13px; font-weight: 600;
  color: var(--rank-ink);
  text-align: center; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rankings .podium-col .pod-score {
  font-size: 12.5px; color: var(--rank-ink-2);
  font-variant-numeric: tabular-nums;
}
.rankings .podium-col .pod-block {
  width: 100%;
  border-radius: 10px 10px 0 0;
  display: flex; align-items: flex-start; justify-content: center;
  padding-top: 8px;
  font-weight: 800; font-size: 26px;
  letter-spacing: -0.02em;
  color: #fff;
  text-shadow: 0 1px 0 rgba(0,0,0,.18);
}
.rankings .podium-col[data-rank="1"] .pod-block {
  background: linear-gradient(180deg, var(--gold-1), var(--gold-2));
  height: 92px;
  box-shadow: 0 8px 20px -6px rgba(251,191,36,.55), inset 0 1px 0 rgba(255,255,255,.35);
}
.rankings .podium-col[data-rank="2"] .pod-block {
  background: linear-gradient(180deg, var(--silver-1), var(--silver-2));
  height: 68px;
  box-shadow: 0 8px 20px -6px rgba(148,163,184,.55), inset 0 1px 0 rgba(255,255,255,.35);
}
.rankings .podium-col[data-rank="3"] .pod-block {
  background: linear-gradient(180deg, var(--bronze-1), var(--bronze-2));
  height: 48px;
  box-shadow: 0 8px 20px -6px rgba(217,119,6,.55), inset 0 1px 0 rgba(255,255,255,.25);
}
.rankings .podium-col[data-rank="1"] { order: 2; }
.rankings .podium-col[data-rank="2"] { order: 1; }
.rankings .podium-col[data-rank="3"] { order: 3; }

/* Full ordered list below the podium. */
.rankings .rank-list { display: flex; flex-direction: column; gap: 7px; }
.rankings .rank-row {
  position: relative;
  display: flex; align-items: center; gap: 10px;
  padding: 9px 12px 9px 10px;
  background: var(--rank-surface);
  border: 1px solid var(--rank-line);
  border-radius: 12px;
  min-height: 50px;
}
.rankings .rank-row .rank-badge {
  flex: 0 0 28px;
  width: 28px; height: 28px;
  border-radius: 8px;
  display: flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: 13px;
  background: color-mix(in srgb, var(--rank-ink) 7%, transparent);
  color: var(--rank-ink);
  font-variant-numeric: tabular-nums;
}
.rankings .rank-row[data-rank="1"] .rank-badge {
  background: linear-gradient(180deg, var(--gold-1), var(--gold-2));
  color: #fff;
  box-shadow: 0 0 0 2px rgba(251,191,36,.28);
  text-shadow: 0 1px 0 rgba(0,0,0,.15);
}
.rankings .rank-row[data-rank="2"] .rank-badge {
  background: linear-gradient(180deg, var(--silver-1), var(--silver-2));
  color: #fff;
  box-shadow: 0 0 0 2px rgba(148,163,184,.35);
  text-shadow: 0 1px 0 rgba(0,0,0,.15);
}
.rankings .rank-row[data-rank="3"] .rank-badge {
  background: linear-gradient(180deg, var(--bronze-1), var(--bronze-2));
  color: #fff;
  box-shadow: 0 0 0 2px rgba(217,119,6,.30);
  text-shadow: 0 1px 0 rgba(0,0,0,.2);
}
.rankings .rank-row .rank-avatar {
  width: 30px; height: 30px; flex: 0 0 30px;
  color: var(--accent, #2563eb);
}
.rankings .rank-row .rank-avatar svg {
  width: 100%; height: 100%; display: block;
}
.rankings .rank-row .rank-name {
  flex: 1 1 auto; min-width: 0;
  font-size: 14.5px; font-weight: 500;
  color: var(--rank-ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rankings .rank-row .rank-score {
  font-size: 14px; font-weight: 600;
  color: var(--rank-ink);
  font-variant-numeric: tabular-nums;
  display: flex; align-items: baseline; gap: 6px;
}
.rankings .rank-row .rank-score .delta {
  font-size: 11.5px; font-weight: 600;
  color: var(--rank-green);
}
.rankings .rank-row .rank-move {
  flex: 0 0 34px; text-align: right;
  font-size: 11.5px; font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--rank-ink-3);
}
.rankings .rank-row .rank-move.up   { color: var(--rank-green); }
.rankings .rank-row .rank-move.down { color: var(--rank-red); }
.rankings .rank-row[data-me="1"] {
  background: color-mix(in srgb, var(--accent, #2563eb) 10%, transparent);
  border-color: var(--accent, #2563eb);
  box-shadow: 0 0 0 1px var(--accent, #2563eb) inset,
              0 2px 10px -4px color-mix(in srgb, var(--accent, #2563eb) 40%, transparent);
}
.rankings .rank-row[data-me="1"] .rank-name {
  font-weight: 700;
  color: var(--accent, #2563eb);
}
.rankings .rank-row[data-me="1"]::before {
  content: "You";
  position: absolute; top: -8px; right: 10px;
  font-size: 10px; font-weight: 700; letter-spacing: 0.08em;
  background: var(--accent, #2563eb); color: #fff;
  padding: 2px 7px; border-radius: 999px;
}
.rankings .rank-cut {
  text-align: center;
  padding: 6px 0;
  color: var(--rank-ink-3);
  font-size: 18px; letter-spacing: 6px;
}

/* ══════════════════════════════════════════════════════════════════════
   Phase 7 — SHARED CHROME PRIMITIVES  (.btn / .chip / .kbd / .tabs)

   These are opt-in helpers. They do NOT replace any existing selector,
   so ship-state of every view is unchanged until a view explicitly
   adopts them. New chrome (editor Claude-outbox buttons, presenter
   transport controls, participant composer) can reach for .btn / .chip
   instead of inventing its own styled button, so hover states and focus
   rings stay coherent across surfaces.

   Contract:
     .btn                — default "outlined" button (no primary emphasis)
     .btn.primary        — deck-accent filled, use once per action cluster
     .btn.ghost          — no chrome until hover; filter/toggle affordances
     .btn.danger         — used with .btn or .btn.ghost; reveals red on hover
     .btn.icon           — square-ish, for icon-only
     .btn.sm             — compact size (toolbars)
     .chip               — tiny status pill; .chip.ok / .chip.warm variants
     .kbd                — keyboard-cap glyph for shortcut hints
     .tabs               — pill group of mutually-exclusive buttons

   All reach through chrome tokens so they track the user's dark/light
   preference automatically. No !important, no fighting with view-specific
   CSS — specificity is kept at a single class selector everywhere.
   ══════════════════════════════════════════════════════════════════════ */

.btn {
  --btn-bg: var(--chrome-panel-2, var(--chrome-card-strong, var(--chrome-card)));
  --btn-fg: var(--chrome-fg);
  --btn-br: var(--chrome-border-strong, var(--chrome-border));
  display: inline-flex; align-items: center; gap: 8px;
  font: inherit;
  font-family: var(--sans);
  font-size: 13px; font-weight: 500;
  padding: 7px 12px;
  background: var(--btn-bg); color: var(--btn-fg);
  border: 1px solid var(--btn-br);
  border-radius: var(--radius-md, 8px);
  cursor: pointer;
  text-decoration: none;
  transition: background var(--dur-fast, 120ms) var(--ease, ease),
              border-color var(--dur-fast, 120ms) var(--ease, ease),
              color var(--dur-fast, 120ms) var(--ease, ease),
              transform var(--dur-fast, 120ms) var(--ease, ease);
}
.btn:hover {
  background: var(--chrome-bg-hover, var(--chrome-btn-bg-hover, var(--chrome-bg)));
  border-color: var(--chrome-border-strong, var(--chrome-border));
}
.btn:active { transform: translateY(1px); }
.btn:focus-visible {
  outline: none;
  box-shadow: var(--chrome-ring, 0 0 0 3px color-mix(in srgb, var(--chrome-accent, #7db3e0) 28%, transparent));
}
.btn:disabled,
.btn[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none;
}
.btn svg { width: 15px; height: 15px; flex-shrink: 0; }

.btn.primary {
  --btn-bg: var(--chrome-accent);
  --btn-fg: var(--chrome-accent-ink, #ffffff);
  --btn-br: transparent;
  font-weight: 600;
  box-shadow: 0 1px 0 rgba(255,255,255,.08) inset, var(--chrome-shadow, 0 1px 2px rgba(0,0,0,.2));
}
.btn.primary:hover {
  --btn-bg: var(--chrome-accent-hi, var(--chrome-accent));
  border-color: transparent;
}

.btn.ghost {
  --btn-bg: transparent;
  --btn-br: transparent;
  color: var(--chrome-muted);
}
.btn.ghost:hover {
  color: var(--chrome-fg);
  background: var(--chrome-bg-hover, var(--chrome-bg));
  border-color: transparent;
}

.btn.danger:hover {
  color: var(--chrome-danger);
  border-color: var(--chrome-danger);
}

.btn.icon { padding: 7px; }
.btn.sm { font-size: 12px; padding: 5px 10px; }
.btn.sm.icon { padding: 5px; }

/* ── chip ──────────────────────────────────────────────────────────── */
.chip {
  display: inline-flex; align-items: center; gap: 6px;
  font-family: var(--sans);
  font-size: 11px; font-weight: 500;
  padding: 3px 8px;
  background: var(--chrome-bg-hover, var(--chrome-bg));
  color: var(--chrome-muted);
  border: 1px solid var(--chrome-border);
  border-radius: var(--radius-pill, 999px);
  white-space: nowrap;
}
.chip .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--chrome-muted); }
.chip.ok {
  color: var(--chrome-ok, #2f7a43);
  border-color: color-mix(in srgb, var(--chrome-ok, #2f7a43) 30%, var(--chrome-border));
}
.chip.ok .dot {
  background: var(--chrome-ok, #2f7a43);
  box-shadow: 0 0 6px color-mix(in srgb, var(--chrome-ok, #2f7a43) 60%, transparent);
}
.chip.warm {
  color: var(--chrome-warm, var(--chrome-accent));
  border-color: color-mix(in srgb, var(--chrome-warm, var(--chrome-accent)) 30%, var(--chrome-border));
}
.chip.warm .dot { background: var(--chrome-warm, var(--chrome-accent)); }
.chip.danger {
  color: var(--chrome-danger);
  border-color: color-mix(in srgb, var(--chrome-danger) 30%, var(--chrome-border));
}
.chip.danger .dot { background: var(--chrome-danger); }

/* ── kbd — keyboard shortcut glyph ─────────────────────────────────── */
.kbd {
  display: inline-block;
  font-family: var(--mono);
  font-size: 10px; line-height: 1;
  padding: 2px 5px;
  background: var(--chrome-bg-sunken, var(--chrome-bg));
  color: var(--chrome-muted);
  border: 1px solid var(--chrome-border);
  border-radius: 4px;
}

/* ── tabs — pill-group switcher (role toggle, mode toggle, etc.) ──── */
.tabs {
  display: inline-flex; gap: 2px;
  padding: 4px;
  background: var(--chrome-bg-sunken, var(--chrome-bg));
  border: 1px solid var(--chrome-border);
  border-radius: var(--radius-pill, 999px);
}
.tabs button,
.tabs a {
  font: inherit; font-family: var(--sans);
  font-size: 13px; font-weight: 500;
  padding: 6px 14px;
  background: transparent; color: var(--chrome-muted);
  border: 1px solid transparent;
  border-radius: var(--radius-pill, 999px);
  cursor: pointer;
  text-decoration: none;
  transition: color var(--dur-fast, 120ms) var(--ease, ease),
              background var(--dur-fast, 120ms) var(--ease, ease);
}
.tabs button:hover,
.tabs a:hover { color: var(--chrome-fg); }
.tabs button.active,
.tabs button[aria-pressed="true"],
.tabs a.active,
.tabs a[aria-current="page"] {
  background: var(--chrome-panel-2, var(--chrome-card-strong, var(--chrome-card)));
  color: var(--chrome-fg);
  border-color: var(--chrome-border-strong, var(--chrome-border));
  box-shadow: var(--chrome-shadow);
}

/* .tabs.full — fill container width, children share space equally. Use for
   narrow contexts (sidebar rows, toolbars) where inline-flex would overflow. */
.tabs.full { display: flex; width: 100%; }
.tabs.full > button,
.tabs.full > a {
  flex: 1;
  justify-content: center;
  text-align: center;
  padding-left: 8px; padding-right: 8px;
}

/* .tabs.sm — compact variant for tight sidebar/toolbar contexts. Pairs
   cleanly with .tabs.full when the row has limited horizontal real estate. */
.tabs.sm { padding: 3px; }
.tabs.sm button,
.tabs.sm a {
  font-size: 11px;
  font-weight: 500;
  padding: 4px 10px;
  letter-spacing: 0.01em;
}

/* .tabs.full-count — adds a counter badge next to the label. Used by the
   editor feedback filter row (active 3 / completed 2 / archived / rejected). */
.tabs .tab-count {
  display: inline-block;
  margin-left: 6px;
  font-family: var(--mono);
  font-size: 10px; font-weight: 600;
  color: var(--chrome-muted);
  opacity: 0.8;
}
.tabs button.active .tab-count,
.tabs a.active .tab-count,
.tabs a[aria-current="page"] .tab-count { color: var(--chrome-fg); opacity: 1; }

/* HTML-source decks use a sandboxed deck-owned iframe, driven by the same
   presenter/audience nav state as block decks. */
.html-source-host {
  width: 100%;
  height: 100%;
  background: #000;
}

.html-source-frame {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
  background: #fff;
}
