/* OBS Lower Thirds — overlay styles (dynamic element model).
   Written flat (no CSS nesting) so it runs on the older Chromium builds
   embedded in OBS 28/29 as well as current ones. */

* { margin: 0; padding: 0; box-sizing: border-box; }

html, body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: transparent;
}

/* test backgrounds (?bg=checker|black|white|green) */
body.bg-checker {
  background: repeating-conic-gradient(#3c3c40 0% 25%, #2a2a2e 0% 50%) 0 0 / 36px 36px;
}
body.bg-black { background: #000; }
body.bg-white { background: #dfe3e8; }
body.bg-green { background: #00b140; }

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

#stage {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: flex-end;
  justify-content: flex-start;
  padding: 0 var(--margin-side, 0px) var(--margin-bottom, 64px) var(--margin-side, 0px);
  font-family: var(--font, sans-serif);
}
#stage[data-anchor="center"] { justify-content: center; }
#stage[data-anchor="right"]  { justify-content: flex-end; }

#stage.hidden { visibility: hidden; }

.lt {
  width: fit-content;
  max-width: var(--maxw, 70%);
  filter: var(--shadow-filter, none);
}
#stage[data-fullwidth="1"] .lt { width: 100%; max-width: none; }

/* ------------------------------------------------------------- grid */

.lt-grid {
  display: grid;
  direction: ltr;            /* physical placement; text direction lives in .txt */
  column-gap: var(--gap, 4px);
  row-gap: var(--gap, 4px);
  align-items: stretch;
  justify-items: stretch;
}

/* one grid cell may hold several elements — they line up horizontally */
.cell {
  display: flex;
  align-items: stretch;
  justify-content: flex-start;
  min-width: 0;
  gap: var(--gap, 4px);
}
/* in RTL the bars hug the right edge of their cell, as broadcast RTL does */
#stage[data-dir="rtl"] .cell { justify-content: flex-end; }
.cell[data-stretch="1"] { justify-content: stretch; }

/* -------------------------------------------------------------- box */

.box {
  position: relative;
  width: fit-content;
  max-width: 100%;
  display: flex;
  align-items: center;
  overflow: hidden;
}

/* text container (clips the swap animation) */
.txt {
  position: relative;
  overflow: hidden;
  display: block;
  width: 100%;
  text-align: var(--talign, start);
}
#stage[data-dir="rtl"] .txt { direction: rtl; }
#stage[data-dir="ltr"] .txt { direction: ltr; }
.line { display: block; }
/* shrink-to-fit's hidden measurer: the text on one line at the box's own
   font size, outside .line so a fitted size never feeds back into it */
.meas { position: absolute; top: 0; left: 0; visibility: hidden; white-space: nowrap; pointer-events: none; }
/* width/height/left/right are set inline at swap time to freeze the outgoing
   text's layout so it cannot re-wrap while it animates out */
.line-exit {
  position: absolute;
  top: 0;
}

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

.box.img-box {
  justify-content: center;
  overflow: hidden;
}
/* a logo is a still or a short video — both sit in the same slot */
.box.img-box img,
.box.img-box video {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) scale(var(--img-scale, 1));
  max-width: calc(100% - 4px);
  max-height: calc(100% - 4px);
  object-fit: contain;
  display: block;
}

/* ------------------------------------------------------------- edges */

/* A chevron end-cap: the element is pointed on one end and notched on the other,
   so a row of segments chains into one ribbon. There is no artwork involved —
   the shape is a clip on the element, so its colour is the element's own
   background and every segment recolours independently.

   The point faces the way the text READS: right in LTR, left in RTL. Pointing
   it the other way makes the ribbon read as an arrow running back against the
   sentence, which is what it did before this was swapped. */
.box[data-edges="chevron"] {
  clip-path: polygon(
    0 0,
    calc(100% - var(--chamfer, 26px)) 0,
    100% 50%,
    calc(100% - var(--chamfer, 26px)) 100%,
    0 100%,
    var(--chamfer, 26px) 50%
  );
  /* The point and the notch both sit INSIDE the element's own box, so two
     abutting boxes leave a chamfer-wide hole at the seam no matter how the
     polygon is drawn — they only mesh if each segment is pulled back over its
     neighbour by exactly that much. The pull is applied to the CELL by
     applyElementStyle, not here: a box that fills its cell would simply re-grow
     and swallow a margin of its own.

     The V is cut at 50% of each element's OWN height, so segments of different
     heights meet at different points and the seam opens out in the middle.
     Filling the row is what keeps every apex on one line. */
  align-self: stretch;
}
#stage[data-dir="rtl"] .box[data-edges="chevron"] {
  clip-path: polygon(
    var(--chamfer, 26px) 0,
    100% 0,
    calc(100% - var(--chamfer, 26px)) 50%,
    100% 100%,
    var(--chamfer, 26px) 100%,
    0 50%
  );
}

/* One-sided cuts. data-ends="end" keeps the START flat and cuts only the end
   (a chevron with its point but no notch; a slant on the end only);
   data-ends="start" keeps the END flat (notch only; slant on the start only).
   Start and end follow the reading direction, so each has an RTL mirror. */
.box[data-edges="chevron"][data-ends="end"] {
  clip-path: polygon(0 0, calc(100% - var(--chamfer, 26px)) 0, 100% 50%, calc(100% - var(--chamfer, 26px)) 100%, 0 100%);
}
.box[data-edges="chevron"][data-ends="start"] {
  clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%, var(--chamfer, 26px) 50%);
}
#stage[data-dir="rtl"] .box[data-edges="chevron"][data-ends="end"] {
  clip-path: polygon(var(--chamfer, 26px) 0, 100% 0, 100% 100%, var(--chamfer, 26px) 100%, 0 50%);
}
#stage[data-dir="rtl"] .box[data-edges="chevron"][data-ends="start"] {
  clip-path: polygon(0 0, 100% 0, calc(100% - var(--chamfer, 26px)) 50%, 100% 100%, 0 100%);
}
.box[data-edges="chamfer"][data-ends="start"] {
  clip-path: polygon(var(--chamfer, 26px) 0, 100% 0, 100% 100%, 0 100%);
}
.box[data-edges="chamfer"][data-ends="end"] {
  clip-path: polygon(0 0, 100% 0, calc(100% - var(--chamfer, 26px)) 100%, 0 100%);
}
#stage[data-dir="rtl"] .box[data-edges="chamfer"][data-ends="start"] {
  clip-path: polygon(0 0, calc(100% - var(--chamfer, 26px)) 0, 100% 100%, 0 100%);
}
#stage[data-dir="rtl"] .box[data-edges="chamfer"][data-ends="end"] {
  clip-path: polygon(0 0, 100% 0, 100% 100%, var(--chamfer, 26px) 100%);
}

