/* ============================================================================
   ameng-ppt-design · base.css
   Token-based design system + slide primitives. DO NOT edit per deck.
   A theme file (assets/themes/*.css) overrides the tokens in :root only.
   All colors are OKLCH. No hardcoded hex in layouts — always var(--token).
   ============================================================================ */

/* ---- 1. Default token contract (a theme overrides these) ----------------- */
:root {
  /* Surfaces */
  --bg:          oklch(98.5% 0.004 95);    /* deck/page background */
  --surface-1:   oklch(100% 0 0);          /* raised card */
  --surface-2:   oklch(96% 0.005 95);      /* sunken / subtle fill */
  --line:        oklch(88% 0.006 95);      /* hairline borders */
  --line-strong: oklch(78% 0.01 95);

  /* Ink (text) — 3 levels of hierarchy */
  --ink-1: oklch(22% 0.01 260);            /* primary text / headlines */
  --ink-2: oklch(44% 0.012 260);           /* secondary text */
  --ink-3: oklch(60% 0.012 260);           /* muted / captions */

  /* Accent — used SEMANTICALLY (emphasis, data highlight), not decoration.
     Default is EMBER (warm), not blue: 禁蓝 is a house rule, and a blue default
     was a latent trap (any deck that forgets to link a theme would render blue). */
  --accent:     oklch(64% 0.17 45);        /* primary accent — ember (was blue 256) */
  --accent-ink: oklch(98% 0.01 60);        /* text sitting on accent */
  --accent-2:   oklch(66% 0.13 184);       /* secondary accent (charts/dual, teal) */
  --accent-soft: oklch(92% 0.05 50);       /* warm tint background for accent */

  /* Media overlay — legibility veil + text for captions ON a photo (image-hero /
     image-grid). NEUTRAL hue (chroma 0) so 禁蓝 never trips; not pure black/white. */
  --scrim:    oklch(20% 0 0 / 0.62);       /* darkening veil under text over photos */
  --on-media: oklch(97% 0 0);              /* caption text sitting on a photo/scrim */

  /* Typography families. Theme may swap. Self-hosted/system — NO Inter/Roboto. */
  --font-display: var(--ppt-display, "Source Han Serif SC", "Songti SC", "Noto Serif CJK SC", Georgia, serif);
  --font-body:    var(--ppt-body, "PingFang SC", "HarmonyOS Sans SC", "Source Han Sans SC", "Noto Sans CJK SC", system-ui, sans-serif);
  --font-mono:    var(--ppt-mono, "JetBrains Mono", "Sarasa Mono SC", "DejaVu Sans Mono", ui-monospace, monospace);

  /* Type scale — fluid, anchored to a 1280-wide stage. Big scale CONTRAST. */
  --fs-eyebrow: clamp(0.72rem, 0.6rem + 0.4cqw, 0.92rem);
  --fs-body:    clamp(1rem, 0.86rem + 0.55cqw, 1.32rem);
  --fs-lead:    clamp(1.3rem, 1rem + 1cqw, 1.9rem);
  --fs-h3:      clamp(1.5rem, 1.1rem + 1.4cqw, 2.3rem);
  --fs-h2:      clamp(2rem, 1.3rem + 2.4cqw, 3.4rem);
  --fs-h1:      clamp(2.8rem, 1.6rem + 4cqw, 5rem);
  --fs-display: clamp(3.6rem, 1.8rem + 6cqw, 7rem);
  --fs-stat:    clamp(3rem, 1.6rem + 5cqw, 6rem);

  /* Weights & tracking */
  --w-light: 300; --w-reg: 400; --w-med: 500; --w-semi: 600; --w-bold: 700;
  /* 字重阶梯（越大越细、越小越粗）：display 级超大字与数据大数字降一档，
     避免 100px+ 的 bold 糊成一块砖；h1 及以下保持原字重。主题按气质覆盖
     （neo-brutalist 要 800 的粗野、dark-luxe 要更细的奢感，都在主题里盖）。 */
  --w-display: var(--w-semi);   /* .display / .zh-mega 超大标题 */
  --w-stat:    var(--w-semi);   /* .data-hero__num / .stat__num / .donut__val 大数字 */
  --track-tight: -0.02em;
  --track-wide:  0.12em;          /* ALL-CAPS labels only; 0.05–0.12em is the sane band */
  --leading-tight: 1.08;
  --leading-body:  1.62;

  /* Spacing scale (8px rhythm, non-uniform on purpose) */
  --s-1: 0.25rem; --s-2: 0.5rem; --s-3: 0.75rem; --s-4: 1rem;
  --s-5: 1.5rem; --s-6: 2rem; --s-7: 3rem; --s-8: 4.5rem; --s-9: 6.5rem;

  /* Radius & shadow — restrained. One radius family per deck. */
  --r-1: 6px; --r-2: 12px; --r-3: 20px; --r-pill: 999px;
  --shadow-1: 0 1px 2px oklch(0% 0 0 / 0.05);
  --shadow-2: 0 6px 24px -8px oklch(0% 0 0 / 0.14);
  --shadow-3: 0 24px 64px -20px oklch(0% 0 0 / 0.22);

  /* Deck geometry — a slide is a FIXED stage, scaled to fit the viewport. */
  --slide-w: 1280px;
  --slide-h: 720px;               /* 16:9 fixed stage. */
  --pad: clamp(2.5rem, 1rem + 4cqw, 5.5rem);
  --content-max: 1100px;          /* reading width cap so text never over-stretches */

  /* Motion */
  --dur: 480ms;
  --ease: cubic-bezier(0.16, 1, 0.3, 1);

  /* Swappable accent palette (set data-accent on .slide for per-section zones).
     House rule: ONE accent per deck/section, used 60-30-10. 禁蓝 by default —
     "ink-blue" exists only for the report theme, never a flagship default. */
  --pal-ember:   oklch(64% 0.17 45);
  --pal-sand:    oklch(70% 0.12 75);
  --pal-cinnabar:oklch(58% 0.20 30);
  --pal-olive:   oklch(58% 0.10 130);
  --pal-mint:    oklch(72% 0.14 160);
  --pal-magenta: oklch(62% 0.20 348);
  --pal-cyan:    oklch(66% 0.13 215);
}

