  *{box-sizing:border-box}
  /* No native scrollbars. Every scrolling surface here is either a full-bleed
     canvas or a panel, and an OS scrollbar reads as a stray artifact on both —
     especially on a projector. Scrolling itself is untouched. */
  *{scrollbar-width:none; -ms-overflow-style:none}
  *::-webkit-scrollbar{width:0; height:0; display:none}
  html,body{height:100%; height:100dvh; overflow:hidden}
  body{
    margin:0; background:var(--canvas); color:var(--on-dark); font-family:var(--sans);
    font-size:16px; line-height:1.55; -webkit-font-smoothing:antialiased;
    overflow:hidden; overscroll-behavior:none;
    touch-action:manipulation;                    /* no browser double-tap zoom fighting ours */
    -webkit-tap-highlight-color:transparent;
  }
  button{font-family:inherit; color:inherit}
  :focus-visible{outline:2px solid var(--primary); outline-offset:2px; border-radius:var(--r-sm)}
  .serif{font-family:var(--serif)}

  /* --- entrance motion (SplitText-style, no animation library) --- */
  .st-word{display:inline-block; white-space:nowrap}
  .st-unit{display:inline-block; will-change:transform,opacity}
  /* splitText() cuts on whitespace, so a Japanese or Chinese headline — which
     has none — comes back as a single .st-word that nowrap then refuses to
     break, and it runs off the page. The units inside are inline-blocks and
     can break between themselves, so letting this one wrap is enough; the
     per-character animation is untouched. */
  html[lang="ja"] .st-word, html[lang="zh"] .st-word{white-space:normal}
  .rise{opacity:0}
  @media (prefers-reduced-motion:reduce){
    .st-unit{opacity:1 !important; transform:none !important}
    .rise{opacity:1 !important; transform:none !important}
  }

