/* ═══════════════════════════════════════════════════════════════════════════
   BASE — design-system defaults and STRUCTURAL layout for the skeleton
   (absolute-fade, single-active) deck model.

   Design tokens (the :root custom properties + [data-theme] overrides) live in
   ./tokens.css and are pulled in by the @import below. They were split out
   (B-001 AC-2.4) so a rich, self-contained scroll-snap deck can opt-in to the
   shared palette/typography WITHOUT inheriting the structural rules in THIS
   file (`body { overflow: hidden }`, `#deck { height: 100vh }`,
   `.slide { position: absolute }`) — those rules collapse a scroll-snap deck's
   scroll container and freeze its nav. Rich decks should link tokens.css, not
   base.css. See docs/RICH-DECKS.md.

   theme.css (generated from theme.json) overrides the tokens via the normal
   CSS cascade — load order in the skeleton template is tokens (via this
   @import) → base structural → theme.css.
   ═══════════════════════════════════════════════════════════════════════════ */

@import url('./tokens.css');

* { box-sizing: border-box; }

body {
  margin: 0;
  /* B-112: the window is the frame; the canvas centres itself inside it (see
     #deck). Grid centring was tried and does not work here — an item larger than
     its area gets safe-aligned to the start by Chromium, so the 1920x1080 canvas
     sat at x=426.7, y=240 in a 1280x600 window and ran off two edges. */
  width: 100vw;
  height: 100vh;
  font-family: var(--font-body);
  line-height: var(--leading-body);
  background: var(--bg);
  color: var(--fg);
  overflow: hidden;
  /* Antialiasing — matches the gold-standard deck so Pretendard renders
     crisply (especially Korean) instead of relying on the platform default. */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  transition: background var(--motion-base), color var(--motion-base);
}

/* B-112: the canvas. Fixed 1920x1080 in CSS pixels, scaled as a whole to fit the
   window (stage.js sets --stage-scale). Layout therefore always happens at the
   authored size, so a small window shows a small slide instead of a re-wrapped or
   cropped one. `transform` and not `zoom`: zoom re-runs layout, which is exactly
   the reflow this removes. */
#deck {
  width: var(--canvas-w);
  height: var(--canvas-h);
  position: relative;
  /* Centred on the window's centre point, THEN scaled about its own centre. The
     percentage translate resolves against the untransformed 1920x1080 box, so the
     pair holds at any scale and any window size — no dependence on how the parent
     aligns an oversized child, which is what broke the grid version. */
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%) scale(var(--stage-scale));
  transform-origin: center center;
}

.slide {
  position: absolute;
  inset: 0;
  padding: var(--slide-padding);
  display: flex;
  flex-direction: column;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--motion-base);
}

.slide[data-active="true"] {
  opacity: 1;
  pointer-events: auto;
}

/* ── Atmosphere backdrop (B-039) ─────────────────────────────────────────
   Optional per-theme depth layer behind slide content: a gradient/glow the
   theme supplies via its `atmosphere` tokens (--atmosphere-bg, generated from
   theme.json). Lives on .slide::before (not body) so it is INSIDE the element
   html2canvas captures for PDF export and so it tracks [data-theme]. Defaults
   to `transparent` → a preset with no atmosphere keeps its flat solid --bg. */
.slide::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: var(--atmosphere-bg, transparent);
}
/* Keep content above the backdrop layer. */
.slide-content { position: relative; z-index: 1; }

/* ── Entry reveal stagger (B-003 D6 / AC-2.1) ────────────────────────────
   Direct children of the active slide's content column fade up in sequence
   when the slide becomes active. Inactive slides hold their children in the
   pre-reveal state, so re-activating a slide re-plays the reveal. Tokens
   (--reveal-*) live in tokens.css; values follow the spec range
   (translateY 8–16px, duration 300–500ms, stagger 40–80ms) and reuse the
   gold-standard --ease-out-expo curve.

   Scoped to `.slide-content > *` (the scaffold wraps slide body in
   `.slide-content`) so the reveal targets real content, not the flex wrapper. */
.slide .slide-content > * {
  opacity: 0;
  transform: translateY(var(--reveal-distance));
  transition:
    opacity var(--reveal-duration) var(--ease-out-expo),
    transform var(--reveal-duration) var(--ease-out-expo);
}

