/**
 * Stibok base block styles + interaction primitives.
 * Everything references theme.json tokens so brand-kit swaps restyle instantly.
 */

/* ---------- box-sizing floor ----------
   Stibok blocks must not depend on a theme's global box-sizing reset. Neither the
   active theme NOR the editor-canvas iframe is guaranteed to set border-box (the
   iframe defaults to content-box; a theme may ship no reset at all). With
   content-box, a padded block that is height-stretched — e.g. a card as a grid
   item under the default `align-items: stretch` — renders its padding OUTSIDE the
   track the grid sized for it, so the box overflows its row and overlaps the next
   row. Reset the whole Stibok subtree to border-box at zero specificity (`:where`)
   so any explicit rule still wins, and so the front and the canvas match by
   construction (this sheet loads on both). */
:where( [class*="wp-block-stibok-"] ),
:where( [class*="wp-block-stibok-"] * ) {
	box-sizing: border-box;
}

/* ---------- Section + decor layer ---------- */
.stibok-section {
	position: relative;
	isolation: isolate;
	/* No `overflow: hidden` here on purpose: it would make every section a scroll
	   container and break `position: sticky` for descendants (a sticky child would
	   pin to the section, not the viewport). Clipping is scoped to the layers that
	   actually need it — the decor layer (.stibok-section__decor) and the bg layer
	   (.stibok-section__bg) — so decor blobs stay clipped while sticky works. */
}

/* Kill WP theme.json `blockGap` on top-level sections.
   Core emits `:root :where(.is-layout-flow) > * { margin-block-start: var(--wp--
   preset--spacing--md) }` (and the constrained twin). page-canvas is `is-layout-flow`,
   so every adjacent `.stibok-section` picked up a seam of `spacing--md` between bands
   — on top of the section's OWN padding, which is the real vertical rhythm. Sections
   own their spacing; the layout-flow gap is noise. `:where()` is zero-specificity, so
   `:root … > .stibok-section` (0,2,0) wins cleanly. Does NOT touch the section's own
   chrome `margin` attr (render.php), nor gap inside stacks/rows. */
:root :where(.is-layout-flow) > .stibok-section,
:root :where(.is-layout-constrained) > .stibok-section {
	margin-block-start: 0;
	margin-block-end: 0;
}

/* Background layer (1.3): behind overlay + decor + content. z: Z_LAYERS['bg'] = -1.
   Self-clipping so a bg image (or future radius) never bleeds past the section. */
.stibok-section__bg {
	position: absolute;
	inset: 0;
	z-index: -1;
	pointer-events: none;
	overflow: hidden;
	background-size: cover;
	background-position: center;
	background-repeat: no-repeat;
}

/* Overlay scrim (1.3): between background image and decor. z: Z_LAYERS['overlay'] = 0 */
.stibok-section__overlay {
	position: absolute;
	inset: 0;
	z-index: 0;
	pointer-events: none;
}

.stibok-section__decor {
	position: absolute;
	inset: 0;
	z-index: 1; /* was 0; Z_LAYERS['decor'] = 1 */
	pointer-events: none;
	overflow: hidden;
}

.stibok-decor {
	position: absolute;
	/* MV.7 (review finding 8): NO `will-change` here. This outer anchor's only `transform`
	   is the STATIC centering `translate(-50%,-50%)` (decor_node) — it never animates, so a
	   `will-change: transform` hint promoted an IDLE element to its own compositor layer for
	   no benefit (memory cost, "don't will-change idle elements"). Decor MOTION lives on the
	   inner `.stibok-decor__fx`/`.stibok-motion` (rotate/translate/scale longhands + the
	   `--sm-loop-*` slots), which the browser promotes on its own while a compositor-eligible
	   animation runs — the MV.7 benchmark confirmed sole-writer longhand loops hold ~60fps
	   under a 20x main-thread throttle WITHOUT any will-change hint. */
}

/* Inner effects layer: rotate + motion compose here via CSS variables so they
   never fight the outer anchor/centering transform. */
.stibok-decor__fx {
	display: block;
	width: 100%;
	height: 100%;
	/* MV.4: decor's static tilt is an ordinary `rotate` LONGHAND slot now (was re-stated
	   inside a composed `transform` on a decor-specific rule). It composes with the motion
	   `rotate` longhand on the base `.stibok-motion` rule below: a MOTION decor's rotate is
	   owned by that composition (which folds `--stibok-decor-rotate` in alongside entrance/
	   hover/loop), and this longhand is overridden by it (same specificity, later in source);
	   a STATIC decor (no `.stibok-motion`) gets its tilt from this longhand alone. No
	   translate/scale here — a static decor never sets the `--sm-*` slots, and a motion
	   decor's translate/scale are owned by the base composition (so decor now composes with
	   hover translate/scale too — the old re-statement dropped those, latent clobber #2/#3). */
	rotate: var(--stibok-decor-rotate, 0deg);
	transform-origin: center;
	/* MV.4: decor's REST filter (its designed softness — accent-glow's blur, etc.). Decor is
	   the one motion context that keeps a filter at rest: it carries NO box-shadow, so HV.8's
	   filter-buffer-clips-box-shadow concern does not apply (base `.stibok-motion` stays
	   filter-free for exactly that reason). Composes the reveal blur slot + a filter-animating
	   loop slot. The generic `.stibok-motion` reveal/settle/hover filter rules fold
	   `--stibok-decor-filter` in so a motion decor keeps its softness through those states. */
	filter: var(--stibok-decor-filter,) var(--sm-filter, blur(var(--sm-blur, 0px))) var(--sm-loop-filter,);
}

/* CL2.V3 — background decor video. Sized like the image layer; the object-fit is set
   INLINE per instance (cover default, contain opt-in), so it is not repeated here.
   `pointer-events` is already none on `.stibok-section__decor`, which is what stops the
   video's own controls surface (and any future picture-in-picture affordance) from
   swallowing clicks meant for the content above it. */
.stibok-decor__fx > video {
	display: block;
	width: 100%;
	height: 100%;
	max-inline-size: 100%;
}

.stibok-decor__fx > img {
	display: block;
	width: 100%;
	height: 100%;
	object-fit: contain;
}

/* Editor-canvas only (1.18 / D22): the layers panel marks the hovered layer and
   any hidden layer. These classes are emitted solely by the editor decor preview
   (never by the PHP render), so they match only inside the canvas iframe — the
   front output is untouched. The hovered layer gets an accent ring; a hidden
   layer stays visible-but-dimmed so the author can still place it while editing. */
.stibok-decor.is-stibok-hover {
	outline: 2px solid var(--wp-admin-theme-color, currentColor);
	outline-offset: 2px;
}
.stibok-decor.is-stibok-hidden {
	opacity: 0.2 !important;
	outline: 1px dashed var(--wp-admin-theme-color, currentColor);
}

/* Motion canvas badge (1.19) moved to editor-canvas-chrome.css (CC.1) — authoring-
   only chrome must not live in the front stylesheet. The BlockListBlock HOC still
   adds `has-stibok-motion` (editor-only class); chrome CSS paints the cue. */

/* Shape divider (1.17): absolute edge layer between decor and content.
   z: Z_LAYERS['shape-divider'] = 2. Sits in the section's compensating-margin
   gap (margin-block added inline = divider height) and spills over the adjacent
   section. Logical insets only; the orientation/flip/offset transform is emitted
   inline by Render_Helpers::shape_divider(). No overflow/contain here (sibling of
   content, never an ancestor — sticky descendants are unaffected). */
.stibok-section__divider {
	position: absolute;
	inset-inline: 0;
	z-index: 2;
	line-height: 0;
	pointer-events: none;
}
.stibok-section__divider svg {
	display: block;
	width: 100%;
	height: 100%;
}
/* Painted-mask variant (1.17a): the shape geometry is a mask over a color/gradient
   background, so a divider can be a gradient (SVG fill can't take a CSS gradient).
   mask-size 100% 100% = preserveAspectRatio:none (stretch to the section width). */
.stibok-section__divider--paint {
	-webkit-mask-repeat: no-repeat;
	mask-repeat: no-repeat;
	-webkit-mask-position: center;
	mask-position: center;
	-webkit-mask-size: 100% 100%;
	mask-size: 100% 100%;
}
.stibok-section__divider--top {
	inset-block-end: 100%;
}
.stibok-section__divider--bottom {
	inset-block-start: 100%;
}

.stibok-section__content {
	position: relative;
	z-index: 3; /* Z_LAYERS['content'] = 3 (above shape-divider) */
}

/* D-13 — the minimum side gutter.
   The inner container carries `max-width` + `margin-inline:auto` (section/render.php), which
   centres it only while the viewport is WIDER than the cap. `wide` is 75rem — about 1307px once
   root-scaling is applied — so on the commonest desktop widths (1280–1366) the cap never engages,
   the inner box fills the viewport, and headings and body copy ran flush against the physical edge
   of the display. 33 of the 76 bundled compositions use `wide`.
   It stayed invisible because every demo was built on the DEFAULT page template, where the theme's
   `has-global-padding is-layout-constrained` wrapper supplied a gutter; Stibok's own `page-canvas`
   is `is-layout-flow` with zero padding, so the section is on its own — and that is the template
   real Stibok pages use.
   Padding rather than a `min()` on the max-width, because it must apply at EVERY viewport, not
   only when the cap is the binding constraint. Border-box is guaranteed by the reset at the top of
   this file, so the declared max-width still bounds the outer box. */
.stibok-section__inner {
	padding-inline: var(--wp--custom--container--gutter, clamp(1rem, 4vw, 2.5rem));
}

/* `full` means edge-to-edge and is the one width that MEANS it — a full-bleed band, a background
   image row. It opts out (class emitted by section/render.php). */
.stibok-section__inner.is-cw-full {
	padding-inline: 0;
}

/* ---------- Tone token remaps (1.3 / 1.4) ---------- */
/* Both rules explicitly define every semantic token so nested tones override
   parent tones correctly — e.g. a dark card inside a light section, or a light
   stack inside a dark section. Whichever rule is closer in the DOM wins.
   CL1.5.2: this static block is now the FALLBACK DEFAULT — the dynamic tone-CSS
   emitter (Render_Helpers::tone_remap_css()) regenerates these rules from the
   (skin-overridable) settings.custom.tonePairs and is enqueued AFTER this sheet
   on both the front and the canvas iframe, so a skin's polarity override wins on
   equal specificity. With no override the emitted rules equal this block.       */
.is-tone-dark {
	color: var(--wp--preset--color--base);
	--wp--custom--semantic--surface:          var(--wp--preset--color--contrast);
	--wp--custom--semantic--surface-2:        var(--wp--preset--color--contrast-2);
	--wp--custom--semantic--on-surface:       var(--wp--preset--color--base);
	--wp--custom--semantic--on-surface-muted: var(--wp--preset--color--muted-2);
	--wp--custom--semantic--border:           var(--wp--preset--color--contrast-2);
	--wp--custom--semantic--accent-soft:      var(--wp--preset--color--accent-soft-dark);
	/* ACCENT + DANGER POLARITY (obsidian-contrast fix). The palette's `accent` / `danger` /
	   `accent-hover` are tuned to sit on a LIGHT surface, and the tone remap never re-pointed
	   them — so on the dark surface every FOREGROUND use of them failed WCAG AA: the `eyebrow`
	   heading variant (`text: {accent}`) rendered #4f46e5 on #10121a = 2.97:1, and the
	   required-field marker (`{danger}`) #dc2626 on #10121a = 3.87:1. Both need 4.5:1.

	   There is no single value that fixes this while `on-accent` stays white: `{accent}` is
	   BOTH a foreground (eyebrow, link/soft button, list marker, tabs badge) and a FILL
	   (primary button, checked checkbox, meter). Foreground needs L >= 0.203 to clear 4.5:1
	   over #10121a; white-on-fill needs L <= 0.183 to clear 4.5:1 — an empty interval. So the
	   accent PAIR inverts with the tone, exactly like `accent-soft` above: the accent goes
	   light and `on-accent` goes dark, which makes ONE ratio serve both roles (the same number
	   whether the accent is the ink or the fill). This is the standard dark-theme move.

	   The light values are derived with color-mix from the PALETTE slugs rather than hardcoded,
	   so re-valuing `accent`/`danger` (a skin, Style Studio, an AI design system) still
	   re-themes the dark tone — a literal hex would sever that indirection. 50% white on the
	   default palette gives accent #a7a2f2 (8.09:1 on `contrast`, 4.56:1 on `contrast-2`) and
	   danger #ee9292 (8.16:1 / 4.60:1); `accent-hover` mixes to 40% so hover BRIGHTENS on dark
	   (#b9b5f5, 9.78:1) the way `accent-hover` darkens on light. */
	--wp--custom--semantic--accent:           color-mix(in srgb, var(--wp--preset--color--accent) 50%, white);
	--wp--custom--semantic--accent-hover:     color-mix(in srgb, var(--wp--preset--color--accent-hover) 40%, white);
	--wp--custom--semantic--on-accent:        var(--wp--preset--color--contrast);
	--wp--custom--semantic--danger:           color-mix(in srgb, var(--wp--preset--color--danger) 50%, white);
	/* Paint the toned container's OWN surface so a `tone:"dark"` region is actually dark —
	   without this, tone only re-pointed the semantic tokens + text color, leaving a dark
	   region's background transparent (light text on the light page = invisible; dark cards
	   floating on a light band). `background-color` (not `background`) so a decor gradient layer
	   still shows. `--sm-tone-bg` lets a container opt OUT (set it to `transparent`) when it must
	   stay unpainted (e.g. tone used only to recolor text over an image/decor). */
	background-color: var(--sm-tone-bg, var(--wp--custom--semantic--surface));
}
.is-tone-light {
	color: var(--wp--preset--color--contrast);
	--wp--custom--semantic--surface:          var(--wp--preset--color--base);
	--wp--custom--semantic--surface-2:        var(--wp--preset--color--base-2);
	--wp--custom--semantic--on-surface:       var(--wp--preset--color--contrast);
	--wp--custom--semantic--on-surface-muted: var(--wp--preset--color--muted);
	--wp--custom--semantic--border:           var(--wp--preset--color--border);
	--wp--custom--semantic--accent-soft:      var(--wp--preset--color--accent-soft);
	/* Restore the light-surface accent pair. REQUIRED, not decorative: this block's rule is
	   "every tone defines every token", and without these four a `.is-tone-light` card nested
	   inside a `.is-tone-dark` section would INHERIT the lightened dark-tone accent (pale
	   indigo ink on white, dark ink on a pale fill) — the exact nesting bug the note above
	   warns about. They re-state the root defaults, so an un-nested light tone is unchanged. */
	--wp--custom--semantic--accent:           var(--wp--preset--color--accent);
	--wp--custom--semantic--accent-hover:     var(--wp--preset--color--accent-hover);
	--wp--custom--semantic--on-accent:        var(--wp--preset--color--on-accent);
	--wp--custom--semantic--danger:           var(--wp--preset--color--danger);
	background-color: var(--sm-tone-bg, var(--wp--custom--semantic--surface));
}

/* Decor size scale (relative to section). */
/* Decor size is applied inline as width/height (px/rem/%) from the editor. */

/* Position is applied inline from the anchor x/y + offsets (see render-helpers). */

/* ---------- Layout primitives ---------- */
.stibok-stack { display: flex; flex-direction: column; }
.stibok-row { display: flex; flex-direction: row; flex-wrap: nowrap; }
.stibok-cluster { display: flex; flex-wrap: wrap; align-items: center; }
.stibok-grid { display: grid; }

.stibok-stack > *,
.stibok-row > *,
.stibok-cluster > *,
.stibok-grid > * { margin: 0; }

/* ---------- Overflow guard (structural horizontal-blowout containment) ----------
 * Makes horizontal overflow of an AI-composed section STRUCTURALLY IMPOSSIBLE
 * regardless of the IR the model emits (a row confused for a column, an over-columned
 * grid, a card with a long unbroken token). The standard flex/grid blowout fix:
 *
 *   1. `min-inline-size: 0` on ROW / CLUSTER / GRID children only. Flex/grid items
 *      default to `min-width: auto` (min-content); a wide card then pushes its track
 *      PAST its share. `0` lets them shrink to their share. Grid tracks already use
 *      `minmax(0,1fr)` (Layout_Compiler); children still need the lowered floor.
 *
 *      STACK is intentionally EXCLUDED. A column stack does not share horizontal
 *      tracks the way row/grid do. A stack that must shrink inside a row/grid already
 *      gets `min-inline-size: 0` as THAT parent's child. Zeroing stack CHILDREN plus
 *      `align-items: center|start|end` plus `overflow-wrap: anywhere` collapses
 *      card/form width to ~1ch (shrink-to-fit + indefinite `%` width circularity) —
 *      the bake-off contact/testimonial failure. Do NOT re-add stack to this rule.
 *   2. Non-stretched stack children clamp with `max-inline-size: 100%`. Fill
 *      containers (card/form/grid/row/stack/cluster/accordion) inside hug-aligned
 *      stacks also get `align-self: stretch` — otherwise card→form→field
 *      (`min-inline-size:0` + `inline-size:100%`) shrink-to-fits to ~0 width.
 *   3. `overflow-wrap: anywhere` + `word-break: break-word` on text-bearing elements so
 *      a single long unbroken token (a URL, a hash) can't force intrinsic width.
 *   4. `max-inline-size: 100%` on embedded media/graphic layers so a wide asset never
 *      exceeds its column.
 *   5. A SECTION-content safety net: `overflow-x: clip` on `.stibok-section__content`
 *      hides any residual horizontal spill WITHOUT a page-wide scrollbar. `clip` (never
 *      `hidden`/`auto`/`scroll`) is deliberate — it does NOT establish a scroll
 *      container, so descendant `position: sticky` still pins to the real scroll
 *      ancestor (the 1.24/R1 sticky hazard is avoided), and it is scoped to the CONTENT
 *      layer, NOT the section root: shape dividers (`.stibok-section__divider`) are
 *      SIBLINGS of the content wrapper at the section root and spill via compensating
 *      margin, so they are never cropped. `overflow-y` stays `visible` (clip on one axis
 *      does not coerce the other) so vertical spill — dropdowns, sticky, a shape-divider
 *      panel — is untouched.
 */
.stibok-row > *,
.stibok-cluster > *,
.stibok-grid > * { min-inline-size: 0; }

/* Hug-align stack children: cap to the stack without lowering the min floor (see §1). */
.stibok-stack.align-center > *,
.stibok-stack.align-start > *,
.stibok-stack.align-end > * { max-inline-size: 100%; }

/*
 * Fill containers inside a hug-aligned stack MUST stretch. Otherwise a card/form
 * shrink-to-fits against indefinite `%` widths (`.stibok-field { min-inline-size:0 }`
 * + `.stibok-field__control { inline-size:100% }`) and collapses to ~0–1ch — the
 * bake-off contact/testimonial failure. Text/heading/button stay hugged+centered.
 *
 * The CL1 structural blocks (`timeline`, `tabs`, `meter`) are fill containers too — they
 * were added AFTER this allowlist and left off it, so a timeline/tabs inside a centered
 * stack (a common layout) shrink-wrapped to its content width instead of filling. `table`
 * and `marquee` self-size (own `inline-size:100%` / `max-inline-size:100%`) so they are not
 * listed. A stretched `meter` ring stays centered inside its now-full-width box (harmless).
 *
 * `diagram` (Phase INF, INF.2) joins this list PROACTIVELY: EVERY one of its direct
 * children (`__scaffold`, `__center`, `__items`) is `position:absolute`, so it has NO
 * in-flow content to derive an intrinsic width from — exactly the timeline/tabs/meter
 * collapse-to-~0 failure this allowlist exists to fix, just discovered before shipping
 * instead of after.
 */
.stibok-stack.align-center > :is(.stibok-card, .stibok-form, .stibok-grid, .stibok-row, .stibok-stack, .stibok-cluster, .stibok-accordion, .stibok-timeline, .stibok-tabs, .stibok-meter, .stibok-diagram),
.stibok-stack.align-start > :is(.stibok-card, .stibok-form, .stibok-grid, .stibok-row, .stibok-stack, .stibok-cluster, .stibok-accordion, .stibok-timeline, .stibok-tabs, .stibok-meter, .stibok-diagram),
.stibok-stack.align-end > :is(.stibok-card, .stibok-form, .stibok-grid, .stibok-row, .stibok-stack, .stibok-cluster, .stibok-accordion, .stibok-timeline, .stibok-tabs, .stibok-meter, .stibok-diagram) {
	align-self: stretch;
}

.stibok-heading,
.stibok-text,
.stibok-list__content,
.stibok-button__label { overflow-wrap: anywhere; word-break: break-word; }

.stibok-image__img,
.stibok-icon__glyph,
.stibok-embed,
.stibok-decor__fx > img { max-inline-size: 100%; }

.stibok-section__content { overflow-x: clip; }

/* Gap scale. */
.gap-xxxs { gap: var(--wp--preset--spacing--xxxs); }
.gap-xxs { gap: var(--wp--preset--spacing--xxs); }
.gap-xs { gap: var(--wp--preset--spacing--xs); }
.gap-sm { gap: var(--wp--preset--spacing--sm); }
.gap-md { gap: var(--wp--preset--spacing--md); }
.gap-lg { gap: var(--wp--preset--spacing--lg); }
.gap-xl { gap: var(--wp--preset--spacing--xl); }
.gap-xxl { gap: var(--wp--preset--spacing--xxl); }

/* Alignment helpers. */
.align-start { align-items: flex-start; }
.align-center { align-items: center; }
.align-end { align-items: flex-end; }
.align-stretch { align-items: stretch; }
.justify-start { justify-content: flex-start; }
.justify-center { justify-content: center; }
.justify-end { justify-content: flex-end; }
.justify-between { justify-content: space-between; }
.text-center { text-align: center; }
.text-start { text-align: start; }
.text-end { text-align: end; }

/* Grid item inline-axis alignment (the 2D align pad's main axis on a grid). The
   cross axis reuses the `.align-*` (align-items) helpers above. */
.justify-items-start { justify-items: start; }
.justify-items-center { justify-items: center; }
.justify-items-end { justify-items: end; }

/* ---------- Container intent (layout v2 — §2.1, 1.14) ---------- */
/* `row.distribute` — how children share the main axis (Layout_Compiler::compile_row). */
.stibok-row.distribute-equal > * { flex: 1 1 0; min-width: 0; }
/* `content`/`split` children keep their natural (content-based or fixed-width,
   R1.15 `layout.size=fixed`) width — historically `flex:0 0 auto` (never grow,
   never shrink). Without a wrap-or-shrink fallback, content-sized children whose
   width (individually or SUMMED) exceeds the row's available inline size push
   the overflowing one off-screen instead of reflowing — the intrinsic-floor
   invariant (`a row must never overflow its container`, floor phase F.3) broken
   (measured: a benefits stack + a form, ~1263px + ~443px in a 1280px row, form
   pushed ~377–522px off-screen). Two layered fallbacks close it, matching the
   layout-system-v1 §5 "container-query-default, breakpoint-free" doctrine —
   neither depends on the `container.narrow` stop, so the floor holds at ANY
   width, not just below a configured breakpoint:
     1. `flex-wrap: wrap` — the CSS-native "wrap only if it doesn't fit"
        primitive: a no-op whenever the packed children already fit one line
        (the common case — nav rows, button clusters, label+value rows), and
        reflows the overflowing child(ren) onto a new line otherwise.
     2. `flex-shrink: 1` (was `0`) — closes the residual gap wrap alone leaves: a
        SINGLE content-sized child (now alone on its own wrapped line, or the
        row's only child) whose own intrinsic width still exceeds the container
        cannot be helped by wrapping — nothing else to move out of its way — so
        it must be allowed to shrink. `min-inline-size:0` already applies to
        every row child (§ Overflow guard above), so this newly-permitted
        shrink can actually reach 0 instead of being clamped at `auto`
        (min-content); text/heading children then wrap their own lines via the
        existing `overflow-wrap:anywhere` guard instead of holding a single
        unbroken line past the container edge. `flex-grow` stays `0` — a
        content-sized child never grows to fill leftover space (unchanged
        intent), it only gives ground under pressure it can't otherwise escape.
   Placed BEFORE the `.narrow-*` rules below so an explicit `narrow:keep`/
   `narrow:scroll` escape hatch (same specificity, later in source) still wins
   and forces `nowrap` when the author deliberately opts out of wrapping (the
   shrink stays — `narrow:keep`/`narrow:scroll` only re-assert `flex-wrap`,
   never `flex-shrink`, so those escapes still shrink before they scroll/keep). */
.stibok-row.distribute-content { flex-wrap: wrap; }
.stibok-row.distribute-content > * { flex: 0 1 auto; }
.stibok-row.distribute-split { justify-content: space-between; flex-wrap: wrap; }
.stibok-row.distribute-split > * { flex: 0 1 auto; }
.stibok-row.distribute-sidebar-start > * { flex: 1 1 0; min-width: 0; }
.stibok-row.distribute-sidebar-start > :first-child { flex: 0 0 auto; }
.stibok-row.distribute-sidebar-end > * { flex: 1 1 0; min-width: 0; }
.stibok-row.distribute-sidebar-end > :last-child { flex: 0 0 auto; }

/* `row.narrow` — explicit behaviour when the container gets narrow (§5.1).
   `wrap`/`scroll`/`keep` apply immediately; `collapse` switches to a column when
   the row's container is narrow (the container-query default — see below).
   `narrow-keep`/`narrow-scroll` are AUTHOR ESCAPE HATCHES from the distribute-
   content/-split wrap floor above: same specificity, later in source → they win
   and force `nowrap` (accepting a deliberate scroll/overflow risk) when the
   author explicitly opts out of wrapping. */
.stibok-row.narrow-wrap { flex-wrap: wrap; }
.stibok-row.narrow-keep { flex-wrap: nowrap; }
.stibok-row.narrow-scroll { flex-wrap: nowrap; overflow-x: auto; }

/* Child `layout` trait — per-child Fill / Hug / Fixed (1.15; Layout_Compiler::compile_child).
   Maps 1:1 from Figma auto-layout child resizing (`docs/layout-system-v1.md` §2.2).
   Scoped to each flex/grid parent so a per-child intent OVERRIDES the container's
   `distribute` default (equal specificity → these rules, placed AFTER the distribute
   block, win on source order). The Fixed length rides a CSS custom property
   (`--stibok-self-basis`) set inline by the trait, so the *declaration* lives here
   (low specificity, overridable) and only the value is per-instance.
   Precedence (R3, 1.16): an explicit appearance flex/grid-item prop overrides the
   trait. These intent rules are TWO classes deep (`.stibok-row > .is-layout-fill`,
   specificity 0-2-0), so the appearance compiler emits its layout-adv props under a
   DOUBLED hash class (`.stibok-x-h.stibok-x-h`, also 0-2-0) which — printed in
   wp_footer, after this file — wins the source-order tiebreak. */
.stibok-row > .is-layout-fill,
.stibok-stack > .is-layout-fill,
.stibok-cluster > .is-layout-fill { flex: 1 1 0; min-width: 0; }
.stibok-row > .is-layout-hug,
.stibok-stack > .is-layout-hug,
.stibok-cluster > .is-layout-hug { flex: 0 0 auto; }
.stibok-row > .is-layout-fixed,
.stibok-stack > .is-layout-fixed,
.stibok-cluster > .is-layout-fixed { flex: 0 0 var( --stibok-self-basis, auto ); min-width: 0; }

/* Grid parent — `size` maps to track/cell sizing, not flex (flex props are inert
   on a grid item). `Fill` stretches the cell (the grid default, made explicit);
   `Hug` shrinks to content; `Fixed` pins the inline size. */
.stibok-grid > .is-layout-fill { justify-self: stretch; }
.stibok-grid > .is-layout-hug { justify-self: start; }
.stibok-grid > .is-layout-fixed { justify-self: start; width: var( --stibok-self-basis, auto ); }
.stibok-grid > .is-col-span { grid-column: span var( --stibok-self-span, 1 ); }

/* `alignSelf` + `order` work the same in flex and grid (cross/block axis + order). */
.is-self-start { align-self: start; }
.is-self-center { align-self: center; }
.is-self-end { align-self: end; }
.is-self-stretch { align-self: stretch; }
.is-self-baseline { align-self: baseline; }
/* `order` is guarded (a11y — breaks reading order; never AI-emitted). */
.is-ordered { order: var( --stibok-self-order, 0 ); }

/* Container-query setup (the §5 default) is emitted dynamically by
   Render_Helpers::container_css() — the size-query `stibok` container on the two
   always-full-width block-level slots (`.stibok-section__content`, `.stibok-card__body`)
   plus the single STOP-DEPENDENT collapse rule
   `@container stibok (max-width:<container.narrow>){.stibok-row.narrow-collapse{…}}`,
   whose threshold reads the configured `settings.custom.container.narrow` (2.1).
   It attaches via wp_add_inline_style() on the front AND the editor canvas iframe.

   Why generated, not static: the collapse stop is config-driven; and container-type
   must sit ONLY on these block-level slots — `inline-size` makes inline size ignore
   CONTENT, so any hug-sized flex/grid primitive carrying it would collapse to ~0
   (the 1.24 hazard, R1). The static `.narrow-wrap/-keep/-scroll` rules below are NOT
   stop-dependent → they stay here. */

/* Per-slot container-query wrapper (2.2b, layout-system-v1 §5). A block carrying an
   `appearance.container` layer is wrapped in `.stibok-cq` by the trait render filter so
   ANY slot — not just the section content / card body — becomes a `stibok` query
   container; the block's compiled `@container` rules then measure this wrapper. SAFE to
   carry container-type here (R1 / Standing #3): `.stibok-cq` is always block-level
   (`display:block`, or `flex:1`/fixed-width when it carries a moved `is-layout-*` class —
   NEVER hug, since the compact axis is dropped on a hug node), so `inline-size` never
   makes it ignore content and collapse. `inline-size` (not `size`) leaves the block axis
   uncontained → descendant position:sticky still pins; no overflow/contain. The wrapper
   takes the block's place in the parent flex/grid (the moved child-layout classes apply
   to it via the `.stibok-<parent> > .is-layout-*` rules above); the inner block fills it. */
.stibok-cq { display: block; container: stibok / inline-size; }
.stibok-cq > * { inline-size: 100%; }

/* ---------- Atoms ---------- */
.stibok-card { display: block; height: 100%; }
/* The single content region (one InnerBlocks). A real element so appearance.parts.body
   can target it independently of the card box; block flow preserves card layout. */
.stibok-card__body { display: block; }

/* List block (1.20): real per-<li> parts — item > marker + content. The list
   keeps <ul>/<ol> semantics with list-style:none + role=list; markers are real,
   appearance-styleable elements (size/position/color via the `marker` part). */
.stibok-list { list-style: none; margin-block: 0; padding-inline-start: 0; display: flex; flex-direction: column; gap: var(--stibok-list-item-gap, var(--wp--preset--spacing--md)); }
.stibok-list__item { display: flex; align-items: start; gap: var(--stibok-list-marker-gap, 0.3125rem); }
.stibok-list.marker-align-center > .stibok-list__item { align-items: center; }
.stibok-list__marker { flex: 0 0 auto; min-width: var(--stibok-list-marker-size, 1em); display: inline-flex; align-items: center; justify-content: flex-start; color: var(--stibok-list-marker-color, var(--wp--custom--semantic--accent, var(--wp--preset--color--accent, currentColor))); /* D-15 — tone-aware: the semantic accent lightens on a dark section; the preset is the fallback outside a toned scope. */ margin-block-start: var(--stibok-list-marker-offset-y, 0); }
.stibok-list.is-ordered { counter-reset: stibok-li calc(var(--stibok-list-start, 1) - 1); }
.stibok-list.is-ordered > .stibok-list__item > .stibok-list__marker,
.stibok-list.marker-bullet > .stibok-list__item > .stibok-list__marker { font-size: var(--stibok-list-marker-size, inherit); }
.stibok-list__marker > svg,
.stibok-list__marker > img,
.stibok-list__marker-glyph { width: var(--stibok-list-marker-size, 1em); height: var(--stibok-list-marker-size, 1em); }
.stibok-list__marker > img,
.stibok-list__marker-glyph { object-fit: contain; }
.stibok-list__content { flex: 1 1 auto; min-width: 0; text-align: start; }
/* Ordered: real ordinals via a CSS counter in the marker span (styleable). */
.stibok-list.is-ordered > .stibok-list__item { counter-increment: stibok-li; }
.stibok-list.is-ordered > .stibok-list__item > .stibok-list__marker::before { content: counter( stibok-li ) "."; }
.stibok-list.is-ordered.ordered-style-decimal-paren > .stibok-list__item > .stibok-list__marker::before { content: counter( stibok-li ) ")"; }
.stibok-list.is-ordered.ordered-style-lower-alpha > .stibok-list__item > .stibok-list__marker::before { content: counter( stibok-li, lower-alpha ) "."; }
.stibok-list.is-ordered.ordered-style-upper-alpha > .stibok-list__item > .stibok-list__marker::before { content: counter( stibok-li, upper-alpha ) "."; }
.stibok-list.is-ordered.ordered-style-lower-roman > .stibok-list__item > .stibok-list__marker::before { content: counter( stibok-li, lower-roman ) "."; }
.stibok-list.is-ordered.ordered-style-upper-roman > .stibok-list__item > .stibok-list__marker::before { content: counter( stibok-li, upper-roman ) "."; }
/* marker=none → collapse the marker box. */
.stibok-list.marker-none > .stibok-list__item > .stibok-list__marker { display: none; }

/* List DOM prototype: the trailing "Add list item" control row (editor only). */
.stibok-list__item-add { list-style: none; }
.stibok-list__item-add .components-button { margin-block: 0.25rem 0; }

/* Divider block */
.stibok-divider { border: 0; border-block-start-style: solid; }
.stibok-divider.is-variant-dashed { border-block-start-style: dashed; }
.stibok-divider.is-variant-gradient { border-block-start-style: solid; border-image: linear-gradient(to right, transparent, currentColor 20%, currentColor 80%, transparent) 1; }
/* Icon variant (1.21): a flex row — two lines flanking a centered icon-ref. The
   root <div> sets `color` (token); lines + icon both inherit it (currentColor). */
.stibok-divider.is-variant-icon { display: flex; align-items: center; gap: var(--wp--preset--spacing--xs); border: 0; }
.stibok-divider__line { flex: 1 1 0; border-block-start-style: solid; }
.stibok-divider__icon { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; line-height: 0; }
.stibok-cluster.is-variant-badge {
	display: inline-flex;
	align-items: center;
	gap: var(--wp--preset--spacing--xxxs);
	padding: var(--wp--preset--spacing--xxxs) var(--wp--preset--spacing--xxs);
	border-radius: var(--wp--custom--radius--pill, 999px);
	background: var(--wp--preset--color--accent-soft);
	color: var(--wp--preset--color--accent);
	font-size: var(--wp--preset--font-size--xs);
	font-weight: 600;
	line-height: 1;
	width: fit-content;
}
.stibok-rating { display: inline-flex; align-items: center; position: relative; color: var(--wp--preset--color--warning); }
.stibok-rating__empty,
.stibok-rating__filled { display: inline-flex; gap: 0.125rem; }
.stibok-rating__filled { position: absolute; inset-inline-start: 0; top: 0; overflow: hidden; white-space: nowrap; }
/* Review-count text (the `count` part) sits after the stars. */
.stibok-rating__count { margin-inline-start: 0.5rem; font-size: 0.875em; color: var(--wp--custom--semantic--on-surface-muted); }
/* Body text base size (1.12): the standalone `size` attr was promoted to the
   appearance fontSize shortcut; this is the default when none is set. */
.stibok-text { font-size: var(--wp--preset--font-size--md); }
.stibok-icon { display: inline-flex; align-items: center; justify-content: center; width: fit-content; }
/* Boxed icon = a surface chip (variant, replaces the removed `boxed` bool). */
.stibok-icon.is-variant-boxed { background: var(--wp--preset--color--accent-soft); border-radius: var(--wp--custom--radius--md, 0.625rem); padding: var(--wp--preset--spacing--xxs); }
/* Image block (1.21): the figure is the root; `img` + `caption` are parts. The
   shape variant styles the img part (no overflow clip needed). */
.stibok-image { display: block; margin: 0; max-width: 100%; }
.stibok-image__img { display: block; max-width: 100%; height: auto; }
/* Shape variants (replace the removed `rounded` bool): plain = no class. */
.stibok-image.is-variant-rounded .stibok-image__img { border-radius: var(--wp--custom--radius--lg, 1rem); }
.stibok-image.is-variant-avatar { width: fit-content; }