/* per-section accent re-binding (工业纸感 pattern): one zone = one accent.
   Each also re-binds --accent-ink to the color that ACTUALLY contrasts on that
   accent (WCAG-checked): solid bands (.hl--full / .card--accent / mark) read on
   the band, so the ink must follow the band's luminance, not a single theme
   default. Most palette colors are mid-light → DARK ink wins; only cinnabar
   (deep red) takes near-white. Without this, switching to a light accent (sand /
   mint) left near-white ink on a light band = unreadable. */
.slide[data-accent="ember"],   .deck[data-accent="ember"]   { --accent: var(--pal-ember);    --accent-ink: oklch(21% 0.03 45);  }
.slide[data-accent="sand"],    .deck[data-accent="sand"]    { --accent: var(--pal-sand);     --accent-ink: oklch(22% 0.03 75);  }
.slide[data-accent="cinnabar"],.deck[data-accent="cinnabar"]{ --accent: var(--pal-cinnabar); --accent-ink: oklch(98% 0.01 30);  }
.slide[data-accent="olive"],   .deck[data-accent="olive"]   { --accent: var(--pal-olive);    --accent-ink: oklch(20% 0.03 130); }
.slide[data-accent="mint"],    .deck[data-accent="mint"]    { --accent: var(--pal-mint);     --accent-ink: oklch(22% 0.04 160); }
.slide[data-accent="magenta"], .deck[data-accent="magenta"] { --accent: var(--pal-magenta);  --accent-ink: oklch(21% 0.03 348); }
.slide[data-accent="cyan"],    .deck[data-accent="cyan"]    { --accent: var(--pal-cyan);     --accent-ink: oklch(20% 0.03 215); }

/* ---- 2. Reset & page ----------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; height: 100%; }
/* UA block-margin reset: spacing is controlled by flex `gap` / `.stack` /
   explicit margins — NOT browser-default block margins. Without this a
   `<p class="h3">` carries ~1em top margin (≈font-size) that COMPOUNDS with the
   flex `gap`, showing as an oversized eyebrow→title gap (worst in sparse cards,
   where it shoves the title to the bottom). `:where()` = 0 specificity, so any
   explicit margin (inline style, .stack, theme) still wins. */
:where(p, h1, h2, h3, h4, h5, h6, blockquote, figure, ul, ol, dl, dd) { margin: 0; }
body {
  background: var(--bg);
  color: var(--ink-1);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  line-height: var(--leading-body);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow: hidden;                /* the deck owns the screen */
}

/* ---- 3. Deck stage & fit-to-viewport scaling ----------------------------- */
/* .deck is the fixed 1280×720 stage. runtime.js sets --fit
   to scale it to the window. This keeps layout math simple & pixel-stable. */
.deck {
  position: fixed; inset: 0;
  display: grid; place-items: center;
  /* color set here (not only on body) so inherited text — headings without an
     explicit color — re-resolves --ink-1 INSIDE the theme/dark scope. body is an
     ancestor of .deck, so body's computed color would keep the light value
     under [data-theme="dark"]. */
  color: var(--ink-1);
  /* paint the letterbox (area around the scaled stage) so toggling dark mode (T)
     covers the WHOLE viewport — no light edges leaking. Derived from --bg so it
     flips automatically with the theme; slightly recessed = intentional backdrop. */
  background: color-mix(in oklab, var(--bg) 90%, #000);
}
.deck__stage {
  position: relative;
  width: var(--slide-w);
  height: var(--slide-h);
  transform: scale(var(--fit, 1));
  transform-origin: center center;
  background: var(--bg);
  overflow: hidden;
  /* Make the stage a query container so the type/space scale below resolves in
     `cqw` (1% of the FIXED 1280 stage = 12.8px), NOT `vw` (1% of the browser
     viewport). This is the fix for: (a) export looking different per browser
     size, (b) overflow the headless gate missed — both were caused by vw-based
     fonts growing on wider viewports. Now every viewport, the export, and the
     gate all render the identical 1280-anchored layout. */
  container-type: inline-size;
}
/* Center the MAIN stage by pinning its center to the viewport center, then
   scaling about that point. `place-items:center` alone start-anchors an
   unscaled 1280px box once the viewport is narrower than it, so the scaled
   deck drifts off-center (and clips) at odd window widths — translate(-50%,-50%)
   fixes that deterministically at any size. Scoped with :not(.ppt-thumb …) so
   thumbnail frames (overview + presenter, which JS scales inline) are left
   untouched. */
:not(.ppt-thumb) > .deck > .deck__stage {
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%) scale(var(--fit, 1));
}
/* ---- 4. Slide ------------------------------------------------------------ */
.slide {
  position: absolute; inset: 0;
  padding: var(--pad);
  display: flex; flex-direction: column;
  opacity: 0; visibility: hidden;
  pointer-events: none;
  /* last-resort wrap for unbroken long tokens (URL / long English word / code
     id) so they fold instead of inking past the safe zone. Only kicks in when
     wrapping is allowed — nowrap elements (.fit-text, .scope spans) unaffected. */
  overflow-wrap: break-word;
}
.slide.is-active { opacity: 1; visibility: visible; pointer-events: auto; }