.slide[data-active="true"] .slide-content > *:not([data-fragment]) {
  opacity: 1;
  transform: translateY(0);
}

/* ── Fragments (B-033) ────────────────────────────────────────────────────
   Opt-in step reveal: mark any element `data-fragment` and it stays hidden
   until navigation.js reveals it one step at a time (reveal.js semantics),
   toggling `.fragment-visible`. Excluded from the entry-reveal above via
   `:not([data-fragment])` so the auto-stagger never force-shows a fragment.
   Works anywhere in the slide (not just direct children) and animates with the
   same reveal tokens. */
.slide [data-fragment] {
  opacity: 0;
  transform: translateY(var(--reveal-distance));
  transition:
    opacity var(--reveal-duration) var(--ease-out-expo),
    transform var(--reveal-duration) var(--ease-out-expo);
}

.slide [data-fragment].fragment-visible {
  opacity: 1;
  transform: translateY(0);
}

/* Stagger: each successive child waits one --reveal-stagger step longer.
   Cap at 6 so a content-heavy slide doesn't drag the last item too late. */
.slide[data-active="true"] .slide-content > *:nth-child(1) { transition-delay: calc(var(--reveal-stagger) * 0); }
.slide[data-active="true"] .slide-content > *:nth-child(2) { transition-delay: calc(var(--reveal-stagger) * 1); }
.slide[data-active="true"] .slide-content > *:nth-child(3) { transition-delay: calc(var(--reveal-stagger) * 2); }
.slide[data-active="true"] .slide-content > *:nth-child(4) { transition-delay: calc(var(--reveal-stagger) * 3); }
.slide[data-active="true"] .slide-content > *:nth-child(5) { transition-delay: calc(var(--reveal-stagger) * 4); }
.slide[data-active="true"] .slide-content > *:nth-child(n+6) { transition-delay: calc(var(--reveal-stagger) * 5); }

/* ── nav dots (B-003 D3 / AC-1.3) ────────────────────────────────────────
   Created by navigation.js and appended to <body>. Fixed vertical rail on the
   right, mirroring the gold-standard `.nav-dots`. Clicking a dot jumps to that
   slide; the active dot scales up.

   B-095: the rail used to be a single unbounded column, and one dot is 32px of
   rail (24px target + 0.5rem gap). A 39-slide deck therefore asked for 1240px on
   an 810px stage — and because the rail is CENTRED, it spilled off the top and the
   bottom at once: 14 of 39 slides unreachable at 1440x810, 26 of 39 at 768x432.
   The deck advertised navigation it could not deliver, and it got worse the longer
   the deck. Capping the rail at the stage turns the overflow into a second column
   instead: the box is right-anchored with auto width, so extra columns grow
   leftward, away from the edge, and every dot stays on stage at any slide count.
   8rem buys 22–2vmin px of geometric clearance to the `.deck-controls` cluster
   (bottom 2vmin; total height: 1px border + 5px pad + ~30px button + 5px pad +
   1px border = ~42px; clearance = 64 − 2vmin − 42 = 22 − 2vmin). At 1920×1080
   that is ~0px; above it goes negative — the z-index is the actual guarantee at
   large sizes, not geometry. In `@media (pointer: coarse)` the cluster grows to
   ~56px (min-height 44px + 12px chrome), so the override below bumps the rail to
   11rem to restore ≥10px clearance across common touch viewports. */