.box[data-edges="rounded"] { border-radius: var(--radius, 14px); }
.box[data-edges="chamfer"] {
  clip-path: polygon(var(--chamfer, 26px) 0, 100% 0, calc(100% - var(--chamfer, 26px)) 100%, 0 100%);
  /* the inset that keeps text clear of the slant is added to the operator's
     own padding in applyElementStyle, not forced over it from here */
}
#stage[data-dir="rtl"] .box[data-edges="chamfer"] {
  clip-path: polygon(0 0, calc(100% - var(--chamfer, 26px)) 0, 100% 100%, var(--chamfer, 26px) 100%);
}

/* ------------------------------------------------------------ accent */

.box::before {
  content: "";
  position: absolute;
  display: none;
  background: var(--accent-color, #1c56d6);
  z-index: 2;
}
.box[data-accent="top"]::before {
  display: block;
  top: 0; left: 0; right: 0;
  height: var(--accent-h, 6px);
}
.box[data-accent="underline"]::before,
.box[data-accent="bottom"]::before {
  display: block;
  bottom: 0; left: 0; right: 0;
  height: var(--accent-h, 6px);
}
.box[data-accent="side"]::before {
  display: block;
  top: 0; bottom: 0; left: 0;
  width: var(--accent-h, 6px);
}
#stage[data-dir="rtl"] .box[data-accent="side"]::before {
  left: auto; right: 0;
}