/* entry animation runs only when a slide becomes active (explicit override) */
.slide.is-active [data-anim] { animation: var(--anim, ppt-fade-up) var(--dur) var(--ease) both; }

/* ============ AUTO-MOTION — 默认动效（零标签，每页自动活起来） ===============
   过去：入场动画只对带 data-anim 的元素生效 → 作者漏标 = 该元素静止 → 整页死板。
   现在：活动页的内容块按文档顺序自动逐个上浮；多项容器(grid/bento/bars/flow/
   funnel/layers/matrix/list…)改为让其「子项」逐个错峰，而不是整块一起冒出；柱状
   图自动生长、关键数字自动弹入。写普通 HTML 就有动效，不用给每个元素挂 data-anim。
   退出：单元素加 data-anim="none"；整个 deck 加 data-automotion="off"。
   导出/打印/reduced-motion 一律定格到结束帧（freeze 规则在下方 + render VTB=1600）。
   —— :where() 零特异度，作者用 data-anim 显式覆盖任意元素都赢得过它。 */

/* A) 顶层内容块：直接逐个上浮（叶子型元素，不含多项容器） */
.deck:not([data-automotion="off"]) .slide.is-active
  > :where(.eyebrow,.kicker,h1,h2,h3,h4,.h1,.h2,.h3,.h4,.display,.zh-mega,.lead,
     .body,.measure,p,blockquote,.quote,figure,.frame,.terminal,.data-hero,.rule,
     .tag,.tag-row,img,.prod,.scope,.evo,.donut,.gauge):not([data-anim]):not([data-anim="none"]):not(.notes) {
  animation: ppt-fade-up var(--dur) var(--ease) both;
  animation-delay: calc(var(--i, 0) * 70ms);
}
/* B) 多项容器：错峰它的「直接子项」（卡片/柱/步骤逐个进，而非整块） */
.deck:not([data-automotion="off"]) .slide.is-active
  :where(.grid-2,.grid-3,.grid-4,.bento,.cards,.bars,.flow,.funnel,.layers,.matrix2,
     .timeline,.kpi-row,.donut-row,.zones,.contrast,.split,ul,ol)
  > :not([data-anim]):not([data-anim="none"]):not(.notes) {
  animation: ppt-fade-up var(--dur) var(--ease) both;
  animation-delay: calc(var(--i, 0) * 70ms + 90ms);
}
/* 图表自己长出来（柱生长 + 数字弹入），无需手挂 .fx-*；显式 .fx-* 仍可覆盖 */
.deck:not([data-automotion="off"]) .slide.is-active .bar__fill:not(.fx-grow-y):not(.fx-pop) {
  transform-origin: left; animation: ppt-grow-x .72s var(--ease) both;
  animation-delay: calc(var(--i, 0) * 70ms + 0.12s);
}
.deck:not([data-automotion="off"]) .slide.is-active
  :where(.data-hero__num,.donut__val,.gauge__val,.kpi__num,.stat__num):not([data-anim]) {
  animation: ppt-pop var(--dur) var(--ease) both; animation-delay: 0.1s;
}

/* ---- 5. Chrome: progress bar, page number, footer ------------------------ */
.deck__progress {
  position: absolute; left: 0; top: 0; height: 3px; width: 100%;
  background: transparent; z-index: 5;
}
.deck__progress > i {
  display: block; height: 100%; width: var(--progress, 0%);
  background: var(--accent); transition: width var(--dur) var(--ease);
}
.slide__num {
  position: absolute; right: var(--pad); bottom: calc(var(--pad) * 0.55);
  font-family: var(--font-mono); font-size: 0.8rem; letter-spacing: 0.08em;
  color: var(--ink-3); z-index: 4;
}
.slide__foot {
  position: absolute; left: var(--pad); bottom: calc(var(--pad) * 0.55);
  font-size: 0.78rem; letter-spacing: 0.04em; color: var(--ink-3); z-index: 4;
}

/* ---- 6. Typographic primitives ------------------------------------------ */
.eyebrow {
  font-size: var(--fs-eyebrow); font-weight: var(--w-semi);
  letter-spacing: var(--track-wide); text-transform: uppercase;
  /* accent deepened with ink so kickers stay legible on the LIGHT palettes
     (sand/cyan accents alone wash out to ~2:1 on paper) */
  color: color-mix(in oklab, var(--accent) 70%, var(--ink-1));
}
.display {
  font-family: var(--font-display); font-size: var(--fs-display);
  font-weight: var(--w-display); line-height: var(--leading-tight);
  letter-spacing: var(--track-tight); color: var(--ink-1);
  text-wrap: balance;
}
.h1 { font-family: var(--font-display); font-size: var(--fs-h1); font-weight: var(--w-bold);
      line-height: var(--leading-tight); letter-spacing: var(--track-tight); text-wrap: balance; }