.nav-dots {
  position: fixed;
  right: calc(1.4rem + var(--stage-bar-x));  /* B-111: stage edge, not window edge */
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  flex-wrap: wrap;
  max-height: calc(100dvh - 8rem);
  gap: 0.5rem;
  /* B-104: this rule used to carry `pointer-events: none`, on the reading that an
     unbounded container would turn its own gaps into a dead zone for
     click-to-advance. Both halves of that reading were wrong.
     The dead-zone worry does not apply: navigation.js appends the rail to <body>
     (navigation.js:132) while click-to-advance listens on `#deck`
     (navigation.js:273), so the rail is the deck's SIBLING — a click that lands on
     it never reaches the advance handler at all, guard or no guard. And a control
     surface absorbing a near-miss is the behaviour we want, not a dead zone.
     What `none` actually bought was the opposite failure. It made the container
     transparent to hit testing, so a click in a rail gap fell through to the slide
     beneath and ADVANCED the deck — while the user was aiming at a jump target, the
     one gesture on this control that must never advance. Measured on the 39-dot
     fixture, clicking the mid-column gap: 1440x810 / 1280x720 / 1280x600 / 768x432
     all went slide 0 -> 1, and a 1px census found 16984 / 29736 / 21408 / 22528 px
     of the rail's own box hit-testing into `#deck`. With the declaration gone the
     same clicks are inert (0 -> 0) and the census leaks nothing.
     The cost is real and deliberate: the rail's whole box — 56px wide at 1440x810,
     152px at 768x432 — no longer advances the deck. That is a control surface
     behaving like one. Its OVERLAP with slide content is a separate, unfixed
     geometry question (see the wrap note above); do not re-solve it here with
     pointer-events. */
  z-index: 100;
}

.nav-dots .nav-dot {
  /* B-031: 24x24 tap target (WCAG 2.2 AA) around the visual dot. The background
     paints only the content box, so the dot stays small while the padding
     extends the clickable area — the rail looks light, the target isn't a
     pinpoint. B-053: visual dot trimmed 10px → 8px (padding 7 → 8) — users read
     the rail as too heavy on glass/high-DPI surfaces; tap area stays 24px. */
  width: 24px;
  height: 24px;
  padding: 8px;
  box-sizing: border-box;
  pointer-events: auto;
  border: 0;
  border-radius: 50%;
  background: var(--muted);
  /* B-084: this MUST stay after the `background` shorthand. It used to sit
     above it, where the shorthand reset it back to border-box — so the 8px dot
     the comment above describes has never actually rendered: every dot painted
     the full 24px tap target, and the active one 33.6px after scale(1.4). The
     same trap is repeated in `.active` below, which re-declares the shorthand. */
  background-clip: content-box;
  opacity: 0.5;
  cursor: pointer;
  transition: transform var(--motion-fast), opacity var(--motion-fast), background var(--motion-fast);
}

.nav-dots .nav-dot:hover { opacity: 0.85; }

.nav-dots .nav-dot.active {
  background: var(--accent);
  /* Re-declaring the shorthand resets `background-clip` to border-box, so this
     has to be restated here too — the active dot is the most visible one on the
     rail and was painting at 33.6px (24 × 1.4) instead of 11.2px. */
  background-clip: content-box;
  opacity: 1;
  transform: scale(1.4);
}

/* The clickable counter (counter → goto, AC-1.3) gets a subtle affordance. */
.deck-counter { user-select: none; }
.deck-counter:hover { color: var(--accent-text); }

/* ── prefers-reduced-motion (B-003 / AC-2.2) ─────────────────────────────
   Users who ask for reduced motion get content shown immediately — no fade,
   no translate, no stagger delay. The dot scale transition is also neutralised
   so nothing animates. */
@media (prefers-reduced-motion: reduce) {
  .slide .slide-content > *:not([data-fragment]),
  .slide[data-active="true"] .slide-content > *:not([data-fragment]) {
    opacity: 1;
    transform: none;
    transition: none;
    transition-delay: 0s;
  }
  /* Fragments (B-033) stay steppable — hidden until revealed — but appear
     instantly (no fade/translate). The .fragment-visible state still governs
     visibility, so stepping works; only the animation is dropped. */
  .slide [data-fragment] { transition: none; transform: none; }
  /* Inter-slide transform variants collapse to an instant swap. */
  .slide { transition: none; transform: none; }
  .nav-dots .nav-dot { transition: none; }
}

/* ── Inter-slide transition variants (B-033) ──────────────────────────────
   Default (no attribute): the opacity fade on `.slide` above. Opt in on <body>
   (next to data-theme) with data-transition="none" (instant, no fade) or
   "fade-scale" (fade + a subtle zoom-in). Both are direction-agnostic — no
   per-slide JS state — so they compose with the absolute-stack model and the
   WYSIWYG export (which force-activates each slide). */