/* Chevron accent bands. A band the full height of the box hugging its start
   (::before) or end (::after) edge, one thickness plus one depth wide. Its
   FREE side is cut like a chevron with the box's own --chamfer, so it runs
   parallel to the box's point or notch; its OUTER side is flat and is shaped
   by the box's own clip-path — a chevron box makes it a point or a notch, a
   square box leaves it flat. The box's background is untouched. */
.box[data-accent="chevron-start"]::before,
.box[data-accent="chevron-both"]::before,
.box[data-accent="chevron-end"]::after,
.box[data-accent="chevron-both"]::after {
  content: "";
  position: absolute;
  display: block;
  top: 0; bottom: 0;
  width: calc(var(--accent-h, 6px) + var(--chamfer, 26px));
  background: var(--accent-color, #1c56d6);
  z-index: 2;
  pointer-events: none;
}
/* start band: LTR at the left, free side is a point toward the text */
.box[data-accent="chevron-start"]::before,
.box[data-accent="chevron-both"]::before {
  left: 0;
  clip-path: polygon(0 0, calc(100% - var(--chamfer, 26px)) 0, 100% 50%, calc(100% - var(--chamfer, 26px)) 100%, 0 100%);
}
/* end band: LTR at the right, free side is a notch (parallel to the point) */
.box[data-accent="chevron-end"]::after,
.box[data-accent="chevron-both"]::after {
  right: 0;
  clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%, var(--chamfer, 26px) 50%);
}
#stage[data-dir="rtl"] .box[data-accent="chevron-start"]::before,
#stage[data-dir="rtl"] .box[data-accent="chevron-both"]::before {
  left: auto; right: 0;
  clip-path: polygon(var(--chamfer, 26px) 0, 100% 0, 100% 100%, var(--chamfer, 26px) 100%, 0 50%);
}
#stage[data-dir="rtl"] .box[data-accent="chevron-end"]::after,
#stage[data-dir="rtl"] .box[data-accent="chevron-both"]::after {
  right: auto; left: 0;
  clip-path: polygon(0 0, 100% 0, calc(100% - var(--chamfer, 26px)) 50%, 100% 100%, 0 100%);
}

/* ------------------------------------------------- property morphing */

#stage.prop-anim { transition: padding var(--change-ms, 450ms) var(--ease); }
#stage.prop-anim .lt {
  transition: filter var(--change-ms, 450ms) var(--ease),
              max-width var(--change-ms, 450ms) var(--ease);
}
#stage.prop-anim .lt-grid {
  transition: column-gap var(--change-ms, 450ms) var(--ease),
              row-gap var(--change-ms, 450ms) var(--ease);
}
/* through a text swap the bar follows its text's height on the slide's own
   clock: two lines to one shrinks with the swap instead of holding the taller
   height and snapping at the end */
#stage.prop-anim .txt { transition: height var(--change-ms, 450ms) var(--ease); }
/* while a swap is in flight the cuts beside a chevron are recomputed every
   frame to the bar's current height; they must land at once, not be eased
   toward a target that has already moved on */
#stage.prop-anim .box.cut-follow {
  transition-property: background, background-color, color, width, min-width,
                       border-radius, padding, font-size, letter-spacing,
                       opacity, min-height;
}
#stage.prop-anim .box {
  transition-property: background, background-color, color, width, min-width,
                       border-radius, padding, font-size, letter-spacing,
                       clip-path, opacity, min-height;
  transition-duration: var(--change-ms, 450ms);
  transition-timing-function: var(--ease);
}
#stage.prop-anim .txt {
  transition: height var(--change-ms, 450ms) var(--ease);
}
#stage.prop-anim .box.img-box img,
#stage.prop-anim .box.img-box video {
  transition: transform var(--change-ms, 450ms) var(--ease),
              opacity var(--change-ms, 450ms) var(--ease);
}

/* --------------------------------------------------- text swap anim */