.h2 { font-family: var(--font-display); font-size: var(--fs-h2); font-weight: var(--w-semi);
      line-height: 1.15; letter-spacing: var(--track-tight); text-wrap: balance; }
.h3 { font-size: var(--fs-h3); font-weight: var(--w-semi); line-height: 1.22; }
/* Reading-width caps are in `ch` (width of "0"). CJK glyphs are ~2× that, so a
   small ch cap wraps CJK far too early (40ch ≈ 20 汉字). Caps widened for CJK-first
   decks; for Latin these stay within a comfortable measure. */
.lead { font-size: var(--fs-lead); color: var(--ink-2); font-weight: var(--w-reg);
        line-height: 1.4; max-width: 52ch; text-wrap: pretty; }
.body { font-size: var(--fs-body); color: var(--ink-2); max-width: 72ch; text-wrap: pretty; }
.muted { color: var(--ink-3); }
.accent { color: var(--accent); }
mark { background: var(--accent-soft); color: var(--ink-1); padding: 0 0.18em; border-radius: var(--r-1); }

/* stat / big number */
.stat { display: flex; flex-direction: column; gap: var(--s-2); }
.stat__num { font-family: var(--font-display); font-size: var(--fs-stat); font-weight: var(--w-stat);
             line-height: 1; letter-spacing: var(--track-tight); color: var(--ink-1);
             font-variant-numeric: tabular-nums; }
.stat__num .unit { font-size: 0.42em; color: var(--ink-3); margin-left: 0.15em; font-weight: var(--w-med); }
.stat__label { font-size: var(--fs-eyebrow); letter-spacing: 0.06em; color: var(--ink-3); text-transform: uppercase; }

/* quote */
.quote { font-family: var(--font-display); font-size: var(--fs-h2); font-weight: var(--w-med);
         line-height: 1.3; letter-spacing: var(--track-tight); color: var(--ink-1); text-wrap: balance; }
.quote__by { margin-top: var(--s-5); font-size: var(--fs-body); color: var(--ink-3); }

/* ---- 7. Layout helpers --------------------------------------------------- */
.stack > * + * { margin-top: var(--gap, var(--s-4)); }
.cluster { display: flex; flex-wrap: wrap; gap: var(--gap, var(--s-4)); align-items: center; }
.spread { display: flex; justify-content: space-between; align-items: flex-end; gap: var(--s-6); }
.fill { flex: 1; min-height: 0; }
.center { display: flex; flex-direction: column; justify-content: center; height: 100%; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap, var(--s-6)); }
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--gap, var(--s-5)); }
.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--gap, var(--s-4)); }

/* ---- 7b. Safe-zone container (structural overflow prevention) ------------- */
/* The slide's padding already encodes the safe zone (y∈[85,636] with chrome).
   Wrap dense content in .slide__safe to make children SHARE that height and
   shrink (min-height:0 lets flex/grid children compress) instead of pushing
   past the box and getting silently clipped by .deck__stage{overflow:hidden}.
   Pair grids with `.grid--fit` so rows distribute within the safe box. */
.slide__safe { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; gap: var(--gap, var(--s-5)); }
.slide__safe.center { justify-content: center; }
.slide__safe > .fill { min-height: 0; }
.grid--fit { min-height: 0; align-content: stretch; }
.grid--fit > * { min-height: 0; min-width: 0; }

/* ---- 7c. fit-text: shrink-to-fit a single hero line ----------------------- */
/* For big non-wrapping hero numbers/titles whose length varies (a long stat, a
   long title). runtime.js measures the active slide and reduces font-size until
   the line fits its container width — so a longer-than-expected value scales
   down rather than overflowing. Use on ONE-LINE heroes; not for body copy. */
.fit-text { white-space: nowrap; max-width: 100%; }
/* bento: deliberate non-uniform composition */
.bento { display: grid; gap: var(--s-4); grid-template-columns: repeat(6, 1fr); grid-auto-rows: 1fr; }
.bento > .col-2 { grid-column: span 2; } .bento > .col-3 { grid-column: span 3; }
.bento > .col-4 { grid-column: span 4; } .bento > .row-2 { grid-row: span 2; }

/* card surface */
.card {
  background: var(--surface-1); border: 1px solid var(--line);
  border-radius: var(--r-2); padding: var(--s-5);
  display: flex; flex-direction: column; gap: var(--s-3);
}
.card--soft { background: var(--surface-2); border-color: transparent; }
/* Solid payload blocks (card--accent / z--r / fn--on / fu--on / ly--on / mq--on
   / sc--on) all draw from ONE deck-wide block color — a DEEPENED accent with
   near-white ink (house rule: 色块统一一色 + 反白). Themes define the literals;
   per-section data-accent recolors chrome/eyebrow/hl, NEVER the blocks. */
.deck { --accent-block: var(--accent); --accent-block-ink: var(--accent-ink); }
.card--accent { background: var(--accent-block); color: var(--accent-block-ink); border-color: transparent; }
/* ALL text inside an accent card flips to ink-on-block — otherwise .body/.lead/.muted
   keep their dark color and become unreadable on the fill (contrast bug). */