[data-transition="none"] .slide { transition: none; }
[data-transition="fade-scale"] .slide {
  transform: scale(1.015);
  transition: opacity var(--motion-base), transform var(--motion-base);
}
[data-transition="fade-scale"] .slide[data-active="true"] { transform: scale(1); }

/* Bound stage — caps content width and centers it so text (h2/lead) and
   diagrams share one centered column on large/full-screen displays instead of
   pinning text to the left edge. Mirrors the gold-standard deck pattern.
   Diagram blocks keep their own `max-width + margin:auto` (diagrams.css); they
   nest inside this column, so the two never fight on specificity. */
.slide-content {
  flex: 1;
  display: flex;
  flex-direction: column;
  /* B-110: `safe` matters now that this is the alignment nearly every content
     slide uses. Plain `center` splits overflow to BOTH sides, so a slide whose
     content is taller than the stage loses its title off the top — and this box
     is `overflow: hidden`, so it is gone, not scrolled to. `safe` centres while
     there is room and falls back to flex-start the moment there is not. */
  justify-content: safe center;
  max-height: 100%;
  overflow: hidden;
  width: 100%;
  /* B-111: was `min(1200px, 90vw)`. The px cap meant the column stopped growing
     at 1333px of window while the stage kept going, so the deck used a smaller
     and smaller share of it: 82% at 1440, 70% at 1680, 61% at 1920. Expressed
     against the stage instead, the share is the same at every size. Paragraphs
     are still held to `--measure` (68ch), so the wider column buys room for
     cards, grids and tables rather than 100-character lines.
     B-113: the pair moved to tokens.css (`--content-max` / `--content-share`) so
     a bundle can speak it and motif.css derives its footer inset from the same
     source — the HAND-COUPLED note that lived here is retired, not relocated. */
  max-width: min(var(--content-max), calc(var(--content-share) * 100%));
  margin-inline: auto;
  /* B-047: CJK line-breaking baseline. Korean/Japanese/Chinese text carries no
     spaces between words, so the default `word-break: normal` snaps a word in
     half at the box edge ("정품인증 데↵이터를", "있습니↵다") — an i18n defect the
     framework had no rule against (base/tokens/diagrams grepped clean). keep-all
     forbids breaks inside a CJK word; non-CJK text is unaffected (it behaves as
     normal), so English decks render byte-identical. overflow-wrap:break-word
     still lets a single over-long token (a bare URL) wrap instead of overflow.
     Both properties inherit, so this one rule covers every text descendant
     (h1–h4, p, li, figcaption, .g-text, .pull, …). Note: keep-all only guards a
     word's interior — a two-word compound with a space ("표준 데이터") can still
     split across lines in a narrow box; author it with &nbsp; to hold together. */
  word-break: keep-all;
  overflow-wrap: break-word;
}

h1, h2, h3 { font-family: var(--font-heading); color: var(--primary); margin: 0 0 0.5em; line-height: var(--leading-heading); }
h1 { font-size: var(--title-size); }
h2 { font-size: var(--h2-size); }
h3 { font-size: var(--h3-size); }
/* B-114 (P1-1): spacing gets ONE owner. `.slide-content` is a flex column, and flex
   siblings do not collapse margins, so the UA's 1em block-START margin on <p> ADDED to
   the previous block's bottom margin: title-to-body ran `0.5em·(h2 size) +
   1em·(body size)` (~52px where the design says ~24), and paragraph pairs ran 2em.
   These element rules keep prose spacing deliberate in NESTED contexts (cards, panels)
   at (0,0,1), where deck.css still wins with the same selector.
   B-116 (round 11): the first draft owned spacing with block-END margins only, and the
   refuters measured the hole — a table or an archetype container has no end margin, so
   the paragraph after one sat FLUSH (22px → 0 on both gallery decks: broadsheet s6
   table→source, signal s3 stat-row→source). The column's rhythm is owned below by the
   direct-child rule instead: every pair of column blocks gets `--element-gap`,
   whatever the elements are, and the last child sheds its margin so centring is not
   skewed by a trailing 1em (measured: up to 28px of off-centre). */
p { font-size: var(--body-size); margin-block: 0 1em; }
ul, ol { margin-block: 0 1em; }