/* Hosted video (CL2.V2) — a first-party <video>. Mirrors `.stibok-image` so the two media
   atoms behave identically inside a layout: the figure bounds the width, the media fills it.
   `display:block` on the <video> matters — a <video> is INLINE by default, which leaves a
   baseline gap under it that reads as a stray few pixels of padding no author asked for. */
.stibok-video { display: block; margin: 0; max-width: 100%; }
.stibok-video__video { display: block; max-width: 100%; height: auto; background: var(--wp--custom--semantic--surface-2); }
.stibok-video__caption { margin-block-start: var(--wp--preset--spacing--xxs); font-size: var(--wp--preset--font-size--sm); color: var(--wp--custom--semantic--on-surface-muted); line-height: 1.4; }

/* Content graphic (B1) — a placeable inline SVG / raster. The wrapper bounds the
   width (inline-size / max-inline-size from the instance, rem); the content fills it.
   `framed` (recipe variant) adds a padded/bordered surface via the compiled recipe. */
.stibok-graphic { display: block; margin: 0; max-width: 100%; }
.stibok-graphic__content { display: block; inline-size: 100%; block-size: auto; max-inline-size: 100%; }
.stibok-image.is-variant-avatar .stibok-image__img { border-radius: 50%; aspect-ratio: 1; object-fit: cover; width: 100%; height: 100%; }
.stibok-image__caption { margin-block-start: var(--wp--preset--spacing--xxs); font-size: var(--wp--preset--font-size--sm); color: var(--wp--custom--semantic--on-surface-muted); line-height: 1.4; }
/* CANVAS-ONLY (CL2.9a): image/render.php emits no <figcaption> when `caption` is empty, so
   the editor's placeholder is chrome the front never draws — one per tile in a gallery
   grid. `is-empty` (emitted by editor.js only) collapses it; hover / focus-within brings it
   back. Same idiom as `.stibok-table__caption.is-empty`. */
.stibok-image__caption.is-empty { opacity: 0; max-block-size: 0; margin-block: 0; overflow: hidden; transition: opacity 0.12s ease, max-block-size 0.12s ease; }
.stibok-image:hover .stibok-image__caption.is-empty, .stibok-image__caption.is-empty:focus-within { opacity: 1; max-block-size: 4rem; margin-block-start: var(--wp--preset--spacing--xxs); overflow: visible; }
/* Heading `highlight` part (1.21): a token-backed mark (curated format — never a
   raw inline color). The class is enforced server-side on every <mark>. */
.stibok-heading__highlight { background: var(--wp--preset--color--accent-soft); color: inherit; padding-inline: 0.15em; border-radius: var(--wp--custom--radius--sm, 0.25rem); }
.stibok-button { display: inline-flex; align-items: center; justify-content: center; gap: var(--wp--preset--spacing--xxxs); cursor: pointer; text-decoration: none; border: 0; }
/* Normalize a native <button class="stibok-button"> (the in-form submit, 2.7b) so
   it renders byte-for-byte like the <a> variant — strip UA chrome/font; the recipe
   variant supplies the background/color/padding for both element types. */
button.stibok-button { font: inherit; background: none; -webkit-appearance: none; appearance: none; }
/* Double-submit guard (2.7b): forms.js disables the submit button + flags the form
   while a submission is in flight — give the user visible feedback. */
button.stibok-button:disabled { cursor: default; opacity: 0.6; }
.stibok-form.is-submitting { cursor: progress; }
/* Button parts (1.22): label + leading/trailing icon-ref slots (C10 Role B —
   the icon sizes with the label text [1em] and inherits currentColor). */
.stibok-button__label { display: inline; }
.stibok-button__iconBefore,
.stibok-button__iconAfter { display: inline-flex; align-items: center; flex: 0 0 auto; }
.stibok-button__iconBefore > svg,
.stibok-button__iconAfter > svg,
.stibok-button__iconBefore > img,
.stibok-button__iconAfter > img { width: 1em; height: 1em; }
/* Icon block glyph part (1.22): the inner svg/img the icon block draws. */
.stibok-icon__glyph { display: block; }

/* ---------- Link trait (1.6) — stretched-link overlay ---------- */
/* has-stretched-link: establishes a new stacking context on the container.   */
/* stibok-stretched-link: absolutely-positioned overlay <a> covering the block.*/
/* z-index:1 on overlay; inner a/button get position:relative + z-index:2 so  */
/* they remain clickable above the overlay.                                    */
/* stibok-visually-hidden: accessible name for the overlay anchor (§4a).       */
.has-stretched-link { position: relative; }
.stibok-stretched-link {
	position: absolute;
	inset: 0;
	z-index: 1;
}
.has-stretched-link a:not(.stibok-stretched-link),
.has-stretched-link button {
	position: relative;
	z-index: 2;
}
.stibok-visually-hidden {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	white-space: nowrap;
	border: 0;
}

/* ---------- Position trait (1.7) — z-token scale + abs-child context ---------- */
/* Semantic z-index token scale: decor < sticky < overlay < modal < toast.
   Used by apply_position() (Traits); raw numeric z-index values are banned on the AI path. */
:root {
	--z-decor:   1;
	--z-sticky:  100;
	--z-overlay: 200;
	--z-modal:   300;
	--z-toast:   400;
}
/* has-abs-child: added by Traits::apply_position() to direct parent of absolute-positioned
   children (D16 guardrail). Makes the parent the positioning context. */
.has-abs-child { position: relative; }

/* ---------- Visibility trait (1.5) ---------- */
/* Viewport media-queries: documented exception to the container-query default.
   hide-mobile / hide-tablet / hide-desktop classes are emitted by Traits::apply_visibility().
   Decor nodes use the same class (set by render-helpers.php decor_node() hideMobile flag).
   Logical CSS is n/a for display:none rules (display is not a directional prop).
   DR10 (1.13): the @media blocks are NO LONGER static here — they are emitted from
   the configured breakpoints (settings.custom.breakpoints) via Render_Helpers::visibility_css(),
   attached on the FRONT via wp_add_inline_style('stibok-base', …) AND in the editor
   CANVAS via the block_editor_settings_all `styles` array (the iframe is not reached by
   enqueue_block_assets), so the hide thresholds track the theme config instead of being
   hardcoded and stay at parity front↔canvas (D18). See §1.13 DR10 carry-forward. */

/* ---------- Motion trait (reusable: sections, decor, any block) ---------- */
/* M3: motion is a FILE-BASED registry. A slug's `from` state is injected as inline
   --sm-* / --smh- custom props by Render_Helpers::motion(); these GENERIC rules
   compose them — there is NO per-slug CSS rule (a new two-state variant renders
   with no CSS edit). Composed transform/filter via CSS variables:
     --sm-ex/--sm-ey entrance offset, --sm-scale entrance scale,
     --sm-rotate entrance rotate, --sm-blur/--sm-filter entrance filter,
     --sm-px/--sm-py scroll (parallax) offset (set by interactions.js). */
/* MV.2: register every transform-composition slot as a TYPED custom property so the
   `translate`/`rotate`/`scale` LONGHANDS below compose predictably. `inherits:false` keeps
   a child from picking up a parent's motion offset (V1 unregistered vars inherited — a
   latent double-offset bug); each `initial-value` == the neutral fallback the matching
   `var(--slot, …)` used in V1, so a registered-but-unset slot resolves to the SAME value it
   did before (a registered prop resolves to its `initial-value`, NOT the var() 2nd-arg
   fallback, when unset — the two MUST match or a baseline shifts; asserted by
   MotionCompositionGoldenTest::test_property_initial_values_match_neutral_fallbacks). On an
   engine without @property these at-rules are ignored and the identical var() fallbacks take
   over — graceful. (--sm-blur/--sm-filter stay UNregistered: filter is not a longhand slot;
   --sm-stagger/--si/--sm-delay/etc. stay unregistered because the stagger relies on
   inheritance.) */
@property --sm-ex { syntax: "<length>"; inherits: false; initial-value: 0px; }
@property --sm-ey { syntax: "<length>"; inherits: false; initial-value: 0px; }
@property --sm-px { syntax: "<length>"; inherits: false; initial-value: 0px; }
@property --sm-py { syntax: "<length>"; inherits: false; initial-value: 0px; }
@property --smh-x { syntax: "<length>"; inherits: false; initial-value: 0px; }
@property --smh-y { syntax: "<length>"; inherits: false; initial-value: 0px; }
@property --sm-scale  { syntax: "<number>"; inherits: false; initial-value: 1; }
@property --smh-scale { syntax: "<number>"; inherits: false; initial-value: 1; }
@property --sm-rotate  { syntax: "<angle>"; inherits: false; initial-value: 0deg; }
@property --smh-rotate { syntax: "<angle>"; inherits: false; initial-value: 0deg; }
/* MV.3: loop composition slots. A loop `@keyframes` animates these (var path) instead of
   the whole `transform` property, so a running loop no longer clobbers a coexisting
   entrance/hover/parallax or a decor's static rotate. Registered typed + non-inheriting +
   identity initial-value (== the var() fallback used below), so an element with no loop
   resolves them to identity and the composition is unchanged (behavior-neutral until a
   loop drives them). A SOLE-writer loop bypasses these and animates the isolated longhand
   directly for compositor promotion (see Render_Helpers::compile_keyframe_step). */
@property --sm-loop-x { syntax: "<length>"; inherits: false; initial-value: 0px; }
@property --sm-loop-y { syntax: "<length>"; inherits: false; initial-value: 0px; }
@property --sm-loop-scale  { syntax: "<number>"; inherits: false; initial-value: 1; }
@property --sm-loop-rotate { syntax: "<angle>"; inherits: false; initial-value: 0deg; }

.stibok-motion {
	/* MV.2: the V1 single composed `transform` split into the CSS Transforms L2 individual
	   longhands translate/rotate/scale, each a calc() over the SAME slot vars — so cross-axis
	   layers (entrance offset --sm-ex/ey, parallax --sm-px/py, hover --smh-x/y; entrance ×
	   hover scale; entrance + hover rotate) live on SEPARATE properties and never clobber one
	   another. Behavior-neutral vs the V1 single transform: translate stays first, and a
	   uniform `scale` commutes with a z-axis `rotate`. The legacy single `transform:`
	   composition is re-emitted in the `@supports not (translate: 0px)` fallback at the end of
	   this block for engines without individual transform properties (total-loss guard). */
	translate:
		calc(var(--sm-ex, 0px) + var(--sm-px, 0px) + var(--smh-x, 0px) + var(--sm-loop-x, 0px))
		calc(var(--sm-ey, 0px) + var(--sm-py, 0px) + var(--smh-y, 0px) + var(--sm-loop-y, 0px));
	rotate: calc(var(--sm-rotate, 0deg) + var(--smh-rotate, 0deg) + var(--sm-loop-rotate, 0deg) + var(--stibok-decor-rotate, 0deg));
	scale: calc(var(--sm-scale, 1) * var(--smh-scale, 1) * var(--sm-loop-scale, 1));
	/* MV.4: `--stibok-decor-rotate` (0deg for a non-decor element — identity, behavior-neutral)
	   is folded into the `rotate` longhand so a MOTION decor's static tilt composes on the SAME
	   property as its entrance/hover/loop rotation. This replaces the old
	   `.stibok-decor__fx.stibok-motion` transform re-statement (deleted below): decor is no
	   longer a special case, so decor × hover / decor × loop / decor × entrance all compose
	   without clobber. */
	/* MV.3: the fourth composition layer `--sm-loop-*` (identity when no loop) lets a
	   COMPOSED loop animate a var without seizing the whole longhand — so a loop + a
	   coexisting entrance/hover/parallax compose instead of clobbering. A sole-writer loop
	   animates the longhand DIRECTLY (its keyframe emits `rotate:`/`translate:`/`scale:`,
	   which overrides these calc()s outright — correct, since nothing else writes them). */
	/* HV.8: NO `filter` at rest. A `filter` (even the no-op `brightness(1)
	   drop-shadow(0 0 0 transparent) blur(0)`) renders the element into a filter
	   buffer sized to the border-box, which CLIPS the element's own `box-shadow`
	   (an elevated card's shadow vanished — "shadows are being cut"). So the filter
	   is applied ONLY in the states that actually use it: the reveal blur-in
	   (`[data-stibok-reveal]` below) and the hover brighten/glow
	   (`.stibok-motion[data-stibok-hover]:hover` below). At plain rest there is no
	   filter, so `box-shadow` paints in full. */
}

/* Every reveal element also carries .stibok-motion, so the rule is scoped with
   both (specificity 0-2-0) to WIN over a block's own base `transition` (e.g.
   .stibok-card's recipe transition, 0-1-0) — otherwise that `transition: all`
   would clobber the per-child stagger delay (caught in browser, 1.19). */
.stibok-motion[data-stibok-reveal] {
	opacity: 0;
	/* Reveal delay = configured delay + the stagger step (1.19, §4a). For a
	   non-staggered element --si is unset (0), so the stagger term is 0; a
	   staggered child inherits --sm-stagger and carries its own --si. */
	--sm-reveal-delay: calc(
		var(--sm-delay, 0ms) +
		min(calc(var(--sm-stagger, 60ms) * var(--si, 0)), var(--sm-stagger-max, 600ms))
	);
	/* MV.2: transition the longhands (translate/rotate/scale) that the composition now uses,
	   not the old single `transform`. Same duration/easing/delay, so the reveal reads identically. */
	transition:
		opacity var(--sm-duration, 600ms) var(--sm-easing, cubic-bezier(0.16, 1, 0.3, 1)) var(--sm-reveal-delay),
		translate var(--sm-duration, 600ms) var(--sm-easing, cubic-bezier(0.16, 1, 0.3, 1)) var(--sm-reveal-delay),
		rotate var(--sm-duration, 600ms) var(--sm-easing, cubic-bezier(0.16, 1, 0.3, 1)) var(--sm-reveal-delay),
		scale var(--sm-duration, 600ms) var(--sm-easing, cubic-bezier(0.16, 1, 0.3, 1)) var(--sm-reveal-delay),
		filter var(--sm-duration, 600ms) var(--sm-easing, cubic-bezier(0.16, 1, 0.3, 1)) var(--sm-reveal-delay);
	will-change: opacity, translate, rotate, scale, filter;
	/* HV.8: the blur-in filter lives HERE (pre-reveal), not on the base `.stibok-motion`
	   (which is filter-free so a rest box-shadow is not clipped). `--sm-filter` is the
	   escape hatch; absent, the fallback composes the blur slot so blur-in works. MV.4:
	   `var(--stibok-decor-filter,)` (empty for a non-decor element) folds a decor's rest
	   softness into the blur-in so a decor WITH an entrance keeps its filter through reveal. */
	filter: var(--stibok-decor-filter,) var(--sm-filter, blur(var(--sm-blur, 0)));
}

/* Revealed: settle the COMPOSITE properties directly to the rest state. Resetting
   the properties (not the --sm-* vars) is what lets the inline from-state vars win
   for the initial frame yet be overridden here — an inline custom prop cannot beat
   a later rule's real `transform`/`filter` declaration (M3 cascade note). */
.stibok-motion[data-stibok-reveal].is-revealed {
	opacity: 1;
	/* Settle ONLY the entrance contribution (drop --sm-ex/ey/scale/rotate/blur) but
	   KEEP parallax (--sm-px/py) and hover (--smh-*) so those still work on a revealed
	   element — the settle longhands read only the parallax/hover slots, never the entrance
	   ones. MV.2: re-state the LONGHAND PROPERTIES (not `--sm-ey:0` etc.). The render still
	   emits the from-state INLINE (`--sm-ey:var(--sm-distance,…)`), and an inline custom prop
	   BEATS a stylesheet `--sm-ey:0` reset — but it cannot beat a later rule's real
	   `translate`/`rotate`/`scale` declaration. So settling the properties (V1's cascade
	   trick, now per-longhand) is what lets the inline from-state win the first frame yet be
	   overridden here. */
	/* MV.3: keep the loop layer (--sm-loop-*) in the settled composition so a COMPOSED
	   loop on a revealed element keeps animating after the entrance settles (the entrance
	   contribution --sm-ex/ey/scale/rotate is still dropped). */
	translate:
		calc(var(--sm-px, 0px) + var(--smh-x, 0px) + var(--sm-loop-x, 0px))
		calc(var(--sm-py, 0px) + var(--smh-y, 0px) + var(--sm-loop-y, 0px));
	/* MV.4: fold `--stibok-decor-rotate` (identity for a non-decor element) so a revealed
	   MOTION decor keeps its static tilt after the entrance settles (the entrance --sm-rotate
	   is still dropped). Replaces the deleted decor `.is-revealed` transform re-statement. */
	rotate: calc(var(--smh-rotate, 0deg) + var(--sm-loop-rotate, 0deg) + var(--stibok-decor-rotate, 0deg));
	scale: calc(var(--smh-scale, 1) * var(--sm-loop-scale, 1));
	/* HV.8: settle to NO filter so a revealed card's box-shadow is not clipped. The
	   hover brighten/glow is re-applied on :hover by the rule below (0-4-0, later in
	   source → wins over this 0-3-0 settle for a revealed element that IS hovered).
	   MV.4: `var(--stibok-decor-filter, none)` is `none` for a non-decor element (box-shadow
	   unclipped) but keeps a revealed decor's softness (decor has no box-shadow — safe).
	   MV.6/MV.4-L1: the `--sm-loop-filter` slot is NOT composed here — `none <fn>` is invalid
	   CSS, so a non-decor filter loop would drop the whole declaration. A composed filter loop
	   is instead gated onto `.is-lfilter` below (empty fallback, always valid). */
	filter: var(--stibok-decor-filter, none);
}

/* MV.6 / MV.4-L1: a revealed element carrying a COMPOSED filter loop (render marks it
   `.is-lfilter`, mirroring HV.9's `.is-hfilter`) composes the animating `--sm-loop-filter`
   slot with the decor filter. The decor-filter fallback is EMPTY (not `none`) so a non-decor
   filter loop emits a valid `filter: <loop-fn>` instead of the invalid `filter: none <fn>`.
   Specificity 0-3-0 == `.is-revealed`; later in source, so it wins for a filter-loop reveal.
   Unreachable with bundled presets (none animate filter); correct for a future filter loop. */
.stibok-motion[data-stibok-reveal].is-revealed.is-lfilter {
	filter: var(--stibok-decor-filter,) var(--sm-loop-filter,);
}

/* MV.6(b) — opacity ownership. `opacity` is ATOMIC (no longhand, not calc-composable), so a
   loop that animates opacity (e.g. `breathe-glow`) and an entrance reveal (opacity 0→1)
   cannot compose — the LOOP owns steady-state opacity. When both coexist the render marks
   `.is-loop-opacity`; the reveal then yields opacity to the loop rather than declaring an
   `opacity:0`/`opacity:1` the running animation silently overrides. `opacity: revert` drops
   the reveal's opacity declarations back to the element's own value (1) — the loop's
   `@keyframes` drives the live opacity under `no-preference`. Under reduced motion the loop
   binding is off and the reduced-motion reveal rule below forces opacity:1, so the element is
   never hidden. Placed AFTER `.is-revealed` (0-3-0) so it wins on source order. The reveal's
   transform/filter hand-off is untouched — only opacity yields. */
.stibok-motion[data-stibok-reveal].is-loop-opacity {
	opacity: revert;
}

/* Once the entrance reveal has finished (interactions.js adds `.is-settled` on
   transitionend), swap the long reveal transition (+ its stagger delay) for the short
   interactive timing so a later HOVER on a revealed element isn't stuck on the 600ms
   reveal duration/delay, and parallax tracks scroll with only a light smoothing. */
.stibok-motion[data-stibok-reveal].is-settled {
	/* MV.2: interactive-timing transition on the longhands (was `transform`). */
	transition:
		translate var(--smh-duration, 200ms) var(--smh-easing, ease),
		rotate var(--smh-duration, 200ms) var(--smh-easing, ease),
		scale var(--smh-duration, 200ms) var(--smh-easing, ease),
		filter var(--smh-duration, 200ms) var(--smh-easing, ease);
}

/* MV.4: the decor `.stibok-decor__fx.stibok-motion` transform re-statement (and its
   `.is-revealed` variant) are GONE. Decor rotate is now an ordinary `rotate` longhand slot
   (`--stibok-decor-rotate`, folded into the base `.stibok-motion` rotate composition above)
   and decor filter an ordinary filter slot (`--stibok-decor-filter`, folded into the reveal/
   settle/hover filter rules) — so decor composes through the SAME longhand pipeline as every
   other layer. This removes the class of clobber the re-statement papered over: decor × hover
   translate/scale (the re-statement dropped `--smh-*`) and decor × loop now compose cleanly. */

/* ---------- Hover trigger (M3, CSS :hover, no observer) ---------- */
/* A slug's rest state is the neutral fallback of each `var(--smh-<slot>, …)` in the
   composed `.stibok-motion` rule above (NOT emitted inline — HV.7: an inline rest slot
   would outrank this stylesheet :hover swap and pin the var, so hover never fired). Its
   hover target IS inline as --smh-<slot>-h. :hover swaps each slot to its -h target so
   the transition is data-driven with no per-slug rule. */
/* All slots default to a NO-OP (identity transform / brightness 1 / a transparent
   drop-shadow) so a hover that only uses one property never clobbers the element's
   own box-shadow or filter. The shadow is a `drop-shadow()` (composes with the
   element's box-shadow — unlike `box-shadow`, which would override the recipe's) and
   appears ONLY on hover (the rest state is transparent). */
/* A hover element ALSO carries `.stibok-motion` (added by the render) so the hover
   contribution composes into the one `.stibok-motion` translate/rotate/scale LONGHANDS
   above (MV.2 — the hover FILTER is applied separately, only on the `.is-hfilter:hover`
   rule below — HV.8/9). This rule
   only owns the hover TRANSITION so a hover-only element (no reveal) still animates the
   swap. */
[data-stibok-hover] {
	/* MV.2: transition the longhands the composed hover now swaps (was `transform`). */
	transition:
		translate var(--smh-duration, 200ms) var(--smh-easing, ease),
		rotate var(--smh-duration, 200ms) var(--smh-easing, ease),
		scale var(--smh-duration, 200ms) var(--smh-easing, ease),
		filter var(--smh-duration, 200ms) var(--smh-easing, ease);
}
[data-stibok-hover]:hover {
	--smh-x: var(--smh-x-h, 0px);
	--smh-y: var(--smh-y-h, 0px);
	--smh-scale: var(--smh-scale-h, 1);
	--smh-rotate: var(--smh-rotate-h, 0deg);
	--smh-bright: var(--smh-bright-h, 1);
	--smh-glow: var(--smh-glow-h, 0 0 0 transparent);
}
/* HV.8/HV.9: the hover filter is applied ONLY on :hover AND only to elements the render
   marked `.is-hfilter` (their hover actually uses a bright/glow slot). Two reasons the
   `.is-hfilter` gate matters: (1) a no-op `filter` (brightness(1) drop-shadow(0 0 0
   transparent)) still buffers the element and CLIPS its own box-shadow — so a
   transform-only hover (grow/tilt/nudge/shrink/sink) must get NO filter or its shadow
   would vanish during hover (HV.9, caught in review). (2) at rest there is no filter at
   all (see `.stibok-motion`) so a rest box-shadow is never clipped. brightness + a
   drop-shadow glow read the swapped --smh-bright/--smh-glow. Specificity 0-4-0 (later in
   source than `.is-revealed` 0-3-0) so it wins for a revealed element that is hovered.
   The drop-shadow (not box-shadow) composes with the element's own box-shadow. */
.stibok-motion[data-stibok-hover].is-hfilter:hover {
	/* MV.4: `var(--stibok-decor-filter,)` (empty for a non-decor element) folds a decor's
	   rest softness into a filter-hover so a hovered decor keeps its glow (decor × hover-filter
	   — latent clobber #3, now composed). */
	filter: var(--stibok-decor-filter,) brightness(var(--smh-bright, 1)) drop-shadow(var(--smh-glow, 0 0 0 transparent));
}
/* NOTE: a block recipe's own `:hover` delta is gated `:not([data-stibok-hover])` by the
   recipe compiler, so a motion hover fully owns the `:hover` state — the composed
   `.stibok-motion` transform above (+ the `--smh-*` swap) is the sole hover effect, no
   specificity fight with a recipe hover needed. */

@media (prefers-reduced-motion: reduce) {
	.stibok-motion[data-stibok-reveal] {
		opacity: 1;
		transition: none;
		/* MV.2: the V1 blanket `transform:none` no longer neutralizes the longhands, so zero
		   translate/rotate/scale directly. `transform:none` kept for the @supports-fallback
		   engines (where the composition is still a single `transform`). MV.4: neutralize only
		   the ANIMATED slots (entrance offset/rotate/scale, loop) while PRESERVING a decor's
		   STATIC design terms — `rotate: var(--stibok-decor-rotate, 0deg)` keeps a motion
		   decor's tilt (it is 0deg/`none`-equivalent for a non-decor element), and
		   `filter: var(--stibok-decor-filter, none)` keeps a decor's rest softness (`none` for
		   a non-decor element, so its box-shadow stays unclipped). The loop is already off under
		   reduced motion — its `@keyframes` animation is gated `@media (prefers-reduced-motion:
		   no-preference)`, so `--sm-loop-*` sits at its identity initial-value here. This
		   replaces the deleted decor-specific reduced-motion re-statement. */
		translate: none;
		rotate: var(--stibok-decor-rotate, 0deg);
		scale: none;
		transform: none;
		filter: var(--stibok-decor-filter, none);
	}
	[data-stibok-hover] { transition: none; }
	[data-stibok-hover]:hover {
		--smh-x: 0px;
		--smh-y: 0px;
		--smh-scale: 1;
		--smh-rotate: 0deg;
		--smh-bright: 1;
		--smh-glow: 0 0 0 transparent;
	}
	/* HV.9: under reduced motion the --smh-* slots above are all identity, so the hover
	   filter would resolve to the no-op brightness(1) drop-shadow(transparent) that clips
	   box-shadow. Drop it entirely (a11y: no hover effect anyway). */
	.stibok-motion[data-stibok-hover].is-hfilter:hover { filter: none; }
}

/* MV.2 — total-loss guard (review finding 2 / risk HIGH). The composition above uses the
   CSS Transforms L2 individual `translate`/`rotate`/`scale` properties (floors: Chrome 104 /
   Safari 14.1 / Firefox 72, mid-2022). On an engine WITHOUT them those longhand declarations
   are simply dropped and the element would lose ALL composed motion (a far worse failure than
   the degraded loop smoothness of a missing @property) — so re-emit the V1 SINGLE `transform:`
   composition here. Degraded but functional: cross-axis layers fold back into one `transform`
   (V1's known clobber surface) instead of independent longhands, but motion still renders. MV.4:
   decor no longer has its own `transform` re-statement (its rotate is a `rotate` longhand slot),
   so a STATIC decor's tilt would vanish on these engines — re-emit it as a single `transform`
   here too; a MOTION decor gets its full composition (incl. `--stibok-decor-rotate`) from the
   `.stibok-motion` fallback below, which wins over this rule (same specificity, later in source).
   KNOWN LIMIT: on this fallback path a motion-decor's static tilt is dropped under reduced motion
   (the reduced-motion `transform:none` above) — an aria-hidden cosmetic loss on a pre-2022 engine. */
@supports not (translate: 0px) {
	.stibok-decor__fx {
		transform: rotate(var(--stibok-decor-rotate, 0deg));
	}
	.stibok-motion {
		transform:
			translate3d(
				calc(var(--sm-ex, 0px) + var(--sm-px, 0px) + var(--smh-x, 0px) + var(--sm-loop-x, 0px)),
				calc(var(--sm-ey, 0px) + var(--sm-py, 0px) + var(--smh-y, 0px) + var(--sm-loop-y, 0px)),
				0
			)
			scale(calc(var(--sm-scale, 1) * var(--smh-scale, 1) * var(--sm-loop-scale, 1)))
			rotate(calc(var(--sm-rotate, 0deg) + var(--smh-rotate, 0deg) + var(--sm-loop-rotate, 0deg) + var(--stibok-decor-rotate, 0deg)));
	}
	.stibok-motion[data-stibok-reveal] {
		transition:
			opacity var(--sm-duration, 600ms) var(--sm-easing, cubic-bezier(0.16, 1, 0.3, 1)) var(--sm-reveal-delay),
			transform var(--sm-duration, 600ms) var(--sm-easing, cubic-bezier(0.16, 1, 0.3, 1)) var(--sm-reveal-delay),
			filter var(--sm-duration, 600ms) var(--sm-easing, cubic-bezier(0.16, 1, 0.3, 1)) var(--sm-reveal-delay);
		will-change: opacity, transform, filter;
	}
	.stibok-motion[data-stibok-reveal].is-revealed {
		transform:
			translate3d(
				calc(var(--sm-px, 0px) + var(--smh-x, 0px) + var(--sm-loop-x, 0px)),
				calc(var(--sm-py, 0px) + var(--smh-y, 0px) + var(--sm-loop-y, 0px)),
				0
			)
			scale(calc(var(--smh-scale, 1) * var(--sm-loop-scale, 1)))
			rotate(calc(var(--smh-rotate, 0deg) + var(--sm-loop-rotate, 0deg) + var(--stibok-decor-rotate, 0deg)));
	}
	.stibok-motion[data-stibok-reveal].is-settled {
		transition:
			transform var(--smh-duration, 200ms) var(--smh-easing, ease),
			filter var(--smh-duration, 200ms) var(--smh-easing, ease);
	}
	[data-stibok-hover] {
		transition:
			transform var(--smh-duration, 200ms) var(--smh-easing, ease),
			filter var(--smh-duration, 200ms) var(--smh-easing, ease);
	}
}

/* ===========================================================================
   Forms (2.5 layout; 2.7c-2 control skins + layout flexibility). The control skin
   (bg/border/radius/padding) comes from the `input` recipe (`.stibok-input`); these
   rules add the structural grid, the label/help/error part chrome, the custom
   checkbox/radio/select skins (appearance:none + base-select PE), the icon
   affordance, the option layouts, and the `columns`/`span` + label-position layout.
   rem-only (boxes scale in `em`), logical props (RTL-safe), semantic tokens, no hex.
   A multi-column form collapses to one column when the nearest `stibok` slot is
   narrow (Render_Helpers::container_css() overrides --stibok-form-cols).
   =========================================================================== */
.stibok-form {
	display: grid;
	/* grid-template-columns is set inline (render.php) reading --stibok-form-cols so the
	   @container narrow-collapse rule can override the var to 1. */
	align-items: start;
}

.stibok-field {
	/* One shared radius token for the form's control surfaces (cards + the group
	   options-wrapper), matched to the input recipe's radius.md so a styled form reads
	   as a single border/radius language. */
	--stibok-field-radius: var(--wp--custom--radius--md, 0.5rem);
	grid-column: 1 / -1; /* `span` < full overrides this inline (--stibok-field-span). */
	display: flex;
	flex-direction: column;
	gap: 0.375rem;
	min-inline-size: 0;
	/* fieldset reset (radio/checkbox groups render as <fieldset>). */
	border: 0;
	margin: 0;
	padding: 0;
}

/* Non-field children (submit button, intro/legal copy) span the full row by default. */
.stibok-form > :not(.stibok-field) { grid-column: 1 / -1; }

/* Submit alignment (enables the inline newsletter pattern — a content-sized button
   that does not span the row). */
/* `.stibok-form__notice` is excluded here so the CANVAS-ONLY warning strip (below) always
   spans the row, whatever the submit alignment is. It never exists on the front. */
.stibok-form.is-submitalign-start > :not(.stibok-field):not(.stibok-form__notice)  { grid-column: auto; justify-self: start; }
.stibok-form.is-submitalign-center > :not(.stibok-field):not(.stibok-form__notice) { grid-column: auto; justify-self: center; }
.stibok-form.is-submitalign-end > :not(.stibok-field):not(.stibok-form__notice)    { grid-column: auto; justify-self: end; }

/* CANVAS-ONLY (CL2.9d) — the missing-recipient warning. `is-missing-recipient` and
   `.stibok-form__notice` are emitted by editor.js only (form/render.php writes neither), so
   this block is editor-scoped by construction, like the grid-lightbox `is-editing` affordance.
   WHY it exists: with no `submit.to` the native driver refuses to send and returns 500, so the
   visitor's message is DISCARDED — a silent, total failure the old affordance (the words "No
   recipient set yet." inside a collapsed inspector panel) could not surface in the flow that
   actually produces such forms: inserting a pattern or generating a page.
   Admin-theme colour + currentColor only, no hardcoded hex. */
.stibok-form.is-missing-recipient {
	/* Drawn INSIDE the form: `.stibok-section__content` sets `overflow-x: clip`, so a positive
	   offset gets its left and right edges cropped and the "box" reads as two stray horizontal
	   rules (measured — review LOW-5). */
	outline: 0.0625rem dashed var( --wp-admin-theme-color, currentColor );
	outline-offset: -0.0625rem;
}
.stibok-form > .stibok-form__notice {
	grid-column: 1 / -1;
	justify-self: stretch;
	padding: 0.5rem 0.75rem;
	border-inline-start: 0.1875rem solid var( --wp-admin-theme-color, currentColor );
	background: color-mix( in srgb, var( --wp-admin-theme-color, currentColor ) 10%, transparent );
	color: currentColor;
	font-size: var( --wp--preset--font-size--sm, 0.875rem );
	line-height: 1.4;
}
.stibok-field__label {
	font-size: var(--wp--preset--font-size--sm);
	font-weight: 600;
	color: var(--wp--custom--semantic--on-surface);
	padding: 0; /* legend reset */
}
.stibok-field__required {
	color: var(--wp--custom--semantic--danger);
	margin-inline-start: 0.125rem;
}

.stibok-input,
.stibok-field__control {
	inline-size: 100%;
	box-sizing: border-box;
	font: inherit;
	/* Normalize native chrome so the recipe skin owns the look; the select dropdown
	   arrow is replaced by .stibok-field__chevron (PE) / the base-select picker-icon. */
	appearance: none;
}

.stibok-field__help {
	margin: 0;
	font-size: var(--wp--preset--font-size--sm);
	color: var(--wp--custom--semantic--on-surface-muted);
}

.stibok-field__error {
	font-size: var(--wp--preset--font-size--sm);
	color: var(--wp--custom--semantic--danger);
}
.stibok-field__error:empty { display: none; }

/* Live-validation invalid state (set by forms.js via the Constraint Validation
   API + server-mapped 422 errors). Token-driven; no hardcoded colors. */
.stibok-field__control[aria-invalid="true"],
.stibok-field__box[aria-invalid="true"] {
	border-color: var(--wp--custom--semantic--danger);
	outline-color: var(--wp--custom--semantic--danger);
}

/* Size variants — scale the control font (the box + icon ride `em`, so they follow);
   the label keeps its own size. */
.stibok-field.is-size-sm { font-size: var(--wp--preset--font-size--sm); }
.stibok-field.is-size-lg { font-size: var(--wp--preset--font-size--lg); }

/* --- Control wrapper (icon affordance + select trigger) --------------------- */
.stibok-field__controlWrap {
	position: relative;
	display: flex;
	align-items: center;
}
.stibok-field__controlWrap > .stibok-field__control { inline-size: 100%; }

/* Inline icon — absolutely placed inside the wrapper; the control reserves room on
   the matching logical side. */
.stibok-field__icon {
	position: absolute;
	display: inline-flex;
	align-items: center;
	inline-size: 1em;
	block-size: 1em;
	pointer-events: none;
	color: var(--wp--custom--semantic--on-surface-muted);
	/* em-based so the offset + reserved gutter track the control font (is-size-*). */
	inset-inline-start: 0.7em;
}
.stibok-field__controlWrap.is-icon-trailing .stibok-field__icon {
	inset-inline-start: auto;
	inset-inline-end: 0.7em;
}
.stibok-field__controlWrap.is-icon-leading > .stibok-field__control  { padding-inline-start: 2.2em; }
.stibok-field__controlWrap.is-icon-trailing > .stibok-field__control { padding-inline-end: 2.2em; }