.card--accent :where(.stat__num, .stat__label, .h2, .h3, .lead, .body, .muted, .eyebrow, p, h2, h3, li) {
  color: var(--accent-block-ink);
}
.card--accent :where(.muted, .body, .data-hero__src, .stat__label) { opacity: 0.85; }
.card--accent .tag { background: oklch(100% 0 0 / 0.18); border-color: transparent; color: var(--accent-block-ink); }

/* pill / tag */
.tag { display: inline-flex; align-items: center; gap: var(--s-2);
       font-size: var(--fs-eyebrow); letter-spacing: 0.04em; color: var(--ink-2);
       background: var(--surface-2); border-radius: var(--r-pill);
       padding: 0.3em 0.85em; border: 1px solid var(--line);
       white-space: nowrap; /* pills never wrap internally (fallback-font safety) */ }

/* divider */
.rule { height: 1px; background: var(--line); border: 0; margin: var(--s-5) 0; }

/* simple bar for honest, real-data charts (no fake numbers) */
.bars { display: flex; flex-direction: column; gap: var(--s-3); }
.bar { display: grid; grid-template-columns: 9rem 1fr auto; align-items: center; gap: var(--s-4); }
/* 24px 级条厚：10px 细线撑不起「数据是主角」，两轮新鲜眼睛评审都点名过 */
.bar__track { height: 1.5rem; background: var(--surface-2); border-radius: var(--r-pill); overflow: hidden; }
.bar__fill { height: 100%; width: var(--v, 0%); background: var(--accent); border-radius: var(--r-pill); }
.bar__val { font-family: var(--font-mono); font-size: 0.9rem; color: var(--ink-2); font-variant-numeric: tabular-nums; }

/* ---- 8. Speaker notes & overview (driven by runtime.js) ------------------ */
.notes, .script { display: none; }
/* bottom sheet = grip (drag to resize, lives outside the editable body) + body.
   Height: auto up to 42vh by default; once the user drags, --notes-h wins (≤80vh). */
.ppt-notes-overlay {
  position: fixed; inset: auto 0 0 0;
  height: var(--notes-h, auto);
  max-height: min(80vh, max(42vh, var(--notes-h, 0px)));
  background: oklch(15% 0.01 260 / 0.97); color: oklch(96% 0 0);
  font-family: var(--font-body); font-size: 1rem; line-height: 1.6;
  z-index: 50; display: none; flex-direction: column;
  border-top: 2px solid var(--accent);
}
.ppt-notes-overlay.is-open { display: flex; }
.ppt-notes-overlay__grip {
  flex: none; display: flex; align-items: center; justify-content: center;
  height: 18px; cursor: ns-resize; touch-action: none;
}
.ppt-notes-overlay__grip span {
  display: block; width: 44px; height: 4px; border-radius: 999px;
  background: oklch(70% 0 0 / 0.4); transition: background 0.15s ease;
}
.ppt-notes-overlay__grip:hover span { background: oklch(85% 0 0 / 0.65); }
.ppt-notes-overlay__body {
  flex: 1; min-height: 0; overflow: auto; outline: none;
  padding: 0 var(--s-7) var(--s-6);
}
.ppt-overview {
  position: fixed; inset: 0; background: oklch(14% 0.012 260 / 0.97); z-index: 60;
  display: none; grid-template-columns: repeat(auto-fill, minmax(248px, 1fr));
  gap: var(--s-5) var(--s-4); padding: var(--s-7) var(--s-6); overflow: auto; align-content: start;
}
.ppt-overview.is-open { display: grid; }
/* each cell = a real scaled-down live preview of the slide + a caption */
.ppt-ov__cell {
  appearance: none; border: 0; background: transparent; padding: 0; margin: 0;
  cursor: pointer; text-align: left; display: flex; flex-direction: column; gap: 0.5rem;
}
.ppt-ov__frame {
  position: relative; width: 100%; aspect-ratio: 16 / 9; overflow: hidden;
  border-radius: var(--r-2); border: 2px solid oklch(100% 0 0 / 0.14); background: var(--bg);
  box-shadow: 0 6px 22px oklch(0% 0 0 / 0.38); transition: border-color .15s, transform .15s, box-shadow .15s;
}
.ppt-ov__cell:hover .ppt-ov__frame { transform: translateY(-3px); border-color: oklch(100% 0 0 / 0.5); }
.ppt-ov__cell:focus-visible { outline: none; }
.ppt-ov__cell:focus-visible .ppt-ov__frame { border-color: var(--accent); box-shadow: 0 0 0 3px oklch(from var(--accent) l c h / 0.4), 0 6px 22px oklch(0% 0 0 / 0.38); }
.ppt-ov__cell.is-current .ppt-ov__frame { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent), 0 6px 22px oklch(0% 0 0 / 0.38); }
/* mini deck/stage scaffold (overview + presenter share .ppt-thumb) so global
   `.deck--chrome .slide` selectors still match. JS scales the mini .deck itself
   (NOT .deck__stage): the export freeze rule `.deck.ppt-exporting .deck__stage`
   force-resets the stage transform with !important, which would clobber an
   inline scale on the stage — scaling the .deck wrapper sidesteps that entirely. */