/* B-116: the column rhythm, one rule, no orphan pairs. A fixed token rather than `em`
   so a heading does not open a heading-sized crater under itself. (0,1,0)-with-child
   specificity: a deck.css ELEMENT rule will not beat this for direct children — that
   is deliberate; column rhythm is column policy, and a deck that wants its own writes
   `.slide-content > x`. */
.slide-content > * { margin-block: 0 var(--element-gap); }
.slide-content > :last-child { margin-block-end: 0; }
/* B-031: cap body-copy line length near the readable band. Scoped to direct
   children of the content column so diagram-nested paragraphs (already in narrow
   cards) are unaffected. */
.slide-content > p { max-width: var(--measure); }
/* B-051: keep content-column images at their intrinsic aspect ratio. The flex
   column's default `align-items: stretch` stretches a direct-child <img> (a
   cover logo, a screenshot) to the full column width and distorts it (the
   Rejuran mark, 8.06:1, came out smeared). The .hero-mark already opts out with
   align-self; make it the default for any bare <img> so authors don't have to
   remember. */
.slide-content > img {
  align-self: center;
  max-width: 100%;
  height: auto;
}

/* ── deck-controls cluster (D5 / B-004 AC-2.1) ───────────────────────────
   A fixed bottom-right cluster of nav buttons + the counter. `align-items:
   center` puts the counter text on the same optical baseline as the icon
   buttons (they have different intrinsic line-heights), and every child is a
   fixed-height flex row so the cluster never looks ragged. On small screens
   the cluster shrinks its gap/padding/icon size and is allowed to wrap so it
   can never overflow the viewport edge. */
.deck-controls {
  position: fixed;
  /* B-111: + the letterbox bar, so the cluster sits on the stage, not in the bar. */
  bottom: calc(2vmin + var(--stage-bar-y));
  right: calc(2vmin + var(--stage-bar-x));
  display: flex;
  align-items: center;
  gap: 6px;
  max-width: calc(100vw - 4vmin);
  flex-wrap: wrap;
  justify-content: flex-end;
  /* Neutral translucent — theme-agnostic. html2canvas can't parse color-mix. */
  background: rgba(255, 255, 255, 0.85);
  padding: 5px 8px;
  border-radius: var(--radius-md);
  backdrop-filter: blur(6px);
  border: 1px solid var(--card-border);
  /* B-095: one above `.nav-dots`. Both used to sit at 100, and `.nav-dots` is
     appended to <body> by navigation.js AFTER this cluster is parsed — so the tie
     broke on DOM order and the overhanging rail took the hit test on the cluster.
     `elementFromPoint` at a button's centre returned `nav.nav-dots`, and a real
     click timed out with "intercepts pointer events". Keyboard still worked, which
     is exactly why it survived review: the feature was not dead, only unclickable.

     What this line does TODAY, measured rather than assumed: at 1440x810 with 39
     slides, restoring the tie alone does not bring the defect back, and neither
     does removing either sibling declaration alone — the max-height above keeps the
     rail off the cluster, and pointer-events keeps the container out of the hit
     test. All three have to go at once, which is what the historical CSS was and
     what ci-validate-chrome.mjs §F restores on every run. So this is the layer that
     survives a change of geometry, not the one holding the defect off now. It costs
     one declaration. */
  z-index: 101;
}

.deck-controls button {
  border: 0;
  background: transparent;
  color: var(--fg);
  cursor: pointer;
  font-size: clamp(15px, 2.2vw, 18px);
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 30px;
  height: 30px;
  padding: 0 6px;
  border-radius: var(--radius-sm);
  transition: background var(--motion-fast);
}
.deck-controls button:hover { background: rgba(0, 0, 0, 0.08); }
/* Inline stroke icons. `currentColor` tracks the button's `color: var(--fg)`,
   so icons follow the theme with zero per-theme rules. Sized here (not via
   font-size) with the same clamp the cluster uses, so they shrink together on
   small screens and stay pixel-crisp. */
