/**
 * Football Font Preview — frontend card.
 *
 * All product-specific values arrive as CSS custom properties injected
 * inline per product (see FFP_Frontend::build_inline_css). The values
 * below are only safety defaults.
 */

.ffp-preview {
	--ffp-font: inherit;
	--ffp-text-color: #1e1e1e;
	--ffp-outline: 0 transparent;
	--ffp-bg: #ffffff;
	--ffp-name-size: 42px;
	--ffp-number-size: 96px;
	--ffp-letter-spacing: 2px;
	--ffp-gap: 8px;
	--ffp-digit-height: 90px;
	--ffp-digit-gap: 8px;
	--ffp-letter-height: 56px;
	/* Reserved block heights: font size (font mode) or glyph height (SVG mode). */
	--ffp-number-block: var(--ffp-number-size);
	--ffp-name-block: var(--ffp-name-size);

	box-sizing: border-box;
	background: var(--ffp-bg);
	border: 1px solid rgba(0, 0, 0, 0.08);
	border-radius: 12px;
	padding: 24px 20px 28px;
	margin: 20px 0;
	text-align: center;
	/* Reserve vertical space up-front to minimize layout shift. */
	min-height: calc(var(--ffp-name-block) + var(--ffp-number-block) + var(--ffp-gap) + 72px);
	transition: opacity 0.15s ease;
	/* v2.22.34 — the preview is often INSERTED INTO an option plugin's flex
	   wrapper (WAPF renders its field groups as display:flex). WebKit sizes
	   such a flex item to min-content (≈0) and the whole shirt stage
	   collapses to 0×0, while Chromium stretches it to the full row — the
	   proven cause of the broken iPhone layout. An explicit full-width flex
	   basis makes every engine identical; harmless in plain block flow. */
	width: 100%;
	flex: 0 0 100%;
	min-width: 0;
	align-self: stretch;
}

.ffp-preview.ffp-hidden {
	display: none;
}

.ffp-preview__heading {
	/* Inherits the site's typography deliberately. */
	font-size: 0.85em;
	font-weight: 600;
	letter-spacing: 0.02em;
	opacity: 0.65;
	margin-bottom: 14px;
}

/* Names/numbers are Latin — always render the stage LTR, even on RTL pages. */
.ffp-preview__stage {
	direction: ltr;
	unicode-bidi: isolate;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	gap: var(--ffp-gap);
}

.ffp-preview__name,
.ffp-preview__number {
	/* Latin names/numbers stay LTR even inside an RTL (Hebrew) product page. */
	direction: ltr;
	unicode-bidi: isolate;
	font-family: var(--ffp-font);
	color: var(--ffp-text-color);
	-webkit-text-stroke: var(--ffp-outline);
	/* p5cmp160 — NO CSS case transform: the rendered string is already the
	 * policy-correct case (custom names uppercased in the pipeline; preset
	 * names case-preserved). A text-transform here would repaint preset
	 * lowercase with uppercase glyphs behind the resolver's back. */
	line-height: 1.05;
	white-space: nowrap;
	max-width: 100%;
	overflow: hidden;
	/* Informational only — not selectable/clickable. */
	user-select: none;
	pointer-events: none;
}

.ffp-preview__name {
	font-size: var(--ffp-name-size);
	letter-spacing: var(--ffp-letter-spacing);
}

.ffp-preview__number {
	font-size: var(--ffp-number-size);
	letter-spacing: normal;
}

.ffp-preview__name.ffp-empty,
.ffp-preview__number.ffp-empty {
	display: none;
}

/* SVG artwork mode — DIGITS ONLY (Phase 3: the SVG-letter name rules are
   removed; player names render via the WOFF2 webfont below). Digit glyphs
   share one height, keep their native aspect ratio, and sit on a common
   baseline (flex-end). The artwork is rendered as <img> and is NEVER
   recolored or modified. */
.ffp-preview__number.ffp-number--svg {
	display: flex;
	align-items: flex-end;
	justify-content: center;
	font-size: 0;
	line-height: 0;
}

.ffp-preview__number.ffp-number--svg {
	height: var(--ffp-digit-height);
}

/* p5cmp124 — SIGNED digit spacing. CSS flex `gap` rejects negative lengths
   (an invalid var() substitution silently degrades to 0), so the digit gap
   is applied as a leading margin on every digit after the first: identical
   geometry for gap >= 0, and a NEGATIVE gap genuinely overlaps digits.
   margin-inline-start = the flow-leading edge, correct in LTR and RTL. */
.ffp-preview__number.ffp-number--svg > .ffp-svg-digit + .ffp-svg-digit {
	margin-inline-start: var(--ffp-digit-gap);
}

.ffp-preview__number.ffp-number--svg.ffp-empty {
	display: none;
}

.ffp-svg-digit {
	display: block;
	height: 100%;
}

.ffp-svg-digit img {
	display: block;
	height: 100%;
	width: auto;
}

/* p5cmp115/116/118/120 — SVG NUMBER OUTLINE (vector layer, silhouette).
   Active only when the operator enabled it (.ffp-number-outline is toggled
   by the runtime). The VISIBLE digit <img> is NEVER filtered (a CSS filter
   forced mobile/high-DPR engines to rasterize the vector artwork into a
   fixed-resolution intermediate surface — p5cmp120); instead a separate
   vector layer (svg.ffp-noutl-layer, an id-namespaced sanitized copy of the
   SAME asset run through the closing+ring silhouette filter) is absolutely
   positioned BEHIND the untouched img inside each digit host. Only the
   digit's outer contour (and genuine silhouette holes, e.g. inside a 0) is
   outlined, while internal artwork (crests, logos, decorative dots) drawn on
   the opaque digit body creates no alpha boundary and stays exactly as
   uploaded. NO per-shape stroke exists (p5cmp116 removed it — it outlined
   internal paths like the AFA crest). The ring radii come from the shared
   image-relative spacing derivation, converted to viewBox units by JS.
   overflow:visible keeps thick outlines from clipping at digit/container
   edges; the shirt-stage clip (mockup "clip" setting) stays authoritative. */