.ppt-thumb .deck { position: absolute; top: 0; left: 0; width: var(--slide-w, 1280px); height: var(--slide-h, 720px); min-height: 0; display: block; transform-origin: top left; background: transparent; }
.ppt-thumb .deck__stage { position: absolute; top: 0; left: 0; }
.ppt-ov__cap { display: flex; align-items: baseline; gap: 0.55em; padding: 0 2px;
  color: oklch(100% 0 0 / 0.82); font: 500 0.82rem/1.3 var(--font-body); }
.ppt-ov__cap b { font-family: var(--font-mono); font-size: 0.72rem; color: oklch(100% 0 0 / 0.5); min-width: 1.7em; flex: none; }
.ppt-ov__cap span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ppt-ov__cell.is-current .ppt-ov__cap, .ppt-ov__cell.is-current .ppt-ov__cap b { color: var(--accent); }

/* keyboard-shortcut help affordance — bottom-center, subtle, screen-only.
   Hover (or click, or press ? / H) reveals the panel. Hidden in print/export. */
.ppt-help { position: fixed; left: 50%; bottom: 14px; transform: translateX(-50%); z-index: 45; }
.ppt-help__btn {
  font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.04em;
  color: var(--ink-3); background: var(--surface-1); border: 1px solid var(--line);
  border-radius: var(--r-pill); padding: 0.34em 0.9em; cursor: help;
  box-shadow: var(--shadow-1); opacity: 0.5; transition: opacity 0.2s var(--ease);
}
.ppt-help:hover .ppt-help__btn, .ppt-help.is-open .ppt-help__btn { opacity: 1; }
.ppt-help__panel {
  position: absolute; left: 50%; bottom: calc(100% + 8px);
  transform: translateX(-50%) translateY(6px);
  width: 232px; background: var(--surface-1); color: var(--ink-1);
  border: 1px solid var(--line); border-radius: var(--r-2); box-shadow: var(--shadow-3);
  padding: var(--s-4) var(--s-5); opacity: 0; visibility: hidden;
  transition: opacity 0.18s var(--ease), transform 0.18s var(--ease);
}
.ppt-help:hover .ppt-help__panel, .ppt-help.is-open .ppt-help__panel {
  opacity: 1; visibility: visible; transform: translateX(-50%) translateY(0);
}
.ppt-help__row { display: flex; align-items: center; gap: 0.6em; padding: 0.3em 0;
  font-size: 0.78rem; color: var(--ink-2); }
.ppt-help__row b { margin-left: auto; font-weight: var(--w-med); color: var(--ink-2); }
.ppt-help__row .k { display: inline-flex; gap: 0.25em; }
.ppt-help kbd { font-family: var(--font-mono); font-size: 0.68rem; line-height: 1.5;
  background: var(--surface-2); border: 1px solid var(--line); border-bottom-width: 2px;
  border-radius: 4px; padding: 0.06em 0.45em; color: var(--ink-1); }
@media print { .ppt-help { display: none !important; } }

/* ---- 9. Motion keyframes ------------------------------------------------- */
@keyframes ppt-fade-up { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: none; } }
@keyframes ppt-fade    { from { opacity: 0; } to { opacity: 1; } }
@keyframes ppt-rise    { from { opacity: 0; transform: translateY(40px) scale(0.98); } to { opacity: 1; transform: none; } }
@keyframes ppt-wipe    { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } }
@keyframes ppt-blur-in { from { opacity: 0; filter: blur(12px); } to { opacity: 1; filter: none; } }

[data-anim="fade"]   { --anim: ppt-fade; }
[data-anim="rise"]   { --anim: ppt-rise; }
[data-anim="wipe"]   { --anim: ppt-wipe; }
[data-anim="blur"]   { --anim: ppt-blur-in; }
/* explicit stagger group (still supported) */
.anim-stagger > * { animation: ppt-fade-up var(--dur) var(--ease) both;
  animation-delay: calc(var(--i, 0) * 80ms); }

/* document-order index --i (1..12) — drives BOTH the explicit .anim-stagger and
   the AUTO-MOTION cascade above. Set on: explicit groups, the active slide's
   direct children, and every multi-item container's children. Extended to 12
   (was capped at 6 → 7+ items used to all fire together). */