.deck-controls button svg {
  display: block;
  width: clamp(16px, 2.2vw, 18px);
  height: clamp(16px, 2.2vw, 18px);
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.deck-controls button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* B-031: the 30px desktop buttons already meet WCAG 2.2 AA (24px), so keep them
   compact on a mouse; bump to the 44px touch target only on coarse pointers.
   The taller cluster (~56px) eats into the 8rem nav-dots headroom: at 1024×768
   the bottom dot's 24px WCAG target is clipped to ~17px. 11rem restores the
   geometric clearance to ≥10px across all common touch sizes. */
@media (pointer: coarse) {
  .deck-controls button { min-width: 44px; min-height: 44px; }
  .deck-controls .deck-counter { min-height: 44px; }
  .nav-dots { max-height: calc(100dvh - 11rem); }
}
/* The counter shares the row height + centering so its baseline lines up with
   the buttons rather than floating above them. */
.deck-controls .deck-counter {
  display: inline-flex;
  align-items: center;
  height: 30px;
  padding: 0 4px;
  font-size: clamp(0.75rem, 1.6vw, 0.9rem);
  font-variant-numeric: tabular-nums;
  color: var(--fg);
  white-space: nowrap;
}
[data-theme="dark"] .deck-controls { background: rgba(10, 10, 10, 0.85); }
[data-theme="dark"] .deck-controls button:hover { background: rgba(255, 255, 255, 0.1); }

/* ── brand logo auto-swap (D7 / B-004 AC-3.4) ────────────────────────────
   Two brand assets ship per deck: a dark-on-light mark (.logo-light) and a
   light-on-dark mark (.logo-dark). Exactly one shows at a time, picked by the
   active [data-theme]. Default theme is dark → show the light (white) mark.
   keynote-dark stays dark, so it keeps the light mark; toggling to light swaps
   to the dark mark. The swap is pure CSS (display), so the visible <img>'s
   `src` changes with the theme — see navigation/theme-toggle. */
.brand-mark {
  position: fixed;
  bottom: calc(1.4rem + var(--stage-bar-y));  /* B-111 */
  left: calc(1.4rem + var(--stage-bar-x));
  height: clamp(22px, 3.2vh, 32px);
  width: auto;
  opacity: 0.55;
  z-index: 100;
}
.logo-light { display: none; }
[data-theme="dark"] .logo-dark { display: inline-block; }
[data-theme="dark"] .logo-light { display: none; }
[data-theme="light"] .logo-dark { display: none; }
[data-theme="light"] .logo-light { display: inline-block; }

/* Hero brand logo on the cover slide (brand "hero" role). Reuses the
   .logo-dark/.logo-light theme-swap above, but renders in-flow and larger than
   the corner .brand-mark. Stays visible during PDF capture (it's cover content,
   not chrome). */
.hero-mark {
  align-self: flex-start; /* .slide-content is a flex column (align-items:stretch);
                             without this the logo stretches to the full column
                             width and distorts. Keep it at intrinsic aspect. */
  height: clamp(40px, 7vh, 72px);
  width: auto;
  margin-bottom: clamp(1rem, 3vh, 2rem);
}

.deck-progress {
  position: fixed;
  top: var(--stage-bar-y);   /* B-111: the stage's top edge */
  left: var(--stage-bar-x);
  right: var(--stage-bar-x);
  height: 3px;
  background: transparent;
}

.deck-progress span {
  display: block;
  height: 100%;
  background: var(--accent);
  transition: width var(--motion-base);
}

/* Hide UI chrome during PDF capture */
body.pdf-capture .deck-controls,
body.pdf-capture .nav-dots,
body.pdf-capture .brand-mark,
body.pdf-capture .deck-progress { display: none !important; }
body.pdf-capture .slide {
  position: static;
  opacity: 1 !important;
  pointer-events: auto !important;
  transition: none !important;
  page-break-after: always;
}
/* B-081: the motif folio (motif.css `body[data-folio] .slide::after`) must NOT
   render in the html2canvas path, because pdf.js already stamps `n / total` with
   jsPDF after the raster — both would land bottom-right and overlap. This is the
   ONLY suppressor for that path on purpose: guarding the jsPDF stamp as well
   would leave the exported PDF with no page number at all, and no gate asserts
   that a page number exists. `!important` is warranted here — it must beat the
   deck's own deck.css, which loads last by design (B-055). */
body.pdf-capture .slide::after { display: none !important; }
/* Force reveal content visible in the static PDF layout — every slide is laid
   out at once, so the per-slide `[data-active]` reveal would otherwise leave
   non-active slides' children at opacity:0. */
body.pdf-capture .slide .slide-content > * {
  opacity: 1 !important;
  transform: none !important;
  transition: none !important;
}

/* ── Speaker notes (B-032) ───────────────────────────────────────────────
   Authored as `<aside class="slide-notes" hidden>…</aside>` inside a .slide.
   Never shown in the audience deck (the presenter console reads it via JS).
   `[hidden]` already hides it; this is the belt-and-suspenders rule so a note
   that omits the attribute still never paints on the deck surface. */
.slide-notes { display: none !important; }

/* ── Presenter console (B-032) ───────────────────────────────────────────
   Applied only in the window opened with ?presenter=1 (body.presenter-mode).
   Hides the audience chrome and lays out a current / next / notes console with
   a timer bar. Self-contained: no extra stylesheet ships. */
body.presenter-mode { overflow: auto; }
body.presenter-mode #deck,
body.presenter-mode .deck-controls,
body.presenter-mode .nav-dots,
body.presenter-mode .deck-progress,
body.presenter-mode .brand-mark { display: none !important; }

.presenter-console {
  position: fixed;
  inset: 0;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-body);
}
.pv-bar {
  display: flex;
  align-items: center;
  gap: clamp(0.6rem, 1.5vw, 1.2rem);
  padding: clamp(0.6rem, 1.5vh, 1rem) clamp(1rem, 2.5vw, 2rem);
  border-bottom: 1px solid var(--card-border);
}
.pv-counter { font-weight: 800; font-size: clamp(1rem, 2vw, 1.4rem); color: var(--accent-text); }
.pv-timer {
  font-family: var(--font-mono);
  font-size: clamp(1.1rem, 2.4vw, 1.7rem);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.pv-spacer { flex: 1; }
.pv-reset, .pv-nav {
  font: inherit;
  font-size: var(--small-size);
  padding: 0.4rem 0.9rem;
  min-height: 44px;
  border: 1px solid var(--card-border);
  border-radius: var(--radius-md);
  background: var(--bg-secondary);
  color: var(--fg);
  cursor: pointer;
}
.pv-nav { font-weight: 700; }
.pv-reset:hover, .pv-nav:hover { border-color: var(--accent); }
.pv-grid {
  flex: 1;
  display: grid;
  grid-template-columns: 1.6fr 1fr;
  gap: clamp(0.8rem, 2vw, 1.6rem);
  padding: clamp(0.8rem, 2vw, 1.6rem);
  min-height: 0;
}
.pv-side { display: grid; grid-template-rows: 1fr 1fr; gap: clamp(0.8rem, 2vw, 1.6rem); min-height: 0; }
.pv-pane {
  display: flex;
  flex-direction: column;
  min-height: 0;
  border: 1px solid var(--card-border);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
/* B-078: no uppercase register. presenter.js authors these labels in Korean
   ("현재" / "다음" / "노트"), so `text-transform: uppercase` was a no-op that left
   only its caps-tuned tracking behind. Weight + `--muted` + the tint band already
   separate the label from the pane, which is the same register `.card .tag` uses. */
.pv-label {
  margin: 0;
  padding: 0.4rem 0.8rem;
  font-size: var(--small-size);
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--muted);
  background: var(--bg-secondary);
}
/* The current/next panes shrink real slide content into a 16:9 preview frame. */
.pv-stage {
  flex: 1;
  overflow: hidden;
  padding: clamp(0.8rem, 2vw, 1.6rem);
  container-type: inline-size;
}
.pv-stage > * { max-width: 100%; }
.pv-stage-next { opacity: 0.85; }
.pv-notes {
  flex: 1;
  overflow: auto;
  padding: clamp(0.8rem, 2vw, 1.6rem);
  font-size: clamp(1rem, 1.6vw, 1.35rem);
  line-height: 1.6;
}
.pv-notes p { margin: 0 0 0.7em; }
.pv-empty { color: var(--muted); font-style: italic; }
@media (max-width: 760px) {
  .pv-grid { grid-template-columns: 1fr; }
}