.ffp-preview__number.ffp-number--svg.ffp-number-outline {
	overflow: visible;
}
.ffp-number-outline .ffp-svg-digit {
	overflow: visible;
}
/* Digit hosts become the layer's positioning context. z-index:0 creates a
   stacking context that CONTAINS the ordering: the layer stays at z:auto
   and the img is lifted to z:1, so the ring paints BEHIND the artwork
   without any negative z-index (the p5cmp50 offset-layer rule: negative z
   can sink below stage layers and is banned). .ffp-noutl-host wraps bare
   admin-editor digit imgs (the storefront's .ffp-svg-digit span is used
   as-is). */
.ffp-number-outline .ffp-svg-digit,
.ffp-number-outline .ffp-noutl-host {
	position: relative;
	z-index: 0;
}
.ffp-number-outline .ffp-noutl-host {
	display: block;
	font-size: 0;
	line-height: 0;
	overflow: visible;
}
.ffp-number-outline .ffp-svg-digit img,
.ffp-number-outline .ffp-noutl-host img {
	position: relative;
	z-index: 1;
}
.ffp-number-outline .ffp-noutl-layer {
	position: absolute;
	left: 0;
	top: 0;
	width: 100%;
	height: 100%;
	overflow: visible;
	pointer-events: none;
}

/* ===================================================================
   rc25 Webfont NAME renderer. Name letters render as real text glyphs from a
   loaded WOFF2 webfont (numbers stay SVG artwork). Structured appearance drives
   the --ffp-name-* custom properties (set on .ffp-preview from the Font Asset
   fields); the sanitized, asset-scoped Advanced CSS
   (.ffp-font-asset-<id> .ffp-name-glyph { ... }) layers visual overrides on top.
   The glyph engine positions/rotates the spans exactly like the SVG path, so
   the curve models and Maximum glyph rotation are identical.
   =================================================================== */
.ffp-preview__name.ffp-name--webfont {
	line-height: 0;
	max-width: 100%;
}
/* p5cmp84 — ABOVE-CAP INK MUST NOT BE CLIPPED (straight webfont names only).
   The base .ffp-preview__name rule sets overflow:hidden, and this rule pinned
   the box to exactly var(--ffp-letter-height) — which equals the stage's
   font-size (1em). A native accented capital legitimately paints ABOVE the
   cap line (e.g. Bayern Í: yMax 1947/2048 = 0.951em vs cap 0.744em), so the
   top of the real WOFF2 glyph was cut off — the accent looked thin/detached.
   FIX: release the clip and let the box grow only if it ever needs to, while
   min-height keeps the ORIGINAL positioning anchor. Measured: box height,
   bottom anchor and glyph baseline are byte-identical to before (200/225px
   at a 200px letter height) — the ink simply extends upward now.
   Scope: straight WEBFONT names ONLY. Curved names (overflow:visible;
   height:auto below), SVG digits, numbers, the shirt stage and the overall
   preview boundaries are untouched. */
.ffp-preview__name.ffp-name--webfont.ffp-font-name--straight {
	display: flex;
	align-items: flex-end;
	justify-content: center;
	overflow: visible;
	height: auto;
	min-height: var(--ffp-letter-height);
}
/* The ink layers inside the straight webfont name must not re-clip it. */
.ffp-preview__name.ffp-name--webfont.ffp-font-name--straight .ffp-font-name-stage,
.ffp-preview__name.ffp-name--webfont.ffp-font-name--straight .ffp-name-glyph,
.ffp-preview__name.ffp-name--webfont.ffp-font-name--straight .ffp-name-glyph__front,
.ffp-preview__name.ffp-name--webfont.ffp-font-name--straight .ffp-name-glyph__offset {
	overflow: visible;
}
/* p5cmp131 — CURVED CENTERING MUST SURVIVE OVERFLOW. The curved stage is
   sized to the engine's FINAL rotated-ink bounds (+ symmetric padding), so
   centering the stage box IS centering the final rendered name. The old
   block + `margin: 0 auto` construction breaks exactly when the stage is
   WIDER than the overlay box (the placement's max-width%): CSS resolves
   auto margins to 0 under negative free space, the stage goes flush LEFT
   and overflows RIGHT — the name reads visually right of the chosen
   center axis by (stageWidth − overlayWidth)/2 (measured live: 26.7px on
   LAMINE YAMAL). Flex `justify-content: center` overflows SYMMETRICALLY
   (same construction the straight mode already uses), so the true ink
   midpoint stays on the axis at any name length. */
