/* Structure only. Colour, font, size and padding come from the page; the theme skins .nstr.
   Exposed: --nstr-duration (transition time), --nstr-shift / --nstr-shift-x (slide distances),
   --nstr-crawl-gap / --nstr-crawl-edge (the crawl's spacing and edge fade),
   --nstr-bg / --nstr-ink (the colours a message asks for; the script sets them on the host element,
   or on .nstr itself when no host was named, and the theme decides what they paint). */

.nstr {
	position: relative;
}

/* No host named: the strip paints itself. With a host (the theme's bar) the host wears the colours. */
.nstr--self-host {
	background-color: var(--nstr-bg);
	color: var(--nstr-ink);
	transition: background-color var(--nstr-duration, 400ms) ease, color var(--nstr-duration, 400ms) ease;
}

.nstr__items {
	display: grid; /* every message in one cell: the strip is as tall as its tallest message and never jumps */
	overflow-x: clip; /* a sideways slide must not spill past the strip... */
	overflow-clip-margin: 0.3em; /* ...but a focus ring may */
}

.nstr--multi .nstr__items {
	padding-inline: 2.25em; /* room for the toggle on both sides, so the text stays centred */
}

.nstr--no-toggle .nstr__items {
	padding-inline: 0;
}

.nstr__item {
	grid-area: 1 / 1;
	margin: 0;
}

/* The server prints items after the first `hidden`. Keeping them in the grid cell, invisible, gives the
   strip its tallest message's height from the first paint, so starting the rotation moves nothing. */
.nstr--multi .nstr__item[hidden] {
	display: block;
	visibility: hidden;
}

.nstr__link {
	color: inherit;
	text-underline-offset: 0.15em;
}

/* An icon sits in the text at about the height of a capital; a registered font names itself in a class
   and the theme's families are printed after this sheet. */
.nstr-icon {
	display: inline-block;
	width: 1.15em;
	height: 1.15em;
	margin-inline: 0.1em;
	vertical-align: -0.22em;
	line-height: 1;
}

.nstr-icon svg {
	display: block;
	width: 100%;
	height: 100%;
}

/* Rotation styles apply only once the script has run (it adds .nstr--live); before that the
   server's markup (first item visible, the rest hidden) is the whole behaviour.
   An item rests where it enters from (--nstr-in) and leaves to (--nstr-out); the script stamps
   data-fx on it with the effect of the message that is ENTERING, so both ends of a swap agree. The
   default, slide-up, is the 0.1.0 look: fade plus a small rise. */
.nstr--live .nstr__item {
	--nstr-in: translateY(var(--nstr-shift, 0.4em));
	--nstr-out: translateY(calc(var(--nstr-shift, 0.4em) * -1));
	opacity: 0;
	visibility: hidden;
	transform: var(--nstr-in);
	transition:
		opacity var(--nstr-duration, 400ms) ease,
		transform var(--nstr-duration, 400ms) ease,
		visibility 0s linear var(--nstr-duration, 400ms);
}

.nstr--live .nstr__item[data-fx="slide-down"] {
	--nstr-in: translateY(calc(var(--nstr-shift, 0.4em) * -1));
	--nstr-out: translateY(var(--nstr-shift, 0.4em));
}

.nstr--live .nstr__item[data-fx="slide-left"] {
	--nstr-in: translateX(var(--nstr-shift-x, 1.2em));
	--nstr-out: translateX(calc(var(--nstr-shift-x, 1.2em) * -1));
}

.nstr--live .nstr__item[data-fx="slide-right"] {
	--nstr-in: translateX(calc(var(--nstr-shift-x, 1.2em) * -1));
	--nstr-out: translateX(var(--nstr-shift-x, 1.2em));
}

/* A stadium board: the whole width, no fade, and both items on one easing so they travel as one ribbon.
   100% is the item's own width, which is the cell's. At rest the next message waits off the right edge,
   and visibility (not opacity) is what hides it. */
.nstr--live .nstr__item[data-fx="ticker"] {
	--nstr-in: translateX(100%);
	--nstr-out: translateX(-100%);
	opacity: 1;
	transition-timing-function: ease-in-out;
}

/* The gutter is the toggle's room, so the ribbon must not run under it: clip at the text's own box (less
   the focus ring's margin). :has() because the effect is per message; it applies only while a ticker is in play. */
.nstr--multi .nstr__items:has(> [data-fx="ticker"]) {
	clip-path: inset(-0.3em calc(2.25em - 0.3em));
}