.anim-stagger > *:nth-child(1),  .slide.is-active > *:nth-child(1),  .slide.is-active :where(.grid-2,.grid-3,.grid-4,.bento,.cards,.bars,.flow,.funnel,.layers,.matrix2,.timeline,.asym,.two-col,.imggrid,.kpi-row,.donut-row,.zones,.contrast,.split,ul,ol) > *:nth-child(1)  { --i: 0; }
.anim-stagger > *:nth-child(2),  .slide.is-active > *:nth-child(2),  .slide.is-active :where(.grid-2,.grid-3,.grid-4,.bento,.cards,.bars,.flow,.funnel,.layers,.matrix2,.timeline,.asym,.two-col,.imggrid,.kpi-row,.donut-row,.zones,.contrast,.split,ul,ol) > *:nth-child(2)  { --i: 1; }
.anim-stagger > *:nth-child(3),  .slide.is-active > *:nth-child(3),  .slide.is-active :where(.grid-2,.grid-3,.grid-4,.bento,.cards,.bars,.flow,.funnel,.layers,.matrix2,.timeline,.asym,.two-col,.imggrid,.kpi-row,.donut-row,.zones,.contrast,.split,ul,ol) > *:nth-child(3)  { --i: 2; }
.anim-stagger > *:nth-child(4),  .slide.is-active > *:nth-child(4),  .slide.is-active :where(.grid-2,.grid-3,.grid-4,.bento,.cards,.bars,.flow,.funnel,.layers,.matrix2,.timeline,.asym,.two-col,.imggrid,.kpi-row,.donut-row,.zones,.contrast,.split,ul,ol) > *:nth-child(4)  { --i: 3; }
.anim-stagger > *:nth-child(5),  .slide.is-active > *:nth-child(5),  .slide.is-active :where(.grid-2,.grid-3,.grid-4,.bento,.cards,.bars,.flow,.funnel,.layers,.matrix2,.timeline,.asym,.two-col,.imggrid,.kpi-row,.donut-row,.zones,.contrast,.split,ul,ol) > *:nth-child(5)  { --i: 4; }
.anim-stagger > *:nth-child(6),  .slide.is-active > *:nth-child(6),  .slide.is-active :where(.grid-2,.grid-3,.grid-4,.bento,.cards,.bars,.flow,.funnel,.layers,.matrix2,.timeline,.asym,.two-col,.imggrid,.kpi-row,.donut-row,.zones,.contrast,.split,ul,ol) > *:nth-child(6)  { --i: 5; }
.anim-stagger > *:nth-child(7),  .slide.is-active > *:nth-child(7),  .slide.is-active :where(.grid-2,.grid-3,.grid-4,.bento,.cards,.bars,.flow,.funnel,.layers,.matrix2,.timeline,.asym,.two-col,.imggrid,.kpi-row,.donut-row,.zones,.contrast,.split,ul,ol) > *:nth-child(7)  { --i: 6; }
.anim-stagger > *:nth-child(8),  .slide.is-active > *:nth-child(8),  .slide.is-active :where(.grid-2,.grid-3,.grid-4,.bento,.cards,.bars,.flow,.funnel,.layers,.matrix2,.timeline,.asym,.two-col,.imggrid,.kpi-row,.donut-row,.zones,.contrast,.split,ul,ol) > *:nth-child(8)  { --i: 7; }
.anim-stagger > *:nth-child(9),  .slide.is-active > *:nth-child(9),  .slide.is-active :where(.grid-2,.grid-3,.grid-4,.bento,.cards,.bars,.flow,.funnel,.layers,.matrix2,.timeline,.asym,.two-col,.imggrid,.kpi-row,.donut-row,.zones,.contrast,.split,ul,ol) > *:nth-child(9)  { --i: 8; }
.anim-stagger > *:nth-child(10), .slide.is-active > *:nth-child(10), .slide.is-active :where(.grid-2,.grid-3,.grid-4,.bento,.cards,.bars,.flow,.funnel,.layers,.matrix2,.timeline,.asym,.two-col,.imggrid,.kpi-row,.donut-row,.zones,.contrast,.split,ul,ol) > *:nth-child(10) { --i: 9; }
.anim-stagger > *:nth-child(11), .slide.is-active > *:nth-child(11), .slide.is-active :where(.grid-2,.grid-3,.grid-4,.bento,.cards,.bars,.flow,.funnel,.layers,.matrix2,.timeline,.asym,.two-col,.imggrid,.kpi-row,.donut-row,.zones,.contrast,.split,ul,ol) > *:nth-child(11) { --i: 10; }
.anim-stagger > *:nth-child(12), .slide.is-active > *:nth-child(12), .slide.is-active :where(.grid-2,.grid-3,.grid-4,.bento,.cards,.bars,.flow,.funnel,.layers,.matrix2,.timeline,.asym,.two-col,.imggrid,.kpi-row,.donut-row,.zones,.contrast,.split,ul,ol) > *:nth-child(12) { --i: 11; }

/* Graphic entrance primitives — for charts/diagrams (bars, rings, flow lines, idle pulse).
   Opt-in classes that fire only when a slide is active, so a graphic "draws itself" on arrival.
   ALWAYS authored with `both` fill so the reduced-motion + .ppt-exporting rules can snap them to
   their END frame (full bar / settled ring) — that is what keeps PDF/PPTX export from ever
   capturing a chart mid-animation. Prefer these over hand-rolled @keyframes in each deck. */
@keyframes ppt-pop    { from { opacity: 0; transform: scale(0.82); } to { opacity: 1; transform: scale(1); } }
@keyframes ppt-grow-y { from { transform: scaleY(0.06); } to { transform: scaleY(1); } }       /* set transform-origin:bottom on the bar */
@keyframes ppt-grow-x { from { transform: scaleX(0); } to { transform: scaleX(1); } }          /* horizontal bars: origin left */
@keyframes ppt-breathe{ 50% { transform: scale(1.035); opacity: 0.82; } }
@keyframes ppt-dash   { to  { stroke-dashoffset: -28; } }                                       /* on an SVG path with stroke-dasharray */
@keyframes ppt-ring   { from { stroke-dashoffset: var(--ring-circ, 295); } }                    /* donut ring: draw to its --v offset */
@keyframes ppt-draw   { from { stroke-dashoffset: var(--draw-len, 900); } to { stroke-dashoffset: 0; } } /* one-shot line draw-in */
.slide.is-active .fx-pop     { animation: ppt-pop var(--dur) var(--ease) both; }
.slide.is-active .fx-grow-y  { transform-origin: bottom; animation: ppt-grow-y .72s var(--ease) both; }
.slide.is-active .fx-grow-x  { transform-origin: left; animation: ppt-grow-x .72s var(--ease) both; }
.slide.is-active .fx-breathe { animation: ppt-breathe 5s ease-in-out infinite; }
.slide.is-active .fx-dash    { animation: ppt-dash 2.4s linear infinite; }
/* one-shot SVG stroke draw-in (linechart / 关系连线)：author 可用 --draw-len 精确给
   path 长度，默认 900 覆盖常见折线。≤1600ms + both fill → 四条导出定帧路径安全。 */