.line-exit.swap-slide { animation: kf-line-out var(--change-ms, 450ms) var(--ease) both; }
.line-enter.swap-slide { animation: kf-line-in var(--change-ms, 450ms) var(--ease) both; }
.line-exit.swap-fade { animation: kf-fade-out var(--change-ms, 450ms) var(--ease) both; }
.line-enter.swap-fade { animation: kf-fade-in var(--change-ms, 450ms) var(--ease) both; }

@keyframes kf-line-out { from { transform: translateY(0); } to { transform: translateY(-112%); } }
@keyframes kf-line-in  { from { transform: translateY(112%); } to { transform: translateY(0); } }
@keyframes kf-fade-out { from { opacity: 1; } to { opacity: 0; } }
@keyframes kf-fade-in  { from { opacity: 0; } to { opacity: 1; } }

/* --swap-ms is set per swap by the rotation; a plain commit leaves it unset
   and falls back to the look's change duration */
.img-exit {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%) scale(var(--img-scale, 1));
  max-width: calc(100% - 4px);
  max-height: calc(100% - 4px);
  object-fit: contain;
  animation: kf-fade-out var(--swap-ms, var(--change-ms, 450ms)) var(--ease) both;
}
.img-enter { animation: kf-fade-in var(--swap-ms, var(--change-ms, 450ms)) var(--ease) both; }

/* logo-swap styles. Every transform keyframe has to carry the centring
   translate and --img-scale, or the picture jumps out of its box mid-swap. */
.img-exit.sw-slide  { animation-name: kf-logo-slide-out; }
.img-enter.sw-slide { animation-name: kf-logo-slide-in; }
.img-exit.sw-flip   { animation-name: kf-logo-flip-out; }
.img-enter.sw-flip  { animation-name: kf-logo-flip-in; }
.img-exit.sw-zoom   { animation-name: kf-logo-zoom-out; }
.img-enter.sw-zoom  { animation-name: kf-logo-zoom-in; }

@keyframes kf-logo-slide-out {
  from { opacity: 1; transform: translate(-50%, -50%) scale(var(--img-scale, 1)); }
  to   { opacity: 0; transform: translate(-50%, -150%) scale(var(--img-scale, 1)); }
}
@keyframes kf-logo-slide-in {
  from { opacity: 0; transform: translate(-50%, 50%) scale(var(--img-scale, 1)); }
  to   { opacity: 1; transform: translate(-50%, -50%) scale(var(--img-scale, 1)); }
}
@keyframes kf-logo-flip-out {
  from { opacity: 1; transform: translate(-50%, -50%) scale(var(--img-scale, 1)) rotateY(0deg); }
  to   { opacity: 0; transform: translate(-50%, -50%) scale(var(--img-scale, 1)) rotateY(90deg); }
}
@keyframes kf-logo-flip-in {
  from { opacity: 0; transform: translate(-50%, -50%) scale(var(--img-scale, 1)) rotateY(-90deg); }
  to   { opacity: 1; transform: translate(-50%, -50%) scale(var(--img-scale, 1)) rotateY(0deg); }
}
@keyframes kf-logo-zoom-out {
  from { opacity: 1; transform: translate(-50%, -50%) scale(var(--img-scale, 1)); }
  to   { opacity: 0; transform: translate(-50%, -50%) scale(calc(var(--img-scale, 1) * 1.35)); }
}
@keyframes kf-logo-zoom-in {
  from { opacity: 0; transform: translate(-50%, -50%) scale(calc(var(--img-scale, 1) * 0.65)); }
  to   { opacity: 1; transform: translate(-50%, -50%) scale(var(--img-scale, 1)); }
}

/* The heavier swaps. These are the ones worth reaching for on a station ident:
   the two pictures move as one piece rather than dissolving into each other. */