.ffp-preview__name.ffp-name--webfont.ffp-font-name--curved {
	display: flex;
	justify-content: center;
	height: auto;
	overflow: visible;
}
.ffp-name--webfont .ffp-font-name-stage {
	direction: ltr;
	/* rc28: font-size is the letter height with NO scale factor — webfont
	   glyphs are never resized to fit, so their metrics stay the font's own. */
	font-size: var(--ffp-letter-height);
}
.ffp-name--webfont.ffp-font-name--straight .ffp-font-name-stage {
	display: flex;
	align-items: flex-end;
	justify-content: center;
	line-height: 1;
}
.ffp-name--webfont.ffp-font-name--curved .ffp-font-name-stage {
	display: block;
	position: relative;
	/* p5cmp131: the stage width IS the engine's ink-bounds width — flex must
	   never shrink it to fit (the absolutely-positioned glyphs inside would
	   not shrink with it, silently re-introducing the off-center overflow). */
	flex: 0 0 auto;
	margin: 0 auto;
}

/* The rendered name glyph. rc28: PRESERVE the font's natural metrics — no
   forced width, no horizontal scaling, no synthetic bold/italic. Base visual
   styling reads the structured appearance vars; each is overridable per Font
   Asset via the sanitized Advanced CSS. */
.ffp-name-glyph {
	/* The webfont family, or inherit when unset. NOTE: `inherit` is a CSS-wide
	   keyword that is INVALID inside a comma-separated font-family list — writing
	   `var(--x), inherit` drops the whole declaration and the glyph silently
	   renders in the page's fallback font (looked like "wrong glyph shapes").
	   Keep the fallback INSIDE var() so the value is always valid. */
	font-family: var(--ffp-name-font-family, inherit);
	color: var(--ffp-name-fill, #FFFFFF);
	-webkit-text-stroke: var(--ffp-name-outline-width, 0) var(--ffp-name-outline, #000000);
	text-shadow: var(--ffp-name-shadow, none);
	opacity: var(--ffp-name-opacity, 1);
	font-weight: var(--ffp-name-weight, 400);
	font-style: var(--ffp-name-style, normal);
	/* Keep the exact uploaded face: never let the browser synthesize bold or
	   italic (that distorts the glyph shapes), never condense/expand. */
	font-stretch: normal;
	font-synthesis: none;
	letter-spacing: 0;
	width: auto;
	min-width: 0;
	max-width: none;
	/* Stroke behind the fill so the outline never eats into the glyph. */
	paint-order: stroke fill;
	white-space: pre;
	transform-origin: center bottom;
}
.ffp-name--webfont.ffp-font-name--straight .ffp-name-glyph {
	/* Straight neutral: natural inline flow, natural advance, no transform. */
	display: inline-block;
	position: static;
	width: auto;
	transform: none;
	margin-inline-start: var(--ffp-letter-spacing, 0px);
}
.ffp-font-name--straight .ffp-font-name-stage > .ffp-name-glyph:first-child,
.ffp-font-name--straight .ffp-font-space + .ffp-name-glyph {
	margin-inline-start: 0;
}
.ffp-name--webfont.ffp-font-name--curved .ffp-name-glyph {
	/* Curved: position + rotate ONLY. width:auto keeps the natural advance; no
	   text-align box, no scale — the glyph ink is exactly the font's own. */
	position: absolute;
	display: inline-block;
	width: auto;
	transform-origin: center bottom;
}
.ffp-font-space {
	display: inline-block;
	width: var(--ffp-word-space, calc(var(--ffp-letter-height) * 0.35));
	height: 1px;
}
.ffp-name--webfont.ffp-font-name--curved .ffp-font-space {
	display: none;
}

/* Shirt-image mockup: the image defines the canvas; the name/number lines
   the glyph engine already drives are wrapped in absolutely positioned
   overlay layers using normalized percentage coordinates. No admin editor
   controls, handles or outlines exist in this stylesheet — customer-facing
   markup stays clean. */
.ffp-preview--shirt {
	min-height: 0;
}

/* Configurable preview frame (v2.1.3): width/max-width/height/padding and
   alignment come from resolved settings via custom properties — nothing is
   hard-coded. Defaults reproduce the legacy full-width auto-height look. */
.ffp-preview-frame {
	box-sizing: border-box;
	width: var(--ffp-frame-width, 100%);
	max-width: var(--ffp-frame-max-width, none);
	padding: var(--ffp-frame-padding, 0);
}

.ffp-preview-frame--fixed {
	height: var(--ffp-frame-height, auto);
	display: flex;
	align-items: center;
	justify-content: center;
	overflow: hidden;
}

.ffp-shirt-stage {
	position: relative;
	width: 100%;
	margin: 0 auto;
	direction: ltr;
}

.ffp-shirt-stage--clip {
	overflow: hidden;
}

.ffp-shirt-stage__image {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: contain;
	pointer-events: none;
	user-select: none;
}

.ffp-shirt-stage__name,
.ffp-shirt-stage__number {
	position: absolute;
}

/* Overlay layers hide independently of the stage. visibility (not display)
   keeps glyph geometry measurable and prevents any layout shift when the
   personalization state changes. */
.ffp-shirt-stage__name.ffp-layer-hidden,
.ffp-shirt-stage__number.ffp-layer-hidden {
	visibility: hidden;
}

/* Overlays own their sizing via the stage-derived CSS vars; the inner
   name/number elements keep every existing behavior (curve, warp, fit). */
.ffp-shirt-stage__name .ffp-preview__name,
.ffp-shirt-stage__number .ffp-preview__number {
	max-width: 100%;
}

.ffp-shirt-stage__number .ffp-preview__number.ffp-number--svg {
	justify-content: center;
}

@media (max-width: 768px) {
	.ffp-preview {
		padding: 18px 14px 22px;
		border-radius: 10px;
	}
}

/* p5cmp104 — the storefront conversion control (.ffp-convert-responsive /
   .ffp-convert-panel) is REMOVED; conversion lives on the Responsive Layouts
   admin page. */

/* p5cmp78 — the composite/synthetic-accent system is REMOVED. Every name
   glyph is plain text (the exact WOFF2 character or its base letter); there
   are no mark elements, no composite containers, no accent CSS. */
/* p5cmp7 TEXT-RENDERING HARDENING: iOS Safari font boosting (text autosizing)
   can inflate a text-only span → visibly wider/heavier letters. Pin
   text-size-adjust on every glyph so none is ever boosted. */
.ffp-name-glyph,
.ffp-preview__name {
	-webkit-text-size-adjust: 100%;
	text-size-adjust: 100%;
}
/* p5cmp50 — OFFSET LAYER / HARD EXTRUSION via TWO EXPLICIT LAYERS (no negative z-index,
   no ::before). z-index:-1 proved unreliable even with isolation:isolate — a negative-z
   pseudo can still paint behind the local stacking context / an ancestor and vanish, so
   the rear layer stayed invisible even at a 1em offset. Each name glyph now carries two
   REAL child spans built by renderFontNameInto:
     .ffp-name-glyph__front  — the foreground letter (full name appearance; z-index:1 when
                               the effect is on).
     .ffp-name-glyph__offset — the rear duplicate, shown ONLY when the effect is enabled
                               (display:none otherwise), absolutely overlaid on the glyph,
                               em-translated, recoloured; z-index:0.
   BOTH z-indexes are >= 0, so the rear ALWAYS paints ABOVE the shirt/stage and BELOW the
   foreground — it can never fall behind the stage. It is the actual font glyph (not a
   text-shadow / box-shadow / raster filter), so hollow counters stay hollow and blur is
   exactly zero. X/Y/colour/opacity/scale are CSS custom properties, so a placement-only
   nudge changes a variable — never the glyph DOM or FontFace. The outer .ffp-name-glyph
   keeps the CAP-ARC transform and is the ONLY measured unit; __front flows inline so the
   glyph advance is IDENTICAL to the old single-text glyph → measurement is unchanged
   whether the effect is on or off, and both inner layers inherit the glyph's final
   transform (the arc is never recalculated). */

/* The glyph must be the containing block for the absolute rear layer. Curved glyphs
   already are (arc transform); a plain
   STRAIGHT glyph is position:static, so give it position:relative via a COMPOUND selector
   (ffp-has-offset + ffp-font-name--straight are on the SAME name element → 0,3,0, wins the
   3-class straight rule by source order). */
.ffp-has-offset.ffp-font-name--straight .ffp-name-glyph { position: relative; }

/* Foreground letter. -webkit-text-stroke does NOT inherit, so re-apply the name outline
   here (fill colour, shadow, font, paint-order all inherit from the glyph). Under the
   effect it is relatively positioned with z-index:1 so it always paints ABOVE the rear
   duplicate, without leaving the inline flow (glyph advance unchanged). */
.ffp-name-glyph__front {
	color: inherit;
	-webkit-text-stroke: var(--ffp-name-outline-width, 0) var(--ffp-name-outline, #000000);
	paint-order: stroke fill;
}
/* p5cmp55 — Y-ORIGIN FIX (the ONLY change vs p5cmp51; NO grid, NO layout redesign). Under
   the effect, `__front: inline-block` gives the foreground its own block box so its text
   paints from the same top:0 as the absolute rear → Y=0 aligns EXACTLY. As plain `inline`
   (p5cmp51) __front sat on the font BASELINE, offset from the glyph top by the half-leading,
   while the rear block paints from top:0 — so they disagreed by ~half-leading (~5px at 80px)
   at Y=0. Browser-proven: X0/Y0 → 0.0px on every glyph; X0.10em → 8px horizontal / 0
   vertical; ±Y → ±8px vertical; and the glyph advance + stage width stay byte-identical to a
   non-offset name (inline-block preserves the shrink-to-fit width AND the inline baseline).
   Scoped to .ffp-has-offset so non-offset names remain inline (unchanged). */
.ffp-has-offset .ffp-name-glyph__front { display: inline-block; position: relative; z-index: 1; }

/* Rear duplicate. The spans always exist in the DOM, so hide it unless the effect is on. */
.ffp-name-glyph__offset { display: none; }
.ffp-has-offset .ffp-name-glyph__offset {
	display: block;
	position: absolute;
	left: 0;
	top: 0;
	z-index: 0;                          /* above the stage, below the foreground */
	color: var(--ffp-offset-color, #C37552);
	opacity: var(--ffp-offset-opacity, 1);
	transform: translate(var(--ffp-offset-x, 0.035em), var(--ffp-offset-y, 0.025em)) scale(var(--ffp-offset-scale, 1));
	transform-origin: inherit;
	font: inherit;
	line-height: inherit;
	letter-spacing: inherit;
	white-space: pre;
	pointer-events: none;
	text-shadow: none;
	filter: none;                       /* blur = 0, always */
	-webkit-text-stroke: 0 transparent; /* fill-only by default (duplicate_outline off) */
}
/* duplicate_outline = also stroke the rear layer with the offset colour (default: fill only). */
.ffp-has-offset.ffp-offset-outline .ffp-name-glyph__offset {
	-webkit-text-stroke: var(--ffp-name-outline-width, 0) var(--ffp-offset-color, #C37552);
	paint-order: stroke fill;
}
/* SVG digit rear layer: a duplicated glyph <g> recoloured behind the foreground. Its
   proportional translate is computed in user units by the builder (SVG has no em). */
.ffp-offset-digit { opacity: var(--ffp-offset-opacity, 1); }

/* =====================================================================
   p5cmp175 — SECOND NAME OUTLINE (dual outline, UNIFIED renderer).
   Painting order back-to-front: outline 2 → outline 1 → text fill.
   With the second outline ON, the classic attached -webkit-text-stroke
   CANNOT paint outline 1: HTML text and SVG text rasterize ~0.5px apart
   (hinting) even at the same fractional position, so mixed layers never
   register (live-proven, p5cmp175). Instead ONE svg INSIDE each glyph
   renders outline2 → outline1 → fill as three <text> layers of the SAME
   character at the SAME fractional baseline in ONE coordinate system —
   visible thickness = exactly the UI widths (W1, W2). The DOM text keeps
   layout and accessibility but paints nothing. Second outline OFF keeps
   today's single-outline rendering verbatim (class off, svgs removed). */
/* The glyph is the svg's containing block. The straight layout rule
   (.ffp-name--webfont.ffp-font-name--straight .ffp-name-glyph, 0-3-1)
   pins position:static, so the compound selector must win there — same
   precedent as .ffp-has-offset (p5cmp50): both classes sit on the SAME
   name element. Curved glyphs stay absolutely positioned. */
.ffp-name-ring-on .ffp-name-glyph { position: relative; }
.ffp-name-ring-on.ffp-font-name--straight .ffp-name-glyph { position: relative; }
.ffp-name-ring-on.ffp-font-name--curved .ffp-name-glyph { position: absolute; }
/* Dual active → the DOM text must not paint AT ALL (its fill/stroke are
   re-rendered inside the svg by the same renderer that draws the rings).
   !important is deliberate and safe (p5cmp176): the asset-scoped Advanced
   name CSS is allowed to declare color / -webkit-text-stroke(-width/-color)
   / -webkit-text-fill-color / paint-order, is injected AFTER this sheet at
   the same specificity — and its value sanitizer can never emit `!`
   (character allowlist), so these declarations win over ANY stored
   Advanced CSS at display time without touching the stored CSS itself. */
.ffp-name-ring-on .ffp-name-glyph,
.ffp-name-ring-on .ffp-name-glyph__front {
	color: transparent !important;
	-webkit-text-fill-color: transparent !important;
	-webkit-text-stroke: 0 transparent !important;
}
.ffp-name-ring-on .ffp-name-glyph svg.ffp-name-ring {
	position: absolute;
	left: 0;
	top: 0;
	overflow: visible; /* both outlines extend beyond the glyph box */
	pointer-events: none;
}


/* =====================================================================
   p5cmp207 — REVEAL STATE MACHINE (PRESENTATIONAL ONLY).

   Every rule is scoped behind a root class that only the storefront
   animator adds (ffp-rv-exit / ffp-rv-name / ffp-rv-num). frontend.css is
   also loaded by the admin, so without the animator this whole block is
   inert — no calibration jitter, nothing to opt out of.

   ANIMATED SURFACES — unchanged from p5cmp206, and for the same reasons:
   - .ffp-name-glyph        opacity + filter ONLY. Its transform is the
                            CAP-ARC placement (translate+rotate written
                            inline by layoutFontNameDom) and is NEVER
                            touched, so straight and curved names use one
                            identical code path.
   - .ffp-name-glyph > *    the `scale` PROPERTY, never the transform
                            shorthand. __front and the ring svg carry no
                            transform; __offset carries the offset effect
                            own translate+scale, which `scale` multiplies
                            rather than replaces.
   - .ffp-svg-digit         opacity + filter + transform. Nothing writes a
                            transform there, and the outline layer is its
                            CHILD so artwork and outline move together.

   THE REFERENCE EXIT USES transform:scale() ON THE GLYPH ITSELF. We cannot:
   that channel is the arc. Our exit therefore mirrors the enter across the
   SAME three safe surfaces.

   TWO LADDERS
   The forward index comes from :nth-child, the reverse from
   :nth-last-child. Both are stylesheet rules, so the cssText reset that
   clearFontGlyphStyles() runs at the top of EVERY layout pass cannot reach
   them. Only the digit COUNT crosses over inline, on the preview root, so
   the letters can continue the reverse sequence the digits started.

   NAME -> NUMBER HANDOFF
   There is no delay offset. ENTERING_NUMBER is a SEPARATE PHASE the
   animator starts only after the last name glyph animationend, so the
   number physically cannot begin early. Until then the digits are marked
   but pinned invisible by the ffp-rv-name rule below.
   ================================================================== */
.ffp-preview {
	--ffp-rv-duration: 150ms;
	--ffp-rv-step: 40ms;
	/* p5cmp212 — THE NUMBER'S OWN EXIT STEP. A deliberate divergence from the
	 * reference, taken on measured evidence: the machinery was proven correct
	 * (the delayed digit holds opacity 1 / blur 0 / scale 1 for its whole
	 * delay), but 40ms is only ~2.4 frames at 60Hz, and when the first digit
	 * finally starts the second is still at opacity 0.599 — so 79% of a
	 * two-digit exit has both digits fading at once and reads as simultaneous.
	 * A name spreads 40ms over many letters and does not have this problem, so
	 * this variable exists ONLY for the discrete number exit: every enter, the
	 * name exit and custom typing all still use --ffp-rv-step. */
	--ffp-rv-digit-exit-step: 80ms;
	--ffp-rv-easing: ease-out;
	--ffp-rv-blur: 8px;
	--ffp-rv-scale: 0.8;
	/* p5cmp210 — the SCALE ORIGIN. Measured on the reference site: its glyph
	 * images carry no transform-origin at all, so they scale about the CSS
	 * default, the element CENTRE (sampled live: origin y = 40.94px on an
	 * 81.9px glyph). p5cmp207 used `center bottom`, which anchors the baseline
	 * and therefore drags the glyph's TOP edge through 20% of its height —
	 * ~16px on our 80px name — during the 150ms. That vertical travel is what
	 * read as a "pop" on every appended character and as a lurch on the
	 * two-digit number exit. Scaling about the centre has no net travel. */
	--ffp-rv-origin: 50% 50%;
}

/* p5cmp210 — THE ARM PIN (first-selection flash).
 * A discrete ENTER commits the finished stage into the LIVE tree, and only
 * the next animator pass marks it. That gap is one frame, and one frame of a
 * fully opaque name IS the flash. While the machine is armed, any stage child
 * is pinned at the enter-from opacity, so a glyph committed during the gap
 * cannot paint solid; the mark that follows starts its animation from exactly
 * the same value, so nothing jumps. Digits already had this guarantee via the
 * handoff gate — this is the same idea for the name. */
.ffp-preview.ffp-rv-arm .ffp-font-name-stage > * { opacity: 0; }

/* --- ENTER: opacity 0->authored, blur 8px->0, scale 0.8->1 --- */
@keyframes ffp-rv-in-glyph {
	from { opacity: 0; filter: blur(var(--ffp-rv-blur, 8px)); }
	to   { opacity: var(--ffp-name-opacity, 1); filter: blur(0); }
}
@keyframes ffp-rv-in-scale { from { scale: var(--ffp-rv-scale, 0.8); } to { scale: 1; } }
@keyframes ffp-rv-in-digit {
	from { opacity: 0; filter: blur(var(--ffp-rv-blur, 8px)); transform: scale(var(--ffp-rv-scale, 0.8)); }
	to   { opacity: 1; filter: blur(0); transform: scale(1); }
}
/* --- EXIT: the exact mirror --- */
@keyframes ffp-rv-out-glyph {
	from { opacity: var(--ffp-name-opacity, 1); filter: blur(0); }
	to   { opacity: 0; filter: blur(var(--ffp-rv-blur, 8px)); }
}
@keyframes ffp-rv-out-scale { from { scale: 1; } to { scale: var(--ffp-rv-scale, 0.8); } }
@keyframes ffp-rv-out-digit {
	from { opacity: 1; filter: blur(0); transform: scale(1); }
	to   { opacity: 0; filter: blur(var(--ffp-rv-blur, 8px)); transform: scale(var(--ffp-rv-scale, 0.8)); }
}

/* =============== EXIT PHASE — one global reverse sequence =============== */
/* p5cmp212 — the reverse sequence is now TWO blocks, not one ladder:
 *   digit  d (0 = last digit)  ->  d * digitExitStep
 *   letter l (0 = last letter) ->  digitCount * digitExitStep + l * step
 * so the letters begin only once every digit slot has been spent. --ffp-rv-r
 * is therefore a LOCAL reverse index within its own line (it used to fold the
 * digit count into the letters' index, which no longer works when the two
 * lines step at different rates). */
.ffp-preview.ffp-rv-exit .ffp-name-glyph.ffp-rv-out {
	--ffp-rv-delay: calc(var(--ffp-rv-numcount, 0) * var(--ffp-rv-digit-exit-step) + var(--ffp-rv-r, 0) * var(--ffp-rv-step));
	will-change: opacity, filter;
	animation: ffp-rv-out-glyph var(--ffp-rv-duration) var(--ffp-rv-easing) var(--ffp-rv-delay) both;
}
.ffp-preview.ffp-rv-exit .ffp-name-glyph.ffp-rv-out > * {
	transform-origin: var(--ffp-rv-origin, 50% 50%);
	will-change: scale;
	animation: ffp-rv-out-scale var(--ffp-rv-duration) var(--ffp-rv-easing) var(--ffp-rv-delay) both;
}
.ffp-preview.ffp-rv-exit .ffp-svg-digit.ffp-rv-out {
	--ffp-rv-delay: calc(var(--ffp-rv-r, 0) * var(--ffp-rv-digit-exit-step));
	transform-origin: var(--ffp-rv-origin, 50% 50%);
	will-change: opacity, filter, transform;
	animation: ffp-rv-out-digit var(--ffp-rv-duration) var(--ffp-rv-easing) var(--ffp-rv-delay) both;
}

/* =============== ENTER PHASE — name first, then number =============== */
.ffp-preview.ffp-rv-name .ffp-name-glyph.ffp-rv-in {
	--ffp-rv-delay: calc(var(--ffp-rv-i, 0) * var(--ffp-rv-step));
	will-change: opacity, filter;
	animation: ffp-rv-in-glyph var(--ffp-rv-duration) var(--ffp-rv-easing) var(--ffp-rv-delay) both;
}
.ffp-preview.ffp-rv-name .ffp-name-glyph.ffp-rv-in > * {
	transform-origin: var(--ffp-rv-origin, 50% 50%);
	will-change: scale;
	animation: ffp-rv-in-scale var(--ffp-rv-duration) var(--ffp-rv-easing) var(--ffp-rv-delay) both;
}
/* THE HANDOFF GATE: while the NAME is entering, digits marked to enter are
   held invisible. They cannot appear early even if the browser would
   otherwise paint them. */
.ffp-preview.ffp-rv-name .ffp-svg-digit.ffp-rv-in { opacity: 0; }
.ffp-preview.ffp-rv-num .ffp-svg-digit.ffp-rv-in {
	--ffp-rv-delay: calc(var(--ffp-rv-i, 0) * var(--ffp-rv-step));
	transform-origin: var(--ffp-rv-origin, 50% 50%);
	will-change: opacity, filter, transform;
	animation: ffp-rv-in-digit var(--ffp-rv-duration) var(--ffp-rv-easing) var(--ffp-rv-delay) both;
}

/* =============== TYPING — a micro-animation, not a cascade =============== */
/* Every appended glyph shares ONE fixed one-step delay, matching the
   measured reference behaviour (a fixed 40ms, never index x 40ms). These
   rules carry an extra class so they outrank the cascade rules above. */
.ffp-preview.ffp-rv-name .ffp-name-glyph.ffp-rv-in.ffp-rv-append,
.ffp-preview.ffp-rv-name .ffp-name-glyph.ffp-rv-in.ffp-rv-append > *,
.ffp-preview.ffp-rv-name .ffp-svg-digit.ffp-rv-in.ffp-rv-append {
	--ffp-rv-delay: var(--ffp-rv-step);
}
/* An appended DIGIT animates immediately — typing never waits for the name. */
.ffp-preview.ffp-rv-name .ffp-svg-digit.ffp-rv-in.ffp-rv-append {
	opacity: 1;
	transform-origin: var(--ffp-rv-origin, 50% 50%);
	will-change: opacity, filter, transform;
	animation: ffp-rv-in-digit var(--ffp-rv-duration) var(--ffp-rv-easing) var(--ffp-rv-delay) both;
}

/* --- forward index ladder: name stage children (spaces included) --- */
.ffp-preview[class*="ffp-rv-"] .ffp-font-name-stage > :nth-child(1) { --ffp-rv-i: 0; }
.ffp-preview[class*="ffp-rv-"] .ffp-font-name-stage > :nth-child(2) { --ffp-rv-i: 1; }
.ffp-preview[class*="ffp-rv-"] .ffp-font-name-stage > :nth-child(3) { --ffp-rv-i: 2; }
.ffp-preview[class*="ffp-rv-"] .ffp-font-name-stage > :nth-child(4) { --ffp-rv-i: 3; }
.ffp-preview[class*="ffp-rv-"] .ffp-font-name-stage > :nth-child(5) { --ffp-rv-i: 4; }
.ffp-preview[class*="ffp-rv-"] .ffp-font-name-stage > :nth-child(6) { --ffp-rv-i: 5; }
.ffp-preview[class*="ffp-rv-"] .ffp-font-name-stage > :nth-child(7) { --ffp-rv-i: 6; }
.ffp-preview[class*="ffp-rv-"] .ffp-font-name-stage > :nth-child(8) { --ffp-rv-i: 7; }
.ffp-preview[class*="ffp-rv-"] .ffp-font-name-stage > :nth-child(9) { --ffp-rv-i: 8; }
.ffp-preview[class*="ffp-rv-"] .ffp-font-name-stage > :nth-child(10) { --ffp-rv-i: 9; }
.ffp-preview[class*="ffp-rv-"] .ffp-font-name-stage > :nth-child(11) { --ffp-rv-i: 10; }
.ffp-preview[class*="ffp-rv-"] .ffp-font-name-stage > :nth-child(12) { --ffp-rv-i: 11; }
.ffp-preview[class*="ffp-rv-"] .ffp-font-name-stage > :nth-child(13) { --ffp-rv-i: 12; }
.ffp-preview[class*="ffp-rv-"] .ffp-font-name-stage > :nth-child(14) { --ffp-rv-i: 13; }
.ffp-preview[class*="ffp-rv-"] .ffp-font-name-stage > :nth-child(15) { --ffp-rv-i: 14; }
.ffp-preview[class*="ffp-rv-"] .ffp-font-name-stage > :nth-child(16) { --ffp-rv-i: 15; }
.ffp-preview[class*="ffp-rv-"] .ffp-font-name-stage > :nth-child(17) { --ffp-rv-i: 16; }
.ffp-preview[class*="ffp-rv-"] .ffp-font-name-stage > :nth-child(18) { --ffp-rv-i: 17; }
.ffp-preview[class*="ffp-rv-"] .ffp-font-name-stage > :nth-child(19) { --ffp-rv-i: 18; }
.ffp-preview[class*="ffp-rv-"] .ffp-font-name-stage > :nth-child(20) { --ffp-rv-i: 19; }
.ffp-preview[class*="ffp-rv-"] .ffp-font-name-stage > :nth-child(21) { --ffp-rv-i: 20; }
.ffp-preview[class*="ffp-rv-"] .ffp-font-name-stage > :nth-child(22) { --ffp-rv-i: 21; }
.ffp-preview[class*="ffp-rv-"] .ffp-font-name-stage > :nth-child(23) { --ffp-rv-i: 22; }
.ffp-preview[class*="ffp-rv-"] .ffp-font-name-stage > :nth-child(24) { --ffp-rv-i: 23; }

/* --- forward index ladder: digits --- */
.ffp-preview[class*="ffp-rv-"] .ffp-preview__number > :nth-child(1) { --ffp-rv-i: 0; }
.ffp-preview[class*="ffp-rv-"] .ffp-preview__number > :nth-child(2) { --ffp-rv-i: 1; }
.ffp-preview[class*="ffp-rv-"] .ffp-preview__number > :nth-child(3) { --ffp-rv-i: 2; }
.ffp-preview[class*="ffp-rv-"] .ffp-preview__number > :nth-child(4) { --ffp-rv-i: 3; }

/* --- reverse ladder: digits leave FIRST, so their reverse index is simply
       their distance from the end --- */
.ffp-preview.ffp-rv-exit .ffp-preview__number > :nth-last-child(1) { --ffp-rv-r: 0; }
.ffp-preview.ffp-rv-exit .ffp-preview__number > :nth-last-child(2) { --ffp-rv-r: 1; }
.ffp-preview.ffp-rv-exit .ffp-preview__number > :nth-last-child(3) { --ffp-rv-r: 2; }
.ffp-preview.ffp-rv-exit .ffp-preview__number > :nth-last-child(4) { --ffp-rv-r: 3; }

/* --- reverse ladder: letters continue AFTER every digit, so each adds the
       digit count the animator wrote on the root --- */
.ffp-preview.ffp-rv-exit .ffp-font-name-stage > :nth-last-child(1) { --ffp-rv-r: 0; }
.ffp-preview.ffp-rv-exit .ffp-font-name-stage > :nth-last-child(2) { --ffp-rv-r: 1; }
.ffp-preview.ffp-rv-exit .ffp-font-name-stage > :nth-last-child(3) { --ffp-rv-r: 2; }
.ffp-preview.ffp-rv-exit .ffp-font-name-stage > :nth-last-child(4) { --ffp-rv-r: 3; }
.ffp-preview.ffp-rv-exit .ffp-font-name-stage > :nth-last-child(5) { --ffp-rv-r: 4; }
.ffp-preview.ffp-rv-exit .ffp-font-name-stage > :nth-last-child(6) { --ffp-rv-r: 5; }
.ffp-preview.ffp-rv-exit .ffp-font-name-stage > :nth-last-child(7) { --ffp-rv-r: 6; }
.ffp-preview.ffp-rv-exit .ffp-font-name-stage > :nth-last-child(8) { --ffp-rv-r: 7; }
.ffp-preview.ffp-rv-exit .ffp-font-name-stage > :nth-last-child(9) { --ffp-rv-r: 8; }
.ffp-preview.ffp-rv-exit .ffp-font-name-stage > :nth-last-child(10) { --ffp-rv-r: 9; }
.ffp-preview.ffp-rv-exit .ffp-font-name-stage > :nth-last-child(11) { --ffp-rv-r: 10; }
.ffp-preview.ffp-rv-exit .ffp-font-name-stage > :nth-last-child(12) { --ffp-rv-r: 11; }
.ffp-preview.ffp-rv-exit .ffp-font-name-stage > :nth-last-child(13) { --ffp-rv-r: 12; }
.ffp-preview.ffp-rv-exit .ffp-font-name-stage > :nth-last-child(14) { --ffp-rv-r: 13; }
.ffp-preview.ffp-rv-exit .ffp-font-name-stage > :nth-last-child(15) { --ffp-rv-r: 14; }
.ffp-preview.ffp-rv-exit .ffp-font-name-stage > :nth-last-child(16) { --ffp-rv-r: 15; }
.ffp-preview.ffp-rv-exit .ffp-font-name-stage > :nth-last-child(17) { --ffp-rv-r: 16; }
.ffp-preview.ffp-rv-exit .ffp-font-name-stage > :nth-last-child(18) { --ffp-rv-r: 17; }
.ffp-preview.ffp-rv-exit .ffp-font-name-stage > :nth-last-child(19) { --ffp-rv-r: 18; }
.ffp-preview.ffp-rv-exit .ffp-font-name-stage > :nth-last-child(20) { --ffp-rv-r: 19; }
.ffp-preview.ffp-rv-exit .ffp-font-name-stage > :nth-last-child(21) { --ffp-rv-r: 20; }
.ffp-preview.ffp-rv-exit .ffp-font-name-stage > :nth-last-child(22) { --ffp-rv-r: 21; }
.ffp-preview.ffp-rv-exit .ffp-font-name-stage > :nth-last-child(23) { --ffp-rv-r: 22; }
.ffp-preview.ffp-rv-exit .ffp-font-name-stage > :nth-last-child(24) { --ffp-rv-r: 23; }

/* Settled: no animation, no compositing hint pinned per glyph. */
.ffp-preview.ffp-rv-settled .ffp-name-glyph,
.ffp-preview.ffp-rv-settled .ffp-name-glyph > *,
.ffp-preview.ffp-rv-settled .ffp-svg-digit {
	animation: none;
	will-change: auto;
}

/* Reduced motion: the content is simply THERE. Not a shorter duration, not a
   faster curve — no exit, no enter, no stagger, no blur, no scale. The
   animator also skips its own pass and commits immediately, so this is belt
   and braces. */
@media (prefers-reduced-motion: reduce) {
	.ffp-preview.ffp-rv-exit .ffp-name-glyph.ffp-rv-out,
	.ffp-preview.ffp-rv-exit .ffp-name-glyph.ffp-rv-out > *,
	.ffp-preview.ffp-rv-exit .ffp-svg-digit.ffp-rv-out,
	.ffp-preview.ffp-rv-name .ffp-name-glyph.ffp-rv-in,
	.ffp-preview.ffp-rv-name .ffp-name-glyph.ffp-rv-in > *,
	.ffp-preview.ffp-rv-name .ffp-svg-digit.ffp-rv-in,
	.ffp-preview.ffp-rv-num .ffp-svg-digit.ffp-rv-in {
		animation: none !important;
		will-change: auto !important;
		scale: none !important;
		opacity: 1 !important;
	}
}
