/*
 * Softex UI Kit v1.0 — design tokens.
 *
 * theme.json owns everything the block editor needs to know about (palette,
 * font sizes, spacing, layout width). This file carries the rest of the kit —
 * radii, shadows, transitions, control heights — as custom properties, plus
 * the component rules that WordPress has no setting for.
 *
 * Values here are the kit's, verbatim. Nothing should be tuned by eye: if a
 * number needs to change, change it in the kit first.
 */

:root {
	/*
	 * Brand.
	 *
	 * The resting red used to be #f22f2a. White text on it measures 4.03:1 —
	 * under the 4.5:1 normal-size text needs — and that one value was painting
	 * 38 filled controls across the site: every primary button, the filter
	 * chips, the "Рекомендована" badge, the "Популярний" ribbon. Stepping the
	 * whole ramp one shade down puts white text at 4.60:1 while staying close
	 * enough that the two reds do not read as different colours.
	 *
	 * The logo file still carries the original #f22f2a. It is a mark, not a
	 * control, and nothing places it directly against a button.
	 */
	--sx-brand: #e32621;
	--sx-brand-hover: #c91d19;
	--sx-brand-active: #b01714;
	--sx-brand-50: #fff2f1;
	--sx-brand-100: #ffe2e0;

	/* The hover border on cards. It was written by hand as #ffd0cd in nine
	   places across six files, which meant a change to the brand red would
	   have left this tint behind in all of them. */
	--sx-brand-200: #ffd0cd;

	/*
	 * The brand red when it is a letterform rather than a surface.
	 *
	 * #f22f2a measures 4.03:1 on white — under the 4.5:1 that normal-size text
	 * has to clear, so every red label, link and arrow on the site was failing.
	 * The darker step clears it at 5.72:1 on white and 5.23:1 on the brand tint,
	 * and at the sizes this colour is used the two are hard to tell apart.
	 *
	 * Separate token rather than a global redefinition: the red keeps its own
	 * value everywhere it is a fill, a border or a rule, where the ratio that
	 * matters is the one against whatever sits on top of it.
	 */
	--sx-brand-text: #c91d19;

	/* Pre-kit alias. products.css, pricing-plans.css and faq.css were written
	   against this name with #e30613 baked in, which is a different red from
	   the kit's. Aliasing it here keeps those files on the kit without having
	   to rewrite every rule that reads it. */
	--softextheme-brand-red: var( --sx-brand );

	/* Neutral */
	--sx-neutral-950: #111318;
	--sx-neutral-900: #181a20;
	--sx-neutral-800: #252830;
	--sx-neutral-700: #3f424a;
	--sx-neutral-600: #5e626c;
	--sx-neutral-500: #696d78;
	--sx-neutral-400: #9ca0a8;
	--sx-neutral-300: #d5d8de;
	--sx-neutral-200: #e7e9ed;
	--sx-neutral-100: #f1f2f4;
	--sx-neutral-50: #f6f7f9;
	--sx-white: #fff;

	/* Semantic */
	--sx-text: var( --sx-neutral-950 );
	--sx-text-secondary: var( --sx-neutral-600 );
	--sx-text-muted: var( --sx-neutral-500 );
	--sx-bg: var( --sx-white );
	--sx-surface: var( --sx-neutral-50 );
	--sx-border: var( --sx-neutral-200 );
	--sx-border-strong: var( --sx-neutral-300 );
	--sx-danger: #d92d20;
	--sx-icon-disabled: #a7abb3;

	/* The other half of the pair. There was a --sx-danger but no counterpart, so
	   the "message sent" and "in stock" states were written with a green and two
	   tints of it typed straight into contacts.css and shop.css — three files
	   away from each other and from any definition. Green is a state, not an
	   accent: it says something happened, and it never appears anywhere else. */
	--sx-success: #1a7f37;
	--sx-success-50: #f2faf5;
	--sx-success-100: #cfe8d6;

	/*
	 * ---- The heading scale ----
	 *
	 * Three sizes, and every heading on the site is one of them. Written down
	 * here because they had drifted: theme.json still declared h1 as a fixed
	 * 4rem from when page titles were 64px, so any heading the theme did not
	 * catch by name — the privacy policy's, the homepage hero's — came out a
	 * third larger than the one on the page beside it.
	 *
	 *   --sx-title-page     26→34px  the title of a page. h1. Every page.
	 *   --sx-title-section  19→24px  a section heading inside a page. h2.
	 *
	 * There is no third size and no page-level exception. There used to be:
	 * the title was 48px and the two heroes whose titles are long — a product
	 * named in full, a case whose title is a sentence — sat a step below it at
	 * 34. Setting the whole site to 34 removes the exception instead of
	 * documenting it, and the section size came down with it so that a
	 * heading still reads as smaller than the title above it.
	 *
	 * theme.json carries the same three numbers as literals, so a heading that
	 * no rule here reaches still lands on the scale. Change one, change both.
	 */

	/*
	 * The size of a section heading, everywhere on the site.
	 *
	 * These headings used to carry the theme's "H2 48" preset, which was set
	 * when page titles were 64px. Page titles are smaller now, and a 48px
	 * section heading read louder than the title above it. One number here so
	 * the whole site steps together — the preset stays for anything that
	 * genuinely needs a 48px statement.
	 */
	--sx-title-section: clamp( 1.1875rem, 1rem + 0.6vw, 1.5rem );

	/*
	 * The title of a page — every page on the site, with nothing exempt.
	 * Written once because the two that existed had drifted apart: one was
	 * 54px at weight 800, the other 48px at 600, and side by side they read
	 * as two different typefaces.
	 */
	--sx-title-page: clamp( 1.6rem, 1.1rem + 1.5vw, 2.125rem );

	/* Radius */
	--sx-radius-sm: 8px;
	--sx-radius-btn: 9px;
	--sx-radius-md: 12px;
	--sx-radius-lg: 16px;
	--sx-radius-xl: 24px;

	/* Shadows */
	--sx-shadow-card: 0 10px 40px rgba( 20, 25, 35, 0.06 );
	--sx-shadow-card-hover: 0 12px 32px rgba( 20, 25, 35, 0.08 );
	--sx-shadow-lift: 0 16px 40px rgba( 20, 25, 35, 0.08 );
	--sx-shadow-brand: 0 8px 20px rgba( 242, 47, 42, 0.18 );
	--sx-shadow-screenshot: 0 20px 50px rgba( 20, 25, 35, 0.12 );

	/*
	 * Stacking order.
	 *
	 * These four are the only layers that have to agree with each other across
	 * files: the sticky product nav has to sit under the header, the scrim has
	 * to cover the page but not the header it belongs to, and the lightbox
	 * covers all three. They were written as 20 / 240 / 250 / 999 in four
	 * separate files, where nothing said which was meant to win.
	 *
	 * Values of 1-3 inside a component are deliberately left alone — they are
	 * local to their own stacking context and mean nothing outside it.
	 */
	--sx-z-sticky: 20;
	--sx-z-scrim: 240;
	--sx-z-header: 250;
	--sx-z-overlay: 999;

	/* Motion */
	--sx-transition-fast: 200ms;
	--sx-transition-base: 300ms;
	--sx-transition-slow: 600ms;
	--sx-ease: cubic-bezier( 0.2, 0.8, 0.2, 1 );
	--sx-ease-reveal: cubic-bezier( 0.22, 1, 0.36, 1 );

	/* Controls */
	--sx-control-height: 48px;
	--sx-control-height-lg: 54px;
	--sx-header-height: 76px;
	--sx-header-height-mobile: 64px;

	/*
	 * How much of the top of the window is already taken.
	 *
	 * On a phone the header is an island at the *bottom*, so only the WordPress
	 * admin bar counts. From 1200px the header sticks to the top and has to be
	 * added — see the override below.
	 *
	 * Everything that sticks to the top reads this instead of carrying its own
	 * guess: before, one sticky sat at 0, another at 88px and a third at 96px,
	 * and none of them knew how tall the header actually was.
	 *
	 * --sx-header-h is measured by header-nav.js on load and on resize; the
	 * value here is only what applies until the script runs.
	 */
	--sx-header-h: 84px;
	--sx-admin-bar: 0px;
	--sx-sticky-top: var( --sx-admin-bar );

	/* Layout */
	--sx-container: 1320px;

	/* Backgrounds from the kit */
	--sx-gradient-surface: linear-gradient( 135deg, #fff 0%, #f1f3f6 100% );
	--sx-gradient-hero: linear-gradient( 135deg, #fff 0%, #f6f7f9 55%, #edeff3 100% );
	--sx-glow-brand: radial-gradient( circle, rgba( 242, 47, 42, 0.12 ) 0%, rgba( 242, 47, 42, 0 ) 70% );

	/*
	 * The drifting ground under the hero and the closing call to action.
	 *
	 * Four stops rather than the three the static hero uses, because only the
	 * quarter of it that the window shows is on screen at any moment: with the
	 * background blown up to 400% and the position swept end to end, the panel
	 * reads a different part of this ramp as it goes, and three stops give it
	 * too little to find.
	 *
	 * The accent is the third stop and it is barely a colour — #ffeceb is white
	 * with the brand red breathed on it. Anything stronger and a page whose
	 * whole job is a headline in near-black on near-white acquires a coloured
	 * wash that moves, which is not an accent but a distraction; and the
	 * contrast under the hero copy has to hold at every point in the sweep, not
	 * just at the ends.
	 */
	--sx-gradient-drift: linear-gradient( 120deg, #ffffff 0%, #f2f4f8 32%, #ffeceb 62%, #eef0f4 100% );
}

/*
 * Slides the oversized gradient layer across and back.
 *
 * Out and home in one loop rather than 0 to 100 on repeat: a one-way sweep
 * jumps back to the start every eight seconds, and on a ground this pale the
 * jump is the only part anyone notices.
 *
 * The moving thing is a layer four times the width of its panel, shifted by
 * transform. The obvious way to write this — background-size: 400% with the
 * background-position animated — looks identical and costs a full repaint of
 * the panel on every frame, for as long as the page is open, because neither
 * background-size nor background-position can be handed to the compositor.
 * A transform can, so the browser paints the gradient once and then only moves
 * it. Same picture; the work stops after the first frame.
 *
 * -75% of a layer four panels wide is three panels of travel, which is exactly
 * the distance background-position 0% to 100% covers at 400%.
 */
@keyframes sx-gradient-drift {
	0%,
	100% {
		transform: translateX( 0 );
	}

	50% {
		transform: translateX( -75% );
	}
}

/* The container padding the kit specifies at 1440 and below. */
:root :where( body ) {
	--wp--style--root--padding-left: clamp( 20px, 4vw, 60px );
	--wp--style--root--padding-right: clamp( 20px, 4vw, 60px );
}

/* ---- Box model ----
   Widths and heights count padding and borders. Without this the two are added
   on top, and any element given both a size and padding comes out larger than
   the number says: it is what made every button 76px tall against a 48px
   control height, and what fitted seven client logos into a row sized for
   eight. Set once here rather than patched per component, which is how those
   two got past review in the first place.

   Declared on the root and inherited, rather than forced on every element, so
   a component that deliberately opts out takes its subtree with it — the menu
   group logo does exactly that, sizing itself content-box so its padding reads
   as a chip around a 26px mark instead of eating into it. */
html {
	box-sizing: border-box;
}

*,
*::before,
*::after {
	box-sizing: inherit;
}

/* ---- Breadcrumbs ----
   Shared: the article header and the blog header both show the same trail. */

.sx-crumbs {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 8px;
	margin-bottom: 24px;
	color: var( --sx-text-secondary );
	font-size: 0.8125rem;
}

.sx-crumbs a {
	color: var( --sx-text-secondary );
	text-decoration: none;
}

.sx-crumbs a:hover {
	color: var( --sx-brand-text );
}

.sx-crumbs span[aria-hidden] {
	color: var( --sx-border-strong );
}

/* The current page is not a link, so it needs to stop looking like the rest of
   the trail without shouting. */
.sx-crumbs__current {
	color: var( --sx-text );
}

/* ---- Eyebrow / label ----
   Set in the darker red rather than the brand red. At 12px in caps this is the
   smallest text on the site, and the brand red measures 4.03:1 on white — under
   the 4.5:1 the size demands. The active red measures 5.72:1 and is close
   enough that the two do not read as different colours. The brand red keeps its
   job as a fill, where the ratio is computed against the text on top of it. */

.sx-eyebrow,
.softextheme-product-eyebrow {
	font-size: 0.75rem;
	font-weight: 600;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var( --sx-brand-text );
}

/* ---- Buttons ----
   theme.json sets the colours and radius; the kit's height, motion and the
   secondary/text variants live here, since the editor has no setting for them. */

.wp-element-button,
.wp-block-button__link {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	/*
	 * border-box is what makes --sx-control-height mean the height of the
	 * button. Without it min-height applies to the content box only, and every
	 * <a> styled as a button came out at 48px plus its own padding — 76px,
	 * against 48px for a <button>, which browsers default to border-box. The
	 * two sat side by side on the careers page and did not match.
	 */
	box-sizing: border-box;
	min-height: var( --sx-control-height );
	transition: background-color var( --sx-transition-fast ) var( --sx-ease ),
		transform var( --sx-transition-fast ) var( --sx-ease ),
		box-shadow var( --sx-transition-fast ) var( --sx-ease );
}

.wp-element-button:hover,
.wp-block-button__link:hover {
	transform: translateY( -1px );
	box-shadow: var( --sx-shadow-brand );
}

/* A real press, not just the hover lift removed: the button goes a pixel below
   its resting position, which is what makes a tap feel like it landed. */
.wp-element-button:active,
.wp-block-button__link:active {
	transform: translateY( 1px );
	box-shadow: none;
}

.wp-element-button:disabled,
.wp-block-button__link:disabled {
	background: var( --sx-neutral-200 );
	color: var( --sx-neutral-400 );
	box-shadow: none;
	transform: none;
	cursor: not-allowed;
}

/* Secondary: outline on white. */
.is-style-outline > .wp-block-button__link,
.wp-block-button.is-style-outline .wp-block-button__link {
	background: transparent;
	border: 1px solid var( --sx-border-strong );
	color: var( --sx-text );
}

.is-style-outline > .wp-block-button__link:hover,
.wp-block-button.is-style-outline .wp-block-button__link:hover {
	background: var( --sx-surface );
	border-color: #bfc3ca;
	box-shadow: none;
}

.is-style-outline > .wp-block-button__link:active,
.wp-block-button.is-style-outline .wp-block-button__link:active {
	background: var( --sx-neutral-200 );
}

/* ---- Form controls ---- */

input[type="text"],
input[type="email"],
input[type="tel"],
input[type="url"],
input[type="number"],
input[type="search"],
select,
textarea {
	min-height: var( --sx-control-height );
	padding: 0 16px;
	border: 1px solid var( --sx-border-strong );
	border-radius: var( --sx-radius-btn );
	background: var( --sx-white );
	font-family: inherit;
	font-size: 0.9375rem;
	color: var( --sx-text );
	transition: border-color var( --sx-transition-fast ) var( --sx-ease ),
		box-shadow var( --sx-transition-fast ) var( --sx-ease );
}

textarea {
	min-height: 120px;
	padding: 14px 16px;
	resize: vertical;
}

input:focus,
select:focus,
textarea:focus {
	border-color: var( --sx-brand-text );
	box-shadow: 0 0 0 3px rgba( 242, 47, 42, 0.1 );
	outline: none;
}

/* ---- Filled fields ----
 *
 * The look the request forms use: no box, a tinted ground and a single line
 * under it. Written once here rather than three times, because the popup, the
 * contacts page and the CV form are one thing to a visitor and should not
 * drift apart.
 *
 * Not the site-wide default. A box is right for a control sitting in a table —
 * the quantity in the calculator, the search — where it has to read as a
 * control among other content. It is a form's own fields that do not need one,
 * because everything around them is already fields.
 *
 * The ground is what makes this work on white. Left borderless on a white
 * dialog there is nothing but a hairline to say where to write, which is thin
 * both to look at and against the requirement that a field's boundary be
 * visible.
 *
 * The border is kept at full width and made transparent rather than removed:
 * the invalid rule below colours a border, and there has to be one there for
 * it to colour. It also keeps the field the same size in every state.
 *
 * Rounded on all four corners. The first version rounded only the top, which
 * is the usual shape for a field with an underline — it reads as a sheet of
 * paper with a line ruled under it. On its own in a dialog it just looked like
 * a box someone had forgotten to finish.
 */
.sx-form--filled input,
.sx-form--filled select,
.sx-form--filled textarea {
	border: 1px solid transparent;
	border-radius: var( --sx-radius-btn );
	background: var( --sx-neutral-100 );
}

.sx-form--filled input:hover,
.sx-form--filled select:hover,
.sx-form--filled textarea:hover {
	background: var( --sx-neutral-200 );
}

/*
 * The same focus the rest of the site uses — brand outline and a soft glow —
 * rather than a second idea invented for these three forms.
 */
.sx-form--filled input:focus,
.sx-form--filled select:focus,
.sx-form--filled textarea:focus {
	border-color: var( --sx-brand-text );
	box-shadow: 0 0 0 3px rgba( 242, 47, 42, 0.1 );
	background: var( --sx-neutral-100 );
	outline: none;
}

input[aria-invalid="true"],
.sx-field--error input,
.sx-field--error textarea {
	border-color: var( --sx-danger );
}

.sx-field__error {
	font-size: 0.8125rem;
	color: var( --sx-danger );
}

input:disabled,
select:disabled,
textarea:disabled {
	background: var( --sx-neutral-100 );
	color: var( --sx-neutral-400 );
}

@media ( max-width: 767px ) {
	input[type="text"],
	input[type="email"],
	input[type="tel"],
	input[type="url"],
	input[type="number"],
	input[type="search"],
	select {
		min-height: 52px;
	}
}

/* ---- Badges ---- */

.sx-badge {
	display: inline-block;
	padding: 6px 10px;
	border-radius: 6px;
	background: var( --sx-brand-50 );
	color: var( --sx-brand-hover );
	font-size: 0.75rem;
	font-weight: 600;
}

.sx-badge--gray {
	background: var( --sx-neutral-100 );
	color: var( --sx-text-secondary );
}

/* ---- Dividers ---- */

hr,
.wp-block-separator {
	height: 1px;
	border: 0;
	background: var( --sx-border );
}

/* ---- Icons ----
 *
 * One rule for the whole site, so nothing has to be decided per section:
 *
 *   default   — Softex Red on the very light red tint
 *   secondary — Dark, for icons that sit in body text rather than a card
 *   disabled  — grey
 *
 * No blue, green, yellow or purple icons anywhere, and every icon inside a
 * given section shares one style and one colour. Product brand colours belong
 * on that product's own page, not in the shared UI — mixing a dozen vendor
 * palettes into the interface is exactly what breaks the 70/20/10 balance.
 *
 * One exception, and only this shape of it: a vendor's own mark may carry that
 * vendor's colours while the element it sits in is hovered or focused, never at
 * rest. The mobile operator marks on the contacts page work this way — grey in
 * the page, true colour under the pointer (contacts.css). The point of the rule
 * is what the page looks like when nobody is touching it; a colour that appears
 * only in passing does not set the page's palette. Devices without a pointer
 * keep the colours, since there is nothing there to reveal them.
 */

.sx-icon,
.sx-icon-box {
	color: var( --sx-brand-text );
}

.sx-icon--secondary {
	color: var( --sx-text );
}

.sx-icon--disabled,
.sx-icon-box--disabled {
	color: var( --sx-icon-disabled );
	background: var( --sx-neutral-100 );
}

.sx-icon-box {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 48px;
	height: 48px;
	border-radius: var( --sx-radius-md );
	background: var( --sx-brand-50 );
}

.sx-icon svg,
.sx-icon-box svg {
	width: 24px;
	height: 24px;
}

/* ---- Reduced motion ----
   The kit asks for animation to be cut, not merely shortened: anything that
   moves is reduced to a state change. */

@media ( prefers-reduced-motion: reduce ) {
	*,
	*::before,
	*::after {
		animation-duration: 0.01ms !important;
		animation-iteration-count: 1 !important;
		transition-duration: 0.01ms !important;
		scroll-behavior: auto !important;
	}

	.wp-element-button:hover,
	.wp-block-button__link:hover {
		transform: none;
	}
}

/* ---- Keyboard focus ----
   Seven of the sixteen stylesheets defined their own focus ring and the other
   nine had none, so tabbing through the cart, the price list or the tariff
   table lost the cursor entirely. One base rule here; the components that
   already style their own focus still win on specificity.

   Drawn in near-black rather than the brand red so it stays visible on white,
   on the grey surfaces and on the red buttons alike. :focus-visible, not
   :focus, so a mouse click does not leave a ring behind. */

:where( a, button, input, select, textarea, summary, [tabindex] ):focus-visible {
	outline: 2px solid var( --sx-text );
	outline-offset: 2px;
	border-radius: 2px;
}

/* ---- Jumping to an anchor ----
   Every in-page link on the site lands with a cut: the hero's "Дізнатися
   більше", the product pages' "Розрахувати вартість", the ITS service links
   the old site's redirects still point at (#cashalot, #fredo-zvit, #etapy).
   The page simply is somewhere else, and the reader has to work out where.
   Scrolling there instead keeps the thread.

   Set once, here, rather than per component: an anchor is an anchor wherever
   it is, and the alternative is remembering this in every stylesheet.

   Off for anyone who asked for less motion — a page that slides under you is
   exactly what that setting is for. Only the document scrolls this way;
   carousels and panels that script their own scrolling are untouched. */

@media ( prefers-reduced-motion: no-preference ) {
	html {
		scroll-behavior: smooth;
	}
}

/* ---- Headings ----
   Ukrainian words are long, so a heading left to wrap on its own regularly
   drops a single word onto the last line. balance evens the lines out; pretty
   only prevents the orphan, which is what body copy needs. */

h1,
h2,
h3,
.sx-page-title,
.sx-404__title {
	text-wrap: balance;

	/*
	 * The same long words, at display size, on the narrowest screens: at 320px
	 * "впроваджуємо" in the homepage heading is wider than the column it sits
	 * in, and a word has nowhere to break. It pushed the hero, and with it the
	 * whole page, 8px past the right edge — the horizontal scroll that had
	 * been blamed on the client logos and was never theirs.
	 *
	 * break-word, so it only gives way when the word genuinely cannot fit and
	 * ordinary wrapping is untouched everywhere else.
	 */
	overflow-wrap: break-word;
}

p,
li {
	text-wrap: pretty;
}

/* WordPress puts .admin-bar on <body> whenever the toolbar is showing, which is
   a plainer signal than its own custom property and does not need a fallback. */
.admin-bar {
	--sx-admin-bar: 32px;
}

@media ( max-width: 782px ) {
	.admin-bar {
		--sx-admin-bar: 46px;
	}
}

/* From the desktop breakpoint the header is pinned to the top of the window, so
   it joins the admin bar in the space that sticky elements have to clear. */
@media ( min-width: 1200px ) {
	:root {
		--sx-sticky-top: calc( var( --sx-admin-bar ) + var( --sx-header-h ) );
	}
}

/* ---- Розкриття ----
 *
 * One way to open and close a panel, shared by everything on the site that
 * folds: the price list's category sections, its filter drawer, and anything
 * added later.
 *
 * Height is animated with grid-template-rows going 0fr → 1fr rather than with
 * max-height. A max-height animation needs a number picked by hand, and the
 * number is always wrong: too small and long content is clipped, too large and
 * the panel appears to hang before it moves, because the transition spends its
 * first half travelling through empty space. The grid technique animates to
 * the content's real height, whatever it turns out to be.
 *
 * display: none cannot be transitioned at all, which is why the closed state
 * here is a class rather than the hidden attribute.
 *
 * Global prefers-reduced-motion above cuts every duration to nothing, so the
 * fold still works for anyone who has asked for less movement — it just
 * arrives at its end state immediately.
 */

.sx-fold {
	display: grid;
	grid-template-rows: 1fr;
	transition: grid-template-rows var( --sx-transition-base ) var( --sx-ease );
}

/* The child has to be allowed to be shorter than its content, and to clip it:
   without both, the row collapses while the content keeps its full height and
   simply overflows the closed panel. */
.sx-fold > * {
	min-height: 0;
	overflow: hidden;
}

.sx-fold.is-closed {
	grid-template-rows: 0fr;
}

/*
 * Something appearing in place — a card revealed by "показати ще", a row that
 * comes back after a filter is cleared.
 *
 * The first version of this moved 8px over 300ms and started every card at
 * once, and it read as no animation at all: the page grew to its new length in
 * one frame, and a fade that brief underneath a jump that size is not what the
 * eye reports. What makes it read as movement is the queue — cards arriving one
 * after another — so each is delayed by its position, set from the script as
 * --sx-reveal-delay. Six cards take about half a second to all be in.
 */
@keyframes sx-reveal {
	from {
		opacity: 0;
		transform: translateY( 18px ) scale( 0.985 );
	}
	to {
		opacity: 1;
		transform: none;
	}
}

.sx-revealing {
	animation: sx-reveal 420ms var( --sx-ease-reveal ) both;
	animation-delay: var( --sx-reveal-delay, 0ms );
}