/* --- Select: fallback chevron (PE) + base-select picker --------------------- */
.stibok-field__controlWrap.is-select > .stibok-field__control { padding-inline-end: 2rem; }
.stibok-field__chevron {
	position: absolute;
	inset-inline-end: 0.75rem;
	inset-block-start: 50%;
	transform: translateY(-50%);
	pointer-events: none;
	color: var(--wp--custom--semantic--on-surface-muted);
}
/* A multiple <select> is a listbox (no dropdown) — no chevron / reserved space. */
.stibok-field__controlWrap.is-multiple > .stibok-field__control { padding-inline-end: 0; }
.stibok-field__controlWrap.is-multiple .stibok-field__chevron { display: none; }

@supports (appearance: base-select) {
	/* Opt the <select> into the Customizable Select API; the fallback chevron is
	   replaced by the picker-icon, themed dropdown + selectedcontent + checkmark. */
	select.stibok-field__control { appearance: base-select; }
	.stibok-field__controlWrap.is-select > .stibok-field__chevron { display: none; }
	select.stibok-field__control::picker-icon {
		color: var(--wp--custom--semantic--on-surface-muted);
		transition: transform 0.15s ease;
	}
	select.stibok-field__control:open::picker-icon { transform: rotate(180deg); }
	select.stibok-field__control selectedcontent { display: inline-flex; align-items: center; }
	select.stibok-field__control::picker(select) {
		appearance: base-select;
		background: var(--wp--custom--semantic--surface);
		border: 0.0625rem solid var(--wp--custom--semantic--border);
		border-radius: var(--wp--custom--radius--md, 0.5rem);
		box-shadow: var(--wp--preset--shadow--sm);
		padding-block: 0.25rem;
	}
	select.stibok-field__control option {
		display: flex;
		align-items: center;
		gap: 0.5rem;
		padding-block: 0.375rem;
		padding-inline: 0.75rem;
	}
	/* Theme every option highlight (hover / keyboard focus / current) with the brand
	   accent so no native-blue UA highlight leaks through. */
	select.stibok-field__control option:hover,
	select.stibok-field__control option:focus,
	select.stibok-field__control option:checked,
	select.stibok-field__control option:where(:active, [aria-selected="true"]) {
		background: color-mix(in srgb, var(--wp--custom--semantic--accent) 14%, transparent);
		color: var(--wp--custom--semantic--on-surface);
	}
	select.stibok-field__control option:focus { outline: none; }
	select.stibok-field__control option::checkmark {
		content: "✓";
		color: var(--wp--custom--semantic--accent);
	}
}

/* --- Custom checkbox / radio box (appearance:none; tick/dot = ::before) ----- */
.stibok-field__box {
	appearance: none;
	flex: 0 0 auto;
	inline-size: 1.15em;
	block-size: 1.15em;
	margin: 0;
	display: inline-grid;
	place-content: center;
	border: 0.0625rem solid var(--wp--custom--semantic--border);
	background: var(--wp--custom--semantic--surface);
	cursor: pointer;
	transition: background-color 0.15s ease, border-color 0.15s ease;
}
.stibok-field__box[type="radio"]    { border-radius: 50%; }
.stibok-field__box[type="checkbox"] { border-radius: 0.25rem; }
.stibok-field__box::before {
	content: "";
	inline-size: 0.65em;
	block-size: 0.65em;
	background: var(--wp--custom--semantic--on-accent);
	transform: scale(0);
	transition: transform 0.12s ease-in-out;
}
.stibok-field__box[type="radio"]::before { border-radius: 50%; }
.stibok-field__box[type="checkbox"]::before {
	clip-path: polygon(13% 50%, 34% 71%, 87% 18%, 97% 30%, 34% 92%, 3% 60%);
}
.stibok-field__box:checked {
	background: var(--wp--custom--semantic--accent);
	border-color: var(--wp--custom--semantic--accent);
}
.stibok-field__box:checked::before { transform: scale(1); }
.stibok-field__box:focus-visible {
	outline: 0.125rem solid var(--wp--custom--semantic--accent);
	outline-offset: 0.125rem;
}

/* --- Option rows + layouts (radio/checkbox groups) ------------------------- */
.stibok-field__option {
	display: flex;
	align-items: center;
	gap: 0.5rem;
	font-weight: 400;
}
.stibok-field__optionLabel { min-inline-size: 0; }

fieldset.stibok-field > .stibok-field__control {
	display: flex;
	flex-direction: column;
	gap: 0.5rem;
	/* One shared radius for every form surface (cards, the group options-wrapper, and —
	   via the input recipe — the inputs all resolve to radius.md) so a styled form reads
	   as a single system. Invisible until a surface (e.g. a form-parts `fieldControl`
	   background cascade) is applied. */
	border-radius: var(--stibok-field-radius);
}
fieldset.stibok-field.is-optlayout-inline > .stibok-field__control {
	flex-direction: row;
	flex-wrap: wrap;
	column-gap: 1rem;
}
fieldset.stibok-field.is-optlayout-grid > .stibok-field__control {
	display: grid;
	grid-template-columns: repeat(var(--stibok-opt-cols, 2), minmax(0, 1fr));
}
/* Stack/inline groups hug their option content, so a `fieldControl` background cascade
   reads as a surface AROUND the options — not a full-width bar behind a couple of
   checkboxes. The grid layout keeps its full-width tracks (the cards fill the row). */
fieldset.stibok-field:not(.is-optlayout-grid) > .stibok-field__control {
	inline-size: fit-content;
	max-inline-size: 100%;
}

/* Card-style options — each row a bordered, selectable surface. The border weight/color
   + radius match the input recipe's default control skin (0.0625rem solid {border} +
   radius.md), so cards and inputs share one border language. */
.stibok-field.is-optstyle-cards .stibok-field__option {
	border: 0.0625rem solid var(--wp--custom--semantic--border);
	border-radius: var(--stibok-field-radius);
	padding-block: 0.5rem;
	padding-inline: 0.75rem;
	background: var(--wp--custom--semantic--surface);
	cursor: pointer;
	transition: background-color 0.15s ease, border-color 0.15s ease;
}
.stibok-field.is-optstyle-cards .stibok-field__option:has(:checked) {
	border-color: var(--wp--custom--semantic--accent);
	background: color-mix(in srgb, var(--wp--custom--semantic--accent) 10%, transparent);
}

/* Single consent checkbox — the label wraps the skinned box + the option label. */
.stibok-field.is-kind-checkbox .stibok-field__label {
	display: flex;
	align-items: center;
	gap: 0.5rem;
	font-weight: 400;
}

/* --- Label position (form-wide default cascades; per-field override wins) ----
   The form-scope selectors are wrapped in :where() so their specificity is 0 — a
   per-field `.stibok-field.is-labelpos-*` rule (higher specificity) always wins. */
:where(.stibok-form.is-labelpos-inline) .stibok-field:not(.is-kind-radio):not(.is-kind-checkbox),
.stibok-field.is-labelpos-inline {
	display: grid;
	grid-template-columns: var(--stibok-label-width, max-content) minmax(0, 1fr);
	align-items: center;
	column-gap: 0.75rem;
	row-gap: 0.375rem;
}
:where(.stibok-form.is-labelpos-inline) .stibok-field:not(.is-kind-radio):not(.is-kind-checkbox) > .stibok-field__help,
:where(.stibok-form.is-labelpos-inline) .stibok-field:not(.is-kind-radio):not(.is-kind-checkbox) > .stibok-field__error,
.stibok-field.is-labelpos-inline > .stibok-field__help,
.stibok-field.is-labelpos-inline > .stibok-field__error { grid-column: 1 / -1; }

:where(.stibok-form.is-labelpos-hidden) .stibok-field__label,
.stibok-field.is-labelpos-hidden .stibok-field__label {
	position: absolute;
	inline-size: 1px;
	block-size: 1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	margin: -1px;
	padding: 0;
	border: 0;
}

/* Form-level message region (filled by forms.js on AJAX, or server-rendered on
   the JS-off return path). Token-driven success / error states. */
.stibok-form__message {
	font-size: var(--wp--preset--font-size--sm);
	padding-block: 0.5rem;
	padding-inline: 0.75rem;
	border-radius: var(--wp--custom--radius--sm, 0.25rem);
}
.stibok-form__message:empty { display: none; }
.stibok-form__message.is-success {
	color: color-mix( in srgb, var(--wp--custom--semantic--success) 72%, black );
	background: color-mix( in srgb, var(--wp--custom--semantic--success) 14%, transparent );
	border: 1px solid color-mix( in srgb, var(--wp--custom--semantic--success) 32%, transparent );
}
.stibok-form__message.is-error {
	color: color-mix( in srgb, var(--wp--custom--semantic--danger) 70%, black );
	background: color-mix( in srgb, var(--wp--custom--semantic--danger) 12%, transparent );
	border: 1px solid color-mix( in srgb, var(--wp--custom--semantic--danger) 30%, transparent );
}

/* Honeypot — present in the DOM but visually hidden (NOT display:none, so naive
   bots still fill it). Off-screen clip technique. */
.stibok-form__hp {
	position: absolute;
	inline-size: 1px;
	block-size: 1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
	border: 0;
	padding: 0;
	margin: -1px;
}

/* ---------------------------------------------------------------------------
   Forms — EDITOR-CANVAS chrome (2.6). These classes are emitted ONLY by the
   field/form edit() (the front render uses real <select>/<option>/<input>), so
   they are inert on the front (like the .stibok-motion badge, 1.19) and live
   here because stibok.css is the canvas sheet (add_editor_style) — editor-ui.css
   does NOT reach the iframe. Logical props, semantic tokens, rem-only.
   --------------------------------------------------------------------------- */
/* Editable label text — an inline RichText span inside the real <label>/<legend>. */
.stibok-field__labelText { display: inline; min-inline-size: 1ch; }

/* In-canvas per-row OPTION editor (select/radio/checkbox, 2.7d). The option rows are
   the REAL front DOM (.stibok-field__option + .stibok-field__box + .stibok-field__
   optionLabel — they pick up the real custom skins so the canvas == the front), so
   these rules add only the editor affordances: the select marker, the thin inline
   submit-VALUE token field, and the add/remove buttons. The __option* helper classes
   below are emitted ONLY in canvas → inert on the front (the 1.19 badge pattern). */
.stibok-field__optionMarker {
	flex: 0 0 auto;
	color: var(--wp--custom--semantic--on-surface-muted);
}
/* The editable option label takes its content width (canvas only — scoped to the
   in-canvas field via [data-type] so the front option label is untouched); the small
   value/remove affordances sit immediately after it (left-grouped, never floated to a
   far-right gulf) and never squeeze it. */
.stibok-field[data-type="stibok/field"] .stibok-field__optionLabel { flex: 0 1 auto; min-inline-size: 4ch; }
/* The select's in-canvas options list hugs its content so a `fieldControl` background
   cascade reads as a surface around the options, not a full-width band (canvas only —
   the front select control is the real full-width <select>, untouched). */
.stibok-field[data-type="stibok/field"].is-kind-select .stibok-field__controlWrap.is-select {
	inline-size: fit-content;
	max-inline-size: 100%;
}
/* The submit-value editor is a Dropdown — its toggle is a small icon button; the value
   field lives in an absolute popover (it never occupies row space, so it cannot squeeze
   the option label even in a narrow grid card). */
.stibok-field__optionValue,
.stibok-field__optionReveal,
.stibok-field__optionRemove { flex: 0 0 auto; }
/* A row whose submit value differs from its label gets a quiet accent dot on the
   value toggle (a badge — NOT a pressed/"stuck" state; the toggle is pressed only
   while its popover is open). */
.stibok-field__optionReveal.is-custom { position: relative; }
.stibok-field__optionReveal.is-custom::after {
	content: "";
	position: absolute;
	inset-block-start: 0.125rem;
	inset-inline-end: 0.125rem;
	inline-size: 0.375rem;
	block-size: 0.375rem;
	border-radius: 50%;
	background: var(--wp--custom--semantic--accent);
}
.stibok-field__optionAdd { margin-block-start: 0.25rem; }
/* The value popover content (rendered into the canvas iframe by WP). */
.stibok-field__optionValuePopover .components-base-control { margin-block-end: 0; }
.stibok-field__optionValuePopover input {
	min-inline-size: 12rem;
	font-family: monospace;
}

/* Canvas icon preview (ServerSideRender output of stibok/icon, wrapped in the absolute
   .stibok-field__icon slot): keep the rendered glyph at 1em so it matches the front
   Icon_Ref svg. On the front .stibok-field__icon IS the svg (no descendant), so these
   descendant rules are canvas-only and inert there. */
.stibok-field__icon > * { display: inline-flex; line-height: 0; }
.stibok-field__icon svg,
.stibok-field__icon img { inline-size: 1em; block-size: 1em; }

/* Hidden field — a compact, selectable chip (the front emits an invisible input). */
.stibok-field.is-kind-hidden {
	flex-direction: row;
	align-items: center;
	gap: 0.5rem;
	padding-block: 0.25rem;
	padding-inline: 0.5rem;
	border: 0.0625rem dashed var(--wp--custom--semantic--border);
	border-radius: 0.25rem;
	color: var(--wp--custom--semantic--on-surface-muted);
	font-size: var(--wp--preset--font-size--sm);
}
.stibok-field__hiddenTag {
	text-transform: uppercase;
	letter-spacing: 0.04em;
	font-size: 0.6875rem;
	font-weight: 600;
}
.stibok-field__hiddenVal { font-family: monospace; }

/* ==== Site shell — nav skin + top contact-bar + footer (3.5.2) ==== */
/* Theme template-part chrome (header.html / footer.html). Logical CSS props only
   (RTL-safe); colors via theme.json tokens (rebrandable). This sheet is loaded
   into the Site Editor canvas via add_editor_style, so the skin renders
   identically in the editor and on the front. core/navigation owns the menu
   behaviour (mobile overlay + submenu toggles via the WP Interactivity API) — we
   skin it, never re-implement it. */

/* -- Top contact-bar -- */
.stibok-topbar {
	background: var(--wp--custom--semantic--topbar-surface, var(--wp--preset--color--contrast));
	color: var(--wp--custom--semantic--topbar-on, var(--wp--preset--color--base));
}
.stibok-topbar a {
	color: inherit;
	text-decoration: none;
}
.stibok-topbar a:hover,
.stibok-topbar a:focus-visible {
	text-decoration: underline;
}
.stibok-topbar__contact {
	margin-block: 0;
}
.stibok-topbar__note {
	margin-block: 0;
	color: var(--wp--custom--semantic--topbar-muted, var(--wp--preset--color--muted-2));
}

/* -- Header bar (logo + nav row) -- */
.stibok-header__bar {
	gap: var(--wp--preset--spacing--md);
}

/* -- Primary navigation skin (core/navigation descendants) -- */
.stibok-header .wp-block-navigation {
	font-size: var(--wp--preset--font-size--md);
}
.stibok-header .wp-block-navigation-item__content {
	padding-block: 0.375rem;
	padding-inline: 0.5rem;
	border-radius: var(--wp--custom--radius--sm, 0.375rem);
	color: var(--wp--preset--color--contrast);
	text-decoration: none;
	transition: color var(--wp--custom--transition--fast, 120ms ease),
		background-color var(--wp--custom--transition--fast, 120ms ease);
}
.stibok-header .wp-block-navigation-item__content:hover,
.stibok-header .wp-block-navigation-item__content:focus-visible {
	color: var(--wp--preset--color--accent);
}
/* Current-page state: core emits aria-current="page" on the active link; skin it. */
.stibok-header .wp-block-navigation-item__content[aria-current] {
	color: var(--wp--preset--color--accent);
	font-weight: 600;
}

/* Submenu dropdown skin (2-level menu). */
.stibok-header .wp-block-navigation .wp-block-navigation__submenu-container {
	min-inline-size: 12rem;
	padding-block: var(--wp--preset--spacing--xxs);
	padding-inline: 0;
	border: 0.0625rem solid var(--wp--preset--color--border);
	border-radius: var(--wp--custom--radius--md, 0.625rem);
	background-color: var(--wp--preset--color--base);
	box-shadow: var(--wp--preset--shadow--md);
}
.stibok-header .wp-block-navigation__submenu-container .wp-block-navigation-item__content {
	padding-block: 0.5rem;
	padding-inline: var(--wp--preset--spacing--xs);
	border-radius: 0;
}
.stibok-header .wp-block-navigation__submenu-container .wp-block-navigation-item__content:hover,
.stibok-header .wp-block-navigation__submenu-container .wp-block-navigation-item__content:focus-visible {
	background-color: var(--wp--preset--color--accent-soft);
	color: var(--wp--preset--color--accent-hover);
}

/* Mobile overlay skin (core owns the open/close; we tune surface + spacing). */
.stibok-header .wp-block-navigation__responsive-container.is-menu-open {
	background-color: var(--wp--preset--color--base);
	padding-block: var(--wp--preset--spacing--xl);
	padding-inline: var(--wp--preset--spacing--md);
}

/* -- Footer -- */
.stibok-footer__columns {
	margin-block-end: 0;
}
.stibok-footer__heading {
	margin-block: 0 var(--wp--preset--spacing--xs);
	text-transform: uppercase;
	letter-spacing: 0.04em;
	color: var(--wp--preset--color--muted);
}
.stibok-footer__about {
	margin-block-start: var(--wp--preset--spacing--xs);
	max-inline-size: 28rem;
}
.stibok-footer__social {
	margin-block-start: var(--wp--preset--spacing--sm);
}
.stibok-footer__links {
	margin: 0;
	padding-inline-start: 0;
	list-style: none;
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--xxs);
}
.stibok-footer__links a {
	color: var(--wp--preset--color--contrast-2);
	text-decoration: none;
}
.stibok-footer__links a:hover,
.stibok-footer__links a:focus-visible {
	color: var(--wp--preset--color--accent);
	text-decoration: underline;
}
.stibok-footer__contact {
	margin-block: 0 var(--wp--preset--spacing--xs);
	line-height: 1.6;
}
.stibok-footer__bottom {
	gap: var(--wp--preset--spacing--xs);
}
.stibok-footer__copy,
.stibok-footer__legal {
	margin-block: 0;
}
.stibok-footer__legal a {
	color: var(--wp--preset--color--muted);
	text-decoration: none;
}
.stibok-footer__legal a:hover,
.stibok-footer__legal a:focus-visible {
	text-decoration: underline;
}

/* ---------------------------------------------------------------------------
 * Accordion (3.5.3, D31) — a disclosure group. Structure + the smooth open/close
 * animation live here; the recipe (defaults/accordion.json) owns the themeable
 * paint (colours / hover). The front toggles a `data-open` attr on each panel via
 * the `stibok/accordion` IAPI store; the editor sets it to "true" for all-items-
 * expanded canvas parity (§1.7, D18). With JS off the render.php <noscript> forces
 * every panel open (progressive enhancement).
 * ------------------------------------------------------------------------- */
.stibok-accordion {
	--stibok-accordion-radius: var(--wp--custom--radius--lg, 0.75rem);
	--stibok-accordion-border: var(--wp--preset--color--border);
	--stibok-accordion-duration: 0.28s;
}

/* Border-box on the structural elements so `inline-size:100%` + padding never
 * overflows the item (the front theme resets box-sizing, but the editor-canvas
 * iframe defaults to content-box — without this the padded, full-width trigger
 * spilled past the item's `overflow:hidden` frame and the marker was CLIPPED in
 * the canvas only, a D18 parity break). */
.stibok-accordion__item,
.stibok-accordion__trigger,
.stibok-accordion__panel,
.stibok-accordion__panel-inner,
.stibok-accordion__panel-body {
	box-sizing: border-box;
}

.stibok-accordion__heading {
	margin: 0;
	font-size: inherit;
	font-weight: inherit;
	line-height: inherit;
}

.stibok-accordion__trigger {
	display: flex;
	align-items: center;
	gap: var(--wp--preset--spacing--xs, 0.5rem);
	inline-size: 100%;
	margin: 0;
	padding-block: var(--wp--preset--spacing--sm, 0.75rem);
	padding-inline: var(--wp--preset--spacing--md, 1rem);
	background: transparent;
	border: 0;
	font: inherit;
	font-weight: 600;
	color: inherit;
	text-align: start;
	cursor: pointer;
}
.stibok-accordion__trigger:focus-visible {
	outline: 0.125rem solid var(--wp--preset--color--accent);
	outline-offset: -0.125rem;
}

.stibok-accordion__icon,
.stibok-accordion__marker {
	flex: 0 0 auto;
	display: inline-flex;
	align-items: center;
}
.stibok-accordion__label {
	flex: 1 1 auto;
	min-inline-size: 0;
}
.stibok-accordion__glyph {
	inline-size: 1em;
	block-size: 1em;
}

.stibok-accordion__marker {
	transition: transform 0.2s ease;
}
/* Rotate mode (default): the single glyph spins 180° when the row opens. */
.stibok-accordion__trigger[aria-expanded="true"] .stibok-accordion__marker[data-marker-mode="rotate"] {
	transform: rotate(180deg);
}
/* Swap mode: a closed/open glyph PAIR (e.g. +/−) — show the closed glyph while
 * collapsed, the open glyph while expanded (no rotation). */
.stibok-accordion__marker-glyph {
	display: inline-flex;
	align-items: center;
}
.stibok-accordion__marker[data-marker-mode="swap"] .stibok-accordion__marker-glyph.is-open {
	display: none;
}
.stibok-accordion__trigger[aria-expanded="true"] .stibok-accordion__marker[data-marker-mode="swap"] .stibok-accordion__marker-glyph.is-closed {
	display: none;
}
.stibok-accordion__trigger[aria-expanded="true"] .stibok-accordion__marker[data-marker-mode="swap"] .stibok-accordion__marker-glyph.is-open {
	display: inline-flex;
}

/* Collapsible panel: grid-rows 0fr↔1fr gives a content-height-aware slide with no
 * JS measurement. `visibility` flips at the END of the collapse so closed content
 * leaves the tab order + a11y tree (and can still animate — unlike display:none). */
.stibok-accordion__panel {
	display: grid;
	grid-template-rows: 0fr;
	visibility: hidden;
	transition:
		grid-template-rows var(--stibok-accordion-duration) ease,
		visibility 0s linear var(--stibok-accordion-duration);
}
.stibok-accordion__panel[data-open="true"] {
	grid-template-rows: 1fr;
	visibility: visible;
	transition: grid-template-rows var(--stibok-accordion-duration) ease, visibility 0s;
}
/* The overflow clipper — NO padding here: `grid-template-rows:0fr` zeroes this
 * element's box, but padding on the clipper would still render (leaking residual
 * closed-panel height). The padding lives on the nested `__panel-body` INSIDE the
 * clipper, so it is clipped to 0 when collapsed. */
.stibok-accordion__panel-inner {
	overflow: hidden;
	min-block-size: 0;
}
.stibok-accordion__panel-body {
	padding-block: 0 var(--wp--preset--spacing--md, 1rem);
	padding-inline: var(--wp--preset--spacing--md, 1rem);
}

@media (prefers-reduced-motion: reduce) {
	.stibok-accordion__panel,
	.stibok-accordion__panel[data-open="true"],
	.stibok-accordion__marker {
		transition: none;
	}
}

/* Variant: bordered (default) — one outer frame with dividers between rows.
 * `overflow:hidden` clips the trigger hover fill to the rounded frame. */
.stibok-accordion.is-variant-bordered {
	border: 0.0625rem solid var(--stibok-accordion-border);
	border-radius: var(--stibok-accordion-radius);
	overflow: hidden;
}
.stibok-accordion.is-variant-bordered .stibok-accordion__item + .stibok-accordion__item {
	border-block-start: 0.0625rem solid var(--stibok-accordion-border);
}

/* Variant: separated — each row is a standalone bordered card. */
.stibok-accordion.is-variant-separated {
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--sm, 0.75rem);
}
.stibok-accordion.is-variant-separated .stibok-accordion__item {
	border: 0.0625rem solid var(--stibok-accordion-border);
	border-radius: var(--stibok-accordion-radius);
	overflow: hidden;
}

/* Variant: filled — filled trigger bars with subtle row gaps. */
.stibok-accordion.is-variant-filled {
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--xs, 0.5rem);
}
.stibok-accordion.is-variant-filled .stibok-accordion__item {
	border-radius: var(--wp--custom--radius--md, 0.5rem);
	overflow: hidden;
}

/* Editor canvas: honour data-open (defaultOpen / selected item). Never on the front. */
.stibok-accordion.is-editing .stibok-accordion__panel[data-open="true"] {
	grid-template-rows: 1fr;
	visibility: visible;
}

/* ===========================================================================
 * Tabs (CL1.1) — a tabbed panel group (APG Tabs). The tablist is built by the tabs
 * render above the panels; the store owns active-panel switching + roving-tabindex
 * keyboard nav. Structural layout + per-variant SHAPE live here (token-var colors so
 * skins restyle via tokens); the recipe owns the idle/hover/panel/badge token colors.
 * All sizing is rem/token-based and RTL-safe (logical props). The active-tab visuals
 * key off `[aria-selected="true"]` (an attribute state the recipe cannot express).
 * =========================================================================== */
.stibok-tabs {
	--stibok-tabs-accent: var(--wp--preset--color--accent);
	--stibok-tabs-border: var(--wp--preset--color--border);
	--stibok-tabs-gap: var(--wp--preset--spacing--xs, 0.5rem);
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--md, 1rem);
}
.stibok-tabs.is-orientation-vertical {
	flex-direction: row;
	align-items: flex-start;
}

.stibok-tabs__list,
.stibok-tabs__tab,
.stibok-tabs__panel,
.stibok-tabs__panel-inner {
	box-sizing: border-box;
}

/* Tablist — a flex row of tab buttons above the panels. */
.stibok-tabs__list {
	position: relative;
	display: flex;
	flex-wrap: wrap;
	gap: var(--stibok-tabs-gap);
	align-items: stretch;
	margin: 0;
	padding: 0;
}
.stibok-tabs.is-orientation-vertical .stibok-tabs__list {
	flex-direction: column;
	flex: 0 0 auto;
	flex-wrap: nowrap;
}
/* scroll collapse: keep the tablist on one line and let it scroll horizontally when it
 * overflows its container (robust, no JS). `none` keeps the default wrap. */
.stibok-tabs[data-collapse="scroll"] .stibok-tabs__list {
	flex-wrap: nowrap;
	overflow-x: auto;
	scrollbar-width: thin;
}

/* Tab button — colors (idle/hover) come from the recipe; shape/state visuals here. */
.stibok-tabs__tab {
	display: inline-flex;
	align-items: center;
	gap: var(--wp--preset--spacing--xs, 0.5rem);
	margin: 0;
	padding-block: var(--wp--preset--spacing--sm, 0.75rem);
	padding-inline: var(--wp--preset--spacing--md, 1rem);
	background: transparent;
	border: 0;
	font: inherit;
	font-weight: 600;
	color: inherit;
	white-space: nowrap;
	cursor: pointer;
}
.stibok-tabs__tab:focus-visible {
	outline: 0.125rem solid var(--stibok-tabs-accent);
	outline-offset: -0.125rem;
}
.stibok-tabs__icon {
	flex: 0 0 auto;
	display: inline-flex;
	align-items: center;
}
/* Editor: empty tab icon affordance (+ placeholder) for in-tablist IconRef picker. */
.stibok-tabs.is-editing .stibok-tabs__iconPick {
	display: inline-flex;
	flex: 0 0 auto;
}
.stibok-tabs.is-editing .stibok-tabs__icon.is-empty {
	inline-size: 1.25rem;
	block-size: 1.25rem;
	justify-content: center;
	border: 0.0625rem dashed currentColor;
	border-radius: var(--wp--custom--radius--sm, 0.25rem);
	opacity: 0.45;
	cursor: pointer;
}
.stibok-tabs.is-editing .stibok-tabs__icon.is-empty:hover,
.stibok-tabs.is-editing .stibok-tabs__icon.is-empty:focus-visible {
	opacity: 0.85;
}
.stibok-tabs.is-editing .stibok-tabs__iconPlaceholder {
	font-size: 0.875rem;
	line-height: 1;
}
.stibok-tabs__glyph {
	inline-size: 1em;
	block-size: 1em;
}
.stibok-tabs__tab-label {
	min-inline-size: 0;
}
.stibok-tabs__badge {
	flex: 0 0 auto;
	display: inline-flex;
	align-items: center;
	padding-inline: var(--wp--preset--spacing--2xs, 0.375rem);
	border-radius: var(--wp--custom--radius--pill, 62.4375rem);
	font-size: 0.75em;
	font-weight: 700;
	line-height: 1.6;
}

/* Indicator: a decorative track element (present for appearance-part targeting +
 * skins). The active-tab highlight itself rides the tab's own `[aria-selected]` visual
 * below, so the indicator stays out of the flow by default. */
.stibok-tabs__indicator {
	display: none;
}

/* Panels. `[hidden]` (SSR + the store's `data-wp-bind--hidden`) hides inactive panels;
 * the no-JS `<noscript>` override reveals them all. */
.stibok-tabs__panel {
	min-inline-size: 0;
}
.stibok-tabs.is-orientation-vertical .stibok-tabs__panel {
	flex: 1 1 auto;
}
.stibok-tabs__panel[hidden] {
	display: none;
}
.stibok-tabs__panel-inner {
	min-inline-size: 0;
}

/* --- Variant: line (default) — an underline under the active tab. --- */
.stibok-tabs.is-variant-line .stibok-tabs__list {
	border-block-end: 0.0625rem solid var(--stibok-tabs-border);
}
.stibok-tabs.is-variant-line .stibok-tabs__tab {
	box-shadow: inset 0 -0.125rem 0 0 transparent;
	margin-block-end: -0.0625rem;
	transition: box-shadow 0.2s ease, color 0.2s ease;
}
.stibok-tabs.is-variant-line .stibok-tabs__tab[aria-selected="true"] {
	box-shadow: inset 0 -0.125rem 0 0 var(--stibok-tabs-accent);
	color: var(--stibok-tabs-accent);
}
.stibok-tabs.is-variant-line.is-orientation-vertical .stibok-tabs__list {
	border-block-end: 0;
	border-inline-end: 0.0625rem solid var(--stibok-tabs-border);
}
.stibok-tabs.is-variant-line.is-orientation-vertical .stibok-tabs__tab {
	box-shadow: inset -0.125rem 0 0 0 transparent;
	margin-block-end: 0;
	margin-inline-end: -0.0625rem;
}
.stibok-tabs.is-variant-line.is-orientation-vertical .stibok-tabs__tab[aria-selected="true"] {
	box-shadow: inset -0.125rem 0 0 0 var(--stibok-tabs-accent);
}