.nstr--no-toggle .nstr__items:has(> [data-fx="ticker"]) {
	clip-path: none;
}

/* Crawl (display, not a transition): the script moves every item into one .nstr__rail and runs it right to
   left; --nstr-crawl-* are its numbers. The edges fade, so letters appear on the right and dissolve on the left
   (--nstr-crawl-edge). --nstr-crawl-gap is the space after each message. The block padding is the focus ring's
   room inside the mask, which hides anything outside the box. */
.nstr--crawl .nstr__items {
	display: block;
	contain: inline-size; /* the row is far wider than the strip; it must not size the box it runs in */
	padding: 0.3em 0;
	margin-block: -0.3em;
	-webkit-mask-image: linear-gradient(90deg, transparent, #000 var(--nstr-crawl-edge, 3em), #000 calc(100% - var(--nstr-crawl-edge, 3em)), transparent);
	mask-image: linear-gradient(90deg, transparent, #000 var(--nstr-crawl-edge, 3em), #000 calc(100% - var(--nstr-crawl-edge, 3em)), transparent);
}

/* The toggle sits over the right of the strip; the row stops short of it. */
.nstr--crawl.nstr--multi:not(.nstr--no-toggle) .nstr__items {
	margin-inline-end: 2.25em;
}

.nstr__rail {
	position: relative; /* the items' offsetParent, for placing a focused one */
	display: flex;
	width: max-content;
	animation: nstr-crawl var(--nstr-crawl-time, 30s) linear var(--nstr-crawl-delay, 0s) var(--nstr-crawl-count, infinite) both;
}

.nstr--crawl .nstr__item {
	flex: none;
	white-space: nowrap;
	padding-inline-end: var(--nstr-crawl-gap, 6em);
}

.nstr--crawl.is-held .nstr__rail {
	animation-play-state: paused;
}

.nstr--crawl.is-placed .nstr__rail {
	animation: none;
	transform: translateX(var(--nstr-crawl-at, 0px));
}

@keyframes nstr-crawl {
	from { transform: translateX(var(--nstr-crawl-from, 0px)); }
	to { transform: translateX(var(--nstr-crawl-to, -100%)); }
}

.nstr--live .nstr__item[data-fx="zoom"] {
	--nstr-in: scale(0.88);
	--nstr-out: scale(1.12);
}

.nstr--live .nstr__item[data-fx="fade"] {
	--nstr-in: none;
	--nstr-out: none;
}

.nstr--live .nstr__item[data-fx="none"] {
	--nstr-in: none;
	--nstr-out: none;
	transition: none;
}

.nstr--live .nstr__item.is-active {
	opacity: 1;
	visibility: visible;
	transform: none;
	transition-delay: 0s;
}

.nstr--live .nstr__item.is-leaving {
	transform: var(--nstr-out);
}

.nstr__toggle {
	position: absolute;
	inset-block: 0;
	inset-inline-end: 0;
	display: flex;
	align-items: center;
	justify-content: center;
	width: 2.25em;
	margin: 0;
	padding: 0;
	border: 0;
	border-radius: 0;
	background: none;
	color: inherit;
	font: inherit;
	line-height: 1;
	cursor: pointer;
}

.nstr__toggle[hidden] {
	display: none;
}

.nstr__toggle:focus-visible {
	outline: 2px solid currentColor;
	outline-offset: -2px;
}

.nstr__icon {
	width: 1em;
	height: 1em;
}

.nstr__icon--play,
.nstr__toggle.is-paused .nstr__icon--pause {
	display: none;
}

.nstr__toggle.is-paused .nstr__icon--play {
	display: block;
}

/* Reduced motion still rotates (the pause button is the stop) but whatever the effect, only fades, and quickly.
   "none" is an instant swap and stays one. */
@media (prefers-reduced-motion: reduce) {
	.nstr--live .nstr__item:not([data-fx="none"]),
	.nstr--live .nstr__item:not([data-fx="none"]).is-leaving {
		transform: none;
		transition-duration: 200ms, 200ms, 0s;
		transition-delay: 0s, 0s, 200ms;
	}

	/* The ticker has no fade of its own to fall back on: without this, both items sit at full opacity for the 200 ms. */
	.nstr--live .nstr__item[data-fx="ticker"]:not(.is-active) {
		opacity: 0;
	}

	.nstr--live .nstr__item.is-active {
		transition-delay: 0s;
	}

	.nstr--self-host {
		transition-duration: 200ms;
	}
}