.img-exit.sw-push   { animation-name: kf-logo-push-out; }
.img-enter.sw-push  { animation-name: kf-logo-push-in; }
.img-exit.sw-wipe   { animation-name: kf-logo-wipe-out; }
.img-enter.sw-wipe  { animation-name: kf-logo-wipe-in; }
.img-exit.sw-cube   { animation-name: kf-logo-cube-out; }
.img-enter.sw-cube  { animation-name: kf-logo-cube-in; }
.img-exit.sw-iris   { animation-name: kf-logo-iris-out; }
.img-enter.sw-iris  { animation-name: kf-logo-iris-in; }
/* push and wipe travel with the reading direction, so they mirror in RTL */
#stage[data-dir="rtl"] .img-exit.sw-push  { animation-name: kf-logo-push-out-rtl; }
#stage[data-dir="rtl"] .img-enter.sw-push { animation-name: kf-logo-push-in-rtl; }
#stage[data-dir="rtl"] .img-exit.sw-wipe  { animation-name: kf-logo-wipe-out-rtl; }
#stage[data-dir="rtl"] .img-enter.sw-wipe { animation-name: kf-logo-wipe-in-rtl; }

/* push: the outgoing picture is shoved off and the incoming one takes its
   place, both moving together — no dissolve, so it reads as one object */
@keyframes kf-logo-push-out {
  from { opacity: 1; transform: translate(-50%, -50%) scale(var(--img-scale, 1)); }
  to   { opacity: 1; transform: translate(calc(-50% - var(--push-dx, 300px)), -50%) scale(var(--img-scale, 1)); }
}
@keyframes kf-logo-push-in {
  from { opacity: 1; transform: translate(calc(-50% + var(--push-dx, 300px)), -50%) scale(var(--img-scale, 1)); }
  to   { opacity: 1; transform: translate(-50%, -50%) scale(var(--img-scale, 1)); }
}
@keyframes kf-logo-push-out-rtl {
  from { opacity: 1; transform: translate(-50%, -50%) scale(var(--img-scale, 1)); }
  to   { opacity: 1; transform: translate(calc(-50% + var(--push-dx, 300px)), -50%) scale(var(--img-scale, 1)); }
}
@keyframes kf-logo-push-in-rtl {
  from { opacity: 1; transform: translate(calc(-50% - var(--push-dx, 300px)), -50%) scale(var(--img-scale, 1)); }
  to   { opacity: 1; transform: translate(-50%, -50%) scale(var(--img-scale, 1)); }
}

/* wipe: a hard edge travels across and the picture changes behind it */
@keyframes kf-logo-wipe-out {
  from { clip-path: inset(0 0 0 0); }
  to   { clip-path: inset(0 100% 0 0); }
}
@keyframes kf-logo-wipe-in {
  from { clip-path: inset(0 0 0 100%); }
  to   { clip-path: inset(0 0 0 0); }
}
@keyframes kf-logo-wipe-out-rtl {
  from { clip-path: inset(0 0 0 0); }
  to   { clip-path: inset(0 0 0 100%); }
}
@keyframes kf-logo-wipe-in-rtl {
  from { clip-path: inset(0 100% 0 0); }
  to   { clip-path: inset(0 0 0 0); }
}

/* cube: a quarter turn about the box's own edge, with the perspective set on
   .box.img-box — the two faces read as sides of a solid, not a flip in place */
@keyframes kf-logo-cube-out {
  from { opacity: 1; transform: perspective(900px) translate(-50%, -50%) scale(var(--img-scale, 1)) rotateY(0deg); }
  to   { opacity: 0; transform: perspective(900px) translate(-50%, -50%) scale(var(--img-scale, 1)) rotateY(-84deg); }
}
@keyframes kf-logo-cube-in {
  from { opacity: 0; transform: perspective(900px) translate(-50%, -50%) scale(var(--img-scale, 1)) rotateY(84deg); }
  to   { opacity: 1; transform: perspective(900px) translate(-50%, -50%) scale(var(--img-scale, 1)) rotateY(0deg); }
}

/* iris: a circle opens out of the middle */
@keyframes kf-logo-iris-out {
  from { opacity: 1; clip-path: circle(78% at 50% 50%); }
  to   { opacity: 0; clip-path: circle(0% at 50% 50%); }
}
@keyframes kf-logo-iris-in {
  from { opacity: 0; clip-path: circle(0% at 50% 50%); }
  to   { opacity: 1; clip-path: circle(78% at 50% 50%); }
}

/* ------------------------------------------------------ in/out anims */