.slide.is-active .fx-draw    { stroke-dasharray: var(--draw-len, 900);
                               animation: ppt-draw 1.1s var(--ease) both; }

/* Respect reduced motion — accessibility (web rules). */
@media (prefers-reduced-motion: reduce) {
  .slide.is-active [data-anim], .anim-stagger > * { animation: none !important; }
  /* Also neutralize BESPOKE keyframe entrances (e.g. .is-active .bar{animation:bar-grow},
     ring-pop, dash-run, breathe) that don't use [data-anim]. Collapse to ~0 + one iteration so
     the element jumps to its end frame with no perceptible motion (don't use animation:none —
     keyframes that animate FROM a hidden/zero state would otherwise revert to that state). */
  .deck .slide.is-active *,
  .deck .slide.is-active *::before,
  .deck .slide.is-active *::after {
    animation-duration: .001s !important; animation-delay: 0s !important;
    animation-iteration-count: 1 !important; transition-duration: .001s !important;
  }
  .deck__progress > i { transition: none; }
}

/* ---- 9.5 Signature primitives (工业纸感 DNA) ----------------------------- */

/* Film grain / atmosphere — opt-in via data-grain on .deck. Overlay blend. */
.deck[data-grain] .deck__stage::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 3;
  opacity: 0.4; mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 1  0 0 0 0 1  0 0 0 0 1  0 0 0 0.05 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
}
/* on light themes flip the grain to dark dots */
.deck[data-grain][data-theme="light"] .deck__stage::after,
.deck[data-grain].is-light .deck__stage::after {
  mix-blend-mode: multiply; opacity: 0.5;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  0 0 0 0.06 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
}

/* Inline highlight band — the signature: accent bar behind a key phrase.
   <span class="hl">关键短语</span>. Sits behind text, slightly skewed off-grid. */
.hl {
  position: relative; white-space: nowrap; padding: 0 0.12em;
  color: var(--ink-1); isolation: isolate;
}
.hl::before {
  content: ""; position: absolute; z-index: -1; left: -0.04em; right: -0.04em;
  bottom: 0.04em; height: 0.5em; background: var(--accent);
  opacity: 0.9; transform: skewX(-3deg); border-radius: 1px;
}
/* full band = solid inline highlight (red-on-white etc). Real background so it
   survives layered/dotted slide backgrounds; clones cleanly across line wraps. */
.hl--full {
  color: var(--accent-ink); background: var(--accent);
  padding: 0.04em 0.2em; border-radius: 2px;
  -webkit-box-decoration-break: clone; box-decoration-break: clone;
}
.hl--full::before { display: none; }

/* reading-width cap helper */
.measure { max-width: var(--content-max); }

/* CJK display title tiers (Swiss "极致字号对比" — pair a big tier with small body) */
.zh-mega { font-family: var(--font-display); font-weight: var(--w-display);
  font-size: clamp(3.2rem, 1.4rem + 6cqw, 7.5rem); line-height: 1.02;
  letter-spacing: var(--track-tight); text-wrap: balance; }

/* ---- 10. Print / PDF export (one slide per page, no scaling) -------------
   The Export ▸ PDF button (export.js) injects an exact @page size for the
   current ratio before calling print(); these rules also make a manual
   Cmd/Ctrl+P paginate every slide instead of printing only the active one. */
@media print {
  html, body { overflow: visible; background: #fff; margin: 0; padding: 0; }
  .deck { position: static; display: block; }
  .deck__stage {
    position: static; transform: none; box-shadow: none;
    width: var(--slide-w); height: auto; overflow: visible;
  }
  .slide {
    position: relative; inset: auto;
    width: var(--slide-w); height: var(--slide-h);
    opacity: 1 !important; visibility: visible !important; pointer-events: auto;
    overflow: hidden;
    break-after: page; page-break-after: always;
  }
  .slide:last-child { break-after: auto; page-break-after: auto; }
  .slide [data-anim] { animation: none !important; opacity: 1 !important; transform: none !important; }
  /* catch-all: AUTO-MOTION animates non-[data-anim] elements too — collapse every
     entrance to its end frame (both-fill holds it) so print never catches mid-motion. */
  .slide *, .slide *::before, .slide *::after {
    animation-duration: 0s !important; animation-delay: 0s !important;
    transition-duration: 0s !important; transition-delay: 0s !important;
  }
  /* never print the authoring/runtime chrome */
  .ppt-toolbar, .ppt-history, .ppt-toast, .ppt-help, .ppt-overview,
  .ppt-notes-overlay, .deck__progress, .slide__num { display: none !important; }
}