/* --- Variant: pill — rounded buttons, filled when active. --- */
.stibok-tabs.is-variant-pill .stibok-tabs__tab {
	border-radius: var(--wp--custom--radius--pill, 62.4375rem);
	transition: background 0.2s ease, color 0.2s ease;
}
.stibok-tabs.is-variant-pill .stibok-tabs__tab[aria-selected="true"] {
	background: var(--stibok-tabs-accent);
	color: var(--wp--preset--color--base, #fff);
}

/* --- Variant: enclosed — folder-style tabs joined to the panel frame. --- */
.stibok-tabs.is-variant-enclosed .stibok-tabs__list {
	gap: 0;
	border-block-end: 0.0625rem solid var(--stibok-tabs-border);
}
.stibok-tabs.is-variant-enclosed .stibok-tabs__tab {
	border: 0.0625rem solid transparent;
	border-block-end: 0;
	border-start-start-radius: var(--wp--custom--radius--sm, 0.375rem);
	border-start-end-radius: var(--wp--custom--radius--sm, 0.375rem);
	margin-block-end: -0.0625rem;
	transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
.stibok-tabs.is-variant-enclosed .stibok-tabs__tab[aria-selected="true"] {
	border-color: var(--stibok-tabs-border);
	background: var(--wp--preset--color--base);
}

/* --- Variant: vertical — full-width stacked buttons (pairs with orientation:vertical). --- */
.stibok-tabs.is-variant-vertical .stibok-tabs__tab {
	inline-size: 100%;
	justify-content: flex-start;
	border-radius: var(--wp--custom--radius--md, 0.5rem);
	transition: background 0.2s ease, color 0.2s ease;
}
.stibok-tabs.is-variant-vertical .stibok-tabs__tab[aria-selected="true"] {
	background: var(--wp--preset--color--accent-soft);
	color: var(--stibok-tabs-accent);
}

/* --- Variant: switch — a segmented 2-option toggle (e.g. monthly/annual). --- */
.stibok-tabs.is-variant-switch .stibok-tabs__list {
	display: inline-flex;
	flex-wrap: nowrap;
	gap: 0;
	padding: 0.25rem;
	border-radius: var(--wp--custom--radius--pill, 62.4375rem);
	border: 0.0625rem solid var(--stibok-tabs-border);
	align-self: center;
}
.stibok-tabs.is-variant-switch .stibok-tabs__tab {
	border-radius: var(--wp--custom--radius--pill, 62.4375rem);
	padding-block: var(--wp--preset--spacing--xs, 0.5rem);
	transition: background 0.2s ease, color 0.2s ease;
}
.stibok-tabs.is-variant-switch .stibok-tabs__tab[aria-selected="true"] {
	background: var(--stibok-tabs-accent);
	color: var(--wp--preset--color--base, #fff);
}

/* --- Responsive collapse: accordion (CL1.1b) — on a narrow CONTAINER the tabs reflow
 * to an independent-open accordion. Driven by `@container` (no JS width-watching): each
 * panel carries a disclosure header (hidden at wide) + a `data-open` inner; a narrow
 * container hides the tablist, reveals the disclosures, shows every panel, and collapses
 * closed inners. `data-active` (not `hidden`) governs the wide single-active hide so the
 * narrow rule can override it without stranding panels out of the a11y tree. --- */
.stibok-tabs[data-collapse="accordion"] {
	/* `display:block` (not the default flex) so `container-type:inline-size` measures the
	 * element's real, parent-filled width — a flex/grid container with inline-size
	 * containment collapses to its content width (the slider's cq hazard). The tablist +
	 * panels stack the same as the flex column (horizontal orientation). A `vertical`
	 * orientation is coerced away when accordion is set (validator M1), so the lost
	 * flex-row is a non-issue; a raw hand-authored vertical+accordion degrades to this
	 * stack. The flex `gap` is replaced by an explicit margin below. */
	display: block;
	container-type: inline-size;
	container-name: stibok-tabs;
}
/* Restore the tablist↔panels spacing the flex `gap` provided (display:block drops gap).
 * Wide only — at narrow the tablist is `display:none`, so this margin never shows. */
.stibok-tabs[data-collapse="accordion"] .stibok-tabs__list {
	margin-block-end: var(--wp--preset--spacing--md, 1rem);
}
/* Wide: accordion panels hide the inactive ones (single-active) via data-active. */
.stibok-tabs[data-collapse="accordion"] .stibok-tabs__panel[data-active="false"] {
	display: none;
}
/* The disclosure header — hidden at wide (the tablist owns navigation there). */
.stibok-tabs__disclosure {
	display: none;
	inline-size: 100%;
	align-items: center;
	justify-content: space-between;
	gap: var(--wp--preset--spacing--xs, 0.5rem);
	margin: 0;
	padding-block: var(--wp--preset--spacing--sm, 0.75rem);
	padding-inline: var(--wp--preset--spacing--md, 1rem);
	background: transparent;
	border: 0;
	border-block-start: 0.0625rem solid var(--stibok-tabs-border);
	font: inherit;
	font-weight: 600;
	color: inherit;
	text-align: start;
	cursor: pointer;
}
.stibok-tabs__disclosure:focus-visible {
	outline: 0.125rem solid var(--stibok-tabs-accent);
	outline-offset: -0.125rem;
}
/* A chevron that points down when closed, up (180°) when the section is open. */
.stibok-tabs__disclosure-marker {
	flex: 0 0 auto;
	inline-size: 1em;
	block-size: 1em;
	background: currentColor;
	transition: transform 0.2s ease;
	-webkit-mask: no-repeat center / contain url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
	mask: no-repeat center / contain url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}
.stibok-tabs__disclosure[aria-expanded="true"] .stibok-tabs__disclosure-marker {
	transform: rotate(180deg);
}

@container stibok-tabs (max-width: 40rem) {
	/* Narrow container → reflow to an accordion. */
	.stibok-tabs[data-collapse="accordion"] .stibok-tabs__list {
		display: none;
	}
	.stibok-tabs[data-collapse="accordion"] .stibok-tabs__panel {
		padding: 0;
	}
	/* Reveal EVERY section — must match the wide `[data-active="false"]{display:none}`
	 * specificity (0,4,0) + come later (in @container) to win the cascade. */
	.stibok-tabs[data-collapse="accordion"] .stibok-tabs__panel[data-active="false"] {
		display: block;
	}
	.stibok-tabs[data-collapse="accordion"] .stibok-tabs__disclosure {
		display: flex;
	}
	/* Collapse a closed section's content (the disclosure header stays visible). */
	.stibok-tabs[data-collapse="accordion"] .stibok-tabs__panel-inner[data-open="false"] {
		display: none;
	}
}

@media (prefers-reduced-motion: reduce) {
	/* `!important` so the reset beats the per-variant tab rules (specificity (0,3,0),
	 * e.g. `.stibok-tabs.is-variant-line .stibok-tabs__tab`) that declare the transition
	 * — a plain (0,1,0) reset would lose the cascade and animate anyway (review MED). */
	.stibok-tabs__tab {
		transition: none !important;
	}
	.stibok-tabs__disclosure-marker {
		transition: none !important;
	}
}

/* Editor canvas (is-editing, CL1.2): the tablist is built by tabsEdit above the
 * InnerBlocks panels; the parent hides inactive panel wrappers via a scoped <style> so
 * one panel shows at a time (front parity). These rules only apply in the canvas. */
.stibok-tabs.is-editing .stibok-tabs__tab {
	cursor: pointer;
}
/* The editor wraps the panel InnerBlocks in a `.stibok-tabs__panels` div (the front has
 * the panels as direct children). Make that wrapper fill the row like a front panel so
 * vertical-orientation parity holds — otherwise it hugs its content (review MED-1). */
.stibok-tabs.is-editing .stibok-tabs__panels {
	flex: 1 1 auto;
	min-inline-size: 0;
	display: flex;
	flex-direction: column;
}
/* An empty badge slot on the active tab is an editable affordance — show it as a subtle
 * dashed placeholder, NOT a solid pill that looks like a real badge. */
.stibok-tabs.is-editing .stibok-tabs__badge:empty {
	background: transparent;
	border: 0.0625rem dashed currentColor;
	opacity: 0.45;
}

/* ===========================================================================
 * Timeline (CL1.3) — a vertical/horizontal step sequence with a connector rail. Purely
 * structural (NO JS): marker number is a CSS `counter`, item state is a static class,
 * the connector is a flex-grown line in each item's aside. Token-var colours (skinnable
 * via the recipe); the `is-state-*` visuals key off attribute classes the recipe can't
 * express. rem-only, logical props.
 * =========================================================================== */
.stibok-timeline {
	--stibok-timeline-marker-size: 2rem;
	--stibok-timeline-accent: var(--wp--preset--color--accent);
	list-style: none;
	margin: 0;
	padding: 0;
	counter-reset: stibok-timeline;
	display: flex;
	flex-direction: column;
}
.stibok-timeline__item {
	counter-increment: stibok-timeline;
	display: grid;
	grid-template-columns: var(--stibok-timeline-marker-size) 1fr;
	column-gap: var(--wp--preset--spacing--md, 1rem);
}
.stibok-timeline__aside {
	display: flex;
	flex-direction: column;
	align-items: center;
}
.stibok-timeline__marker {
	flex: 0 0 auto;
	box-sizing: border-box;
	inline-size: var(--stibok-timeline-marker-size);
	block-size: var(--stibok-timeline-marker-size);
	display: inline-flex;
	align-items: center;
	justify-content: center;
	border-radius: 50%;
	font-weight: 600;
	font-size: 0.875em;
	line-height: 1;
}
.stibok-timeline__index {
	display: inline-flex;
}
.stibok-timeline__index::before {
	content: counter(stibok-timeline);
}
.stibok-timeline__glyph {
	inline-size: 1em;
	block-size: 1em;
}
/* Dot fallback: dot mode (or icon mode with no icon → an empty marker) shrinks to a dot. */
.stibok-timeline.is-marker-dot .stibok-timeline__marker,
.stibok-timeline__marker:empty {
	inline-size: 0.75rem;
	block-size: 0.75rem;
	margin-block: calc((var(--stibok-timeline-marker-size) - 0.75rem) / 2);
}
/* Connector: the rail below the marker, filling to the next item's marker. */
.stibok-timeline__connector {
	flex: 1 1 auto;
	inline-size: 0.125rem;
	min-block-size: var(--wp--preset--spacing--sm, 0.75rem);
	margin-block-start: 0.25rem;
	border-radius: 0.0625rem;
}
.stibok-timeline__item:last-child .stibok-timeline__connector,
.stibok-timeline.is-connector-none .stibok-timeline__connector {
	display: none;
}
.stibok-timeline.is-connector-dashed .stibok-timeline__connector {
	background: none !important; /* override the recipe's solid bg */
	inline-size: 0;
	border-inline-start: 0.125rem dashed var(--wp--preset--color--border);
}
.stibok-timeline__body {
	min-inline-size: 0;
	padding-block-end: var(--wp--preset--spacing--lg, 1.5rem);
}
.stibok-timeline__item:last-child .stibok-timeline__body {
	padding-block-end: 0;
}
.stibok-timeline__label {
	display: block;
	font-weight: 600;
}
.stibok-timeline__meta {
	display: block;
	margin-block-start: 0.125rem;
	font-size: 0.8125em;
	color: var(--wp--custom--semantic--on-surface-muted);
}
.stibok-timeline__content {
	min-inline-size: 0;
}
/* CANVAS-ONLY (CL2.9a). `label`/`meta` are OPTIONAL — timeline-item/render.php emits
 * neither span when its attr is empty, so the editor's always-on RichText placeholder
 * would be chrome the front never draws (D18): two ghost rows per step on a roadmap whose
 * date/title live in the step BODY (a composition contract can only fill child nodes).
 * `is-empty` is emitted by editor.js only — the front never carries it — so this collapses
 * the empty field in the canvas and hover / focus-within brings it back, keeping it
 * discoverable and authorable. Same idiom as `.stibok-table__caption.is-empty` below. */
.stibok-timeline__label.is-empty,
.stibok-timeline__meta.is-empty {
	opacity: 0;
	max-block-size: 0;
	margin-block: 0;
	overflow: hidden;
	transition: opacity 0.12s ease, max-block-size 0.12s ease;
}
.stibok-timeline__item:hover .stibok-timeline__label.is-empty,
.stibok-timeline__item:hover .stibok-timeline__meta.is-empty,
.stibok-timeline__label.is-empty:focus-within,
.stibok-timeline__meta.is-empty:focus-within {
	opacity: 1;
	max-block-size: 3rem;
	overflow: visible;
}

/* State classes (attribute-driven — the recipe styles the base marker; these overlay). */
/* done = a FILLED marker (a completed step reads distinctly from a neutral one);
 * active = an accent ring on the (current) marker; upcoming = dimmed. The leading
 * `.stibok-timeline` lifts specificity to (0,4,0) so the fill beats the recipe's
 * `marker.bg` (0,3,0, injected later in source order). */
.stibok-timeline .stibok-timeline__item.is-state-done .stibok-timeline__marker {
	background: var(--stibok-timeline-accent);
	color: var(--wp--preset--color--base, #fff);
}
.stibok-timeline__item.is-state-active .stibok-timeline__marker {
	outline: 0.125rem solid var(--stibok-timeline-accent);
	outline-offset: 0.125rem;
}
.stibok-timeline__item.is-state-upcoming {
	opacity: 0.6;
}

/* Horizontal orientation: steps in a row, marker + connector on top of each body. */
.stibok-timeline.is-orientation-horizontal {
	flex-direction: row;
	overflow-x: auto;
}
.stibok-timeline.is-orientation-horizontal .stibok-timeline__item {
	grid-template-columns: none;
	grid-template-rows: var(--stibok-timeline-marker-size) 1fr;
	flex: 1 0 10rem;
	row-gap: var(--wp--preset--spacing--sm, 0.75rem);
	column-gap: 0;
}
.stibok-timeline.is-orientation-horizontal .stibok-timeline__aside {
	flex-direction: row;
}
.stibok-timeline.is-orientation-horizontal .stibok-timeline__connector {
	inline-size: auto;
	block-size: 0.125rem;
	min-inline-size: var(--wp--preset--spacing--sm, 0.75rem);
	min-block-size: 0;
	margin-block-start: 0;
	margin-inline-start: 0.25rem;
}
.stibok-timeline.is-orientation-horizontal .stibok-timeline__marker:empty {
	margin-block: 0;
	margin-inline: calc((var(--stibok-timeline-marker-size) - 0.75rem) / 2);
}
.stibok-timeline.is-orientation-horizontal .stibok-timeline__body {
	padding-block-end: 0;
	padding-inline-end: var(--wp--preset--spacing--md, 1rem);
}
/* Horizontal + dashed: the dashed rule draws an inline-START border on a 0-width box
 * (a vertical tick), which is wrong when the rail runs horizontally — redraw it as a
 * block-START dashed border on a 0-height box (review MED). */
.stibok-timeline.is-orientation-horizontal.is-connector-dashed .stibok-timeline__connector {
	border-inline-start: 0;
	border-block-start: 0.125rem dashed var(--wp--preset--color--border);
	block-size: 0;
}

/* ===========================================================================
 * Multi-step form / wizard (CL2.P4). The server renders EVERY step visible and the nav
 * `hidden`, so a JS-off visitor gets one long working form; the store then collapses it
 * to one step at a time and reveals the nav. Nothing here is a trust boundary — the
 * server validates every field in every step regardless of what is on screen.
 * =========================================================================== */
.stibok-form-step {
	border: 0;
	margin: 0;
	padding: 0;
	min-inline-size: 0; /* fieldset defaults to min-content and would break the grid */
	display: grid;
	grid-template-columns: subgrid;
	grid-column: 1 / -1;
	gap: inherit;
}

/* A fieldset cannot be a grid container in older engines; fall back to a plain block flow. */
@supports not (grid-template-columns: subgrid) {
	.stibok-form-step:not([hidden]) {
		display: contents;
	}
}

/* MUST come after the `display` rules above. A `display` declaration on the element beats the
 * UA's `[hidden] { display: none }`, so without this the step machinery sets `hidden` while
 * every step stays on screen — a wizard that is pure decoration.
 *
 * This is a FLOOR, not a list. It was first written as four hand-named selectors and the very
 * next element the store hides — the submit button, which is `.stibok-button { display:
 * inline-flex }` — was missed, leaving "Send enquiry" live on step 1. Any `stibok-` element
 * that sets `display` and is ever hidden by an attribute needs this, so state it once for all
 * of them rather than trying to remember the list. */
[class^="stibok-"][hidden],
[class*=" stibok-"][hidden] {
	display: none !important;
}

.stibok-form-step__legend {
	grid-column: 1 / -1;
	padding: 0;
	font-weight: 600;
}

.stibok-form-step:focus {
	outline: none; /* focus is moved here programmatically for a11y, not for display */
}

.stibok-form__steps {
	grid-column: 1 / -1;
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0.75rem 1rem;
}

.stibok-form__progress {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 0.5rem 1rem;
	margin: 0;
	padding: 0;
	list-style: none;
	flex: 1 1 auto;
}

.stibok-form__progress-step {
	display: inline-flex;
	align-items: center;
	gap: 0.375rem;
	font-size: 0.8125rem;
	opacity: 0.55;
}

.stibok-form__progress-step.is-active,
.stibok-form__progress-step.is-done {
	opacity: 1;
}

.stibok-form__progress-dot {
	inline-size: 0.5rem;
	block-size: 0.5rem;
	border-radius: 50%;
	background: currentColor;
}

.stibok-form__progress-step.is-active .stibok-form__progress-dot {
	box-shadow: 0 0 0 0.1875rem color-mix(in srgb, currentColor 30%, transparent);
}

/* The polite "Step 2 of 4" announcement is for assistive tech; the dots carry it visually. */
.stibok-form__progress-status {
	position: absolute;
	inline-size: 1px;
	block-size: 1px;
	margin: -1px;
	padding: 0;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
}

.stibok-form__stepnav {
	grid-column: 1 / -1;
	display: flex;
	gap: 0.5rem;
	justify-content: flex-end;
}

/* ===========================================================================
 * Countdown (CL2.P2) — a countdown to one author-set moment. Progressive: the `<time>`
 * target is what ships and what a JS-off visitor sees; the units are revealed by the
 * client. Under reduced motion the figure is computed once and never ticks (the store
 * decides — nothing here animates, so there is no keyframe to gate). rem-only.
 * =========================================================================== */
.stibok-countdown {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 0.75rem;
}

/* The authored date: the no-JS truth, and the accessible name once digits appear. */
.stibok-countdown__target {
	font-variant-numeric: tabular-nums;
}

.stibok-countdown__units {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: 0.75rem;
}

.stibok-countdown__unit {
	display: inline-flex;
	flex-direction: column;
	align-items: center;
	min-inline-size: 3.5rem;
}

.stibok-countdown__value {
	font-size: clamp(1.75rem, 1.2rem + 2vw, 3rem);
	line-height: 1.05;
	font-weight: 700;
	/* Tabular figures stop the whole row shivering as digits change. */
	font-variant-numeric: tabular-nums;
}

.stibok-countdown__label {
	font-size: 0.8125rem;
	letter-spacing: 0.04em;
	text-transform: uppercase;
}

.stibok-countdown__expired {
	margin: 0;
}

/* boxed: each unit gets its own tile. */
.stibok-countdown.is-variant-boxed .stibok-countdown__unit {
	padding-block: 0.625rem;
	padding-inline: 0.875rem;
	border-radius: var(--wp--custom--radius--md, 0.5rem);
	background: var(--wp--custom--semantic--surface-2, rgba(0, 0, 0, 0.05));
}

/* inline: one running line — value and label side by side, smaller figures. */
.stibok-countdown.is-variant-inline .stibok-countdown__unit {
	flex-direction: row;
	align-items: baseline;
	gap: 0.25rem;
	min-inline-size: 0;
}

.stibok-countdown.is-variant-inline .stibok-countdown__value {
	font-size: 1.25rem;
}

.stibok-countdown.is-variant-inline .stibok-countdown__label {
	text-transform: none;
	letter-spacing: 0;
}

/* ===========================================================================
 * Code / terminal (CL2.P1) — a monospace snippet block. Whitespace is significant:
 * `white-space: pre` on the `<pre>` and NO padding tricks that would shift the first
 * line. Long lines scroll INSIDE the block (`overflow-x`) so a wide snippet can never
 * push the page sideways — the composition waves proved a horizontal page scroll is the
 * most common responsive defect. Colours come from the recipe (token vars), so a skin
 * restyles the block — including any future syntax token-tints. rem-only, logical props.
 * =========================================================================== */
.stibok-code {
	display: block;
	border-radius: var(--wp--custom--radius--md, 0.5rem);
	overflow: hidden; /* keeps the chrome bar's corners clipped to the block radius */
	background: var(--wp--custom--semantic--surface-2, #f3f4f6);
	color: var(--wp--custom--semantic--on-surface, inherit);
}

/* The chrome bar: window dots, a filename label and the copy button. */
.stibok-code__chrome {
	display: flex;
	align-items: center;
	gap: 0.5rem;
	padding-block: 0.5rem;
	padding-inline: 0.75rem;
	font-size: 0.8125rem;
	line-height: 1.4;
	border-block-end: 1px solid var(--wp--custom--semantic--border, rgba(0, 0, 0, 0.12));
}

.stibok-code__label {
	font-family: var(--wp--preset--font-family--mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* Three decorative dots — a window affordance, never announced. */
.stibok-code__dots {
	display: inline-flex;
	flex: none;
	inline-size: 2.25rem;
	block-size: 0.625rem;
	background-image: radial-gradient(circle, currentColor 40%, transparent 42%);
	background-size: 0.75rem 0.625rem;
	background-repeat: repeat-x;
	opacity: 0.45;
}

/* Pushed to the far end of the chrome bar. */
.stibok-code__copy {
	margin-inline-start: auto;
	flex: none;
	font: inherit;
	color: inherit;
	background: transparent;
	border: 1px solid var(--wp--custom--semantic--border, rgba(0, 0, 0, 0.2));
	border-radius: var(--wp--custom--radius--sm, 0.25rem);
	padding-block: 0.125rem;
	padding-inline: 0.5rem;
	cursor: pointer;
}

.stibok-code__copy:hover,
.stibok-code__copy:focus-visible {
	background: color-mix(in srgb, currentColor 12%, transparent);
}

.stibok-code__pre {
	margin: 0;
	padding-block: 0.875rem;
	padding-inline: 0.875rem;
	overflow-x: auto; /* a wide snippet scrolls itself, never the page */
	white-space: pre;
	font-family: var(--wp--preset--font-family--mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
	font-size: 0.875rem;
	line-height: 1.6;
	tab-size: 2;
}

/* The shell prompt sits OUTSIDE `<code>` so copy-to-clipboard never picks it up. */
.stibok-code__prompt {
	user-select: none;
	-webkit-user-select: none;
	margin-inline-end: 0.5rem;
	opacity: 0.85;
}

.stibok-code__code {
	font: inherit;
	color: inherit;
	background: none;
	padding: 0;
}

/* In the editor canvas the code is a PlainText textarea — make it look like the front. */
textarea.stibok-code__code {
	display: block;
	inline-size: 100%;
	resize: vertical;
	border: 0;
	outline: none;
	box-shadow: none;
	white-space: pre;
	overflow-wrap: normal;
}

/* Canvas parity (D18) for the shell prompt. On the front the `<code>` is INLINE, so
 * `$ npm install…` sits on one line; in the canvas the PlainText textarea is block-level,
 * which pushed the prompt onto a line of its own. Lay the pre out as a row in the canvas
 * only — `.block-editor-block-list__block` exists solely in the editor, so the front is
 * untouched. */
.block-editor-block-list__block.stibok-code .stibok-code__pre {
	display: flex;
	align-items: flex-start;
}

.block-editor-block-list__block.stibok-code .stibok-code__code {
	flex: 1 1 auto;
	inline-size: auto;
	min-inline-size: 0;
}

/* ===========================================================================
 * Meter (CL1.4) — a progress/proportion meter: bar, ring (SVG stroke-dashoffset), or
 * segments (pips). The fill is CSS-driven from `--stibok-meter-pct` (bar width / ring
 * dashoffset computed server-side / filled pip count), so a motion reveal can transition
 * it. Token-var colours (skinnable via the recipe). rem-only, logical props.
 * =========================================================================== */
.stibok-meter {
	--stibok-meter-fill: var(--wp--preset--color--accent);
	--stibok-meter-track: var(--wp--preset--color--border);
	--stibok-meter-bar-height: 0.5rem;
	--stibok-meter-ring-size: 5rem;
	--stibok-meter-ring-stroke: 3.5; /* SVG user units (viewBox is 40) */
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--2xs, 0.375rem);
}
.stibok-meter.is-size-sm {
	--stibok-meter-bar-height: 0.375rem;
	--stibok-meter-ring-size: 3.5rem;
	--stibok-meter-ring-stroke: 4;
}
.stibok-meter.is-size-lg {
	--stibok-meter-bar-height: 0.75rem;
	--stibok-meter-ring-size: 7rem;
	--stibok-meter-ring-stroke: 3;
}
.stibok-meter__label {
	font-weight: 600;
	font-size: 0.875em;
}
/* CANVAS-ONLY (CL2.9a): meter/render.php omits the label span when empty — collapse the
   editor's empty placeholder, reveal on hover / focus. `is-empty` is editor-emitted only. */
.stibok-meter__label.is-empty {
	opacity: 0;
	max-block-size: 0;
	overflow: hidden;
	transition: opacity 0.12s ease, max-block-size 0.12s ease;
}
.stibok-meter:hover .stibok-meter__label.is-empty,
.stibok-meter__label.is-empty:focus-within {
	opacity: 1;
	max-block-size: 3rem;
	overflow: visible;
}
.stibok-meter__value {
	font-size: 0.8125em;
}

/* bar */
.stibok-meter.is-variant-bar .stibok-meter__track {
	inline-size: 100%;
	block-size: var(--stibok-meter-bar-height);
	border-radius: var(--wp--custom--radius--pill, 62.4375rem);
	overflow: hidden;
	background: var(--stibok-meter-track);
}
.stibok-meter.is-variant-bar .stibok-meter__fill {
	block-size: 100%;
	inline-size: var(--stibok-meter-pct, 0%);
	border-radius: inherit;
	background: var(--stibok-meter-fill);
	transition: inline-size 0.6s ease;
}

/* ring + gauge (INF.10c) — gauge is ring's own geometry re-parametrized to a half
 * circumference "dome" (Render_Helpers::meter_gauge_geometry()'s own docblock has the
 * full derivation); both share the SAME centered layout + fill styling, so their
 * selectors are combined below rather than duplicated. */
.stibok-meter.is-variant-ring,
.stibok-meter.is-variant-gauge {
	align-items: center;
}
.stibok-meter.is-variant-ring .stibok-meter__track {
	inline-size: var(--stibok-meter-ring-size);
	block-size: var(--stibok-meter-ring-size);
	display: block;
}
/* gauge's track is a HALF-circle dome, not a full circle — a 2:1 viewBox crop
 * (render.php's `viewBox="0 2 40 20"`) needs a matching 2:1 CSS box so the SVG scales
 * uniformly with no letterboxing, instead of the square box `ring` uses. */
.stibok-meter.is-variant-gauge .stibok-meter__track {
	inline-size: var(--stibok-meter-ring-size);
	aspect-ratio: 2 / 1;
	display: block;
}
.stibok-meter__ring-bg {
	stroke: var(--stibok-meter-track);
	stroke-width: var(--stibok-meter-ring-stroke);
	/* Round caps only matter once a dash actually CUTS the loop (gauge's fixed
	 * half-circumference dasharray) — a no-op on ring's own undashed, fully closed
	 * circle, so this is safe to share unconditionally. */
	stroke-linecap: round;
}
.stibok-meter.is-variant-ring .stibok-meter__fill,
.stibok-meter.is-variant-gauge .stibok-meter__fill {
	/* `stroke:currentColor` + `color` (the recipe's `fill.text` token, else the fallback
	 * var) so a per-recipe/skin override recolours the ring/gauge like it does the bar/
	 * segments `fill.bg` — the recipe engine can't set an SVG `stroke` directly (review). */
	stroke: currentColor;
	color: var(--stibok-meter-fill);
	stroke-width: var(--stibok-meter-ring-stroke);
	stroke-linecap: round;
	transition: stroke-dashoffset 0.6s ease;
}
/* Phase INF-Polish P.1 item 4: `gauge` sweeps its arc 0→value on reveal instead of
 * rendering static — the SAME reveal-observer + CSS-transition mechanism `diagram`'s
 * own draw-on already established (D-INF6), no new JS. `render.php` always emits the
 * CORRECT final `stroke-dashoffset` as the `<circle>`'s own attribute (canvas parity,
 * D18 — meterEdit never adds `data-stibok-reveal`, so this override never applies in
 * the editor and the gauge shows its settled value unconditionally, exactly like
 * `ring`'s own draw-on today) — these two rules OVERRIDE that attribute via a plain
 * (0,2,0)-ish class+attribute selector, which always wins over an SVG presentation
 * attribute regardless of specificity, so no `!important` is needed. At REST
 * (`[data-stibok-reveal]`, not yet `.is-revealed`) the fill is pulled to
 * `--stibok-meter-gauge-hide` (the fixed, pct-independent "0% shown" dashoffset,
 * computed server-side from the SAME `Render_Helpers::meter_gauge_geometry()` — never a
 * literal here, so it can't drift from `Render_Helpers::METER_RING_RADIUS`); on
 * `.is-revealed` it transitions to `--stibok-meter-gauge-offset` (the real per-instance
 * target, the exact number the raw attribute already carries).
 */
.stibok-meter.is-variant-gauge[data-stibok-reveal] .stibok-meter__fill {
	stroke-dashoffset: var(--stibok-meter-gauge-hide);
}
.stibok-meter.is-variant-gauge[data-stibok-reveal].is-revealed .stibok-meter__fill {
	stroke-dashoffset: var(--stibok-meter-gauge-offset);
	transition: stroke-dashoffset 0.9s ease;
}

/* segments */
.stibok-meter.is-variant-segments .stibok-meter__track {
	display: flex;
	gap: 0.25rem;
}
.stibok-meter__segment {
	flex: 1 1 0;
	block-size: var(--stibok-meter-bar-height);
	border-radius: var(--wp--custom--radius--sm, 0.25rem);
	background: var(--stibok-meter-track);
}
.stibok-meter.is-variant-segments .stibok-meter__fill {
	background: var(--stibok-meter-fill);
}

@media (prefers-reduced-motion: reduce) {
	.stibok-meter__fill {
		transition: none !important;
	}
}

/* ===========================================================================
 * Table (CL1.5) — a data table / feature matrix. Semantic `<table>` with typed cells;
 * responsive scroll (overflow-x + min-width) or cards (each row → a card, cells labeled
 * by `data-label`). Token-var colours (skinnable via the recipe). rem-only, logical props.
 * =========================================================================== */
.stibok-table-wrap {
	inline-size: 100%;
}
.stibok-table-wrap[data-collapse="scroll"] {
	overflow-x: auto;
}
/* Sticky header (review MED): bound the wrapper's block size + give it a real vertical
 * scroll region, so `position:sticky` on the `<thead>` actually pins within a container
 * that scrolls (a bare `overflow-x:auto` wrapper never scrolls vertically → the header
 * would scroll away on page scroll). */
.stibok-table-wrap[data-sticky-header] {
	max-block-size: 32rem;
	overflow: auto;
}
.stibok-table {
	inline-size: 100%;
	border-collapse: collapse;
	text-align: start;
}
.stibok-table-wrap[data-collapse="scroll"] .stibok-table {
	min-inline-size: var(--stibok-table-min-width, 40rem);
}
.stibok-table__caption {
	caption-side: top;
	text-align: start;
	padding-block-end: var(--wp--preset--spacing--sm, 0.75rem);
	font-size: 0.875em;
	color: var(--wp--custom--semantic--on-surface-muted);
}
.stibok-table__headCell,
.stibok-table__rowHeader,
.stibok-table__cell {
	padding-block: var(--wp--preset--spacing--xs, 0.5rem);
	padding-inline: var(--wp--preset--spacing--sm, 0.75rem);
	text-align: start;
	vertical-align: middle;
}
.stibok-table__headCell,
.stibok-table__rowHeader {
	font-weight: 600;
}
.stibok-table__marker {
	display: inline-flex;
	align-items: center;
}
.stibok-table__marker.is-check {
	color: var(--wp--preset--color--accent);
}
.stibok-table__marker.is-cross {
	/* De-emphasised via `opacity`, NOT via a tone-blind colour: the ✗ glyph is muted
	 * relative to its surface (semantic token → muted on light, muted-2 on dark) rather
	 * than the raw light grey, which on a `tone:dark` table sat at ~2:1 while the same
	 * table's caption already followed tone (review LOW-1). `.is-check` stays `accent`
	 * on purpose — a positive marker is the brand colour in both tones. */
	color: var(--wp--custom--semantic--on-surface-muted);
	opacity: 0.6;
}
.stibok-table__glyph {
	inline-size: 1em;
	block-size: 1em;
}

/* rules (borders) */
.stibok-table.is-rules-rows .stibok-table__row,
.stibok-table.is-rules-all .stibok-table__row,
.stibok-table.is-rules-rows .stibok-table__head tr,
.stibok-table.is-rules-all .stibok-table__head tr {
	border-block-end: 0.0625rem solid var(--wp--custom--semantic--border);
}
.stibok-table.is-rules-cols :where( .stibok-table__headCell, .stibok-table__cell, .stibok-table__rowHeader ),
.stibok-table.is-rules-all :where( .stibok-table__headCell, .stibok-table__cell, .stibok-table__rowHeader ) {
	border-inline-end: 0.0625rem solid var(--wp--custom--semantic--border);
}

/* Surfaces + borders below follow the SEMANTIC tone tokens, never the raw light presets
 * (`base`/`border`/`accent-soft`), which do not re-point under `.is-tone-dark`. CL2.8b:
 * the first dark-skinned matrix (`pricing-matrix` under `obsidian`) rendered its sticky
 * row-header column as a white slab with white-on-white labels — the text followed tone,
 * the painted background did not. Same class as the LOW-1 marker fix above. */

/* striped */
.stibok-table.is-variant-striped .stibok-table__row:nth-child(even) {
	background: var(--wp--custom--semantic--surface-2, rgba(0, 0, 0, 0.03));
}

/* highlight column (a recommended plan) */
.stibok-table__highlight {
	background: var(--wp--custom--semantic--accent-soft);
}

/* sticky header + first column */
.stibok-table.is-sticky-header .stibok-table__head th {
	position: sticky;
	inset-block-start: 0;
	background: var(--wp--custom--semantic--surface);
	z-index: 1;
}
.stibok-table.is-sticky-col .stibok-table__rowHeader {
	position: sticky;
	inset-inline-start: 0;
	background: var(--wp--custom--semantic--surface);
}

/* cards: on a narrow viewport each row becomes a card; the header row is hidden and each
 * cell is labeled by its column via `data-label`. */
@media (max-width: 40rem) {
	.stibok-table-wrap[data-collapse="cards"] .stibok-table,
	.stibok-table-wrap[data-collapse="cards"] .stibok-table__row {
		display: block;
	}
	.stibok-table-wrap[data-collapse="cards"] .stibok-table__head {
		display: none;
	}
	.stibok-table-wrap[data-collapse="cards"] .stibok-table__row {
		border: 0.0625rem solid var(--wp--custom--semantic--border);
		border-radius: var(--wp--custom--radius--md, 0.5rem);
		margin-block-end: var(--wp--preset--spacing--sm, 0.75rem);
		padding: var(--wp--preset--spacing--xs, 0.5rem);
	}
	.stibok-table-wrap[data-collapse="cards"] .stibok-table__rowHeader {
		display: block;
		border-block-end: 0.0625rem solid var(--wp--custom--semantic--border);
		margin-block-end: 0.25rem;
		padding-block-end: 0.25rem;
	}
	.stibok-table-wrap[data-collapse="cards"] .stibok-table__cell {
		display: flex;
		justify-content: space-between;
		gap: var(--wp--preset--spacing--sm, 0.75rem);
		text-align: end;
	}
	.stibok-table-wrap[data-collapse="cards"] .stibok-table__cell::before {
		content: attr(data-label);
		font-weight: 600;
		text-align: start;
	}
}

/* CL1.6 / table-canvas-ux editor chrome (`.is-editing` only). Cell paddings match
 * the front — DO NOT pad `<th>`/`<td>` for grips. Select grips live OUTSIDE the
 * cell box (col above, row at inline-start). Hovering the grip still matches the
 * host `:hover` (descendant overflow). Wrap gets editor-only padding + overflow
 * so outside chrome is not clipped. Insert/Delete/★ live on the block toolbar —
 * in-canvas `+`/`×`/`★` clusters are gone. */
.stibok-table-wrap.is-editing {
	/* Room for outside chrome; do not alter <table> cell metrics. */
	padding-block-start: 1.75rem;
	padding-inline-start: 1.75rem;
	overflow: visible;
}
.stibok-table-wrap.is-editing[data-sticky-header] {
	/* Sticky max-height clips editing chrome; front sticky unchanged. */
	max-block-size: none;
	overflow: visible;
}
.stibok-table-wrap.is-editing .stibok-table__headCell,
.stibok-table-wrap.is-editing .stibok-table__rowHeader,
.stibok-table-wrap.is-editing .stibok-table__cell {
	position: relative;
}
.stibok-table-wrap.is-editing .stibok-table__colctl,
.stibok-table-wrap.is-editing .stibok-table__rowctl {
	position: absolute;
	z-index: 2;
	display: inline-flex;
	gap: 0.125rem;
	padding: 0.125rem;
	background: var(--wp-admin-theme-color, #3858e9);
	border-radius: 0.25rem;
	box-shadow: 0 0.0625rem 0.25rem rgba(0, 0, 0, 0.2);
	opacity: 0.35;
	transition: opacity 0.12s ease;
}
/* Column grip: above the head cell. Row grip: before the row header. */
.stibok-table-wrap.is-editing .stibok-table__colctl {
	inset-block-end: 100%;
	inset-block-start: auto;
	inset-inline-start: 0.125rem;
	margin-block-end: 0.1875rem;
}
.stibok-table-wrap.is-editing .stibok-table__rowctl {
	inset-block-start: 0.125rem;
	inset-inline-end: 100%;
	inset-inline-start: auto;
	margin-inline-end: 0.1875rem;
}
.stibok-table-wrap.is-editing .stibok-table__headCell:hover .stibok-table__colctl,
.stibok-table-wrap.is-editing .stibok-table__headCell:focus-within .stibok-table__colctl,
.stibok-table-wrap.is-editing .stibok-table__rowHeader:hover .stibok-table__rowctl,
.stibok-table-wrap.is-editing .stibok-table__rowHeader:focus-within .stibok-table__rowctl,
.stibok-table-wrap.is-editing .stibok-table__headCell.is-selected .stibok-table__colctl,
.stibok-table-wrap.is-editing .stibok-table__rowHeader.is-selected .stibok-table__rowctl {
	opacity: 1;
}
.stibok-table-wrap.is-editing .stibok-table__ctlbtn.components-button {
	min-inline-size: 1.25rem;
	block-size: 1.25rem;
	padding: 0 0.25rem;
	color: #fff;
	font-size: 0.6875rem;
	line-height: 1;
}
.stibok-table-wrap.is-editing .stibok-table__ctlbtn.components-button.is-pressed {
	background: rgba(255, 255, 255, 0.25);
}
.stibok-table-wrap.is-editing .stibok-table__ctlbtn.components-button svg {
	inline-size: 0.875rem;
	block-size: 0.875rem;
}
.stibok-table-wrap.is-editing .stibok-table__markerbtn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	min-inline-size: 1.5rem;
	cursor: pointer;
	border-radius: 0.25rem;
}
.stibok-table-wrap.is-editing .stibok-table__markerbtn:focus-visible {
	outline: 0.125rem solid var(--wp-admin-theme-color, #3858e9);
	outline-offset: 0.0625rem;
}
/* Col/row selection + focused body cell (editor-only). outline = no layout shift. */
.stibok-table-wrap.is-editing .stibok-table__headCell.is-selected,
.stibok-table-wrap.is-editing .stibok-table__rowHeader.is-selected,
.stibok-table-wrap.is-editing .stibok-table__cell.is-selected,
.stibok-table-wrap.is-editing .stibok-table__cell.is-active {
	outline: 0.125rem solid var(--wp-admin-theme-color, #3858e9);
	outline-offset: -0.125rem;
}
/* Brief highlight after add / move / duplicate (editor-only; no layout shift). */
@keyframes stibok-table-flash {
	0% { background-color: color-mix(in srgb, var(--wp-admin-theme-color, #3858e9) 28%, transparent); }
	100% { background-color: transparent; }
}
.stibok-table-wrap.is-editing .stibok-table__headCell.is-flash,
.stibok-table-wrap.is-editing .stibok-table__rowHeader.is-flash,
.stibok-table-wrap.is-editing .stibok-table__cell.is-flash {
	animation: stibok-table-flash 0.48s ease-out;
}
@media ( prefers-reduced-motion: reduce ) {
	.stibok-table-wrap.is-editing .stibok-table__headCell.is-flash,
	.stibok-table-wrap.is-editing .stibok-table__rowHeader.is-flash,
	.stibok-table-wrap.is-editing .stibok-table__cell.is-flash {
		animation: none;
		background-color: color-mix(in srgb, var(--wp-admin-theme-color, #3858e9) 18%, transparent);
	}
}
.stibok-table-wrap.is-editing .stibok-table__cellrt,
.stibok-table-wrap.is-editing .stibok-table__headrt {
	display: inline-block;
	min-inline-size: 2rem;
}
/* Empty caption collapses in the editor (front omits empty captions) — reveal on
 * wrap hover or when the caption itself is focused so authors can still add one. */
.stibok-table-wrap.is-editing .stibok-table__caption.is-empty {
	opacity: 0;
	max-block-size: 0;
	overflow: hidden;
	padding-block: 0;
	margin-block: 0;
	transition: opacity 0.12s ease, max-block-size 0.12s ease;
}
.stibok-table-wrap.is-editing:hover .stibok-table__caption.is-empty,
.stibok-table-wrap.is-editing .stibok-table__caption.is-empty:focus-within {
	opacity: 1;
	max-block-size: 4rem;
	padding-block-end: var(--wp--preset--spacing--sm, 0.75rem);
	overflow: visible;
}

/* ===========================================================================
 * Slider (3.5.4, D31) — a carousel. Native CSS scroll-snap owns the swipe/scroll
 * (NO JS dep, NOT Swiper — R11); the IAPI store owns only the enhancements. All
 * sizing is rem/token-based and RTL-safe (logical props; native scroll direction).
 * =========================================================================== */
.stibok-slider {
	--stibok-slider-per-view: 1;
	--stibok-slider-gap: var(--wp--preset--spacing--md, 1rem);
	--stibok-slider-arrow-size: 2.5rem;
	--stibok-slider-arrow-offset: 0rem;
	--stibok-slider-dot-size: 0.5rem;
	--stibok-slider-accent: var(--wp--preset--color--accent, currentColor);
	/* Vertical breathing room INSIDE the scroll track so a hover-scaled slide (or its
	 * shadow) is not clipped by the track's `overflow-y`. Override per slider for
	 * bigger hover effects. */
	--stibok-slider-bleed: 0.75rem;
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--sm, 0.75rem);
}
.stibok-slider *,
.stibok-slider .stibok-slider__slide {
	box-sizing: border-box;
}

/* Stage = the scroll viewport + (position:sides) the overlaid arrows. Positioned so
 * the side arrows can be absolutely placed against the track edges. */
.stibok-slider__stage {
	position: relative;
}

/* The scroll track: a horizontally scroll-snapping flex row. The scrollbar is
 * hidden — arrows/dots/swipe are the affordances. `padding-block` (matched by a
 * negative `margin-block`) gives hover-scaled slides vertical room without shifting
 * the surrounding layout — the block axis is clipped (`overflow-y`) for scroll-snap,
 * so the padding is what keeps a growing slide from being cut off top/bottom. */
.stibok-slider__viewport {
	display: flex;
	flex-direction: row;
	gap: var(--stibok-slider-gap);
	overflow-x: auto;
	overflow-y: hidden;
	scroll-snap-type: x mandatory;
	scroll-behavior: smooth;
	scrollbar-width: none;
	-webkit-overflow-scrolling: touch;
	overscroll-behavior-x: contain;
	padding-block: var(--stibok-slider-bleed);
	margin-block: calc(-1 * var(--stibok-slider-bleed));
}
.stibok-slider__viewport::-webkit-scrollbar {
	display: none;
}

/* Each slide fills 1/perView of the viewport minus the shared gaps. */
.stibok-slider__slide {
	flex: 0 0 calc(
		(100% - (var(--stibok-slider-per-view) - 1) * var(--stibok-slider-gap)) /
			var(--stibok-slider-per-view)
	);
	min-inline-size: 0;
	scroll-snap-align: start;
}

/* Controls footer: arrows + dots + counter on one row below the track. */
.stibok-slider__controls {
	display: flex;
	align-items: center;
	gap: var(--wp--preset--spacing--sm, 0.75rem);
}
.stibok-slider__arrows {
	display: flex;
	gap: 0.375rem;
	flex: 0 0 auto;
}
.stibok-slider__dots {
	display: flex;
	flex: 1 1 auto;
	align-items: center;
	justify-content: center;
	gap: 0.5rem;
	flex-wrap: wrap;
}
.stibok-slider__counter {
	flex: 0 0 auto;
	font-size: 0.8125rem;
	color: var(--wp--custom--semantic--on-surface-muted);
	font-variant-numeric: tabular-nums;
}

.stibok-slider__arrow {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	inline-size: var(--stibok-slider-arrow-size);
	block-size: var(--stibok-slider-arrow-size);
	padding: 0;
	background: var(--wp--preset--color--surface, transparent);
	border: 0.0625rem solid var(--wp--preset--color--border, currentColor);
	border-radius: 50%;
	color: inherit;
	cursor: pointer;
	transition: background-color 0.2s ease, border-color 0.2s ease, opacity 0.2s ease;
}
.stibok-slider__arrow:hover {
	background: var(--wp--preset--color--surface-2, transparent);
}
.stibok-slider__arrow:focus-visible {
	outline: 0.125rem solid var(--stibok-slider-accent);
	outline-offset: 0.125rem;
}
.stibok-slider__arrow[disabled] {
	opacity: 0.4;
	cursor: default;
}
.stibok-slider__glyph {
	inline-size: 1.25em;
	block-size: 1.25em;
}

/* Position:sides — arrows overlaid on the left/right edges of the track, vertically
 * centred. `inset-inline` carries the configurable offset: 0 = flush to the edge,
 * POSITIVE pulls them inward over the slides, NEGATIVE pushes them OUTSIDE the
 * slides (into the surrounding gutter). The container ignores pointer events so the
 * gap between the arrows stays interactive; the buttons re-enable them. */
.stibok-slider.is-controls-sides .stibok-slider__arrows {
	position: absolute;
	inset-block: 0;
	inset-inline: var(--stibok-slider-arrow-offset);
	display: flex;
	align-items: center;
	justify-content: space-between;
	pointer-events: none;
	gap: 0;
}
.stibok-slider.is-controls-sides .stibok-slider__arrow {
	pointer-events: auto;
}

/* Reveal:hover — controls fade in on hover / keyboard focus of the slider. */
.stibok-slider.is-controls-hover .stibok-slider__arrows,
.stibok-slider.is-controls-hover .stibok-slider__dots {
	opacity: 0;
	transition: opacity 0.2s ease;
}
.stibok-slider.is-controls-hover:hover .stibok-slider__arrows,
.stibok-slider.is-controls-hover:focus-within .stibok-slider__arrows,
.stibok-slider.is-controls-hover:hover .stibok-slider__dots,
.stibok-slider.is-controls-hover:focus-within .stibok-slider__dots {
	opacity: 1;
}

/* Dots — a HOLLOW muted ring by default vs a FILLED accent pill when active, so the
 * current page reads at a glance (the prior all-filled styling made every dot look
 * active). */
.stibok-slider__dot {
	inline-size: var(--stibok-slider-dot-size);
	block-size: var(--stibok-slider-dot-size);
	padding: 0;
	background: transparent;
	border: 0.09375rem solid currentColor;
	border-radius: 999rem;
	opacity: 0.4;
	cursor: pointer;
	transition: opacity 0.2s ease, inline-size 0.2s ease, background-color 0.2s ease;
}
.stibok-slider__dot:hover {
	opacity: 0.7;
}
.stibok-slider__dot:focus-visible {
	outline: 0.125rem solid var(--stibok-slider-accent);
	outline-offset: 0.1875rem;
}
.stibok-slider__dot[aria-current="true"] {
	opacity: 1;
	inline-size: calc(var(--stibok-slider-dot-size) * 2.5);
	background: var(--stibok-slider-accent);
	border-color: var(--stibok-slider-accent);
}

/* Progress bar: a thin full-width track above the controls. */
.stibok-slider__progress {
	block-size: 0.1875rem;
	inline-size: 100%;
	background: var(--wp--preset--color--border, currentColor);
	border-radius: 0.1875rem;
	overflow: hidden;
}
.stibok-slider__progress-bar {
	display: block;
	block-size: 100%;
	background: var(--stibok-slider-accent);
	transition: inline-size 0.3s ease;
}

/* Variant: cards — each slide is a soft padded surface (recipe adds the tone). */
.stibok-slider.is-variant-cards .stibok-slider__slide {
	padding: var(--wp--preset--spacing--md, 1rem);
	border-radius: var(--wp--custom--radius--lg, 0.75rem);
}

@media (prefers-reduced-motion: reduce) {
	.stibok-slider__viewport {
		scroll-behavior: auto;
	}
	.stibok-slider__progress-bar,
	.stibok-slider__dot,
	.stibok-slider__arrow,
	.stibok-slider.is-controls-hover .stibok-slider__arrows,
	.stibok-slider.is-controls-hover .stibok-slider__dots {
		transition: none;
	}
}

/* Editor canvas parity (§1.7, D18): the canvas shows slide 1 at the start with the
 * others reachable by scroll — the same track + classes as the front. */
.stibok-slider.is-editing .stibok-slider__viewport {
	overflow-x: auto;
}

/* ===================================================================
 * Modal (3.5.5, D31) — a detached overlay dialog (CTA modal / drawer).
 *
 * The `<dialog>` fills the viewport as a transparent centering layer; the native
 * `::backdrop` is suppressed and `.stibok-modal__backdrop` is the SINGLE visible scrim
 * for BOTH the native `showModal()` path and the class-toggle fallback (no double-dim).
 * Closed = UA `dialog:not([open])` display:none; the store sets `[open]` to reveal, and
 * `:target` reveals it with JS off (OQ5). Logical props throughout (RTL-safe).
 * =================================================================== */

.stibok-modal__dialog {
	position: fixed;
	inset: 0;
	inline-size: 100%;
	block-size: 100%;
	max-inline-size: 100%;
	max-block-size: 100%;
	margin: 0;
	padding: var(--wp--preset--spacing--md, 1rem);
	border: 0;
	background: transparent;
	/* The card is internally scrolled + height-capped, so the dialog never scrolls;
	 * `visible` lets the card's drop-shadow render past the padding gap. */
	overflow: visible;
	z-index: var(--z-modal, 300);
}
.stibok-modal__dialog[open],
.stibok-modal__dialog:target {
	display: grid;
	place-items: center;
}
.stibok-modal__dialog::backdrop {
	background: transparent;
}

/* The single scrim (an `<a href="#">` so it also closes with JS off) — fades in/out. */
.stibok-modal__backdrop {
	position: fixed;
	inset: 0;
	display: block;
	background: rgba(16, 18, 26, 0.55);
	border: 0;
	cursor: pointer;
	z-index: 0;
	opacity: 0;
	transition: opacity 0.25s ease;
}
.stibok-modal__dialog[open] .stibok-modal__backdrop,
.stibok-modal__dialog:target .stibok-modal__backdrop {
	opacity: 1;
}

/* The visible card — a FLEX COLUMN so the header stays put and only the body scrolls.
 * Height-capped to the dialog's content box (which already reserves the padding gap);
 * `box-sizing:border-box` so the cap accounts for the card's own borders. Fades + rises
 * + scales in (and reverses out). */
.stibok-modal__container {
	box-sizing: border-box;
	position: relative;
	z-index: 1;
	display: flex;
	flex-direction: column;
	inline-size: min(100%, 32rem);
	/* Cap to the VIEWPORT (not `100%`): with the dialog's `place-items:center` the single
	 * grid row sizes to content, so a `100%` cap would resolve to the content height (no
	 * cap). `dvh` is definite regardless; the `vh` line is the fallback for old browsers.
	 * The `2 * md` matches the dialog's padding, leaving a gap top+bottom. */
	max-block-size: calc(100vh - 2 * var(--wp--preset--spacing--md, 1rem));
	max-block-size: calc(100dvh - 2 * var(--wp--preset--spacing--md, 1rem));
	/* The card itself never scrolls — the body does — so its rounded corners clip it. */
	overflow: hidden;
	background: var(--wp--preset--color--surface, #fff);
	color: var(--wp--preset--color--on-surface, #10121a);
	border-radius: var(--wp--custom--radius--lg, 0.75rem);
	box-shadow: var(--wp--preset--shadow--xl, 0 1.5rem 3.75rem -0.75rem rgba(16, 18, 26, 0.22));
	opacity: 0;
	transform: translateY(0.75rem) scale(0.97);
	transition: opacity 0.25s ease, transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.stibok-modal__dialog[open] .stibok-modal__container,
.stibok-modal__dialog:target .stibok-modal__container {
	opacity: 1;
	transform: none;
}

/* Enter FROM state (the browser transitions from here on first display). Progressive:
 * ignored where @starting-style is unsupported → the modal just appears (still fine). */
@starting-style {
	.stibok-modal__dialog[open] .stibok-modal__backdrop,
	.stibok-modal__dialog:target .stibok-modal__backdrop {
		opacity: 0;
	}
	.stibok-modal__dialog[open] .stibok-modal__container,
	.stibok-modal__dialog:target .stibok-modal__container {
		opacity: 0;
		transform: translateY(0.75rem) scale(0.97);
	}
}

/* Sizes (the card's max inline-size). */
.stibok-modal.is-size-sm .stibok-modal__container { inline-size: min(100%, 24rem); }
.stibok-modal.is-size-md .stibok-modal__container { inline-size: min(100%, 32rem); }
.stibok-modal.is-size-lg .stibok-modal__container { inline-size: min(100%, 48rem); }
.stibok-modal.is-size-full .stibok-modal__container {
	inline-size: 100%;
}

/* Header + title + close — a NON-shrinking row pinned above the scrolling body. */
.stibok-modal__header {
	box-sizing: border-box;
	flex: none;
	display: flex;
	align-items: flex-start;
	justify-content: space-between;
	gap: var(--wp--preset--spacing--sm, 0.5rem);
	padding: var(--wp--preset--spacing--lg, 1.5rem) var(--wp--preset--spacing--lg, 1.5rem) 0;
	margin: 0;
}
.stibok-modal__title {
	margin: 0;
	font-size: var(--wp--preset--font-size--lg, 1.25rem);
	line-height: 1.3;
}
.stibok-modal__close {
	flex: none;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	inline-size: 2rem;
	block-size: 2rem;
	margin-inline-start: auto;
	color: var(--wp--custom--semantic--on-surface-muted);
	border-radius: var(--wp--custom--radius--md, 0.5rem);
	text-decoration: none;
	transition: color 0.15s ease, background 0.15s ease;
}
.stibok-modal__close:hover,
.stibok-modal__close:focus-visible {
	color: var(--wp--preset--color--on-surface, #10121a);
	background: var(--wp--preset--color--surface-2, rgba(16, 18, 26, 0.06));
}
.stibok-modal__close .stibok-modal__glyph {
	inline-size: 1.25em;
	block-size: 1.25em;
}

/* The body is the ONLY scroll region (min-block-size:0 lets a flex child shrink to
 * enable its own overflow). Its own padding is fully inside the scroll area. */
.stibok-modal__body {
	box-sizing: border-box;
	flex: 1 1 auto;
	min-block-size: 0;
	overflow-y: auto;
	padding: var(--wp--preset--spacing--lg, 1.5rem);
}
/* A header directly above the body supplies the gap, so trim the body's top padding. */
.stibok-modal__header + .stibok-modal__body {
	padding-block-start: var(--wp--preset--spacing--md, 1rem);
}
.stibok-modal__body > *:first-child { margin-block-start: 0; }

/* Drawer variant: an edge-to-edge side panel, full viewport height, sliding in from the
 * inline-end edge. No dialog padding (the panel touches the top/bottom/end edges) and no
 * radius; `size` maps to the panel WIDTH (not a centered-card max-width). */
.stibok-modal.is-variant-drawer .stibok-modal__dialog {
	padding: 0;
}
.stibok-modal.is-variant-drawer .stibok-modal__dialog[open],
.stibok-modal.is-variant-drawer .stibok-modal__dialog:target {
	place-items: stretch end;
}
.stibok-modal.is-variant-drawer .stibok-modal__container {
	block-size: 100vh;
	block-size: 100dvh;
	max-block-size: 100vh;
	max-block-size: 100dvh;
	border-radius: 0;
	/* Slide the full panel width off the inline-end edge (RTL-aware). */
	transform: translateX(100%);
	transition: opacity 0.25s ease, transform 0.32s cubic-bezier(0.16, 1, 0.3, 1);
}
[dir="rtl"] .stibok-modal.is-variant-drawer .stibok-modal__container {
	transform: translateX(-100%);
}
/* Drawer widths — `size` controls how wide the panel is. */
.stibok-modal.is-variant-drawer.is-size-sm .stibok-modal__container { inline-size: min(100%, 20rem); }
.stibok-modal.is-variant-drawer.is-size-md .stibok-modal__container { inline-size: min(100%, 26rem); }
.stibok-modal.is-variant-drawer.is-size-lg .stibok-modal__container { inline-size: min(100%, 34rem); }
.stibok-modal.is-variant-drawer.is-size-full .stibok-modal__container { inline-size: min(100%, 48rem); }
.stibok-modal.is-variant-drawer .stibok-modal__dialog[open] .stibok-modal__container,
.stibok-modal.is-variant-drawer .stibok-modal__dialog:target .stibok-modal__container {
	transform: none;
	opacity: 1;
}
@starting-style {
	.stibok-modal.is-variant-drawer .stibok-modal__dialog[open] .stibok-modal__container,
	.stibok-modal.is-variant-drawer .stibok-modal__dialog:target .stibok-modal__container {
		transform: translateX(100%);
	}
	[dir="rtl"] .stibok-modal.is-variant-drawer .stibok-modal__dialog[open] .stibok-modal__container {
		transform: translateX(-100%);
	}
}

/* JS-orchestrated EXIT: the store adds `.is-closing`, the card + scrim animate OUT, then
 * (on `transitionend`) the store calls `dialog.close()` — so the top-layer removal + the
 * browser's native focus-restore happen cleanly AFTER the animation, never fighting it
 * mid-transition (which dropped focus to <body>). Placed last so it wins the open-state
 * specificity ties. */
.stibok-modal.is-closing .stibok-modal__backdrop {
	opacity: 0;
}
.stibok-modal.is-closing .stibok-modal__container {
	opacity: 0;
	transform: translateY(0.75rem) scale(0.97);
}
.stibok-modal.is-variant-drawer.is-closing .stibok-modal__container {
	opacity: 1;
	transform: translateX(100%);
}
[dir="rtl"] .stibok-modal.is-variant-drawer.is-closing .stibok-modal__container {
	transform: translateX(-100%);
}

/* Scroll-lock on the document while any modal is open. `padding-inline-end` compensates
 * the removed scrollbar (the width is measured + set as `--stibok-sbw` by the store) so
 * the BACKGROUND content doesn't shift; the store defers releasing the lock until the
 * exit transition ends so the fixed dialog doesn't reflow mid-close. */
html.is-stibok-modal-open {
	overflow: hidden;
	padding-inline-end: var(--stibok-sbw, 0px);
}

@media (prefers-reduced-motion: reduce) {
	.stibok-modal__dialog,
	.stibok-modal__backdrop,
	.stibok-modal__container,
	.stibok-modal.is-variant-drawer .stibok-modal__container { transition: none; }
	.stibok-modal__container { opacity: 1; transform: none; }
	.stibok-modal__backdrop { opacity: 1; }
	.stibok-modal__close { transition: none; }
}

/* Editor canvas parity (§1.7, D18): the modal renders INLINE + visible in the canvas
 * (NOT hidden behind a trigger) so its content is editable — the same
 * `.stibok-modal__*` classes as the front, but as an in-flow card, not a fixed overlay.
 * The body must NOT be a capped scroll region here (all content editable). */
.stibok-modal.is-editing .stibok-modal__dialog {
	position: static;
	display: block;
	inline-size: auto;
	block-size: auto;
	padding: 0;
	overflow: visible;
	z-index: auto;
}
.stibok-modal.is-editing .stibok-modal__backdrop { display: none; }
.stibok-modal.is-editing .stibok-modal__container {
	opacity: 1;
	transform: none;
	/* Keep the `size`-driven width (front parity) — do NOT force `auto`; only lift the
	 * height cap so ALL body content is visible + editable in the canvas. */
	max-block-size: none;
	block-size: auto;
	overflow: visible;
}
.stibok-modal.is-editing .stibok-modal__body {
	overflow: visible;
}

/* ---------------------------------------------------------------------------
 * Lightbox variant (CL1.7b) — a fullscreen dark gallery overlay reusing the modal
 * runtime. The container holds a `slider` of the full images; the scrim is a deep dim
 * so photos read on any page. Structure only — the recipe's `lightbox` variant owns the
 * skinnable close/title tokens ({on-accent}). No baked hex except the var() fallbacks the
 * modal block already uses.
 * ------------------------------------------------------------------------- */
.stibok-modal.is-variant-lightbox .stibok-modal__backdrop {
	background: var(--wp--preset--color--overlay-strong, rgba(9, 10, 15, 0.92));
}
/* Lightbox is a flex column that MUST fit the dialog's content box: the image stage
 * flexes, the progress/controls footer is `flex:none` so arrows are never clipped by
 * the container's `overflow:hidden` (the previous `max-block-size: 100dvh - 8rem` on
 * the image alone left no room for the footer once the admin bar / padding ate into
 * the viewport). */
.stibok-modal.is-variant-lightbox .stibok-modal__container {
	inline-size: min(100%, 64rem);
	block-size: calc(100dvh - 2 * var(--wp--preset--spacing--md, 1rem));
	max-block-size: calc(100dvh - 2 * var(--wp--preset--spacing--md, 1rem));
	/* Keep the circular arrows clear of the container's overflow:hidden edge — without
	 * this the bottom 1px of the ring (and any focus ring) is shaved off. */
	padding-block-end: var(--wp--preset--spacing--xs, 0.5rem);
	background: transparent;
	box-shadow: none;
	border-radius: 0;
}
/* WP admin bar overlays the top of the viewport; shrink the lightbox so controls
 * stay above the bottom edge (32px desktop / 46px ≤782px — WP's own breakpoints). */
.admin-bar .stibok-modal.is-variant-lightbox .stibok-modal__container {
	block-size: calc(100dvh - 2 * var(--wp--preset--spacing--md, 1rem) - 32px);
	max-block-size: calc(100dvh - 2 * var(--wp--preset--spacing--md, 1rem) - 32px);
}
@media screen and (max-width: 782px) {
	.admin-bar .stibok-modal.is-variant-lightbox .stibok-modal__container {
		block-size: calc(100dvh - 2 * var(--wp--preset--spacing--md, 1rem) - 46px);
		max-block-size: calc(100dvh - 2 * var(--wp--preset--spacing--md, 1rem) - 46px);
	}
}
.stibok-modal.is-variant-lightbox .stibok-modal__header {
	/* Close sits over the stage; don't reserve a full header band that steals image room. */
	position: absolute;
	inset-block-start: 0;
	inset-inline-end: 0;
	z-index: 2;
	padding: var(--wp--preset--spacing--sm, 0.75rem);
	pointer-events: none;
}
.stibok-modal.is-variant-lightbox .stibok-modal__close {
	pointer-events: auto;
}
/* Body fills the container; the slider owns the column (stage + footer). */
.stibok-modal.is-variant-lightbox .stibok-modal__body {
	padding: 0;
	overflow: hidden;
	display: flex;
	flex-direction: column;
	flex: 1 1 auto;
	min-block-size: 0;
	align-items: stretch;
}
.stibok-modal.is-variant-lightbox .stibok-modal__close:hover,
.stibok-modal.is-variant-lightbox .stibok-modal__close:focus-visible {
	background: var(--wp--preset--color--surface-inverse-2, rgba(255, 255, 255, 0.14));
}
.stibok-modal.is-variant-lightbox .stibok-slider {
	inline-size: 100%;
	flex: 1 1 auto;
	min-block-size: 0;
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--sm, 0.75rem);
}
.stibok-modal.is-variant-lightbox .stibok-slider__stage {
	flex: 1 1 auto;
	min-block-size: 0;
	display: flex;
	flex-direction: column;
}
.stibok-modal.is-variant-lightbox .stibok-slider__viewport {
	inline-size: 100%;
	flex: 1 1 auto;
	min-block-size: 0;
	align-items: center;
	/* No hover-bleed padding inside the lightbox — it stole height from the image stage. */
	padding-block: 0;
	margin-block: 0;
}
.stibok-modal.is-variant-lightbox .stibok-slider__slide {
	display: flex;
	align-items: center;
	justify-content: center;
	align-self: stretch;
	block-size: 100%;
	overflow: hidden;
}
.stibok-modal.is-variant-lightbox .stibok-slider__slide .stibok-image {
	display: flex;
	align-items: center;
	justify-content: center;
	max-inline-size: 100%;
	max-block-size: 100%;
	block-size: 100%;
}
.stibok-modal.is-variant-lightbox .stibok-slider__progress,
.stibok-modal.is-variant-lightbox .stibok-slider__controls {
	flex: none;
}
/* The lightbox draws its content on a near-black scrim (rgba(9,10,15,.92) → #1d1e22) but
 * never told the token system so — it renders inside whatever tone the host section has,
 * which is normally light. Muted text therefore came out as the LIGHT `muted` (#6b7280) on
 * near-black: 3.44:1, failing WCAG AA. Found by axe in QA.8 on the slider's "N / M" counter
 * and the image caption.
 *
 * Fix it at the token, not at each element: the recipe compiler already emits the counter as
 * `color: var(--wp--custom--semantic--on-surface-muted)` — correctly tone-aware — so
 * re-pointing that semantic var here fixes the counter through the mechanism the design
 * system already has, and carries any muted part added to a lightbox later. `muted-2` is the
 * palette's muted-on-dark (the slug `.is-tone-dark` itself picks); on the scrim it is 6.32:1.
 * (Element-level overrides would also have LOST here: the recipe CSS is inline <style> in the
 * page, so at equal specificity it wins on document order.)
 *
 * Only `on-surface-muted` is re-pointed, not the full `.is-tone-dark` set: the close button
 * and the arrows below set their own foregrounds for this scrim, and the rest of the tone
 * set (`surface`, `border`, `accent-soft`) has nothing to act on inside an overlay whose
 * container is transparent. */
/* The lightbox draws on a near-black scrim; tell the token system so, and every muted part
 * inside follows (the slider counter and the image caption both read
 * `--wp--custom--semantic--on-surface-muted`). This ONE re-point is what makes them
 * scrim-aware — no per-element caption rule is needed, now that the base rules use the
 * semantic token (tone-contrast increment) rather than the raw `muted` preset. */
/* `on-accent` is pinned for the SAME reason. Everywhere else `{on-accent}` is the ink ON an
 * `{accent}` FILL, so the two invert together with the tone (see `.is-tone-dark`); the lightbox
 * recipe is the one consumer that uses it as ink on the SCRIM instead. Inside a `tone:"dark"`
 * section it would therefore follow the dark tone's `on-accent` (near-black `contrast`) and the
 * close button + title would vanish into the near-black scrim. The scrim is its own surface, so
 * the overlay names its own foreground: `base` (#ffffff) = 16.65:1, matching the close button
 * rule below. */
.stibok-modal.is-variant-lightbox {
	--wp--custom--semantic--on-surface-muted: var(--wp--preset--color--muted-2);
	--wp--custom--semantic--on-accent:        var(--wp--preset--color--base);
}
/* The slider arrows were INVISIBLE on the scrim: 1.12:1 (QA.8 review). Two causes, both
 * the "styled for a surface it is not on" shape:
 *   - `.stibok-slider__arrow` is `color: inherit`, so on the scrim it inherited the host
 *     section's light tone (#10121a) — a near-black chevron on near-black;
 *   - its `background: var(--wp--preset--color--surface, transparent)` names a slug NO
 *     theme defines (the palette is `base`/`base-2`; `surface` exists only in the SEMANTIC
 *     layer) — QA.7's exact finding, one instance it did not reach. Saved by its fallback,
 *     so it degrades to `transparent` rather than black, and the intended pill has simply
 *     never rendered ON ANY PAGE. Fixing that base rule is a slider-wide change and is
 *     filed, not done here — but it is why `background` must be pinned below: if a theme
 *     ever DID define `surface`, the arrow would gain a white pill and this white glyph
 *     would vanish into it.
 * axe cannot see either (an SVG glyph has no text node), which is the demo's own point
 * that a scanner is a floor. `base` (#ffffff) on the scrim = 16.65:1, matching the close
 * button; the `muted-2` ring = 6.32:1. WCAG 1.4.11 asks 3:1 for non-text. */
.stibok-modal.is-variant-lightbox .stibok-slider__arrow {
	background: transparent;
	border-color: var(--wp--preset--color--muted-2);
	color: var(--wp--preset--color--base);
}
/* Full images are contained to the STAGE (never cropped). Cap to the flex stage, not
 * the viewport — the footer (progress + arrows) must keep its own band. Zoomable
 * images get `cursor: zoom-in` from JS (`is-zoomable`); zoomed state pans inside the
 * slide's `overflow:hidden` frame. */
.stibok-modal.is-variant-lightbox .stibok-slider__slide img {
	max-block-size: 100%;
	max-inline-size: 100%;
	inline-size: auto;
	block-size: auto;
	margin-inline: auto;
	object-fit: contain;
	user-select: none;
	-webkit-user-drag: none;
}
.stibok-modal.is-variant-lightbox .stibok-slider__slide img.is-zoomable {
	cursor: zoom-in;
}
.stibok-modal.is-variant-lightbox .stibok-slider__slide.is-zoomed {
	cursor: zoom-out;
}
.stibok-modal.is-variant-lightbox .stibok-slider__slide.is-zoomed img {
	max-inline-size: none;
	max-block-size: none;
	cursor: grab;
	/* Natural 1:1 size is applied as inline width/height by the zoom store. */
}
.stibok-modal.is-variant-lightbox .stibok-slider__slide.is-zoomed.is-panning img,
.stibok-modal.is-variant-lightbox .stibok-slider__slide.is-zoomed img:active {
	cursor: grabbing;
}
/* While zoomed, the track must not steal horizontal pans for slide scrolling. */
.stibok-modal.is-variant-lightbox .stibok-slider__viewport.is-zoom-locked {
	overflow-x: hidden;
	scroll-snap-type: none;
}

/* A gallery item that opens the lightbox — the whole slide (CL1.7b) or the whole grid image
 * (CL1.7c) is a zoom affordance (an `<a href="#overlay">` so it ALSO opens with JS off via the
 * modal `:target` fallback; the store preventDefaults + jumps the inner slider to this item's
 * index when JS is on). Both triggers share one appearance. */
.stibok-slider__lightbox-trigger,
.stibok-image__lightbox-trigger {
	display: block;
	inline-size: 100%;
	padding: 0;
	border: 0;
	background: none;
	color: inherit;
	cursor: zoom-in;
	text-decoration: none;
}
.stibok-slider__lightbox-trigger:focus-visible,
.stibok-image__lightbox-trigger:focus-visible {
	outline: 2px solid var(--wp-admin-theme-color, currentColor);
	outline-offset: 2px;
}

/* Editor canvas affordance (CL1.7b): the overlay is generated on the FRONT (not an editor
 * block), so the canvas shows a hint badge + a zoom cursor on slides so the author knows the
 * gallery is lightbox-enabled. Editor-scoped (`.is-editing`) — never on the front. */
.stibok-slider__lightbox-hint {
	display: inline-flex;
	align-items: center;
	inline-size: fit-content;
	margin-block-end: var(--wp--preset--spacing--xs, 0.5rem);
	padding: 0.25rem 0.5rem;
	font-size: var(--wp--preset--font-size--xs, 0.75rem);
	color: var(--wp--custom--semantic--on-surface-muted);
	background: var(--wp--preset--color--surface-2, rgba(16, 18, 26, 0.06));
	border-radius: var(--wp--custom--radius--sm, 0.375rem);
}
.stibok-slider.is-editing.is-lightbox .stibok-slider__slide {
	cursor: zoom-in;
}

/* Grid gallery editor affordance (CL1.7c): `is-editing is-lightbox` is emitted ONLY on the
 * editor canvas (grid/render.php never adds either on the front), so this pseudo-hint is
 * editor-scoped by construction AND by the `.is-editing` guard (defense-in-depth, matching the
 * slider hint). A dashed outline + a corner badge tell the author the grid's images open a
 * fullscreen lightbox on the front; direct images get the zoom cursor. The badge is absolutely
 * positioned so it is NOT a grid track item (no layout shift). */
.stibok-grid.is-editing.is-lightbox {
	position: relative;
	outline: 1px dashed var(--wp--preset--color--outline, rgba(16, 18, 26, 0.24));
	outline-offset: 0.25rem;
}
.stibok-grid.is-editing.is-lightbox::after {
	content: "⤢";
	position: absolute;
	inset-block-start: 0.25rem;
	inset-inline-end: 0.25rem;
	inline-size: 1.5rem;
	block-size: 1.5rem;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	font-size: var(--wp--preset--font-size--xs, 0.75rem);
	color: var(--wp--custom--semantic--on-surface-muted);
	background: var(--wp--preset--color--surface-2, rgba(16, 18, 26, 0.06));
	border-radius: var(--wp--custom--radius--sm, 0.375rem);
	pointer-events: none;
}
.stibok-grid.is-editing.is-lightbox > .stibok-image {
	cursor: zoom-in;
}

/* Standalone image.lightbox editor affordance (CL1.7d): canvas-only cue that the
 * image opens a fullscreen overlay on the front. Front never emits is-editing. */
.stibok-image.is-editing.is-lightbox {
	position: relative;
	outline: 1px dashed var(--wp--preset--color--outline, rgba(16, 18, 26, 0.24));
	outline-offset: 0.25rem;
	cursor: zoom-in;
}
.stibok-image.is-editing.is-lightbox::after {
	content: "⤢";
	position: absolute;
	inset-block-start: 0.25rem;
	inset-inline-end: 0.25rem;
	inline-size: 1.5rem;
	block-size: 1.5rem;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	font-size: var(--wp--preset--font-size--xs, 0.75rem);
	color: var(--wp--custom--semantic--on-surface-muted);
	background: var(--wp--preset--color--surface-2, rgba(16, 18, 26, 0.06));
	border-radius: var(--wp--custom--radius--sm, 0.375rem);
	pointer-events: none;
	z-index: 1;
}

/* ---------------------------------------------------------------------------
 * Embed (3.5.6) — a safe, host-bounded third-party iframe (Google Maps first).
 * The `frame` part is a ratio-box; the iframe fills it. Logical props / RTL-safe.
 * ------------------------------------------------------------------------- */
.stibok-embed {
	display: block;
	inline-size: 100%;
}
.stibok-embed__frame {
	position: relative;
	inline-size: 100%;
	/* aspect-ratio set inline from the `ratio` token (front) / attribute (canvas). */
	overflow: hidden;
	border-radius: var(--stibok-radius-md, 0.5rem);
	background: var(--wp--preset--color--surface, rgba(0, 0, 0, 0.04));
}
.stibok-embed__iframe {
	position: absolute;
	inset: 0;
	inline-size: 100%;
	block-size: 100%;
	border: 0;
	display: block;
}

/* Editor canvas parity (§1.7, D18): the canvas NEVER loads the live third-party
 * iframe (perf/privacy) — it shows a placeholder card inside the same ratio-box,
 * so the author sees the map's size + intent without a network hit. */
.stibok-embed.is-editing .stibok-embed__frame {
	display: grid;
	place-items: center;
	border: 1px dashed var(--stibok-border, rgba(0, 0, 0, 0.18));
	background: var(--wp--preset--color--surface, rgba(0, 0, 0, 0.03));
}
.stibok-embed__placeholder {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: 0.5rem;
	padding: 1rem;
	text-align: center;
	color: var(--stibok-muted, rgba(0, 0, 0, 0.6));
}
.stibok-embed__placeholder-icon {
	font-size: 2rem;
	line-height: 1;
	color: var(--wp-admin-theme-color, currentColor);
}
.stibok-embed__placeholder-label {
	font-weight: 600;
	color: inherit;
}
.stibok-embed__placeholder-host {
	font-size: 0.8125rem;
	word-break: break-all;
	opacity: 0.85;
}

/* "AI Section" inserter block (4.5c) — the in-canvas placeholder body. Editor-only (the block is
   replaced by the generated section on success; it never renders on the front). */
.stibok-ai-section__body {
	display: flex;
	flex-direction: column;
	gap: 0.5rem;
	inline-size: 100%;
	text-align: start;
}
.stibok-ai-section__actions {
	display: flex;
	align-items: center;
	gap: 0.5rem;
}
.stibok-ai-section__errors {
	margin-block: 0.25rem 0;
	padding-inline-start: 1.25rem;
	list-style: disc;
	font-size: 0.8125rem;
}

/* ── Marquee (CL1.8) ──────────────────────────────────────────────────────────
   A seamless continuous auto-scroll strip (logo wall / ticker). The track is a
   `width:max-content` flex rail holding TWO identical groups; the loop keyframe
   (register_keyframe → `.stibok-motion[data-stibok-kf="marquee-x|y"]`) translates it
   -50% (exactly one group width) so the 2nd group tiles the 1st with no seam. Speed is
   a fixed `--sm-kf-duration` per preset (linear, jank-free). No per-instance JS;
   reduced-motion drops the binding (registry gate) + this file hides the clone. */
.stibok-marquee {
	overflow: hidden;
	max-inline-size: 100%;
}
.stibok-marquee__track {
	display: flex;
	inline-size: max-content;
	max-inline-size: none;
	will-change: transform;
}
.stibok-marquee__group {
	display: flex;
	flex: 0 0 auto;
	align-items: center;
	gap: var(--stibok-marquee-gap, var(--wp--preset--spacing--xl));
	/* Trailing gap so the loop boundary spaces the repeats like the inner items. */
	padding-inline-end: var(--stibok-marquee-gap, var(--wp--preset--spacing--xl));
}
/* Direction: the keyframe always runs 0 → -50%; right/down just reverse it. This
   `animation-direction` longhand outranks the binding's `animation:` shorthand reset
   (more specific selector). */
.stibok-marquee.is-direction-right .stibok-marquee__track,
.stibok-marquee.is-direction-down .stibok-marquee__track {
	animation-direction: reverse;
}
/* Pause on hover / keyboard focus (default on; the render sets data-pause-hover). */
.stibok-marquee[data-pause-hover]:hover .stibok-marquee__track,
.stibok-marquee[data-pause-hover]:focus-within .stibok-marquee__track {
	animation-play-state: paused;
}
/* Vertical orientation: a fixed-height viewport; the track stacks in a column
   (`block-size:max-content` = two groups tall) and the marquee-y keyframe scrolls it by
   -50% = exactly one group height. */
.stibok-marquee.is-orientation-vertical {
	block-size: 24rem;
}
.stibok-marquee.is-orientation-vertical .stibok-marquee__track {
	flex-direction: column;
	inline-size: auto;
	block-size: max-content;
}
.stibok-marquee.is-orientation-vertical .stibok-marquee__group {
	flex-direction: column;
	padding-inline-end: 0;
	padding-block-end: var(--stibok-marquee-gap, var(--wp--preset--spacing--xl));
}
/* Edge-fade variant: a mask that softens both ends so items enter/leave gently. */
.stibok-marquee.is-variant-faded {
	-webkit-mask-image: linear-gradient(to right, transparent, #000 8%, #000 92%, transparent);
	mask-image: linear-gradient(to right, transparent, #000 8%, #000 92%, transparent);
}
.stibok-marquee.is-variant-faded.is-orientation-vertical {
	-webkit-mask-image: linear-gradient(to bottom, transparent, #000 8%, #000 92%, transparent);
	mask-image: linear-gradient(to bottom, transparent, #000 8%, #000 92%, transparent);
}
/* Reduced motion → static: hide the aria-hidden clone so the strip shows ONE clean set. */
@media (prefers-reduced-motion: reduce) {
	.stibok-marquee__group[aria-hidden="true"] {
		display: none;
	}
}

/* ── Diagram (Phase INF, INF.2) ──────────────────────────────────────────────
 * The infographics assembly point: ONE container, `mode:"radial"` this build. Items
 * are absolutely positioned via the per-item --dx/--dy custom properties (rem, from
 * Render_Helpers::radial_positions()); the scaffold SVG (a decorative ring stroke)
 * and an optional center label/icon are also absolutely positioned, center-locked.
 *
 * Fable F5 (reserve-now): the scaffold consumes a FROZEN named custom-property set —
 * --stibok-diagram-stroke-w/-dash/-cap/-seg-radius/-marker-size — so a skin can give
 * the "same diagram" a radically different stroke CHARACTER (hairline/bold/dashed/
 * hand-drawn) with ZERO new geometry. stroke-w/dash/cap are consumed by every ring/
 * connector-based mode; **seg-radius is consumed by `matrix`** (P.3, the matrix
 * quadrant `border-radius` below — the one mode whose "segments" are real DOM boxes);
 * the SVG-path segment modes (cycle/funnel/pyramid) still have no consumer (a genuine
 * corner-fillet on the arc/chevron path is a bigger geometry change than any increment
 * has scoped); **marker-size is now consumed** (INF.9, `.stibok-diagram__
 * marker`'s diameter, below) — reserved since INF.2, unconsumed until `hotspots`
 * shipped the first actual marker dot.
 */
.stibok-diagram {
	/* Phase INF-Polish P.1 item 6 (review E4): raised from `0.0625rem` (1px @ 16
	 * base — measured `#e4e7ec` on `#f4f5f7` = 1.15:1, effectively invisible) to a
	 * visible mid-weight line; paired with the `{on-surface-muted}` recipe token now shared by
	 * `ring`/`connector`/`marker` (recipes/defaults/diagram.json) — `#6b7280` reads
	 * 4.47–4.83:1 against the theme's `base`/`base-2` surfaces (well past the 3:1
	 * WCAG non-text minimum), replacing the OTHER extreme (`connector`/`marker`
	 * falling through to unstyled full-ink `currentColor`, ~17–19:1 — "opposite
	 * weights" in the SAME diagram family, review E4). */
	--stibok-diagram-stroke-w: 0.125rem;
	--stibok-diagram-dash: none;
	--stibok-diagram-cap: round;
	/* Phase INF-Polish P.3 fix (MED-2): baseline is `0` (square), NOT `0.25rem` — this
	 * var INHERITS and the matrix quadrant rule below reads it, so a non-zero baseline
	 * silently rounded EVERY pre-existing matrix diagram (no variant, no `segRadius`).
	 * `0` keeps an unstyled matrix square; a variant (`soft` = 0.75rem) or a per-block
	 * `segRadius` opts IN. This value is also the omit-default sentinel mirrored in
	 * render-helpers.php / ir-validator.php / editor-logic.js (D18) — keep it in sync. */
	--stibok-diagram-seg-radius: 0;
	--stibok-diagram-marker-size: 1.5rem;
	/* Phase INF-Polish P.3 (D-P3): the missing STROKE-COLOUR knob. There was
	 * width/dash/cap but no colour var, so a line's colour could not be set
	 * independently of text (`stroke="currentColor"` tied it to the item `color`).
	 * Deliberately NOT given a value here — every stroke sink reads it with a
	 * `currentColor` FALLBACK (`stroke: var(--stibok-diagram-stroke-color,
	 * currentColor)`), so UNSET is byte-for-byte "follow the site" (the pre-P.3
	 * behaviour), and a skin/variant/per-block control that sets it wins. Frozen
	 * name — never rename (Fable F5).
	 *
	 * P.3: `--stibok-diagram-backdrop` (a `<background-image>` value — a grid for
	 * `blueprint`, a solid dark fill for `neon`) and `--stibok-diagram-glow` (a
	 * `<filter>` on the scaffold — `neon`'s bloom) are the ONE new var each the
	 * plan sanctioned for the two characters that need a backdrop/effect not
	 * already expressible through the stroke set — a skin restyle can set them too,
	 * so no character is un-reproducible via vars (D-P1). Both consumed below with a
	 * no-op default (`none`), so an unset diagram is unchanged; `background-image`
	 * (never `background-color`) keeps an author's own `appearance.background`
	 * untouched. */
	background-image: var(--stibok-diagram-backdrop, none);
	position: relative;
}

/* ── Diagram — fit-to-width (Outside labels) ───────────────────────────────────
 * When `labelPlacement:outside` the natural square (radius + offset + item pad)
 * often exceeds the available slot; section `overflow-x:clip` then crops the
 * side callouts. `is-fit` makes the diagram a size container and scales the
 * inner composition with `min(1, 100cqi / --diagram-natural)` so the whole
 * brochure ring shrinks proportionally instead of clipping.
 *
 * SSR seeds a generous `--diagram-natural` (worst-case outward clearance) so
 * first paint never clips. `interactions.js` (front) and the canvas mirror then
 * measure the real content bbox and tighten the var — `.is-fit-measured` marks
 * that pass. `--diagram-fit-floor` is the ring-only lower bound.
 *
 * NO CSS `block-size` on `.is-fit` — a `calc(natural × scale)` height was shorter
 * than the collapsed 2-col list and clipped cards on phones (even when the
 * narrow-stop reset tried to set `auto`). Layout height is `auto` by default;
 * ring-mode empty-band tightening is JS-only (`applyDiagramFitNatural` sets an
 * inline block-size, cleared again once INF.4 collapses the list).
 *
 * Uses an unnamed `container-type:inline-size` for `100cqi` (the named `stibok`
 * container stays on `.stibok-cq` / cq-safe slots). Narrow-stop fit kill lives in
 * `container_css()` alongside the list reflow. Prototype only (auto for outside). */
.stibok-diagram.is-fit {
	/* Own size container for `100cqi` scale math. Deliberately UNNAMED —
	   the named `stibok` query container stays only on `.stibok-cq` here
	   (StylesheetTest). Narrow-stop fit kill lives in container_css() and
	   matches the ancestor/self slot the INF.4 list reflow already uses. */
	container-type: inline-size;
	inline-size: 100%;
	max-inline-size: 100%;
	/* visible — never hard-clip callouts; natural sizing + scale must contain them. */
	overflow: visible;
	/* Height from content (collapsed list) or from JS inline block-size (ring).
	   Never a CSS calc() that can undershoot the list and clip. */
	min-block-size: 0;
}
.stibok-diagram.is-fit > .stibok-diagram__fit-scale {
	/* Containing block for scaffold / center / items. When natural > slot width,
	   grid/flex "center" leaves the oversized layout box start-aligned in some
	   engines — scale then shrinks around that off-centre box (empty on one side,
	   clipped on the other). Explicit 50% + translate(-50%) keeps the unscaled
	   box centred even while it overflows, so scale(top center) stays centred. */
	position: relative;
	inset-inline-start: 50%;
	inline-size: var(--diagram-natural, 24rem);
	block-size: var(--diagram-natural, 24rem);
	min-block-size: var(--diagram-natural, 24rem);
	translate: -50% 0;
	scale: min( 1, 100cqi / var(--diagram-natural, 24rem) );
	transform-origin: top center;
	overflow: visible;
}
/* Narrow-stop fit kill lives in Render_Helpers::container_css() (same `stibok`
 * stop as the list reflow) — do NOT duplicate a hardcoded 40rem query here. */

/* Every anchor below sits at the DEAD CENTER (50%/50%) — logical inset-inline-start/
 * inset-block-start is exactly as correct as physical left/top here (50% from either
 * edge is the same point in LTR or RTL), so the RTL-safe logical props cost nothing.
 * The actual per-item offset (--dx/--dy) rides `translate`, which is always a physical
 * x/y pair regardless of writing mode (D-INF8's geometry is computed server-side as
 * physical (x,y)/(cos,sin) coordinates) — that is correct BECAUSE the anchor it is
 * added to is direction-agnostic, not despite it. */
.stibok-diagram__scaffold {
	position: absolute;
	inset-inline-start: 50%;
	inset-block-start: 50%;
	translate: -50% -50%;
	pointer-events: none;
	/* Phase INF-Polish P.4 MED-1: the `<svg>` box is a SQUARE sized `2·radius` (F1),
	 * but `labelPlacement:outside` (the labelled-ring default) pushes each NON-ICON
	 * node — and the P.4 leader tick that connects the ring to it — out to `radius+3`
	 * rem, i.e. to viewBox radius `50 + 3·(50/r) > 50`. A cardinal-direction tick's
	 * outer endpoint therefore lands OUTSIDE the `0 0 100 100` viewport (top node →
	 * `y2` negative, side node → `x2 > 100`) and, with the SVG's default `overflow`
	 * (clip-to-viewport), was mostly hidden — only ~45° diagonal ticks escaped. The
	 * hotspots scaffold already set `overflow:visible` for the same reason (~line
	 * 4246); the ticks/spokes/segments are `pointer-events:none` decor and the wrapper
	 * already reserves the node extent (F12 min-height + node padding), so lifting the
	 * clip cannot capture pointers or paint past what the nodes themselves occupy. */
	overflow: visible;
	/* Phase INF-Polish P.3: the `neon` character's bloom (and any skin glow) — a
	 * no-op `none` default, so an unset diagram is unchanged. Scoped to the
	 * scaffold so it lifts ring/segment/connector/band strokes together without
	 * touching the real item content that sits outside the `<svg>`. */
	filter: var(--stibok-diagram-glow, none);
}
/* Phase INF-Polish P.1 items 1+2 (D-P4, owner-observed defect): an icon child gets
 * an OPAQUE chip — a solid token-filled disc with the glyph "knocked out" (forced
 * to a contrasting on-accent color) — plus a subtle token shadow (item 2) so it
 * reads as a physical disc floating over the connector, not a flat cutout the line
 * shows through. This is the OTHER half of the connector fix ({@see
 * Render_Helpers::pullback_endpoints()}/`DIAGRAM_NODE_PULLBACK_REM`, render.php):
 * the owner-observed defect was a transparent icon glyph with the connector line
 * running straight through it — the chip covers the join AND (D-P4) the connector
 * geometry now also stops short of the chip's own edge, so real diagram tools'
 * "line terminates at the node" convention holds on both counts.
 *
 * `color` needs `!important`: the icon block writes its OWN configured color as an
 * INLINE style (`stibok/icon` render.php, `style="color:…"`), and an inline style
 * always out-ranks any external stylesheet rule regardless of selector specificity
 * — there is no other way to "knock out" an author's chosen icon color here. Scoped
 * narrowly (this one selector) — the same sparing, documented use already
 * established elsewhere in this file (e.g. the narrow-collapse form-field reset).
 */
.stibok-diagram__item > .stibok-icon {
	background: var(--wp--preset--color--accent);
	color: var(--wp--preset--color--base, #fff) !important;
	border-radius: 50%;
	padding: var(--wp--preset--spacing--sm, 1rem);
	box-shadow: var(--wp--preset--shadow--md, 0 0.25rem 0.75rem -0.125rem rgba(16, 18, 26, 0.10));
}
.stibok-diagram__ring {
	stroke-width: var(--stibok-diagram-stroke-w);
	stroke-dasharray: var(--stibok-diagram-dash);
	stroke-linecap: var(--stibok-diagram-cap);
	/* P.3 (D-P3): the stroke-colour knob reaches the SAME set width/dash/cap do —
	 * the ring (here) and every connector (`.stibok-diagram__connector`, below).
	 * `currentColor` fallback = unchanged when unset (the `<circle>`'s own
	 * `stroke="currentColor"` attribute produced exactly this before). The draw-on
	 * ring (`.stibok-diagram__stroke`) is this same element, so it is covered; the
	 * filled `.stibok-diagram__segment`/`__band` shapes take no stroke (they read a
	 * FILL, not this) and the marker dot is a filled disc, so — matching "the same
	 * set, no more no less" — neither joins the colour knob. */
	stroke: var(--stibok-diagram-stroke-color, currentColor);
}
.stibok-diagram__center {
	position: absolute;
	inset-inline-start: 50%;
	inset-block-start: 50%;
	translate: -50% -50%;
	text-align: center;
	margin: 0;
	max-inline-size: 16rem;
}
.stibok-diagram__center-icon {
	width: 2em;
	height: 2em;
}
/* Browser-verified bug fix (INF.5, found building this increment's own demo — see
 * docs/demos/inf.5.md): every real `<li>`/`<ol>` child of `.stibok-diagram__items` is
 * `position:absolute` (below), so this list contributes ZERO height to normal flow —
 * `position:relative` alone left it a ZERO-HEIGHT containing block, so each item's
 * `inset-block-start:50%` resolved to 0 (the TOP of a 0-height box) instead of the
 * diagram's real vertical center, silently shifting every ring/cycle upward by exactly
 * half the diagram's own `min-height` (present since INF.2 — items and the wrapper's
 * OWN `min-height` never actually shared a coordinate space; measured via Playwright
 * getBoundingClientRect, not visible at a casual screenshot glance because the
 * accumulated offset looked "roughly ring-shaped"). Fix: make `.items` an ABSOLUTE,
 * full-box overlay (position:absolute + a full inset + 100% size) — EXACTLY the same
 * technique `.stibok-diagram__scaffold`/`.stibok-diagram__center` already use to anchor
 * against `.stibok-diagram`'s real box — so `.item`'s 50% anchors now resolve against
 * the SAME real diagram box the scaffold ring does. `Render_Helpers::container_css()`'s
 * collapse rule resets `inset-inline-start`/`inset-block-start`/`inline-size`/
 * `block-size` back to `auto` alongside `position:static` (unchanged grid behavior).
 */
.stibok-diagram__items {
	position: absolute;
	inset-inline-start: 0;
	inset-block-start: 0;
	inline-size: 100%;
	block-size: 100%;
	list-style: none;
	margin: 0;
	padding: 0;
}
/* ── Specificity ladder for `.stibok-diagram__item` — READ BEFORE touching ANY
 * rule that writes this element's `position` / `translate` / `inset-*`. Four
 * reviews have now hit this exact seam — INF.4's collapse, INF.5's canvas-parity
 * HIGH, INF.6's motion HIGH, and INF.9's hotspots mode (which DID add a 5th
 * writer — see below, it is a documented, deliberate EXCEPTION to layer 4's rule,
 * not a violation of it) — plus INF.10a's review fix, which found layer 4 was
 * resetting `position`/`translate` but NOT the base rule's `inset-inline-start:
 * 50%;inset-block-start:50%` (stibok.css), so a collapsed item still carried a
 * stale 50% OFFSET once `position:relative` changed what that percentage means
 * (CSS2.1 §9.4.3) — see layer 4's own entry below. Insets are now a first-class
 * part of this ladder, not an afterthought — read the whole ladder before adding
 * a 6th writer:
 *
 *   1. BASE ring/cycle position (this rule, just below)                    — (0,1,0)
 *   2. `.stibok-chrome` canvas-parity compound (this rule, INF.5)          — (0,2,0)
 *   3. INF.6 motion composition (`.stibok-motion[data-stibok-reveal]`, §"draw-on +
 *      sequential item reveal" below)                                     — (0,3,0)
 *      …`.is-revealed` variant                                            — (0,4,0)
 *   4. `Render_Helpers::container_css()`'s collapse-to-list reset (render-
 *      helpers.php) — repeats EVERY compound above (plain, `.stibok-chrome`, both
 *      motion variants, and `.stibok-chrome` × motion) at MATCHING specificity, so
 *      it wins each tie by SOURCE ORDER (always injected AFTER this file), never by
 *      out-specifying them. Resets `position:relative;translate:none` AND (as of
 *      INF.10a — see that fix's own comment on this rule in render-helpers.php)
 *      `inset-inline-start:auto;inset-block-start:auto`, so the base rule's `50%`
 *      insets (layer 1) don't survive the flip to `position:relative` as a stale
 *      offset. A future writer that adds a NEW inset (or changes what the base
 *      rule's insets mean) must extend THIS reset too, not just the
 *      `position`/`translate` pair.
 *   5. INF.9's `.stibok-diagram.is-mode-hotspots .stibok-diagram__item…` compound
 *      set (§"Diagram — hotspots mode" below, ~line 3777) — the SAME 6 chrome/
 *      motion combinations layers 1–4 already enumerate (plain, `.stibok-chrome`,
 *      motion, motion`.is-revealed`, chrome×motion, chrome×motion`.is-revealed`),
 *      each with TWO extra ancestor classes (`.stibok-diagram`+`.is-mode-hotspots`)
 *      prefixed on — (0,3,0) through (0,7,0). Two classes higher than its
 *      non-prefixed counterpart at every other layer (including layer 4's
 *      matching-specificity reset, which this hotspots set has NO equivalent of),
 *      so it wins ALL of them purely by specificity, before source order is ever
 *      consulted. This is a DOCUMENTED EXCEPTION to the layer-4 convention:
 *      hotspots wins by OUT-SPECIFYING every writer above (both wide AND narrow —
 *      no collapse-time mirror in `container_css()` at all), not by mirroring them
 *      at matching specificity + relying on source order. Why this is safe: hotspots
 *      sets `position:static` (not `relative`), and `static` un-absolutes the `<li>`
 *      at least as thoroughly as every OTHER mode's collapse reset does with
 *      `relative` — there is no weaker state for a later, lower-specificity rule to
 *      fail to reach, so the usual "does the reset still win the tie" hazard this
 *      ladder exists to catch does not apply here. The one thing a static ancestor
 *      changes — the numbered step-badge `::before`'s containing block — is handled
 *      by retargeting the badge to the CALLOUT's own element instead of the `<li>`
 *      (verified: INF.5's HIGH, "badge stacks at one corner instead of floating over
 *      its own card," does NOT recur here — see the hotspots section's own comment
 *      for the measurement).
 *
 * Rule for the NEXT (6th) writer of `position` / `translate` / `inset-*` on this
 * element: either (a) mirror it on BOTH sides — the new compound selector HERE,
 * and its matching-specificity twin in `container_css()` (covering every
 * property it writes, not just `position`/`translate` — see layer 4's own INF.10a
 * note above for why insets count too), relying on source order like layers 1–4
 * — or (b) out-specify deliberately, like layer 5, AND prove two things before
 * shipping: the narrow-stop collapse still reflows correctly (measured with a
 * real bounding-box, not just `getComputedStyle()` — see layer 4's INF.10a note
 * for why an inspected-property-only check can miss a stale inset), and whatever
 * anchors the numbered step-badge `::before` still resolves against real content
 * (not a stacked/empty box) — AND update this comment either way. Skipping
 * either the mirror or the proof is exactly how INF.6 broke (the two new motion
 * compounds out-ranked the collapse reset's OLD 2-selector list by accident,
 * un-mirrored and unproven, so a staggered item kept its ring `translate` past
 * the collapse stop instead of reflowing into the list) — and how INF.10a's own
 * inset gap slipped past three prior reviews (mirrored on `position`/`translate`
 * only, never audited against the base rule's OWN inset writes).
 *
 * **INF.8 (`snake` mode) checked this ladder and added NOTHING to it.**
 * `Render_Helpers::snake_positions()` emits its rem offsets into the SAME
 * `--dx`/`--dy` custom properties every other mode already writes — snake is a
 * 4th CONSUMER of this existing rule, not a new WRITER. See the
 * "Diagram — snake mode" section below for the one (non-position) rule it does
 * add (`.is-mode-snake .stibok-diagram__item{max-inline-size:…}`).
 *
 * **INF.10a (`funnel`/`pyramid` mode) also checked this ladder and added
 * NOTHING to it.** `Render_Helpers::diagram_band_positions()` emits its rem
 * offsets into the SAME `--dx`/`--dy` custom properties every other mode
 * already writes — a 5th CONSUMER, not a new writer. See the "Diagram —
 * funnel/pyramid mode" section below — its only new rule is
 * `.stibok-diagram__band{stroke:none}` (mirrors `.stibok-diagram__segment`),
 * not a `position`/`translate` writer, and no per-mode item-cap rule was added
 * (unlike `snake`'s uniform row width, a band's own width VARIES by index —
 * see the section's own comment).
 *
 * **INF.10b (`matrix` mode) IS the 6th writer — a genuine CSS-grid cell has no
 * `--dx`/`--dy` to consume at all.** `.stibok-diagram.is-mode-matrix
 * .stibok-diagram__item…` (§"Diagram — matrix mode" below) repeats the SAME 6
 * chrome/motion combinations layers 1–4 already enumerate, each with the SAME
 * TWO extra ancestor classes hotspots' own layer 5 uses (`.stibok-diagram` +
 * `.is-mode-matrix`) — so it wins every combination by SPECIFICITY, exactly like
 * layer 5, needing no wide/narrow distinction or `container_css()` mirror
 * either. Chose option (b) above (out-specify), not (a) — mirroring layer 5's
 * own reasoning: `position:relative` (not hotspots' `static`) un-absolutes the
 * item at least as thoroughly as every OTHER mode's collapse reset does, so the
 * usual "does the reset still win the tie below the stop" hazard this ladder
 * exists to catch does not apply (matrix's OWN narrow-stop CSS,
 * `Render_Helpers::container_css()`, only ever touches `grid-template-columns`/
 * `-rows` and the axis-label/quadrant-caption visibility — never `position`/
 * `translate`/`inset-*` again). The numbered step-badge `::before` needed NO
 * retargeting (unlike hotspots'): `position:relative` keeps the `<li>` itself a
 * valid positioned ancestor, so the UNMODIFIED base badge rule (below) just
 * works — verified with a live 4-quadrant numbered fixture (each badge floats
 * over its own cell, not stacked at one corner).
 */
/* Review HIGH finding (canvas-parity review): `.stibok-diagram__item` MUST win
 * `position:absolute` over `.stibok-chrome{position:relative}` (editor-canvas-
 * chrome.css, canvas-only) — both are otherwise 1-class selectors, so which one
 * wins is a LOAD-ORDER tie (editor-canvas-chrome.css loads after this file →
 * chrome used to win, silently un-ringing every diagram item in the canvas).
 * The prior fix used an INLINE style (editor.js), which unconditionally beats
 * EVERY stylesheet rule — including `Render_Helpers::container_css()`'s
 * collapse-to-list reset below the narrow `stibok` container stop, breaking the
 * INF.4 responsive collapse IN THE CANVAS ONLY (a D18 violation). Listing the
 * compound `.stibok-diagram__item.stibok-chrome` selector alongside the plain
 * one fixes that.
 *
 * ⚠ TWO DIFFERENT TIES — do not conflate them (re-review corrected an earlier,
 * wrong version of this comment that claimed the whole thing is specificity-safe):
 *   1. vs `.stibok-chrome{position:relative}` (chrome.css) — settled by
 *      SPECIFICITY: (0,2,0) > (0,1,0). Robust, order-independent. ✔
 *   2. vs `Render_Helpers::container_css()`'s collapse reset — that block emits
 *      the SAME compound selector, so it is (0,2,0) vs (0,2,0): a DEAD TIE
 *      settled by SOURCE ORDER (the later sheet wins; `@container` adds no
 *      specificity). The collapse reset wins today only because `container_css()`
 *      is injected AFTER this file. **Reordering the canvas stylesheets would
 *      silently reintroduce the D18 collapse bug** (proven in a headless cascade
 *      repro). This is the same load-order convention every OTHER `container_css()`
 *      collapse rule already relies on — including `.stibok-diagram__items`
 *      pre-INF.5 — so it is a documented structural convention, not a new
 *      fragility. If that convention is ever revisited, revisit this rule too.
 *
 * The compound selector is a no-op on the front (`.stibok-chrome` never reaches
 * production markup).
 */
.stibok-diagram__item,
.stibok-diagram__item.stibok-chrome {
	position: absolute;
	inset-inline-start: 50%;
	inset-block-start: 50%;
	/* Edge-anchor increment: `--anchor-x`/`--anchor-y` (render.php +
	   withDiagramItemCanvas, {@see Render_Helpers::diagram_label_anchor()}) move the
	   attachment point from the box CENTRE to the EDGE/CORNER nearest the hub for a
	   pushed-out (`is-place-outside`) NON-ICON node, so a leader tick meets the label's
	   own edge like a brochure callout. Emitted for that case ONLY — every other item
	   falls back to `-50%`, i.e. today's centre anchoring, byte-for-byte. */
	translate: calc(var(--anchor-x, -50%) + var(--dx)) calc(var(--anchor-y, -50%) + var(--dy));
	max-inline-size: 16rem;
	/* INF.5 editor-only bug fix (found browser-verifying this increment's own demo,
	 * docs/demos/inf.5.md): a `card`/`cluster`/`stack` child's OWN base rule sets
	 * `height:100%` (fill-the-grid-cell, the normal/correct behavior in a row/grid/
	 * stack context). On the FRONT that is harmless here — `.stibok-diagram__item` is a
	 * SEPARATE `<li>` wrapping the child, and the `<li>`'s own height is `auto`
	 * (percentage-height on the child then correctly falls back to auto, per CSS's
	 * indefinite-containing-block rule). In the EDITOR, `withDiagramItemCanvas`
	 * (editor.js) stamps the `stibok-diagram__item` class directly onto the CHILD's own
	 * wrapper (there is no separate `<li>` — WP's BlockListBlock decorates one element,
	 * not two) — so `.stibok-diagram__items`' new DEFINITE 100% block-size (the fix
	 * just above) suddenly gave that same `height:100%` a real number to resolve
	 * against, stretching every card to the diagram's full height. `block-size:auto`
	 * (a HIGHER-specificity-tie winner: same 1-class specificity as the child's own
	 * `.stibok-card{height:100%}`, but this rule is LATER in this file, and per the CSS
	 * Logical Properties cascade rule a later flow-relative declaration wins a tie) is a
	 * no-op on the front (the `<li>` never had `height:100%` to begin with) and fixes
	 * the editor-only stretch. */
	block-size: auto;
	/* Fable F15 (INF.4): crossing the collapse breakpoint changes `translate` (this
	   ring offset → `none`, Render_Helpers::container_css()) AND `position` (absolute
	   → relative — NOT `static`; see the collapse rule's own comment for why: the
	   scaffold.numbers ::before badge needs the item to stay a positioned ancestor)
	   at the SAME stop. `position` cannot transition (a discrete property), so it
	   flips immediately — but that is what MAKES the morph work: the item lands in
	   its new grid slot instantly while still carrying its old ring `translate`
	   offset, and this transition eases that leftover offset down to `none` over
	   .4s, reading as the ring sliding into the list instead of snapping.
	   `--sm-loop-*`/`.stibok-motion` audit: the composed loop `translate` (MV.3, line
	   ~627) lives on `.stibok-motion`, which today is NEVER the SAME element as
	   `.stibok-diagram__item` — motion is applied either to the diagram's OWN root
	   (`.stibok-diagram.stibok-motion`) or to the child block rendered INSIDE the
	   `<li>` (e.g. `.stibok-icon.stibok-motion`), never to the `<li>` wrapper itself.
	   So this `translate`/`transition` never shares a property with a composed
	   `--sm-loop-*` value on one element. INF.6 (sequential item reveal, not yet
	   shipped) must re-run this check before adding `.stibok-motion`/reveal classes
	   directly to `.stibok-diagram__item` — that WOULD put two `translate` writers on
	   one element and the later-in-source rule (whichever) would win, clobbering the
	   other. */
	transition: translate 0.4s ease;
}
@media (prefers-reduced-motion: reduce) {
	/* The compound selector must be repeated here too: the base rule above sets
	 * `transition` at 2-class specificity for a canvas (`.stibok-chrome`) item,
	 * which would otherwise beat this 1-class override regardless of source
	 * order, silently re-enabling the transition in the canvas under reduced
	 * motion. */
	.stibok-diagram__item,
	.stibok-diagram__item.stibok-chrome {
		transition: none;
	}
}

/* ── Diagram — item child intrinsic-width floor (post-CL2-merge batch, Item 4) ──
 * A `stack > card` diagram item (any mode) collapsed to a pathologically narrow
 * box (~52px @1280 / ~48px @375, measured — vs. ~147-198px for a bare `card >
 * heading` item with identical text). NOT a colour issue, NOT specific to
 * funnel/pyramid/cycle (reproduced on radial/hub/matrix too, non-deterministically
 * per item index) — a genuine browser intrinsic-sizing bug: `.stibok-stack` is a
 * column FLEX container (stibok.css, "Layout primitives"), and a flex container's
 * min-content/fit-content width contribution for TEXT descendants honours
 * `overflow-wrap:anywhere` (`.stibok-heading` sets it, "Overflow guard" section
 * above) far more aggressively than plain block layout does — nesting a SECOND
 * flex layer (`stack`) between the item and its `card` (itself `display:block`,
 * so `card > heading` alone never hits this) lets that automatic-minimum-size
 * collapse the whole subtree toward zero, leaving only the card's own padding as
 * the rendered width. Confirmed content-independent: every collapsed instance
 * measured the SAME width regardless of the item's own text.
 *
 * Two DIFFERENT existing mechanisms compute this element's width and both hit the
 * bug, so both need a floor:
 *   1. WIDE state — `.stibok-diagram__item`'s own base rule (above) leaves it
 *      `position:absolute` with no `inline-size` (only `max-inline-size:16rem`),
 *      so the ITEM sizes via CSS shrink-to-fit — which recurses into the child
 *      stack's own (buggy) intrinsic width.
 *   2. NARROW/collapsed state — `Render_Helpers::container_css()`'s O-3 rule
 *      (`.stibok-diagram:not(.is-mode-hotspots):not(.is-mode-matrix)
 *      .stibok-diagram__items:not(.is-numbered) > .stibok-diagram__item > *
 *      {inline-size:fit-content;…}`) deliberately shrink-wraps the child to
 *      center it under `text-align:center` — `fit-content` is ALSO a min/max-
 *      content-based algorithm, so it hits the identical collapse.
 *
 * Fix: a `min-inline-size` FLOOR on the item's direct child, not a fixed
 * `inline-size` — `min-width` is applied by the UA as a hard constraint AFTER
 * whatever algorithm computes the tentative width (shrink-to-fit above, or
 * `fit-content` in O-3's rule), so it raises the pathological near-zero result
 * without fighting either mechanism's own intent: the wide-state ring/funnel/
 * pyramid/matrix item still sizes itself (now correctly) and the narrow-state
 * O-3 rule still shrink-wraps + centers non-buggy content (an icon+label
 * cluster already renders comfortably above this floor) exactly as before. A
 * fixed `inline-size` here would have fought #2 outright (O-3's `fit-content`
 * would simply keep re-winning at matching-or-higher specificity) and given
 * every mode one uniform box width, losing the "shrink to fit an icon+short-
 * label" narrow-state look O-3 was written for.
 *
 * NOT a 3rd/8th writer of `position`/`translate`/`inset-*` (a different
 * property, on a different element — the CHILD, not `.stibok-diagram__item`
 * itself) — the specificity-ladder comment above is unaffected, same reasoning
 * the tangential-label `rotate` rule below already documents for itself.
 *
 * `hotspots` is excluded: its callout children already carry their OWN
 * definite `inline-size` (`min(14rem, calc(100% - 2rem))`, "Diagram — hotspots
 * mode" section below) at higher specificity (3 ancestor classes vs. this
 * rule's 2), so a floor here would be a redundant no-op for it at best — kept
 * out entirely to avoid any interaction with its collapsed-grid `auto!important`
 * reset (render-helpers.php). `matrix` is NOT excluded: its item is
 * `display:flex;align-items:center` (own section below), so an unwrapped
 * `stack > card` child hits the SAME shrink-to-fit-adjacent collapse there too
 * (measured) — the floor fixes it the same way, centered in the quadrant cell
 * exactly as `align-items:center` already intends.
 *
 * 8rem is comfortably under every mode's own item cap (16rem ring default,
 * 14rem hotspots, 12rem snake) so it cannot push a child wider than its `<li>`
 * wants to be, and comfortably under the available single-column width once
 * collapsed (verified @375).
 *
 * `:not(.stibok-visually-hidden)` — found by the e2e sweep (`diagram-matrix.
 * spec.mjs`, "narrow width, NO axisLabels authored: quadrant caption stays
 * sr-only"): matrix's un-authored `.stibok-diagram__quadrant-label` is an
 * ORDINARY direct child of `.stibok-diagram__item` that uses the project's
 * standard sr-only technique (`width:1px` + `clip:rect(0,0,0,0)`, "Position
 * trait" section above) to stay invisible-but-announced until `axisLabels` is
 * authored. `min-width` unconditionally wins over a smaller explicit `width`
 * (CSS2.1 §10.4), so the floor was un-hiding it (measured 128px, no longer
 * clipped) — a real a11y regression this exclusion closes structurally: NO
 * diagram-item child that opts into `.stibok-visually-hidden` should ever get
 * a width floor, regardless of mode. */
.stibok-diagram:not(.is-mode-hotspots) .stibok-diagram__item > *:not(.stibok-visually-hidden) {
	min-inline-size: 8rem;
}

/* ── Diagram — tangential labels (Phase INF-Polish P.1 item 5) ───────────────
 * Rotates a ring-mode item's own content along the ring using the per-item
 * `--label-rotate` (render.php, {@see Render_Helpers::diagram_label_rotate_deg()})
 * — already flipped past `±90°` so text never reads upside-down on the ring's
 * lower half. `rotate` is an INDEPENDENT individual-transform property from the
 * `translate` the base rule above already writes (both compose in the fixed
 * translate→rotate→scale order regardless of source order, CSS Transforms
 * Level 2) — this is a NEW property on `.stibok-diagram__item`, not a 7th writer
 * of position/translate/inset, so the specificity-ladder comment above is
 * unaffected. Scoped to radial/cycle/hub — the three modes whose `angle` is
 * real ring geometry (snake/funnel/pyramid/matrix always emit `angle:0deg`, a
 * harmless placeholder, Fable F6 — rotating by it would be visually wrong, so
 * this never reaches those modes via the mode-class gate).
 *
 * Excludes an ICON child (`:not(:has(.stibok-icon))`): item 1's opaque chip is
 * a circle (rotation-invariant), but the GLYPH inside it is not — tilting an
 * icon sideways would look broken, so icon nodes stay upright and only
 * text/card/cluster captions get the tangential treatment.
 *
 * Edge-anchor increment: this used to be UNCONDITIONAL for the three ring modes.
 * It is now OPT-IN behind the root `.is-label-rotate` class that
 * `scaffold.labelRotate:true` stamps (omit-default false) — tangential text is a
 * strong stylistic statement, not a sane always-on default, and horizontal
 * captions are the readable baseline. `--label-rotate` is still emitted per item
 * unconditionally (the "harmless unused var" convention); only this CONSUMER is
 * gated, so turning the switch on needs no re-render of the item vars.
 */
.stibok-diagram.is-mode-radial.is-label-rotate .stibok-diagram__item:not(:has(.stibok-icon)),
.stibok-diagram.is-mode-cycle.is-label-rotate .stibok-diagram__item:not(:has(.stibok-icon)),
.stibok-diagram.is-mode-hub.is-label-rotate .stibok-diagram__item:not(:has(.stibok-icon)) {
	/* Phase INF-Polish O-1 (owner decision 2026-07-24: COMPOSE) — the tangential
	 * label rotation must ADD to the motion engine's rotation, not out-specify and
	 * silently discard it. `.stibok-diagram__item` ∈ STAGGER_CONTAINERS' children,
	 * so it can carry `.stibok-motion`, whose composed rule writes
	 * `rotate: calc(--sm-rotate + --smh-rotate + --sm-loop-rotate + --stibok-decor-
	 * rotate)`. This rule sits on the SAME element at higher specificity; a plain
	 * `rotate: var(--label-rotate)` would win outright and drop every motion slot —
	 * the exact silent-no-op class of bug MV.4/decor-hover already bit this project
	 * with (the owner's own "slow ring rotation" WOW item would never fire, with no
	 * error to explain why). Fold ALL five writers into one `calc()` so an
	 * author-enabled rotation composes with the tangential label instead of being
	 * discarded. Accepted trade-off: while a rotation runs, the angles add, so the
	 * label is no longer strictly tangential — visible and explicable, unlike
	 * nothing happening. Mirrored in the canvas by the SAME shared stylesheet (the
	 * editor stamps `--label-rotate`; the motion slots are 0 there). P.4 turns ring
	 * rotation ON in the showcase to show it live. */
	rotate: calc(
		var(--label-rotate, 0deg) + var(--sm-rotate, 0deg) + var(--smh-rotate, 0deg)
		+ var(--sm-loop-rotate, 0deg) + var(--stibok-decor-rotate, 0deg)
	);
}

/* ── Diagram — readable ring rotation (Phase INF-Polish P.4 MED-2) ────────────
 * A `motion.loop{rotate-loop}` is applied by the generic motion trait to the diagram
 * ROOT (`.stibok-diagram.stibok-motion`), so the whole wrapper — scaffold ring AND the
 * center label AND every item — turns as a rigid body: the center word makes a full
 * turn and item captions read upside-down at 180°. The owner-delegated fix (readability
 * wins) keeps the wrapper turning — so the ring/spokes/segments and the node POSITIONS
 * still orbit, and each leader tick stays glued to its node (they rotate together) — but
 * COUNTER-rotates the center label and each node's own content by the SAME animation
 * reversed, so all text stays perfectly upright and legible at every instant (an
 * "orbiting nodes, upright labels" carousel, not spinning text).
 *
 * Why this is exact, not approximate: the wrapper's `rotate-loop` runs 0deg→360deg at
 * `--sm-kf-duration` / `--sm-kf-easing` (both linear here, both cascading to these
 * descendants); this counter-spin runs 0deg→-360deg with the SAME duration + easing +
 * (page-load) start + `infinite` iteration, so at every frame the two are exact
 * negatives → net content orientation is 0deg. It targets the node's CHILD
 * (`.stibok-diagram__item > *`), never the `<li>` itself, so the P.4 item `rotate`
 * composition (`--label-rotate` + the motion slots, just above) and the O-1 compose
 * contract are untouched — the counter-spin is a separate transform on a separate box.
 * Keyed to the `rotate-loop` binding (the ONE rotation loop; a `float`/`pulse` loop
 * must NOT counter-rotate) — the same slug-scoped-binding pattern marquee uses.
 *
 * Reduced motion: this whole rule sits under `no-preference`, AND the wrapper's own
 * `@keyframes` binding is `no-preference`-gated in the registry — so under `reduce`
 * BOTH stop and everything rests static + upright (never a half-countered tilt). With
 * the loop OFF the nodes sit exactly where P.4 places them (this adds no rest transform).
 */
@keyframes stibok-diagram-counterspin { from { rotate: 0deg; } to { rotate: -360deg; } }
@media (prefers-reduced-motion: no-preference) {
	.stibok-diagram.stibok-motion[data-stibok-kf="rotate-loop"] > .stibok-diagram__center,
	.stibok-diagram.stibok-motion[data-stibok-kf="rotate-loop"] .stibok-diagram__item > * {
		animation: stibok-diagram-counterspin var(--sm-kf-duration, 600ms) var(--sm-kf-easing, linear) infinite;
	}
}

/* ── Diagram — draw-on + sequential item reveal (Phase INF, INF.6) ──────────
 * D-INF6: draw-on is a STATIC `transition` on `stroke-dashoffset` — NOT a
 * `@keyframes` registry entry (no `Css_Sanitizer::KEYFRAME_PROPERTY_ALLOWLIST`
 * change). `pathLength="100"` (render.php) normalizes every scaffold stroke to a
 * 0–100 dash space so the offset math is deterministic regardless of the real
 * path length. The trigger is the EXISTING `data-stibok-reveal` IntersectionObserver
 * (interactions.js) — it adds `.is-revealed` on scroll, and IMMEDIATELY under
 * reduced motion / no IntersectionObserver, so the ring is ALWAYS the drawn state
 * with JS off (R6-style proof: SSR always renders the full path; only the
 * transition's start point is `stroke-dashoffset:100`, never the DOM/markup). Only
 * `.stibok-diagram__ring` (a genuine stroke, `fill="none"`) carries this class today
 * — `cycle`'s segments are FILLED wedges (Fable F8), unaffected by dash properties.
 */
.stibok-diagram.is-draw .stibok-diagram__stroke {
	stroke-dasharray: 100;
	stroke-dashoffset: 100;
	transition: stroke-dashoffset 0.9s ease;
}
.stibok-diagram.is-draw.is-revealed .stibok-diagram__stroke {
	stroke-dashoffset: 0;
}
@media (prefers-reduced-motion: reduce) {
	.stibok-diagram.is-draw .stibok-diagram__stroke {
		transition: none;
	}
}

/* Fable F13 (draw-then-populate choreography, ~zero extra cost): draw-on and the
 * sequential item reveal (below) ship as two INDEPENDENT triggers (both opt in via
 * their own `data-stibok-reveal`, both fire off the SAME scroll intersection) — left
 * alone they animate CONCURRENTLY. Sequence them instead: the ring draws (0–0.9s,
 * above), then items stagger in `0.9s + var(--i)·120ms` apart (F6's already-emitted
 * per-item index — zero extra markup), then the center (if present) settles LAST,
 * `0.9s + var(--n)·120ms` (strictly after every item's own delay). This OVERRIDES the
 * generic `--sm-reveal-delay` formula (`--sm-stagger * --si`, §4a) ONLY while
 * `.is-draw` is active — deliberately a FIXED cadence locked to the 0.9s draw
 * duration, not the author's configured `--sm-stagger` step, because it must land
 * exactly when the ring finishes. Without `.is-draw` (no draw-on), the generic
 * per-`--sm-stagger` cascade is untouched. One rule each, reusing vars already on the
 * element — no new markup beyond render.php's `--n`-on-center (INF.2 already emits
 * `--i`/`--n` per item, F6).
 */
.stibok-diagram.is-draw .stibok-diagram__item.stibok-motion[data-stibok-reveal] {
	--sm-reveal-delay: calc(0.9s + var(--i, 0) * 120ms);
}
.stibok-diagram.is-draw .stibok-diagram__center.stibok-motion[data-stibok-reveal] {
	--sm-reveal-delay: calc(0.9s + var(--n, 0) * 120ms);
}

/* INF.6 gotcha, flagged since INF.4/5 (see the F15 comment above): `.stibok-diagram__item`
 * already OWNS `translate` for its own ring/cycle POSITION (`--dx`/`--dy`); `.stibok-motion`
 * (MV.2) ALSO owns `translate` for the composed entrance/hover/parallax/loop OFFSET.
 * Marking an item `.stibok-motion` (Render_Helpers::stagger_children(), 'diagram' joining
 * STAGGER_CONTAINERS) puts TWO writers on one property on the SAME element — without this,
 * whichever rule is later in source wins OUTRIGHT, either freezing every item at the
 * diagram's center (motion wins) or silently dropping the entrance's offset entirely
 * (position wins). Compose them instead — exactly like `.stibok-motion`'s own internal
 * layering (MV.2/MV.3) and MV.4's `--stibok-decor-rotate` fold-in: a staggered item's
 * `translate` is its OWN position PLUS the motion composition. `.stibok-diagram__center`
 * gets the SAME treatment (F13's "center last") using its own `-50% -50%` anchor instead of
 * `--dx`/`--dy`. Both compound selectors are MORE specific than the base item/center rules
 * AND the generic `.is-revealed` settle rule (below), so specificity — not source order —
 * keeps the item/center position intact through every reveal phase; no separate reduced-
 * motion override is needed (the settled formula already reads only the parallax/hover/loop
 * slots, each 0 under reduced motion, resolving to plain `-50%+dx/dy`).
 *
 * ⚠ That same higher specificity ((0,3,0)/(0,4,0) for the item — see the "specificity
 * ladder" comment on the base rule above) ALSO out-ranked `Render_Helpers::container_css()`'s
 * INF.4 collapse-to-list reset, which only listed the plain/`.stibok-chrome` selectors
 * (0,1,0)/(0,2,0) — a staggered item below the narrow stop kept this `translate` instead of
 * collapsing to the static grid (INF.6 review HIGH, "H1"). Fixed on the OTHER side of the
 * tie: the collapse reset now repeats these SAME compound selectors (render-helpers.php)
 * so it wins back by the established later-source-wins convention, not by out-specifying
 * this rule. The center's OWN motion compounds below ((0,2,1)/(0,3,1)) have the identical
 * shape and hit the identical hazard: P.2 promoted the center into normal flow instead of
 * hiding it (`display:none` no longer applies — see the P.2 item 4 comment on
 * `.stibok-diagram__scaffold`/`.stibok-diagram__center` in `container_css()`), so its
 * `translate` is very much NOT moot below the stop — a P.2 fix-pass review HIGH (F1) caught
 * the reset still only listing the plain selector, out-ranked by these compounds exactly
 * like the item was pre-fix. `container_css()` now repeats the SAME two motion compounds
 * for the center, same convention as the item.
 */
.stibok-diagram__item.stibok-motion[data-stibok-reveal] {
	/* The `--anchor-*` fallbacks repeat the base rule's edge-anchor contract — these
	   compounds REPLACE that `translate` outright, so a pushed-out label would snap
	   back to centre-anchored the moment it is staggered without them. */
	translate:
		calc(var(--anchor-x, -50%) + var(--dx) + var(--sm-ex, 0px) + var(--sm-px, 0px) + var(--smh-x, 0px) + var(--sm-loop-x, 0px))
		calc(var(--anchor-y, -50%) + var(--dy) + var(--sm-ey, 0px) + var(--sm-py, 0px) + var(--smh-y, 0px) + var(--sm-loop-y, 0px));
}
.stibok-diagram__item.stibok-motion[data-stibok-reveal].is-revealed {
	translate:
		calc(var(--anchor-x, -50%) + var(--dx) + var(--sm-px, 0px) + var(--smh-x, 0px) + var(--sm-loop-x, 0px))
		calc(var(--anchor-y, -50%) + var(--dy) + var(--sm-py, 0px) + var(--smh-y, 0px) + var(--sm-loop-y, 0px));
}
.stibok-diagram__center.stibok-motion[data-stibok-reveal] {
	translate:
		calc(-50% + var(--sm-ex, 0px) + var(--sm-px, 0px) + var(--smh-x, 0px) + var(--sm-loop-x, 0px))
		calc(-50% + var(--sm-ey, 0px) + var(--sm-py, 0px) + var(--smh-y, 0px) + var(--sm-loop-y, 0px));
}
.stibok-diagram__center.stibok-motion[data-stibok-reveal].is-revealed {
	translate:
		calc(-50% + var(--sm-px, 0px) + var(--smh-x, 0px) + var(--sm-loop-x, 0px))
		calc(-50% + var(--sm-py, 0px) + var(--smh-y, 0px) + var(--sm-loop-y, 0px));
}

/* ── Diagram — sequential segment/band draw (Phase INF-Polish P.1 item 3) ────
 * `scaffold.draw:true` (D-INF6) always animated the RING (a genuine stroke) via
 * `stroke-dashoffset`, above — but `cycle`'s segments and `funnel`/`pyramid`'s
 * bands are FILLED wedges (Fable F8), which a dash property cannot touch: the
 * wrapper carried `.is-draw`/`data-stibok-reveal` harmlessly, with nothing to
 * animate. A real defect, not a missing feature — same opt-in, silently
 * inconsistent across modes. Fixed with the SAME reveal-observer trigger, an
 * OPACITY fade instead of a dashoffset sweep: segment/band `i` fades in
 * `220ms · i` apart (the per-shape `--i`, now ALSO emitted on the `<path>`
 * itself — render.php — not just the item `<li>`), then that shape's OWN item/
 * label follows one fixed beat later. Independent of whether the author ALSO
 * configured `motion.entrance.stagger` (F13, above) — this is an unconditional
 * part of `scaffold.draw` for these three modes, not a second opt-in; render.php's
 * `<noscript>` override forces both to their settled (fully opaque) state when
 * JS is genuinely absent, mirroring the ring/center's own no-JS fallback.
 */
.stibok-diagram.is-draw .stibok-diagram__segment,
.stibok-diagram.is-draw .stibok-diagram__band {
	opacity: 0;
	transition: opacity 0.5s ease;
	transition-delay: calc(var(--i, 0) * 220ms);
}
.stibok-diagram.is-draw.is-revealed .stibok-diagram__segment,
.stibok-diagram.is-draw.is-revealed .stibok-diagram__band {
	opacity: 1;
}
.stibok-diagram.is-draw.is-mode-cycle .stibok-diagram__item,
.stibok-diagram.is-draw.is-mode-funnel .stibok-diagram__item,
.stibok-diagram.is-draw.is-mode-pyramid .stibok-diagram__item {
	opacity: 0;
	transition: opacity 0.5s ease;
	transition-delay: calc(220ms + var(--i, 0) * 220ms);
}
.stibok-diagram.is-draw.is-revealed.is-mode-cycle .stibok-diagram__item,
.stibok-diagram.is-draw.is-revealed.is-mode-funnel .stibok-diagram__item,
.stibok-diagram.is-draw.is-revealed.is-mode-pyramid .stibok-diagram__item {
	opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
	.stibok-diagram.is-draw .stibok-diagram__segment,
	.stibok-diagram.is-draw .stibok-diagram__band,
	.stibok-diagram.is-draw.is-mode-cycle .stibok-diagram__item,
	.stibok-diagram.is-draw.is-mode-funnel .stibok-diagram__item,
	.stibok-diagram.is-draw.is-mode-pyramid .stibok-diagram__item {
		transition: none;
	}
}

/* ── Diagram — cycle mode (Phase INF, INF.3) ─────────────────────────────────
 * A segmented process ring: item positioning is IDENTICAL to radial (same
 * .stibok-diagram__item rule above); only the scaffold differs — N token-colored
 * .stibok-diagram__segment <path>s (Render_Helpers::diagram_cycle_segment_paths())
 * replace the plain ring circle. Segment fills are set as --seg-<i> custom
 * properties on THIS element (an ancestor of the <svg>) by render.php's per-item
 * fill cascade (Fable F8); the <path>'s own fill="var(--seg-<i>,currentColor)"
 * just reads them — no color decision lives in CSS.
 */
.stibok-diagram__segment {
	stroke: none;
}

/* Phase INF-Polish P.2 item 1 (review E2) — browser-verified fix (found rendering
 * this increment's own demo, docs/demos/inf-polish.p2.md): render.php stages this
 * item's own on-colour as `color` on the `<li>` — an INHERITED property. But
 * `theme/theme.json` styles `h1,h2,h3,h4,h5,h6{color:var(--wp--preset--color--
 * contrast)}` DIRECTLY on the heading tag (global-styles, a real declaration —
 * even a low-specificity one still beats mere inheritance, CSS's cascade only
 * falls back to "inherited" when NO rule targets the element at all). A
 * `heading` child (the funnel/pyramid/matrix demo's own label pattern) therefore
 * NEVER actually picked up the on-colour — measured: the SAME near-black ink this
 * whole item exists to fix, reading ~2.9:1 against an `accent` (#4f46e5) band.
 *
 * P.2 fix-pass review HIGH (F2): reads `var(--stibok-on-surface, inherit)` now,
 * not a bare `color: inherit` — the inherited CUSTOM PROPERTY chain (docs/blocks/
 * diagram.md T14's "background + foreground are a pair" convention; see the main
 * per-item loop's own docblock in render.php), not whatever `color` happens to
 * resolve to on the immediate parent. This is what makes `item > stack > card >
 * heading` (an ordinary nesting the OLD direct-child-only guard could not see)
 * resolve correctly: `.stibok-card`'s own rule below re-declares `--stibok-on-
 * surface` for its subtree, so THIS heading — however many plain (non-surface)
 * wrappers sit between it and the card — reads the CARD's on-colour, not the
 * diagram item's. `:not(:has(> .stibok-card))` is REMOVED from the matrix
 * selector below (the now-unnecessary direct-child guard the fix-pass replaces —
 * a direct card child is protected by the SAME card rule, no special case). */
.stibok-diagram.is-mode-cycle .stibok-diagram__item :is(h1, h2, h3, h4, h5, h6),
.stibok-diagram.is-mode-funnel .stibok-diagram__item :is(h1, h2, h3, h4, h5, h6),
.stibok-diagram.is-mode-pyramid .stibok-diagram__item :is(h1, h2, h3, h4, h5, h6),
.stibok-diagram.is-mode-matrix .stibok-diagram__item :is(h1, h2, h3, h4, h5, h6) {
	color: var(--stibok-on-surface, inherit);
}

/* P.2 fix-pass review HIGH (F1) — the on-colour MAPPING, split out of the inline
 * style so a container query can reach it.
 *
 * `render.php` stages only the neutral per-index CARRIER `--on-seg-active` inline
 * (the value genuinely has to be inline — it varies per item index). THIS rule is
 * the mapping: carrier → the inherited `--stibok-on-surface` every downstream
 * consumer already reads (the nested-card/toned re-declarations below, the heading
 * override above, the promoted matrix quadrant caption in
 * `Render_Helpers::container_css()`) → the item's own `color`.
 *
 * Why it had to move out of the inline style: for `cycle`/`funnel`/`pyramid` the
 * PAIRED background is an SVG `<path fill>` inside `.stibok-diagram__scaffold`, and
 * the narrow collapse `display:none`s that whole scaffold. Below the stop the fill
 * was gone but the inline on-colour survived — measured on the showcase at 390px:
 * six funnel/pyramid labels at `rgb(255,255,255)` on `rgb(255,255,255)`, 1.00:1,
 * literally invisible. An INLINE `color` cannot be reset by any `@container` rule
 * short of `!important`; a static rule can, and `container_css()` does exactly that
 * for those three modes (see its own comment). No `!important` anywhere.
 *
 * `matrix` is listed here and deliberately NOT reset on collapse: it paints
 * `background-color` on the `<li>` itself (`render.php`), so its pairing is just as
 * true at 390px as at 1280px.
 *
 * Scoped to the four carrier-staging modes only — `radial`/`hub`/`snake` have no
 * segment fill and are left to plain inheritance exactly as before (no new `color`
 * writer on their items). The `var(…, var(--wp--custom--semantic--on-surface))`
 * fallback covers an item with no fill of its own (more items than staged
 * segments), which previously got no declaration at all.
 *
 * Specificity (0,3,0) — the SAME as the collapse-state reset in `container_css()`,
 * which is injected AFTER this file and therefore wins the tie by the load-order
 * convention every other diagram collapse rule here already relies on (see the
 * `.stibok-diagram__item` specificity-ladder comment above). */
.stibok-diagram.is-mode-cycle .stibok-diagram__item,
.stibok-diagram.is-mode-funnel .stibok-diagram__item,
.stibok-diagram.is-mode-pyramid .stibok-diagram__item,
.stibok-diagram.is-mode-matrix .stibok-diagram__item {
	--stibok-on-surface: var(--on-seg-active, var(--wp--custom--semantic--on-surface));
	color: var(--stibok-on-surface);
}

/* Fable F10: an auto step-numbering badge — works in every mode, not just cycle. */
.stibok-diagram__items.is-numbered {
	counter-reset: stibok-diagram-step;
	/* P.2 fix-pass review MED (F3): the badge's own geometry, named ONCE here so
	 * every consumer derives from it instead of repeating a literal. Two consumers
	 * today: this rule's own `::before` box (which uses the inset NEGATED — the
	 * badge deliberately straddles the item's corner in the wide state), and
	 * `Render_Helpers::container_css()`'s collapsed state, which flips the inset
	 * positive AND reserves inline room for the badge on the item so content can
	 * never sit underneath it. Scoped to `.is-numbered` (not the `.stibok-diagram`
	 * skin-token block above) — these are internal layout constants, not a skin
	 * surface, and nothing should advertise them. */
	--stibok-diagram-badge-size: 1.75rem;
	--stibok-diagram-badge-inset: 0.5rem;
}
.stibok-diagram__items.is-numbered > .stibok-diagram__item {
	counter-increment: stibok-diagram-step;
}
.stibok-diagram__items.is-numbered > .stibok-diagram__item::before {
	content: counter(stibok-diagram-step);
	position: absolute;
	inset-inline-start: calc(-1 * var(--stibok-diagram-badge-inset));
	inset-block-start: calc(-1 * var(--stibok-diagram-badge-inset));
	inline-size: var(--stibok-diagram-badge-size);
	block-size: var(--stibok-diagram-badge-size);
	display: grid;
	place-items: center;
	border-radius: 50%;
	background: var(--wp--preset--color--accent);
	color: var(--wp--preset--color--on-accent);
	font-size: 0.8125rem;
	font-weight: 600;
	line-height: 1;
	/* INF.5 editor-only bug fix (found browser-verifying this increment's own demo,
	 * docs/demos/inf.5.md): every stibok/* block wrapper in the canvas carries
	 * `.stibok-chrome` (CC.1, the hover type/name badge), whose OWN `::before` rule
	 * (editor-canvas-chrome.css) sets `opacity:0` by default (revealed only on hover,
	 * `.is-hovered::before{opacity:1}`). A `::before` is ONE pseudo-element per real
	 * element — `.stibok-diagram__item` IS `.stibok-chrome` for a diagram item (same
	 * element, not a separate wrapper) — so BOTH rules target the SAME `::before`:
	 * this rule wins content/position/background (higher specificity: 3 classes +
	 * child combinator beats chrome's 1 class), but never declared `opacity`, so
	 * chrome's `opacity:0` silently won that ONE property by default, making every
	 * step badge invisible until the block was hovered (front-only until now — the
	 * badges never actually rendered as a positioned ring in the canvas before this
	 * increment's geometry mirror, so the collision was never observable). Declaring
	 * `opacity:1` here is a no-op on the front (`.stibok-chrome` never reaches
	 * production markup) and restores the badge as an always-visible, non-hover-gated
	 * element in the editor — accepted tradeoff: hovering a NUMBERED diagram item no
	 * longer also shows the transient chrome type-name label (the persistent step
	 * badge is the more important signal there). */
	opacity: 1;
	/* Review LOW-5 (canvas-parity review): `opacity` was the only chrome `::before`
	 * property reset above — `.stibok-chrome::before` (editor-canvas-chrome.css)
	 * also sets `transform: translateY(0.1875rem)` (nudges ITS badge down to
	 * overlap the selection ring above the frame — a ~3px offset wrong for this
	 * circular step badge), `padding-block`/`padding-inline` (chrome's rectangular
	 * label padding, pushing this centered digit off-center inside the circle),
	 * `z-index: 30`, and `transition: opacity 120ms ease` (irrelevant once opacity
	 * is fixed at 1, but left implicit it would silently inherit chrome's value).
	 * All are no-ops on the front (`.stibok-chrome` never reaches production
	 * markup). */
	transform: none;
	padding-block: 0;
	padding-inline: 0;
	z-index: auto;
	pointer-events: none;
	transition: none;
}

/* ── Diagram — hub mode (Phase INF, INF.7) ───────────────────────────────────
 * Hub-and-spoke: item positioning is IDENTICAL to radial/cycle (the SAME
 * .stibok-diagram__item rule above — no NEW writer of position/translate on
 * that element, so the "specificity ladder" comment above is untouched); only
 * the scaffold differs — N token-stroked .stibok-diagram__connector <path>s
 * (Render_Helpers::diagram_hub_connector_paths()) replace the ring/segments.
 * Connectors are purely decorative (the whole scaffold <svg> is aria-hidden,
 * R4) and read the SAME frozen stroke-character vars the radial ring uses
 * (Fable F5) so a skin can restyle ring/segment/connector consistently with
 * zero new geometry. The collapse-to-list rule (Render_Helpers::container_css())
 * already `display:none`s the WHOLE `.stibok-diagram__scaffold` below the
 * narrow stop — connectors need no collapse rule of their own.
 */
.stibok-diagram__connector {
	stroke-width: var(--stibok-diagram-stroke-w);
	stroke-dasharray: var(--stibok-diagram-dash);
	stroke-linecap: var(--stibok-diagram-cap);
	/* P.3 (D-P3): same stroke-colour knob as the ring — covers hub spokes, the
	 * snake rail, and the hotspots leader INK line (all `.stibok-diagram__
	 * connector`). The hotspots halo under-line (`.is-halo`, below) keeps its own
	 * light stroke: its 2-class rule out-specifies this one, so the knob never
	 * bleeds the halo. `currentColor` fallback = unchanged when unset. */
	stroke: var(--stibok-diagram-stroke-color, currentColor);
}

/* ── Diagram — snake mode (Phase INF, INF.8) ─────────────────────────────────
 * A "boustrophedon" grid-of-rows sequence: item positioning REUSES the exact
 * SAME .stibok-diagram__item --dx/--dy rule above (Render_Helpers::
 * snake_positions() computes rem offsets in the SAME center-relative space
 * radial_positions() does) — this mode adds ZERO new writers of
 * position/translate on that element, so the "specificity ladder" comment
 * above is UNCHANGED (no new tie to settle on either side — extend it only
 * if a future mode adds a genuinely new writer). Only the scaffold differs —
 * a connecting "rail" made of N-1 token-stroked .stibok-diagram__connector
 * <path>s (Render_Helpers::diagram_snake_rail_paths()), the SAME class/rule
 * the hub spokes already use, just between consecutive items instead of
 * center→satellite. The collapse-to-list rule already `display:none`s the
 * WHOLE `.stibok-diagram__scaffold` below the narrow stop (same as hub) —
 * no new collapse rule either.
 *
 * The one genuinely NEW declaration: a narrower item cap, since up to
 * `scaffold.perRow` (2–6) items must share one row's width without
 * overlapping — the default 16rem cap (base .stibok-diagram__item rule)
 * is comfortably too wide for that. A cosmetic, local choice (noted at
 * INF.8 acceptance), NOT a position/translate writer.
 */
.stibok-diagram.is-mode-snake .stibok-diagram__item {
	max-inline-size: 12rem;
}

/* ── Diagram — funnel/pyramid mode (Phase INF, INF.10a) ──────────────────────
 * Fixed-geometry stacked bands (D1 — width is a pure function of a band's INDEX
 * among N, never a numeric value): item positioning is
 * Render_Helpers::diagram_band_positions() (a vertical band stack, not a ring) —
 * it still emits rem offsets in the SAME center-relative `--dx`/`--dy` space
 * `radial_positions()`/`snake_positions()` already use (D-INF9), so this mode
 * adds ZERO new writers of position/translate on `.stibok-diagram__item` — the
 * "specificity ladder" comment above is UNCHANGED (a 5th CONSUMER of the base
 * rule, not a new writer; mirrors INF.8's own "checked this ladder and added
 * nothing to it" note). Only the scaffold differs — N filled trapezoid
 * `.stibok-diagram__band` `<path>`s (Render_Helpers::diagram_band_paths()),
 * drawn directly in the SAME square `0 0 100 100` viewBox every ring mode uses
 * (Fable F1 — unlike `snake`, no non-square exception is needed here). Each
 * band's fill is the SAME `--seg-<i>` cascade `cycle` already established
 * (Fable F8) — `stroke:none` mirrors `.stibok-diagram__segment`'s own rule
 * (a filled shape, not a stroked line/connector).
 */
.stibok-diagram__band {
	stroke: none;
}

/* ── Diagram — hotspots mode (Phase INF, INF.9) ──────────────────────────────
 * Annotated-image callouts: a positioned overlay on an image, NOT a ring — the
 * SECOND mode (after `snake`) whose item positioning is not
 * `radial_positions()`/the shared `--dx`/`--dy` translate every ring mode uses.
 * Each callout positions ITSELF via its OWN, already-existing `position` trait
 * (widened to accept a numeric-percent anchor, D-INF8) — so `.stibok-diagram__item`
 * (the shared `<li>` wrapper) must NOT carry the base rule's ring translate here.
 * This is the "4th potential hit" the specificity-ladder comment above warns
 * about — extended (not gated silently): a NEW compound-selector set, one class
 * MORE specific than every existing writer in that ladder (three extra classes:
 * `.stibok-diagram.is-mode-hotspots .stibok-diagram__item…` vs the base rule's
 * one/two), so it wins by SPECIFICITY (not source order) regardless of where a
 * hotspots item also happens to carry `.stibok-chrome` (canvas) and/or
 * `.stibok-motion[data-stibok-reveal]` (an authored container stagger) — every
 * compound the ladder lists is repeated here so none of them can slip through.
 *
 * **Browser-verified bug fix (found building THIS increment's own demo, before
 * commit — see docs/demos/inf.9.md):** the FIRST version of this rule used
 * `position:relative`, reasoning (by wrong analogy with `container_css()`'s
 * collapse reset) that the numbered step-badge `::before` needs a positioned
 * ancestor. It does NOT need this element to be it — and `position:relative`
 * here is actively WRONG: a `position:relative` `<li>` becomes a POSITIONED
 * element, so it becomes the containing block for its OWN `position:absolute`
 * callout child, which then resolves its `inset-inline-start:X%` against this
 * tiny, content-collapsed `<li>` box instead of `.stibok-diagram`'s real box —
 * every callout clustered together near the list's own flow position instead of
 * landing on its own point (measured with `getBoundingClientRect()`: all three
 * callouts shared the identical `top`, only the `left` differed slightly).
 * `position:static` is the ONLY value that lets `position:absolute` correctly
 * skip past this element to find `.stibok-diagram` (the true containing block,
 * matching the front exactly — see the canvas-only `.items` override below for
 * why that skip lands on the SAME box either way). The step-badge is retargeted
 * to anchor on the CALLOUT'S OWN element instead (below), which is REAL,
 * correctly-positioned content in both the wide and collapsed states — so this
 * rule needs no wide/narrow distinction at all, unlike every other writer on
 * this ladder.
 */
.stibok-diagram.is-mode-hotspots .stibok-diagram__item,
.stibok-diagram.is-mode-hotspots .stibok-diagram__item.stibok-chrome,
.stibok-diagram.is-mode-hotspots .stibok-diagram__item.stibok-motion[data-stibok-reveal],
.stibok-diagram.is-mode-hotspots .stibok-diagram__item.stibok-motion[data-stibok-reveal].is-revealed,
.stibok-diagram.is-mode-hotspots .stibok-diagram__item.stibok-chrome.stibok-motion[data-stibok-reveal],
.stibok-diagram.is-mode-hotspots .stibok-diagram__item.stibok-chrome.stibok-motion[data-stibok-reveal].is-revealed {
	position: static;
	inset-inline-start: auto;
	inset-block-start: auto;
	translate: none;
}
/* Browser-verified bug fix, same demo pass as the `position:static` fix above:
 * `.stibok-card`/`.stibok-cluster`/`.stibok-stack`'s OWN base rule sets
 * `height:100%` (fill-the-grid-cell — correct in a row/grid/stack context).
 * Every OTHER diagram mode makes this harmless because the `<li>` itself sits
 * between the child and `.stibok-diagram` and has an AUTO height, so a
 * percentage height on the child (an indefinite containing block, per CSS)
 * falls back to its own content height. Hotspots removes that buffer on
 * purpose (the fix above) — the callout child's `position:absolute` now
 * resolves its OWN percentage height directly against `.stibok-diagram`'s
 * real, DEFINITE box height (the base image's own aspect-ratio) — so
 * `height:100%` stretched every callout card to the full diagram height
 * instead of its own content (measured: all three callout cards shared the
 * exact same rendered height, equal to the diagram's own height). Mirrors the
 * EXISTING `.stibok-diagram__item{block-size:auto}` fix (INF.5) one level
 * deeper — same root cause, same fix, a new element it now reaches. */
.stibok-diagram.is-mode-hotspots .stibok-diagram__item > * {
	block-size: auto;
	/* Browser-verified, same pass: with ONLY `inset-inline-start` set (never
	 * `inline-size`), an absolutely-positioned box uses CSS's shrink-to-fit
	 * sizing, bounded by the remaining space to the containing block's FAR
	 * edge — a callout anchored near the image's right/bottom edge (e.g.
	 * anchorX:"78%") had almost no room to shrink-to-fit into and rendered as a
	 * near-vertical sliver (measured: 101px wide × 404px tall, vs. ~247×194 for
	 * a center-anchored callout with identical content, EVEN with a
	 * `max-inline-size` cap — a max only bounds the upper size, it cannot stop
	 * shrink-to-fit from squeezing the box smaller near an edge). A FIXED
	 * `inline-size` (not `max-`) removes shrink-to-fit from the equation
	 * entirely — every callout gets the SAME, predictable card width
	 * regardless of anchor position; one near an edge may visually overflow
	 * past the image's own edge (a legible, common annotated-photo pattern —
	 * the card floats just outside the frame, still tied to its point by the
	 * leader line — not a bug, and authors can always pull it back in with a
	 * smaller offset). Mirrors the SAME "one bounded card width" convention
	 * `.stibok-diagram__item` gives ring modes (16rem) / `snake` (12rem).
	 *
	 * Phase INF-Polish P.2 item 5 (review E3): `min(14rem, calc(100% - 2rem))` —
	 * a HARD CLAMP to the diagram box. The auto-flip fix (Traits::apply_position())
	 * stops most callouts from growing PAST the far edge, but a callout whose
	 * anchor sits just past the flip threshold (e.g. 55%) still grows a full 14rem
	 * from a point not far enough from that edge to fit it — this clamp shrinks the
	 * card instead of letting it overflow the OTHER side, on any diagram box
	 * narrower than ~16rem (14rem + the 2rem margin). A no-op (still exactly 14rem)
	 * on any wider box. */
	inline-size: min(14rem, calc(100% - 2rem));
}
/* The scaffold `<svg>` is a FULL-BLEED overlay here — not the fixed-diameter,
 * center-locked box every ring mode uses (Fable F1 does not apply: there is no
 * radius) — it covers the base image's own rendered box exactly, so its
 * `viewBox="0 0 100 100"`/`preserveAspectRatio="none"` percent space lines up
 * 1:1 with the callouts' own CSS percent positions (render.php, T11). A fresh
 * override (no prior writer of this element's position for hotspots), so no
 * ladder entry is needed — 3 classes beats the base rule's 1 outright. */
.stibok-diagram.is-mode-hotspots .stibok-diagram__scaffold {
	inset-inline-start: 0;
	inset-block-start: 0;
	inline-size: 100%;
	block-size: 100%;
	translate: none;
	overflow: visible;
}
/* The marker dot is real HTML (not inside the scaffold `<svg>` above) — a
 * `<circle>` drawn in that non-uniformly-scaled viewBox would render as an
 * ELLIPSE on a non-square image (render.php's own docblock); a plain CSS
 * circle has no such problem. Reuses the F5-reserved `--stibok-diagram-
 * marker-size` var (declared since INF.2, unconsumed until now) for its
 * diameter. The `box-shadow` ring is the "contrast halo" F16 called for — a
 * `currentColor` dot alone is invisible over a photo whose own color happens
 * to match; a token-colored ring around it stays legible over any background. */
.stibok-diagram__marker {
	position: absolute;
	translate: -50% -50%;
	inline-size: var(--stibok-diagram-marker-size);
	block-size: var(--stibok-diagram-marker-size);
	border-radius: 50%;
	background: currentColor;
	box-shadow: 0 0 0 0.125rem var(--wp--preset--color--base, #fff);
	pointer-events: none;
	/* Phase INF-Polish P.2 item 6 (review E5): the step number (render.php now
	 * prints it as the marker's own text content, unconditionally — hotspots is
	 * ALWAYS numbered) centered inside the dot — "marker i ↔ card i" is now true
	 * ON THE IMAGE ITSELF, not only via the numbered list below it. `color` is the
	 * SAME base/white the box-shadow halo already uses (the marker's own fill is
	 * `currentColor`, typically a dark ink — white digit reads reliably against it
	 * without a second token decision). */
	display: grid;
	place-items: center;
	color: var(--wp--preset--color--base, #fff);
	font-size: 0.75rem;
	font-weight: 700;
	line-height: 1;
}
/* Phase INF-Polish P.2 item 6 (review E5): a two-pass stroke for the hotspots
 * leader line — a hairline `currentColor` line alone is invisible over imagery
 * whose own tone happens to match (measured: a night photo). `.is-halo` is a REAL
 * earlier `<line>` sibling (render.php) painted with a wide, light stroke UNDER
 * the normal ink line — SVG paints in document order, so the halo never covers
 * the line drawn after it. No `stroke` attribute on the halo element itself (an
 * SVG default of `none`) — this rule owns both its color and width entirely. */
.stibok-diagram__connector.is-halo {
	stroke: var(--wp--preset--color--base, #fff);
	stroke-width: calc(var(--stibok-diagram-stroke-w) * 4);
	stroke-linecap: round;
}
/* Numbered step-badge (Fable F10, `is-numbered` — forced on unconditionally for
 * `hotspots`, render.php): every OTHER mode anchors this `::before` to the `<li>`
 * itself (stibok.css base rule), because the `<li>` IS the ring-positioned item
 * there. For hotspots the `<li>` is deliberately `position:static` (see the
 * ladder rule above — it must NOT be a positioned ancestor, or it breaks the
 * callout's own percent anchor), so it is never a valid `::before` anchor here —
 * carries NO width/height of its own either way (its child is `position:absolute`
 * in the wide state, out of the `<li>`'s flow). Anchoring on the CALLOUT'S OWN
 * element instead works in BOTH states: it is `position:absolute` (a real,
 * correctly-placed box) in the wide/overlay state, and `position:relative`
 * (`container_css()`'s `!important` reset, mirrors this SAME "give the badge a
 * positioned anchor" need) once collapsed to a normal-flow grid cell — so,
 * unlike every other writer on the ladder above, this needs NO wide/narrow
 * suppress-then-restore pair at all.
 *
 * TWO selectors, not one — the FRONT and the CANVAS disagree on whether the
 * `<li>` and the callout are the SAME element or two:
 *   - FRONT: render.php always wraps the callout in its OWN `<li>` — the callout
 *     is a real CHILD of `.stibok-diagram__item` (`.item > *`).
 *   - CANVAS: `withDiagramItemCanvas` stamps `.stibok-diagram__item` directly
 *     onto the callout's OWN wrapper (there is no separate `<li>` — same
 *     established fact the base rule's own docblock already documents) — so
 *     `.item` and `.item.stibok-chrome` (canvas only, CC.1) are the SAME single
 *     element, needing a DIRECT `::before`, not a child combinator.
 * The canvas variant ALSO needs the SAME chrome-collision resets the base rule
 * declares (a `.stibok-chrome::before` also targets this pseudo-element, CC.1) —
 * repeated here for the SAME reason, both are no-ops on the front.
 *
 * Browser-verified bug fix (found building THIS increment's own demo): the
 * ORIGINAL base rule's `::before` (on the `<li>` itself, unscoped — every mode
 * shares it) is NOT automatically inert just because this file also adds a
 * retargeted copy below — both are separate pseudo-elements (one belongs to the
 * `<li>`, one to its CHILD) and the base one still fires. With the `<li>` now
 * `position:static` (the ladder fix above), that base badge's OWN
 * `position:absolute` skipped past the static `<li>` to `.stibok-diagram__items`
 * (still positioned) — landing every callout's badge at THAT shared origin, the
 * image's own top-left corner, all three stacked on one spot (only the last
 * painted, "3", was visible — found visually, not by the test suite). Must be
 * suppressed explicitly. */
.stibok-diagram.is-mode-hotspots .stibok-diagram__items.is-numbered > .stibok-diagram__item::before {
	display: none;
}
/* P.4 deliverable 3: the marker now lives INSIDE its callout `<li>` (render.php), so
 * `> *` would also match it — `:not(.stibok-diagram__marker)` keeps the step badge on
 * the callout child only (the marker carries its own visible number already). */
.stibok-diagram.is-mode-hotspots .stibok-diagram__items.is-numbered > .stibok-diagram__item:not(.stibok-chrome) > *:not(.stibok-diagram__marker)::before,
.stibok-diagram.is-mode-hotspots .stibok-diagram__items.is-numbered > .stibok-diagram__item.stibok-chrome::before {
	content: counter(stibok-diagram-step);
	position: absolute;
	inset-inline-start: -0.5rem;
	inset-block-start: -0.5rem;
	inline-size: 1.75rem;
	block-size: 1.75rem;
	display: grid;
	place-items: center;
	border-radius: 50%;
	background: var(--wp--preset--color--accent);
	color: var(--wp--preset--color--on-accent);
	font-size: 0.8125rem;
	font-weight: 600;
	line-height: 1;
	pointer-events: none;
	z-index: 1;
}
.stibok-diagram.is-mode-hotspots .stibok-diagram__items.is-numbered > .stibok-diagram__item.stibok-chrome::before {
	/* Canvas-only chrome-collision resets — mirrors the base rule's OWN LOW-5 fix
	 * (stibok.css, above) verbatim; see its comment for the full rationale. */
	opacity: 1;
	transform: none;
	padding-block: 0;
	padding-inline: 0;
	transition: none;
}
/* Canvas-only (D18, `.is-editing` — CC.1's own convention, a no-op on the front:
 * `.is-editing` never reaches production markup): native block editing keeps
 * EVERY child, including the base image, inside ONE `useInnerBlocksProps` region
 * (`editor.js`'s `diagramEdit` hotspots branch) — unlike render.php, which renders
 * the image as a separate, real-flow SIBLING outside the `<ol>` (T2/T11). Without
 * this override, `.stibok-diagram__items`' universal `position:absolute;inset:0`
 * (base rule above) would swallow the image into an out-of-flow box with no
 * intrinsic size, collapsing the WHOLE canvas diagram to zero height. Resetting
 * `.items` to normal flow here lets the image (now its first, real-flow child)
 * establish the box's height exactly like the front — and does NOT change where
 * a callout's OWN `position:absolute` (its own `positionStyle()`/`Traits::
 * apply_position()` mirror, entirely generic, not diagram-specific) resolves its
 * percentages against: `position:absolute` always skips a `position:static`
 * ancestor to find the nearest POSITIONED one, which is `.stibok-diagram` itself
 * either way — and on the front `.items` is deliberately sized to be byte-
 * identical to `.stibok-diagram`'s own box (`inset:0` + 100%/100%), so the
 * percentage math is IDENTICAL in both cases, not merely close. */
.stibok-diagram.is-mode-hotspots.is-editing .stibok-diagram__items {
	position: static;
	inset-inline-start: auto;
	inset-block-start: auto;
	inline-size: auto;
	block-size: auto;
}

/* ── Diagram — hover / focus emphasis (Phase INF-Polish P.4, deliverable 2) ───
 * Hovering — or keyboard-focusing, `:focus-visible` — a node LIFTS it (a subtle
 * `scale` + drop-shadow) and DIMS its siblings, so the active node reads forward.
 * `scale` is an INDEPENDENT individual-transform property: it composes with the
 * `translate` (ring position) + `rotate` (tangential label) the item already writes,
 * per the fixed CSS Transforms Level 2 translate→rotate→scale order — so this is NOT a
 * new writer of `position`/`translate`/`inset` and the specificity-ladder above is
 * untouched. Sibling dim uses `:has()` on the list (only the OTHER items fade). Real
 * DOM interaction (the node is `tabindex="0"`, render.php) — safe, unlike the old decor
 * `:hover` that could never fire under `pointer-events:none`. Scoped to
 * `:not(.is-mode-hotspots)` — hotspots owns its own callout-reveal semantics (below).
 * ⚠ NO `transition` is added on the item here — deliberately. `.stibok-diagram__item`
 * already has THREE `transition` writers (the F15 collapse morph on the base rule, the
 * INF.6 `.stibok-motion[data-stibok-reveal]` reveal-delay cadence, and the P.1
 * `.is-draw.is-mode-cycle` opacity draw), several at the SAME specificity this rule
 * would land at — restating `transition` here won/tied and DISCARDED their cadence
 * (measured: `diagram-motion.spec.mjs`'s F13 + sequential-draw specs regressed). So the
 * hover lift applies INSTANTLY (a snappy pop, not an eased glide) — the emphasis is the
 * `scale`+shadow+dim, not the easing, and staying off `transition` keeps every existing
 * motion cadence intact. The hover/dim rules below write only `scale`/`filter`/`opacity`/
 * `outline` (never `translate`/`transition`), so they never collide with that composition. */
.stibok-diagram:not(.is-mode-hotspots) .stibok-diagram__item:hover,
.stibok-diagram:not(.is-mode-hotspots) .stibok-diagram__item:focus-visible {
	scale: 1.06;
	z-index: 2;
	filter: drop-shadow(0 0.25rem 0.5rem rgba(16, 18, 26, 0.18));
}
/* Keyboard focus ring (focus-visible only — no ring on a mouse click). */
.stibok-diagram:not(.is-mode-hotspots) .stibok-diagram__item:focus-visible {
	outline: 0.125rem solid var(--wp--preset--color--accent, currentColor);
	outline-offset: 0.25rem;
}
/* Edge-anchor increment: the sibling DIM alone is switchable —
 * `scaffold.hoverDim:false` stamps `.is-hover-dim-off` on the root (omit-default
 * true, so today's behaviour is unchanged unless an author opts out). Only these two
 * opacity rules are gated: the lift/scale/shadow and the keyboard focus ring above
 * stay unconditional, since they ARE the emphasis (and the focus ring is an a11y
 * requirement, never author-disablable). */
.stibok-diagram:not(.is-mode-hotspots):not(.is-hover-dim-off) .stibok-diagram__items:has(.stibok-diagram__item:hover) .stibok-diagram__item:not(:hover),
.stibok-diagram:not(.is-mode-hotspots):not(.is-hover-dim-off) .stibok-diagram__items:has(.stibok-diagram__item:focus-visible) .stibok-diagram__item:not(:focus-visible) {
	opacity: 0.55;
}
@media (prefers-reduced-motion: reduce) {
	/* Respect reduced motion: no lift-scale (the shadow/dim still convey emphasis). */
	.stibok-diagram:not(.is-mode-hotspots) .stibok-diagram__item:hover,
	.stibok-diagram:not(.is-mode-hotspots) .stibok-diagram__item:focus-visible {
		scale: none;
	}
}

/* ── Diagram — hotspots callout reveal mode (Phase INF-Polish P.4, deliverable 3) ──
 * `scaffold.callouts:hover` hides each callout CARD at rest and reveals it on
 * `:hover`/`:focus-visible` of its OWN marker (co-located in the same `<li>`,
 * render.php). The `<li>` is `position:static` and zero-size, but `:hover` matches when
 * the pointer is over any DESCENDANT (the marker, or the revealed card), and
 * `:focus-within` fires when the focusable marker (`role="button" tabindex="0"`) takes
 * keyboard focus — so one pure-CSS pair covers mouse AND keyboard with no JS and no
 * cross-DOM index correlation. `opacity` + `pointer-events` (NOT `visibility`/`display`)
 * keeps the card in the accessibility tree at all times — the callout `<ol>` still
 * carries every bit of real information (D-INF9) whether or not it is visually revealed.
 * Touch has no hover, so a card stays hidden until tapped (tap = focus → reveal), and
 * the collapse-to-numbered-list already covers small screens. */
.stibok-diagram.is-mode-hotspots.is-callouts-hover .stibok-diagram__marker {
	cursor: pointer;
	pointer-events: auto;
}
.stibok-diagram.is-mode-hotspots.is-callouts-hover .stibok-diagram__item > :not(.stibok-diagram__marker) {
	opacity: 0;
	pointer-events: none;
	transition: opacity 0.2s ease;
}
.stibok-diagram.is-mode-hotspots.is-callouts-hover .stibok-diagram__item:hover > :not(.stibok-diagram__marker),
.stibok-diagram.is-mode-hotspots.is-callouts-hover .stibok-diagram__item:focus-within > :not(.stibok-diagram__marker) {
	opacity: 1;
	pointer-events: auto;
}
@media (prefers-reduced-motion: reduce) {
	.stibok-diagram.is-mode-hotspots.is-callouts-hover .stibok-diagram__item > :not(.stibok-diagram__marker) {
		transition: none;
	}
}

/* ── Diagram — matrix mode (Phase INF, INF.10b, D1) ──────────────────────────
 * A 2×2 (or up to 3×3) quadrant grid — the classic SWOT/Eisenhower shape. Fixed
 * geometry only (a quadrant's cell comes from its INDEX, row-major, never a data
 * value); `venn` was DEMOTED to defer (Fable F17, docs/deferred-features.md) —
 * this build ships `matrix` only. The FIRST diagram mode expressible as a PURE
 * CSS GRID with NO scaffold `<svg>`/geometry math at all (render.php/editor.js's
 * `matrix` branch computes only `Render_Helpers::diagram_matrix_cols()`, a
 * one-line int→int step function).
 *
 * `.stibok-diagram.is-mode-matrix` is ITSELF a small CSS grid — a 3×3 frame of
 * `grid-template-areas` (an `auto 1fr auto` track on each axis) with the four
 * axis-label captions in the outer ring and `.stibok-diagram__items` (the REAL
 * quadrant grid, its own NESTED `display:grid`) filling the center cell. An
 * absent axis label simply never renders that `<div>` (render.php), so its
 * `auto`-sized track collapses to zero — no placeholder markup needed either
 * side of that decision.
 */
.stibok-diagram.is-mode-matrix {
	display: grid;
	grid-template-columns: auto 1fr auto;
	grid-template-rows: auto 1fr auto;
	grid-template-areas:
		".    mx-top    ."
		"mx-left mx-items mx-right"
		".    mx-bottom .";
	gap: var(--wp--preset--spacing--sm);
}
.stibok-diagram__axis-label {
	font-size: 0.8125rem;
	font-weight: 600;
	color: var(--wp--preset--color--contrast-2);
	text-align: center;
}
.stibok-diagram__axis-label.is-axis-top { grid-area: mx-top; }
.stibok-diagram__axis-label.is-axis-bottom { grid-area: mx-bottom; }
.stibok-diagram__axis-label.is-axis-left { grid-area: mx-left; }
.stibok-diagram__axis-label.is-axis-right { grid-area: mx-right; }
/* `.stibok-diagram__items`'s base rule (above) is a `position:absolute;inset:0`
 * FULL-BOX ring overlay (INF.5) — matrix needs none of that; it is an ordinary,
 * intrinsically-sized grid CELL of the outer frame above. `grid-template-columns`/
 * `-rows` are set INLINE per instance (render.php's own `$mx_cols`, the author's
 * item count) — a SQUARE `$cols×$cols` track set lets CSS grid auto-placement do
 * BOTH halves of "pad empty cells" (D1's own edge case) for free: fewer than
 * `$cols²` children leave trailing cells blank (no filler markup); MORE grow
 * extra IMPLICIT rows past the ideal 3×3 shape (never dropping a child, unlike
 * `venn`'s fixed circle-count cap). */
.stibok-diagram.is-mode-matrix .stibok-diagram__items {
	grid-area: mx-items;
	position: static;
	inset-inline-start: auto;
	inset-block-start: auto;
	inline-size: auto;
	block-size: auto;
	display: grid;
	gap: var(--wp--preset--spacing--md);
}
/* Specificity-ladder 6th WRITER (stibok.css's `.stibok-diagram__item` ladder
 * comment, above, records this) — matrix does NOT reuse the ring `--dx`/`--dy`
 * translate at all (unlike snake/funnel/pyramid's "consumer, not writer" note):
 * a genuine CSS-grid cell must never carry `position:absolute` (the base rule)
 * or any composed motion `translate` (a percentage `translate` shifts a grid
 * item by its OWN size, displacing it from its cell — wrong for a grid layout).
 * `position:relative` (not hotspots' `static`) — a matrix item has no
 * independently-`position:absolute`'d descendant to skip past (unlike a hotspots
 * callout), and `relative` keeps it a valid positioned ancestor for the
 * `scaffold.numbers` `::before` step-badge (mirrors `container_css()`'s own
 * collapse-reset choice, same reasoning). Repeats the SAME 6 chrome/motion
 * compounds the ladder comment enumerates (2 EXTRA ancestor classes over each
 * counterpart — `.stibok-diagram.is-mode-matrix` — so every combination wins by
 * SPECIFICITY, exactly like hotspots' own layer 5, needing no wide/narrow
 * distinction either). */
.stibok-diagram.is-mode-matrix .stibok-diagram__item,
.stibok-diagram.is-mode-matrix .stibok-diagram__item.stibok-chrome,
.stibok-diagram.is-mode-matrix .stibok-diagram__item.stibok-motion[data-stibok-reveal],
.stibok-diagram.is-mode-matrix .stibok-diagram__item.stibok-motion[data-stibok-reveal].is-revealed,
.stibok-diagram.is-mode-matrix .stibok-diagram__item.stibok-chrome.stibok-motion[data-stibok-reveal],
.stibok-diagram.is-mode-matrix .stibok-diagram__item.stibok-chrome.stibok-motion[data-stibok-reveal].is-revealed {
	position: relative;
	inset-inline-start: auto;
	inset-block-start: auto;
	translate: none;
}
/* Cosmetic cell styling — no ladder conflict (no motion/chrome compound fights
 * over `max-inline-size`/`display`/`padding`), so a single 2-class selector
 * outranks the base rule's 1-class `max-inline-size:16rem` outright. The child
 * fills its own cell naturally (a grid item with no explicit sizing takes its
 * track's size) — authoring guidance (docs/blocks/diagram.md T14): an UNSURFACED
 * child (a `stack` wrapping heading/text) reads against this cell's own
 * `background-color` (the F8 cascade, set inline per item); a `card`'s own
 * background would cover it, same gotcha as funnel/pyramid's band fill.
 *
 * Phase INF-Polish P.2 item 1 (review E2): the item's `color` reads its OWN
 * on-colour so an unsurfaced quadrant's label follows its own cell's fill
 * instead of the theme's fixed ink color.
 *
 * P.2 fix-pass review HIGH (F2): `color` is now set INLINE by render.php,
 * unconditionally (`--stibok-on-surface:<token>;color:var(--stibok-on-surface)`
 * on this SAME `<li>`) — the SEPARATE `:not(:has(> .stibok-card))`-guarded CSS
 * rule this comment used to describe is REMOVED (a card's own rule below now
 * protects it at ANY depth, not just as a direct child — see docs/blocks/
 * diagram.md T14 for the full "background+foreground pair" convention). */
.stibok-diagram.is-mode-matrix .stibok-diagram__item {
	max-inline-size: none;
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	text-align: center;
	padding: var(--wp--preset--spacing--lg);
	min-block-size: 8rem;
	/* Phase INF-Polish P.3: `matrix` is the ONE mode whose "segments" are real DOM
	 * boxes (each quadrant paints its own `background-color`), so the frozen
	 * `--stibok-diagram-seg-radius` var — with no consumer on the SVG-path modes
	 * (rounding an arc/chevron path is a geometry change) — has a genuine,
	 * geometry-free consumer here: the quadrant corner radius.
	 *
	 * MED-2 fix: the baseline `--stibok-diagram-seg-radius` is `0` (set on
	 * `.stibok-diagram` above), so an unstyled matrix stays SQUARE — rounding is opt-in
	 * via a variant (`soft` = 0.75rem) or a per-block `segRadius`. Before this fix the
	 * baseline was `0.25rem`, which INHERITED here and silently rounded every
	 * pre-existing matrix. The `,0` var() fallback is belt-and-suspenders (the var is
	 * always defined on `.stibok-diagram`); it and the baseline now agree at `0`. */
	border-radius: var(--stibok-diagram-seg-radius, 0);
}

/* P.5 — "a surface and its on-colour are a pair", PROJECT-WIDE (docs/plans/
 * post-cl2-merge-integration-batch.md Item 1). Originated as a diagram-scoped fix
 * (P.2 fix-pass review HIGH F2 / LOW F5, docs/blocks/diagram.md T14): the
 * on-colour is staged as the INHERITED custom property `--stibok-on-surface` and
 * consumed via `color:var(--stibok-on-surface)`; any REAL surface painted further
 * down MUST re-declare the var for its own descendants, so the INNERMOST surface
 * always wins regardless of nesting depth — no direct-child check, no `:has()`,
 * no depth guard. That is exactly why the mechanism generalizes cleanly: it was
 * never diagram-specific, only diagram-SCOPED. This rule drops the
 * `.stibok-diagram__item` ancestor prefix the P.2 version needed, so the SAME
 * three surface signals now protect a `stack > card` (or any nesting) ANYWHERE
 * on the site, not just inside a diagram — closing the bug at its root instead
 * of only where it was first measured (a `stack > card` nested ANYWHERE, not just
 * inside a diagram item, could inherit a mismatched ancestor `color` the exact
 * same way; measured live pre-fix: `rgb(255,255,255)` text over a
 * `rgb(255,255,255)` card background, 1.0:1).
 *
 * `.stibok-card` is DELIBERATELY NOT listed here anymore: every shipped card
 * variant sets its own `bg` (recipes/defaults/card.json), and
 * `Recipe_Compiler::declarations()` now calls the SAME resolver
 * (`Render_Helpers::on_surface_pairing_decls()`) directly inside the card's OWN
 * `.stibok-card.is-variant-<slug>` rule — a MORE ACCURATE value than the ambient
 * default below (it pairs with the variant's OWN authored bg, not just "whatever
 * the theme's ambient on-surface is"), and a rule that's already more specific
 * than this catch-all, so it always wins. `Appearance_Compiler::declarations()`
 * does the identical thing for a per-instance Style-Studio `base.bg` — which is
 * why `.stibok-has-bg` below is now mostly a DEFENSE-IN-DEPTH default (every real
 * `stibok-has-bg` occurrence already carries an accurate `--stibok-on-surface` in
 * its OWN `.stibok-x-<hash>` rule, which wins by load order — appearance CSS is
 * always injected after this sheet, front and canvas alike), not the primary path.
 *
 * `.is-tone-dark`/`.is-tone-light` stay pinned to the ambient
 * `--wp--custom--semantic--on-surface` here — that IS the accurate value for a
 * tone: `tone_remap_css()` already re-points the semantic var itself to the
 * correct bucket-specific colour (dark → base/white, light → contrast/near-black),
 * so reading it through `--stibok-on-surface` is correct at any nesting depth,
 * not just an approximation (see the tone-remap block near the top of this file,
 * and the "subsumes CL2's D-10/D-14/D-15" note there for how those relate). */
.stibok-has-bg,
.is-tone-dark,
.is-tone-light {
	--stibok-on-surface: var(--wp--custom--semantic--on-surface);
	color: var(--stibok-on-surface);
}

/* The heading-override companion (theme.json global-styles sets `h1..h6{color:
 * var(--wp--preset--color--contrast)}` DIRECTLY on the tag — a real declaration
 * that beats mere inheritance even at (0,0,1) specificity — see the diagram-scoped
 * version of this same fix above for the full explanation). Generalizes that fix
 * project-wide: a heading nested (at ANY depth) inside a card, a toned block, or
 * an appearance-painted surface now re-reads the inherited on-colour instead of
 * staying pinned to the theme's flat heading ink. Every arm writes the IDENTICAL
 * `color:var(--stibok-on-surface, inherit)` declaration, so it does not matter
 * which arm's selector happens to match first when surfaces nest (`--stibok-on-
 * surface` itself still resolves via normal inheritance to the NEAREST real
 * surface, independent of which selector arm fired) — see the diagram-mode
 * version of this rule (above) for the sibling case where the closest surface is
 * the diagram item itself (a segment fill, not one of these three classes).
 *
 * BROWSER-VERIFIED FIX: each ancestor class is DOUBLED (`.stibok-card.stibok-card`,
 * etc.) for specificity (0,2,1), not the single-class (0,1,1) a first pass shipped.
 * `heading`'s OWN recipe (recipes/defaults/heading.json) sets `text:{on-surface}`
 * on EVERY non-eyebrow variant — `.stibok-heading.is-variant-title{color:…}` is a
 * REAL (0,2,0) declaration that a (0,1,1) override loses to on the class-count
 * column alone, regardless of the extra tag selector. Found live in the editor:
 * a `card` with `appearance.base.bg:"{accent-soft}"` (no explicit text) correctly
 * resolved `--stibok-on-surface` to the paired `accent` at the CARD (getComputedStyle
 * confirmed the var was `#50B848` at both the card AND the heading), yet the
 * heading's rendered `color` stayed the theme's near-black ink — the (0,1,1) rule
 * lost outright to the recipe's (0,2,0) one. The SAME idiom this codebase already
 * uses for this exact problem (Appearance_Compiler's tripled `.stibok-x-<hash>`
 * class, R3) — doubling here is the minimal bump that reliably wins (0,2,1) against
 * (0,2,0) via the tag column, without reaching for `!important`. Losing to a
 * heading's OWN explicit per-instance `appearance.base.text` override (a tripled
 * hash class, (0,3,0)+ applied directly, no descendant) is correct and unaffected —
 * that stays a deliberate author override, same R3 precedence as everywhere else. */
.stibok-card.stibok-card :is(h1, h2, h3, h4, h5, h6),
.stibok-has-bg.stibok-has-bg :is(h1, h2, h3, h4, h5, h6),
.is-tone-dark.is-tone-dark :is(h1, h2, h3, h4, h5, h6),
.is-tone-light.is-tone-light :is(h1, h2, h3, h4, h5, h6) {
	color: var(--stibok-on-surface, inherit);
}

/* ── Diagram — skin CHARACTERS as recipe variants (Phase INF-Polish P.3, D-P1) ─
 * FOUR ready-made looks, each ONLY a set of the frozen `--stibok-diagram-*` var
 * values (Fable F5) — NO new geometry, NO new render branch. This is the ONE CSS
 * block the plan's "one CSS block, two entry points, no duplication" refers to:
 *   - PER-BLOCK override — the diagram's `variant` attribute stamps
 *     `is-variant-<slug>` (render.php / editor canvas), so it wins right here.
 *   - SITE skin — a `Style_Pack.recipeDefaults.diagram = "<slug>"` swaps the
 *     recipe's DEFAULT variant; render.php resolves that swapped default into the
 *     SAME `is-variant-<slug>` class for any diagram without its own variant, so a
 *     site skin restyles every diagram automatically while a per-block variant
 *     still overrides. Both entry points resolve to THIS block — no duplication.
 * The variant slugs are registered in recipes/defaults/diagram.json (so they list
 * in the editor's Variant control + the manifest); their VALUES live here because
 * the recipe compiler emits only the token-property whitelist, not custom props.
 * Every colour is a variation-safe palette slug (ThemeColorSlugVariationSafetyTest).
 * Default (no variant) stays "follow the site" — none of these are the default. */
.stibok-diagram.is-variant-blueprint {
	/* Hairline, dashed, monochrome technical line over a faint measured grid. */
	--stibok-diagram-stroke-w: 0.0625rem;
	--stibok-diagram-dash: 0.25rem 0.2rem;
	--stibok-diagram-cap: butt;
	--stibok-diagram-stroke-color: var(--wp--preset--color--accent);
	--stibok-diagram-seg-radius: 0;
	--stibok-diagram-backdrop:
		repeating-linear-gradient(to right, var(--wp--preset--color--border) 0 0.0625rem, transparent 0.0625rem 2rem),
		repeating-linear-gradient(to bottom, var(--wp--preset--color--border) 0 0.0625rem, transparent 0.0625rem 2rem);
}
.stibok-diagram.is-variant-editorial {
	/* Bold, solid, high-contrast ink — flat butt caps read as a print rule. */
	--stibok-diagram-stroke-w: 0.375rem;
	--stibok-diagram-dash: none;
	--stibok-diagram-cap: butt;
	--stibok-diagram-stroke-color: var(--wp--preset--color--contrast);
}
.stibok-diagram.is-variant-soft {
	/* Rounded, airy dotted strokes in a pastel accent + generously rounded cells.
	 * (Per-segment "pastel FILLS" would need the F8 fill cascade — a data change
	 * outside this var-only increment — so the soft feel comes from the pastel
	 * stroke + round caps + rounded matrix cells, not a recoloured fill palette.) */
	--stibok-diagram-stroke-w: 0.1875rem;
	--stibok-diagram-dash: 0.125rem 0.9rem;
	--stibok-diagram-cap: round;
	--stibok-diagram-stroke-color: var(--wp--preset--color--accent-soft);
	--stibok-diagram-seg-radius: 0.75rem;
}
.stibok-diagram.is-variant-neon {
	/* Bright accent strokes blooming over a dark panel. The dark backdrop is the
	 * `contrast` token (always the opposite pole of the surface, so it stays a dark
	 * spotlight in a light theme / a light one in a dark theme — always distinct,
	 * always token-pure), the glow is a two-pass accent drop-shadow on the scaffold. */
	--stibok-diagram-stroke-w: 0.125rem;
	--stibok-diagram-cap: round;
	--stibok-diagram-stroke-color: var(--wp--preset--color--accent);
	--stibok-diagram-backdrop: linear-gradient(var(--wp--preset--color--contrast), var(--wp--preset--color--contrast));
	--stibok-diagram-glow: drop-shadow(0 0 0.375rem var(--wp--preset--color--accent)) drop-shadow(0 0 0.75rem var(--wp--preset--color--accent));
	/* MED-2/MED-1 fix (P.3 review): neon paints a full-bleed DARK backdrop (`contrast`)
	 * but previously restyled only the scaffold stroke — never the item/center CONTENT
	 * colour — so any text sitting DIRECTLY on the backdrop (the center label; a
	 * radial/hub/snake item's plain text) kept the theme's near-black ink and rendered
	 * ~1:1 invisible (measured: `<p>Glow` `rgb(16,18,26)` on `rgb(16,18,26)`).
	 *
	 * Fix, using the EXISTING F2 on-surface convention (no parallel mechanism): stage a
	 * LIGHT on-colour and consume it as `color` here at the diagram root, so it INHERITS
	 * to the center label + any plain item content. `base` is the token opposite pole of
	 * the `contrast` backdrop — variation-safe (exists in every shipped variation,
	 * ThemeColorSlugVariationSafetyTest) and it FLIPS correctly with the theme (in a dark
	 * theme `contrast` is light and `base` is dark), which a hardcoded colour could not.
	 *
	 * Pairing doctrine preserved — this is a ROOT default that inheritance carries, and
	 * every real inner surface OVERRIDES it via its OWN higher-specificity `color`:
	 *   - segment modes (cycle/funnel/pyramid/matrix): the per-item mapping above
	 *     (`.stibok-diagram.is-mode-* .stibok-diagram__item`) re-declares
	 *     `--stibok-on-surface` to the segment's paired on-colour → item text pairs with
	 *     its FILL, not the neon backdrop.
	 *   - an inner card / toned / `stibok-has-bg` surface: the F5 rule re-declares
	 *     `--stibok-on-surface` to the ambient on-colour → its text stays dark-on-light,
	 *     NOT force-lit. */
	--stibok-on-surface: var(--wp--preset--color--base);
	color: var(--stibok-on-surface);
}
/* MED-1: headings are pinned by theme.json global-styles (`h1..h6{color:contrast}`,
 * a real declaration that beats mere inheritance — the same reason the segment-mode
 * heading override below exists), so a heading sitting on the neon backdrop would stay
 * near-black even with the root `color` above. Re-point it to the inherited
 * `--stibok-on-surface` for ANY mode under neon — the SAME declaration/value as the
 * segment-mode heading rule, so this is one convention, not a parallel one. In a
 * segment mode the item re-declared `--stibok-on-surface` to its segment colour, so a
 * heading there still pairs with its fill; only content on the bare backdrop reads the
 * neon `base`. */
.stibok-diagram.is-variant-neon .stibok-diagram__item :is(h1, h2, h3, h4, h5, h6) {
	color: var(--stibok-on-surface, inherit);
}
/* MED-1: the center LABEL is coloured by the diagram RECIPE, not inheritance — the
 * recipe emits `.stibok-diagram .stibok-diagram__center{color:{on-surface}}` (the
 * ambient semantic on-surface, dark on a light page — correct normally) into
 * `stibok-recipes-inline-css`, a REAL (0,2,0) declaration on the `<p>` that beats the
 * neon root `color` above. Over the neon dark backdrop that leaves the label near-black
 * (the measured `<p>Glow` 1:1 defect). Re-point it to the neon `--stibok-on-surface`
 * (`base`) at (0,3,0) so it wins the recipe rule; same F2 token, so it still flips
 * correctly in a dark theme. */
.stibok-diagram.is-variant-neon .stibok-diagram__center {
	color: var(--stibok-on-surface);
}

/* ===========================================================================
 * Stat (Phase INF, INF-Stat) — a KPI/stat-callout leaf: a big formatted figure
 * (prefix+value+suffix) + a caption label + an optional delta/trend, with an
 * optional count-up-on-scroll (interactions.js overrides the number 0→N on
 * reveal then writes the exact SSR string back on the last frame — the
 * settled DOM is always byte-identical to this CSS's target markup, R6).
 *
 * Frozen skin-addressable vars (mirrors the diagram scaffold's Fable F5
 * pattern — freeze the names here so a skin can restyle the figure without
 * new geometry/markup): --stibok-stat-figure-size, --stibok-stat-figure-weight.
 *
 * NOT a stack "fill container" (unlike diagram/timeline/tabs): a stat is a
 * compact, self-sizing callout — like `rating` — so it is deliberately absent
 * from the align-* stretch allowlist above; it hugs + centers like text.
 *
 * The up/down/neutral delta tint is a plain static rule (like `.stibok-rating`'s
 * warning-color star fill) rather than a recipe-part base — every rendered
 * `__delta` always carries exactly one `is-dir-*` class (render.php infers it
 * when absent, Fable F4), so there is no "undirected" state to cover, and
 * keeping it out of the recipe avoids a same-specificity fight against a
 * recipe part rule that loads later in the cascade.
 * =========================================================================== */
.stibok-stat {
	--stibok-stat-figure-size: var(--wp--preset--font-size--xxxl, 2.75rem);
	--stibok-stat-figure-weight: 700;
	display: flex;
	flex-direction: column;
	gap: var(--wp--preset--spacing--3xs, 0.25rem);
}
.stibok-stat.is-align-start { align-items: flex-start; text-align: start; }
.stibok-stat.is-align-center { align-items: center; text-align: center; }
.stibok-stat.is-align-end { align-items: flex-end; text-align: end; }

.stibok-stat__figure {
	display: flex;
	align-items: baseline;
	gap: 0.125rem;
	font-size: var(--stibok-stat-figure-size);
	font-weight: var(--stibok-stat-figure-weight);
	line-height: 1.1;
	font-variant-numeric: tabular-nums;
}
.stibok-stat__prefix,
.stibok-stat__suffix {
	font-size: 0.5em;
	font-weight: 600;
	opacity: 0.75;
}
.stibok-stat__label {
	font-size: 0.9375rem;
}
.stibok-stat__delta {
	display: inline-flex;
	align-items: center;
	font-size: 0.8125rem;
	font-weight: 600;
}
.stibok-stat__delta.is-dir-up { color: var(--wp--preset--color--success); }
.stibok-stat__delta.is-dir-down { color: var(--wp--preset--color--danger); }
.stibok-stat__delta.is-dir-neutral { color: var(--wp--custom--semantic--on-surface-muted); }