#stage.anim-in .anim-el {
  animation-duration: var(--el-in-ms, var(--in-ms, 700ms));
  animation-timing-function: var(--ease);
  animation-fill-mode: both;
  animation-delay: calc(var(--i, 0) * var(--stagger, 90ms) + var(--el-delay, 0ms));
}
#stage.anim-out .anim-el {
  animation-duration: var(--out-ms, 500ms);
  animation-timing-function: var(--ease);
  animation-fill-mode: both;
  animation-delay: calc((var(--n, 4) - 1 - var(--i, 0)) * var(--stagger, 90ms));
}

/* Reactions: an element moving because another element's logo changed. These
   run on their own, outside the in/out sequence, so they set every animation
   property rather than inheriting the .anim-el block. */
.box.react-flick,
.box.react-replay,
.box.react-pulse {
  animation-duration: var(--react-ms, 400ms);
  animation-timing-function: var(--ease);
  animation-fill-mode: both;
  animation-delay: 0s;
}
.box.react-flick  { animation-name: kf-react-flick; }
.box.react-replay { animation-name: kf-react-replay; }
.box.react-pulse  { animation-name: kf-react-pulse; }
#stage[data-dir="rtl"] .box.react-flick  { animation-name: kf-react-flick-rtl; }
#stage[data-dir="rtl"] .box.react-replay { animation-name: kf-react-replay-rtl; }

/* flick: out one side, back in from the other — it reads as the element
   sweeping the change through rather than merely twitching */
@keyframes kf-react-flick {
  0%   { transform: translateX(0); opacity: 1; }
  40%  { transform: translateX(34px); opacity: 0; }
  60%  { transform: translateX(-34px); opacity: 0; }
  100% { transform: translateX(0); opacity: 1; }
}
@keyframes kf-react-flick-rtl {
  0%   { transform: translateX(0); opacity: 1; }
  40%  { transform: translateX(-34px); opacity: 0; }
  60%  { transform: translateX(34px); opacity: 0; }
  100% { transform: translateX(0); opacity: 1; }
}
@keyframes kf-react-replay {
  from { transform: translateX(-80px); opacity: 0; }
  to   { transform: translateX(0); opacity: 1; }
}
@keyframes kf-react-replay-rtl {
  from { transform: translateX(80px); opacity: 0; }
  to   { transform: translateX(0); opacity: 1; }
}
@keyframes kf-react-pulse {
  0%   { transform: scale(1); }
  45%  { transform: scale(1.16); }
  100% { transform: scale(1); }
}

/* slide-up */
#stage[data-in="slide-up"].anim-in .anim-el { animation-name: kf-slideup-in; }
#stage[data-out="slide-up"].anim-out .anim-el { animation-name: kf-slideup-out; }
@keyframes kf-slideup-in  { from { transform: translateY(45vh); } to { transform: translateY(0); } }
@keyframes kf-slideup-out { from { transform: translateY(0); } to { transform: translateY(45vh); } }

/* slide-side */
#stage[data-in="slide-side"].anim-in .anim-el { animation-name: kf-slideside-in; }
#stage[data-out="slide-side"].anim-out .anim-el { animation-name: kf-slideside-out; }
#stage[data-dir="rtl"][data-in="slide-side"].anim-in .anim-el { animation-name: kf-slideside-in-rtl; }
#stage[data-dir="rtl"][data-out="slide-side"].anim-out .anim-el { animation-name: kf-slideside-out-rtl; }
@keyframes kf-slideside-in      { from { transform: translateX(-80px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes kf-slideside-out     { from { transform: translateX(0); opacity: 1; } to { transform: translateX(-80px); opacity: 0; } }
@keyframes kf-slideside-in-rtl  { from { transform: translateX(80px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes kf-slideside-out-rtl { from { transform: translateX(0); opacity: 1; } to { transform: translateX(80px); opacity: 0; } }

/* wipe */
#stage[data-in="wipe"].anim-in .anim-el { animation-name: kf-wipe-in; }
#stage[data-out="wipe"].anim-out .anim-el { animation-name: kf-wipe-out; }
#stage[data-dir="rtl"][data-in="wipe"].anim-in .anim-el { animation-name: kf-wipe-in-rtl; }
#stage[data-dir="rtl"][data-out="wipe"].anim-out .anim-el { animation-name: kf-wipe-out-rtl; }
@keyframes kf-wipe-in      { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } }
@keyframes kf-wipe-out     { from { clip-path: inset(0 0 0 0); } to { clip-path: inset(0 100% 0 0); } }
@keyframes kf-wipe-in-rtl  { from { clip-path: inset(0 0 0 100%); } to { clip-path: inset(0 0 0 0); } }
@keyframes kf-wipe-out-rtl { from { clip-path: inset(0 0 0 0); } to { clip-path: inset(0 0 0 100%); } }

/* fade */
#stage[data-in="fade"].anim-in .anim-el { animation-name: kf-fade-in; }
#stage[data-out="fade"].anim-out .anim-el { animation-name: kf-fade-out; }

/* pop */
#stage[data-in="pop"].anim-in .anim-el { animation-name: kf-pop-in; }
#stage[data-out="pop"].anim-out .anim-el { animation-name: kf-pop-out; }
@keyframes kf-pop-in  { from { transform: scale(0.84) translateY(26px); opacity: 0; } to { transform: scale(1) translateY(0); opacity: 1; } }
@keyframes kf-pop-out { from { transform: scale(1) translateY(0); opacity: 1; } to { transform: scale(0.84) translateY(26px); opacity: 0; } }

/* One element can opt out of the look's entrance and use its own.

   These sit AFTER the stage rules on purpose. The extra .box beats the plain
   '#stage[data-in=X].anim-in .anim-el' rules on specificity, but it only TIES
   with the direction-qualified '#stage[data-dir=rtl][data-in=X]...' ones — and
   a tie is broken by source order. Placed earlier, they lost that tie and a
   per-element entrance was silently ignored in RTL whenever the look was set
   to slide-side or wipe. Do not move this block back above them. */
#stage.anim-in .box.anim-el[data-in="slide-up"]  { animation-name: kf-slideup-in; }
#stage.anim-in .box.anim-el[data-in="fade"]      { animation-name: kf-fade-in; }
#stage.anim-in .box.anim-el[data-in="pop"]       { animation-name: kf-pop-in; }
#stage.anim-in .box.anim-el[data-in="slide-side"] { animation-name: kf-slideside-in; }
#stage.anim-in .box.anim-el[data-in="wipe"]       { animation-name: kf-wipe-in; }
#stage[data-dir="rtl"].anim-in .box.anim-el[data-in="slide-side"] { animation-name: kf-slideside-in-rtl; }
#stage[data-dir="rtl"].anim-in .box.anim-el[data-in="wipe"]       { animation-name: kf-wipe-in-rtl; }
/* the exit mirrors whatever entrance the element chose */
#stage.anim-out .box.anim-el[data-in="slide-up"]  { animation-name: kf-slideup-out; }
#stage.anim-out .box.anim-el[data-in="fade"]      { animation-name: kf-fade-out; }
#stage.anim-out .box.anim-el[data-in="pop"]       { animation-name: kf-pop-out; }
#stage.anim-out .box.anim-el[data-in="slide-side"] { animation-name: kf-slideside-out; }
#stage.anim-out .box.anim-el[data-in="wipe"]       { animation-name: kf-wipe-out; }
#stage[data-dir="rtl"].anim-out .box.anim-el[data-in="slide-side"] { animation-name: kf-slideside-out-rtl; }
#stage[data-dir="rtl"].anim-out .box.anim-el[data-in="wipe"]       { animation-name: kf-wipe-out-rtl; }

/* note: wipe animates clip-path, which overrides a chamfer clip while the
   element is animating; the chamfer is restored when the animation ends */

/* --------------------------------------------------------- pvw badge */

#pvw-tag {
  position: fixed;
  top: 10px;
  left: 10px;
  z-index: 99;
  font: 700 13px/1 system-ui, sans-serif;
  letter-spacing: 2px;
  color: #ffd76a;
  background: rgba(20, 20, 20, 0.55);
  border: 1px solid rgba(255, 215, 106, 0.7);
  border-radius: 4px;
  padding: 5px 8px;
}
